Skip to content

Commit

Permalink
improved opencast api exceptions and handlers,
Browse files Browse the repository at this point in the history
This PR fixes #56
  • Loading branch information
ferishili committed Dec 16, 2024
1 parent cc2ba67 commit bfa2f3b
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 2 deletions.
126 changes: 126 additions & 0 deletions classes/api/handler/api_handler_stack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* API Handler Stack class for Opencast API services.
*
* @package tool_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace tool_opencast\api\handler;

use GuzzleHttp\HandlerStack;
use tool_opencast\api\middleware\api_middlewares;

/**
* API Handler Stack class for Opencast API services.
*
* @package tool_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api_handler_stack {

/** @var HandlerStack $handlerstack */
private HandlerStack $handlerstack;

/**
* Constructor of class api_handler_stack.
*/
public function __construct() {
// Get the default Guzzle Handlers.
$this->handlerstack = HandlerStack::create();
$this->register_custom_handlers();
}

/**
* Registers custom handlers to the Guzzle HandlerStack.
*
* This method initially removes the default 'http_errors' handler and adds a custom handler
* for handling HTTP errors using the 'api_middlewares::http_errors()' middleware.
*
* @return void
*/
private function register_custom_handlers() {
// As for http errors, we use a custom handler.
$this->handlerstack->remove('http_errors');
$this->handlerstack->unshift(api_middlewares::http_errors(), 'tool_opencast_http_errors');
}

/**
* Adds a new handler to the handler stack.
*
* This function adds a new middleware handler to the existing handler stack.
* The handler can be added either at the beginning or end of the stack.
*
* @param callable $middleware The middleware function to be added to the stack.
* @param string $name The name of the middleware for identification.
* @param bool $first Optional. If true, adds the middleware to the beginning of the stack. Default is true.
*
* @return bool Returns true if the handler was successfully added to the stack.
*
* @throws moodle_exception If the handler stack is empty or the handler cannot be added.
*/
public function add_handler_to_stack(callable $middleware, string $name, bool $first = true): bool {
if (!empty($this->handlerstack)) {
if ($first) {
$this->handlerstack->unshift($middleware, $name);
} else {
$this->handlerstack->push($middleware, $name);
}
return true;
}
throw new moodle_exception('exception_code_unabletoaddhandler', 'tool_opencast');
}

/**
* Removes a handler from the handler stack.
*
* This function attempts to remove a handler with the specified name from the handler stack.
* If the handler is found and successfully removed, it returns true. Otherwise, it returns false.
*
* @param string $name The name of the handler to be removed from the stack.
*
* @return bool Returns true if the handler was successfully removed, false otherwise.
*/
public function remove_handler_from_stack($name): bool {
$isremoved = false;
try {
if ($this->handlerstack && $this->handlerstack->findByName($name) !== false) {
$this->handlerstack->remove($name);
$isremoved = true;
}
} catch (\Throwable $th) {
return false;
}
return $isremoved;
}

/**
* Retrieves the current handler stack.
*
* This method returns the HandlerStack object that contains all the registered middleware handlers.
*
* @return HandlerStack The current handler stack containing all registered middleware handlers.
*/
public function get_handler_stack() {
return $this->handlerstack;
}
}
100 changes: 100 additions & 0 deletions classes/api/middleware/api_middlewares.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* API Middlewares for Opencast API client.
*
* @package tool_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace tool_opencast\api\middleware;

use GuzzleHttp\Exception\ConnectException;
use Psr\Http\Message\ResponseInterface;
use tool_opencast\exception\opencast_api_http_errors_exception;

/**
* API Middlewares for Opencast API client.
*
* @package tool_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api_middlewares {
/**
* Middleware that throws exceptions for both 4xx and 5xx error as well as the cURL errors.
*
* @return callable(callable): callable Returns a function that accepts the next handler.
*/
public static function http_errors(): callable {
return static function (callable $handler): callable {
return static function ($request, array $options) use ($handler) {
// To get the 4xx and 5xx http errors, we need to check if the "http_errors" option is set.
$onfulfilled = empty($options['http_errors']) ? null :
static function (ResponseInterface $response) use ($request) {
$code = $response->getStatusCode();
if ($code < 400) {
return $response;
}
$exceptionstringkey = \sprintf("exception_request_%s", $code);
if (!get_string_manager()->string_exists($exceptionstringkey, 'tool_opencast')) {
$exceptionstringkey = 'exception_request_generic';
}
throw new opencast_api_http_errors_exception($exceptionstringkey, $code);
};

// This on rejected function would only get invoked if there is a connection error, mostly to catch cURL errors.
$onrejected = static function (\RuntimeException|string $reason) {
// No reason of any kind, we directly throw generic exception message.
if (empty($reason)) {
throw new opencast_api_http_errors_exception('exception_request_generic', 500);
}

// As default we assume the generic exception messages and code.
$reasonstring = get_string('exception_connect_generic', 'tool_opencast');
$code = 500;

// When the exception is of type ConnectException, we extract the reason string and code.
if ($reason instanceof ConnectException) {
$reasonstring = $reason->getMessage();
$code = $reason->getCode();
} else if (is_string($reason)) {
// Otherwise, if the reason is a string, we take that as the reason.
$reasonstring = $reason;
}

// In case the error is cURL, we try to make it more human readable.
if (preg_match('/cURL error (\d+):/', $reasonstring, $matches)) {
$curlerrornum = (int) $matches[1];
$reasonstring = curl_strerror($curlerrornum);
}

// At the end, we append the reason string to the "exception_connect" string!
$exceptionmessage = get_string('exception_connect', 'tool_opencast', $reasonstring);
// Throw the exception with message replacement, as we already got the message text.
throw new opencast_api_http_errors_exception($exceptionmessage, $code, true);
};

// Finally, we pass the above callable closures as promise fulfillment and rejection handlers.
return $handler($request, $options)->then($onfulfilled, $onrejected);
};
};
}
}
55 changes: 55 additions & 0 deletions classes/exception/opencast_api_http_errors_exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Opencast API HTTP Errors Exception.
* This is the exception mostly to be used in middlewares to find and replace the error message.
*
* @package tool_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace tool_opencast\exception;

use moodle_exception;

/**
* Opencast API HTTP Errors Exception.
* This is the exception mostly to be used in middlewares to find and replace the error message.
*
* @package tool_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class opencast_api_http_errors_exception extends moodle_exception {
/**
* Constructor of class opencast_api_http_errors_exception.
*
* @param string $errorkey the error string key
* @param int $errorcodenum the error code
* @param bool $replacemessage the flag to determine whether to replace the errorkey with message.
*/
public function __construct(string $errorkey, int $errorcodenum, bool $replacemessage = false) {
$this->code = $errorcodenum;
parent::__construct($errorkey, 'tool_opencast');
if ($replacemessage) {
$this->message = $errorkey;
}
}
}
61 changes: 61 additions & 0 deletions classes/exception/opencast_api_response_exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Opencast API Response Exception.
* This should be used to throw exception when a response is made, in order to digest the response from Opencast API
* and to decide the best error message.
*
* @package tool_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace tool_opencast\exception;

use moodle_exception;

/**
* Opencast API Response Exception.
* This should be used to throw exception when a response is made, in order to digest the response from Opencast API
* and to decide the best error message.
*
* @package tool_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class opencast_api_response_exception extends moodle_exception {
/**
* Constructor of class opencast_api_response_exception.
*
* @param array $response the response array that must contain the following:
* - reason: the reason for the exception
* - code: the exception/error code
* @param bool $replacemessage the flag to determine whether to replace the reason with message.
*/
public function __construct(array $response, bool $replacemessage = true) {
$reason = !empty($response['reason']) ? $response['reason'] : null;
$errorkey = !empty($reason) ? $reason : 'exception_request_generic';
$this->code = isset($response['code']) ? $response['code'] : 500;
parent::__construct($errorkey, 'tool_opencast');
// In case, the reason has already been set by middleware exception, we should show it as error message.
if (!empty($reason) && $replacemessage) {
$this->message = $reason;
}
}
}
5 changes: 4 additions & 1 deletion classes/local/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
namespace tool_opencast\local;

use local_chunkupload\local\chunkupload_file;
use tool_opencast\api\handler\api_handler_stack;
use tool_opencast\empty_configuration_exception;

defined('MOODLE_INTERNAL') || die;
Expand Down Expand Up @@ -279,6 +280,8 @@ public function __construct($instanceid = null,
'timeout' => (intval($this->timeout) / 1000),
'connect_timeout' => (intval($this->connecttimeout) / 1000),
];
$apihandlerstack = new api_handler_stack();
$config['handler'] = $apihandlerstack->get_handler_stack();
$this->opencastapi = new \OpencastApi\Opencast($config, [], $enableingest);
$this->opencastrestclient = new \OpencastApi\Rest\OcRestClient($config);
}
Expand Down Expand Up @@ -620,7 +623,7 @@ public function connection_test_credentials() {

// If the credentials are invalid, return a corresponding http code.
if (!$userinfo) {
return 400; // Bad Request.
return !empty($response['code']) ? $response['code'] : 400; // Bad Request.
}

// If the connection fails or the Opencast instance could not be found, return the http code.
Expand Down
Loading

0 comments on commit bfa2f3b

Please sign in to comment.