Skip to content

Commit

Permalink
MDL-51571 mod_lti: Improve service error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
polothy authored and samchaffee committed Feb 25, 2016
1 parent 03b8b55 commit 00e2706
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 38 deletions.
122 changes: 122 additions & 0 deletions mod/lti/classes/service_exception_handler.php
@@ -0,0 +1,122 @@
<?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/>.

/**
* Exception handler for LTI services
*
* @package mod_lti
* @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace mod_lti;

defined('MOODLE_INTERNAL') || die();

require_once(__DIR__.'/../locallib.php');
require_once(__DIR__.'/../servicelib.php');

/**
* Handles exceptions when handling incoming LTI messages.
*
* Ensures that LTI always returns a XML message that can be consumed by the caller.
*
* @package mod_lti
* @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class service_exception_handler {
/**
* Enable error response logging.
*
* @var bool
*/
protected $log = false;

/**
* The LTI service message ID, if known.
*
* @var string
*/
protected $id = '';

/**
* The LTI service message type, if known.
*
* @var string
*/
protected $type = 'unknownRequest';

/**
* Constructor.
*
* @param boolean $log Enable error response logging.
*/
public function __construct($log) {
$this->log = $log;
}

/**
* Set the LTI message ID being handled.
*
* @param string $id
*/
public function set_message_id($id) {
if (!empty($id)) {
$this->id = $id;
}
}

/**
* Set the LTI message type being handled.
*
* @param string $type
*/
public function set_message_type($type) {
if (!empty($type)) {
$this->type = $type;
}
}

/**
* Echo an exception message encapsulated in XML
*
* @param \Exception $exception The exception that was thrown
*/
public function handle(\Exception $exception) {
$message = $exception->getMessage();

// Add the exception backtrace for developers.
if (debugging('', DEBUG_DEVELOPER)) {
$message .= "\n".format_backtrace(get_exception_info($exception)->backtrace, true);
}

// Switch to response.
$type = str_replace('Request', 'Response', $this->type);

// Build the appropriate xml.
$response = lti_get_response_xml('failure', $message, $this->id, $type);

$xml = $response->asXML();

// Log the request if necessary.
if ($this->log) {
lti_log_response($xml, $exception);
}

echo $xml;
}
}
38 changes: 37 additions & 1 deletion mod/lti/locallib.php
Expand Up @@ -1913,7 +1913,43 @@ function lti_should_log_request($rawbody) {
function lti_log_request($rawbody) {
if ($tempdir = make_temp_directory('mod_lti', false)) {
if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
file_put_contents($tempfile, $rawbody);
$content = "Request Headers:\n";
foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
$content .= "$header: $value\n";
}
$content .= "Request Body:\n";
$content .= $rawbody;

file_put_contents($tempfile, $content);
chmod($tempfile, 0644);
}
}
}

/**
* Log an LTI response
*
* @param string $responsexml The response XML
* @param Exception $e If there was an exception, pass that too
*/
function lti_log_response($responsexml, $e = null) {
if ($tempdir = make_temp_directory('mod_lti', false)) {
if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
$content = '';
if ($e instanceof Exception) {
$info = get_exception_info($e);

$content .= "Exception:\n";
$content .= "Message: $info->message\n";
$content .= "Debug info: $info->debuginfo\n";
$content .= "Backtrace:\n";
$content .= format_backtrace($info->backtrace, true);
$content .= "\n";
}
$content .= "Response XML:\n";
$content .= $responsexml;

file_put_contents($tempfile, $content);
chmod($tempfile, 0644);
}
}
Expand Down
38 changes: 23 additions & 15 deletions mod/lti/service.php
Expand Up @@ -31,11 +31,18 @@
require_once($CFG->dirroot.'/mod/lti/servicelib.php');

// TODO: Switch to core oauthlib once implemented - MDL-30149.
use mod_lti\service_exception_handler;
use moodle\mod\lti as lti;

$rawbody = file_get_contents("php://input");

if (lti_should_log_request($rawbody)) {
$logrequests = lti_should_log_request($rawbody);
$errorhandler = new service_exception_handler($logrequests);

// Register our own error handler so we can always send valid XML response.
set_exception_handler(array($errorhandler, 'handle'));

if ($logrequests) {
lti_log_request($rawbody);
}

Expand Down Expand Up @@ -73,20 +80,13 @@
$messagetype = $child->getName();
}

// We know more about the message, update error handler to send better errors.
$errorhandler->set_message_id(lti_parse_message_id($xml));
$errorhandler->set_message_type($messagetype);

switch ($messagetype) {
case 'replaceResultRequest':
try {
$parsed = lti_parse_grade_replace_message($xml);
} catch (Exception $e) {
$responsexml = lti_get_response_xml(
'failure',
$e->getMessage(),
uniqid(),
'replaceResultResponse');

echo $responsexml->asXML();
break;
}
$parsed = lti_parse_grade_replace_message($xml);

$ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid));

Expand All @@ -99,8 +99,12 @@

$gradestatus = lti_update_grade($ltiinstance, $parsed->userid, $parsed->launchid, $parsed->gradeval);

if (!$gradestatus) {
throw new Exception('Grade replace response');
}

$responsexml = lti_get_response_xml(
$gradestatus ? 'success' : 'failure',
'success',
'Grade replace response',
$parsed->messageid,
'replaceResultResponse'
Expand Down Expand Up @@ -157,8 +161,12 @@

$gradestatus = lti_delete_grade($ltiinstance, $parsed->userid);

if (!$gradestatus) {
throw new Exception('Grade delete request');
}

$responsexml = lti_get_response_xml(
$gradestatus ? 'success' : 'failure',
'success',
'Grade delete request',
$parsed->messageid,
'deleteResultResponse'
Expand Down
33 changes: 11 additions & 22 deletions mod/lti/servicelib.php
Expand Up @@ -57,6 +57,10 @@ function lti_get_response_xml($codemajor, $description, $messageref, $messagetyp
}

function lti_parse_message_id($xml) {
if (empty($xml->imsx_POXHeader)) {
return '';
}

$node = $xml->imsx_POXHeader->imsx_POXRequestHeaderInfo->imsx_messageIdentifier;
$messageid = (string)$node;

Expand Down Expand Up @@ -285,29 +289,14 @@ function lti_verify_sourcedid($ltiinstance, $parsed) {
function lti_extend_lti_services($data) {
$plugins = get_plugin_list_with_function('ltisource', $data->messagetype);
if (!empty($plugins)) {
try {
// There can only be one.
if (count($plugins) > 1) {
throw new coding_exception('More than one ltisource plugin handler found');
}
$data->xml = new SimpleXMLElement($data->body);
$callback = current($plugins);
call_user_func($callback, $data);
} catch (moodle_exception $e) {
$error = $e->getMessage();
if (debugging('', DEBUG_DEVELOPER)) {
$error .= ' '.format_backtrace(get_exception_info($e)->backtrace);
}
$responsexml = lti_get_response_xml(
'failure',
$error,
$data->messageid,
$data->messagetype
);

header('HTTP/1.0 400 bad request');
echo $responsexml->asXML();
// There can only be one.
if (count($plugins) > 1) {
throw new coding_exception('More than one ltisource plugin handler found');
}
$data->xml = new SimpleXMLElement($data->body);
$callback = current($plugins);
call_user_func($callback, $data);

return true;
}
return false;
Expand Down
85 changes: 85 additions & 0 deletions mod/lti/tests/service_exception_handler_test.php
@@ -0,0 +1,85 @@
<?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/>.

/**
* Tests Exception handler for LTI services
*
* @package mod_lti
* @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

use mod_lti\service_exception_handler;

defined('MOODLE_INTERNAL') || die();

/**
* Tests Exception handler for LTI services
*
* @package mod_lti
* @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_lti_service_exception_handler_testcase extends advanced_testcase {
/**
* Testing service error handling.
*/
public function test_handle() {
$handler = new service_exception_handler(false);
$handler->set_message_id('123');
$handler->set_message_type('testRequest');
$handler->handle(new Exception('Error happened'));

$this->expectOutputRegex('/imsx_codeMajor>failure/');
$this->expectOutputRegex('/imsx_description>Error happened/');
$this->expectOutputRegex('/imsx_messageRefIdentifier>123/');
$this->expectOutputRegex('/imsx_operationRefIdentifier>testRequest/');
$this->expectOutputRegex('/imsx_POXBody><testResponse/');
}

/**
* Testing service error handling when message ID and type are not known yet.
*/
public function test_handle_early_error() {
$handler = new service_exception_handler(false);
$handler->handle(new Exception('Error happened'));

$this->expectOutputRegex('/imsx_codeMajor>failure/');
$this->expectOutputRegex('/imsx_description>Error happened/');
$this->expectOutputRegex('/imsx_messageRefIdentifier\/>/');
$this->expectOutputRegex('/imsx_operationRefIdentifier>unknownRequest/');
$this->expectOutputRegex('/imsx_POXBody><unknownResponse/');
}

/**
* Testing that a log file is generated when logging is turned on.
*/
public function test_handle_log() {
global $CFG;

$this->resetAfterTest();

$handler = new service_exception_handler(true);

ob_start();
$handler->handle(new Exception('Error happened'));
ob_end_clean();

$this->assertTrue(is_dir($CFG->dataroot.'/temp/mod_lti'));
$files = glob($CFG->dataroot.'/temp/mod_lti/mod_lti_response*');
$this->assertEquals(1, count($files));
}
}

0 comments on commit 00e2706

Please sign in to comment.