diff --git a/config/vufind/config.ini b/config/vufind/config.ini
index 4f8076d50eda381750e8c5116c421fc118e28ade..c1218c8c43895b3438ad4132e51de14056eaf18a 100644
--- a/config/vufind/config.ini
+++ b/config/vufind/config.ini
@@ -91,6 +91,11 @@ generator = "VuFind 2.2"
 [Session]
 type                        = File
 lifetime                    = 3600 ; Session lasts for 1 hour
+; Keep-alive interval in seconds. When set to a positive value, the session is kept
+; alive with a JavaScript call as long as a VuFind page is open in the browser. 
+; Default is 0 (disabled). When keep-alive is enabled, session lifetime above can be
+; reduced to e.g. 600.
+;keepAlive = 60
 ;file_save_path              = /tmp/vufind_sessions
 ;memcache_host               = localhost
 ;memcache_port               = 11211
diff --git a/config/vufind/searches.ini b/config/vufind/searches.ini
index c590ea3b78029fe703e42a481d130507a37f727a..f6fbd8eff1999c09437556dcea46e22b63e2ae18 100644
--- a/config/vufind/searches.ini
+++ b/config/vufind/searches.ini
@@ -326,14 +326,22 @@ side[] = "ExpandFacets:Author"
 
 ; This section controls the "New Items" search.
 [NewItem]
+; New item information can be retrieved from Solr or from the ILS; this setting
+; controls which mechanism is used. If using Solr, change tracking must be enabled
+; (see https://vufind.org/wiki/tracking_record_changes). If using the ILS, your
+; driver must support the getNewItems() method.
+; Valid options: ils, solr; default: ils
+method = ils
 ; Comma-separated list of date ranges to offer to the user (i.e. 1,5,30 = one day
-; old, or five days old, or thirty days old).  Be careful about raising the maximum
-; age too high -- searching very long date ranges may put a load on your ILS.
+; old, or five days old, or thirty days old). If using the "ils" method, be careful
+; about raising the maximum age too high -- searching very long date ranges may put
+; a load on your ILS.
 ranges = 1,5,30
-; This setting controls the maximum number of pages of results that will show up
-; when doing a new item search.  It is necessary to limit the number of results to
-; avoid getting a "too many boolean clauses" error from the Solr index (see notes
-; at http://vufind.org/jira/browse/VUFIND-128 for more details).  However, if you
+; This setting only applies when using the "ils" method. It controls the maximum
+; number of pages of results that will show up when doing a new item search. 
+; It is necessary to limit the number of results to avoid getting a "too many boolean
+; clauses" error from the Solr index (see notes at
+; http://vufind.org/jira/browse/VUFIND-128 for more details).  However, if you
 ; set the value too low, you may get the same results no matter which range setting
 ; is selected!
 result_pages = 10
diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index a9d55c809247b3f1693d47c94985441d7dc5297b..083800ed48bf745f64319b1008544940502124c9 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -101,8 +101,9 @@ $config = array(
     'controller_plugins' => array(
         'factories' => array(
             'holds' => array('VuFind\Controller\Plugin\Factory', 'getHolds'),
-            'storageRetrievalRequests' => array('VuFind\Controller\Plugin\Factory', 'getStorageRetrievalRequests'),
+            'newitems' => array('VuFind\Controller\Plugin\Factory', 'getNewItems'),
             'reserves' => array('VuFind\Controller\Plugin\Factory', 'getReserves'),
+            'storageRetrievalRequests' => array('VuFind\Controller\Plugin\Factory', 'getStorageRetrievalRequests'),
         ),
         'invokables' => array(
             'db-upgrade' => 'VuFind\Controller\Plugin\DbUpgrade',
diff --git a/module/VuFind/src/VuFind/Controller/AjaxController.php b/module/VuFind/src/VuFind/Controller/AjaxController.php
index 015dc5c2259da7dfcaa41307f4c69fef8908670b..78e995a7c35664d0f010d6067493cc4916581bd2 100644
--- a/module/VuFind/src/VuFind/Controller/AjaxController.php
+++ b/module/VuFind/src/VuFind/Controller/AjaxController.php
@@ -1348,6 +1348,19 @@ class AjaxController extends AbstractBase
         return $this->output($html, self::STATUS_OK);
     }
 
+    /**
+     * Keep Alive
+     *
+     * This is responsible for keeping the session alive whenever called
+     * (via JavaScript)
+     *
+     * @return \Zend\Http\Response
+     */
+    protected function keepAliveAjax()
+    {
+        return $this->output(true, self::STATUS_OK);
+    }
+
     /**
      * Convenience method for accessing results
      *
diff --git a/module/VuFind/src/VuFind/Controller/MyResearchController.php b/module/VuFind/src/VuFind/Controller/MyResearchController.php
index d747fecffa83492c32ab99e012bac944065f7509..a038a418e0e546f47ffb3309903002722bbdf9e1 100644
--- a/module/VuFind/src/VuFind/Controller/MyResearchController.php
+++ b/module/VuFind/src/VuFind/Controller/MyResearchController.php
@@ -504,7 +504,7 @@ class MyResearchController extends AbstractBase
         $source = $this->params()->fromPost(
             'source', $this->params()->fromQuery('source', 'VuFind')
         );
-        $driver = $this->getRecordLoader()->load($id, $source);
+        $driver = $this->getRecordLoader()->load($id, $source, true);
         $listID = $this->params()->fromPost(
             'list_id', $this->params()->fromQuery('list_id', null)
         );
@@ -789,21 +789,9 @@ class MyResearchController extends AbstractBase
      */
     protected function getDriverForILSRecord($current)
     {
-        try {
-            if (!isset($current['id'])) {
-                throw new RecordMissingException();
-            }
-            $record = $this->getServiceLocator()->get('VuFind\RecordLoader')
-                ->load($current['id']);
-        } catch (RecordMissingException $e) {
-            $factory = $this->getServiceLocator()
-                ->get('VuFind\RecordDriverPluginManager');
-            $record = $factory->get('Missing');
-            $record->setRawData(
-                array('id' => isset($current['id']) ? $current['id'] : null)
-            );
-            $record->setSourceIdentifier('Solr');
-        }
+        $id = isset($current['id']) ? $current['id'] : null;
+        $record = $this->getServiceLocator()->get('VuFind\RecordLoader')
+            ->load($id, 'VuFind', true);
         $record->setExtraDetail('ils_details', $current);
         return $record;
     }
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Factory.php b/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
index aec0729b62662cace51a46bf3e8c2939a28788fb..8f93d7c4dcc242dca4edb2e9b4727135960bdf3c 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
@@ -52,17 +52,18 @@ class Factory
     }
 
     /**
-     * Construct the StorageRetrievalRequests plugin.
+     * Construct the NewItems plugin.
      *
      * @param ServiceManager $sm Service manager.
      *
-     * @return StorageRetrievalRequests
+     * @return Reserves
      */
-    public static function getStorageRetrievalRequests(ServiceManager $sm)
+    public static function getNewItems(ServiceManager $sm)
     {
-        return new StorageRetrievalRequests(
-            $sm->getServiceLocator()->get('VuFind\HMAC')
-        );
+        $search = $sm->getServiceLocator()->get('VuFind\Config')->get('searches');
+        $config = isset($search->NewItem)
+            ? $search->NewItem : new \Zend\Config\Config(array());
+        return new NewItems($config);
     }
 
     /**
@@ -79,4 +80,18 @@ class Factory
             && $config->Reserves->search_enabled;
         return new Reserves($useIndex);
     }
+
+    /**
+     * Construct the StorageRetrievalRequests plugin.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return StorageRetrievalRequests
+     */
+    public static function getStorageRetrievalRequests(ServiceManager $sm)
+    {
+        return new StorageRetrievalRequests(
+            $sm->getServiceLocator()->get('VuFind\HMAC')
+        );
+    }
 }
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php b/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php
new file mode 100644
index 0000000000000000000000000000000000000000..b896624978ff051525c341e6b6a9bd7b1c9c515f
--- /dev/null
+++ b/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php
@@ -0,0 +1,204 @@
+<?php
+/**
+ * VuFind Action Helper - New Items Support Methods
+ *
+ * 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_Plugins
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://www.vufind.org  Main Page
+ */
+namespace VuFind\Controller\Plugin;
+use Zend\Mvc\Controller\Plugin\AbstractPlugin, Zend\Config\Config;
+
+/**
+ * Zend action helper to perform new items-related actions
+ *
+ * @category VuFind2
+ * @package  Controller_Plugins
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://www.vufind.org  Main Page
+ */
+class NewItems extends AbstractPlugin
+{
+    /**
+     * Configuration
+     *
+     * @var Config
+     */
+    protected $config;
+
+    /**
+     * Constructor
+     *
+     * @param Config $config Configuration
+     */
+    public function __construct(Config $config)
+    {
+        $this->config = $config;
+    }
+
+    /**
+     * Figure out which bib IDs to load from the ILS.
+     *
+     * @param \VuFind\ILS\Connection                     $catalog ILS connection
+     * @param \VuFind\Search\Solr\Params                 $params  Solr parameters
+     * @param string                                     $range   Range setting
+     * @param string                                     $dept    Department setting
+     * @param \Zend\Mvc\Controller\Plugin\FlashMessenger $flash   Flash messenger
+     *
+     * @return array
+     */
+    public function getBibIDsFromCatalog($catalog, $params, $range, $dept, $flash)
+    {
+        // The code always pulls in enough catalog results to get a fixed number
+        // of pages worth of Solr results.  Note that if the Solr index is out of
+        // sync with the ILS, we may see fewer results than expected.
+        $resultPages = $this->getResultPages();
+        $perPage = $params->getLimit();
+        $newItems = $catalog->getNewItems(1, $perPage * $resultPages, $range, $dept);
+
+        // Build a list of unique IDs
+        $bibIDs = array();
+        for ($i=0; $i<count($newItems['results']); $i++) {
+            $bibIDs[] = $newItems['results'][$i]['id'];
+        }
+
+        // Truncate the list if it is too long:
+        $limit = $params->getQueryIDLimit();
+        if (count($bibIDs) > $limit) {
+            $bibIDs = array_slice($bibIDs, 0, $limit);
+            $flash->setNamespace('info')->addMessage('too_many_new_items');
+        }
+
+        return $bibIDs;
+    }
+
+    /**
+     * Get fund list
+     *
+     * @return array
+     */
+    public function getFundList()
+    {
+        if ($this->getMethod() == 'ils') {
+            $catalog = $this->getController()->getILS();
+            return $catalog->checkCapability('getFunds')
+                ? $catalog->getFunds() : array();
+        }
+        return array();
+    }
+
+    /**
+     * Get the hidden filter settings.
+     *
+     * @return array
+     */
+    public function getHiddenFilters()
+    {
+        if (!isset($this->config->filter)) {
+            return array();
+        }
+        if (is_string($this->config->filter)) {
+            return array($this->config->filter);
+        }
+        $hiddenFilters = array();
+        foreach ($this->config->filter as $current) {
+            $hiddenFilters[] = $current;
+        }
+        return $hiddenFilters;
+    }
+
+    /**
+     * Get the maximum range setting (or return 0 for no limit).
+     *
+     * @return int
+     */
+    public function getMaxAge()
+    {
+        return max($this->getRanges());
+    }
+
+    /**
+     * Get method setting
+     *
+     * @return string
+     */
+    public function getMethod()
+    {
+        return isset($this->config->method) ? $this->config->method : 'ils';
+    }
+
+    /**
+     * Get range settings
+     *
+     * @return array
+     */
+    public function getRanges()
+    {
+        // Find out if there are user configured range options; if not,
+        // default to the standard 1/5/30 days:
+        $ranges = array();
+        if (isset($this->config->ranges)) {
+            $tmp = explode(',', $this->config->ranges);
+            foreach ($tmp as $range) {
+                $range = intval($range);
+                if ($range > 0) {
+                    $ranges[] = $range;
+                }
+            }
+        }
+        if (empty($ranges)) {
+            $ranges = array(1, 5, 30);
+        }
+        return $ranges;
+    }
+
+    /**
+     * Get the result pages setting.
+     *
+     * @return int
+     */
+    public function getResultPages()
+    {
+        if (isset($this->config->result_pages)) {
+            $resultPages = intval($this->config->result_pages);
+            if ($resultPages < 1) {
+                $resultPages = 10;
+            }
+        } else {
+            $resultPages = 10;
+        }
+        return $resultPages;
+    }
+
+    /**
+     * Get a Solr filter to limit to the specified number of days.
+     *
+     * @param int $range Days to search
+     *
+     * @return string
+     */
+    public function getSolrFilter($range)
+    {
+        return 'first_indexed:[NOW-' . $range .'DAY TO NOW]';
+    }
+}
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/Controller/SearchController.php b/module/VuFind/src/VuFind/Controller/SearchController.php
index e11b50997555041ed87a10d19b135d567f95e1f8..35849a6393762077fed4f6c24bf94f2acc2b0f52 100644
--- a/module/VuFind/src/VuFind/Controller/SearchController.php
+++ b/module/VuFind/src/VuFind/Controller/SearchController.php
@@ -302,28 +302,11 @@ class SearchController extends AbstractSearch
             return $this->forwardTo('Search', 'NewItemResults');
         }
 
-        // Find out if there are user configured range options; if not,
-        // default to the standard 1/5/30 days:
-        $ranges = array();
-        $searchSettings = $this->getConfig('searches');
-        if (isset($searchSettings->NewItem->ranges)) {
-            $tmp = explode(',', $searchSettings->NewItem->ranges);
-            foreach ($tmp as $range) {
-                $range = intval($range);
-                if ($range > 0) {
-                    $ranges[] = $range;
-                }
-            }
-        }
-        if (empty($ranges)) {
-            $ranges = array(1, 5, 30);
-        }
-
-        $catalog = $this->getILS();
-        $fundList = $catalog->checkCapability('getFunds')
-            ? $catalog->getFunds() : array();
         return $this->createViewModel(
-            array('fundList' => $fundList, 'ranges' => $ranges)
+            array(
+                'fundList' => $this->newItems()->getFundList(),
+                'ranges' => $this->newItems()->getRanges()
+            )
         );
     }
 
@@ -340,72 +323,40 @@ class SearchController extends AbstractSearch
 
         // Validate the range parameter -- it should not exceed the greatest
         // configured value:
-        $searchSettings = $this->getConfig('searches');
-        $maxAge = 0;
-        if (isset($searchSettings->NewItem->ranges)) {
-            $tmp = explode(',', $searchSettings->NewItem->ranges);
-            foreach ($tmp as $current) {
-                if (intval($current) > $maxAge) {
-                    $maxAge = intval($current);
-                }
-            }
-        }
+        $maxAge = $this->newItems()->getMaxAge();
         if ($maxAge > 0 && $range > $maxAge) {
             $range = $maxAge;
         }
 
-        // The code always pulls in enough catalog results to get a fixed number
-        // of pages worth of Solr results.  Note that if the Solr index is out of
-        // sync with the ILS, we may see fewer results than expected.
-        if (isset($searchSettings->NewItem->result_pages)) {
-            $resultPages = intval($searchSettings->NewItem->result_pages);
-            if ($resultPages < 1) {
-                $resultPages = 10;
-            }
+        // Are there "new item" filter queries specified in the config file?
+        // If so, load them now; we may add more values. These will be applied
+        // later after the whole list is collected.
+        $hiddenFilters = $this->newItems()->getHiddenFilters();
+
+        // Depending on whether we're in ILS or Solr mode, we need to do some
+        // different processing here to retrieve the correct items:
+        if ($this->newItems()->getMethod() == 'ils') {
+            // Use standard search action with override parameter to show results:
+            $bibIDs = $this->newItems()->getBibIDsFromCatalog(
+                $this->getILS(),
+                $this->getResultsManager()->get('Solr')->getParams(),
+                $range, $dept, $this->flashMessenger()
+            );
+            $this->getRequest()->getQuery()->set('overrideIds', $bibIDs);
         } else {
-            $resultPages = 10;
-        }
-        $catalog = $this->getILS();
-        $params = $this->getResultsManager()->get('Solr')->getParams();
-        $perPage = $params->getLimit();
-        $newItems = $catalog->getNewItems(1, $perPage * $resultPages, $range, $dept);
-
-        // Build a list of unique IDs
-        $bibIDs = array();
-        for ($i=0; $i<count($newItems['results']); $i++) {
-            $bibIDs[] = $newItems['results'][$i]['id'];
-        }
-
-        // Truncate the list if it is too long:
-        $limit = $params->getQueryIDLimit();
-        if (count($bibIDs) > $limit) {
-            $bibIDs = array_slice($bibIDs, 0, $limit);
-            $this->flashMessenger()->setNamespace('info')
-                ->addMessage('too_many_new_items');
+            // Use a Solr filter to show results:
+            $hiddenFilters[] = $this->newItems()->getSolrFilter($range);
         }
 
-        // Use standard search action with override parameter to show results:
-        $this->getRequest()->getQuery()->set('overrideIds', $bibIDs);
-
-        // Are there "new item" filter queries specified in the config file?
-        // If so, we should apply them as hidden filters so they do not show
-        // up in the user-selected facet list.
-        if (isset($searchSettings->NewItem->filter)) {
-            if (is_string($searchSettings->NewItem->filter)) {
-                $hiddenFilters = array($searchSettings->NewItem->filter);
-            } else {
-                $hiddenFilters = array();
-                foreach ($searchSettings->NewItem->filter as $current) {
-                    $hiddenFilters[] = $current;
-                }
-            }
+        // If we found hidden filters above, apply them now:
+        if (!empty($hiddenFilters)) {
             $this->getRequest()->getQuery()->set('hiddenFilters', $hiddenFilters);
         }
 
         // Call rather than forward, so we can use custom template
         $view = $this->resultsAction();
 
-        // Customize the URL helper to make sure it builds proper reserves URLs
+        // Customize the URL helper to make sure it builds proper new item URLs
         // (check it's set first -- RSS feed will return a response model rather
         // than a view model):
         if (isset($view->results)) {
@@ -432,7 +383,7 @@ class SearchController extends AbstractSearch
         ) {
             return $this->forwardTo('Search', 'ReservesResults');
         }
-        
+
         // No params?  Show appropriate form (varies depending on whether we're
         // using driver-based or Solr-based reserves searching).
         if ($this->reserves()->useIndex()) {
diff --git a/module/VuFind/src/VuFind/Record/Loader.php b/module/VuFind/src/VuFind/Record/Loader.php
index 9f03a0319d08cdd92ce0c2fb269efff2ecfade17..446003705681d3c4583bd85cc15335fb71c3ee9a 100644
--- a/module/VuFind/src/VuFind/Record/Loader.php
+++ b/module/VuFind/src/VuFind/Record/Loader.php
@@ -71,21 +71,29 @@ class Loader
     /**
      * Given an ID and record source, load the requested record object.
      *
-     * @param string $id     Record ID
-     * @param string $source Record source
+     * @param string $id              Record ID
+     * @param string $source          Record source
+     * @param bool   $tolerateMissing Should we load a "Missing" placeholder
+     * instead of throwing an exception if the record cannot be found?
      *
      * @throws \Exception
      * @return \VuFind\RecordDriver\AbstractBase
      */
-    public function load($id, $source = 'VuFind')
+    public function load($id, $source = 'VuFind', $tolerateMissing = false)
     {
         $results = $this->searchService->retrieve($source, $id)->getRecords();
-        if (count($results) < 1) {
-            throw new RecordMissingException(
-                'Record ' . $source . ':' . $id . ' does not exist.'
-            );
+        if (count($results) > 0) {
+            return $results[0];
+        }
+        if ($tolerateMissing) {
+            $record = $this->recordFactory->get('Missing');
+            $record->setRawData(array('id' => $id));
+            $record->setSourceIdentifier($source);
+            return $record;
         }
-        return $results[0];
+        throw new RecordMissingException(
+            'Record ' . $source . ':' . $id . ' does not exist.'
+        );
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Factory.php b/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
index 5177cc9cbf0177b46cfafe75985a51bddc1d5620..676edbc1d06e4df25c80ff2a4de0919b5c8bb44f 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
@@ -253,6 +253,21 @@ class Factory
         return new JsTranslations($sm->get('transesc'));
     }
 
+    /**
+     * Construct the KeepAlive helper.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return KeepAlive
+     */
+    public static function getKeepAlive(ServiceManager $sm)
+    {
+        $config = $sm->getServiceLocator()->get('VuFind\Config')->get('config');
+        return new KeepAlive(
+            isset($config->Session->keepAlive) ? $config->Session->keepAlive : 0
+        );
+    }
+
     /**
      * Construct the ProxyUrl helper.
      *
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/KeepAlive.php b/module/VuFind/src/VuFind/View/Helper/Root/KeepAlive.php
new file mode 100644
index 0000000000000000000000000000000000000000..8e7902b9ad92b6444cc9bdcea664c3f53ac3c4a7
--- /dev/null
+++ b/module/VuFind/src/VuFind/View/Helper/Root/KeepAlive.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * KeepAlive view helper
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ * Copyright (C) The National Library of Finland 2014.
+ *
+ * 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  View_Helpers
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org   Main Site
+ */
+namespace VuFind\View\Helper\Root;
+
+/**
+ * KeepAlive view helper
+ *
+ * @category VuFind2
+ * @package  View_Helpers
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org   Main Site
+ */
+class KeepAlive extends \Zend\View\Helper\AbstractHelper
+{
+    /**
+     * Keep-alive interval in seconds or 0 if disabled
+     *
+     * @var int
+     */
+    protected $interval;
+
+    /**
+     * Constructor
+     *
+     * @param int $interval Keep-alive interval in seconds or 0 if disabled
+     */
+    public function __construct($interval)
+    {
+        $this->interval = $interval;
+    }
+
+    /**
+     * Returns the keep-alive interval.
+     *
+     * @return int
+     */
+    public function __invoke()
+    {
+        return $this->interval;
+    }
+}
\ No newline at end of file
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa528d9bf9385daf98480f29186d537cf8aaaa62
--- /dev/null
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php
@@ -0,0 +1,223 @@
+<?php
+
+/**
+ * New items controller plugin tests.
+ *
+ * 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  Tests
+ * @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/vufind2:unit_tests Wiki
+ */
+
+namespace VuFindTest\Controller\Plugin;
+
+use VuFind\Controller\Plugin\NewItems;
+use VuFindTest\Unit\TestCase as TestCase;
+use Zend\Config\Config;
+
+/**
+ * New items controller plugin tests.
+ *
+ * @category VuFind2
+ * @package  Tests
+ * @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/vufind2:unit_tests Wiki
+ */
+class NewItemsTest extends TestCase
+{
+    /**
+     * Test ILS bib ID retrieval.
+     *
+     * @return void
+     */
+    public function testGetBibIDsFromCatalog()
+    {
+        $flash = $this->getMock('Zend\Mvc\Controller\Plugin\FlashMessenger');
+        $config = new Config(array('result_pages' => 10));
+        $newItems = new NewItems($config);
+        $bibs = $newItems->getBibIDsFromCatalog(
+            $this->getMockCatalog(), $this->getMockParams(), 10, 'a', $flash
+        );
+        $this->assertEquals(array(1, 2), $bibs);
+    }
+
+    /**
+     * Test ILS bib ID retrieval with ID limit.
+     *
+     * @return void
+     */
+    public function testGetBibIDsFromCatalogWithIDLimit()
+    {
+        $flash = $this->getMock('Zend\Mvc\Controller\Plugin\FlashMessenger');
+        $flash->expects($this->once())->method('setNamespace')
+            ->with($this->equalTo('info'))->will($this->returnValue($flash));
+        $flash->expects($this->once())->method('addMessage')
+            ->with($this->equalTo('too_many_new_items'));
+        $config = new Config(array('result_pages' => 10));
+        $newItems = new NewItems($config);
+        $bibs = $newItems->getBibIDsFromCatalog(
+            $this->getMockCatalog(), $this->getMockParams(1), 10, 'a', $flash
+        );
+        $this->assertEquals(array(1), $bibs);
+    }
+
+    /**
+     * Test default ILS getFunds() behavior.
+     *
+     * @return void
+     */
+    public function testGetFundList()
+    {
+        $catalog = $this->getMock(
+            'VuFind\ILS\Connection', array('checkCapability', 'getFunds'),
+            array(), '', false
+        );
+        $catalog->expects($this->once())->method('checkCapability')
+            ->with($this->equalTo('getFunds'))->will($this->returnValue(true));
+        $catalog->expects($this->once())->method('getFunds')
+            ->will($this->returnValue(array('a', 'b', 'c')));
+        $controller = $this->getMock('VuFind\Controller\SearchController');
+        $controller->expects($this->once())->method('getILS')
+            ->will($this->returnValue($catalog));
+        $newItems = new NewItems(new Config(array()));
+        $newItems->setController($controller);
+        $this->assertEquals(array('a', 'b', 'c'), $newItems->getFundList());
+    }
+
+    /**
+     * Test a single hidden filter.
+     *
+     * @return void
+     */
+    public function testGetSingleHiddenFilter()
+    {
+        $config = new Config(array('filter' => 'a:b'));
+        $newItems = new NewItems($config);
+        $this->assertEquals(array('a:b'), $newItems->getHiddenFilters());
+    }
+
+    /**
+     * Test a single hidden filter.
+     *
+     * @return void
+     */
+    public function testGetMultipleHiddenFilters()
+    {
+        $config = new Config(array('filter' => array('a:b', 'b:c')));
+        $newItems = new NewItems($config);
+        $this->assertEquals(array('a:b', 'b:c'), $newItems->getHiddenFilters());
+    }
+
+    /**
+     * Test various default values.
+     *
+     * @return void
+     */
+    public function testDefaults()
+    {
+        $config = new Config(array());
+        $newItems = new NewItems($config);
+        $this->assertEquals(array(), $newItems->getHiddenFilters());
+        $this->assertEquals('ils', $newItems->getMethod());
+        $this->assertEquals(30, $newItems->getMaxAge());
+        $this->assertEquals(array(1, 5, 30), $newItems->getRanges());
+        $this->assertEquals(10, $newItems->getResultPages());
+    }
+
+    /**
+     * Test custom range settings.
+     *
+     * @return void
+     */
+    public function testCustomRanges()
+    {
+        $config = new Config(array('ranges' => '10,150,300'));
+        $newItems = new NewItems($config);
+        $this->assertEquals(array(10, 150, 300), $newItems->getRanges());
+    }
+
+    /**
+     * Test custom result pages setting.
+     *
+     * @return void
+     */
+    public function testCustomResultPages()
+    {
+        $config = new Config(array('result_pages' => '2'));
+        $newItems = new NewItems($config);
+        $this->assertEquals(2, $newItems->getResultPages());
+    }
+
+    /**
+     * Test Solr filter generator.
+     *
+     * @return void
+     */
+    public function testGetSolrFilter()
+    {
+        $range = 30;
+        $expected = 'first_indexed:[NOW-' . $range .'DAY TO NOW]';
+        $newItems = new NewItems(new Config(array()));
+        $this->assertEquals($expected, $newItems->getSolrFilter($range));
+    }
+
+    /**
+     * Get a mock catalog object (for use in getBibIDs tests).
+     *
+     * @return \VuFind\ILS\Connection
+     */
+    protected function getMockCatalog()
+    {
+        $catalog = $this->getMock(
+            'VuFind\ILS\Connection', array('getNewItems'), array(), '', false
+        );
+        $catalog->expects($this->once())->method('getNewItems')
+            ->with(
+                $this->equalTo(1), $this->equalTo(200),
+                $this->equalTo(10), $this->equalTo('a')
+            )
+            ->will(
+                $this->returnValue(
+                    array('results' => array(array('id' => 1), array('id' => 2)))
+                )
+            );
+        return $catalog;
+    }
+
+    /**
+     * Get a mock params object.
+     *
+     * @param int $idLimit Mock ID limit value
+     *
+     * @return \VuFind\Search\Solr\Params
+     */
+    protected function getMockParams($idLimit = 1024)
+    {
+        $params = $this
+            ->getMock('VuFind\Search\Solr\Params', array(), array(), '', false);
+        $params->expects($this->once())->method('getLimit')
+            ->will($this->returnValue(20));
+        $params->expects($this->once())->method('getQueryIDLimit')
+            ->will($this->returnValue($idLimit));
+        return $params;
+    }
+}
\ No newline at end of file
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Record/LoaderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Record/LoaderTest.php
index 81a137fd5e9875ba78830004bc22a2a658494bcf..c034887a619e2ce0b12a788f0617167aa3159ae9 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Record/LoaderTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Record/LoaderTest.php
@@ -65,6 +65,28 @@ class LoaderTest extends TestCase
         $loader->load('test');
     }
 
+    /**
+     * Test "tolerate missing records" feature.
+     *
+     * @return void
+     */
+    public function testToleratedMissingRecord()
+    {
+        $collection = $this->getCollection(array());
+        $service = $this->getMock('VuFindSearch\Service');
+        $service->expects($this->once())->method('retrieve')
+            ->with($this->equalTo('VuFind'), $this->equalTo('test'))
+            ->will($this->returnValue($collection));
+        $missing = $this->getDriver('missing', 'Missing');
+        $factory = $this->getMock('VuFind\RecordDriver\PluginManager');
+        $factory->expects($this->once())->method('get')
+            ->with($this->equalTo('Missing'))
+            ->will($this->returnValue($missing));
+        $loader = $this->getLoader($service, $factory);
+        $record = $loader->load('test', 'VuFind', true);
+        $this->assertEquals($missing, $record);
+    }
+
     /**
      * Test single record.
      *
diff --git a/themes/blueprint/js/keep_alive.js b/themes/blueprint/js/keep_alive.js
new file mode 100644
index 0000000000000000000000000000000000000000..5556008d6762ef7d2ebe940bba359c7196057d1d
--- /dev/null
+++ b/themes/blueprint/js/keep_alive.js
@@ -0,0 +1,7 @@
+/*global path, keepAliveInterval */
+
+$(document).ready(function() {
+  window.setInterval(function() {
+    $.getJSON(path + '/AJAX/JSON', {method: 'keepAlive'});
+  }, keepAliveInterval * 1000);
+});
diff --git a/themes/blueprint/templates/layout/layout.phtml b/themes/blueprint/templates/layout/layout.phtml
index b280e799f557a0e88663945d4a48fc64fd63108f..d0d7f8e3948a5c2d4c468a48d5eb3cf15d0efd23 100644
--- a/themes/blueprint/templates/layout/layout.phtml
+++ b/themes/blueprint/templates/layout/layout.phtml
@@ -59,6 +59,13 @@
             $this->headScript()->appendFile("jquery.tabSlideOut.v2.0.js");
             $this->headScript()->appendFile("feedback.js");
         }
+        
+        // Session keep-alive
+        if ($this->KeepAlive()) {
+            $this->headScript()->appendScript('var keepAliveInterval = '
+                . $this->KeepAlive());
+            $this->headScript()->appendFile("keep_alive.js");
+        }
     ?>
     <?=$this->headScript()?>
   </head>
diff --git a/themes/bootprint/js/pubdate_vis.js b/themes/bootprint/js/pubdate_vis.js
index 247aa1ae52a1792ccbcd5675cad9105953b0e5bd..f72dd7dd580e42d99ca1bca27f33ed08d63dd730 100644
--- a/themes/bootprint/js/pubdate_vis.js
+++ b/themes/bootprint/js/pubdate_vis.js
@@ -106,12 +106,12 @@ function loadVis(facetFields, searchParams, baseURL, zooming) {
         });
 
         if (hasFilter) {
-          var newdiv = document.createElement('div');
+          var newdiv = document.createElement('span');
           var text = document.getElementById("clearButtonText").innerHTML;
           newdiv.setAttribute('id', 'clearButton' + key);
           newdiv.innerHTML = '<a href="' + htmlEncode(val['removalURL']) + '">' + text + '</a>';
           newdiv.className += "dateVisClear";
-          placeholder.append(newdiv);
+          placeholder.before(newdiv);
         }
       });
     }
diff --git a/themes/bootstrap/js/cart.js b/themes/bootstrap/js/cart.js
index 3308a86adcb0f7c72aec42d2e1c87e21fddfb361..23c61e7afc8cb244da4125627f8f4526e09ce15c 100644
--- a/themes/bootstrap/js/cart.js
+++ b/themes/bootstrap/js/cart.js
@@ -52,6 +52,15 @@ function addItemToCart(id,source) {
   $('#cartItems strong').html(parseInt($('#cartItems strong').html(), 10)+1);
   return true;
 }
+function uniqueArray(op) {
+  var ret = [];
+  for(var i=0;i<op.length;i++) {
+    if(ret.indexOf(op[i]) < 0) {
+      ret.push(op[i]);
+    }
+  }
+  return ret;
+}
 function removeItemFromCart(id,source) {
   var cartItems = getCartItems();
   var cartSources = getCartSources();
@@ -77,10 +86,10 @@ function removeItemFromCart(id,source) {
       var oldSources = cartSources.slice(0);
       cartSources.splice(sourceIndex,1);
       // Adjust source index characters
-      for(var i=cartItems.length;i--;) {
-        var si = cartItems[i].charCodeAt(0)-65;
+      for(var j=cartItems.length;j--;) {
+        var si = cartItems[j].charCodeAt(0)-65;
         var ni = cartSources.indexOf(oldSources[si]);
-        cartItems[i] = String.fromCharCode(65+ni)+cartItems[i].substring(1);
+        cartItems[j] = String.fromCharCode(65+ni)+cartItems[j].substring(1);
       }
     }
     if(cartItems.length > 0) {
@@ -95,16 +104,6 @@ function removeItemFromCart(id,source) {
   }
   return false;
 }
-function uniqueArray(op) {
-  var ret = [];
-  for(var i=0;i<op.length;i++) {
-    if(ret.indexOf(op[i]) < 0) {
-      ret.push(op[i]);
-    }
-  }
-  return ret;
-}
-
 function registerUpdateCart($form) {
   if($form) {
     $("#updateCart, #bottom_updateCart").unbind('click').click(function(){
@@ -213,12 +212,12 @@ $(document).ready(function() {
       url: path + '/AJAX/JSON?' + $.param({method:'exportFavorites'}),
       type:'POST',
       dataType:'json',
-      data:getDataFromForm($(evt.target)),
+      data:Lightbox.getFormData($(evt.target)),
       success:function(data) {
         if(data.data.needs_redirect) {
           document.location.href = data.data.result_url;
         } else {
-          changeModalContent(data.data.result_additional);
+          Lightbox.changeContent(data.data.result_additional);
         }
       },
       error:function(d,e) {
diff --git a/themes/bootstrap/js/common.js b/themes/bootstrap/js/common.js
index 10ff91ffdf88b9652c0adb9717a49b1aac682630..6f035b6cfbcd3c18b58811e8d56cdce8466cf9a6 100644
--- a/themes/bootstrap/js/common.js
+++ b/themes/bootstrap/js/common.js
@@ -1,4 +1,4 @@
-/*global path, vufindString */
+/*global Lightbox, path, vufindString */
 
 /* --- GLOBAL FUNCTIONS --- */
 function htmlEncode(value){
@@ -145,7 +145,11 @@ $(document).ready(function() {
   var url = window.location.href;
   if(url.indexOf('?' + 'print' + '=') != -1  || url.indexOf('&' + 'print' + '=') != -1) {
     $("link[media='print']").attr("media", "all");
-    window.print();
+    $(document).ajaxStop(function() {
+      window.print();
+    });
+    // Make an ajax call to ensure that ajaxStop is triggered
+    $.getJSON(path + '/AJAX/JSON', {method: 'keepAlive'});
   }
     
   // Collapsing facets
@@ -182,7 +186,7 @@ $(document).ready(function() {
     var parts = this.href.split('/');
     return Lightbox.get(parts[parts.length-3],'Save',{id:$(this).attr('id')});
   });
-  Lightbox.addFormCallback('emailSearch', function(html) {
+  Lightbox.addFormCallback('emailSearch', function(x) {
     Lightbox.confirm(vufindString['bulk_email_success']);
   });
 });
\ No newline at end of file
diff --git a/themes/bootstrap/js/keep_alive.js b/themes/bootstrap/js/keep_alive.js
new file mode 100644
index 0000000000000000000000000000000000000000..5556008d6762ef7d2ebe940bba359c7196057d1d
--- /dev/null
+++ b/themes/bootstrap/js/keep_alive.js
@@ -0,0 +1,7 @@
+/*global path, keepAliveInterval */
+
+$(document).ready(function() {
+  window.setInterval(function() {
+    $.getJSON(path + '/AJAX/JSON', {method: 'keepAlive'});
+  }, keepAliveInterval * 1000);
+});
diff --git a/themes/bootstrap/js/lightbox.js b/themes/bootstrap/js/lightbox.js
index 80fc65ee094460c6b3ddd0fd1614d885a9c515b4..dc1f5fa87d0653bd18aadacd72822702f56c9b02 100644
--- a/themes/bootstrap/js/lightbox.js
+++ b/themes/bootstrap/js/lightbox.js
@@ -52,7 +52,7 @@ var Lightbox = {
     if(typeof expectsError === "undefined" || expectsError) {
       this.formCallbacks[formName] = function(html) {
         Lightbox.checkForError(html, func);
-      }
+      };
     } else {
       this.formCallbacks[formName] = func;
     }
@@ -383,13 +383,13 @@ var Lightbox = {
     } else {
       $(form).unbind('submit').submit(function(evt){
         Lightbox.submit($(evt.target), function(html){
-          Lightbox.checkForError(html, Lightbox.close)
+          Lightbox.checkForError(html, Lightbox.close);
         });
         return false;
       });
     }
-  },
-}
+  }
+};
 
 /**
  * This is a full handler for the login form
@@ -491,7 +491,7 @@ function ajaxLogin(form) {
       }
     }
   });
-};
+}
 
 /**
  * This is where you add click events to open the lightbox.
diff --git a/themes/bootstrap/js/pubdate_vis.js b/themes/bootstrap/js/pubdate_vis.js
index 24c28c8d27b72043161adb2c4788cdd128455311..b166b6cbbca7ab6dbae24b4a29fdda8f6a3441bf 100644
--- a/themes/bootstrap/js/pubdate_vis.js
+++ b/themes/bootstrap/js/pubdate_vis.js
@@ -106,12 +106,12 @@ function loadVis(facetFields, searchParams, baseURL, zooming) {
         });
 
         if (hasFilter) {
-          var newdiv = document.createElement('div');
+          var newdiv = document.createElement('span');
           var text = document.getElementById("clearButtonText").innerHTML;
           newdiv.setAttribute('id', 'clearButton' + key);
           newdiv.innerHTML = '<a href="' + htmlEncode(val['removalURL']) + '">' + text + '</a>';
           newdiv.className += "dateVisClear";
-          placeholder.append(newdiv);
+          placeholder.before(newdiv);
         }
       });
     }
diff --git a/themes/bootstrap/js/record.js b/themes/bootstrap/js/record.js
index 3d3c66906b1720a973df8f98641023d4f7868b5a..5de7ae7f6e77356f42350d12199c398e2a81fa15 100644
--- a/themes/bootstrap/js/record.js
+++ b/themes/bootstrap/js/record.js
@@ -1,4 +1,4 @@
-/*global extractClassParams, Lightbox, path, vufindString */
+/*global deparam, extractClassParams, htmlEncode, Lightbox, path, vufindString */
 
 /**
  * Functions and event handlers specific to record pages.
@@ -164,6 +164,14 @@ $(document).ready(function(){
       Lightbox.checkForError(html, Lightbox.changeContent);
     });
   });
+  // Place a Storage Hold
+  $('.placeStorageRetrievalRequest').click(function() {
+    var params = deparam($(this).attr('href'));
+    params.hashKey = params.hashKey.split('#')[0]; // Remove #tabnav
+    return Lightbox.get('Record', 'StorageRetrievalRequest', params, {}, function(html) {
+      Lightbox.checkForError(html, Lightbox.changeContent);
+    });
+  });
   // Save lightbox
   $('#save-record').click(function() {
     var params = extractClassParams(this);
@@ -215,4 +223,7 @@ $(document).ready(function(){
   Lightbox.addFormCallback('placeHold', function() {
     document.location.href = path+'/MyResearch/Holds';
   });
+  Lightbox.addFormCallback('placeStorageRetrievalRequest', function() {
+    document.location.href = path+'/MyResearch/StorageRetrievalRequests';
+  });
 });
diff --git a/themes/bootstrap/templates/layout/layout.phtml b/themes/bootstrap/templates/layout/layout.phtml
index 6b8b3b94eeb82b5fff6f4c6b66367aa2979565c5..b655c1021aadd9eacea230d208bed4bcd3dfeeee 100644
--- a/themes/bootstrap/templates/layout/layout.phtml
+++ b/themes/bootstrap/templates/layout/layout.phtml
@@ -54,6 +54,13 @@
             }
             $this->headScript()->appendScript($this->jsTranslations()->getScript());
         }
+        
+        // Session keep-alive
+        if ($this->KeepAlive()) {
+            $this->headScript()->appendScript('var keepAliveInterval = '
+                . $this->KeepAlive());
+            $this->headScript()->appendFile("keep_alive.js");
+        }
     ?>
     <?=$this->headScript()?>
   </head>
diff --git a/themes/bootstrap/templates/record/storageretrievalrequest.phtml b/themes/bootstrap/templates/record/storageretrievalrequest.phtml
index fcf8d7557acbdff9432bdb16fb24c3a5652e8a12..00e36e93c2d83efe2033c82727acd3be4ee06baa 100644
--- a/themes/bootstrap/templates/record/storageretrievalrequest.phtml
+++ b/themes/bootstrap/templates/record/storageretrievalrequest.phtml
@@ -10,7 +10,7 @@
 <p class="lead"><?=$this->transEsc('storage_retrieval_request_place_text')?></p>
 <?=$this->flashmessages()?>
 <div class="storage-retrieval-request-form">
-  <form action="" class="form-horizontal" method="post">
+  <form name="placeStorageRetrievalRequest" action="" class="form-horizontal" method="post">
     <? if (in_array("item-issue", $this->extraFields)): ?>
       <div class="control-group">
         <div class="controls">
diff --git a/themes/jquerymobile/js/keep_alive.js b/themes/jquerymobile/js/keep_alive.js
new file mode 100644
index 0000000000000000000000000000000000000000..5556008d6762ef7d2ebe940bba359c7196057d1d
--- /dev/null
+++ b/themes/jquerymobile/js/keep_alive.js
@@ -0,0 +1,7 @@
+/*global path, keepAliveInterval */
+
+$(document).ready(function() {
+  window.setInterval(function() {
+    $.getJSON(path + '/AJAX/JSON', {method: 'keepAlive'});
+  }, keepAliveInterval * 1000);
+});
diff --git a/themes/jquerymobile/templates/layout/layout.phtml b/themes/jquerymobile/templates/layout/layout.phtml
index fd32eb7440f274f4b12e40046139356b8c2cd32e..a7aac014f016b5c62c1823b0057a85cd835f1c46 100644
--- a/themes/jquerymobile/templates/layout/layout.phtml
+++ b/themes/jquerymobile/templates/layout/layout.phtml
@@ -12,6 +12,13 @@
     <?
         // Set global path for Javascript code:
         $this->headScript()->prependScript("path = '" . rtrim($this->url('home'), '/') . "';");
+
+        // Session keep-alive
+        if ($this->KeepAlive()) {
+            $this->headScript()->appendScript('var keepAliveInterval = '
+                . $this->KeepAlive());
+            $this->headScript()->appendFile("keep_alive.js");
+        }
     ?>
     <?=$this->headScript()?>
   </head>
diff --git a/themes/root/theme.config.php b/themes/root/theme.config.php
index 5f2629921266829191a74264617f70ff7fb77582..fc46a3eac552bc4e7b3cdeb0a8715b0c4d5c3352 100644
--- a/themes/root/theme.config.php
+++ b/themes/root/theme.config.php
@@ -19,6 +19,7 @@ return array(
             'historylabel' => array('VuFind\View\Helper\Root\Factory', 'getHistoryLabel'),
             'ils' => array('VuFind\View\Helper\Root\Factory', 'getIls'),
             'jstranslations' => array('VuFind\View\Helper\Root\Factory', 'getJsTranslations'),
+            'keepalive' => array('VuFind\View\Helper\Root\Factory', 'getKeepAlive'),
             'proxyurl' => array('VuFind\View\Helper\Root\Factory', 'getProxyUrl'),
             'openurl' => array('VuFind\View\Helper\Root\Factory', 'getOpenUrl'),
             'record' => array('VuFind\View\Helper\Root\Factory', 'getRecord'),