From 3df2d36ba2dde5422a66f56b06ee9f1eb30b52c1 Mon Sep 17 00:00:00 2001
From: Ere Maijala <ere.maijala@helsinki.fi>
Date: Tue, 18 Feb 2014 07:51:16 +0200
Subject: [PATCH] Implemented support for ILL requests in VuFind core code and
 VoyagerRestful and Demo drivers.

---
 config/vufind/Demo.ini                        |   3 +
 config/vufind/VoyagerRestful.ini              |  33 +-
 config/vufind/config.ini                      |   5 +
 languages/en-gb.ini                           |   6 +
 languages/en.ini                              |  30 +
 languages/fi.ini                              |  30 +
 languages/sv.ini                              |  30 +
 module/VuFind/config/module.config.php        |   4 +-
 .../src/VuFind/Controller/AbstractBase.php    |  15 +-
 .../src/VuFind/Controller/AjaxController.php  |  70 ++
 .../Controller/MyResearchController.php       |  56 ++
 .../src/VuFind/Controller/Plugin/Factory.php  |  14 +
 .../VuFind/Controller/Plugin/ILLRequests.php  | 195 +++++
 .../VuFind/Controller/RecordController.php    | 122 +++
 module/VuFind/src/VuFind/ILS/Connection.php   |  90 ++
 module/VuFind/src/VuFind/ILS/Driver/Demo.php  | 282 +++++-
 .../VuFind/src/VuFind/ILS/Driver/Voyager.php  |   2 +-
 .../src/VuFind/ILS/Driver/VoyagerRestful.php  | 825 +++++++++++++++++-
 module/VuFind/src/VuFind/ILS/Logic/Holds.php  | 236 ++---
 themes/blueprint/css/styles.css               |  31 +-
 themes/blueprint/js/ill.js                    |  29 +
 themes/blueprint/js/record.js                 |  12 +-
 .../templates/RecordTab/holdingsils.phtml     |   4 +
 .../templates/myresearch/illrequests.phtml    | 168 ++++
 .../blueprint/templates/myresearch/menu.phtml |   3 +
 .../templates/record/illrequest.phtml         | 111 +++
 themes/bootstrap/js/ill.js                    |  29 +
 themes/bootstrap/js/record.js                 |  16 +-
 .../templates/RecordTab/holdingsils.phtml     |   4 +
 .../templates/myresearch/illrequests.phtml    | 168 ++++
 .../bootstrap/templates/myresearch/menu.phtml |   3 +
 .../templates/record/illrequest.phtml         | 126 +++
 32 files changed, 2594 insertions(+), 158 deletions(-)
 create mode 100644 module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php
 create mode 100644 themes/blueprint/js/ill.js
 create mode 100644 themes/blueprint/templates/myresearch/illrequests.phtml
 create mode 100644 themes/blueprint/templates/record/illrequest.phtml
 create mode 100644 themes/bootstrap/js/ill.js
 create mode 100644 themes/bootstrap/templates/myresearch/illrequests.phtml
 create mode 100644 themes/bootstrap/templates/record/illrequest.phtml

diff --git a/config/vufind/Demo.ini b/config/vufind/Demo.ini
index 8ce6f422063..e3f4727c3a7 100644
--- a/config/vufind/Demo.ini
+++ b/config/vufind/Demo.ini
@@ -6,3 +6,6 @@ idsInMyResearch = true
 
 ; Whether to support storage retrieval requests
 storageRetrievalRequests = true
+
+; Whether to support ILL requests
+ILLRequests = true
diff --git a/config/vufind/VoyagerRestful.ini b/config/vufind/VoyagerRestful.ini
index 53486e13f85..390d6c02d1e 100644
--- a/config/vufind/VoyagerRestful.ini
+++ b/config/vufind/VoyagerRestful.ini
@@ -134,4 +134,35 @@ holdCheckLimit = 15
 ; to display.  If you set this to false, all items will have renewal checkboxes and
 ; we will only check validity if a user attempts a renewal -- faster, but less
 ; accurate.
-checkUpFront = true
\ No newline at end of file
+checkUpFront = true
+
+; This section controls UB (Universal Borrowing, ILL in VuFind) behavior. To enable, 
+; uncomment (at minimum) the HMACKeys and extraFields settings below. See also
+; section UBRequestSources for mapping between patron ID's and UB libraries.
+[ILLRequests]
+
+; HMACKeys - A list of form element names that will be analyzed for consistency
+; during form processing. Most users should not need to change this setting.
+HMACKeys = item_id:holdings_id
+
+; extraFields - A colon-separated list used to display extra visible fields in the
+; request form. Supported values are "pickUpLibrary", 
+; "pickUpLibraryLocation", "requiredByDate" and "comments" (although comments are
+; not properly stored for UB requests at least in version 8.1)
+extraFields = pickUpLibrary:pickUpLibraryLocation:requiredByDate
+
+; defaultRequiredDate - A colon-separated list used to set the default "not required
+; after" date for holds in the format days:months:years
+; e.g. 0:1:0 will set a "not required after" date of 1 month from the current date
+defaultRequiredDate = 0:1:0
+
+; Optional help texts that can be displayed on the ILL form
+helpText = "ILL Help text for all languages."
+helpText[en-gb] = "ILL Help text for English language."
+
+; This section lists the valid patron id prefixes for UB (ILL in VuFind) requests,
+; and maps them to the their Voyager UB library IDs. Any patron of another library
+; with a prefix listed here may attempt a UB request in this system. 
+[ILLRequestSources]
+;devdb = "1@DEVDB20011102161616"
+;otherdb = "1@OTHERDB20011030191919"
diff --git a/config/vufind/config.ini b/config/vufind/config.ini
index 4f8076d50ed..3e534bc3c95 100644
--- a/config/vufind/config.ini
+++ b/config/vufind/config.ini
@@ -151,6 +151,11 @@ cancel_holds_enabled = false
 ; default is false
 cancel_storage_retrieval_requests_enabled = false
 
+; Determines if ILL requests can be cancelled or not. 
+; Options are true or false.
+; default is false
+cancel_ill_requests_enabled = false
+
 ; Determines if item can be renewed or not. Options are true or false.
 ; default is false
 renewals_enabled = false
diff --git a/languages/en-gb.ini b/languages/en-gb.ini
index 305c7531a07..41313f97aa3 100644
--- a/languages/en-gb.ini
+++ b/languages/en-gb.ini
@@ -35,6 +35,12 @@ hold_cancel_success = "Your request was successfully cancelled"
 hold_cancel_success_items = "request(s) were successfully cancelled"
 hold_error_fail = "Your request failed. Please contact the issue desk for further assistance"
 hold_place_fail_missing = "Your request failed. Some data was missing. Please contact the issue desk for further assistance"
+ill_request_available = "Available for Collection"
+ill_request_cancel_fail = "Your request was not cancelled. Please contact the issue desk for further assistance"
+ill_request_error_fail = "Your request failed. Please contact the issue desk for further assistance"
+ill_request_error_technical = "Your request failed due to a system error. Please contact the issue desk for further assistance"
+ill_request_place_fail_missing = "Your request failed. Some data was missing. Please contact the issue desk for further assistance"
+ill_request_profile_html = "For storage retrieval request information, please establish your <a href="%%url%%">Library Catalogue Profile</a>."
 Item removed from favorites = "Item removed from favourites"
 Library Catalog Password = "Library Catalogue Password"
 Library Catalog Profile = "Library Catalogue Profile"
diff --git a/languages/en.ini b/languages/en.ini
index d6e1ef60f86..b41d29b82e3 100644
--- a/languages/en.ini
+++ b/languages/en.ini
@@ -182,6 +182,8 @@ confirm_dialog_no = Cancel
 confirm_dialog_yes = Confirm
 confirm_hold_cancel_all_text = "Do you wish to cancel all your current holds?"
 confirm_hold_cancel_selected_text = "Do you wish to cancel your selected holds?"
+confirm_ill_request_cancel_all_text = "Do you wish to cancel all your current interlibrary loan requests?"
+confirm_ill_request_cancel_selected_text = "Do you wish to cancel your selected interlibrary loan requests?"
 confirm_storage_retrieval_request_cancel_all_text = "Do you wish to cancel all your current storage retrieval requests?"
 confirm_storage_retrieval_request_cancel_selected_text = "Do you wish to cancel your selected storage retrieval requests?"
 Contents = Contents
@@ -378,6 +380,33 @@ hold_success = "Your request was successful"
 Home = Home
 home_browse = "Browse by"
 Identifier = "Identifier"
+ill_request_available = "Available for Pickup"
+ill_request_canceled = "Canceled"
+ill_request_cancel = "Cancel Interlibrary Loan Request"
+ill_request_cancel_all = "Cancel All Interlibrary Loan Requests"
+ill_request_cancel_fail = "Your request was not canceled. Please contact the circulation desk for further assistance"
+ill_request_cancel_selected = "Cancel Selected Interlibrary Loan Requests"
+ill_request_cancel_success = "Your request was successfully canceled"
+ill_request_cancel_success_items = "request(s) were successfully canceled"
+ill_request_check_text = "Check Interlibrary Loan Request"
+ill_request_comments = "Comments"
+ill_request_date_invalid = "Please enter a valid date"
+ill_request_date_past = "Please enter a date in the future"
+ill_request_empty_selection = "No interlibrary loan requests were selected"
+ill_request_error_blocked = "You do not have sufficient privileges to place an interlibrary loan request on this item"
+ill_request_error_fail = "Your request failed. Please contact the circulation desk for further assistance"
+ill_request_error_technical = "Your request failed due to a system error. Please contact the circulation desk for further assistance"
+ill_request_error_unknown_patron_source = "Patron library not identified in interlibrary loan request."
+ill_request_invalid_pickup = "An invalid pickup location was entered. Please try again"
+ill_request_pick_up_library = "Pick Up Library"
+ill_request_pick_up_location = "Pick Up Location"
+ill_request_place_fail_missing = "Your request failed. Some data was missing. Please contact the circulation desk for further assistance"
+ill_request_place_success = "Your request was successful"
+ill_request_place_text = "Place an Interlibrary Loan Request"
+ill_request_processed = "Processed"
+ill_request_profile_html = "For interlibrary loan request information, please establish your <a href="%%url%%">Library Catalog Profile</a>."
+ill_request_submit_text = "Place Request"
+Interlibrary Loan Requests = "Interlibrary Loan Requests"
 Illustrated = Illustrated
 ils_offline_holdings_message = "Holdings and item availability information is currently unavailable. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:"
 ils_offline_home_message = "Your account details and live item information will be unavailable during this time. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:"
@@ -826,6 +855,7 @@ Year of Publication = "Year of Publication"
 Yesterday = Yesterday
 You do not have any fines = "You do not have any fines"
 You do not have any holds or recalls placed = "You do not have any holds or recalls placed"
+You do not have any interlibrary loan requests placed = "You do not have any interlibrary loan requests placed"
 You do not have any items checked out = "You do not have any items checked out"
 You do not have any saved resources = "You do not have any saved resources. Perform a search and use the Add to Favorites button to save items."
 You do not have any storage retrieval requests placed = "You do not have any storage retrieval requests placed"
diff --git a/languages/fi.ini b/languages/fi.ini
index a9e2079c8fd..92bb8928407 100644
--- a/languages/fi.ini
+++ b/languages/fi.ini
@@ -179,6 +179,8 @@ confirm_dialog_no = Peruuta
 confirm_dialog_yes = Vahvista
 confirm_hold_cancel_all_text = "Haluatko perua kaikki varaukset?"
 confirm_hold_cancel_selected_text = "Haluatko perua valitsemasi varaukset?"
+confirm_ill_request_cancel_all_text = "Haluatko perua kaikki kaukolainatilaukset?"
+confirm_ill_request_cancel_selected_text = "Haluatko perua valitsemasi kaukolainatilaukset?"
 confirm_storage_retrieval_request_cancel_all_text = "Haluatko perua kaikki tilaukset?"
 confirm_storage_retrieval_request_cancel_selected_text = "Haluatko perua valitsemasi tilaukset?"
 Contents = "Sisältö"
@@ -375,6 +377,33 @@ hold_success = "Varauspyyntö onnistui"
 Home = "Koti"
 home_browse = "Selaus:"
 Identifier = "Tunniste"
+ill_request_available = "Noudettavissa"
+ill_request_canceled = "Peruttu"
+ill_request_cancel = "Peru tilaus"
+ill_request_cancel_all = "Peru kaikki tilaukset"
+ill_request_cancel_fail = "Tilaustasi ei peruttu. Ota yhteyttä asiakaspalveluun."
+ill_request_cancel_selected = "Peru valitut tilaukset"
+ill_request_cancel_success = "Tilaus peruttu"
+ill_request_cancel_success_items = "tilaus(ta) peruttu"
+ill_request_check_text = "Tarkista kaukolainatilaus"
+ill_request_comments = "Lisätiedot"
+ill_request_date_invalid = "Syötä kelvollinen päivämäärä"
+ill_request_date_past = "Syötä tätä päivää myöhempi päivämäärä"
+ill_request_empty_selection = "Yhtään tilausta ei valittu"
+ill_request_error_blocked = "Tilaaminen ei ole mahdollista, koska olet lainauskiellossa tai sinulla on jo vastaava tilaus."
+ill_request_error_fail = "Tilaus epäonnistui. Ota yhteyttä kirjaston asiakaspalveluun."
+ill_request_error_technical = "Tilaaminen epäonnistui järjestelmävirheen vuoksi. Ota yhteyttä kirjaston asiakaspalveluun."
+ill_request_error_unknown_patron_source = "Asiakkaan kirjastoa ei tunnistettu kaukolainauksessa."
+ill_request_invalid_pickup = "Valittu noutopaikka on virheellinen. Yritä uudestaan."
+ill_request_pick_up_library = "Noutokirjasto"
+ill_request_pick_up_location = "Noutopaikka"
+ill_request_place_fail_missing = "Tilaus epäonnistui puuttuvien tietojen vuoksi. Ota yhteyttä kirjaston asiakaspalveluun."
+ill_request_place_success = "Kaukolainatilaus onnistui"
+ill_request_place_text = "Tee kaukolainatilaus"
+ill_request_processed = "Käsitelty"
+ill_request_profile_html = "Kirjaudu <a href="%%url%%">kirjastokortilla</a> nähdäksesi kaukolainatilaukset."
+ill_request_submit_text = "Tee kaukolainatilaus"
+Interlibrary Loan Requests = "Kaukolainatilaukset"
 Illustrated = "Kuvitus"
 ils_offline_holdings_message = "Saatavuustiedot eivät ole juuri nyt käytettävissä. Pahoittelemme tästä aiheutunutta vaivaa. Voitte ottaa yhteyttä:"
 ils_offline_home_message = "Tilitietosi ja ajantasaiset saatavuustiedot ovat poissa käytöstä tämän ajan. Pahoittelemme tästä aiheutunutta vaivaa. Voitte ottaa yhteyttä:"
@@ -827,6 +856,7 @@ Year of Publication = "Julkaisuvuosi"
 Yesterday = "Eilisestä"
 You do not have any fines = "Ei maksamattomia maksuja"
 You do not have any holds or recalls placed = "Ei voimassaolevia varauksia"
+You do not have any interlibrary loan requests placed = "Ei voimassaolevia kaukolainatilauksia"
 You do not have any items checked out = "Ei lainoja"
 You do not have any saved resources = "Ei tallennettuja tietueita"
 You do not have any storage retrieval requests placed = "Ei voimassaolevia varastotilauksia"
diff --git a/languages/sv.ini b/languages/sv.ini
index 8d6ae0c9679..f14e6c812e1 100644
--- a/languages/sv.ini
+++ b/languages/sv.ini
@@ -179,6 +179,8 @@ confirm_dialog_no = Nej
 confirm_dialog_yes = Ja
 confirm_hold_cancel_all_text = "Är du säker att du vill återkalla alla reserveringar?"
 confirm_hold_cancel_selected_text = "Är du säker att du vill återkalla utvalda reserveringar?"
+confirm_ill_request_cancel_all_text = "Är du säker du vill återkalla alla fjärrlånbeställningar?"
+confirm_ill_request_cancel_selected_text = "Är du säker du vill återkalla utvalda fjärrlånbeställningar?"
 confirm_storage_retrieval_request_cancel_all_text = "Är du säker du vill återkalla alla beställningar?"
 confirm_storage_retrieval_request_cancel_selected_text = "Är du säker du vill återkalla utvalda beställningar?"
 Contents = "Innehåll"
@@ -375,6 +377,33 @@ hold_success = "Material har reserverats."
 Home = "Hem"
 home_browse = "Bläddring:"
 Identifier = "Identifier"
+ill_request_available = "Inväntar avhämtning"
+ill_request_canceled = "Ã…terkallad"
+ill_request_cancel = "Återkalla beställningen"
+ill_request_cancel_all = "Återkalla alla beställningar"
+ill_request_cancel_fail = "Beställningen kunde inte återkallas. Vänd dig till kundtjänst."
+ill_request_cancel_selected = "Återkalla utvalda beställningar"
+ill_request_cancel_success = "Beställningen har återkallats"
+ill_request_cancel_success_items = "beställning(ar) har återkallats"
+ill_request_check_text = "Kolla fjärrlånbeställning"
+ill_request_comments = "Tilläggsuppgifter"
+ill_request_date_invalid = "Mata i ett giltigt datum"
+ill_request_date_past = "Datumet måste vara i framtiden"
+ill_request_empty_selection = "Inga utvalda beställningar"
+ill_request_error_blocked = "Det är inte möjligt att placera en beställning eftersom du är i låneförbud eller redan har en likvärdig beställning."
+ill_request_error_fail = "Fjärrlånbeställning misslyckades. Vänd dig till bibliotekets kundtjänst."
+ill_request_error_technical = "Beställningen misslyckades. Vänd dig till bibliotekets kundtjänst."
+ill_request_error_unknown_patron_source = "Kundens bibliotek identifieras inte i begäran om fjärrlån."
+ill_request_invalid_pickup = "Avhämtningsplats duger inte. Kolla och försök igen."
+ill_request_pick_up_library = "Avhämtningsbibliotek"
+ill_request_pick_up_location = "Avhämtningsplats"
+ill_request_place_fail_missing = "Beställning misslyckades p.g.a. saknande uppgifter. Vänd dig till bibliotekets kundtjänst."
+ill_request_place_success = "Material har beställts."
+ill_request_place_text = "Fjärrlånbeställning"
+ill_request_processed = "Behandlad"
+ill_request_profile_html = ""Logga in med din <a href="%%url%%">bibliotekskort</a> för att se fjärrlånbeställningar."
+ill_request_submit_text = "Beställ"
+Interlibrary Loan Requests = "Fjärrlånbeställningar"
 Illustrated = "Illustrerad"
 ils_offline_holdings_message = "Holdings and item availability information is currently unavailable. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:"
 ils_offline_home_message = "Your account details and live item information will be unavailable during this time. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:"
@@ -828,6 +857,7 @@ Year of Publication = "Publiceringsår"
 Yesterday = "Från igår"
 You do not have any fines = "Du har inga obetalda avgifter"
 You do not have any holds or recalls placed = "Du har inga reserveringar i kraft"
+You do not have any interlibrary loan requests placed = "Du har inga fjärrlånbeställningar i kraft"
 You do not have any items checked out = "Du har inga lån"
 You do not have any saved resources = "Du har inga sparade resurser"
 You do not have any storage retrieval requests placed = "Du har inga lagerbeställningar i kraft"
diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index 5d8290a919a..f79eb975e7c 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -102,6 +102,7 @@ $config = array(
         'factories' => array(
             'holds' => array('VuFind\Controller\Plugin\Factory', 'getHolds'),
             'storageRetrievalRequests' => array('VuFind\Controller\Plugin\Factory', 'getStorageRetrievalRequests'),
+            'ILLRequests' => array('VuFind\Controller\Plugin\Factory', 'getILLRequests'),
             'reserves' => array('VuFind\Controller\Plugin\Factory', 'getReserves'),
         ),
         'invokables' => array(
@@ -537,7 +538,7 @@ $recordRoutes = array(
 $nonTabRecordActions = array(
     'AddComment', 'DeleteComment', 'AddTag', 'Save', 'Email', 'SMS', 'Cite',
     'Export', 'RDF', 'Hold', 'BlockedHold', 'Home', 'StorageRetrievalRequest',
-    'BlockedStorageRetrievalRequest'
+    'BlockedStorageRetrievalRequest', 'ILLRequest', 'BlockedILLRequest' 
 );
 
 // Define list-related routes -- route name => MyResearch action
@@ -563,6 +564,7 @@ $staticRoutes = array(
     'MyResearch/Favorites', 'MyResearch/Fines',
     'MyResearch/Holds', 'MyResearch/Home', 'MyResearch/Logout', 'MyResearch/Profile',
     'MyResearch/SaveSearch', 'MyResearch/StorageRetrievalRequests',
+    'MyResearch/ILLRequests',
     'QRCode/Show', 'QRCode/Unavailable',
     'OAI/Server', 'Pazpar2/Home', 'Pazpar2/Search', 'Records/Home',
     'Search/Advanced', 'Search/Email', 'Search/History', 'Search/Home',
diff --git a/module/VuFind/src/VuFind/Controller/AbstractBase.php b/module/VuFind/src/VuFind/Controller/AbstractBase.php
index 968468d1c08..c91f2538556 100644
--- a/module/VuFind/src/VuFind/Controller/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Controller/AbstractBase.php
@@ -374,15 +374,24 @@ class AbstractBase extends AbstractActionController
     /**
      * Translate a string if a translator is available.
      *
-     * @param string $msg Message to translate
+     * @param string $msg     Message to translate
+     * @param string $default Default value to use if no translation is found (null
+     * for no default).
      *
      * @return string
      */
-    public function translate($msg)
+    public function translate($msg, $default = null)
     {
-        return $this->getServiceLocator()->has('VuFind\Translator')
+        $translated = $this->getServiceLocator()->has('VuFind\Translator')
             ? $this->getServiceLocator()->get('VuFind\Translator')->translate($msg)
             : $msg;
+        
+        // Did the translation fail to change anything?  If so, use default:
+        if (!is_null($default) && $translated == $msg) {
+            $translated = $default;
+        }
+        
+        return $translated;
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Controller/AjaxController.php b/module/VuFind/src/VuFind/Controller/AjaxController.php
index 015dc5c2259..36f13b10917 100644
--- a/module/VuFind/src/VuFind/Controller/AjaxController.php
+++ b/module/VuFind/src/VuFind/Controller/AjaxController.php
@@ -1087,6 +1087,19 @@ class AjaxController extends AbstractBase
                 $patron = $this->getAuthManager()->storedCatalogLogin();
                 if ($patron) {
                     switch ($requestType) {
+                    case 'ILLRequest':
+                        $results = $catalog->checkILLRequestIsValid(
+                            $id, $data, $patron
+                        );
+    
+                        $msg = $results
+                            ? $this->translate(
+                                'ill_request_place_text'
+                            )
+                            : $this->translate(
+                                'ill_request_error_blocked'
+                            );
+                        break;
                     case 'StorageRetrievalRequest':
                         $results = $catalog->checkStorageRetrievalRequestIsValid(
                             $id, $data, $patron
@@ -1348,6 +1361,63 @@ class AjaxController extends AbstractBase
         return $this->output($html, self::STATUS_OK);
     }
 
+    /**
+     * Get pick up locations for a library
+     *
+     * @return \Zend\Http\Response
+     */
+    protected function getLibraryPickupLocationsAjax()
+    {
+        $this->writeSession();  // avoid session write timing bug
+        $id = $this->params()->fromQuery('id');
+        $pickupLib = $this->params()->fromQuery('pickupLib');
+        if (!empty($id) && !empty($pickupLib)) {
+            // check if user is logged in
+            $user = $this->getUser();
+            if (!$user) {
+                return $this->output(
+                    array(
+                        'status' => false, 
+                        'msg' => $this->translate('You must be logged in first')
+                    ),
+                    self::STATUS_NEED_AUTH
+                );
+            }
+
+            try {
+                $catalog = $this->getILS();
+                $patron = $this->getAuthManager()->storedCatalogLogin();
+                if ($patron) {
+                    $params = array(
+                        'id' => $id,
+                        'pickupLib' => $pickupLib,
+                        'patron' => $patron
+                    );
+                    $results = $catalog->getILLPickupLocations(
+                        $id, $pickupLib, $patron
+                    );
+                    foreach ($results as &$result) {
+                        if (isset($result['name'])) {
+                            $result['name'] = $this->translate(
+                                'location_' . $result['name'], 
+                                $result['name']
+                            );
+                        }
+                    }
+                    return $this->output(
+                        array('locations' => $results), self::STATUS_OK
+                    );
+                }
+            } catch (\Exception $e) {
+                // Do nothing -- just fail through to the error message below.
+            }
+        }
+
+        return $this->output(
+            $this->translate('An error has occurred'), self::STATUS_ERROR
+        );
+    }
+
     /**
      * Convenience method for accessing results
      *
diff --git a/module/VuFind/src/VuFind/Controller/MyResearchController.php b/module/VuFind/src/VuFind/Controller/MyResearchController.php
index d747fecffa8..5690f10d47a 100644
--- a/module/VuFind/src/VuFind/Controller/MyResearchController.php
+++ b/module/VuFind/src/VuFind/Controller/MyResearchController.php
@@ -930,6 +930,62 @@ class MyResearchController extends AbstractBase
         return $view;
     }
 
+    /**
+     * Send list of ill requests to view
+     *
+     * @return mixed
+     */
+    public function illRequestsAction()
+    {
+        // Stop now if the user does not have valid catalog credentials available:
+        if (!is_array($patron = $this->catalogLogin())) {
+            return $patron;
+        }
+
+        // Connect to the ILS:
+        $catalog = $this->getILS();
+
+        // Process cancel requests if necessary:
+        $cancelStatus = $catalog->checkFunction('cancelILLRequests');
+        $view = $this->createViewModel();
+        $view->cancelResults = $cancelStatus
+            ? $this->ILLRequests()->cancelILLRequests(
+                $catalog, $patron
+            )
+            : array();
+        // If we need to confirm
+        if (!is_array($view->cancelResults)) {
+            return $view->cancelResults;
+        }
+
+        // By default, assume we will not need to display a cancel form:
+        $view->cancelForm = false;
+
+        // Get request details:
+        $result = $catalog->getMyILLRequests($patron);
+        $recordList = array();
+        $this->ILLRequests()->resetValidation();
+        foreach ($result as $current) {
+            // Add cancel details if appropriate:
+            $current = $this->ILLRequests()->addCancelDetails(
+                $catalog, $current, $cancelStatus, $patron
+            );
+            if ($cancelStatus 
+                && $cancelStatus['function'] != "getCancelILLRequestLink"
+                && isset($current['cancel_details'])
+            ) {
+                // Enable cancel form if necessary:
+                $view->cancelForm = true;
+            }
+
+            // Build record driver:
+            $recordList[] = $this->getDriverForILSRecord($current);
+        }
+
+        $view->recordList = $recordList;
+        return $view;
+    }
+
     /**
      * Send list of checked out books to view
      *
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Factory.php b/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
index aec0729b626..68a1fa8750d 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
@@ -65,6 +65,20 @@ class Factory
         );
     }
 
+    /**
+     * Construct the ILLRequests plugin.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return ILLRequests
+     */
+    public static function getILLRequests(ServiceManager $sm)
+    {
+        return new ILLRequests(
+            $sm->getServiceLocator()->get('VuFind\HMAC')
+        );
+    }
+
     /**
      * Construct the Reserves plugin.
      *
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php b/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php
new file mode 100644
index 00000000000..2754027fb89
--- /dev/null
+++ b/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php
@@ -0,0 +1,195 @@
+<?php
+/**
+ * VuFind Action Helper - ILL Requests Support Methods
+ *
+ * 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  Controller_Plugins
+ * @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://www.vufind.org  Main Page
+ */
+namespace VuFind\Controller\Plugin;
+use Zend\Mvc\Controller\Plugin\AbstractPlugin, Zend\Session\Container;
+
+/**
+ * Zend action helper to perform ILL request related actions
+ *
+ * @category VuFind2
+ * @package  Controller_Plugins
+ * @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://www.vufind.org  Main Page
+ */
+class ILLRequests extends Holds
+{
+    /**
+     * Grab the Container object for storing helper-specific session
+     * data.
+     *
+     * @return Container
+     */
+    protected function getSession()
+    {
+        if (!isset($this->session)) {
+            $this->session = new Container('ILLRequests_Helper');
+        }
+        return $this->session;
+    }
+
+    /**
+     * Update ILS details with cancellation-specific information, if appropriate.
+     *
+     * @param \VuFind\ILS\Connection $catalog      ILS connection object
+     * @param array                  $ilsDetails   Details from ILS driver's
+     * getMyILLRequests() method
+     * @param array                  $cancelStatus Cancellation settings from ILS
+     * @param array                  $patron       ILS patron 
+     * driver's checkFunction() method
+     *
+     * @return array $ilsDetails with cancellation info added
+     */
+    public function addCancelDetails($catalog, $ilsDetails, $cancelStatus, $patron)
+    {
+        // Generate form details for cancelling requests if enabled
+        if ($cancelStatus) {
+            if ($cancelStatus['function'] == 'getCancelILLRequestsLink'
+            ) {
+                // Build OPAC URL
+                $ilsDetails['cancel_link']
+                    = $catalog->getCancelILLRequestLink(
+                        $ilsDetails,
+                        $patron
+                    );
+            } else {
+                // Form Details
+                $ilsDetails['cancel_details']
+                    = $catalog->getCancelILLRequestDetails(
+                        $ilsDetails,
+                        $patron
+                    );
+                $this->rememberValidId(
+                    $ilsDetails['cancel_details']
+                );
+            }
+        }
+
+        return $ilsDetails;
+    }
+
+    /**
+     * Process cancel request.
+     *
+     * @param \VuFind\ILS\Connection $catalog ILS connection object
+     * @param array                  $patron  Current logged in patron
+     *
+     * @return array                          The result of the cancellation, an
+     * associative array keyed by item ID (empty if no cancellations performed)
+     */
+    public function cancelILLRequests($catalog, $patron)
+    {
+        // Retrieve the flashMessenger helper:
+        $flashMsg = $this->getController()->flashMessenger();
+        $params = $this->getController()->params();
+
+        // Pick IDs to cancel based on which button was pressed:
+        $all = $params->fromPost('cancelAll');
+        $selected = $params->fromPost('cancelSelected');
+        if (!empty($all)) {
+            $details = $params->fromPost('cancelAllIDS');
+        } else if (!empty($selected)) {
+            $details = $params->fromPost('cancelSelectedIDS');
+        } else {
+            // No button pushed -- no action needed
+            return array();
+        }
+
+        if (!empty($details)) {
+            // Confirm?
+            if ($params->fromPost('confirm') === "0") {
+                $url = $this->getController()->url()
+                    ->fromRoute('myresearch-illrequests');
+                if ($params->fromPost('cancelAll') !== null) {
+                    return $this->getController()->confirm(
+                        'ill_request_cancel_all',
+                        $url,
+                        $url,
+                        'confirm_ill_request_cancel_all_text',
+                        array(
+                            'cancelAll' => 1,
+                            'cancelAllIDS' => $params->fromPost('cancelAllIDS')
+                        )
+                    );
+                } else {
+                    return $this->getController()->confirm(
+                        'ill_request_cancel_selected',
+                        $url,
+                        $url,
+                        'confirm_ill_request_cancel_selected_text',
+                        array(
+                            'cancelSelected' => 1,
+                            'cancelSelectedIDS' =>
+                                $params->fromPost('cancelSelectedIDS')
+                        )
+                    );
+                }
+            }
+            
+            foreach ($details as $info) {
+                // If the user input contains a value not found in the session
+                // whitelist, something has been tampered with -- abort the process.
+                if (!in_array($info, $this->getSession()->validIds)) {
+                    $flashMsg->setNamespace('error')
+                        ->addMessage('error_inconsistent_parameters');
+                    return array();
+                }
+            }
+
+            // Add Patron Data to Submitted Data
+            $cancelResults = $catalog->cancelILLRequests(
+                array('details' => $details, 'patron' => $patron)
+            );
+            if ($cancelResults == false) {
+                $flashMsg->setNamespace('error')->addMessage(
+                    'ill_request_cancel_fail'
+                );
+            } else {
+                if ($cancelResults['count'] > 0) {
+                    // TODO : add a mechanism for inserting tokens into translated
+                    // messages so we can avoid a double translation here.
+                    $msg = $this->getController()->translate(
+                        'ill_request_cancel_success_items'
+                    );
+                    $flashMsg->setNamespace('info')->addMessage(
+                        $cancelResults['count'] . ' ' . $msg
+                    );
+                }
+                return $cancelResults;
+            }
+        } else {
+            $flashMsg->setNamespace('error')->addMessage(
+                'ill_request_empty_selection'
+            );
+        }
+        return array();
+    }
+}
diff --git a/module/VuFind/src/VuFind/Controller/RecordController.php b/module/VuFind/src/VuFind/Controller/RecordController.php
index e2c428db40b..4fcf9229151 100644
--- a/module/VuFind/src/VuFind/Controller/RecordController.php
+++ b/module/VuFind/src/VuFind/Controller/RecordController.php
@@ -77,6 +77,18 @@ class RecordController extends AbstractRecord
         return $this->redirectToRecord('#top');
     }
 
+    /**
+     * Action for dealing with blocked ILL requests.
+     *
+     * @return mixed
+     */
+    public function blockedILLRequestAction()
+    {
+        $this->flashMessenger()->setNamespace('error')
+            ->addMessage('ill_request_error_blocked');
+        return $this->redirectToRecord('#top');
+    }
+
     /**
      * Action for dealing with holds.
      *
@@ -289,6 +301,116 @@ class RecordController extends AbstractRecord
         );
     }
 
+    /**
+     * Action for dealing with ILL requests.
+     *
+     * @return mixed
+     */
+    public function illRequestAction()
+    {
+        $driver = $this->loadRecord();
+        
+        // If we're not supposed to be here, give up now!
+        $catalog = $this->getILS();
+        $checkRequests = $catalog->checkFunction(
+            'ILLRequests', 
+            $driver->getUniqueID()
+        );
+        if (!$checkRequests) {
+            return $this->forwardTo('Record', 'Home');
+        }
+
+        // Stop now if the user does not have valid catalog credentials available:
+        if (!is_array($patron = $this->catalogLogin())) {
+            return $patron;
+        }
+
+        // Do we have valid information?
+        // Sets $this->logonURL and $this->gatheredDetails
+        $gatheredDetails = $this->ILLRequests()->validateRequest(
+            $checkRequests['HMACKeys']
+        );
+        if (!$gatheredDetails) {
+            return $this->redirectToRecord();
+        }
+
+        // Block invalid requests:
+        if (!$catalog->checkILLRequestIsValid(
+            $driver->getUniqueID(), $gatheredDetails, $patron
+        )) {
+            return $this->blockedILLRequestAction();
+        }
+
+        // Send various values to the view so we can build the form:
+        
+        $extraFields = isset($checkRequests['extraFields'])
+            ? explode(":", $checkRequests['extraFields']) : array();
+
+        // Process form submissions if necessary:
+        if (!is_null($this->params()->fromPost('placeILLRequest'))) {
+            // If we made it this far, we're ready to place the hold;
+            // if successful, we will redirect and can stop here.
+
+            // Add Patron Data to Submitted Data
+            $details = $gatheredDetails + array('patron' => $patron);
+
+            // Attempt to place the hold:
+            $function = (string)$checkRequests['function'];
+            $results = $catalog->$function($details);
+
+            // Success: Go to Display Storage Retrieval Requests
+            if (isset($results['success']) && $results['success'] == true) {
+                $this->flashMessenger()->setNamespace('info')
+                    ->addMessage('ill_request_place_success');
+                if ($this->inLightbox()) {
+                    return false;
+                }
+                return $this->redirect()->toRoute(
+                    'myresearch-illrequests'
+                );
+            } else {
+                // Failure: use flash messenger to display messages, stay on
+                // the current form.
+                if (isset($results['status'])) {
+                    $this->flashMessenger()->setNamespace('error')
+                        ->addMessage($results['status']);
+                }
+                if (isset($results['sysMessage'])) {
+                    $this->flashMessenger()->setNamespace('error')
+                        ->addMessage($results['sysMessage']);
+                }
+            }
+        }
+
+        // Find and format the default required date:
+        $defaultRequired = $this->ILLRequests()
+            ->getDefaultRequiredDate($checkRequests);
+        $defaultRequired = $this->getServiceLocator()->get('VuFind\DateConverter')
+            ->convertToDisplayDate("U", $defaultRequired);
+
+        // Get pickup libraries
+        $pickupLibraries = $catalog->getILLPickUpLibraries(
+            $driver->getUniqueID(), $patron, $gatheredDetails
+        );
+
+        // Get pickup locations. Note that these are independent of pickup library,
+        // and library specific locations must be retrieved when a library is 
+        // selected.
+        $pickupLocations = $catalog->getPickUpLocations($patron, $gatheredDetails);
+        
+        return $this->createViewModel(
+            array(
+                'gatheredDetails' => $gatheredDetails,
+                'pickupLibraries' => $pickupLibraries,
+                'pickupLocations' => $pickupLocations,
+                'homeLibrary' => $this->getUser()->home_library,
+                'extraFields' => $extraFields,
+                'defaultRequiredDate' => $defaultRequired,
+                'helpText' => $checkRequests['helpText']
+            )
+        );
+    }
+
     /**
      * Is the result scroller active?
      *
diff --git a/module/VuFind/src/VuFind/ILS/Connection.php b/module/VuFind/src/VuFind/ILS/Connection.php
index b327cbaaf96..9ea0d0cae86 100644
--- a/module/VuFind/src/VuFind/ILS/Connection.php
+++ b/module/VuFind/src/VuFind/ILS/Connection.php
@@ -416,6 +416,72 @@ class Connection implements TranslatorAwareInterface
         return $response;
     }
 
+    /**
+     * Check ILL Request
+     *
+     * A support method for checkFunction(). This is responsible for checking
+     * the driver configuration to determine if the system supports storage
+     * retrieval requests.
+     *
+     * @param string $functionConfig The ILL request configuration values
+     *
+     * @return mixed On success, an associative array with specific function keys
+     * and values either for placing requests via a form; on failure, false.
+     */
+    protected function checkMethodILLRequests($functionConfig)
+    {
+        $response = false;
+
+        if ($this->checkCapability('placeILLRequest')
+            && isset($functionConfig['HMACKeys'])
+        ) {
+            $response = array('function' => 'placeILLRequest');
+            $response['HMACKeys'] = explode(':', $functionConfig['HMACKeys']);
+            if (isset($functionConfig['extraFields'])) {
+                $response['extraFields'] = $functionConfig['extraFields'];
+            }
+            if (isset($functionConfig['helpText'])) {
+                $response['helpText'] = $this->getHelpText(
+                    $functionConfig['helpText']
+                );  
+            }
+        }
+        return $response;
+    }
+    
+    /**
+     * Check Cancel ILL Requests
+     *
+     * A support method for checkFunction(). This is responsible for checking
+     * the driver configuration to determine if the system supports Cancelling 
+     * ILL Requests.
+     *
+     * @param string $functionConfig The Cancel function configuration values
+     *
+     * @return mixed On success, an associative array with specific function keys
+     * and values either for cancelling requests via a form or a URL;
+     * on failure, false.
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    protected function checkMethodcancelILLRequests($functionConfig)
+    {
+        $response = false;
+
+        if (isset($this->config->cancel_ill_requests_enabled)
+            && $this->config->cancel_ill_requests_enabled
+        ) {
+            if ($this->checkCapability('cancelILLRequests')) {
+                $response = array('function' => 'cancelILLRequests');
+            } elseif ($this->checkCapability('getCancelILLRequestLink')
+            ) {
+                $response = array(
+                    'function' => 'getCancelILLRequestLink'
+                );
+            }
+        }
+        return $response;
+    }
+    
     /**
      * Get proper help text from the function config
      *
@@ -484,6 +550,30 @@ class Connection implements TranslatorAwareInterface
         return false;
     }
 
+    /**
+     * Check ILL Request is Valid
+     *
+     * This is responsible for checking if an ILL request is valid
+     *
+     * @param string $id     A Bibliographic ID
+     * @param array  $data   Collected Holds Data
+     * @param array  $patron Patron related data
+     *
+     * @return mixed The result of the checkILLRequestIsValid 
+     * function if it exists, false if it does not
+     */
+    public function checkILLRequestIsValid($id, $data, $patron)
+    {
+        if ($this->checkCapability('checkILLRequestIsValid')) {
+            return $this->getDriver()->checkILLRequestIsValid(
+                $id, $data, $patron
+            );
+        }
+        // If the driver has no checkILLRequestIsValid method, we 
+        // will assume that the request is not valid
+        return false;
+    }
+    
     /**
      * Get Holds Mode
      *
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Demo.php b/module/VuFind/src/VuFind/ILS/Driver/Demo.php
index 26b0f196909..3f6f4397a3f 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Demo.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Demo.php
@@ -84,6 +84,13 @@ class Demo extends AbstractBase
      */
     protected $storageRetrievalRequests = true;
 
+    /**
+     * Should we support ILLRequests?
+     *
+     * @var bool
+     */
+    protected $ILLRequests = true;
+    
     /**
      * Date converter object
      *
@@ -122,6 +129,10 @@ class Demo extends AbstractBase
             $this->storageRetrievalRequests
                 = $this->config['Catalog']['storageRetrievalRequests'];
         }
+        if (isset($this->config['Catalog']['ILLRequests'])) {
+            $this->ILLRequests = $this->config['Catalog']['ILLRequests'];
+        }
+        
         // Establish a namespace in the session for persisting fake data (to save
         // on Solr hits):
         $this->session = new SessionContainer('DemoDriver');
@@ -234,15 +245,20 @@ class Demo extends AbstractBase
             'addLink'      => $patron ? rand()%10 == 0 ? 'block' : true : false,
             'storageRetrievalRequest' => 'auto',
             'addStorageRetrievalRequestLink' => $patron
+                ? rand()%10 == 0 ? 'block' : 'check'
+                : false,
+            'ILLRequest'   => 'auto',
+            'addILLRequestLink' => $patron
                 ? rand()%10 == 0 ? 'block' : 'check'
                 : false
         );
     }
 
     /**
-     * Generate a list of holds or storage retrieval requests.
+     * Generate a list of holds, storage retrieval requests or ILL requests.
      * 
-     * @param string $requestType Request type (Holds or StorageRetrievalRequests)
+     * @param string $requestType Request type (Holds, StorageRetrievalRequests or
+     * ILLRequests)
      * 
      * @return ArrayObject List of requests
      */
@@ -624,6 +640,24 @@ class Demo extends AbstractBase
         return $this->session->storageRetrievalRequests;
     }
     
+    /**
+     * Get Patron ILL Requests
+     *
+     * This is responsible for retrieving all ILL requests by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return mixed        Array of the patron's ILL requests
+     */
+    public function getMyILLRequests($patron)
+    {
+        if (!isset($this->session->ILLRequests)) {
+            $this->session->ILLRequests
+                = $this->createRequestList('ILLRequests');
+        }
+        return $this->session->ILLRequests;
+    }
+    
     /**
      * Get Patron Transactions
      *
@@ -1258,6 +1292,239 @@ class Demo extends AbstractBase
         return array('success' => true);
     }
 
+    /**
+     * Check if ILL request available
+     *
+     * This is responsible for determining if an item is requestable
+     *
+     * @param string $id     The Bib ID
+     * @param array  $data   An Array of item data
+     * @param patron $patron An array of patron data
+     *
+     * @return string True if request is valid, false if not
+     */
+    public function checkILLRequestIsValid($id, $data, $patron)
+    {
+        if (!$this->ILLRequests || rand() % 10 == 0) {
+            return false;
+        }
+        return true;
+    }
+    
+    /**
+     * Place ILL Request
+     *
+     * Attempts to place an ILL request on a particular item and returns
+     * an array with result details
+     *
+     * @param array $details An array of item and patron data
+     *
+     * @return mixed An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function placeILLRequest($details)
+    {
+        if (!$this->ILLRequests) {
+            return array(
+                "success" => false,
+                "sysMessage" => 'ILL requests are disabled.'
+            );
+        }
+        // Simulate failure:
+        if (rand() % 2) {
+            return array(
+                "success" => false,
+                "sysMessage" =>
+                    'Demonstrating failure; keep trying and ' .
+                    'it will work eventually.'
+            );
+        }
+
+        if (!isset($this->session->ILLRequests)) {
+            $this->session->ILLRequests = new ArrayObject();
+        }
+        $lastRequest = count($this->session->ILLRequests) - 1;
+        $nextId = $lastRequest >= 0
+            ? $this->session->ILLRequests[$lastRequest]['item_id'] + 1 
+            : 0;
+        
+        // Figure out appropriate expiration date:
+        if (!isset($holdDetails['requiredBy'])
+            || empty($holdDetails['requiredBy'])
+        ) {
+            $expire = strtotime("now + 30 days");
+        } else {
+            try {
+                $expire = $this->dateConverter->convertFromDisplayDate(
+                    "U", $details['requiredBy']
+                );
+            } catch (DateException $e) {
+                // Hold Date is invalid
+                return array(
+                    'success' => false,
+                    'sysMessage' => 'ill_request_date_invalid'
+                );
+            }
+        }
+        if ($expire <= time()) {
+            return array(
+                'success' => false,
+                'sysMessage' => 'ill_request_date_past'
+            );
+        }
+        
+        $this->session->ILLRequests->append(
+            array(
+                "id"       => $details['id'],
+                "location" => $details['pickUpLocation'],
+                "expire"   => date("j-M-y", $expire),
+                "create"  => date("j-M-y"),
+                "processed" => rand()%3 == 0 ? date("j-M-y", $expire) : '',
+                "reqnum"   => sprintf("%06d", $nextId),
+                "item_id"  => $nextId
+            )
+        );
+
+        return array('success' => true);
+    }
+    
+    /**
+     * Get ILL Pickup Libraries
+     *
+     * This is responsible for getting information on the possible pickup libraries
+     *
+     * @param string $id     Record ID
+     * @param array  $patron Patron
+     *
+     * @return bool|array False if request not allowed, or an array of associative 
+     * arrays with libraries.
+     */
+    public function getILLPickupLibraries($id, $patron)
+    {
+        if (!$this->ILLRequests) {
+            return false;
+        }
+        
+        $details = array(
+            array(
+                'id' => 1,
+                'name' => 'Main Library',
+                'isDefault' => true
+            ),                
+            array(
+                'id' => 2,
+                'name' => 'Branch Library',
+                'isDefault' => false
+            )                
+        );
+        
+        return $details;
+    }
+    
+    /**
+     * Get ILL Pickup Locations
+     * 
+     * This is responsible for getting a list of possible pickup locations for a 
+     * library
+     *
+     * @param string $id        Record ID
+     * @param string $pickupLib Pickup library ID
+     * @param array  $patron    Patron
+     *
+     * @return boo|array False if request not allowed, or an array of  
+     * locations.
+     */
+    public function getILLPickupLocations($id, $pickupLib, $patron)
+    {
+        switch ($pickupLib) {
+        case 1:
+            return array(
+                array(
+                    'id' => 1,
+                    'name' => 'Circulation Desk',
+                    'isDefault' => true
+                ),                
+                array(
+                    'id' => 2,
+                    'name' => 'Reference Desk',
+                    'isDefault' => false
+                )
+            );
+        case 2:
+            return array(
+                array(
+                    'id' => 3,
+                    'name' => 'Main Desk',
+                    'isDefault' => false
+                ),
+                array(
+                    'id' => 4,
+                    'name' => 'Library Bus',
+                    'isDefault' => true
+                )
+            );
+        }
+        return array();                
+    }
+
+    /**
+     * Cancel ILL Request
+     *
+     * Attempts to Cancel an ILL request on a particular item. The
+     * data in $cancelDetails['details'] is determined by
+     * getCancelILLRequestDetails().
+     *
+     * @param array $cancelDetails An array of item and patron data
+     *
+     * @return array               An array of data on each request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function cancelILLRequests($cancelDetails)
+    {
+        // Rewrite the items in the session, removing those the user wants to
+        // cancel.
+        $newRequests = new ArrayObject();
+        $retVal = array('count' => 0, 'items' => array());
+        foreach ($this->session->ILLRequests as $current) {
+            if (!in_array($current['reqnum'], $cancelDetails['details'])) {
+                $newRequests->append($current);
+            } else {
+                // 50% chance of cancel failure for testing purposes
+                if (rand() % 2) {
+                    $retVal['count']++;
+                    $retVal['items'][$current['item_id']] = array(
+                        'success' => true,
+                        'status' => 'ill_request_cancel_success'
+                    );
+                } else {
+                    $newRequests->append($current);
+                    $retVal['items'][$current['item_id']] = array(
+                        'success' => false,
+                        'status' => 'ill_request_cancel_fail',
+                        'sysMessage' =>
+                            'Demonstrating failure; keep trying and ' .
+                            'it will work eventually.'
+                    );
+                }
+            }
+        }
+
+        $this->session->ILLRequests = $newRequests;
+        return $retVal;
+    }
+
+    /**
+     * Get Cancel ILL Request Details
+     *
+     * @param array $details An array of item data
+     *
+     * @return string Data for use in a form field
+     */
+    public function getCancelILLRequestDetails($details)
+    {
+        return $details['reqnum'];
+    }
+    
     /**
      * Public Function which specifies renew, hold and cancel settings.
      *
@@ -1283,6 +1550,17 @@ class Demo extends AbstractBase
                     . ' with some <span style="color: red">styling</span>.'
             );
         }
+        if ($function == 'ILLRequests' && $this->ILLRequests) {
+            return array(
+                'enabled' => true,
+                'HMACKeys' => 'number',
+                'extraFields' => 
+                    'comments:pickUpLibrary:pickUpLibraryLocation:requiredByDate',
+                'defaultRequiredDate' => '0:1:0',
+                'helpText' => 'This is an ILL request help text'
+                    . ' with some <span style="color: red">styling</span>.'
+            );
+        }
         return array();
     }
 }
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
index bcec19174af..13979e17882 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
@@ -1625,7 +1625,7 @@ class Voyager extends AbstractBase
             'status' => utf8_encode($sqlRow['STATUS_DESC']),
             'statusDate' => $statusDate,
             'location' => $this->getLocationName($sqlRow['PICKUP_LOCATION_ID']),
-            'created' => $createDate,
+            'create' => $createDate,
             'processed' => $processedDate,
             'expire' => $expireDate,
             'reply' => utf8_encode($sqlRow['REPLY_NOTE']),
diff --git a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
index 73e312a5d3a..b3c203329b7 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
@@ -5,6 +5,7 @@
  * PHP version 5
  *
  * Copyright (C) Villanova University 2007.
+ * 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,
@@ -24,13 +25,15 @@
  * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
  * @author   Demian Katz <demian.katz@villanova.edu>
  * @author   Luke O'Sullivan <l.osullivan@swansea.ac.uk>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki
  */
 namespace VuFind\ILS\Driver;
 use PDO, PDOException, VuFind\Exception\Date as DateException,
-    VuFind\Exception\ILS as ILSException;
-
+    VuFind\Exception\ILS as ILSException,
+    Zend\Session\Container as SessionContainer;
+    
 /**
  * Voyager Restful ILS Driver
  *
@@ -39,6 +42,7 @@ use PDO, PDOException, VuFind\Exception\Date as DateException,
  * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
  * @author   Demian Katz <demian.katz@villanova.edu>
  * @author   Luke O'Sullivan <l.osullivan@swansea.ac.uk>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki
  */
@@ -136,6 +140,13 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
      */
     protected $titleHoldsMode;
 
+    /**
+     * Container for storing cached ILS data.
+     *
+     * @var SessionContainer
+     */
+    protected $session;
+    
     /**
      * Constructor
      *
@@ -149,6 +160,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         parent::__construct($dateConverter);
         $this->holdsMode = $holdsMode;
         $this->titleHoldsMode = $titleHoldsMode;
+        
     }
 
     /**
@@ -196,6 +208,9 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         $this->checkRenewalsUpFront
             = isset($this->config['Renewals']['checkUpFront'])
             ? $this->config['Renewals']['checkUpFront'] : true;
+        
+        // Establish a namespace in the session for persisting cached data
+        $this->session = new SessionContainer('VoyagerRestful_' . $this->dbName);
     }
 
     /**
@@ -216,6 +231,47 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         return $functionConfig;
     }
 
+    /**
+     * Helper function for fetching cached data.
+     * Data is cached for up to 30 seconds so that it would be faster to process
+     * e.g. requests where multiple calls to the backend are made.
+     * 
+     * @param string $id Cache entry id
+     * 
+     * @return mixed|null Cached entry or null if not cached or expired
+     */
+    protected function getCachedData($id)
+    {
+        if (isset($this->session->cache[$id])) {
+            $item = $this->session->cache[$id];
+            if (time() - $item['time'] > 30) {
+                return $item['entry'];
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * Helper function for storing cached data.
+     * Data is cached for up to 30 seconds so that it would be faster to process
+     * e.g. requests where multiple calls to the backend are made.
+     * 
+     * @param string $id    Cache entry id
+     * @param mixed  $entry Entry to be cached
+     * 
+     * @return void
+     */
+    protected function putCachedData($id, $entry)
+    {
+        if (!isset($this->session->cache)) {
+            $this->session->cache = array();
+        }
+        $this->session->cache[$id] = array(
+            'time' => time(),
+            'entry' => $entry
+        );
+    }
+        
     /**
      * Support method for VuFind Hold Logic. Take an array of status strings
      * and determines whether or not an item is holdable based on the
@@ -293,6 +349,20 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         return true;
     }
     
+    /**
+     * Support method for VuFind ILL Logic. Take a holdings row array 
+     * and determine whether or not an ILL (UB) request is allowed.
+     *
+     * @param array $holdingsRow The holdings row to analyze.
+     *
+     * @return bool Whether an item is holdable
+     * @access protected
+     */
+    protected function isILLRequestAllowed($holdingsRow)
+    {
+        return true;
+    }
+    
     /**
      * Protected support method for getHolding.
      *
@@ -339,8 +409,11 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
             $is_borrowable = isset($row['_fullRow']['ITEM_TYPE_ID'])
                 ? $this->isBorrowable($row['_fullRow']['ITEM_TYPE_ID']) : false;
             $is_holdable = $this->isHoldable($row['_fullRow']['STATUS_ARRAY']);
-            $isStorageRetrievalRequestAllowed
-                = $this->isStorageRetrievalRequestAllowed($row);
+            $isStorageRetrievalRequestAllowed 
+                = isset($this->config['StorageRetrievalRequests'])
+                && $this->isStorageRetrievalRequestAllowed($row);
+            $isILLRequestAllowed = isset($this->config['ILLRequests'])
+                && $this->isILLRequestAllowed($row);
             // If the item cannot be borrowed or if the item is not holdable,
             // set is_holdable to false
             if (!$is_borrowable || !$is_holdable) {
@@ -393,13 +466,22 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
                 }
             }
             
+            $ILLRequest = '';
+            $addILLRequestLink = false;
+            if ($patron && $isILLRequestAllowed) {
+                $ILLRequest = 'auto';
+                $addILLRequestLink = 'check';
+            }
+            
             $holding[$i] += array(
                 'is_holdable' => $is_holdable,
                 'holdtype' => $holdType,
                 'addLink' => $addLink,
                 'level' => "copy",
                 'storageRetrievalRequest' => $storageRetrieval,
-                'addStorageRetrievalRequestLink' => $addStorageRetrievalLink
+                'addStorageRetrievalRequestLink' => $addStorageRetrievalLink,
+                'ILLRequest' => $ILLRequest,
+                'addILLRequestLink' => $addILLRequestLink
             );
             unset($holding[$i]['_fullRow']);
         }
@@ -445,6 +527,9 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
      */
     public function checkStorageRetrievalRequestIsValid($id, $data, $patron)
     {
+        if (!isset($this->config['StorageRetrievalRequests'])) {
+            return false;
+        }
         if ($this->checkAccountBlocks($patron['id'])) {
             return 'block';
         }
@@ -461,7 +546,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         }
         return true;
     }
-    
+
     /**
      * Determine Renewability
      *
@@ -660,6 +745,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         }
 
         // Add Params
+        $queryString = array();
         foreach ($params as $key => $param) {
             $queryString[] = urlencode($key). "=" . urlencode($param);
         }
@@ -711,6 +797,18 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         return $simpleXML;
     }
 
+    /**
+     * Encode a string for XML
+     * 
+     * @param string $string String to be encoded
+     * 
+     * @return string Encoded string
+     */
+    protected function encodeXML($string)
+    {
+        return htmlspecialchars($string, ENT_COMPAT, "UTF-8");
+    }
+    
     /**
      * Build Basic XML
      *
@@ -729,7 +827,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
 
             foreach ($nodes as $nodeName => $nodeValue) {
                 $xmlString .= "<" . $nodeName . ">";
-                $xmlString .= htmlspecialchars($nodeValue, ENT_COMPAT, "UTF-8");
+                $xmlString .= $this->encodeXML($nodeValue);
                 // Split out any attributes
                 $nodeName = strtok($nodeName, ' ');
                 $xmlString .= "</" . $nodeName . ">";
@@ -758,6 +856,12 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
      */
     protected function checkAccountBlocks($patronId)
     {
+        $cacheId = "blocks_$patronId";
+        $data = $this->getCachedData($cacheId);
+        if (!is_null($data)) {
+            return $data;
+        }
+        
         $blockReason = false;
 
         // Build Hierarchy
@@ -772,15 +876,15 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
             "view" => "full"
         );
 
-        $blocks = $this->makeRequest($hierarchy, $params);
+        $blockReason = array();
 
+        $blocks = $this->makeRequest($hierarchy, $params);
         if ($blocks) {
             $node = "reply-text";
             $reply = (string)$blocks->$node;
 
             // Valid Response
             if ($reply == "ok" && isset($blocks->blocks)) {
-                $blockReason = array();
                 foreach ($blocks->blocks->institution->borrowingBlock
                     as $borrowBlock
                 ) {
@@ -788,7 +892,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
                 }
             }
         }
-
+        $this->putCachedData($cacheId, $blockReason);
         return $blockReason;
     }
 
@@ -1133,6 +1237,91 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         );
     }
 
+    /**
+     * Get Patron Remote Holds
+     *
+     * This is responsible for retrieving all remote holds by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @throws DateException
+     * @throws ILSException
+     * @return array        Array of the patron's holds on success.
+     */
+    protected function getRemoteHolds($patron)
+    {
+        // Build Hierarchy
+        $hierarchy = array(
+            'patron' =>  $patron['id'],
+            'circulationActions' => 'requests',
+            'holds' => false
+        );
+
+        // Add Required Params
+        $params = array(
+            "patron_homedb" => $this->ws_patronHomeUbId,
+            "view" => "full"
+        );
+
+        $results = $this->makeRequest($hierarchy, $params);
+
+        if ($results === false) {
+            throw new ILSException('System error fetching remote holds');
+        }
+        
+        $replyCode = (string)$results->{'reply-code'};
+        if ($replyCode != 0 && $replyCode != 8) {
+            throw new ILSException('System error fetching remote holds');
+        }
+        $holds = array();
+        if (isset($results->holds->institution)) {
+            foreach ($results->holds->institution as $institution) {
+                // Only take remote holds
+                if ($institution == 'LOCAL') {
+                    continue;
+                }
+                
+                foreach ($institution->hold as $hold) {
+                    $item = $hold->requestItem;
+                    
+                    $holds[] = array(
+                        'id' => '',
+                        'type' => (string)$item->holdType,
+                        'location' => (string)$item->pickupLocation,
+                        'expire' => (string)$item->expiredDate 
+                            ? $this->dateFormat->convertToDisplayDate(
+                                'Y-m-d', (string)$item->expiredDate
+                            )
+                            : '',  
+                        // Looks like expired date shows creation date for
+                        // UB requests, but who knows
+                        'create' => (string)$item->expiredDate 
+                            ? $this->dateFormat->convertToDisplayDate(
+                                'Y-m-d', (string)$item->expiredDate
+                            )
+                            : '',  
+                        'position' => (string)$item->queuePosition,
+                        'available' => (string)$item->status == '2',
+                        'reqnum' => (string)$item->holdRecallId,
+                        'item_id' => (string)$item->itemId,
+                        'volume' => '',
+                        'publication_year' => '',
+                        'title' => (string)$item->itemTitle,
+                        'institution_id' => (string)$institution->attributes()->id,
+                        'institution_name' => (string)$item->dbName,
+                        'institution_dbkey' => (string)$item->dbKey,
+                        'in_transit' => (substr((string)$item->statusText, 0, 13) 
+                            == 'In transit to')
+                          ? substr((string)$item->statusText, 14) 
+                          : ''
+                    );
+                }
+            }
+        }
+        
+        return $holds;
+    }
+    
     /**
      * Place Hold
      *
@@ -1330,8 +1519,23 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
      */
     public function getMyStorageRetrievalRequests($patron)
     {
-        $requests = parent::getMyStorageRetrievalRequests($patron);
-        
+        $requests = array_merge(
+            parent::getMyStorageRetrievalRequests($patron),
+            $this->getRemoteCallSlips($patron)
+        );
+        return $requests;
+    }
+    
+    /**
+     * Get Patron Remote Storage Retrieval Requests (Call Slips). Gets remote
+     * callslips via the API.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return mixed        Array of the patron's storage retrieval requests.
+     */
+    protected function getRemoteCallSlips($patron)
+    {
         // Build Hierarchy
         $hierarchy = array(
             'patron' =>  $patron['id'],
@@ -1351,6 +1555,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         if ($replyCode != 0 && $replyCode != 8) {
             throw new Exception('System error fetching call slips');
         }
+        $requests = array();
         if (isset($results->callslips->institution)) {
             foreach ($results->callslips->institution as $institution) {
                 if ((string)$institution->attributes()->id == 'LOCAL') {
@@ -1370,7 +1575,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
                             : '',  
                         // Looks like expired date shows creation date for 
                         // call slip requests, but who knows
-                        'created' => (string)$item->expiredDate 
+                        'create' => (string)$item->expiredDate 
                             ? $this->dateFormat->convertToDisplayDate(
                                 'Y-m-d', (string)$item->expiredDate
                             )
@@ -1594,4 +1799,598 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
             . '|' . $details['reqnum'];
         return $details;
     }
+
+    /**
+     * A helper function that retrieves UB request details for ILL and caches them
+     * for a short while for faster access.
+     * 
+     * @param string $id     BIB id
+     * @param array  $patron Patron
+     * 
+     * @return boolean|array False if UB request is not available or an array
+     * of details on success
+     */
+    protected function getUBRequestDetails($id, $patron)
+    {
+        $requestId = "ub_{$id}_" . $patron['id'];
+        $data = $this->getCachedData($requestId);
+        if (!empty($data)) {
+            return $data;
+        }
+        
+        if (strstr($patron['id'], '.') === false) {
+            $this->debug(
+                "getUBRequestDetails: no prefix in patron id '{$patron['id']}'"
+            );
+            $this->putCachedData($requestId, false);
+            return false;
+        }
+        list($source, $patronId) = explode('.', $patron['id'], 2);
+        if (!isset($this->config['ILLRequestSources'][$source])) {
+            $this->debug("getUBRequestDetails: source '$source' unknown");
+            $this->putCachedData($requestId, false);
+            return false;
+        }
+
+        list($catSource, $catUsername) = explode('.', $patron['cat_username'], 2);
+        $patronId = $this->encodeXML($patronId);
+        $patronHomeUbId = $this->encodeXML(
+            $this->config['ILLRequestSources'][$source]
+        );
+        $lastname = $this->encodeXML($patron['lastname']);
+        $barcode = $this->encodeXML($catUsername);
+        $bibId = $this->encodeXML($id);
+        $bibDbName = $this->encodeXML($this->config['Catalog']['database']);
+        $localUbId = $this->encodeXML($this->ws_patronHomeUbId);
+        
+        // Call PatronRequestsService first to check that UB is an available request
+        // type. Additionally, this seems to be mandatory, as PatronRequestService 
+        // may fail otherwise.
+        $xml = <<<EOT
+<?xml version="1.0" encoding="UTF-8"?>
+<ser:serviceParameters 
+xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters">
+  <ser:parameters>
+    <ser:parameter key="bibId">
+      <ser:value>$bibId</ser:value>
+    </ser:parameter>
+    <ser:parameter key="bibDbCode">
+      <ser:value>LOCAL</ser:value>
+    </ser:parameter>
+  </ser:parameters>
+  <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$patronHomeUbId" 
+  patronId="$patronId">
+    <ser:authFactor type="B">$barcode</ser:authFactor>
+  </ser:patronIdentifier>
+</ser:serviceParameters>
+EOT;
+
+        $response = $this->makeRequest(
+            array('PatronRequestsService' => false), array(), 'POST', $xml
+        );
+        
+        if ($response === false) {
+            $this->session->UBDetails[$requestId] = array(
+                'time' => time(),
+                'data' => false
+            );
+            $this->putCachedData($requestId, false);
+            return false;
+        }
+        // Process
+        $response->registerXPathNamespace(
+            'ser', 'http://www.endinfosys.com/Voyager/serviceParameters'
+        );
+        $response->registerXPathNamespace(
+            'req', 'http://www.endinfosys.com/Voyager/requests'
+        );
+        foreach ($response->xpath('//ser:message') as $message) {
+            // Any message means a problem, right?
+            $this->putCachedData($requestId, false);
+            return false; 
+        }
+        $requestCount = count(
+            $response->xpath("//req:requestIdentifier[@requestCode='UB']")
+        ); 
+        if ($requestCount == 0) {
+            // UB request not available
+            $this->putCachedData($requestId, false);
+            return false;
+        }
+
+        $xml =  <<<EOT
+<?xml version="1.0" encoding="UTF-8"?>
+<ser:serviceParameters
+xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters">
+  <ser:parameters>
+    <ser:parameter key="bibId">
+      <ser:value>$bibId</ser:value>
+    </ser:parameter>
+    <ser:parameter key="bibDbCode">
+      <ser:value>LOCAL</ser:value>
+    </ser:parameter>
+    <ser:parameter key="bibDbName">
+      <ser:value>$bibDbName</ser:value>
+    </ser:parameter>
+    <ser:parameter key="requestCode">
+      <ser:value>UB</ser:value>
+    </ser:parameter>
+    <ser:parameter key="requestSiteId">
+      <ser:value>$localUbId</ser:value>
+    </ser:parameter>
+  </ser:parameters>
+  <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$patronHomeUbId" 
+  patronId="$patronId">
+    <ser:authFactor type="B">$barcode</ser:authFactor>
+  </ser:patronIdentifier>
+</ser:serviceParameters>               
+EOT;
+        
+        $response = $this->makeRequest(
+            array('PatronRequestService' => false), array(), 'POST', $xml
+        );
+        
+        if ($response === false) {
+            $this->putCachedData($requestId, false);
+            return false;
+        }
+        // Process
+        $response->registerXPathNamespace(
+            'ser', 'http://www.endinfosys.com/Voyager/serviceParameters'
+        );
+        $response->registerXPathNamespace(
+            'req', 'http://www.endinfosys.com/Voyager/requests'
+        );
+        foreach ($response->xpath('//ser:message') as $message) {
+            // Any message means a problem, right?
+            $this->putCachedData($requestId, false);
+            return false; 
+        }
+        $items = array();
+        $libraries = array();
+        $locations = array();
+        $requiredByDate = '';
+        foreach ($response->xpath('//req:field') as $field) {
+            switch ($field->attributes()->labelKey) {
+            case 'selectItem': 
+                foreach ($field->xpath('./req:select/req:option') as $option) {
+                    $items[] = array(
+                        'id' => (string)$option->attributes()->id, 
+                        'name' => (string)$option
+                    );
+                }
+                break;
+            case 'pickupLib': 
+                foreach ($field->xpath('./req:select/req:option') as $option) {
+                    $libraries[] = array(
+                        'id' => (string)$option->attributes()->id,
+                        'name' => (string)$option,
+                        'isDefault' => $option->attributes()->isDefault == 'Y'
+                    );
+                }
+                break;
+            case 'pickUpAt': 
+                foreach ($field->xpath('./req:select/req:option') as $option) {
+                    $locations[] = array(
+                        'id' => (string)$option->attributes()->id,
+                        'name' => (string)$option,
+                        'isDefault' => $option->attributes()->isDefault == 'Y'
+                    );
+                }
+                break;
+            case 'notNeededAfter':
+                $node = current($field->xpath('./req:text'));
+                $requiredByDate = $this->dateFormat->convertToDisplayDate(
+                    "Y-m-d H:i", (string)$node
+                );
+                break;
+            }
+        }
+        $results = array(
+            'items' => $items,
+            'libraries' => $libraries,
+            'locations' => $locations,
+            'requiredBy' => $requiredByDate
+        );
+        $this->putCachedData($requestId, $results);
+        return $results;
+    }
+    
+    /**
+     * checkILLRequestIsValid
+     *
+     * This is responsible for determining if an item is requestable
+     *
+     * @param string $id     The Bib ID
+     * @param array  $data   An Array of item data
+     * @param patron $patron An array of patron data
+     *
+     * @return string True if request is valid, false if not
+     */
+    public function checkILLRequestIsValid($id, $data, $patron)
+    {
+        if (!isset($this->config['ILLRequests'])) {
+            $this->debug('ILL Requests not configured');
+            return false;
+        }
+        
+        $level = isset($data['level']) ? $data['level'] : "copy";
+        $itemID = ($level != 'title' && isset($data['item_id']))
+            ? $data['item_id']
+            : false;
+        
+        if ($level == 'copy' && $itemID === false) {
+            $this->debug('Item ID missing');
+            return false;
+        }
+        
+        $results = $this->getUBRequestDetails($id, $patron);
+        if ($results === false) {
+            $this->debug('getUBRequestDetails returned false');
+            return false;
+        }
+        if ($level == 'copy') {
+            $found = false;
+            foreach ($results['items'] as $item) {
+                if ($item['id'] == "$itemID.$id") {
+                    $found = true;
+                    break;
+                }
+            }
+            if (!$found) {
+                $this->debug('Item not requestable');
+                return false;
+            }
+        }
+        
+        return true;
+    }
+    
+    /**
+     * Get ILL (UB) Pickup Libraries
+     *
+     * This is responsible for getting information on the possible pickup libraries
+     *
+     * @param string $id     Record ID
+     * @param array  $patron Patron
+     *
+     * @return bool|array False if request not allowed, or an array of associative 
+     * arrays with libraries.
+     */
+    public function getILLPickupLibraries($id, $patron)
+    {
+        if (!isset($this->config['ILLRequests'])) {
+            return false;
+        }
+
+        $results = $this->getUBRequestDetails($id, $patron);
+        if ($results === false) {
+            $this->debug('getUBRequestDetails returned false');
+            return false;
+        }
+        
+        return $results['libraries'];
+    }
+    
+    /**
+     * Get ILL (UB) Pickup Locations
+     * 
+     * This is responsible for getting a list of possible pickup locations for a 
+     * library
+     *
+     * @param string $id        Record ID
+     * @param string $pickupLib Pickup library ID
+     * @param array  $patron    Patron
+     *
+     * @return bool|array False if request not allowed, or an array of  
+     * locations.
+     */
+    public function getILLPickupLocations($id, $pickupLib, $patron)
+    {    
+        if (!isset($this->config['ILLRequests'])) {
+            return false;
+        }
+        
+        list($source, $patronId) = explode('.', $patron['id'], 2);
+        if (!isset($this->config['ILLRequestSources'][$source])) {
+            return $this->holdError('ill_request_unknown_patron_source');
+        }
+        
+        list($catSource, $catUsername) = explode('.', $patron['cat_username'], 2);
+        $patronId = $this->encodeXML($patronId);
+        $patronHomeUbId = $this->encodeXML(
+            $this->config['ILLRequestSources'][$source]
+        );
+        $lastname = $this->encodeXML($patron['lastname']);
+        $barcode = $this->encodeXML($catUsername);
+        $localUbId = $this->encodeXML($this->ws_patronHomeUbId);
+        $pickupLib = $this->encodeXML($pickupLib);
+        
+        $xml =  <<<EOT
+<?xml version="1.0" encoding="UTF-8"?>
+<ser:serviceParameters
+xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters">
+  <ser:parameters>
+    <ser:parameter key="pickupLibId">
+      <ser:value>$pickupLib</ser:value>
+    </ser:parameter>
+  </ser:parameters>
+  <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$patronHomeUbId"
+  patronId="$patronId">
+    <ser:authFactor type="B">$barcode</ser:authFactor>
+  </ser:patronIdentifier>
+</ser:serviceParameters>               
+EOT;
+        
+        $response = $this->makeRequest(
+            array('UBPickupLibService' => false), array(), 'POST', $xml
+        );
+        
+        if ($response === false) {
+            return new PEAR_Error('ill_request_error_technical');
+        }
+        // Process
+        $response->registerXPathNamespace(
+            'ser', 'http://www.endinfosys.com/Voyager/serviceParameters'
+        );
+        $response->registerXPathNamespace(
+            'req', 'http://www.endinfosys.com/Voyager/requests'
+        );
+        foreach ($response->xpath('//ser:message') as $message) {
+            // Any message means a problem, right?
+            return new PEAR_Error('ill_request_error_technical');
+        }
+        $locations = array();
+        foreach ($response->xpath('//req:location') as $location) {
+            $locations[] = array(
+                'id' => (string)$location->attributes()->id,
+                'name' => (string)$location,
+                'isDefault' => $location->attributes()->isDefault == 'Y'
+            );
+        }
+        return $locations;
+    }
+    
+    /**
+     * Place ILL (UB) Request
+     *
+     * Attempts to place an UB request on a particular item and returns
+     * an array with result details or a PEAR error on failure of support classes
+     *
+     * @param array $details An array of item and patron data
+     *
+     * @return mixed An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     * @access public
+     */
+    public function placeILLRequest($details)
+    {
+        $patron = $details['patron'];
+        list($source, $patronId) = explode('.', $patron['id'], 2);
+        if (!isset($this->config['ILLRequestSources'][$source])) {
+            return $this->holdError('ill_request_error_unknown_patron_source');
+        }
+                
+        list($catSource, $catUsername) = explode('.', $patron['cat_username'], 2);
+        $patronId = htmlspecialchars($patronId, ENT_COMPAT, 'UTF-8');
+        $patronHomeUbId = $this->encodeXML(
+            $this->config['ILLRequestSources'][$source]
+        );
+        $lastname = $this->encodeXML($patron['lastname']);
+        $ubId = $this->encodeXML($patronHomeUbId);
+        $barcode = $this->encodeXML($catUsername);
+        $pickupLocation = $this->encodeXML($details['pickUpLibraryLocation']);
+        $pickupLibrary = $this->encodeXML($details['pickUpLibrary']);
+        $itemId = $this->encodeXML($details['item_id'] . '.' . $details['id']);
+        $comment = $this->encodeXML(
+            isset($details['comment']) ? $details['comment'] : ''
+        );
+        $bibId = $this->encodeXML($details['id']);
+        $bibDbName = $this->encodeXML($this->config['Catalog']['database']);
+        $localUbId = $this->encodeXML($this->ws_patronHomeUbId);
+        
+        // Convert last interest date from Display Format to Voyager required format
+        try {
+            $lastInterestDate = $this->dateFormat->convertFromDisplayDate(
+                "Y-m-d", $details['requiredBy']
+            );
+        } catch (DateException $e) {
+            // Date is invalid
+            return $this->holdError("ill_request_date_invalid");
+        }
+        
+        // Attempt Request
+        $xml =  <<<EOT
+<?xml version="1.0" encoding="UTF-8"?>
+<ser:serviceParameters
+xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters">
+  <ser:parameters>
+    <ser:parameter key="bibId">
+      <ser:value>$bibId</ser:value>
+    </ser:parameter>
+    <ser:parameter key="bibDbCode">
+      <ser:value>LOCAL</ser:value>
+    </ser:parameter>
+    <ser:parameter key="bibDbName">
+      <ser:value>$bibDbName</ser:value>
+    </ser:parameter>
+    <ser:parameter key="Select_Library">
+      <ser:value>$localUbId</ser:value>
+    </ser:parameter>
+    <ser:parameter key="requestCode">
+      <ser:value>UB</ser:value>
+    </ser:parameter>
+    <ser:parameter key="requestSiteId">
+      <ser:value>$localUbId</ser:value>
+    </ser:parameter>
+    <ser:parameter key="itemId">
+      <ser:value>$itemId</ser:value>
+    </ser:parameter>
+    <ser:parameter key="Select_Pickup_Lib">
+      <ser:value>$pickupLibrary</ser:value>
+    </ser:parameter>
+    <ser:parameter key="PICK">
+      <ser:value>$pickupLocation</ser:value>
+    </ser:parameter>
+    <ser:parameter key="REQNNA">
+      <ser:value>$lastInterestDate</ser:value>
+    </ser:parameter>
+    <ser:parameter key="REQCOMMENTS">
+      <ser:value>$comment</ser:value>
+    </ser:parameter>
+  </ser:parameters>
+  <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$ubId"
+  patronId="$patronId">
+    <ser:authFactor type="B">$barcode</ser:authFactor>
+  </ser:patronIdentifier>
+</ser:serviceParameters>               
+EOT;
+        
+        $response = $this->makeRequest(
+            array('SendPatronRequestService' => false), array(), 'POST', $xml
+        );
+        
+        if ($response === false) {
+            return $this->holdError('ill_request_error_technical');
+        }
+        // Process
+        $response->registerXPathNamespace(
+            'ser', 'http://www.endinfosys.com/Voyager/serviceParameters'
+        );
+        $response->registerXPathNamespace(
+            'req', 'http://www.endinfosys.com/Voyager/requests'
+        );
+        foreach ($response->xpath('//ser:message') as $message) {
+            if ($message->attributes()->type == 'success') {
+                return array(
+                    'success' => true,
+                    'status' => 'ill_request_success'
+                );
+            }
+            if ($message->attributes()->type == 'system') {
+                return $this->holdError('ill_request_error_technical');
+            }
+        }
+
+        return $this->holdError('ill_request_error_blocked');
+    }
+
+    /**
+     * Get Patron ILL Requests
+     *
+     * This is responsible for retrieving all UB requests by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return mixed        Array of the patron's holds on success, PEAR_Error
+     * otherwise.
+     * @access public
+     */
+    public function getMyILLRequests($patron)
+    {
+        return array_merge(
+            $this->getRemoteHolds($patron),
+            $this->getRemoteCallSlips($patron)
+        );
+        
+        return $holds;        
+    }
+    
+    /**
+     * Cancel ILL (UB) Requests
+     *
+     * Attempts to Cancel an UB request on a particular item. The
+     * data in $cancelDetails['details'] is determined by 
+     * getCancelILLRequestDetails().
+     *
+     * @param array $cancelDetails An array of item and patron data
+     *
+     * @return array               An array of data on each request including
+     * whether or not it was successful and a system message (if available)
+     * @access public
+     */
+    public function cancelILLRequests($cancelDetails)
+    {
+        $details = $cancelDetails['details'];
+        $patron = $cancelDetails['patron'];
+        $count = 0;
+        $response = array();
+
+        foreach ($details as $cancelDetails) {
+            list($dbKey, $itemId, $type, $cancelCode) = explode("|", $cancelDetails);
+
+             // Create Rest API Cancel Key
+            $cancelID = ($dbKey ? $dbKey : $this->ws_dbKey) . "|" . $cancelCode;
+
+            // Build Hierarchy
+            $hierarchy = array(
+                "patron" => $patron['id'],
+                 "circulationActions" => 'requests'
+            );
+            // An UB request is 
+            if ($type == 'C') {
+                $hierarchy['callslips'] = $cancelID;
+            } else {
+                $hierarchy['holds'] = $cancelID;
+            }
+            
+            // Add Required Params
+            $params = array(
+                "patron_homedb" => $this->ws_patronHomeUbId,
+                "view" => "full"
+            );
+
+            // Get Data
+            $cancel = $this->makeRequest($hierarchy, $params, "DELETE");
+
+            if ($cancel) {
+
+                // Process Cancel
+                $cancel = $cancel->children();
+                $node = "reply-text";
+                $reply = (string)$cancel->$node;
+                $count = ($reply == "ok") ? $count+1 : $count;
+
+                $response[$itemId] = array(
+                    'success' => ($reply == "ok") ? true : false,
+                    'status' => ($reply == "ok")
+                        ? "ill_request_cancel_success" : "ill_request_cancel_fail",
+                    'sysMessage' => ($reply == "ok") ? false : $reply,
+                );
+
+            } else {
+                $response[$itemId] = array(
+                    'success' => false, 
+                    'status' => "ill_request_cancel_fail"
+                );
+            }
+        }
+        $result = array('count' => $count, 'items' => $response);
+        return $result;
+    }
+    
+    /**
+     * Get Cancel ILL (UB) Request Details
+     *
+     * In Voyager an UB request is either a call slip (pending delivery) or a hold
+     * (pending checkout). In order to cancel an UB request, Voyager requires the 
+     * patron details, an item ID, request type and a recall ID. This function 
+     * returns the information as a string separated by pipes, which is then 
+     * submitted as form data and extracted by the CancelILLRequests function.
+     *
+     * @param array $details An array of item data
+     *
+     * @return string Data for use in a form field
+     * @access public
+     */
+    public function getCancelILLRequestDetails($details)
+    {
+        $details = (isset($details['institution_dbkey']) 
+            ? $details['institution_dbkey'] 
+            : '')
+            . '|' . $details['item_id']
+            . '|' . $details['type']
+            . '|' . $details['reqnum'];
+        return $details;
+    }    
 }
diff --git a/module/VuFind/src/VuFind/ILS/Logic/Holds.php b/module/VuFind/src/VuFind/ILS/Logic/Holds.php
index 949e0e4fd67..ef9805753c8 100644
--- a/module/VuFind/src/VuFind/ILS/Logic/Holds.php
+++ b/module/VuFind/src/VuFind/ILS/Logic/Holds.php
@@ -178,6 +178,9 @@ class Holds
             } else {
                 $holdings = $this->generateHoldings($result, $mode);
             }
+            
+            $holdings = $this->processStorageRetrievalRequests($holdings, $id);
+            $holdings = $this->processILLRequests($holdings, $id);
         }
         return $this->formatHoldings($holdings);
     }
@@ -219,11 +222,6 @@ class Holds
             // Are holds allowed?
             $checkHolds = $this->catalog->checkFunction("Holds", $id);
 
-            // Are storage retrieval requests allowed?
-            $checkStorageRetrievalRequests = $this->catalog->checkFunction(
-                "StorageRetrievalRequests"
-            );
-
             foreach ($result as $copy) {
                 $show = !in_array($copy['location'], $this->hideHoldings);
                 if ($show) {
@@ -234,38 +232,15 @@ class Holds
                             // instead of the hold form:
                             $copy['link'] = $copy['addLink'] === 'block'
                                 ? $this->getBlockedDetails($copy)
-                                : $this->getHoldDetails(
-                                    $copy, $checkHolds['HMACKeys']
+                                : $this->getRequestDetails(
+                                    $copy, $checkHolds['HMACKeys'], 'Hold'
                                 );
                             // If we are unsure whether hold options are available,
                             // set a flag so we can check later via AJAX:
                             $copy['check'] = $copy['addLink'] == 'check';
                         }
                     }
-
-                    if ($checkStorageRetrievalRequests) {
-                        // Is this copy requestable
-                        if (isset($copy['addStorageRetrievalRequestLink'])
-                            && $copy['addStorageRetrievalRequestLink']
-                        ) {
-                            // If the request is blocked, link to an error page
-                            // instead of the hold form:
-                            $copy['storageRetrievalRequestLink']
-                                = $copy['addStorageRetrievalRequestLink'] === 'block'
-                                ? $this->getBlockedStorageRetrievalRequestDetails(
-                                    $copy
-                                )
-                                : $this->getStorageRetrievalRequestDetails(
-                                    $copy,
-                                    $checkStorageRetrievalRequests['HMACKeys']
-                                );
-                            // If we are unsure whether request options are
-                            // available, set a flag so we can check later via AJAX:
-                            $copy['checkStorageRetrievalRequest']
-                                = $copy['addStorageRetrievalRequestLink']
-                                    === 'check';
-                        }
-                    }
+                    
                     $holdings[$copy['location']][] = $copy;
                 }
             }
@@ -305,11 +280,6 @@ class Holds
             // Are holds allowed?
             $checkHolds = $this->catalog->checkFunction("Holds");
 
-            // Are storage retrieval requests allowed?
-            $checkStorageRetrievalRequests = $this->catalog->checkFunction(
-                "StorageRetrievalRequests"
-            );
-
             if ($checkHolds && is_array($holdings)) {
                 // Generate Links
                 // Loop through each holding
@@ -354,89 +324,137 @@ class Holds
                             } else {
                                 /* Build non-opac link */
                                 $holdings[$location_key][$copy_key]['link']
-                                    = $this->getHoldDetails(
-                                        $copy, $checkHolds['HMACKeys']
+                                    = $this->getRequestDetails(
+                                        $copy, $checkHolds['HMACKeys'], 'Hold'
                                     );
                             }
                         }
                     }
                 }
             }
-
-            if ($checkStorageRequests && is_array($holdings)) {
-                // Generate Links
-                // Loop through each holding
-                foreach ($holdings as $location_key => $location) {
-                    foreach ($location as $copy_key => $copy) {
-                        if (isset($copy['addStorageRetrievalRequestLink'])
-                            && $copy['addStorageRetrievalRequestLink']
-                            && $copy['addStorageRetrievalRequestLink'] !== 'block'
-                        ) {
-                            $copy['storageRetrievalRequestLink']
-                                = $this->getStorageRetrievalRequestDetails(
-                                    $copy,
-                                    $checkStorageRetrievalRequests['HMACKeys']
-                                );
-                            // If we are unsure whether storage retrieval
-                            // request is available, set a flag so we can check
-                            // later via AJAX:
-                            $copy['checkStorageRetrievalRequest']
-                                = $copy['addStorageRetrievalRequestLink'] ===
-                                'check';
-                        }
-                    }
-                }
-            }
         }
         return $holdings;
     }
 
     /**
-     * Get Hold Form
-     *
-     * Supplies holdLogic with the form details required to place a hold
+     * Process storage retrieval request information in holdings and set the links
+     * accordingly.
+     * 
+     * @param array $holdings Holdings
      *
-     * @param array $holdDetails An array of item data
-     * @param array $HMACKeys    An array of keys to hash
+     * @return array Modified holdings
+     */
+    protected function processStorageRetrievalRequests($holdings)
+    {
+        if (!is_array($holdings)) {
+            return $holdings;
+        }
+        
+        // Are storage retrieval requests allowed?
+        $requestConfig = $this->catalog->checkFunction(
+            'StorageRetrievalRequests'
+        );
+        
+        if (!$requestConfig) {
+            return $holdings;
+        }
+        
+        // Generate Links
+        // Loop through each holding
+        foreach ($holdings as &$location) {
+            foreach ($location as &$copy) {    
+                // Is this copy requestable
+                if (isset($copy['addStorageRetrievalRequestLink'])
+                    && $copy['addStorageRetrievalRequestLink']
+                ) {
+                    // If the request is blocked, link to an error page
+                    // instead of the form:
+                    if ($copy['addStorageRetrievalRequestLink'] === 'block') {
+                        $copy['storageRetrievalRequestLink'] 
+                            = $this->getBlockedStorageRetrievalRequestDetails($copy);
+                    } else {
+                        $copy['storageRetrievalRequestLink']
+                            = $this->getRequestDetails(
+                                $copy, 
+                                $requestConfig['HMACKeys'],
+                                'StorageRetrievalRequest'
+                            );
+                    }
+                    // If we are unsure whether request options are 
+                    // available, set a flag so we can check later via AJAX:
+                    $copy['checkStorageRetrievalRequest'] 
+                        = $copy['addStorageRetrievalRequestLink'] === 'check';
+                }             
+            }   
+        }
+        return $holdings; 
+    }
+    
+    /**
+     * Process ILL request information in holdings and set the links accordingly.
+     * 
+     * @param array $holdings Holdings
      *
-     * @return array             Details for generating URL
+     * @return array Modified holdings
      */
-    protected function getHoldDetails($holdDetails, $HMACKeys)
+    protected function processILLRequests($holdings)
     {
-        // Generate HMAC
-        $HMACkey = $this->hmac->generate($HMACKeys, $holdDetails);
-
-        // Add Params
-        foreach ($holdDetails as $key => $param) {
-            $needle = in_array($key, $HMACKeys);
-            if ($needle) {
-                $queryString[] = $key. "=" .urlencode($param);
-            }
+        if (!is_array($holdings)) {
+            return $holdings;
         }
-
-        // Add HMAC
-        $queryString[] = "hashKey=" . urlencode($HMACkey);
-        $queryString = implode('&', $queryString);
-
-        // Build Params
-        return array(
-            'action' => 'Hold', 'record' => $holdDetails['id'],
-            'query' => $queryString, 'anchor' => "#tabnav"
+        
+        // Are storage retrieval requests allowed?
+        $requestConfig = $this->catalog->checkFunction(
+            'ILLRequests'
         );
+        
+        if (!$requestConfig) {
+            return $holdings;
+        }
+        
+        // Generate Links
+        // Loop through each holding
+        foreach ($holdings as &$location) {
+            foreach ($location as &$copy) {    
+                // Is this copy requestable
+                if (isset($copy['addILLRequestLink'])
+                    && $copy['addILLRequestLink']
+                ) {
+                    // If the request is blocked, link to an error page
+                    // instead of the form:
+                    if ($copy['addILLRequestLink'] === 'block') {
+                        $copy['ILLRequestLink'] 
+                            = $this->getBlockedILLRequestDetails($copy);
+                    } else {
+                        $copy['ILLRequestLink']
+                            = $this->getRequestDetails(
+                                $copy, 
+                                $requestConfig['HMACKeys'],
+                                'ILLRequest'
+                            );
+                    }
+                    // If we are unsure whether request options are 
+                    // available, set a flag so we can check later via AJAX:
+                    $copy['checkILLRequest'] 
+                        = $copy['addILLRequestLink'] === 'check';
+                }             
+            }   
+        }
+        return $holdings; 
     }
-
+    
     /**
-     * Get Storage Retrieval Request Form
+     * Get Hold Form
      *
-     * Supplies holdLogic with the form details required to place a storage
-     * retrieval request
+     * Supplies holdLogic with the form details required to place a request
      *
-     * @param array $details  An array of item data
-     * @param array $HMACKeys An array of keys to hash
+     * @param array  $details  An array of item data
+     * @param array  $HMACKeys An array of keys to hash
+     * @param string $action   The action for which the details are built
      *
-     * @return array          Details for generating URL
+     * @return array             Details for generating URL
      */
-    protected function getStorageRetrievalRequestDetails($details, $HMACKeys)
+    protected function getRequestDetails($details, $HMACKeys, $action)
     {
         // Generate HMAC
         $HMACkey = $this->hmac->generate($HMACKeys, $details);
@@ -445,7 +463,7 @@ class Holds
         foreach ($details as $key => $param) {
             $needle = in_array($key, $HMACKeys);
             if ($needle) {
-                $queryString[] = $key . "=" . urlencode($param);
+                $queryString[] = $key. "=" .urlencode($param);
             }
         }
 
@@ -455,13 +473,11 @@ class Holds
 
         // Build Params
         return array(
-            'action' => 'StorageRetrievalRequest',
-            'record' => $details['id'],
-            'query' => $queryString,
-            'anchor' => "#tabnav"
+            'action' => $action, 'record' => $details['id'],
+            'query' => $queryString, 'anchor' => "#tabnav"
         );
     }
-
+    
     /**
      * Returns a URL to display a "blocked hold" message.
      *
@@ -493,6 +509,22 @@ class Holds
         );
     }
 
+    /**
+     * Returns a URL to display a "blocked ILL request" message.
+     *
+     * @param array $details An array of item data
+     *
+     * @return array         Details for generating URL
+     */
+    protected function getBlockedILLRequestDetails($details)
+    {
+        // Build Params
+        return array(
+            'action' => 'BlockedILLRequest',
+            'record' => $details['id']
+        );
+    }
+
     /**
      * Get an array of suppressed location names.
      *
diff --git a/themes/blueprint/css/styles.css b/themes/blueprint/css/styles.css
index d4ba5565e89..c1d15a3ba3d 100644
--- a/themes/blueprint/css/styles.css
+++ b/themes/blueprint/css/styles.css
@@ -91,7 +91,7 @@ div.dialogLoading {
     height:100%;
 }
 
-.ajax_availability, .ajax_hold_availability, .ajax_storage_retrieval_request_availability {
+.ajax_availability, .ajax_hold_availability, .ajax_storage_retrieval_request_availability, .ajax_ill_request_availability, .ajax_ill_request_loading {
     background: url(../images/ajax_loading.gif) no-repeat left top;
     padding:0 .5em .5em 20px;
 }
@@ -905,42 +905,21 @@ h3.list {
     padding:.5em .5em .5em 20px;
     margin-right:1em;
 }
-.holdPlace {
+.holdPlace, .storageRetrievalRequestPlace, .ILLRequestPlace {
     background-image:url(../images/fugue/holdPlace.png);
     background-repeat:no-repeat;
     background-position: left;
     padding:.5em .5em .5em 20px;
     margin-right:1em;
 }
-.holdCancel {
+.holdCancel, .storageRetrievalRequestCancel, .ILLRequestCancel {
     background-image:url(../images/fugue/holdCancel.png);
     background-repeat:no-repeat;
     background-position: left;
     padding:.5em .5em .5em 20px;
     margin-right:1em;
 }
-.holdCancelAll {
-    background-image:url(../images/fugue/holdCancelAll.png);
-    background-repeat:no-repeat;
-    background-position: left;
-    padding:.5em .5em .5em 20px;
-    margin-right:1em;
-}
-.storageRetrievalRequestPlace {
-    background-image:url(../images/fugue/holdPlace.png);
-    background-repeat:no-repeat;
-    background-position: left;
-    padding:.5em .5em .5em 20px;
-    margin-right:1em;
-}
-.storageRetrievalRequestCancel {
-    background-image:url(../images/fugue/holdCancel.png);
-    background-repeat:no-repeat;
-    background-position: left;
-    padding:.5em .5em .5em 20px;
-    margin-right:1em;
-}
-.storageRetrievalRequestCancelAll {
+.holdCancelAll, .storageRetrievalRequestCancelAll, .ILLRequestCancelAll {
     background-image:url(../images/fugue/holdCancelAll.png);
     background-repeat:no-repeat;
     background-position: left;
@@ -961,7 +940,7 @@ h3.list {
     padding:.5em .5em .5em 20px;
     margin-right:1em;
 }
-.checkRequest {
+.checkRequest, .checkStorageRetrievalRequest, .checkILLRequest {
     background-image:url(../images/fugue/checkRequest.png);
     background-repeat:no-repeat;
     padding-left:18px;
diff --git a/themes/blueprint/js/ill.js b/themes/blueprint/js/ill.js
new file mode 100644
index 00000000000..fe02ebc41b4
--- /dev/null
+++ b/themes/blueprint/js/ill.js
@@ -0,0 +1,29 @@
+function setUpILLRequestForm(recordId) {
+    $("#ILLRequestForm #pickupLibrary").change(function() {
+        $("#ILLRequestForm #pickupLibraryLocation option").remove();
+        $("#ILLRequestForm #pickupLibraryLocationLabel").addClass("ajax_ill_request_loading");
+        var url = path + '/AJAX/JSON?' + $.param({method:'getLibraryPickupLocations', id: recordId, pickupLib: $("#ILLRequestForm #pickupLibrary").val() });
+        $.ajax({
+            dataType: 'json',
+            cache: false,
+            url: url,
+            success: function(response) {
+                if (response.status == 'OK') {
+                    $.each(response.data.locations, function() {
+                        var option = $("<option></option>").attr("value", this.id).text(this.name);
+                        if (this.isDefault) {
+                            option.attr("selected", "selected");
+                        }
+                        $("#ILLRequestForm #pickupLibraryLocation").append(option);
+                    });
+                }
+                $("#ILLRequestForm #pickupLibraryLocationLabel").removeClass("ajax_ill_request_loading");
+            },
+            fail: function() {
+                $("#ILLRequestForm #pickupLibraryLocationLabel").removeClass("ajax_ill_request_loading");
+            }
+        });   
+        
+    });
+    $("#ILLRequestForm #pickupLibrary").change();
+}
diff --git a/themes/blueprint/js/record.js b/themes/blueprint/js/record.js
index e9be8396065..f4a971ecfe6 100644
--- a/themes/blueprint/js/record.js
+++ b/themes/blueprint/js/record.js
@@ -45,9 +45,6 @@ function setUpCheckRequest() {
                 'checkRequest ajax_hold_availability', 'holdBlocked');
         }
     });
-}
-
-function setUpCheckStorageRetrievalRequest() {
     $('.checkStorageRetrievalRequest').each(function(i) {
         if($(this).hasClass('checkStorageRetrievalRequest')) {
             $(this).addClass('ajax_storage_retrieval_request_availability');
@@ -56,6 +53,14 @@ function setUpCheckStorageRetrievalRequest() {
                 'storageRetrievalRequestBlocked');
         }
     });
+    $('.checkILLRequest').each(function(i) {
+        if($(this).hasClass('checkILLRequest')) {
+            $(this).addClass('ajax_ill_request_availability');
+            var isValid = checkRequestIsValid(this, this.href, 'ILLRequest',
+                'checkILLRequest ajax_ill_request_availability', 
+                'ILLRequestBlocked');
+        }
+    });
 }
 
 function deleteRecordComment(element, recordId, recordSource, commentId) {
@@ -179,5 +184,4 @@ $(document).ready(function(){
     });
 
     setUpCheckRequest();
-    setUpCheckStorageRetrievalRequest();
 });
\ No newline at end of file
diff --git a/themes/blueprint/templates/RecordTab/holdingsils.phtml b/themes/blueprint/templates/RecordTab/holdingsils.phtml
index b15b3169731..8d9292efe7c 100644
--- a/themes/blueprint/templates/RecordTab/holdingsils.phtml
+++ b/themes/blueprint/templates/RecordTab/holdingsils.phtml
@@ -81,6 +81,7 @@
   <? foreach ($holding['items'] as $row): ?>
     <? $check = (isset($row['check']) && $row['check']); ?>
     <? $checkStorageRetrievalRequest = (isset($row['checkStorageRetrievalRequest']) && $row['checkStorageRetrievalRequest']); ?>
+    <? $checkILLRequest = (isset($row['checkILLRequest']) && $row['checkILLRequest']); ?>
     <? if (isset($row['barcode']) && $row['barcode'] != ""): ?>
       <tr vocab="http://schema.org/" typeof="Offer">
         <th><?=$this->transEsc("Copy")?> <?=$this->escapeHtml($row['number'])?></th>
@@ -103,6 +104,9 @@
               <? if (isset($row['storageRetrievalRequestLink']) && $row['storageRetrievalRequestLink']): ?>
                 <a class="storageRetrievalRequestPlace<?=$checkStorageRetrievalRequest ? ' checkStorageRetrievalRequest' : ''?>" href="<?=$this->recordLink()->getRequestUrl($row['storageRetrievalRequestLink'])?>"><span><?=$this->transEsc($checkStorageRetrievalRequest ? "storage_retrieval_request_check_text" : "storage_retrieval_request_place_text")?></span></a>
               <? endif; ?>
+              <? if (isset($row['ILLRequestLink']) && $row['ILLRequestLink']): ?>
+                <a class="ILLRequestPlace<?=$checkILLRequest ? ' checkILLRequest' : ''?>" href="<?=$this->recordLink()->getRequestUrl($row['ILLRequestLink'])?>"><span><?=$this->transEsc($checkILLRequest ? "ill_request_check_text" : "ill_request_place_text")?></span></a>
+              <? endif; ?>
               </div>
             <? else: ?>
               <? /* Begin Unavailable Items (Recalls) */ ?>
diff --git a/themes/blueprint/templates/myresearch/illrequests.phtml b/themes/blueprint/templates/myresearch/illrequests.phtml
new file mode 100644
index 00000000000..77a4df18f3e
--- /dev/null
+++ b/themes/blueprint/templates/myresearch/illrequests.phtml
@@ -0,0 +1,168 @@
+<?
+    // Set up page title:
+    $this->headTitle($this->translate('ILL Requests'));
+
+    // Set up breadcrumbs:
+    $this->layout()->breadcrumbs = '<a href="' . $this->url('myresearch-home') . '">'
+        . $this->transEsc('Your Account') . '</a>' . '<span>&gt;</span><em>'
+        . $this->transEsc('ILL Requests') . '</em>';
+?>
+<div class="<?=$this->layoutClass('mainbody')?>">
+  <h3><?=$this->transEsc('ILL Requests') ?></h3>
+
+  <?=$this->flashmessages()?>
+
+  <? if (!empty($this->recordList)): ?>
+    <? if ($this->cancelForm): ?>
+      <form name="cancelForm" action="" method="post" id="cancelILLRequest">
+        <input type="hidden" id="cancelConfirm" name="confirm" value="0"/>
+        <div class="toolbar">
+          <ul>
+            <li><input type="submit" class="button ILLRequestCancel" name="cancelSelected" value="<?=$this->transEsc("ill_request_cancel_selected") ?>"/></li>
+            <li><input type="submit" class="button ILLRequestCancelAll" name="cancelAll" value="<?=$this->transEsc("ill_request_cancel_all") ?>"/></li>
+          </ul>
+        </div>
+      <div class="clearer"></div>
+    <? endif; ?>
+
+    <ul class="recordSet">
+    <? $iteration = 0; ?>
+    <? foreach ($this->recordList as $resource): ?>
+      <? $iteration++; ?>
+      <? $ilsDetails = $resource->getExtraDetail('ils_details'); ?>
+      <li class="result<? if (($iteration % 2) == 0): ?> alt<? endif; ?>">
+        <? if ($this->cancelForm && isset($ilsDetails['cancel_details'])): ?>
+          <? $safeId = preg_replace('/[^a-zA-Z0-9]/', '', $resource->getUniqueId()); ?>
+          <label for="checkbox_<?=$safeId?>" class="offscreen"><?=$this->transEsc("Select this record")?></label>
+          <input type="hidden" name="cancelAllIDS[]" value="<?=$this->escapeHtml($ilsDetails['cancel_details']) ?>" />
+          <input type="checkbox" name="cancelSelectedIDS[]" value="<?=$this->escapeHtml($ilsDetails['cancel_details']) ?>" class="checkbox" style="margin-left:0;" id="checkbox_<?=$safeId?>" />
+        <? endif; ?>
+        <div id="record<?=$this->escapeHtml($resource->getUniqueId()) ?>">
+          <div class="span-2">
+            <? if ($summThumb = $this->record($resource)->getThumbnail()): ?>
+              <img src="<?=$this->escapeHtml($summThumb)?>" class="summcover" alt="<?=$this->transEsc('Cover Image')?>"/>
+            <? else: ?>
+              <img src="<?=$this->url('cover-unavailable')?>" class="summcover" alt="<?=$this->transEsc('No Cover Image')?>"/>
+            <? endif; ?>
+          </div>
+          <div class="span-10">
+            <?
+                // If this is a non-missing Solr record, we should display a link:
+                if (is_a($resource, 'VuFind\\RecordDriver\\SolrDefault') && !is_a($resource, 'VuFind\\RecordDriver\\Missing')) {
+                    $title = $resource->getTitle();
+                    $title = empty($title) ? $this->transEsc('Title not available') : $this->escapeHtml($title);
+                    echo '<a href="' . $this->recordLink()->getUrl($resource) .
+                        '" class="title">' . $title . '</a>';
+                } else if (isset($ilsDetails['title']) && !empty($ilsDetails['title'])){
+                    // If the record is not available in Solr, perhaps the ILS driver sent us a title we can show...
+                    echo $this->escapeHtml($ilsDetails['title']);
+                } else {
+                    // Last resort -- indicate that no title could be found.
+                    echo $this->transEsc('Title not available');
+                }
+            ?><br/>
+            <? $listAuthor = $resource->getPrimaryAuthor(); if (!empty($listAuthor)): ?>
+              <?=$this->transEsc('by')?>:
+              <a href="<?=$this->record($resource)->getLink('author', $listAuthor)?>"><?=$this->escapeHtml($listAuthor)?></a><br/>
+            <? endif; ?>
+            <? /* TODO: tags
+            {if $resource.tags}
+              <?=$this->transEsc('Your Tags')?>:
+              {foreach from=$resource.tags item=tag name=tagLoop}
+                <a href="{$url}/Search/Results?tag={$tag->tag|escape:"url"}">{$tag->tag|escape}</a>{if !$smarty.foreach.tagLoop.last},{/if}
+              {/foreach}
+              <br/>
+            {/if}
+             */ ?>
+            <? /* TODO: notes
+            {if $resource.notes}
+              <?=$this->transEsc('Notes')?>: {$resource.notes|escape}<br/>
+            {/if}
+             */ ?>
+
+            <? $formats = $resource->getFormats(); if (count($formats) > 0): ?>
+              <?=$this->record($resource)->getFormatList()?>
+              <br/>
+            <? endif; ?>
+            <? if (isset($ilsDetails['volume']) && !empty($ilsDetails['volume'])): ?>
+              <strong><?=$this->transEsc('Volume')?>:</strong> <?=$this->escapeHtml($ilsDetails['volume'])?>
+              <br />
+            <? endif; ?>
+
+            <? if (isset($ilsDetails['publication_year']) && !empty($ilsDetails['publication_year'])): ?>
+              <strong><?=$this->transEsc('Year of Publication')?>:</strong> <?=$this->escapeHtml($ilsDetails['publication_year'])?>
+              <br />
+            <? endif; ?>
+
+            <? if (isset($ilsDetails['institution_name']) && !empty($ilsDetails['institution_name'])): ?>
+              <strong><?=$this->transEsc('institution_' . $ilsDetails['institution_name'], array(), $ilsDetails['institution_name']) ?></strong>
+              <br />
+            <? endif; ?> 
+                
+            <? /* Depending on the ILS driver, the "location" value may be a string or an ID; figure out the best
+               value to display... */ ?>
+            <? $pickupDisplay = ''; ?>
+            <? $pickupTranslate = false; ?>
+            <? if (isset($ilsDetails['location'])): ?>
+              <? if ($this->pickup): ?>
+                <? foreach ($this->pickup as $library): ?>
+                  <? if ($library['locationID'] == $ilsDetails['location']): ?>
+                    <? $pickupDisplay = $library['locationDisplay']; ?>
+                    <? $pickupTranslate = true; ?>
+                  <? endif; ?>
+                <? endforeach; ?>
+              <? endif; ?>
+              <? if (empty($pickupDisplay)): ?>
+                <? $pickupDisplay = $ilsDetails['location']; ?>
+              <? endif; ?>
+            <? endif; ?>
+            <? if (!empty($pickupDisplay)): ?>
+              <strong><?=$this->transEsc('pick_up_location') ?>:</strong>
+              <?=$pickupTranslate ? $this->transEsc($pickupDisplay) : $this->escapeHtml($pickupDisplay)?>
+              <br />
+            <? endif; ?>
+
+            <strong><?=$this->transEsc('Created') ?>:</strong> <?=$this->escapeHtml($ilsDetails['create']) ?>
+            <? if (!empty($ilsDetails['expire'])): ?>
+             | <strong><?=$this->transEsc('Expires') ?>:</strong> <?=$this->escapeHtml($ilsDetails['expire']) ?>
+            <? endif; ?>
+            <br />
+
+            <? if (isset($this->cancelResults['items'])): ?>
+              <? foreach ($this->cancelResults['items'] as $itemId=>$cancelResult): ?>
+                <? if ($itemId == $ilsDetails['item_id'] && $cancelResult['success'] == false): ?>
+                  <div class="error"><?=$this->transEsc($cancelResult['status']) ?><? if ($cancelResult['sysMessage']) echo ' : ' . $this->transEsc($cancelResult['sysMessage']); ?></div>
+                <? endif; ?>
+              <? endforeach; ?>
+            <? endif; ?>
+
+            <? if (isset($ilsDetails['processed']) && $ilsDetails['processed']): ?>
+              <div class="info"><?=$this->transEsc("ill_request_processed") . (is_string($ilsDetails['processed']) ? ': ' . $ilsDetails['processed'] : '') ?></div>
+            <? endif; ?>
+            <? if (isset($ilsDetails['available']) && $ilsDetails['available']): ?>
+              <div class="info"><?=$this->transEsc("ill_request_available") ?></div>
+            <? endif; ?>
+            <? if (isset($ilsDetails['canceled']) && $ilsDetails['canceled']): ?>
+              <div class="info"><?=$this->transEsc("ill_request_canceled") . (is_string($ilsDetails['canceled']) ? ': ' . $ilsDetails['canceled'] : '') ?></div>
+            <? endif; ?>
+            <? if (isset($ilsDetails['cancel_link'])): ?>
+              <p><a href="<?=$this->escapeHtml($ilsDetails['cancel_link']) ?>"><?=$this->transEsc("ill_request_cancel") ?></a></p>
+            <? endif; ?>
+
+          </div>
+          <div class="clear"></div>
+        </div>
+      </li>
+    <? endforeach; ?>
+    </ul>
+    <? if ($this->cancelForm): ?></form><? endif; ?>
+  <? else: ?>
+    <?=$this->transEsc('You do not have any interlibrary loan requests placed') ?>.
+  <? endif; ?>
+</div>
+
+<div class="<?=$this->layoutClass('sidebar')?>">
+  <?=$this->context($this)->renderInContext("myresearch/menu.phtml", array('active' => 'ILLRequests'))?>
+</div>
+
+<div class="clear"></div>
diff --git a/themes/blueprint/templates/myresearch/menu.phtml b/themes/blueprint/templates/myresearch/menu.phtml
index 29f4b52f945..d9e49280740 100644
--- a/themes/blueprint/templates/myresearch/menu.phtml
+++ b/themes/blueprint/templates/myresearch/menu.phtml
@@ -8,6 +8,9 @@
       <? if ($this->ils()->checkFunction('StorageRetrievalRequests')): ?>
       <li<?=$this->active == 'storageRetrievalRequests' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-storageretrievalrequests')?>"><?=$this->transEsc('Storage Retrieval Requests')?></a></li>
       <? endif; ?>
+      <? if ($this->ils()->checkFunction('ILLRequests')): ?>
+      <li<?=$this->active == 'ILLRequests' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-illrequests')?>"><?=$this->transEsc('Interlibrary Loan Requests')?></a></li>
+      <? endif; ?>
       <li<?=$this->active == 'fines' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-fines')?>"><?=$this->transEsc('Fines')?></a></li>
       <li<?=$this->active == 'profile' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-profile')?>"><?=$this->transEsc('Profile')?></a></li>
     <? endif; ?>
diff --git a/themes/blueprint/templates/record/illrequest.phtml b/themes/blueprint/templates/record/illrequest.phtml
new file mode 100644
index 00000000000..4cd30fb0409
--- /dev/null
+++ b/themes/blueprint/templates/record/illrequest.phtml
@@ -0,0 +1,111 @@
+<?
+    // Set up ill script:
+    $this->headScript()->appendFile("ill.js");
+
+    // Set page title.
+    $this->headTitle($this->translate('ill_request_place_text') . ': ' . $this->driver->getBreadcrumb());
+
+    // Set up breadcrumbs:
+    $this->layout()->breadcrumbs = $this->getLastSearchLink($this->transEsc('Search'), '', '<span>&gt;</span>') .
+        $this->recordLink()->getBreadcrumb($this->driver) . '<span>&gt;</span><em>' . $this->transEsc('ill_request_place_text') . '</em>';
+?>
+<h2><?=$this->transEsc('ill_request_place_text')?></h2>
+<? if ($this->helpText): ?>
+<p class="helptext"><?=$this->helpText?></p>
+<? endif; ?>
+
+<?=$this->flashmessages()?>
+<div id="ILLRequestForm" class="ILLRequestForm">
+
+  <form action="" method="post">
+
+    <? if (in_array("itemId", $this->extraFields)): ?>
+      <div>
+        <strong><?=$this->transEsc('ill_request_item')?>:</strong><br/>
+        <select name="gatheredDetails[itemId]">
+          <? foreach ($this->items as $item): ?>
+          <option value="<?=$this->escapeHtml($item['id'])?>"<?=($this->gatheredDetails['itemId'] == $item['id']) ? ' selected="selected"' : ''?>><?=$this->escapeHtml($item['name'])?></option>
+          <? endforeach; ?>
+        </select>
+      </div>
+    <? endif; ?>
+    
+    <? if (in_array("pickUpLibrary", $this->extraFields)): ?>
+      <div>
+      <? if (count($this->pickupLibraries) > 1): ?>
+        <?
+          if (isset($this->gatheredDetails['pickUpLibrary']) && $this->gatheredDetails['pickUpLibrary'] !== "") {
+              $selected = $this->gatheredDetails['pickUpLibrary'];
+          } else {
+              $selected = false;
+          }
+        ?>
+        <strong><?=$this->transEsc("ill_request_pick_up_library")?>:</strong><br/>
+        <select id="pickupLibrary" name="gatheredDetails[pickUpLibrary]">
+        <? foreach ($this->pickupLibraries as $lib): ?>
+          <option value="<?=$this->escapeHtml($lib['id'])?>"<?=(($selected === false && isset($lib['isDefault']) && $lib['isDefault']) || $selected === $lib['id']) ? ' selected="selected"' : ''?>>
+            <?=$this->transEsc('library_' . $lib['name'], null, $lib['name'])?>
+          </option>
+        <? endforeach; ?>
+        </select>
+      <? endif; ?>
+      </div>
+    <? endif; ?>
+
+    <? if (in_array("pickUpLibraryLocation", $this->extraFields)): ?>
+      <div>
+        <span id="pickupLibraryLocationLabel"><strong><?=$this->transEsc("ill_request_pick_up_location")?>:</strong></span><br/>
+        <select id="pickupLibraryLocation" name="gatheredDetails[pickUpLibraryLocation]">
+        </select>
+      </div>
+    <? endif; ?>
+    
+    <? if (in_array("pickUpLocation", $this->extraFields)): ?>
+      <div>
+      <? if (count($this->pickupLocations) > 1): ?>
+        <?
+          if (isset($this->gatheredDetails['pickUpLocation']) && $this->gatheredDetails['pickUpLocation'] !== "") {
+              $selected = $this->gatheredDetails['pickUpLocation'];
+          } elseif (isset($this->homeLibrary) && $this->homeLibrary !== "") {
+              $selected = $this->homeLibrary;
+          } else {
+              $selected = false;
+          }
+        ?>
+        <strong><?=$this->transEsc("pick_up_location")?>:</strong><br/>
+        <select id="pickupLocation" name="gatheredDetails[pickUpLocation]">
+        <? foreach ($this->pickupLocations as $loc): ?>
+          <option value="<?=$this->escapeHtml($loc['id'])?>"<?=(($selected === false && isset($loc['isDefault']) && $loc['isDefault']) || $selected === $loc['id']) ? ' selected="selected"' : ''?>>
+            <?=$this->escapeHtml($loc['name'])?>
+          </option>
+        <? endforeach; ?>
+        </select>
+      <? endif; ?>
+      </div>
+    <? endif; ?>
+    
+    <? if (in_array("requiredByDate", $this->extraFields)): ?>
+      <div>
+      <strong><?=$this->transEsc("hold_required_by")?>: </strong>
+      <div id="requiredByHolder"><input id="requiredByDate" type="text" name="gatheredDetails[requiredBy]" value="<?=(isset($this->gatheredDetails['requiredBy']) && !empty($this->gatheredDetails['requiredBy'])) ? $this->escapeHtml($this->gatheredDetails['requiredBy']) : $this->escapeHtml($this->defaultRequiredDate)?>" size="8" /> <strong>(<?=$this->dateTime()->getDisplayDateFormat()?>)</strong></div>
+      </div>
+    <? endif; ?>
+
+    <? if (in_array("comments", $this->extraFields)): ?>
+      <div>
+      <strong><?=$this->transEsc("Comments")?>:</strong><br/>
+      <textarea rows="3" cols="20" name="gatheredDetails[comment]"><?=isset($this->gatheredDetails['comment']) ? $this->escapeHtml($this->gatheredDetails['comment']) : ''?></textarea>
+      </div>
+    <? endif; ?>
+
+    <input type="submit" name="placeILLRequest" value="<?=$this->transEsc('ill_request_submit_text')?>"/>
+
+  </form>
+
+</div>
+
+<script type="text/javascript">
+$(document).ready(function(){
+    setUpILLRequestForm('<?=$this->escapeHtml($this->driver->getUniqueId()) ?>');
+});
+</script>
diff --git a/themes/bootstrap/js/ill.js b/themes/bootstrap/js/ill.js
new file mode 100644
index 00000000000..923707ddc3d
--- /dev/null
+++ b/themes/bootstrap/js/ill.js
@@ -0,0 +1,29 @@
+function setUpILLRequestForm(recordId) {
+    $("#ILLRequestForm #pickupLibrary").change(function() {
+        $("#ILLRequestForm #pickupLibraryLocation option").remove();
+        $("#ILLRequestForm #pickupLibraryLocationLabel i").addClass("icon-spinner icon-spin");
+        var url = path + '/AJAX/JSON?' + $.param({method:'getLibraryPickupLocations', id: recordId, pickupLib: $("#ILLRequestForm #pickupLibrary").val() });
+        $.ajax({
+            dataType: 'json',
+            cache: false,
+            url: url,
+            success: function(response) {
+                if (response.status == 'OK') {
+                    $.each(response.data.locations, function() {
+                        var option = $("<option></option>").attr("value", this.id).text(this.name);
+                        if (this.isDefault) {
+                            option.attr("selected", "selected");
+                        }
+                        $("#ILLRequestForm #pickupLibraryLocation").append(option);
+                    });
+                }
+                $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("icon-spinner icon-spin");
+            },
+            fail: function() {
+                $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("icon-spinner icon-spin");
+            }
+        });   
+        
+    });
+    $("#ILLRequestForm #pickupLibrary").change();
+}
diff --git a/themes/bootstrap/js/record.js b/themes/bootstrap/js/record.js
index 57336dc2af5..8feb077e2b9 100644
--- a/themes/bootstrap/js/record.js
+++ b/themes/bootstrap/js/record.js
@@ -39,19 +39,25 @@ function checkRequestIsValid(element, requestURL, requestType, blockedClass) {
 
 function setUpCheckRequest() {
   $('.checkRequest').each(function(i) {
-    if($(this).hasClass('checkRequest')) {
+    if ($(this).hasClass('checkRequest')) {
       var isValid = checkRequestIsValid(this, this.href, 'Hold', 'holdBlocked');
     }
   });
-}
-
-function setUpCheckStorageRetrievalRequest() {
   $('.checkStorageRetrievalRequest').each(function(i) {
-    if($(this).hasClass('checkStorageRetrievalRequest')) {
+    if ($(this).hasClass('checkStorageRetrievalRequest')) {
       var isValid = checkRequestIsValid(this, this.href, 'StorageRetrievalRequest',
           'StorageRetrievalRequestBlocked');
     }
   });
+  $('.checkILLRequest').each(function(i) {
+    if ($(this).hasClass('checkILLRequest')) {
+      var isValid = checkRequestIsValid(this, this.href, 'ILLRequest',
+          'ILLRequestBlocked');
+    }
+  });
+}
+
+function setUpCheckStorageRetrievalRequest() {
 }
 
 function deleteRecordComment(element, recordId, recordSource, commentId) {
diff --git a/themes/bootstrap/templates/RecordTab/holdingsils.phtml b/themes/bootstrap/templates/RecordTab/holdingsils.phtml
index 83fddf6957d..62f69dc4252 100644
--- a/themes/bootstrap/templates/RecordTab/holdingsils.phtml
+++ b/themes/bootstrap/templates/RecordTab/holdingsils.phtml
@@ -81,6 +81,7 @@
   <? foreach ($holding['items'] as $row): ?>
     <? $check = (isset($row['check']) && $row['check']); ?>
     <? $checkStorageRetrievalRequest = (isset($row['checkStorageRetrievalRequest']) && $row['checkStorageRetrievalRequest']); ?>
+    <? $checkILLRequest = (isset($row['checkILLRequest']) && $row['checkILLRequest']); ?>
     <? if (isset($row['barcode']) && $row['barcode'] != ""): ?>
       <tr vocab="http://schema.org/" typeof="Offer">
         <th><?=$this->transEsc("Copy")?> <?=$this->escapeHtml($row['number'])?></th>
@@ -101,6 +102,9 @@
               <? if (isset($row['storageRetrievalRequestLink']) && $row['storageRetrievalRequestLink']): ?>
                 <a class="<?=$checkStorageRetrievalRequest ? 'checkStorageRetrievalRequest ' : ''?>inlineblock modal-link placeStorageRetrievalRequest" href="<?=$this->recordLink()->getRequestUrl($row['storageRetrievalRequestLink'])?>" title="<?=$this->transEsc($checkStorageRetrievalRequest ? "storage_retrieval_request_check_text" : "storage_retrieval_request_place_text")?>"><i class="icon-flag"></i>&nbsp;<?=$this->transEsc($checkStorageRetrievalRequest ? "storage_retrieval_request_check_text" : "storage_retrieval_request_place_text")?></a>
               <? endif; ?>
+              <? if (isset($row['ILLRequestLink']) && $row['ILLRequestLink']): ?>
+                <a class="<?=$checkILLRequest ? 'checkILLRequest ' : ''?>inlineblock modal-link placeILLRequest" href="<?=$this->recordLink()->getRequestUrl($row['ILLRequestLink'])?>"  title="<?=$this->transEsc($checkILLRequest ? "ill_request_check_text" : "ill_request_place_text")?>"><i class="icon-flag"></i>&nbsp;<?=$this->transEsc($checkILLRequest ? "ill_request_check_text" : "ill_request_place_text")?></a>
+              <? endif; ?>
             <? else: ?>
               <? /* Begin Unavailable Items (Recalls) */ ?>
               <span class="text-error"><?=$this->transEsc($row['status'])?><link property="availability" href="http://schema.org/OutOfStock" /></span>
diff --git a/themes/bootstrap/templates/myresearch/illrequests.phtml b/themes/bootstrap/templates/myresearch/illrequests.phtml
new file mode 100644
index 00000000000..4ee652f20d8
--- /dev/null
+++ b/themes/bootstrap/templates/myresearch/illrequests.phtml
@@ -0,0 +1,168 @@
+<?
+    // Set up page title:
+    $this->headTitle($this->translate('Interlibrary Loan Requests'));
+
+    // Set up breadcrumbs:
+    $this->layout()->breadcrumbs = '<li><a href="' . $this->url('myresearch-home') . '">' . $this->transEsc('Your Account') . '</a> <span class="divider">&gt;</span></li>'
+        . '<li class="active">' . $this->transEsc('Interlibrary Loan Requests') . '</li>';
+?>
+
+<div class="<?=$this->layoutClass('mainbody')?>">
+  <h2><?=$this->transEsc('Interlibrary Loan Requests') ?></h2>
+
+  <?=$this->flashmessages()?>
+
+  <? if (!empty($this->recordList)): ?>
+    <? if ($this->cancelForm): ?>
+      <form name="cancelForm" class="inline" action="" method="post" id="cancelILLRequest">
+        <input type="hidden" id="submitType" name="cancelSelected" value="1"/>
+        <input type="hidden" id="cancelConfirm" name="confirm" value="0"/>
+        <div class="btn-group">
+          <input id="cancelSelected" name="cancelSelected" type="submit" value="<?=$this->transEsc("ill_request_cancel_selected") ?>" class="btn dropdown-toggle" data-toggle="dropdown"/>
+          <ul class="dropdown-menu">
+            <li class="disabled"><a><?=$this->transEsc("confirm_ill_request_cancel_selected_text") ?></a></li>
+            <li><a href="#" onClick="$('#cancelConfirm').val(1);$('#submitType').attr('name','cancelSelected');$(this).parents('form').submit(); return false;"><?=$this->transEsc('confirm_dialog_yes') ?></a></li>
+            <li><a href="#" onClick="return false;"><?=$this->transEsc('confirm_dialog_no')?></a></li>
+          </ul>
+        </div>
+        <div class="btn-group">
+          <input id="cancelAll" name="cancelAll" type="submit" value="<?=$this->transEsc("ill_request_cancel_all") ?>" class="btn dropdown-toggle" data-toggle="dropdown"/>
+          <ul class="dropdown-menu">
+            <li class="disabled"><a><?=$this->transEsc("confirm_ill_request_cancel_all_text") ?></a></li>
+            <li><a href="#" onClick="$('#cancelConfirm').val(1);$('#submitType').attr('name','cancelAll');$(this).parents('form').submit(); return false;"><?=$this->transEsc('confirm_dialog_yes') ?></a></li>
+            <li><a href="#" onClick="return false;"><?=$this->transEsc('confirm_dialog_no')?></a></li>
+          </ul>
+        </div>
+    <? endif; ?>
+
+    <? $iteration = 0; ?>
+    <? foreach ($this->recordList as $resource): ?>
+      <hr/>
+      <? $iteration++; ?>
+      <? $ilsDetails = $resource->getExtraDetail('ils_details'); ?>
+      <div id="record<?=$this->escapeHtml($resource->getUniqueId()) ?>" class="row-fluid">
+        <? if ($this->cancelForm && isset($ilsDetails['cancel_details'])): ?>
+          <? $safeId = preg_replace('/[^a-zA-Z0-9]/', '', $resource->getUniqueId()); ?>
+          <input type="hidden" name="cancelAllIDS[]" value="<?=$this->escapeHtml($ilsDetails['cancel_details']) ?>" />
+          <div class="pull-left">
+            <input type="checkbox" name="cancelSelectedIDS[]" value="<?=$this->escapeHtml($ilsDetails['cancel_details']) ?>" id="checkbox_<?=$safeId?>" />
+          </div>
+        <? endif; ?>
+        <div class="span2 text-center">
+          <? if ($summThumb = $this->record($resource)->getThumbnail()): ?>
+            <img src="<?=$this->escapeHtml($summThumb)?>" class="summcover" alt="<?=$this->transEsc('Cover Image')?>"/>
+          <? else: ?>
+            <img src="<?=$this->url('cover-unavailable')?>" class="summcover" alt="<?=$this->transEsc('No Cover Image')?>"/>
+          <? endif; ?>
+        </div>
+        <div class="span9">
+          <?
+            // If this is a non-missing Solr record, we should display a link:
+            if (is_a($resource, 'VuFind\\RecordDriver\\SolrDefault') && !is_a($resource, 'VuFind\\RecordDriver\\Missing')) {
+              $title = $resource->getTitle();
+              $title = empty($title) ? $this->transEsc('Title not available') : $this->escapeHtml($title);
+              echo '<a href="' . $this->recordLink()->getUrl($resource)
+                . '" class="title">' . $title . '</a>';
+            } else if (isset($ilsDetails['title']) && !empty($ilsDetails['title'])){
+              // If the record is not available in Solr, perhaps the ILS driver sent us a title we can show...
+              echo $this->escapeHtml($ilsDetails['title']);
+            } else {
+              // Last resort -- indicate that no title could be found.
+              echo $this->transEsc('Title not available');
+            }
+          ?><br/>
+          <? $listAuthor = $resource->getPrimaryAuthor(); if (!empty($listAuthor)): ?>
+            <?=$this->transEsc('by')?>:
+            <a href="<?=$this->record($resource)->getLink('author', $listAuthor)?>"><?=$this->escapeHtml($listAuthor)?></a><br/>
+          <? endif; ?>
+          <? /* TODO: tags
+          {if $resource.tags}
+            <?=$this->transEsc('Your Tags')?>:
+            {foreach from=$resource.tags item=tag name=tagLoop}
+              <a href="{$url}/Search/Results?tag={$tag->tag|escape:"url"}">{$tag->tag|escape}</a>{if !$smarty.foreach.tagLoop.last},{/if}
+            {/foreach}
+            <br/>
+          {/if}
+           */ ?>
+          <? /* TODO: notes
+          {if $resource.notes}
+            <?=$this->transEsc('Notes')?>: {$resource.notes|escape}<br/>
+          {/if}
+           */ ?>
+
+          <? $formats = $resource->getFormats(); if (count($formats) > 0): ?>
+            <?=str_replace('class="', 'class="label label-info ', $this->record($resource)->getFormatList())?>
+            <br/>
+          <? endif; ?>
+          <? if (isset($ilsDetails['volume']) && !empty($ilsDetails['volume'])): ?>
+            <strong><?=$this->transEsc('Volume')?>:</strong> <?=$this->escapeHtml($ilsDetails['volume'])?>
+            <br />
+          <? endif; ?>
+
+          <? if (isset($ilsDetails['publication_year']) && !empty($ilsDetails['publication_year'])): ?>
+            <strong><?=$this->transEsc('Year of Publication')?>:</strong> <?=$this->escapeHtml($ilsDetails['publication_year'])?>
+            <br />
+          <? endif; ?>
+
+          <? /* Depending on the ILS driver, the "location" value may be a string or an ID; figure out the best
+             value to display... */ ?>
+          <? $pickupDisplay = ''; ?>
+          <? $pickupTranslate = false; ?>
+          <? if (isset($ilsDetails['location'])): ?>
+            <? if ($this->pickup): ?>
+              <? foreach ($this->pickup as $library): ?>
+                <? if ($library['locationID'] == $ilsDetails['location']): ?>
+                  <? $pickupDisplay = $library['locationDisplay']; ?>
+                  <? $pickupTranslate = true; ?>
+                <? endif; ?>
+              <? endforeach; ?>
+            <? endif; ?>
+            <? if (empty($pickupDisplay)): ?>
+              <? $pickupDisplay = $ilsDetails['location']; ?>
+            <? endif; ?>
+          <? endif; ?>
+          <? if (!empty($pickupDisplay)): ?>
+            <strong><?=$this->transEsc('pick_up_location') ?>:</strong>
+            <?=$pickupTranslate ? $this->transEsc($pickupDisplay) : $this->escapeHtml($pickupDisplay)?>
+            <br />
+          <? endif; ?>
+
+          <strong><?=$this->transEsc('Created') ?>:</strong> <?=$this->escapeHtml($ilsDetails['create']) ?>
+          <? if (!empty($ilsDetails['expire'])): ?>
+            | <strong><?=$this->transEsc('Expires') ?>:</strong> <?=$this->escapeHtml($ilsDetails['expire']) ?>
+          <? endif; ?>
+          <br />
+
+          <? if (isset($this->cancelResults['items'])): ?>
+            <? foreach ($this->cancelResults['items'] as $itemId=>$cancelResult): ?>
+              <? if ($itemId == $ilsDetails['item_id'] && $cancelResult['success'] == false): ?>
+                <div class="alert alert-error"><?=$this->transEsc($cancelResult['status']) ?><? if ($cancelResult['sysMessage']) echo ' : ' . $this->transEsc($cancelResult['sysMessage']); ?></div>
+              <? endif; ?>
+            <? endforeach; ?>
+          <? endif; ?>
+
+          <? if (isset($ilsDetails['processed']) && $ilsDetails['processed']): ?>
+            <div class="text-success"><?=$this->transEsc("ill_request_processed") . (is_string($ilsDetails['processed']) ? ': ' . $ilsDetails['processed'] : '') ?></div>
+          <? endif; ?>
+          <? if (isset($ilsDetails['available']) && $ilsDetails['available']): ?>
+            <div class="text-success"><?=$this->transEsc("ill_request_available") ?></div>
+          <? endif; ?>
+          <? if (isset($ilsDetails['canceled']) && $ilsDetails['canceled']): ?>
+            <div class="text-success"><?=$this->transEsc("ill_request_canceled") . (is_string($ilsDetails['canceled']) ? ': ' . $ilsDetails['canceled'] : '') ?></div>
+          <? endif; ?>
+          <? if (isset($ilsDetails['cancel_link'])): ?>
+            <p><a href="<?=$this->escapeHtml($ilsDetails['cancel_link']) ?>"><?=$this->transEsc("ill_request_cancel") ?></a></p>
+          <? endif; ?>
+
+        </div>
+      </div>
+    <? endforeach; ?>
+    <? if ($this->cancelForm): ?></form><? endif; ?>
+  <? else: ?>
+    <?=$this->transEsc('You do not have any interlibrary loan requests placed') ?>.
+  <? endif; ?>
+</div>
+
+<div class="<?=$this->layoutClass('sidebar')?>">
+  <?=$this->context($this)->renderInContext("myresearch/menu.phtml", array('active' => 'ILLRequests'))?>
+</div>
diff --git a/themes/bootstrap/templates/myresearch/menu.phtml b/themes/bootstrap/templates/myresearch/menu.phtml
index 47a2e8b2fca..48971b020c0 100644
--- a/themes/bootstrap/templates/myresearch/menu.phtml
+++ b/themes/bootstrap/templates/myresearch/menu.phtml
@@ -7,6 +7,9 @@
     <? if ($this->ils()->checkFunction('StorageRetrievalRequests')): ?>
     <li<?=$this->active == 'storageRetrievalRequests' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-storageretrievalrequests')?>"><?=$this->transEsc('Storage Retrieval Requests')?> <i class="icon-archive pull-right"></i></a></li>
     <? endif; ?>
+    <? if ($this->ils()->checkFunction('ILLRequests')): ?>
+    <li<?=$this->active == 'ILLRequests' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-illrequests')?>"><?=$this->transEsc('Interlibrary Loan Requests')?> <i class="icon-exchange pull-right"></i></a></li>
+    <? endif; ?>
     <li<?=$this->active == 'fines' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-fines')?>"><?=$this->transEsc('Fines')?> <i class="icon-usd pull-right"></i></a></li>
     <li<?=$this->active == 'profile' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-profile')?>"><?=$this->transEsc('Profile')?> <i class="icon-user pull-right"></i></a></li>
   <? endif; ?>
diff --git a/themes/bootstrap/templates/record/illrequest.phtml b/themes/bootstrap/templates/record/illrequest.phtml
new file mode 100644
index 00000000000..24e34aefc03
--- /dev/null
+++ b/themes/bootstrap/templates/record/illrequest.phtml
@@ -0,0 +1,126 @@
+<?
+    // Set up ill script:
+    $this->headScript()->appendFile("ill.js");
+
+    // Set page title.
+    $this->headTitle($this->translate('ill_request_place_text') . ': ' . $this->driver->getBreadcrumb());
+
+    // Set up breadcrumbs:
+    $this->layout()->breadcrumbs = '<li>' . $this->getLastSearchLink($this->transEsc('Search'), '', '<span class="divider">&gt;</span> </li>')
+        . '<li>' . $this->recordLink()->getBreadcrumb($this->driver) . '<span class="divider">&gt;</span> </li>'
+        . '<li class="active">' . $this->transEsc('ill_request_place_text') . '</li>';
+?>
+<p class="lead"><?=$this->transEsc('ill_request_place_text')?></p>
+<?=$this->flashmessages()?>
+<div id="ILLRequestForm" class="storage-retrieval-request-form">
+  <form action="" class="form-horizontal" method="post">
+
+    <? if (in_array("itemId", $this->extraFields)): ?>
+      <div class="control-group">
+        <label class="control-label"><?=$this->transEsc('ill_request_item')?>:</label>
+        <div class="controls">
+          <select id="itemId" name="gatheredDetails[itemId]">
+          <? foreach ($this->items as $item): ?>
+            <option value="<?=$this->escapeHtml($item['id'])?>"<?=($this->gatheredDetails['itemId'] == $item['id']) ? ' selected="selected"' : ''?>>
+              <?=$this->escapeHtml($item['name'])?>
+            </option>
+         <? endforeach; ?>
+          </select>
+        </div>
+      </div>
+    <? endif; ?>
+    
+    <? if (in_array("pickUpLibrary", $this->extraFields)): ?>
+      <div class="control-group">
+      <? if (count($this->pickupLibraries) > 1): ?>
+        <?
+          if (isset($this->gatheredDetails['pickUpLibrary']) && $this->gatheredDetails['pickUpLibrary'] !== "") {
+              $selected = $this->gatheredDetails['pickUpLibrary'];
+          } else {
+              $selected = false;
+          }
+        ?>
+        <label class="control-label"><?=$this->transEsc("ill_request_pick_up_library")?>:</label>
+        <div class="controls">
+          <select id="pickupLibrary" name="gatheredDetails[pickUpLibrary]">
+          <? foreach ($this->pickupLibraries as $lib): ?>
+            <option value="<?=$this->escapeHtml($lib['id'])?>"<?=(($selected === false && isset($lib['isDefault']) && $lib['isDefault']) || $selected === $lib['id']) ? ' selected="selected"' : ''?>>
+              <?=$this->transEsc('library_' . $lib['name'], null, $lib['name'])?>
+            </option>
+          <? endforeach; ?>
+          </select>
+        </div>
+      <? endif; ?>
+      </div>
+    <? endif; ?>
+
+    <? if (in_array("pickUpLibraryLocation", $this->extraFields)): ?>
+      <div class="control-group">
+        <label id="pickupLibraryLocationLabel" class="control-label"><i></i>&nbsp;<?=$this->transEsc("ill_request_pick_up_location")?>:</label>
+        <div class="controls">
+          <select id="pickupLibraryLocation" name="gatheredDetails[pickUpLibraryLocation]">
+          </select>
+        </div>
+      </div>
+    <? endif; ?>
+  
+    <? if (in_array("pickUpLocation", $this->extraFields)): ?>
+      <? if (count($this->pickup) > 1): ?>
+        <div class="control-group">
+          <?
+            if (isset($this->gatheredDetails['pickUpLocation']) && $this->gatheredDetails['pickUpLocation'] !== "") {
+                $selected = $this->gatheredDetails['pickUpLocation'];
+            } elseif (isset($this->homeLibrary) && $this->homeLibrary !== "") {
+                $selected = $this->homeLibrary;
+            } else {
+                $selected = $this->defaultPickup;
+            }
+          ?>
+          <label class="control-label"><?=$this->transEsc("pick_up_location")?>:</label>
+          <div class="controls">
+            <select name="gatheredDetails[pickUpLocation]">
+            <? foreach ($this->pickup as $lib): ?>
+              <option value="<?=$this->escapeHtml($lib['locationID'])?>"<?=($selected == $lib['locationID']) ? ' selected="selected"' : ''?>>
+                <?=$this->escapeHtml($lib['locationDisplay'])?>
+              </option>
+            <? endforeach; ?>
+            </select>
+          </div>
+        </div>
+      <? else: ?>
+        <input type="hidden" name="gatheredDetails[pickUpLocation]" value="<?=$this->escapeHtml($this->defaultPickup)?>" />
+      <? endif; ?>
+    <? endif; ?>
+
+    <? if (in_array("requiredByDate", $this->extraFields)): ?>
+      <div class="control-group">
+        <label class="control-label"><?=$this->transEsc("hold_required_by")?>:</label>
+        <div class="controls">
+          <input id="requiredByDate" type="text" name="gatheredDetails[requiredBy]" value="<?=(isset($this->gatheredDetails['requiredBy']) && !empty($this->gatheredDetails['requiredBy'])) ? $this->escapeHtml($this->gatheredDetails['requiredBy']) : $this->escapeHtml($this->defaultRequiredDate)?>" size="8" />
+          (<?=$this->dateTime()->getDisplayDateFormat()?>)
+        </div>
+      </div>
+    <? endif; ?>
+
+    <? if (in_array("comments", $this->extraFields)): ?>
+      <div class="control-group">
+        <label class="control-label"><?=$this->transEsc("Comments")?>:</label>
+        <div class="controls">
+          <textarea rows="3" cols="20" name="gatheredDetails[comment]"><?=isset($this->gatheredDetails['comment']) ? $this->escapeHtml($this->gatheredDetails['comment']) : ''?></textarea>
+        </div>
+      </div>
+    <? endif; ?>
+
+    <div class="control-group">
+      <div class="controls">
+        <input class="btn btn-primary" type="submit" name="placeILLRequest" value="<?=$this->transEsc('ill_request_submit_text')?>"/>
+      </div>
+    </div>
+  </form>
+</div>
+
+<script type="text/javascript">
+$(document).ready(function(){
+    setUpILLRequestForm('<?=$this->escapeHtml($this->driver->getUniqueId()) ?>');
+});
+</script>
-- 
GitLab