diff --git a/config/application.config.php b/config/application.config.php
index 7bfbfb6a1e591ea2977b9c6f994ce9fb26f7bc78..ae09655cda561301ebbd624a2f7d6a09d672adfb 100644
--- a/config/application.config.php
+++ b/config/application.config.php
@@ -1,5 +1,5 @@
 <?php
-return array(
+$config = array(
     'modules' => array(
         'VuFind',
     ),
@@ -20,3 +20,7 @@ return array(
         ),
     ),
 );
+if (PHP_SAPI == 'cli') {
+    $config['modules'][] = 'VuFind\\CLI';
+}
+return $config;
\ No newline at end of file
diff --git a/import/import-xsl.php b/import/import-xsl.php
index 6a6094e8a0bd024629f10054f0b45ab2a91e3842..077eebde523f2afaf19a389e2a5aa57df57538cc 100644
--- a/import/import-xsl.php
+++ b/import/import-xsl.php
@@ -27,6 +27,7 @@
  * @link     http://vufind.org/wiki/importing_records Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger
-// CliController::harvestnafAction() based on the current filename.
-require_once dirname(__FILE__) . '/../public/index.php';
+// Load the Zend framework -- this will automatically trigger the appropriate
+// controller action based on directory and file names
+define('CLI_DIR', __DIR__);     // save directory name of current script
+require_once __DIR__ . '/../public/index.php';
diff --git a/module/VuFind/CLI/Module.php b/module/VuFind/CLI/Module.php
new file mode 100644
index 0000000000000000000000000000000000000000..d9de28cf96ae73454a7cf9bf63d70fd6e854a990
--- /dev/null
+++ b/module/VuFind/CLI/Module.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace VuFind\CLI;
+use Zend\ModuleManager\ModuleManager,
+    Zend\Mvc\MvcEvent, Zend\Mvc\Router\Http\RouteMatch;
+
+class Module
+{
+    public function getConfig()
+    {
+        return include __DIR__ . '/config/module.config.php';
+    }
+
+    public function getAutoloaderConfig()
+    {
+        // No extra configuration necessary; since this module uses a subset of the
+        // VuFind namespace, its library code is in the main src area of the VuFind
+        // module.
+        return array();
+    }
+
+    public function init(ModuleManager $m)
+    {
+    }
+
+    public function onBootstrap(MvcEvent $e)
+    {
+        $callback = function ($e) {
+            // Get command line arguments and present working directory from
+            // server superglobal:
+            $server = $e->getApplication()->getRequest()->getServer();
+            $args = $server->get('argv');
+            $filename = $args[0];
+            $pwd = $server->get('PWD', CLI_DIR);
+
+            // Convert base filename (minus .php extension) and containing directory
+            // name into action and controller, respectively:
+            $baseFilename = basename($filename);
+            $baseFilename = substr($baseFilename, 0, strlen($baseFilename) - 4);
+            $baseDirname = basename(dirname(realpath($pwd . '/' . $filename)));
+            $routeMatch = new RouteMatch(
+                array('controller' => $baseDirname, 'action' => $baseFilename), 1
+            );
+
+            // Override standard routing:
+            $routeMatch->setMatchedRouteName('default');
+            $e->setRouteMatch($routeMatch);
+        };
+        $events = $e->getApplication()->getEventManager();
+        $events->attach('route', $callback);
+    }
+}
diff --git a/module/VuFind/CLI/config/module.config.php b/module/VuFind/CLI/config/module.config.php
new file mode 100644
index 0000000000000000000000000000000000000000..92e27c0d7250e30c700746ac737148eddd871b0b
--- /dev/null
+++ b/module/VuFind/CLI/config/module.config.php
@@ -0,0 +1,12 @@
+<?php
+namespace VuFind\CLI\Module\Configuration;
+
+$config = array(
+    'controllers' => array(
+        'invokables' => array(
+            'import' => 'VuFind\CLI\Controller\ImportController',
+        ),
+    ),
+);
+
+return $config;
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/Bootstrap.php b/module/VuFind/src/VuFind/Bootstrap.php
index 689785f933ed80e3b82c886cf6abde5b7a802b31..9480117a43984969136fe3b80c649af514772908 100644
--- a/module/VuFind/src/VuFind/Bootstrap.php
+++ b/module/VuFind/src/VuFind/Bootstrap.php
@@ -153,11 +153,13 @@ class Bootstrap
             // Grab the template name from the first child -- we can use this to
             // figure out the current template context.
             $children = $viewModel->getChildren();
-            $parts = explode('/', $children[0]->getTemplate());
-            $viewModel->setVariable('templateDir', $parts[0]);
-            $viewModel->setVariable(
-                'templateName', isset($parts[1]) ? $parts[1] : null
-            );
+            if (!empty($children)) {
+                $parts = explode('/', $children[0]->getTemplate());
+                $viewModel->setVariable('templateDir', $parts[0]);
+                $viewModel->setVariable(
+                    'templateName', isset($parts[1]) ? $parts[1] : null
+                );
+            }
         };
         $this->events->attach('dispatch', $callback);
     }
diff --git a/module/VuFind/src/VuFind/CLI/Controller/AbstractBase.php b/module/VuFind/src/VuFind/CLI/Controller/AbstractBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..4aa60042d4659cd0eac74a07fd35c34af696481b
--- /dev/null
+++ b/module/VuFind/src/VuFind/CLI/Controller/AbstractBase.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * VuFind controller base class (defines some methods that can be shared by other
+ * controllers).
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Controller
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/building_a_recommendations_module Wiki
+ */
+namespace VuFind\CLI\Controller;
+use Zend\Console\Getopt, Zend\Mvc\Controller\AbstractActionController;
+
+/**
+ * VuFind controller base class (defines some methods that can be shared by other
+ * controllers).
+ *
+ * @category VuFind2
+ * @package  Controller
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/building_a_recommendations_module Wiki
+ */
+class AbstractBase extends AbstractActionController
+{
+    protected $consoleOpts;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        // This controller should only be accessed from the command line!
+        if (PHP_SAPI != 'cli') {
+            throw new \Exception('Access denied to command line tools.');
+        }
+
+        // Get access to information about the CLI request.
+        $this->consoleOpts = new Getopt(array());
+    }
+
+    /**
+     * Warn the user if VUFIND_LOCAL_DIR is not set.
+     *
+     * @return void
+     */
+    protected function checkLocalSetting()
+    {
+        if (!getenv('VUFIND_LOCAL_DIR')) {
+            echo "WARNING: The VUFIND_LOCAL_DIR environment variable is not set.\n";
+            echo "This should point to your local configuration directory (i.e. \n";
+            echo realpath(APPLICATION_PATH . '/../local') . ").\n";
+            echo "Without it, inappropriate default settings may be loaded.\n\n";
+        }
+    }
+}
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/CLI/Controller/ImportController.php b/module/VuFind/src/VuFind/CLI/Controller/ImportController.php
new file mode 100644
index 0000000000000000000000000000000000000000..9dc622c71c45c728c817ffe6b1e70c72c450bed9
--- /dev/null
+++ b/module/VuFind/src/VuFind/CLI/Controller/ImportController.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * CLI Controller Module
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Controller
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/building_a_recommendations_module Wiki
+ */
+namespace VuFind\CLI\Controller;
+use VuFind\XSLT\Importer;
+
+/**
+ * This controller handles various command-line tools
+ *
+ * @category VuFind2
+ * @package  Controller
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/building_a_recommendations_module Wiki
+ */
+class ImportController extends AbstractBase
+{
+    /**
+     * XSLT Import Tool
+     *
+     * @return void
+     */
+    public function importXslAction()
+    {
+        // Parse switches:
+        $this->consoleOpts->addRules(
+            array('test-only' => 'Use test mode', 'index-s' => 'Solr index to use')
+        );
+        $testMode = $this->consoleOpts->getOption('test-only') ? true : false;
+        $index = $this->consoleOpts->getOption('index');
+        if (empty($index)) {
+            $index = 'Solr';
+        }
+
+        // Display help message if parameters missing:
+        $argv = $this->consoleOpts->getRemainingArgs();
+        if (!isset($argv[1])) {
+            echo "Usage: import-xsl.php [--test-only] [--index <type>] XML_file "
+                . "properties_file\n"
+                . "\tXML_file - source file to index\n"
+                . "\tproperties_file - import configuration file\n"
+                . "If the optional --test-only flag is set, transformed XML will "
+                . "be displayed\non screen for debugging purposes, but it will "
+                . "not be indexed into VuFind.\n\n"
+                . "If the optional --index parameter is set, it must be followed by "
+                . "the name of\na class for accessing Solr; it defaults to the "
+                . "standard Solr class, but could\nbe overridden with, for example, "
+                . "SolrAuth to load authority records.\n\n"
+                . "Note: See vudl.properties and ojs.properties for configuration "
+                . "examples.\n";
+            exit(1);
+        }
+
+        // Try to import the document if successful:
+        try {
+            Importer::save($argv[0], $argv[1], $index, $testMode);
+        } catch (\Exception $e) {
+            echo "Fatal error: " . $e->getMessage() . "\n";
+            exit(1);
+        }
+        if (!$testMode) {
+            echo "Successfully imported {$argv[0]}...\n";
+        }
+        exit(0);
+    }
+}
diff --git a/module/VuFind/src/VuFind/XSLT/Import/VuFind.php b/module/VuFind/src/VuFind/XSLT/Import/VuFind.php
index 9a9784175f6304f67ef84033f5b927bc8eaffeea..8b3becdf1176f8d7fd9eafc308a3157f226cceda 100644
--- a/module/VuFind/src/VuFind/XSLT/Import/VuFind.php
+++ b/module/VuFind/src/VuFind/XSLT/Import/VuFind.php
@@ -26,7 +26,7 @@
  * @link     http://vufind.org/wiki/importing_records Wiki
  */
 namespace VuFind\XSLT\Import;
-use VuFind\Config\Reader as ConfigReader;
+use DOMDocument, VuFind\Config\Reader as ConfigReader;
 
 /**
  * XSLT support class -- all methods of this class must be public and static;
diff --git a/module/VuFind/src/VuFind/XSLT/Importer.php b/module/VuFind/src/VuFind/XSLT/Importer.php
new file mode 100644
index 0000000000000000000000000000000000000000..20ca5028943568d7e4c44b981e73a4b516ddef99
--- /dev/null
+++ b/module/VuFind/src/VuFind/XSLT/Importer.php
@@ -0,0 +1,169 @@
+<?php
+/**
+ * VuFind XSLT importer
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Support_Classes
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/ Wiki
+ */
+namespace VuFind\XSLT;
+use DOMDocument, VuFind\Config\Reader as ConfigReader,
+    VuFind\Connection\Manager as ConnectionManager,
+    XSLTProcessor;
+
+/**
+ * VuFind XSLT importer
+ *
+ * @category VuFind2
+ * @package  Support_Classes
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/ Wiki
+ */
+class Importer
+{
+    /**
+     * Save an XML file to the Solr index using the specified configuration.
+     *
+     * @param string $xmlFile    XML file to transform.
+     * @param string $properties Properties file.
+     * @param string $index      Solr index to use.
+     * @param bool   $testMode   Are we in test-only mode?
+     *
+     * @throws Exception
+     * @return void
+     */
+    public static function save($xmlFile, $properties, $index = 'Solr',
+        $testMode = false
+    ) {
+        // Process the file:
+        $xml = self::generateXML($xmlFile, $properties);
+
+        // Save the results (or just display them, if in test mode):
+        if (!$testMode) {
+            $solr = ConnectionManager::connectToIndex($index);
+            $result = $solr->saveRecord($xml);
+        } else {
+            echo $xml . "\n";
+        }
+    }
+
+    /**
+     * Transform $xmlFile using the provided $properties configuration.
+     *
+     * @param string $xmlFile    XML file to transform.
+     * @param string $properties Properties file.
+     *
+     * @throws Exception
+     * @return mixed             Transformed XML.
+     */
+    protected static function generateXML($xmlFile, $properties)
+    {
+        // Load properties file:
+        $properties = ConfigReader::getConfigPath($properties, 'import');
+        if (!file_exists($properties)) {
+            throw new Exception("Cannot load properties file: {$properties}.");
+        }
+        $options = parse_ini_file($properties, true);
+
+        // Make sure required parameter is set:
+        if (!isset($options['General']['xslt'])) {
+            throw new Exception(
+                "Properties file ({$properties}) is missing General/xslt setting."
+            );
+        }
+        $xslFile = ConfigReader::getConfigPath(
+            $options['General']['xslt'], 'import/xsl'
+        );
+
+        // Initialize the XSL processor:
+        $xsl = self::initProcessor($options);
+
+        // Load up the style sheet
+        $style = new DOMDocument;
+        if (!$style->load($xslFile)) {
+            throw new Exception("Problem loading XSL file: {$xslFile}.");
+        }
+        $xsl->importStyleSheet($style);
+
+        // Load up the XML document
+        $xml = new DOMDocument;
+        if (!$xml->load($xmlFile)) {
+            throw new Exception("Problem loading XML file: {$xmlFile}.");
+        }
+
+        // Process and return the XML through the style sheet
+        $result = $xsl->transformToXML($xml);
+        if (!$result) {
+            throw new Exception("Problem transforming XML.");
+        }
+        return $result;
+    }
+
+    /**
+     * Initialize an XSLT processor using settings from the user-specified properties
+     * file.
+     *
+     * @param array $options Parsed contents of properties file.
+     *
+     * @throws Exception
+     * @return object        XSLT processor.
+     */
+    protected static function initProcessor($options)
+    {
+        // Prepare an XSLT processor and pass it some variables
+        $xsl = new XSLTProcessor();
+
+        // Register PHP functions, if specified:
+        if (isset($options['General']['php_function'])) {
+            $functions = is_array($options['General']['php_function'])
+                ? $options['General']['php_function']
+                : array($options['General']['php_function']);
+            foreach ($functions as $function) {
+                $xsl->registerPHPFunctions($function);
+            }
+        }
+
+        // Register custom classes, if specified:
+        if (isset($options['General']['custom_class'])) {
+            $classes = is_array($options['General']['custom_class'])
+                ? $options['General']['custom_class']
+                : array($options['General']['custom_class']);
+            foreach ($classes as $class) {
+                // Dynamically generate the requested class:
+                $class = preg_replace('/[^A-Za-z0-9_]/', '', $class);
+                eval("class $class extends \\VuFind\\XSLT\\Import\\$class { }");
+                $methods = get_class_methods($class);
+                foreach ($methods as $method) {
+                    $xsl->registerPHPFunctions($class . '::' . $method);
+                }
+            }
+        }
+
+        // Load parameters, if provided:
+        if (isset($options['Parameters'])) {
+            $xsl->setParameter('', $options['Parameters']);
+        }
+
+        return $xsl;
+    }
+}