Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config/vufind/LOTS.ini
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ kohaOpacUrl = https://opac/
;forgot_password_regex =
forgot_password_regex = "/^\d{4}$/"

; Set to true to write password reset events to a separate log file.
; Log file will be created in the same directory as vufind.log (from config.ini [Logging] file setting).
; Log file name: password_reset.log
password_reset_log = true

[NewPatron]
enable=false
url= /koha-user/create
Expand Down Expand Up @@ -133,6 +138,11 @@ showAvailabilityInResults = false ; Set to false to hide the availability secti
enableDebug = false ; Set to true to enable debug logging
maxBranches = 100 ; Maximum number of branches to process

; LotsLerum-XXX
; When true, Exemplartyp/Avdelning/Placering are shown only in item collapse rows,
; not in the library summary row.
holdingsColumnsInCollapseOnly = false

[NotFinished]
showBankIDOption = false

Expand Down
1 change: 1 addition & 0 deletions languages/sv.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1830,3 +1830,4 @@ search_aria_description = "Sök i bibliotekskatalogen (titel, författare, ämne
; vufind v11
holds_descriptive_text = "Textbeskrivning"
select_page = "Sida"
Cancel = "Avbryt"
19 changes: 16 additions & 3 deletions module/LOTS/src/LOTS/Controller/ForgotPasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ForgotPasswordController extends \VuFind\Controller\AbstractBase implement
{
use \VuFindHttp\HttpServiceAwareTrait;
use \VuFind\ILS\Driver\OAuth2TokenTrait;
use PasswordResetLogTrait;

protected $koha_rest_config = null;
protected $oauth_token = null;
Expand Down Expand Up @@ -47,6 +48,10 @@ public function homeAction()
// Handle form submission
$username = $this->params()->fromPost('username');

if ($this->getRequest()->isPost() && empty($username)) {
$this->logPasswordReset('Password reset: form submitted with empty username');
}

if (!empty($username)) {
try {
// Search for patron using configured search fields
Expand All @@ -60,16 +65,23 @@ public function homeAction()
// Send email
$this->sendResetEmail($patron['email'], $token);

$this->logPasswordReset('Password reset: email sent to patron_id=' . $patron['patron_id']);
// Generic message (don't reveal if user exists)
$message = $this->translate('password_reset_email_sent');
$messageType = 'success';
} else {
// Log why reset was not sent (for troubleshooting), but show generic message
if (!$patron) {
$this->logPasswordReset('Password reset: patron not found for input=' . $username);
} else {
$this->logPasswordReset('Password reset: patron found but no email configured, patron_id=' . ($patron['patron_id'] ?? 'unknown'));
}
// Generic message (don't reveal if user exists or has no email)
$message = $this->translate('password_reset_email_sent');
$messageType = 'success';
}
} catch (\Exception $e) {
error_log('Password reset error: ' . $e->getMessage());
$this->logPasswordReset('Password reset error: ' . $e->getMessage());
$message = $this->translate('password_reset_error');
$messageType = 'error';
}
Expand Down Expand Up @@ -109,7 +121,7 @@ protected function findPatron(string $searchValue): ?array
$searchFields = array_intersect($searchFields, $validFields);

if (empty($searchFields)) {
error_log('No valid patron search fields configured');
$this->logPasswordReset('No valid patron search fields configured');
return null;
}

Expand Down Expand Up @@ -169,7 +181,7 @@ protected function searchPatronByField(string $field, string $value): ?array
'userid' => $patron['userid'] ?? null
];
} catch (\Exception $e) {
error_log("Patron search by $field failed: " . $e->getMessage());
$this->logPasswordReset("Patron search by $field failed: " . $e->getMessage());
return null;
}
}
Expand Down Expand Up @@ -228,4 +240,5 @@ protected function getOAuth2Token(): string
}
return $token->getHeaderValue();
}

}
40 changes: 40 additions & 0 deletions module/LOTS/src/LOTS/Controller/PasswordResetLogTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* Trait for logging password reset events to a dedicated log file
*/

namespace LOTS\Controller;

trait PasswordResetLogTrait
{
/**
* Log password reset event to dedicated log file (if enabled in LOTS.ini).
* Always calls error_log() as fallback; additionally writes to
* password_reset.log in the same directory as vufind.log when
* PasswordRecovery.password_reset_log = true in LOTS.ini.
*
* @param string $message Log message
*
* @return void
*/
protected function logPasswordReset(string $message): void
{
$lotsConfig = $this->getConfig('LOTS');
$enabled = $lotsConfig->PasswordRecovery->password_reset_log ?? false;

$logMessage = date('Y-m-d H:i:s') . ' [PasswordReset] ' . $message . PHP_EOL;

if ($enabled) {
// Derive log dir from main config [Logging] file setting
$mainConfig = $this->getConfig();
$logFile = $mainConfig->Logging->file ?? '';
// Strip alert level suffix (e.g. "/var/log/vufind/vufind.log:alert,error")
$logFilePath = explode(':', $logFile)[0];
$logDir = $logFilePath ? dirname($logFilePath) : '/var/log/vufind';
$resetLogFile = rtrim($logDir, '/') . '/password_reset.log';
error_log($logMessage, 3, $resetLogFile);
} else {
error_log('[PasswordReset] ' . $message);
}
}
}
28 changes: 14 additions & 14 deletions module/LOTS/src/LOTS/Controller/ResetPasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ResetPasswordController extends \VuFind\Controller\AbstractBase implements
{
use \VuFindHttp\HttpServiceAwareTrait;
use \VuFind\ILS\Driver\OAuth2TokenTrait;
use PasswordResetLogTrait;

protected $koha_rest_config = null;
protected $oauth_token = null;
Expand All @@ -26,8 +27,8 @@ class ResetPasswordController extends \VuFind\Controller\AbstractBase implements
public function homeAction()
{
$token = $this->params()->fromQuery('token') ?? $this->params()->fromPost('token');
error_log("DEBUG: Received token from URL: " . var_export($token, true));
error_log("DEBUG: Token length: " . strlen($token));
$this->logPasswordReset("DEBUG: Received token from URL: " . var_export($token, true));
$this->logPasswordReset("DEBUG: Token length: " . strlen($token));
$message = '';
$messageType = 'info';
$tokenValid = false;
Expand All @@ -40,7 +41,7 @@ public function homeAction()
// Validate token
$tokenTable = $this->getTable('PasswordResetToken');
$tokenData = $tokenTable->getValidToken($token);
error_log("DEBUG: Token data from DB: " . var_export($tokenData, true));
$this->logPasswordReset("DEBUG: Token data from DB: " . var_export($tokenData, true));

if (!$tokenData) {
$message = $this->translate('password_reset_token_expired');
Expand All @@ -64,15 +65,13 @@ public function homeAction()
// Mark token as used
$tokenTable->markAsUsed($token);

$message = $this->translate('password_reset_success');
$messageType = 'success';
$tokenValid = false; // Hide form

// Redirect to login after 3 seconds
$this->layout()->setVariable('redirectUrl', '/vufind/MyResearch/Home');
$this->layout()->setVariable('redirectDelay', 3000);
// Redirect to login with success flash message
// Important: do NOT render on /ResetPassword?token=... URL
// so VuFind does not store it as followup after login
$this->flashMessenger()->addMessage('password_reset_success', 'success');
return $this->redirect()->toRoute('myresearch-userlogin');
} catch (\Exception $e) {
error_log('Password update error: ' . $e->getMessage());
$this->logPasswordReset('Password update error: ' . $e->getMessage());
$message = $this->translate('password_reset_update_error');
$messageType = 'error';
}
Expand Down Expand Up @@ -145,7 +144,7 @@ protected function updatePatronPassword(string $patronId, string $newPin): void
$this->koha_rest_config = $this->getConfig('KohaRest');
$this->oauth_token = $this->getOAuth2Token();

error_log("DEBUG: Updating password for patron: " . $patronId);
$this->logPasswordReset("DEBUG: Updating password for patron: " . $patronId);

// Use correct endpoint: POST /patrons/{id}/password
$data = [
Expand All @@ -155,7 +154,7 @@ protected function updatePatronPassword(string $patronId, string $newPin): void

$response = $this->json_http("POST", "/patrons/$patronId/password", json_encode($data));

error_log("Koha password update response: " . $response);
$this->logPasswordReset("Koha password update response: " . $response);

// Check if response contains error
$result = json_decode($response, true);
Expand Down Expand Up @@ -215,6 +214,7 @@ public function json_http($method, $api, $postData = null)

// Get the response body/JSON
return $response->getBody();
}

}

}
38 changes: 27 additions & 11 deletions module/LOTS/src/LOTS/Db/Table/PasswordResetToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@
* @link https://vufind.org Main Site
*/
namespace LOTS\Db\Table;

use Laminas\Db\Adapter\Adapter;
use VuFind\Db\Row\RowGateway;
use VuFind\Db\Table\PluginManager;

/**
* Table Definition for lots_password_reset_tokens
*
Expand Down Expand Up @@ -42,6 +40,28 @@ public function __construct(
$table = 'lots_password_reset_tokens'
) {
parent::__construct($adapter, $tm, $cfg, $rowObj, $table);
$this->ensureTableExists();
}

/**
* Create the table if it does not exist
*
* @return void
*/
protected function ensureTableExists(): void
{
$sql = "CREATE TABLE IF NOT EXISTS lots_password_reset_tokens (
id int(11) NOT NULL AUTO_INCREMENT,
user_id varchar(255) NOT NULL,
token varchar(255) NOT NULL,
email varchar(255) NOT NULL,
created_at timestamp NOT NULL DEFAULT current_timestamp(),
expires_at timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
used tinyint(4) DEFAULT 0,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci";

$this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE);
}

/**
Expand All @@ -54,21 +74,17 @@ public function __construct(
*/
public function createToken(string $userId, string $email): string
{
// Generate secure random token
$token = bin2hex(random_bytes(32));

// Calculate expiration (2 days from now)
$expiresAt = date('Y-m-d H:i:s', strtotime('+2 days'));

// Insert into database

$this->insert([
'user_id' => $userId,
'token' => $token,
'email' => $email,
'expires_at' => $expiresAt,
'used' => 0
]);

return $token;
}

Expand All @@ -88,13 +104,13 @@ public function getValidToken(string $token): ?array
]);
$select->where->lessThanOrEqualTo('created_at', date('Y-m-d H:i:s'));
$select->where->greaterThan('expires_at', date('Y-m-d H:i:s'));

$result = $this->selectWith($select)->current();

if (!$result) {
return null;
}

return [
'id' => $result->id,
'user_id' => $result->user_id,
Expand Down
37 changes: 32 additions & 5 deletions themes/lots/templates/RecordTab/holdingsils.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
}
$columns = array_map('trim', explode(',', $defaultColumns));
$lots_showItemsInVufind = $config->Record->showItemsInVufind ?? false;
// LotsLerum-XXX: When true, Exemplartyp/Avdelning/Placering are shown only in
// item collapse rows, not in the library summary row.
$holdingsColumnsInCollapseOnly = $config->Record->holdingsColumnsInCollapseOnly ?? false;
if ($lots_showItemsInVufind) {
$global_total_items = 0;
$global_avail_items = 0;
Expand Down Expand Up @@ -169,21 +172,31 @@
</td>
<?php
if ($addTypeColumn) {
echo "<td>".$this->transEsc($holding["items"]["0"]['item']['item_type_id'])."</td>";
if ($holdingsColumnsInCollapseOnly) {
echo "<td></td>";
} else {
echo "<td>".$this->transEsc($holding["items"]["0"]['item']['item_type_id'])."</td>";
}
}
?> <td class="main-department">
<?php if (!$holdingsColumnsInCollapseOnly): ?>
<?=(isset($holding["items"][0]["item"]["collection_code_description"])) ? $this->transEsc($holding["items"][0]["item"]["collection_code_description"]) : ''; ?>
<?php
if (!$addPlacementColumn) {
echo (isset($holding["items"][0]["item"]["callnumber"]) && !empty($holding["items"][0]["item"]["callnumber"])) ? ''.$this->transEsc($holding["items"][0]["item"]["callnumber"]) : '';
echo (isset($holding["items"][0]["item"]["location_description"])) ? ', '.$this->transEsc($holding["items"][0]["item"]["location_description"]) : '';
}
?>
<?php endif; ?>
</td>

<?php
if ($addPlacementColumn) {
echo "<td class='main-placement'>".(isset($holding["items"][0]["item"]["callnumber"]) && !empty($holding["items"][0]["item"]["callnumber"]) ? $this->transEsc($holding["items"][0]["item"]["callnumber"]) : '').(isset($holding["items"][0]["item"]["location_description"]) ? ', '.$this->transEsc($holding["items"][0]["item"]["location_description"]) : '')."</td>";
if ($holdingsColumnsInCollapseOnly) {
echo "<td class='main-placement'></td>";
} else {
echo "<td class='main-placement'>".(isset($holding["items"][0]["item"]["callnumber"]) && !empty($holding["items"][0]["item"]["callnumber"]) ? $this->transEsc($holding["items"][0]["item"]["callnumber"]) : '').(isset($holding["items"][0]["item"]["location_description"]) ? ', '.$this->transEsc($holding["items"][0]["item"]["location_description"]) : '')."</td>";
}
}
?>
</tr>
Expand All @@ -204,12 +217,26 @@
try {
echo $this->context($this)->renderInContext(
'RecordTab/holdingsils/' . $this->tab->getTemplate() . '.phtml',
['holding' => $row, 'iteration'=>$holdDetailIterate, 'hideDetailReserveEvenIfInKoha'=>$hideDetailReserveEvenIfInKoha]
[
'holding' => $row,
'iteration' => $holdDetailIterate,
'hideDetailReserveEvenIfInKoha' => $hideDetailReserveEvenIfInKoha,
'addTypeColumn' => $addTypeColumn,
'addPlacementColumn' => $addPlacementColumn,
'holdingsColumnsInCollapseOnly' => $holdingsColumnsInCollapseOnly,
]
);
} catch (Exception $e) {
echo $this->context($this)->renderInContext(
'RecordTab/holdingsils/standard.phtml',
['holding' => $row, 'iteration'=>$holdDetailIterate, 'hideDetailReserveEvenIfInKoha'=>$hideDetailReserveEvenIfInKoha]
[
'holding' => $row,
'iteration' => $holdDetailIterate,
'hideDetailReserveEvenIfInKoha' => $hideDetailReserveEvenIfInKoha,
'addTypeColumn' => $addTypeColumn,
'addPlacementColumn' => $addPlacementColumn,
'holdingsColumnsInCollapseOnly' => $holdingsColumnsInCollapseOnly,
]
);
}
?>
Expand Down Expand Up @@ -313,7 +340,7 @@
<?=$statusColorClass == 'item-avail' ? '<i class="fa fa-check-circle"></i>' : '<i class="fa fa-times-circle"></i>'?>
<?=$this->transEsc($holdStatus)?>
</span>

<!-- Show inventory details if enabled -->
<?php if ($showInventoryDetails && $lots_showItemsInVufind && $library_total_items > 0): ?>
<div class="inventory-info mt-2">
Expand Down
4 changes: 4 additions & 0 deletions themes/lots/templates/RecordTab/holdingsils/standard.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ if (!($hideDetailReserveEvenIfInKoha ?? false) && strlen($holding['barcode'] ??
<?php endif; ?>
<?php endif; ?>
</td>
<?php // Render type column in collapse row if enabled ?>
<?php if ($addTypeColumn ?? false): ?>
<td><?=$this->transEsc($holding["item"]['item_type_id'] ?? '')?></td>
<?php endif; ?>
<td class="collapse-department"><span class="text-muted"><?=$holding["item"]["collection_code_description"]?></span>
</td>
<td class="collapse-placement"><span class="text-muted"><?=$holding["callnumber"]?><?php if (!empty($holding["item_location_description"])): ?>, <?=$holding["item_location_description"]?><?php endif; ?></span>
Expand Down
Loading