From 6c0bfb1449e9a477e417487ec1378cb314c46d52 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 9 Jul 2014 13:22:36 +0800 Subject: [PATCH] MDL-46282 core: Add VERP API This issue is a part of the MDL-47194 Task. This issue is a part of the MDL-39707 Epic. --- lib/adminlib.php | 7 + .../message/inbound/address_manager.php | 472 +++++++++++++ lib/classes/message/inbound/handler.php | 219 ++++++ lib/classes/message/inbound/manager.php | 257 +++++++ lib/db/install.xml | 31 +- lib/db/upgrade.php | 47 ++ lib/upgradelib.php | 11 + message/output/email/message_output_email.php | 12 +- message/tests/fixtures/inbound_fixtures.php | 85 +++ message/tests/inbound_test.php | 667 ++++++++++++++++++ version.php | 2 +- 11 files changed, 1807 insertions(+), 3 deletions(-) create mode 100644 lib/classes/message/inbound/address_manager.php create mode 100644 lib/classes/message/inbound/handler.php create mode 100644 lib/classes/message/inbound/manager.php mode change 100644 => 100755 lib/db/install.xml create mode 100644 message/tests/fixtures/inbound_fixtures.php create mode 100644 message/tests/inbound_test.php diff --git a/lib/adminlib.php b/lib/adminlib.php index eabb1a105eb65..21a52aaa89830 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -202,6 +202,13 @@ function uninstall_plugin($type, $name) { // Delete scheduled tasks. $DB->delete_records('task_scheduled', array('component' => $pluginname)); + // Delete Inbound Message datakeys. + $DB->delete_records_sql('messageinbound_datakeys', + 'handler IN (SELECT id FROM {messageinbound_handlers} WHERE component = ?)', array($pluginname)); + + // Delete Inbound Message handlers. + $DB->delete_records('messageinbound_handlers', array('component' => $pluginname)); + // delete all the logs $DB->delete_records('log', array('module' => $pluginname)); diff --git a/lib/classes/message/inbound/address_manager.php b/lib/classes/message/inbound/address_manager.php new file mode 100644 index 0000000000000..d3d26e26778c5 --- /dev/null +++ b/lib/classes/message/inbound/address_manager.php @@ -0,0 +1,472 @@ +. + +/** + * Incoming Message address manager. + * + * @package core_message + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\message\inbound; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Incoming Message address manager. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class address_manager { + + /** + * @var int The size of the hash component of the address. + * Note: Increasing this value will invalidate all previous key values + * and reduce the potential length of the e-mail address being checked. + * Do not change this value. + */ + const HASHSIZE = 24; + + /** + * @var int A validation status indicating successful validation + */ + const VALIDATION_SUCCESS = 0; + + /** + * @var int A validation status indicating an invalid address format. + * Typically this is an address which does not contain a subaddress or + * all of the required data. + */ + const VALIDATION_INVALID_ADDRESS_FORMAT = 1; + + /** + * @var int A validation status indicating that a handler could not + * be found for this address. + */ + const VALIDATION_UNKNOWN_HANDLER = 2; + + /** + * @var int A validation status indicating that an unknown user was specified. + */ + const VALIDATION_UNKNOWN_USER = 4; + + /** + * @var int A validation status indicating that the data key specified could not be found. + */ + const VALIDATION_UNKNOWN_DATAKEY = 8; + + /** + * @var int A validation status indicating that the mail processing handler was not enabled. + */ + const VALIDATION_DISABLED_HANDLER = 16; + + /** + * @var int A validation status indicating that the user specified was deleted or unconfirmed. + */ + const VALIDATION_DISABLED_USER = 32; + + /** + * @var int A validation status indicating that the datakey specified had reached it's expiration time. + */ + const VALIDATION_EXPIRED_DATAKEY = 64; + + /** + * @var int A validation status indicating that the hash could not be verified. + */ + const VALIDATION_INVALID_HASH = 128; + + /** + * @var int A validation status indicating that the originator address did not match the user on record. + */ + const VALIDATION_ADDRESS_MISMATCH = 256; + + /** + * The handler for the subsequent Inbound Message commands. + * @var \core\message\inbound\handler + */ + private $handler; + + /** + * The ID of the data record + * @var int + */ + private $datavalue; + + /** + * The ID of the data record + * @var string + */ + private $datakey; + + /** + * The processed data record. + * @var \stdClass + */ + private $record; + + /** + * The user. + * @var \stdClass + */ + private $user; + + /** + * Set the handler to use for the subsequent Inbound Message commands. + * + * @param string $classname The name of the class for the handler. + */ + public function set_handler($classname) { + $this->handler = manager::get_handler($classname); + } + + /** + * Return the active handler. + * + * @return \core\message\inbound\handler|null; + */ + public function get_handler() { + return $this->handler; + } + + /** + * Specify an integer data item value for this record. + * + * @param int $datavalue The value of the data item. + * @param string $datakey A hash to use for the datakey + */ + public function set_data($datavalue, $datakey = null) { + $this->datavalue = $datavalue; + + // We must clear the datakey when changing the datavalue. + $this->set_data_key($datakey); + } + + /** + * Specify a known data key for this data item. + * + * If specified, the datakey must already exist in the messageinbound_datakeys + * table, typically as a result of a previous Inbound Message setup. + * + * This is intended as a performance optimisation when sending many + * e-mails with different data to many users. + * + * @param string $datakey A hash to use for the datakey + */ + public function set_data_key($datakey = null) { + $this->datakey = $datakey; + } + + /** + * Return the data key for the data item. + * + * If no data key has been defined yet, this will call generate_data_key() to generate a new key on the fly. + * @return string The secret key for this data item. + */ + public function fetch_data_key() { + global $CFG, $DB; + + // Only generate a key if Inbound Message is actually enabled, and the handler is enabled. + if (!isset($CFG->messageinbound_enabled) || !$this->handler || !$this->handler->enabled) { + return null; + } + + if (!isset($this->datakey)) { + // Attempt to fetch an existing key first if one has not already been specified. + $datakey = $DB->get_field('messageinbound_datakeys', 'datakey', array( + 'handler' => $this->handler->id, + 'datavalue' => $this->datavalue, + )); + if (!$datakey) { + $datakey = $this->generate_data_key(); + } + $this->datakey = $datakey; + } + + return $this->datakey; + } + + /** + * Generate a new secret key for the current data item and handler combination. + * + * @return string The new generated secret key for this data item. + */ + protected function generate_data_key() { + global $DB; + + $key = new \stdClass(); + $key->handler = $this->handler->id; + $key->datavalue = $this->datavalue; + $key->datakey = md5($this->datavalue . '_' . time() . random_string(40)); + $key->timecreated = time(); + + if ($this->handler->defaultexpiration) { + // Apply the default expiration time to the datakey. + $key->expires = $key->timecreated + $this->handler->defaultexpiration; + } + $DB->insert_record('messageinbound_datakeys', $key); + + return $key->datakey; + } + + /** + * Generate an e-mail address for the Inbound Message handler, storing a private + * key for the data object if one was not specified. + * + * @param int $userid The ID of the user to generated an address for. + * @param string $userkey The unique key for this user. If not specified this will be retrieved using + * get_user_key(). This key must have been created using get_user_key(). This parameter is provided as a performance + * optimisation for when generating multiple addresses for the same user. + * @return string|null The generated address, or null if an address could not be generated. + */ + public function generate($userid, $userkey = null) { + global $CFG; + + // Ensure that Inbound Message is enabled and that there is enough information to proceed. + if (!manager::is_enabled()) { + return null; + } + + if ($userkey == null) { + $userkey = get_user_key('messageinbound_handler', $userid); + } + + // Ensure that the minimum requirements are in place. + if (!isset($this->handler) || !$this->handler) { + throw new \coding_exception('Inbound Message handler not specified.'); + } + + // Ensure that the requested handler is actually enabled. + if (!$this->handler->enabled) { + return null; + } + + if (!isset($this->datavalue)) { + throw new \coding_exception('Inbound Message data item has not been specified.'); + } + + $data = array( + self::pack_int($this->handler->id), + self::pack_int($userid), + self::pack_int($this->datavalue), + pack('H*', substr(md5($this->fetch_data_key() . $userkey), 0, self::HASHSIZE)), + ); + $subaddress = base64_encode(implode($data)); + + return $CFG->messageinbound_mailbox . '+' . $subaddress . '@' . $CFG->messageinbound_domain; + } + + /** + * Determine whether the supplied address is of the correct format. + * + * @param string $address The address to test + * @return bool Whether the address matches the correct format + */ + public static function is_correct_format($address) { + global $CFG; + // Messages must match the format mailbox+[data]@domain. + return preg_match('/' . $CFG->messageinbound_mailbox . '\+[^@]*@' . $CFG->messageinbound_domain . '/', $address); + } + + /** + * Process an inbound address to obtain the data stored within it. + * + * @param string $address The fully formed e-mail address to process. + */ + protected function process($address) { + global $DB; + + if (!self::is_correct_format($address)) { + // This address does not contain a subaddress to parse. + return; + } + + // Ensure that the instance record is empty. + $this->record = null; + + $record = new \stdClass(); + $record->address = $address; + + list($localpart) = explode('@', $address, 2); + list($record->mailbox, $encodeddata) = explode('+', $localpart, 2); + $data = base64_decode($encodeddata, true); + if (!$data) { + // This address has no valid data. + return; + } + + $content = @unpack('N2handlerid/N2userid/N2datavalue/H*datakey', $data); + + if (!$content) { + // This address has no data. + return; + } + + if (PHP_INT_SIZE === 8) { + // 64-bit machine. + $content['handlerid'] = $content['handlerid1'] << 32 | $content['handlerid2']; + $content['userid'] = $content['userid1'] << 32 | $content['userid2']; + $content['datavalue'] = $content['datavalue1'] << 32 | $content['datavalue2']; + } else { + if ($content['handlerid1'] > 0 || $content['userid1'] > 0 || $content['datavalue1'] > 0) { + // Any 64-bit integer which is greater than the 32-bit integer size will have a non-zero value in the first + // half of the integer. + throw new moodle_exception('Mixed environment.' + + ' Key generated with a 64-bit machine but received into a 32-bit machine'); + } + $content['handlerid'] = $content['handlerid2']; + $content['userid'] = $content['userid2']; + $content['datavalue'] = $content['datavalue2']; + } + + // Clear the 32-bit to 64-bit variables away. + unset($content['handlerid1']); + unset($content['handlerid2']); + unset($content['userid1']); + unset($content['userid2']); + unset($content['datavalue1']); + unset($content['datavalue2']); + + $record = (object) array_merge((array) $record, $content); + + // Fetch the user record. + $record->user = $DB->get_record('user', array('id' => $record->userid)); + + // Fetch and set the handler. + if ($handler = manager::get_handler_from_id($record->handlerid)) { + $this->handler = $handler; + + // Retrieve the record for the data key. + $record->data = $DB->get_record('messageinbound_datakeys', + array('handler' => $handler->id, 'datavalue' => $record->datavalue)); + } + + $this->record = $record; + } + + /** + * Retrieve the data parsed from the address. + * + * @return \stdClass the parsed data. + */ + public function get_data() { + return $this->record; + } + + /** + * Ensure that the parsed data is valid, and if the handler requires address validation, validate the sender against + * the user record of identified user record. + * + * @param string $address The fully formed e-mail address to process. + * @return int The validation status. + */ + protected function validate($address) { + if (!$this->record) { + // The record does not exist, so there is nothing to validate against. + return self::VALIDATION_INVALID_ADDRESS_FORMAT; + } + + // Build the list of validation errors. + $returnvalue = 0; + + if (!$this->handler) { + $returnvalue += self::VALIDATION_UNKNOWN_HANDLER; + } else if (!$this->handler->enabled) { + $returnvalue += self::VALIDATION_DISABLED_HANDLER; + } + + if (!isset($this->record->data) || !$this->record->data) { + $returnvalue += self::VALIDATION_UNKNOWN_DATAKEY; + } else if ($this->record->data->expires != 0 && $this->record->data->expires < time()) { + $returnvalue += self::VALIDATION_EXPIRED_DATAKEY; + } else { + + if (!$this->record->user) { + $returnvalue += self::VALIDATION_UNKNOWN_USER; + } else { + if ($this->record->user->deleted || !$this->record->user->confirmed) { + $returnvalue += self::VALIDATION_DISABLED_USER; + } + + $userkey = get_user_key('messageinbound_handler', $this->record->user->id); + $hashvalidation = substr(md5($this->record->data->datakey . $userkey), 0, self::HASHSIZE) == $this->record->datakey; + if (!$hashvalidation) { + // The address data did not check out, so the originator is deemed invalid. + $returnvalue += self::VALIDATION_INVALID_HASH; + } + + if ($this->handler->validateaddress) { + // Validation of the sender's e-mail address is also required. + if ($address !== $this->record->user->email) { + // The e-mail address of the originator did not match the + // address held on record for this user. + $returnvalue += self::VALIDATION_ADDRESS_MISMATCH; + } + } + } + } + + return $returnvalue; + } + + /** + * Process the message recipient, load the handler, and then validate + * the sender with the associated data record. + * + * @param string $recipient The recipient of the message + * @param string $sender The sender of the message + */ + public function process_envelope($recipient, $sender) { + // Process the recipient address to retrieve the handler data. + $this->process($recipient); + + // Validate the retrieved data against the e-mail address of the originator. + $this->status = $this->validate($sender); + + return $this->status; + } + + /** + * Process the message against the relevant handler. + * + * @param \stdClass $messagedata The data for the current message being processed. + * @return mixed The result of the handler's message processor. A truthy result suggests a successful send. + */ + public function handle_message(\stdClass $messagedata) { + $this->record = $this->get_data(); + return $this->handler->process_message($this->record, $messagedata); + } + + /** + * Pack an integer into a pair of 32-bit numbers. + * + * @param int $int The integer to pack + * @return string The encoded binary data + */ + protected function pack_int($int) { + if (PHP_INT_SIZE === 8) { + $left = 0xffffffff00000000; + $right = 0x00000000ffffffff; + $l = ($int & $left) >>32; + $r = $int & $right; + + return pack('NN', $l, $r); + } else { + return pack('NN', 0, $int); + } + } +} diff --git a/lib/classes/message/inbound/handler.php b/lib/classes/message/inbound/handler.php new file mode 100644 index 0000000000000..14b3533e57ab9 --- /dev/null +++ b/lib/classes/message/inbound/handler.php @@ -0,0 +1,219 @@ +. + +/** + * Abstract class describing Inbound Message Handlers. + * + * @package core_message + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\message\inbound; + +/** + * Abstract class describing Inbound Message Handlers. + * + * @copyright 2014 Andrew NIcols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @property-read int $id The ID of the handler in the database + * @property-read string $component The component of this handler + * @property-read int $defaultexpiration Default expiration of new addresses for this handler + * @property-read string $description The description of this handler + * @property-read bool $validateaddress Whether the address validation is a requiredment + * @property-read bool $enabled Whether this handler is currently enabled + * @property-read string $classname The name of handler class + */ +abstract class handler { + + /** + * @var int $id The id of the handler in the database. + */ + private $id = null; + + /** + * @var string $component The component to which this handler belongs. + */ + private $component = ''; + + /** + * @var int $defaultexpiration The default expiration time to use when created a new key. + */ + private $defaultexpiration = 86400; + + /** + * @var bool $validateaddress Whether to validate the sender address when processing this handler. + */ + private $validateaddress = true; + + /** + * @var bool $enabled Whether this handler is currently enabled. + */ + private $enabled = false; + + /** + * @var $accessibleproperties A list of the properties which can be read. + */ + private $accessibleproperties = array( + 'id' => true, + 'component' => true, + 'defaultexpiration' => true, + 'validateaddress' => true, + 'enabled' => true, + ); + + /** + * Magic getter to fetch the specified key. + * + * @param string $key The name of the key to retrieve + */ + public function __get($key) { + // Some properties have logic behind them. + $getter = 'get_' . $key; + if (method_exists($this, $getter)) { + return $this->$getter(); + } + + // Check for a commonly accessibly property. + if (isset($this->accessibleproperties[$key])) { + return $this->$key; + } + + // Unknown property - bail. + throw new \coding_exception('unknown_property ' . $key); + } + + /** + * Set the id name. + * + * @param int $id The id to set + * @return int The newly set id + */ + public function set_id($id) { + return $this->id = $id; + } + + /** + * Set the component name. + * + * @param string $component The component to set + * @return string The newly set component + */ + public function set_component($component) { + return $this->component = $component; + } + + /** + * Whether the current handler allows changes to the address validation + * setting. + * + * By default this will return true, but for some handlers it may be + * necessary to disallow such changes. + * + * @return boolean + */ + public function can_change_validateaddress() { + return true; + } + + /** + * Set whether validation of the address is required. + * + * @param bool $validateaddress The new state of validateaddress + * @return bool + */ + public function set_validateaddress($validateaddress) { + return $this->validateaddress = $validateaddress; + } + + /** + * Whether this handler can be disabled (or enabled). + * + * By default this will return true, but for some handlers it may be + * necessary to disallow such changes. For example, a core handler to + * handle rejected mail validation should not be disabled. + * + * @return boolean + */ + public function can_change_enabled() { + return true; + } + + /** + * Set the enabled name. + * + * @param bool $enabled The new state of enabled + * @return bool + */ + public function set_enabled($enabled) { + return $this->enabled = $enabled; + } + + /** + * Set the default validity for new keys. + * + * @param int $period The time in seconds before a key expires + * @return int + */ + public function set_defaultexpiration($period) { + return $this->defaultexpiration = $period; + } + + /** + * Get the non-namespaced name of the current class. + * + * @return string The classname + */ + private function get_classname() { + $classname = get_class($this); + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + + return $classname; + } + + /** + * Return a description for the current handler. + * + * @return string + */ + protected abstract function get_description(); + + /** + * Process the message against the current handler. + * + * @param \stdClass $record The Inbound Message Handler record + * @param \stdClass $messagedata The message data + */ + public abstract function process_message(\stdClass $record, \stdClass $messagedata); + + /** + * Return the content of any success notification to be sent. + * Both an HTML and Plain Text variant must be provided. + * + * If this handler does not need to send a success notification, then + * it should return a falsey value. + * + * @param \stdClass $messagedata The message data. + * @param \stdClass $handlerresult The record for the newly created post. + * @return \stdClass with keys `html` and `plain`. + */ + public function get_success_message(\stdClass $messagedata, $handlerresult) { + return false; + } + +} diff --git a/lib/classes/message/inbound/manager.php b/lib/classes/message/inbound/manager.php new file mode 100644 index 0000000000000..8534df679e464 --- /dev/null +++ b/lib/classes/message/inbound/manager.php @@ -0,0 +1,257 @@ +. + +/** + * Variable Envelope Return Path management. + * + * @package core_message + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\message\inbound; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Variable Envelope Return Path manager class. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manager { + + /** + * Whether the Inbound Message interface is enabled. + * + * @return bool + */ + public static function is_enabled() { + global $CFG; + + // Check whether Inbound Message is enabled at all. + if (!isset($CFG->messageinbound_enabled) || !$CFG->messageinbound_enabled) { + return false; + } + + // Check whether the outgoing mailbox and domain are configured properly. + if (!isset($CFG->messageinbound_mailbox) || empty($CFG->messageinbound_mailbox)) { + return false; + } + + if (!isset($CFG->messageinbound_domain) || empty($CFG->messageinbound_domain)) { + return false; + } + + return true; + } + + /** + * Update the database to create, update, and remove handlers. + * + * @param string $componentname - The frankenstyle component name. + */ + public static function update_handlers_for_component($componentname) { + global $DB; + + $componentname = \core_component::normalize_componentname($componentname); + $existinghandlers = $DB->get_recordset('messageinbound_handlers', array('component' => $componentname)); + foreach ($existinghandlers as $handler) { + if (!class_exists($handler->classname)) { + self::remove_messageinbound_handler($handler); + } + } + + self::create_missing_messageinbound_handlers_for_component($componentname); + } + + /** + * Load handler instances for all of the handlers defined in db/messageinbound_handlers.php for the specified component. + * + * @param string $componentname - The name of the component to fetch the handlers for. + * @return \core\message\inbound\handler[] - List of handlers for this component. + */ + public static function load_default_handlers_for_component($componentname) { + $componentname = \core_component::normalize_componentname($componentname); + $dir = \core_component::get_component_directory($componentname); + + if (!$dir) { + return array(); + } + + $file = $dir . '/db/messageinbound_handlers.php'; + if (!file_exists($file)) { + return array(); + } + + $handlers = null; + require_once($file); + + if (!isset($handlers)) { + return array(); + } + + $handlerinstances = array(); + + foreach ($handlers as $handler) { + $record = (object) $handler; + $record->component = $componentname; + if ($handlerinstance = self::handler_from_record($record)) { + $handlerinstances[] = $handlerinstance; + } else { + throw new \coding_exception("Inbound Message Handler not found for '{$componentname}'."); + } + } + + return $handlerinstances; + } + + /** + * Update the database to contain a list of handlers for a component, + * adding any handlers which do not exist in the database. + * + * @param string $componentname - The frankenstyle component name. + */ + public static function create_missing_messageinbound_handlers_for_component($componentname) { + global $DB; + $componentname = \core_component::normalize_componentname($componentname); + + $expectedhandlers = self::load_default_handlers_for_component($componentname); + foreach ($expectedhandlers as $handler) { + $recordexists = $DB->record_exists('messageinbound_handlers', array( + 'component' => $componentname, + 'classname' => $handler->classname, + )); + + if (!$recordexists) { + $record = self::record_from_handler($handler); + $record->component = $componentname; + $DB->insert_record('messageinbound_handlers', $record); + } + } + } + + /** + * Remove the specified handler. + * + * @param \core\message\inbound\handler $handler The handler to remove + */ + public static function remove_messageinbound_handler($handler) { + global $DB; + + // Delete Inbound Message datakeys. + $DB->delete_records('messageinbound_datakeys', array('handler' => $handler->id)); + + // Delete Inbound Message handlers. + $DB->delete_records('messageinbound_handlers', array('id' => $handler->id)); + } + + /** + * Create a flat stdClass for the handler, appropriate for inserting + * into the database. + * + * @param \core\message\inbound\handler $handler The handler to retrieve the record for. + * @return \stdClass + */ + public static function record_from_handler($handler) { + $record = new \stdClass(); + $record->id = $handler->id; + $record->component = $handler->component; + $record->classname = get_class($handler); + if (strpos($record->classname, '\\') !== 0) { + $record->classname = '\\' . $record->classname; + } + $record->defaultexpiration = $handler->defaultexpiration; + $record->validateaddress = $handler->validateaddress; + $record->enabled = $handler->enabled; + + return $record; + } + + /** + * Load the Inbound Message handler details for a given record. + * + * @param \stdClass $record The record to retrieve the handler for. + * @return \core\message\inbound\handler or false + */ + protected static function handler_from_record($record) { + $classname = $record->classname; + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + if (!class_exists($classname)) { + return false; + } + + $handler = new $classname; + if (isset($record->id)) { + $handler->set_id($record->id); + } + $handler->set_component($record->component); + + // Overload fields which can be modified. + if (isset($record->defaultexpiration)) { + $handler->set_defaultexpiration($record->defaultexpiration); + } + + if (isset($record->validateaddress)) { + $handler->set_validateaddress($record->validateaddress); + } + + if (isset($record->enabled)) { + $handler->set_enabled($record->enabled); + } + + return $handler; + } + + /** + * Load the Inbound Message handler details for a given classname. + * + * @param string $classname The name of the class for the handler. + * @return \core\message\inbound\handler or false + */ + public static function get_handler($classname) { + global $DB; + + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + + $record = $DB->get_record('messageinbound_handlers', array('classname' => $classname), '*', IGNORE_MISSING); + if (!$record) { + return false; + } + return self::handler_from_record($record); + } + + /** + * Load the Inbound Message handler with a given ID + * + * @param int $id + * @return \core\message\inbound\handler or false + */ + public static function get_handler_from_id($id) { + global $DB; + + $record = $DB->get_record('messageinbound_handlers', array('id' => $id), '*', IGNORE_MISSING); + if (!$record) { + return false; + } + return self::handler_from_record($record); + } + +} diff --git a/lib/db/install.xml b/lib/db/install.xml old mode 100644 new mode 100755 index d5b6afdb2d248..5716152abefbb --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3032,5 +3032,34 @@ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index bd97b8c8cb85f..b167855088ecf 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -3790,5 +3790,52 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2014082900.02); } + if ($oldversion < 2014092500.01) { + + // Define table messageinbound_handlers to be created. + $table = new xmldb_table('messageinbound_handlers'); + + // Adding fields to table messageinbound_handlers. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('classname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('defaultexpiration', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '86400'); + $table->add_field('validateaddress', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('enabled', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table messageinbound_handlers. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('classname', XMLDB_KEY_UNIQUE, array('classname')); + + // Conditionally launch create table for messageinbound_handlers. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table messageinbound_datakeys to be created. + $table = new xmldb_table('messageinbound_datakeys'); + + // Adding fields to table messageinbound_datakeys. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('handler', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('datavalue', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('datakey', XMLDB_TYPE_CHAR, '64', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('expires', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table messageinbound_datakeys. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('handler_datavalue', XMLDB_KEY_UNIQUE, array('handler', 'datavalue')); + $table->add_key('handler', XMLDB_KEY_FOREIGN, array('handler'), 'messageinbound_handlers', array('id')); + + // Conditionally launch create table for messageinbound_datakeys. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2014092500.01); + } + return true; } diff --git a/lib/upgradelib.php b/lib/upgradelib.php index 03d307f373c25..9d3f707f55513 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -481,6 +481,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); if ($type === 'message') { message_update_processors($plug); } @@ -518,6 +519,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); if ($type === 'message') { message_update_processors($plug); } @@ -550,6 +552,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); if ($type === 'message') { // Ugly hack! message_update_processors($plug); @@ -650,6 +653,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); $endcallback($component, true, $verbose); } @@ -683,6 +687,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); $endcallback($component, true, $verbose); @@ -718,6 +723,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); $endcallback($component, false, $verbose); @@ -837,6 +843,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); $endcallback($component, true, $verbose); } @@ -876,6 +883,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); $endcallback($component, true, $verbose); @@ -910,6 +918,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { events_update_definition($component); \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); + \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); $endcallback($component, false, $verbose); @@ -1511,6 +1520,7 @@ function install_core($version, $verbose) { events_update_definition('moodle'); \core\task\manager::reset_scheduled_tasks_for_component('moodle'); message_update_providers('moodle'); + \core\message\inbound\manager::update_handlers_for_component('moodle'); // Write default settings unconditionally admin_apply_default_settings(NULL, true); @@ -1574,6 +1584,7 @@ function upgrade_core($version, $verbose) { events_update_definition('moodle'); \core\task\manager::reset_scheduled_tasks_for_component('moodle'); message_update_providers('moodle'); + \core\message\inbound\manager::update_handlers_for_component('moodle'); // Update core definitions. cache_helper::update_definitions(true); diff --git a/message/output/email/message_output_email.php b/message/output/email/message_output_email.php index 722aea1db4c52..247aa5fa028ba 100644 --- a/message/output/email/message_output_email.php +++ b/message/output/email/message_output_email.php @@ -83,8 +83,18 @@ function send_message($eventdata) { } } + // Configure mail replies - this is used for incoming mail replies. + $replyto = ''; + $replytoname = ''; + if (isset($eventdata->replyto)) { + $replyto = $eventdata->replyto; + if (isset($eventdata->replytoname)) { + $replytoname = $eventdata->replytoname; + } + } + $result = email_to_user($recipient, $eventdata->userfrom, $eventdata->subject, $eventdata->fullmessage, - $eventdata->fullmessagehtml, $attachment, $attachname); + $eventdata->fullmessagehtml, $attachment, $attachname, true, $replyto, $replytoname); // Remove an attachment file if any. if (!empty($attachment) && file_exists($attachment)) { diff --git a/message/tests/fixtures/inbound_fixtures.php b/message/tests/fixtures/inbound_fixtures.php new file mode 100644 index 0000000000000..9af72b7470ea4 --- /dev/null +++ b/message/tests/fixtures/inbound_fixtures.php @@ -0,0 +1,85 @@ +. + +/** + * Fixtures for Inbound Message tests. + * + * @package core_message + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\test; +defined('MOODLE_INTERNAL') || die(); + +/** + * A base handler for unit testing. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class handler_base extends \core\message\inbound\handler { + /** + * Get the description for unit tests. + */ + public function get_description() { + } + + /** + * Process a message for unit tests. + * + * @param stdClass $record The record to process + * @param stdClass $messagedata The message data + */ + public function process_message(\stdClass $record, \stdClass $messagedata) { + } +} + +/** + * A handler for unit testing. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class handler_one extends handler_base { +} + +/** + * A handler for unit testing. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class handler_two extends handler_base { +} + +/** + * A handler for unit testing. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class handler_three extends handler_base { +} + +/** + * A handler for unit testing. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class handler_four extends handler_base { +} diff --git a/message/tests/inbound_test.php b/message/tests/inbound_test.php new file mode 100644 index 0000000000000..e07b2f78c594a --- /dev/null +++ b/message/tests/inbound_test.php @@ -0,0 +1,667 @@ +. + +/** + * Tests for core_message_inbound to test Variable Envelope Return Path functionality. + * + * @package core_message + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__ . '/fixtures/inbound_fixtures.php'); + +/** + * Tests for core_message_inbound to test Variable Envelope Return Path functionality. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_message_inbound_testcase extends advanced_testcase { + + /** + * Perform setup tasks generic to each test. + * This includes: + * * configuring the messageinbound_mailbox. + */ + public function setUp() { + global $CFG; + + $this->resetAfterTest(true); + + // Setup the default Inbound Message mailbox settings. + $CFG->messageinbound_domain = 'example.com'; + $CFG->messageinbound_enabled = true; + + // Must be no longer than 15 characters. + $CFG->messageinbound_mailbox = 'moodlemoodle123'; + } + + /** + * Helper to create a new Inbound Message handler. + * + * @param $handlerclass The class of the handler to create + * @param $enabled Whether the handler should be enabled + * @param $component The component + * @param $namepace The namepace + */ + public function helper_create_handler($handlerclass, $enabled = true, $component = 'core_test', $namespace = '\\core\\test\\') { + global $DB; + + $classname = $namespace . $handlerclass; + $record = \core\message\inbound\manager::record_from_handler(new $classname()); + $record->component = $component; + $record->enabled = $enabled; + $record->id = $DB->insert_record('messageinbound_handlers', $record); + $handler = \core_message_inbound_test_manager::handler_from_record($record); + + return $handler; + } + + /** + * Test that the enabled check perform as expected. + */ + public function test_is_enabled() { + global $CFG; + + // First clear all of the settings set in the setUp. + $CFG->messageinbound_domain = null; + $CFG->messageinbound_enabled = null; + $CFG->messageinbound_mailbox = null; + + $this->assertFalse(\core\message\inbound\manager::is_enabled()); + + // Check whether only setting the enabled flag keeps it disabled. + $CFG->messageinbound_enabled = true; + $this->assertFalse(\core\message\inbound\manager::is_enabled()); + + // Check that the mailbox entry on it's own does not enable Inbound Message handling. + $CFG->messageinbound_mailbox = 'moodlemoodle123'; + $CFG->messageinbound_domain = null; + $this->assertFalse(\core\message\inbound\manager::is_enabled()); + + // And that the domain on it's own does not. + $CFG->messageinbound_domain = 'example.com'; + $CFG->messageinbound_mailbox = null; + $this->assertFalse(\core\message\inbound\manager::is_enabled()); + + // And that an invalid mailbox does not. + $CFG->messageinbound_mailbox = ''; + $CFG->messageinbound_domain = 'example.com'; + $this->assertFalse(\core\message\inbound\manager::is_enabled()); + + // And that an invalid domain does not. + $CFG->messageinbound_domain = ''; + $CFG->messageinbound_mailbox = 'moodlemoodle123'; + $this->assertFalse(\core\message\inbound\manager::is_enabled()); + + // Finally a test that ensures that all settings correct enables the system. + $CFG->messageinbound_mailbox = 'moodlemoodle123'; + $CFG->messageinbound_domain = 'example.com'; + $CFG->messageinbound_enabled = true; + + $this->assertTrue(\core\message\inbound\manager::is_enabled()); + } + + /** + * Test that data items conform to RFCs 5231, and 5322 standards for + * addressing, and to RFC 5233 for sub-addressing. + */ + public function test_address_constraints() { + $handler = $this->helper_create_handler('handler_one'); + + // Using the handler created, generate an address for our data entry. + $processor = new core_message_inbound_test_helper(); + $processor->set_handler($handler->classname); + + // Generate some IDs for the data and generate addresses for them. + $dataids = array( + -1, + 0, + 42, + 1073741823, + 2147483647, + ); + + $user = $this->getDataGenerator()->create_user(); + foreach ($dataids as $dataid) { + $processor->set_data($dataid); + $address = $processor->generate($user->id); + $this->assertNotNull($address); + $this->assertTrue(strlen($address) > 0, 'No address generated.'); + $this->assertTrue(strpos($address, '@') !== false, 'No domain found.'); + $this->assertTrue(strpos($address, '+') !== false, 'No subaddress found.'); + + // The localpart must be less than 64 characters. + list($localpart) = explode('@', $address); + $this->assertTrue(strlen($localpart) <= 64, 'Localpart section of address too long'); + + // And the data section should be no more than 48 characters. + list(, $datasection) = explode('+', $localpart); + $this->assertTrue(strlen($datasection) <= 48, 'Data section of address too long'); + } + } + + /** + * Test that the generated e-mail addresses are sufficiently random by + * testing the multiple handlers, multiple users, and multiple data + * items. + */ + public function test_address_uniqueness() { + // Generate a set of handlers. These are in two components, and each + // component has two different generators. + $handlers = array(); + $handlers[] = $this->helper_create_handler('handler_one', true, 'core_test'); + $handlers[] = $this->helper_create_handler('handler_two', true, 'core_test'); + $handlers[] = $this->helper_create_handler('handler_three', true, 'core_test_example'); + $handlers[] = $this->helper_create_handler('handler_four', true, 'core_test_example'); + + // Generate some IDs for the data and generate addresses for them. + $dataids = array( + 0, + 42, + 1073741823, + 2147483647, + ); + + $users = array(); + for ($i = 0; $i < 5; $i++) { + $users[] = $this->getDataGenerator()->create_user(); + } + + // Store the addresses for later comparison. + $addresses = array(); + + foreach ($handlers as $handler) { + $processor = new core_message_inbound_test_helper(); + $processor->set_handler($handler->classname); + + // Check each dataid. + foreach ($dataids as $dataid) { + $processor->set_data($dataid); + + // Check each user. + foreach ($users as $user) { + $address = $processor->generate($user->id); + $this->assertFalse(isset($addresses[$address])); + $addresses[$address] = true; + } + } + } + } + + /** + * Test address parsing of a generated address. + */ + public function test_address_parsing() { + $dataid = 42; + + // Generate a handler to use for this set of tests. + $handler = $this->helper_create_handler('handler_one'); + + // And a user. + $user = $this->getDataGenerator()->create_user(); + + // Using the handler created, generate an address for our data entry. + $processor = new core_message_inbound_test_helper(); + $processor->set_handler($handler->classname); + $processor->set_data($dataid); + $address = $processor->generate($user->id); + + // We should be able to parse the address. + $parser = new core_message_inbound_test_helper(); + $parser->process($address); + $parsedresult = $parser->get_data(); + $this->assertEquals($user->id, $parsedresult->userid); + $this->assertEquals($dataid, $parsedresult->datavalue); + $this->assertEquals($dataid, $parsedresult->data->datavalue); + $this->assertEquals($handler->id, $parsedresult->handlerid); + $this->assertEquals($handler->id, $parsedresult->data->handler); + } + + /** + * Test address parsing of an address with an unrecognised format. + */ + public function test_address_validation_invalid_format_failure() { + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + + // Check that validation fails when no address has been processed. + $result = $parser->validate($user->email); + $this->assertEquals(\core\message\inbound\address_manager::VALIDATION_INVALID_ADDRESS_FORMAT, $result); + + // Test that an address without data fails validation. + $parser->process('bob@example.com'); + $result = $parser->validate($user->email); + $this->assertEquals(\core\message\inbound\address_manager::VALIDATION_INVALID_ADDRESS_FORMAT, $result); + + // Test than address with a subaddress but invalid data fails with VALIDATION_UNKNOWN_DATAKEY. + $parser->process('bob+nodata@example.com'); + $result = $parser->validate($user->email); + $this->assertEquals(\core\message\inbound\address_manager::VALIDATION_INVALID_ADDRESS_FORMAT, $result); + } + + /** + * Test address parsing of an address with an unknown handler. + */ + public function test_address_validation_unknown_handler() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Remove the handler record to invalidate it. + $DB->delete_records('messageinbound_handlers', array( + 'id' => $handler->id, + )); + + $parser->process($address); + $result = $parser->validate($user->email); + $expectedfail = \core\message\inbound\address_manager::VALIDATION_UNKNOWN_HANDLER; + $this->assertEquals($expectedfail, $result & $expectedfail); + } + + /** + * Test address parsing of an address with a disabled handler. + */ + public function test_address_validation_disabled_handler() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Disable the handler. + $record = \core\message\inbound\manager::record_from_handler($handler); + $record->enabled = false; + $DB->update_record('messageinbound_handlers', $record); + + $parser->process($address); + $result = $parser->validate($user->email); + $expectedfail = \core\message\inbound\address_manager::VALIDATION_DISABLED_HANDLER; + $this->assertEquals($expectedfail, $result & $expectedfail); + } + + /** + * Test address parsing of an address for an invalid user. + */ + public function test_address_validation_invalid_user() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate(-1); + + $parser->process($address); + $result = $parser->validate($user->email); + $expectedfail = \core\message\inbound\address_manager::VALIDATION_UNKNOWN_USER; + $this->assertEquals($expectedfail, $result & $expectedfail); + } + + /** + * Test address parsing of an address for a disabled user. + */ + public function test_address_validation_disabled_user() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Unconfirm the user. + $user->confirmed = 0; + $DB->update_record('user', $user); + + $parser->process($address); + $result = $parser->validate($user->email); + $expectedfail = \core\message\inbound\address_manager::VALIDATION_DISABLED_USER; + $this->assertEquals($expectedfail, $result & $expectedfail); + } + + /** + * Test address parsing of an address for an invalid key. + */ + public function test_address_validation_invalid_key() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Remove the data record to invalidate it. + $DB->delete_records('messageinbound_datakeys', array( + 'handler' => $handler->id, + 'datavalue' => $dataid, + )); + + $parser->process($address); + $result = $parser->validate($user->email); + $expectedfail = \core\message\inbound\address_manager::VALIDATION_UNKNOWN_DATAKEY; + $this->assertEquals($expectedfail, $result & $expectedfail); + } + + /** + * Test address parsing of an address for an expired key. + */ + public function test_address_validation_expired_key() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Expire the key by setting it's expiry time in the past. + $key = $DB->get_record('messageinbound_datakeys', array( + 'handler' => $handler->id, + 'datavalue' => $dataid, + )); + + $key->expires = time() - 3600; + $DB->update_record('messageinbound_datakeys', $key); + + $parser->process($address); + $result = $parser->validate($user->email); + $expectedfail = \core\message\inbound\address_manager::VALIDATION_EXPIRED_DATAKEY; + $this->assertEquals($expectedfail, $result & $expectedfail); + } + + /** + * Test address parsing of an address for an invalid hash. + */ + public function test_address_validation_invalid_hash() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Expire the key by setting it's expiry time in the past. + $key = $DB->get_record('messageinbound_datakeys', array( + 'handler' => $handler->id, + 'datavalue' => $dataid, + )); + + $key->datakey = 'invalid value'; + $DB->update_record('messageinbound_datakeys', $key); + + $parser->process($address); + $result = $parser->validate($user->email); + $expectedfail = \core\message\inbound\address_manager::VALIDATION_INVALID_HASH; + $this->assertEquals($expectedfail, $result & $expectedfail); + } + + /** + * Test address parsing of an address for an invalid sender. + */ + public function test_address_validation_invalid_sender() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + $parser->process($address); + $result = $parser->validate('incorrectuser@example.com'); + $expectedfail = \core\message\inbound\address_manager::VALIDATION_ADDRESS_MISMATCH; + $this->assertEquals($expectedfail, $result & $expectedfail); + } + + /** + * Test address parsing of an address for an address which is correct. + */ + public function test_address_validation_success() { + global $DB; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + $dataid = 42; + + $parser = new core_message_inbound_test_helper(); + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + $parser->process($address); + $result = $parser->validate($user->email); + $this->assertEquals(\core\message\inbound\address_manager::VALIDATION_SUCCESS, $result); + + } + + /** + * Test that a handler with no default expiration does not have an + * expiration time applied. + */ + public function test_default_hander_expiry_unlimited() { + global $DB; + + // Set the default expiry of the handler to 0 - no expiration. + $expiration = 0; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + + $record = \core\message\inbound\manager::record_from_handler($handler); + $record->defaultexpiration = $expiration; + $DB->update_record('messageinbound_handlers', $record); + + // Generate an address for the handler. + $dataid = 42; + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Check that the datakey created matches the expirytime. + $key = $DB->get_record('messageinbound_datakeys', array('handler' => $record->id, 'datavalue' => $dataid)); + + $this->assertNull($key->expires); + } + + /** + * Test application of the default expiry on a handler. + */ + public function test_default_hander_expiry_low() { + global $DB; + + // Set the default expiry of the handler to 60 seconds. + $expiration = 60; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + + $record = \core\message\inbound\manager::record_from_handler($handler); + $record->defaultexpiration = $expiration; + $DB->update_record('messageinbound_handlers', $record); + + // Generate an address for the handler. + $dataid = 42; + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Check that the datakey created matches the expirytime. + $key = $DB->get_record('messageinbound_datakeys', array('handler' => $record->id, 'datavalue' => $dataid)); + + $this->assertEquals($key->timecreated + $expiration, $key->expires); + } + + /** + * Test application of the default expiry on a handler. + */ + public function test_default_hander_expiry_medium() { + global $DB; + + // Set the default expiry of the handler to 3600 seconds. + $expiration = 3600; + + // Create test data. + $user = $this->getDataGenerator()->create_user(); + $handler = $this->helper_create_handler('handler_one'); + + $record = \core\message\inbound\manager::record_from_handler($handler); + $record->defaultexpiration = $expiration; + $DB->update_record('messageinbound_handlers', $record); + + // Generate an address for the handler. + $dataid = 42; + + $generator = new core_message_inbound_test_helper(); + $generator->set_handler($handler->classname); + $generator->set_data($dataid); + $address = $generator->generate($user->id); + + // Check that the datakey created matches the expirytime. + $key = $DB->get_record('messageinbound_datakeys', array('handler' => $record->id, 'datavalue' => $dataid)); + + $this->assertEquals($key->timecreated + $expiration, $key->expires); + } + +} + +/** + * A helper function for unit testing to expose protected functions in the core_message_inbound API for testing. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_message_inbound_test_helper extends \core\message\inbound\address_manager { + /** + * The validate function. + * + * @param string $address + * @return int + */ + public function validate($address) { + return parent::validate($address); + } + + /** + * The get_data function. + * + * @return stdClass + */ + public function get_data() { + return parent::get_data(); + } + + /** + * The address processor function. + * + * @param string $address + * @return void + */ + public function process($address) { + return parent::process($address); + } +} + +/** + * A helper function for unit testing to expose protected functions in the core_message_inbound API for testing. + * + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_message_inbound_test_manager extends \core\message\inbound\manager { + /** + * Helper to fetch make the handler_from_record public for unit testing. + * + * @param $record The handler record to fetch + */ + public static function handler_from_record($record) { + return parent::handler_from_record($record); + } +} diff --git a/version.php b/version.php index 99528172c0838..c045a5e3e0fa1 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2014092500.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2014092500.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.