From dc000dce7f10b132b89be31addf6015143bfc4a8 Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Thu, 4 Dec 2014 09:00:47 -0500
Subject: [PATCH] Added support for changing password to ILS-based
 authentication - Supported by Demo, VoyagerRestful and MultiBackend drivers.

---
 config/vufind/VoyagerRestful.ini              |   6 +
 module/VuFind/src/VuFind/Auth/Factory.php     |  10 +-
 module/VuFind/src/VuFind/Auth/ILS.php         | 127 +++++++++++++++++-
 module/VuFind/src/VuFind/Auth/MultiILS.php    |  13 +-
 module/VuFind/src/VuFind/ILS/Connection.php   |  38 ++++++
 module/VuFind/src/VuFind/ILS/Driver/Demo.php  |  33 +++++
 .../src/VuFind/ILS/Driver/MultiBackend.php    |  24 ++++
 .../src/VuFind/ILS/Driver/VoyagerRestful.php  | 108 +++++++++++++++
 .../templates/Auth/ILS/newpassword.phtml      |  12 ++
 .../templates/Auth/MultiILS/newpassword.phtml |  12 ++
 .../templates/Auth/ILS/newpassword.phtml      |  28 ++++
 .../templates/Auth/MultiILS/newpassword.phtml |  28 ++++
 .../templates/Auth/ILS/newpassword.phtml      |  31 +++++
 .../templates/Auth/MultiILS/newpassword.phtml |  30 +++++
 .../templates/Auth/ILS/newpassword.phtml      |  15 +++
 15 files changed, 505 insertions(+), 10 deletions(-)
 create mode 100644 themes/blueprint/templates/Auth/ILS/newpassword.phtml
 create mode 100644 themes/blueprint/templates/Auth/MultiILS/newpassword.phtml
 create mode 100644 themes/bootstrap/templates/Auth/ILS/newpassword.phtml
 create mode 100644 themes/bootstrap/templates/Auth/MultiILS/newpassword.phtml
 create mode 100644 themes/bootstrap3/templates/Auth/ILS/newpassword.phtml
 create mode 100644 themes/bootstrap3/templates/Auth/MultiILS/newpassword.phtml
 create mode 100644 themes/jquerymobile/templates/Auth/ILS/newpassword.phtml

diff --git a/config/vufind/VoyagerRestful.ini b/config/vufind/VoyagerRestful.ini
index 9981b9acde5..7e154d0f616 100644
--- a/config/vufind/VoyagerRestful.ini
+++ b/config/vufind/VoyagerRestful.ini
@@ -249,3 +249,9 @@ disableAvailabilityCheckForRequestGroups = "15:19:21:32"
 [Loans]
 ; Uncomment this line to display the location where each loan was made
 ;display_borrowing_location = true
+
+; Uncomment the following lines to enable password (PIN) change 
+;[changePassword]
+; PIN change parameters. The default limits are taken from the interface documentation.
+;minLength = 5
+;maxLength = 12
diff --git a/module/VuFind/src/VuFind/Auth/Factory.php b/module/VuFind/src/VuFind/Auth/Factory.php
index fc73a13c230..da3feb7ebbf 100644
--- a/module/VuFind/src/VuFind/Auth/Factory.php
+++ b/module/VuFind/src/VuFind/Auth/Factory.php
@@ -49,7 +49,10 @@ class Factory
      */
     public static function getILS(ServiceManager $sm)
     {
-        return new ILS($sm->getServiceLocator()->get('VuFind\ILSConnection'));
+        return new ILS(
+            $sm->getServiceLocator()->get('VuFind\ILSConnection'),
+            $sm->getServiceLocator()->get('VuFind\ILSAuthenticator')
+        );
     }
 
     /**
@@ -124,6 +127,9 @@ class Factory
      */
     public static function getMultiILS(ServiceManager $sm)
     {
-        return new MultiILS($sm->getServiceLocator()->get('VuFind\ILSConnection'));
+        return new MultiILS(
+            $sm->getServiceLocator()->get('VuFind\ILSConnection'),
+            $sm->getServiceLocator()->get('VuFind\ILSAuthenticator')
+        );
     }
 }
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/Auth/ILS.php b/module/VuFind/src/VuFind/Auth/ILS.php
index b05ea77d42a..ec791ccfdc3 100644
--- a/module/VuFind/src/VuFind/Auth/ILS.php
+++ b/module/VuFind/src/VuFind/Auth/ILS.php
@@ -27,6 +27,7 @@
  * @link     http://vufind.org/wiki/vufind2:authentication_handlers Wiki
  */
 namespace VuFind\Auth;
+
 use VuFind\Exception\Auth as AuthException;
 
 /**
@@ -41,6 +42,13 @@ use VuFind\Exception\Auth as AuthException;
  */
 class ILS extends AbstractBase
 {
+    /**
+     * ILS Authenticator
+     *
+     * @var object
+     */
+    protected $authenticator;
+
     /**
      * Catalog connection
      *
@@ -51,11 +59,15 @@ class ILS extends AbstractBase
     /**
      * Set the ILS connection for this object.
      *
-     * @param \VuFind\ILS\Connection $connection ILS connection to set
+     * @param \VuFind\ILS\Connection    $connection    ILS connection to set
+     * @param \VuFind\ILS\Authenticator $authenticator ILS authenticator
      */
-    public function __construct(\VuFind\ILS\Connection $connection)
-    {
+    public function __construct(
+        \VuFind\ILS\Connection $connection,
+        \VuFind\Auth\ILSAuthenticator $authenticator
+    ) {
         $this->setCatalog($connection);
+        $this->authenticator = $authenticator;
     }
 
     /**
@@ -114,6 +126,81 @@ class ILS extends AbstractBase
         throw new AuthException('authentication_error_invalid');
     }
 
+    /**
+     * Does this authentication method support password changing
+     *
+     * @return bool
+     */
+    public function supportsPasswordChange()
+    {
+        return false !== $this->getCatalog()->checkFunction(
+            'changePassword',
+            array('patron' => $this->getLoggedInPatron())
+        );
+    }
+
+    /**
+     * Password policy for a new password (e.g. minLength, maxLength)
+     *
+     * @return array
+     */
+    public function getPasswordPolicy()
+    {
+        $policy = $this->getCatalog()->getPasswordPolicy($this->getLoggedInPatron());
+        return $policy !== false ? $policy : parent::getPasswordPolicy();
+    }
+
+    /**
+     * Update a user's password from the request.
+     *
+     * @param \Zend\Http\PhpEnvironment\Request $request Request object containing
+     * new account details.
+     *
+     * @throws AuthException
+     * @return \VuFind\Db\Row\User New user row.
+     */
+    public function updatePassword($request)
+    {
+        // Ensure that all expected parameters are populated to avoid notices
+        // in the code below.
+        $params = array(
+            'oldpwd' => '', 'password' => '', 'password2' => ''
+        );
+        foreach ($params as $param => $default) {
+            $params[$param] = $request->getPost()->get($param, $default);
+        }
+
+        // Connect to catalog:
+        $patron = $this->authenticator->storedCatalogLogin();
+        if (!$patron) {
+            throw new AuthException('authentication_error_technical');
+        }
+
+        // Validate Input
+        $this->validatePasswordUpdate($params);
+
+        $result = $this->getCatalog()->changePassword(
+            array(
+                'patron' => $patron,
+                'oldPassword' => $params['oldpwd'],
+                'newPassword' => $params['password']
+            )
+        );
+        if (!$result['success']) {
+            throw new AuthException($result['status']);
+        }
+
+        // Update the user and send it back to the caller:
+        $table = $this->getUserTable();
+        $user = $table->getByUsername($patron['cat_username']);
+        $user['cat_password'] = $params['password'];
+        $user->saveCredentials(
+            !isset($user['cat_username']) ? " " : $user['cat_username'],
+            !isset($user['cat_password']) ? " " : $user['cat_password']
+        );
+        return $user;
+    }
+
     /**
      * Update the database using details from the ILS, then return the User object.
      *
@@ -154,4 +241,38 @@ class ILS extends AbstractBase
 
         return $user;
     }
+
+    /**
+     * Make sure passwords match and fulfill ILS policy
+     *
+     * @param array $params request parameters
+     *
+     * @return void
+     */
+    protected function validatePasswordUpdate($params)
+    {
+        // Needs a password
+        if (trim($params['password']) == '') {
+            throw new AuthException('Password cannot be blank');
+        }
+        // Passwords don't match
+        if ($params['password'] != $params['password2']) {
+            throw new AuthException('Passwords do not match');
+        }
+
+        $this->validatePasswordAgainstPolicy($params['password']);
+    }
+
+    /**
+     * Get the Currently Logged-In Patron
+     *
+     * @throws AuthException
+     *
+     * @return array Patron
+     */
+    protected function getLoggedInPatron()
+    {
+        $patron = $this->authenticator->storedCatalogLogin();
+        return $patron ? $patron : null;
+    }
 }
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/Auth/MultiILS.php b/module/VuFind/src/VuFind/Auth/MultiILS.php
index 3b31b052e32..16fd4a1a870 100644
--- a/module/VuFind/src/VuFind/Auth/MultiILS.php
+++ b/module/VuFind/src/VuFind/Auth/MultiILS.php
@@ -59,15 +59,18 @@ class MultiILS extends ILS
         $target = trim($request->getPost()->get('target'));
         $username = trim($request->getPost()->get('username'));
         $password = trim($request->getPost()->get('password'));
-        if ($target == '' || $username == '' || $password == '') {
+        if ($username == '' || $password == '') {
             throw new AuthException('authentication_error_blank');
         }
 
+        // We should have target either separately or already embedded into username
+        if ($target) {
+            $username = "$target.$username";
+        }
+
         // Connect to catalog:
         try {
-            $patron = $this->getCatalog()->patronLogin(
-                "$target.$username", $password
-            );
+            $patron = $this->getCatalog()->patronLogin($username, $password);
         } catch (\Exception $e) {
             throw new AuthException('authentication_error_technical');
         }
@@ -90,7 +93,7 @@ class MultiILS extends ILS
     {
         return $this->getCatalog()->getLoginDrivers();
     }
-    
+
     /**
      * Get default login target (ILS driver/source ID)
      *
diff --git a/module/VuFind/src/VuFind/ILS/Connection.php b/module/VuFind/src/VuFind/ILS/Connection.php
index 3f5c3e85c21..917d33f1a00 100644
--- a/module/VuFind/src/VuFind/ILS/Connection.php
+++ b/module/VuFind/src/VuFind/ILS/Connection.php
@@ -533,6 +533,29 @@ class Connection implements TranslatorAwareInterface
         return $response;
     }
 
+    /**
+     * Check Password Change
+     *
+     * A support method for checkFunction(). This is responsible for checking
+     * the driver configuration to determine if the system supports changing
+     * password.
+     *
+     * @param array $functionConfig The password change configuration values
+     * @param array $params         Patron data
+     *
+     * @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 checkMethodchangePassword($functionConfig, $params)
+    {
+        if ($this->checkCapability('changePassword', array($params ?: array()))) {
+            return array('function' => 'changePassword');
+        }
+        return false;
+    }
+
     /**
      * Get proper help text from the function config
      *
@@ -741,6 +764,21 @@ class Connection implements TranslatorAwareInterface
             : array('notes', 'summary', 'supplements', 'indexes');
     }
 
+    /**
+     * Get the password policy from the driver
+     *
+     * @param array $patron Patron data
+     *
+     * @return bool|array Password policy array or false if unsupported
+     */
+    public function getPasswordPolicy($patron)
+    {
+        return $this->checkCapability(
+            'getConfig', array('changePassword', compact('patron'))
+        ) ? $this->getDriver()->getConfig('changePassword', compact('patron'))
+            : false;
+    }
+
     /**
      * Default method -- pass along calls to the driver if available; return
      * false otherwise.  This allows custom functions to be implemented in
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Demo.php b/module/VuFind/src/VuFind/ILS/Driver/Demo.php
index e55fde5c559..f555ffc2726 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Demo.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Demo.php
@@ -1728,6 +1728,33 @@ class Demo extends AbstractBase
         return $details['reqnum'];
     }
 
+    /**
+     * Change Password
+     *
+     * Attempts to change patron password (PIN code)
+     *
+     * @param array $details An array of patron id and old and new password:
+     *
+     * 'patron'      The patron array from patronLogin
+     * 'oldPassword' Old password
+     * 'newPassword' New password
+     *
+     * @return array An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function changePassword($details)
+    {
+        if (rand() % 3) {
+            return array('success' => true, 'status' => 'change_password_ok');
+        }
+        return array(
+            'success' => false,
+            'status' => 'An error has occurred',
+            'sysMessage' =>
+                'Demonstrating failure; keep trying and it will work eventually.'
+        );
+    }
+
     /**
      * Public Function which specifies renew, hold and cancel settings.
      *
@@ -1768,6 +1795,12 @@ class Demo extends AbstractBase
                     . ' with some <span style="color: red">styling</span>.'
             );
         }
+        if ($function == 'changePassword') {
+            return array(
+                'minLength' => 4,
+                'maxLength' => 20
+            );
+        }
         return array();
     }
 }
diff --git a/module/VuFind/src/VuFind/ILS/Driver/MultiBackend.php b/module/VuFind/src/VuFind/ILS/Driver/MultiBackend.php
index d03a1c3c6af..c499e62a2c1 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/MultiBackend.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/MultiBackend.php
@@ -1105,6 +1105,30 @@ class MultiBackend extends AbstractBase
         throw new ILSException('No suitable backend driver found');
     }
 
+    /**
+     * Change Password
+     *
+     * Attempts to change patron password (PIN code)
+     *
+     * @param array $details An array of patron id and old and new password
+     *
+     * @return mixed An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function changePassword($details)
+    {
+        $source = $this->getSource($details['patron']['cat_username']);
+        $driver = $this->getDriver($source);
+        if ($driver
+            && $this->methodSupported($driver, 'changePassword')
+        ) {
+            return $driver->changePassword(
+                $this->stripIdPrefixes($details, $source)
+            );
+        }
+        throw new ILSException('No suitable backend driver found');
+    }
+
     /**
      * Function which specifies renew, hold and cancel settings.
      *
diff --git a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
index f23ae449a55..eea2fd753cb 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
@@ -3132,4 +3132,112 @@ EOT;
         // exist in the wild, we can make this method more sophisticated.
         return substr($institution, -5) == 'LOCAL';
     }
+
+    /**
+     * Change Password
+     *
+     * Attempts to change patron password (PIN code)
+     *
+     * @param array $details An array of patron id and old and new password:
+     *
+     * 'patron'      The patron array from patronLogin
+     * 'oldPassword' Old password
+     * 'newPassword' New password
+     *
+     * @return array An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function changePassword($details)
+    {
+        $patron = $details['patron'];
+        $id = htmlspecialchars($patron['id'], ENT_COMPAT, 'UTF-8');
+        $lastname = htmlspecialchars($patron['lastname'], ENT_COMPAT, 'UTF-8');
+        $ubId = htmlspecialchars($this->ws_patronHomeUbId, ENT_COMPAT, 'UTF-8');
+        $oldPIN = trim(
+            htmlspecialchars($details['oldPassword'], ENT_COMPAT, 'UTF-8')
+        );
+        if ($oldPIN === '') {
+            // Voyager requires the PIN code to be set even if it was empty
+            $oldPIN = '     ';
+        }
+        $newPIN = trim(
+            htmlspecialchars($details['newPassword'], ENT_COMPAT, 'UTF-8')
+        );
+        $barcode = htmlspecialchars($patron['cat_username'], ENT_COMPAT, 'UTF-8');
+
+        $xml =  <<<EOT
+<?xml version="1.0" encoding="UTF-8"?>
+<ser:serviceParameters
+xmlns:ser="http://www.endinfosys.com/Voyager/serviceParameters">
+   <ser:parameters>
+      <ser:parameter key="oldPatronPIN">
+         <ser:value>$oldPIN</ser:value>
+      </ser:parameter>
+      <ser:parameter key="newPatronPIN">
+         <ser:value>$newPIN</ser:value>
+      </ser:parameter>
+   </ser:parameters>
+   <ser:patronIdentifier lastName="$lastname" patronHomeUbId="$ubId" patronId="$id">
+      <ser:authFactor type="B">$barcode</ser:authFactor>
+   </ser:patronIdentifier>
+</ser:serviceParameters>
+EOT;
+
+        $result = $this->makeRequest(
+            array('ChangePINService' => false), array(), 'POST', $xml
+        );
+
+        $result->registerXPathNamespace(
+            'ser', 'http://www.endinfosys.com/Voyager/serviceParameters'
+        );
+        $error = $result->xpath("//ser:message[@type='error']");
+        if (!empty($error)) {
+            $error = reset($error);
+            if ($error->attributes()->errorCode
+                == 'com.endinfosys.voyager.patronpin.PatronPIN.ValidateException'
+            ) {
+                return array(
+                    'success' => false, 'status' => 'authentication_error_invalid'
+                );
+            }
+            if ($error->attributes()->errorCode
+                ==
+                'com.endinfosys.voyager.patronpin.PatronPIN.ValidateUniqueException'
+            ) {
+                return array(
+                    'success' => false, 'status' => 'password_error_not_unique'
+                );
+            }
+            if ($error->attributes()->errorCode
+                ==
+                'com.endinfosys.voyager.patronpin.PatronPIN.ValidateLengthException'
+            ) {
+                // This issue should not be encountered if the settings are correct.
+                // Log an error and let through for an exception
+                $this->error('ValidateLengthException encountered when trying to'
+                    . ' change patron PIN. Verify PIN length settings.');
+            }
+            throw new ILSException((string)$error);
+        }
+        return array('success' => true, 'status' => 'change_password_ok');
+    }
+
+    /**
+     * Helper method to determine whether or not a certain method can be
+     * called on this driver.  Required method for any smart drivers.
+     *
+     * @param string $method The name of the called method.
+     * @param array  $params Array of passed parameters
+     *
+     * @return bool True if the method can be called with the given parameters,
+     * false otherwise.
+     */
+    public function supportsMethod($method, $params)
+    {
+        // Special case: change password is only available if properly configured.
+        if ($method == 'changePassword') {
+            return isset($this->config['changePassword']);
+        }
+        return is_callable(array($this, $method));
+    }
 }
diff --git a/themes/blueprint/templates/Auth/ILS/newpassword.phtml b/themes/blueprint/templates/Auth/ILS/newpassword.phtml
new file mode 100644
index 00000000000..a5c74c252e1
--- /dev/null
+++ b/themes/blueprint/templates/Auth/ILS/newpassword.phtml
@@ -0,0 +1,12 @@
+<? if (isset($this->username)): ?>
+  <label class="span-4"><?=$this->transEsc('Username') ?>:</label>
+  <input type="text" disabled value="<?=$this->username ?>"/><br/>
+<? endif; ?>
+<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
+  <label class="span-4"><?=$this->transEsc('old_password') ?>:</label>
+  <input type="password" name="oldpwd"/><br/>
+<? endif; ?>
+<label class="span-4"><?=$this->transEsc('new_password') ?>:</label>
+<input type="password" name="password"/><br/>
+<label class="span-4"><?=$this->transEsc('confirm_new_password') ?>:</label>
+<input type="password" name="password2"/><br/>
\ No newline at end of file
diff --git a/themes/blueprint/templates/Auth/MultiILS/newpassword.phtml b/themes/blueprint/templates/Auth/MultiILS/newpassword.phtml
new file mode 100644
index 00000000000..a5c74c252e1
--- /dev/null
+++ b/themes/blueprint/templates/Auth/MultiILS/newpassword.phtml
@@ -0,0 +1,12 @@
+<? if (isset($this->username)): ?>
+  <label class="span-4"><?=$this->transEsc('Username') ?>:</label>
+  <input type="text" disabled value="<?=$this->username ?>"/><br/>
+<? endif; ?>
+<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
+  <label class="span-4"><?=$this->transEsc('old_password') ?>:</label>
+  <input type="password" name="oldpwd"/><br/>
+<? endif; ?>
+<label class="span-4"><?=$this->transEsc('new_password') ?>:</label>
+<input type="password" name="password"/><br/>
+<label class="span-4"><?=$this->transEsc('confirm_new_password') ?>:</label>
+<input type="password" name="password2"/><br/>
\ No newline at end of file
diff --git a/themes/bootstrap/templates/Auth/ILS/newpassword.phtml b/themes/bootstrap/templates/Auth/ILS/newpassword.phtml
new file mode 100644
index 00000000000..40189de011f
--- /dev/null
+++ b/themes/bootstrap/templates/Auth/ILS/newpassword.phtml
@@ -0,0 +1,28 @@
+<? if (isset($this->username)): ?>
+  <div class="control-group">
+    <label class="control-label"><?=$this->transEsc('Username') ?>:</label>
+    <div class="controls">
+      <span class="input-xlarge uneditable-input"><?=$this->username ?></span>
+    </div>
+  </div>
+<? endif; ?>
+<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
+  <div class="control-group">
+    <label class="control-label"><?=$this->transEsc('old_password') ?>:</label>
+    <div class="controls">
+      <input type="password" name="oldpwd"/>
+    </div>
+  </div>
+<? endif; ?>
+<div class="control-group">
+  <label class="control-label"><?=$this->transEsc('new_password') ?>:</label>
+  <div class="controls">
+    <input type="password" name="password"/>
+  </div>
+</div>
+<div class="control-group">
+  <label class="control-label"><?=$this->transEsc('confirm_new_password') ?>:</label>
+  <div class="controls">
+    <input type="password" name="password2"/>
+  </div>
+</div>
\ No newline at end of file
diff --git a/themes/bootstrap/templates/Auth/MultiILS/newpassword.phtml b/themes/bootstrap/templates/Auth/MultiILS/newpassword.phtml
new file mode 100644
index 00000000000..40189de011f
--- /dev/null
+++ b/themes/bootstrap/templates/Auth/MultiILS/newpassword.phtml
@@ -0,0 +1,28 @@
+<? if (isset($this->username)): ?>
+  <div class="control-group">
+    <label class="control-label"><?=$this->transEsc('Username') ?>:</label>
+    <div class="controls">
+      <span class="input-xlarge uneditable-input"><?=$this->username ?></span>
+    </div>
+  </div>
+<? endif; ?>
+<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
+  <div class="control-group">
+    <label class="control-label"><?=$this->transEsc('old_password') ?>:</label>
+    <div class="controls">
+      <input type="password" name="oldpwd"/>
+    </div>
+  </div>
+<? endif; ?>
+<div class="control-group">
+  <label class="control-label"><?=$this->transEsc('new_password') ?>:</label>
+  <div class="controls">
+    <input type="password" name="password"/>
+  </div>
+</div>
+<div class="control-group">
+  <label class="control-label"><?=$this->transEsc('confirm_new_password') ?>:</label>
+  <div class="controls">
+    <input type="password" name="password2"/>
+  </div>
+</div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/Auth/ILS/newpassword.phtml b/themes/bootstrap3/templates/Auth/ILS/newpassword.phtml
new file mode 100644
index 00000000000..04c0fbce37e
--- /dev/null
+++ b/themes/bootstrap3/templates/Auth/ILS/newpassword.phtml
@@ -0,0 +1,31 @@
+<? if (isset($this->username)): ?>
+  <div class="form-group">
+    <label class="col-sm-3 control-label"><?=$this->transEsc('Username') ?>:</label>
+    <div class="col-sm-9">
+      <p class="form-control-static"><?=$this->username ?></p>
+    </div>
+  </div>
+<? endif; ?>
+<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
+  <div class="form-group">
+    <label class="col-sm-3 control-label"><?=$this->transEsc('old_password') ?>:</label>
+    <div class="col-sm-9">
+      <input type="password" name="oldpwd" class="form-control"/>
+      <div class="help-block with-errors"></div>
+    </div>
+  </div>
+<? endif; ?>
+<div class="form-group">
+  <label class="col-sm-3 control-label"><?=$this->transEsc('new_password') ?>:</label>
+  <div class="col-sm-9">
+    <input type="password" id="password" name="password" class="form-control" required aria-required="true"<?=isset($this->passwordPolicy['minLength']) ? ' data-minlength="' . $this->passwordPolicy['minLength'] . '" data-minlength-error="' . $this->escapeHtmlAttr($this->translate('password_minimum_length', array('%%minlength%%' => $this->passwordPolicy['minLength']))) . '"' : ''?><?=isset($this->passwordPolicy['maxLength']) ? ' maxlength="' . $this->passwordPolicy['maxLength'] . '"' : ''?>/>
+    <div class="help-block with-errors"></div>
+  </div>
+</div>
+<div class="form-group">
+  <label class="col-sm-3 control-label"><?=$this->transEsc('confirm_new_password') ?>:</label>
+  <div class="col-sm-9">
+    <input type="password" name="password2" class="form-control" required aria-required="true" data-match="#password" data-match-error="<?=$this->escapeHtmlAttr($this->translate('Passwords do not match'))?>"/>
+    <div class="help-block with-errors"></div>
+  </div>
+</div>
diff --git a/themes/bootstrap3/templates/Auth/MultiILS/newpassword.phtml b/themes/bootstrap3/templates/Auth/MultiILS/newpassword.phtml
new file mode 100644
index 00000000000..1c6e7e2bfba
--- /dev/null
+++ b/themes/bootstrap3/templates/Auth/MultiILS/newpassword.phtml
@@ -0,0 +1,30 @@
+<? if (isset($this->username)): ?>
+  <div class="form-group">
+    <label class="col-sm-3 control-label"><?=$this->transEsc('Username') ?>:</label>
+    <div class="col-sm-9">
+      <p class="form-control-static"><?=$this->username ?></p>
+    </div>
+  </div>
+<? endif; ?>
+<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
+  <div class="form-group">
+    <label class="col-sm-3 control-label"><?=$this->transEsc('old_password') ?>:</label>
+    <div class="col-sm-9">
+      <input type="password" name="oldpwd" class="form-control"/>
+    </div>
+  </div>
+<? endif; ?>
+<div class="form-group">
+  <label class="col-sm-3 control-label"><?=$this->transEsc('new_password') ?>:</label>
+  <div class="col-sm-9">
+    <input type="password" id="password" name="password" class="form-control" required aria-required="true"<?=isset($this->passwordPolicy['minLength']) ? ' data-minlength="' . $this->passwordPolicy['minLength'] . '" data-minlength-error="' . $this->escapeHtmlAttr($this->translate('password_minimum_length', array('%%minlength%%' => $this->passwordPolicy['minLength']))) . '"' : ''?><?=isset($this->passwordPolicy['maxLength']) ? ' maxlength="' . $this->passwordPolicy['maxLength'] . '"' : ''?>/>
+    <div class="help-block with-errors"></div>
+  </div>
+</div>
+<div class="form-group">
+  <label class="col-sm-3 control-label"><?=$this->transEsc('confirm_new_password') ?>:</label>
+  <div class="col-sm-9">
+    <input type="password" name="password2" class="form-control" required aria-required="true" data-match="#password" data-match-error="<?=$this->escapeHtmlAttr($this->translate('Passwords do not match'))?>"/>
+    <div class="help-block with-errors"></div>
+  </div>
+</div>
diff --git a/themes/jquerymobile/templates/Auth/ILS/newpassword.phtml b/themes/jquerymobile/templates/Auth/ILS/newpassword.phtml
new file mode 100644
index 00000000000..4c0b2d107fd
--- /dev/null
+++ b/themes/jquerymobile/templates/Auth/ILS/newpassword.phtml
@@ -0,0 +1,15 @@
+<div data-role="fieldcontain" class="ui-field-contain ui-body ui-br">
+  <? if (isset($this->username)): ?>
+    <input type="hidden" name="username" value="<?=$this->username ?>"/>
+    <label class="ui-input-text"><?=$this->transEsc('Username') ?>:</label>
+    <input type="text" name="username" id="username" value="<?=$this->username ?>" disabled class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset" style="border:1px solid #CCC;box-shadow:rgba(0, 0, 0, 0.1) 0px 1px 4px 0px inset;color:#777"/><br/>
+  <? endif; ?>
+  <? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
+    <label for="oldpwd" class="ui-input-text"><?=$this->transEsc('old_password') ?>:</label>
+    <input type="password" name="oldpwd" id="oldpwd" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
+  <? endif; ?>
+  <label for="password" class="ui-input-text"><?=$this->transEsc('new_password') ?>:</label>
+  <input type="password" name="password" id="password" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
+  <label for="password2" class="ui-input-text"><?=$this->transEsc('confirm_new_password') ?>:</label>
+  <input type="password" name="password2" id="password2" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
+</div>
\ No newline at end of file
-- 
GitLab