diff --git a/module/VuFind/src/VuFind/ILS/Driver/Unicorn.php b/module/VuFind/src/VuFind/ILS/Driver/Unicorn.php
index 023353ff304..650b6386d4d 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Unicorn.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Unicorn.php
@@ -1286,18 +1286,6 @@ protected function formatDateTime($time)
return $dateTimeString;
}
- /**
- * Convert the given ISO-8859-1 string to UTF-8 if it is not already UTF-8.
- *
- * @param string $s The string to convert.
- *
- * @return string The input string converted to UTF-8
- */
- protected function toUTF8($s)
- {
- return (mb_detect_encoding($s, 'UTF-8') == 'UTF-8') ? $s : utf8_encode($s);
- }
-
/**
* Given a location field, return the values relevant to VuFind.
*
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
index 26a9829a04e..af7509e114f 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
@@ -492,7 +492,7 @@ protected function getStatusData($sqlRows)
'status_array' => [$row['STATUS']],
'location' => $row['TEMP_LOCATION'] > 0
? $this->getLocationName($row['TEMP_LOCATION'])
- : utf8_encode($row['LOCATION']),
+ : $this->utf8Encode($row['LOCATION']),
'reserve' => $row['ON_RESERVE'],
'callnumber' => $row['CALLNUMBER'],
'item_sort_seq' => $row['ITEM_SEQUENCE_NUMBER'],
@@ -846,7 +846,7 @@ protected function getPurchaseHistoryData($id)
$raw = $processed = [];
// Collect raw data:
while ($row = $sqlStmt->fetch(PDO::FETCH_ASSOC)) {
- $raw[] = $row['MFHD_ID'] . '||' . utf8_encode($row['ENUMCHRON']);
+ $raw[] = $row['MFHD_ID'] . '||' . $this->utf8Encode($row['ENUMCHRON']);
}
// Deduplicate data and format it:
foreach (array_unique($raw) as $current) {
@@ -986,7 +986,7 @@ protected function getLocationName($id)
$bind = ['id' => $id];
$sqlStmt = $this->executeSQL($sql, $bind);
$sqlRow = $sqlStmt->fetch(PDO::FETCH_ASSOC);
- $cache[$id] = utf8_encode($sqlRow['LOCATION']);
+ $cache[$id] = $this->utf8Encode($sqlRow['LOCATION']);
}
return $cache[$id];
@@ -1008,7 +1008,7 @@ protected function processHoldingRow($sqlRow)
'status' => $sqlRow['STATUS'],
'location' => $sqlRow['TEMP_LOCATION'] > 0
? $this->getLocationName($sqlRow['TEMP_LOCATION'])
- : utf8_encode($sqlRow['LOCATION']),
+ : $this->utf8Encode($sqlRow['LOCATION']),
'reserve' => $sqlRow['ON_RESERVE'],
'callnumber' => $sqlRow['CALLNUMBER'],
'barcode' => $sqlRow['ITEM_BARCODE'],
@@ -1110,7 +1110,7 @@ protected function processHoldingData($data, $id, $patron = null)
$holding[$i] += [
'availability' => $availability['available'],
'enumchron' => isset($row['ITEM_ENUM'])
- ? utf8_encode($row['ITEM_ENUM']) : null,
+ ? $this->utf8Encode($row['ITEM_ENUM']) : null,
'duedate' => $this->processHoldingDueDate($row),
'number' => $number,
'requests_placed' => $requests_placed,
@@ -1293,7 +1293,7 @@ public function patronLogin($username, $login)
}
try {
- $bindUsername = strtolower(utf8_decode($username));
+ $bindUsername = strtolower(mb_convert_encoding($username, 'ISO-8859-1', 'UTF-8'));
$compareLogin = mb_strtolower($login, 'UTF-8');
$sqlStmt = $this->executeSQL($sql, [':username' => $bindUsername]);
@@ -1301,10 +1301,10 @@ public function patronLogin($username, $login)
// rows just to be safe
while ($row = $sqlStmt->fetch(PDO::FETCH_ASSOC)) {
$primary = null !== $row['LOGIN']
- ? mb_strtolower(utf8_encode($row['LOGIN']), 'UTF-8')
+ ? mb_strtolower($this->utf8Encode($row['LOGIN']), 'UTF-8')
: null;
$fallback = $fallbackLoginField && null === $row['LOGIN']
- ? mb_strtolower(utf8_encode($row['FALLBACK_LOGIN']), 'UTF-8')
+ ? mb_strtolower($this->utf8Encode($row['FALLBACK_LOGIN']), 'UTF-8')
: null;
if (
@@ -1314,9 +1314,9 @@ public function patronLogin($username, $login)
&& $fallback == $compareLogin)
) {
return [
- 'id' => utf8_encode($row['PATRON_ID']),
- 'firstname' => utf8_encode($row['FIRST_NAME']),
- 'lastname' => utf8_encode($row['LAST_NAME']),
+ 'id' => $this->utf8Encode($row['PATRON_ID']),
+ 'firstname' => $this->utf8Encode($row['FIRST_NAME']),
+ 'lastname' => $this->utf8Encode($row['LAST_NAME']),
'cat_username' => $username,
'cat_password' => $login,
// There's supposed to be a getPatronEmailAddress stored
@@ -1474,10 +1474,10 @@ protected function processMyTransactionsData($sqlRow, $patron = false)
$transaction = [
'id' => $sqlRow['BIB_ID'],
'item_id' => $sqlRow['ITEM_ID'],
- 'barcode' => utf8_encode($sqlRow['ITEM_BARCODE']),
+ 'barcode' => $this->utf8Encode($sqlRow['ITEM_BARCODE']),
'duedate' => $dueDate,
'dueStatus' => $dueStatus,
- 'volume' => str_replace('v.', '', utf8_encode($sqlRow['ITEM_ENUM'])),
+ 'volume' => str_replace('v.', '', $this->utf8Encode($sqlRow['ITEM_ENUM'])),
'publication_year' => $sqlRow['YEAR'],
'title' => empty($sqlRow['TITLE_BRIEF'])
? $sqlRow['TITLE'] : $sqlRow['TITLE_BRIEF'],
@@ -1495,7 +1495,7 @@ protected function processMyTransactionsData($sqlRow, $patron = false)
}
if (!empty($this->config['Loans']['display_borrowing_location'])) {
$transaction['borrowingLocation']
- = utf8_encode($sqlRow['BORROWING_LOCATION']);
+ = $this->utf8Encode($sqlRow['BORROWING_LOCATION']);
}
return $transaction;
@@ -1619,7 +1619,7 @@ protected function processFinesData($sqlRow)
}
return ['amount' => $sqlRow['FINE_FEE_AMOUNT'],
- 'fine' => utf8_encode($sqlRow['FINE_FEE_DESC']),
+ 'fine' => $this->utf8Encode($sqlRow['FINE_FEE_DESC']),
'balance' => $sqlRow['FINE_FEE_BALANCE'],
'createdate' => $createDate,
'checkout' => $chargeDate,
@@ -1765,7 +1765,7 @@ protected function processMyHoldsData($sqlRow)
'available' => $available,
'reqnum' => $sqlRow['HOLD_RECALL_ID'],
'item_id' => $sqlRow['ITEM_ID'],
- 'volume' => str_replace('v.', '', utf8_encode($sqlRow['ITEM_ENUM'])),
+ 'volume' => str_replace('v.', '', $this->utf8Encode($sqlRow['ITEM_ENUM'])),
'publication_year' => $sqlRow['YEAR'],
'title' => empty($sqlRow['TITLE_BRIEF'])
? $sqlRow['TITLE'] : $sqlRow['TITLE_BRIEF'],
@@ -1957,13 +1957,13 @@ protected function processMyStorageRetrievalRequestsData($sqlRow)
return [
'id' => $sqlRow['BIB_ID'],
- 'status' => utf8_encode($sqlRow['STATUS_DESC']),
+ 'status' => $this->utf8Encode($sqlRow['STATUS_DESC']),
'statusDate' => $statusDate,
'location' => $this->getLocationName($sqlRow['PICKUP_LOCATION_ID']),
'create' => $createDate,
'processed' => $processedDate,
'expire' => $expireDate,
- 'reply' => utf8_encode($sqlRow['REPLY_NOTE']),
+ 'reply' => $this->utf8Encode($sqlRow['REPLY_NOTE']),
'available' => $available,
'canceled' => $sqlRow['STATUS'] == 7 ? $statusDate : false,
'reqnum' => $sqlRow['CALL_SLIP_ID'],
@@ -1971,10 +1971,10 @@ protected function processMyStorageRetrievalRequestsData($sqlRow)
'volume' => str_replace(
'v.',
'',
- utf8_encode($sqlRow['ITEM_ENUM'])
+ $this->utf8Encode($sqlRow['ITEM_ENUM'])
),
- 'issue' => utf8_encode($sqlRow['ITEM_CHRON']),
- 'year' => utf8_encode($sqlRow['ITEM_YEAR']),
+ 'issue' => $this->utf8Encode($sqlRow['ITEM_CHRON']),
+ 'year' => $this->utf8Encode($sqlRow['ITEM_YEAR']),
'title' => empty($sqlRow['TITLE_BRIEF'])
? $sqlRow['TITLE'] : $sqlRow['TITLE_BRIEF'],
];
@@ -2042,23 +2042,23 @@ public function getMyProfile($patron)
$patron = [];
while ($row = $sqlStmt->fetch(PDO::FETCH_ASSOC)) {
if (!empty($row['FIRST_NAME'])) {
- $patron['firstname'] = utf8_encode($row['FIRST_NAME']);
+ $patron['firstname'] = $this->utf8Encode($row['FIRST_NAME']);
}
if (!empty($row['LAST_NAME'])) {
- $patron['lastname'] = utf8_encode($row['LAST_NAME']);
+ $patron['lastname'] = $this->utf8Encode($row['LAST_NAME']);
}
if (!empty($row['PHONE_NUMBER'])) {
if ($primaryPhoneType === $row['PHONE_DESC']) {
- $patron['phone'] = utf8_encode($row['PHONE_NUMBER']);
+ $patron['phone'] = $this->utf8Encode($row['PHONE_NUMBER']);
} elseif ($mobilePhoneType === $row['PHONE_DESC']) {
- $patron['mobile_phone'] = utf8_encode($row['PHONE_NUMBER']);
+ $patron['mobile_phone'] = $this->utf8Encode($row['PHONE_NUMBER']);
}
}
if (!empty($row['PATRON_GROUP_NAME'])) {
- $patron['group'] = utf8_encode($row['PATRON_GROUP_NAME']);
+ $patron['group'] = $this->utf8Encode($row['PATRON_GROUP_NAME']);
}
$validator = new EmailAddressValidator();
- $addr1 = utf8_encode($row['ADDRESS_LINE1']);
+ $addr1 = $this->utf8Encode($row['ADDRESS_LINE1']);
if ($validator->isValid($addr1)) {
$patron['email'] = $addr1;
} elseif (!isset($patron['address1'])) {
@@ -2066,16 +2066,16 @@ public function getMyProfile($patron)
$patron['address1'] = $addr1;
}
if (!empty($row['ADDRESS_LINE2'])) {
- $patron['address2'] = utf8_encode($row['ADDRESS_LINE2']);
+ $patron['address2'] = $this->utf8Encode($row['ADDRESS_LINE2']);
}
if (!empty($row['ZIP_POSTAL'])) {
- $patron['zip'] = utf8_encode($row['ZIP_POSTAL']);
+ $patron['zip'] = $this->utf8Encode($row['ZIP_POSTAL']);
}
if (!empty($row['CITY'])) {
- $patron['city'] = utf8_encode($row['CITY']);
+ $patron['city'] = $this->utf8Encode($row['CITY']);
}
if (!empty($row['COUNTRY'])) {
- $patron['country'] = utf8_encode($row['COUNTRY']);
+ $patron['country'] = $this->utf8Encode($row['COUNTRY']);
}
}
}
@@ -2638,4 +2638,16 @@ protected function executeSQL($sql, $bind = [])
return $sqlStmt;
}
+
+ /**
+ * Convert string from ISO 8859-1 into UTF-8
+ *
+ * @param string $iso88591 String to convert
+ *
+ * @return string
+ */
+ protected function utf8Encode(string $iso88591): string
+ {
+ return mb_convert_encoding($iso88591, 'UTF-8', 'ISO-8859-1');
+ }
}
diff --git a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
index a818ce8d136..5ed7d5a1568 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
@@ -747,7 +747,7 @@ public function getPickUpLocations($patron = false, $holdDetails = null)
while ($row = $sqlStmt->fetch(PDO::FETCH_ASSOC)) {
$pickResponse[] = [
'locationID' => $row['LOCATION_ID'],
- 'locationDisplay' => utf8_encode($row['LOCATION_NAME']),
+ 'locationDisplay' => $this->utf8Encode($row['LOCATION_NAME']),
];
}
}
@@ -1007,7 +1007,7 @@ function ($s) {
while ($row = $sqlStmt->fetch(PDO::FETCH_ASSOC)) {
$results[] = [
'id' => $row['GROUP_ID'],
- 'name' => utf8_encode($row['GROUP_NAME']),
+ 'name' => $this->utf8Encode($row['GROUP_NAME']),
];
}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerRestfulTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerRestfulTest.php
index 71fe6c72414..0ddf55dec4e 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerRestfulTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerRestfulTest.php
@@ -29,6 +29,8 @@
namespace VuFindTest\ILS\Driver;
+use PDOStatement;
+use PHPUnit\Framework\MockObject\MockObject;
use VuFind\ILS\Driver\VoyagerRestful;
/**
@@ -42,6 +44,22 @@
*/
class VoyagerRestfulTest extends \VuFindTest\Unit\ILSDriverTestCase
{
+ /**
+ * Default configuration for driver
+ *
+ * @var array
+ */
+ protected $defaultConfig = [
+ 'Catalog' => ['database' => 'foo'],
+ 'WebServices' => [
+ 'host' => 'foo',
+ 'port' => 1234,
+ 'app' => 'bar',
+ 'dbKey' => 'fake',
+ 'patronHomeUbId' => 'baz',
+ ],
+ ];
+
/**
* Standard setup method.
*
@@ -51,4 +69,127 @@ public function setUp(): void
{
$this->driver = new VoyagerRestful(new \VuFind\Date\Converter());
}
+
+ /**
+ * Test encoding conversion in getPickupLocations()
+ *
+ * @return void
+ */
+ public function testGetPickupLocationsConversion(): void
+ {
+ $location = 'Tést';
+
+ // Create a mock SQL response
+ $mockResult = $this->createMock(PDOStatement::class);
+ $mockResult->method('fetch')->willReturnCallback(function () use ($location) {
+ static $called = false;
+ if ($called) {
+ return null;
+ }
+ $called = true;
+ return [
+ 'LOCATION_ID' => 1,
+ 'LOCATION_NAME' => mb_convert_encoding($location, 'ISO-8859-1', 'UTF-8'),
+ ];
+ });
+
+ // Use an anonymous class to override the executeSQL method for mocking purposes:
+ $driver = $this->getDriverWithMockSqlResponse($mockResult);
+ $this->assertEquals(
+ [
+ [
+ 'locationID' => 1,
+ 'locationDisplay' => $location,
+ ],
+ ],
+ $driver->getPickUpLocations()
+ );
+ }
+
+ /**
+ * Test that request groups are disabled by default.
+ *
+ * @return void
+ */
+ public function testGetRequestGroupsDefaultBehavior(): void
+ {
+ $this->assertFalse($this->driver->getRequestGroups(1, []));
+ }
+
+ /**
+ * Test encoding conversion in getRequestGroups()
+ *
+ * @return void
+ */
+ public function testGetRequestGroupsConversion(): void
+ {
+ $name = 'Tést';
+
+ // Create a mock SQL response
+ $mockResult = $this->createMock(PDOStatement::class);
+ $mockResult->method('fetch')->willReturnCallback(function () use ($name) {
+ static $called = false;
+ if ($called) {
+ return null;
+ }
+ $called = true;
+ return [
+ 'GROUP_ID' => 1,
+ 'GROUP_NAME' => mb_convert_encoding($name, 'ISO-8859-1', 'UTF-8'),
+ ];
+ });
+
+ // Use an anonymous class to override the executeSQL method for mocking purposes:
+ $driver = $this->getDriverWithMockSqlResponse($mockResult);
+ // Enable request groups
+ $driver->setConfig($this->defaultConfig + [
+ 'Holds' => ['extraHoldFields' => 'requestGroup'],
+ ]);
+ $driver->init();
+ $this->assertEquals(
+ [
+ [
+ 'id' => 1,
+ 'name' => $name,
+ ],
+ ],
+ $driver->getRequestGroups(1, [])
+ );
+ }
+
+ /**
+ * Get a VoyagerRestful driver customized to return a mock SQL response.
+ *
+ * @param MockObject&PDOStatement $mockResult Mock result to return from executeSQL
+ *
+ * @return VoyagerRestful
+ */
+ protected function getDriverWithMockSqlResponse(MockObject&PDOStatement $mockResult): VoyagerRestful
+ {
+ return new class ($mockResult) extends VoyagerRestful {
+ /**
+ * Constructor
+ *
+ * @param MockObject&PDOStatement $mockResult Mock result to return from executeSQL
+ */
+ public function __construct(protected MockObject&PDOStatement $mockResult)
+ {
+ parent::__construct(new \VuFind\Date\Converter());
+ }
+
+ /**
+ * Execute an SQL query
+ *
+ * @param string|array $sql SQL statement (string or array that includes
+ * bind params)
+ * @param array $bind Bind parameters (if $sql is string)
+ *
+ * @return PDOStatement
+ */
+ protected function executeSQL($sql, $bind = [])
+ {
+ return $this->mockResult;
+ }
+ };
+ }
}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerTest.php
index 130fe0e0850..9aefa3031bc 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerTest.php
@@ -29,6 +29,8 @@
namespace VuFindTest\ILS\Driver;
+use PDOStatement;
+use PHPUnit\Framework\MockObject\MockObject;
use VuFind\ILS\Driver\Voyager;
/**
@@ -110,4 +112,79 @@ public function testMarcParsing(): void
$results
);
}
+
+ /**
+ * Test that patron usernames are correctly encoded during login.
+ *
+ * @return void
+ */
+ public function testUsernameEncodingDuringLogin(): void
+ {
+ // Create a mock SQL response
+ $mockResult = $this->createMock(PDOStatement::class);
+ $mockResult->method('fetch')->willReturn(null);
+
+ $driver = $this->getDriverWithMockSqlResponse($mockResult);
+ $this->assertNull($driver->patronLogin('Tést', 'foo'));
+ $this->assertEquals([':username' => mb_convert_encoding('tést', 'ISO-8859-1', 'UTF-8')], $driver->lastBind);
+ $this->assertEquals(
+ 'SELECT PATRON.PATRON_ID, PATRON.FIRST_NAME, PATRON.LAST_NAME, PATRON.LAST_NAME as LOGIN '
+ . 'FROM .PATRON, .PATRON_BARCODE '
+ . 'WHERE PATRON.PATRON_ID = PATRON_BARCODE.PATRON_ID AND '
+ . 'lower(PATRON_BARCODE.PATRON_BARCODE) = :username AND PATRON_BARCODE.BARCODE_STATUS IN (1,4)',
+ $driver->lastSql
+ );
+ }
+
+ /**
+ * Get a Voyager driver customized to return a mock SQL response.
+ *
+ * @param MockObject&PDOStatement $mockResult Mock result to return from executeSQL
+ *
+ * @return Voyager
+ */
+ protected function getDriverWithMockSqlResponse(MockObject&PDOStatement $mockResult): Voyager
+ {
+ return new class ($mockResult) extends Voyager {
+ /**
+ * Last SQL statement passed to executeSQL
+ *
+ * @var string
+ */
+ public string $lastSql = '';
+
+ /**
+ * Last bind array passed to executeSQL
+ *
+ * @var array
+ */
+ public array $lastBind = [];
+
+ /**
+ * Constructor
+ *
+ * @param MockObject&PDOStatement $mockResult Mock result to return from executeSQL
+ */
+ public function __construct(protected MockObject&PDOStatement $mockResult)
+ {
+ parent::__construct(new \VuFind\Date\Converter());
+ }
+
+ /**
+ * Execute an SQL query
+ *
+ * @param string|array $sql SQL statement (string or array that includes
+ * bind params)
+ * @param array $bind Bind parameters (if $sql is string)
+ *
+ * @return PDOStatement
+ */
+ protected function executeSQL($sql, $bind = [])
+ {
+ $this->lastSql = $sql;
+ $this->lastBind = $bind;
+ return $this->mockResult;
+ }
+ };
+ }
}
diff --git a/themes/bootstrap3/templates/Helpers/copy-to-clipboard-button.phtml b/themes/bootstrap3/templates/Helpers/copy-to-clipboard-button.phtml
index 54c9e278873..1e024a98419 100644
--- a/themes/bootstrap3/templates/Helpers/copy-to-clipboard-button.phtml
+++ b/themes/bootstrap3/templates/Helpers/copy-to-clipboard-button.phtml
@@ -10,17 +10,17 @@
$script = <<