Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 2023.
Comment thread
maccabeelevine marked this conversation as resolved.
Outdated
*
* 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, 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);
}
}
}
18 changes: 9 additions & 9 deletions module/VuFind/src/VuFind/Http/GuzzleService.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public function get(
*
* @param string $url Request URL
* @param ?string $body Request body document
* @param string $type Request body content type
* @param ?string $type Request body content type
* @param float $timeout Request timeout in seconds
* @param array $headers Request HTTP headers
*
Expand All @@ -154,21 +154,21 @@ public function get(
public function post(
string $url,
?string $body = null,
string $type = 'application/octet-stream',
?string $type = null,
?float $timeout = null,
array $headers = []
): ResponseInterface {
$client = $this->createGuzzleClient($url, $timeout);

$extraHeaders = [
'Content-Length' => strlen($body ?? ''),
];
if ($body) {
$extraHeaders['Content-Type'] = ($type ?? 'application/octet-stream');
Comment thread
demiankatz marked this conversation as resolved.
Outdated
}
$options = [
'body' => $body,
'headers' => array_merge(
[
'Content-Type' => $type,
'Content-Length' => strlen($body ?? ''),
],
$headers
),
'headers' => array_merge($extraHeaders, $headers),
];

return $client->request('POST', $url, $options);
Expand Down
34 changes: 30 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
*/
public function __construct(
\VuFind\Date\Converter $dateConverter,
$sessionFactory
$sessionFactory,
protected Webhook $webhookConnection
Comment thread
demiankatz marked this conversation as resolved.
Outdated
) {
$this->dateConverter = $dateConverter;
$this->sessionFactory = $sessionFactory;
Expand Down Expand Up @@ -2365,6 +2375,21 @@ 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 && $this->webhookConnection) {
Comment thread
demiankatz marked this conversation as resolved.
Outdated
// 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 +2524,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