Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
6 changes: 6 additions & 0 deletions config/vufind/Folio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ available[] = "Open - Awaiting pickup"
in_transit[] = "Open - In transit"
in_transit[] = "Open - Awaiting delivery"

; POST to this webhook address (with no body) after a successful hold is placed.
; A web service hosted there may be used for follow-up actions.
; Example: generating an email to circulation staff
; (https://github.com/lehigh-university-libraries/folio-email-new-requests)
;webhook = https://some.url/some-webhook-path

[Holdings]
; This setting controls the sort order used when retrieving items from FOLIO for the
; holdings display; it should be a space-separated prioritized list of item record
Expand Down
1 change: 1 addition & 0 deletions module/VuFind/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
'VuFind\Connection\ExternalVuFind' => 'VuFind\Connection\ExternalVuFindFactory',
'VuFind\Connection\LibGuides' => 'VuFind\Connection\LibGuidesFactory',
'VuFind\Connection\Relais' => 'VuFind\Connection\RelaisFactory',
'VuFind\Connection\Webhook' => 'Laminas\ServiceManager\Factory\InvokableFactory',
'VuFind\Content\PageLocator' => 'VuFind\Content\PageLocatorFactory',
'VuFind\Content\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
'VuFind\Content\AuthorNotes\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
Expand Down
81 changes: 81 additions & 0 deletions module/VuFind/src/VuFind/Connection/Webhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

/**
* Webhook connection class.
*
* PHP version 8
*
* Copyright (C) Villanova University 2026.
*
* 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, see
* <https://www.gnu.org/licenses/>.
*
* @category VuFind
* @package Connection
* @author Maccabee Levine <msl321@lehigh.edu>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org
*/

namespace VuFind\Connection;

use Psr\Log\LoggerAwareInterface;
use VuFind\Http\GuzzleServiceAwareInterface;
use VuFind\Http\GuzzleServiceAwareTrait;

use function in_array;

/**
* Webhook connection class.
*
* @category VuFind
* @package Connection
* @author Maccabee Levine <msl321@lehigh.edu>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org
*/
class Webhook implements
GuzzleServiceAwareInterface,
LoggerAwareInterface
{
use GuzzleServiceAwareTrait;
use \VuFind\Log\LoggerAwareTrait {
logError as error;
}

/**
* Send a webhook post to the given URL. Log but do not throw any errors.
*
* @param string $url Target URL (required for proper proxy setup for non-local addresses)
* @param ?float $timeout Request timeout in seconds (overrides configuration)
* @param array $successStatusCodes Array of status codes to treat as a successful post
*
* @return void
*/
public function post(string $url, ?float $timeout = null, array $successStatusCodes = [200, 204]): void
{
try {
$response = $this->guzzleService->post($url, null, '', $timeout, []);
$statusCode = $response->getStatusCode();
if (in_array($statusCode, $successStatusCodes)) {
$this->debug('Webhook posted successfully');
} else {
$this->logError(
"Failed to post to webhook. Code: {$statusCode}, body: {$response->getBody()}"
);
}
} catch (\Exception $e) {
$this->logError('Failed to post webhook. Unexpected ' . $e::class . ': ' . (string)$e);
}
}
}
38 changes: 34 additions & 4 deletions module/VuFind/src/VuFind/ILS/Driver/Folio.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use Exception;
use Laminas\Http\Response;
use VuFind\Config\Feature\SecretTrait;
use VuFind\Connection\Webhook;
use VuFind\Exception\ILS as ILSException;
use VuFind\I18n\Translator\TranslatorAwareInterface;
use VuFind\ILS\Logic\AvailabilityStatus;
Expand Down Expand Up @@ -144,16 +145,25 @@ class Folio extends AbstractAPI implements
*/
protected $courseCache = null;

/**
* Timeout in seconds for webhook calls.
*
* @var float
*/
protected float $webhookTimeout = 5;

/**
* Constructor.
*
* @param \VuFind\Date\Converter $dateConverter Date converter object
* @param callable $sessionFactory Factory function returning
* SessionContainer object
* @param \VuFind\Date\Converter $dateConverter Date converter object
* @param callable $sessionFactory Factory function returning
* SessionContainer object
* @param ?Webhook $webhookConnection Connection for webhooks
Comment thread
maccabeelevine marked this conversation as resolved.
Outdated
*/
public function __construct(
\VuFind\Date\Converter $dateConverter,
$sessionFactory
$sessionFactory,
protected ?Webhook $webhookConnection = null
) {
$this->dateConverter = $dateConverter;
$this->sessionFactory = $sessionFactory;
Expand Down Expand Up @@ -2365,6 +2375,25 @@ protected function performHoldRequest(array $requestBody): array
];
}

/**
* Support method for placeHold(): Notify an external process
* that a request was successfully submitted.
*
* @return void
*/
protected function sendWebhookAfterHoldRequest()
{
$url = $this->config['Holds']['webhook'] ?? null;
if ($url) {
if (!$this->webhookConnection) {
throw new ILSException('Webhook connection not set.');
}

// Short timeout -- don't impact user.
$this->webhookConnection->post($url, $this->webhookTimeout);
}
}

/**
* Get allowed service points for a request. Returns null if data cannot be obtained.
*
Expand Down Expand Up @@ -2499,6 +2528,7 @@ public function placeHold($holdDetails)
$requestBody['requestType'] = $requestType;
$result = $this->performHoldRequest($requestBody);
if ($result['success']) {
$this->sendWebhookAfterHoldRequest();
break;
}
}
Expand Down
3 changes: 2 additions & 1 deletion module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public function __invoke(
$manager = $container->get(\Laminas\Session\SessionManager::class);
return new \Laminas\Session\Container("Folio_$namespace", $manager);
};
return parent::__invoke($container, $requestedName, [$sessionFactory]);
$webhookConnection = $container->get(\VuFind\Connection\Webhook::class);
return parent::__invoke($container, $requestedName, [$sessionFactory, $webhookConnection]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,10 @@ protected function createConnector(string $test, ?array $config = null): void
$manager = new \Laminas\Session\SessionManager();
return new \Laminas\Session\Container("Folio_$namespace", $manager);
};
$webhookConnection = $this->createMock(\VuFind\Connection\Webhook::class);
// Create a stub for the SomeClass class
$this->driver = $this->getMockBuilder(Folio::class)
->setConstructorArgs([new \VuFind\Date\Converter(), $factory])
->setConstructorArgs([new \VuFind\Date\Converter(), $factory, $webhookConnection])
->onlyMethods(['makeRequest'])
->getMock();
// Configure the stub
Expand Down
Loading