See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Incoming Message address manager. 19 * 20 * @package core_message 21 * @copyright 2014 Andrew Nicols <andrew@nicols.co.uk> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core\message\inbound; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * Incoming Message address manager. 31 * 32 * @copyright 2014 Andrew Nicols <andrew@nicols.co.uk> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 class address_manager { 36 37 /** 38 * @var int The size of the hash component of the address. 39 * Note: Increasing this value will invalidate all previous key values 40 * and reduce the potential length of the e-mail address being checked. 41 * Do not change this value. 42 */ 43 const HASHSIZE = 24; 44 45 /** 46 * @var int A validation status indicating successful validation 47 */ 48 const VALIDATION_SUCCESS = 0; 49 50 /** 51 * @var int A validation status indicating an invalid address format. 52 * Typically this is an address which does not contain a subaddress or 53 * all of the required data. 54 */ 55 const VALIDATION_INVALID_ADDRESS_FORMAT = 1; 56 57 /** 58 * @var int A validation status indicating that a handler could not 59 * be found for this address. 60 */ 61 const VALIDATION_UNKNOWN_HANDLER = 2; 62 63 /** 64 * @var int A validation status indicating that an unknown user was specified. 65 */ 66 const VALIDATION_UNKNOWN_USER = 4; 67 68 /** 69 * @var int A validation status indicating that the data key specified could not be found. 70 */ 71 const VALIDATION_UNKNOWN_DATAKEY = 8; 72 73 /** 74 * @var int A validation status indicating that the mail processing handler was not enabled. 75 */ 76 const VALIDATION_DISABLED_HANDLER = 16; 77 78 /** 79 * @var int A validation status indicating that the user specified was deleted or unconfirmed. 80 */ 81 const VALIDATION_DISABLED_USER = 32; 82 83 /** 84 * @var int A validation status indicating that the datakey specified had reached it's expiration time. 85 */ 86 const VALIDATION_EXPIRED_DATAKEY = 64; 87 88 /** 89 * @var int A validation status indicating that the hash could not be verified. 90 */ 91 const VALIDATION_INVALID_HASH = 128; 92 93 /** 94 * @var int A validation status indicating that the originator address did not match the user on record. 95 */ 96 const VALIDATION_ADDRESS_MISMATCH = 256; 97 98 /** 99 * The handler for the subsequent Inbound Message commands. 100 * @var \core\message\inbound\handler 101 */ 102 private $handler; 103 104 /** 105 * The ID of the data record 106 * @var int 107 */ 108 private $datavalue; 109 110 /** 111 * The ID of the data record 112 * @var string 113 */ 114 private $datakey; 115 116 /** 117 * The processed data record. 118 * @var \stdClass 119 */ 120 private $record; 121 122 /** 123 * The user. 124 * @var \stdClass 125 */ 126 private $user; 127 128 /** 129 * Set the handler to use for the subsequent Inbound Message commands. 130 * 131 * @param string $classname The name of the class for the handler. 132 */ 133 public function set_handler($classname) { 134 $this->handler = manager::get_handler($classname); 135 } 136 137 /** 138 * Return the active handler. 139 * 140 * @return \core\message\inbound\handler|null; 141 */ 142 public function get_handler() { 143 return $this->handler; 144 } 145 146 /** 147 * Specify an integer data item value for this record. 148 * 149 * @param int $datavalue The value of the data item. 150 * @param string $datakey A hash to use for the datakey 151 */ 152 public function set_data($datavalue, $datakey = null) { 153 $this->datavalue = $datavalue; 154 155 // We must clear the datakey when changing the datavalue. 156 $this->set_data_key($datakey); 157 } 158 159 /** 160 * Specify a known data key for this data item. 161 * 162 * If specified, the datakey must already exist in the messageinbound_datakeys 163 * table, typically as a result of a previous Inbound Message setup. 164 * 165 * This is intended as a performance optimisation when sending many 166 * e-mails with different data to many users. 167 * 168 * @param string $datakey A hash to use for the datakey 169 */ 170 public function set_data_key($datakey = null) { 171 $this->datakey = $datakey; 172 } 173 174 /** 175 * Return the data key for the data item. 176 * 177 * If no data key has been defined yet, this will call generate_data_key() to generate a new key on the fly. 178 * @return string The secret key for this data item. 179 */ 180 public function fetch_data_key() { 181 global $CFG, $DB; 182 183 // Only generate a key if Inbound Message is actually enabled, and the handler is enabled. 184 if (!isset($CFG->messageinbound_enabled) || !$this->handler || !$this->handler->enabled) { 185 return null; 186 } 187 188 if (!isset($this->datakey)) { 189 // Attempt to fetch an existing key first if one has not already been specified. 190 $datakey = $DB->get_field('messageinbound_datakeys', 'datakey', array( 191 'handler' => $this->handler->id, 192 'datavalue' => $this->datavalue, 193 )); 194 if (!$datakey) { 195 $datakey = $this->generate_data_key(); 196 } 197 $this->datakey = $datakey; 198 } 199 200 return $this->datakey; 201 } 202 203 /** 204 * Generate a new secret key for the current data item and handler combination. 205 * 206 * @return string The new generated secret key for this data item. 207 */ 208 protected function generate_data_key() { 209 global $DB; 210 211 $key = new \stdClass(); 212 $key->handler = $this->handler->id; 213 $key->datavalue = $this->datavalue; 214 $key->datakey = md5($this->datavalue . '_' . time() . random_string(40)); 215 $key->timecreated = time(); 216 217 if ($this->handler->defaultexpiration) { 218 // Apply the default expiration time to the datakey. 219 $key->expires = $key->timecreated + $this->handler->defaultexpiration; 220 } 221 $DB->insert_record('messageinbound_datakeys', $key); 222 223 return $key->datakey; 224 } 225 226 /** 227 * Generate an e-mail address for the Inbound Message handler, storing a private 228 * key for the data object if one was not specified. 229 * 230 * @param int $userid The ID of the user to generated an address for. 231 * @param string $userkey The unique key for this user. If not specified this will be retrieved using 232 * get_user_key(). This key must have been created using get_user_key(). This parameter is provided as a performance 233 * optimisation for when generating multiple addresses for the same user. 234 * @return string|null The generated address, or null if an address could not be generated. 235 */ 236 public function generate($userid, $userkey = null) { 237 global $CFG; 238 239 // Ensure that Inbound Message is enabled and that there is enough information to proceed. 240 if (!manager::is_enabled()) { 241 return null; 242 } 243 244 if ($userkey == null) { 245 $userkey = get_user_key('messageinbound_handler', $userid); 246 } 247 248 // Ensure that the minimum requirements are in place. 249 if (!isset($this->handler) || !$this->handler) { 250 throw new \coding_exception('Inbound Message handler not specified.'); 251 } 252 253 // Ensure that the requested handler is actually enabled. 254 if (!$this->handler->enabled) { 255 return null; 256 } 257 258 if (!isset($this->datavalue)) { 259 throw new \coding_exception('Inbound Message data item has not been specified.'); 260 } 261 262 $data = array( 263 self::pack_int($this->handler->id), 264 self::pack_int($userid), 265 self::pack_int($this->datavalue), 266 pack('H*', substr(md5($this->fetch_data_key() . $userkey), 0, self::HASHSIZE)), 267 ); 268 $subaddress = base64_encode(implode($data)); 269 270 return $CFG->messageinbound_mailbox . '+' . $subaddress . '@' . $CFG->messageinbound_domain; 271 } 272 273 /** 274 * Determine whether the supplied address is of the correct format. 275 * 276 * @param string $address The address to test 277 * @return bool Whether the address matches the correct format 278 */ 279 public static function is_correct_format($address) { 280 global $CFG; 281 // Messages must match the format mailbox+[data]@domain. 282 return preg_match('/' . $CFG->messageinbound_mailbox . '\+[^@]*@' . $CFG->messageinbound_domain . '/', $address); 283 } 284 285 /** 286 * Process an inbound address to obtain the data stored within it. 287 * 288 * @param string $address The fully formed e-mail address to process. 289 */ 290 protected function process($address) { 291 global $DB; 292 293 if (!self::is_correct_format($address)) { 294 // This address does not contain a subaddress to parse. 295 return; 296 } 297 298 // Ensure that the instance record is empty. 299 $this->record = null; 300 301 $record = new \stdClass(); 302 $record->address = $address; 303 304 list($localpart) = explode('@', $address, 2); 305 list($record->mailbox, $encodeddata) = explode('+', $localpart, 2); 306 $data = base64_decode($encodeddata, true); 307 if (!$data) { 308 // This address has no valid data. 309 return; 310 } 311 312 $content = @unpack('N2handlerid/N2userid/N2datavalue/H*datakey', $data); 313 314 if (!$content) { 315 // This address has no data. 316 return; 317 } 318 319 if (PHP_INT_SIZE === 8) { 320 // 64-bit machine. 321 $content['handlerid'] = $content['handlerid1'] << 32 | $content['handlerid2']; 322 $content['userid'] = $content['userid1'] << 32 | $content['userid2']; 323 $content['datavalue'] = $content['datavalue1'] << 32 | $content['datavalue2']; 324 } else { 325 if ($content['handlerid1'] > 0 || $content['userid1'] > 0 || $content['datavalue1'] > 0) { 326 // Any 64-bit integer which is greater than the 32-bit integer size will have a non-zero value in the first 327 // half of the integer. 328 throw new \moodle_exception('Mixed environment.' . 329 ' Key generated with a 64-bit machine but received into a 32-bit machine.'); 330 } 331 $content['handlerid'] = $content['handlerid2']; 332 $content['userid'] = $content['userid2']; 333 $content['datavalue'] = $content['datavalue2']; 334 } 335 336 // Clear the 32-bit to 64-bit variables away. 337 unset($content['handlerid1']); 338 unset($content['handlerid2']); 339 unset($content['userid1']); 340 unset($content['userid2']); 341 unset($content['datavalue1']); 342 unset($content['datavalue2']); 343 344 $record = (object) array_merge((array) $record, $content); 345 346 // Fetch the user record. 347 $record->user = $DB->get_record('user', array('id' => $record->userid)); 348 349 // Fetch and set the handler. 350 if ($handler = manager::get_handler_from_id($record->handlerid)) { 351 $this->handler = $handler; 352 353 // Retrieve the record for the data key. 354 $record->data = $DB->get_record('messageinbound_datakeys', 355 array('handler' => $handler->id, 'datavalue' => $record->datavalue)); 356 } 357 358 $this->record = $record; 359 } 360 361 /** 362 * Retrieve the data parsed from the address. 363 * 364 * @return \stdClass the parsed data. 365 */ 366 public function get_data() { 367 return $this->record; 368 } 369 370 /** 371 * Ensure that the parsed data is valid, and if the handler requires address validation, validate the sender against 372 * the user record of identified user record. 373 * 374 * @param string $address The fully formed e-mail address to process. 375 * @return int The validation status. 376 */ 377 protected function validate($address) { 378 if (!$this->record) { 379 // The record does not exist, so there is nothing to validate against. 380 return self::VALIDATION_INVALID_ADDRESS_FORMAT; 381 } 382 383 // Build the list of validation errors. 384 $returnvalue = 0; 385 386 if (!$this->handler) { 387 $returnvalue += self::VALIDATION_UNKNOWN_HANDLER; 388 } else if (!$this->handler->enabled) { 389 $returnvalue += self::VALIDATION_DISABLED_HANDLER; 390 } 391 392 if (!isset($this->record->data) || !$this->record->data) { 393 $returnvalue += self::VALIDATION_UNKNOWN_DATAKEY; 394 } else if ($this->record->data->expires != 0 && $this->record->data->expires < time()) { 395 $returnvalue += self::VALIDATION_EXPIRED_DATAKEY; 396 } else { 397 398 if (!$this->record->user) { 399 $returnvalue += self::VALIDATION_UNKNOWN_USER; 400 } else { 401 if ($this->record->user->deleted || !$this->record->user->confirmed) { 402 $returnvalue += self::VALIDATION_DISABLED_USER; 403 } 404 405 $userkey = get_user_key('messageinbound_handler', $this->record->user->id); 406 $hashvalidation = substr(md5($this->record->data->datakey . $userkey), 0, self::HASHSIZE) == $this->record->datakey; 407 if (!$hashvalidation) { 408 // The address data did not check out, so the originator is deemed invalid. 409 $returnvalue += self::VALIDATION_INVALID_HASH; 410 } 411 412 if ($this->handler->validateaddress) { 413 // Validation of the sender's e-mail address is also required. 414 if ($address !== $this->record->user->email) { 415 // The e-mail address of the originator did not match the 416 // address held on record for this user. 417 $returnvalue += self::VALIDATION_ADDRESS_MISMATCH; 418 } 419 } 420 } 421 } 422 423 return $returnvalue; 424 } 425 426 /** 427 * Process the message recipient, load the handler, and then validate 428 * the sender with the associated data record. 429 * 430 * @param string $recipient The recipient of the message 431 * @param string $sender The sender of the message 432 */ 433 public function process_envelope($recipient, $sender) { 434 // Process the recipient address to retrieve the handler data. 435 $this->process($recipient); 436 437 // Validate the retrieved data against the e-mail address of the originator. 438 $this->status = $this->validate($sender); 439 440 return $this->status; 441 } 442 443 /** 444 * Process the message against the relevant handler. 445 * 446 * @param \stdClass $messagedata The data for the current message being processed. 447 * @return mixed The result of the handler's message processor. A truthy result suggests a successful send. 448 */ 449 public function handle_message(\stdClass $messagedata) { 450 $this->record = $this->get_data(); 451 return $this->handler->process_message($this->record, $messagedata); 452 } 453 454 /** 455 * Pack an integer into a pair of 32-bit numbers. 456 * 457 * @param int $int The integer to pack 458 * @return string The encoded binary data 459 */ 460 protected function pack_int($int) { 461 // If PHP environment is running on a 64-bit. 462 if (PHP_INT_SIZE === 8) { 463 // Will be used to ensures that the result remains as a 32-bit unsigned integer and 464 // doesn't extend beyond 32 bits. 465 $notation = 0xffffffff; 466 467 if ($int < 0) { 468 // If the given integer is negative, set it to -1. 469 $l = -1; 470 } else { 471 // Otherwise, calculate the upper 32 bits of the 64-bit integer. 472 $l = ($int >> 32) & $notation; 473 } 474 475 // Calculate the lower 32 bits of the 64-bit integer. 476 $r = $int & $notation; 477 478 // Pack the values of $l (upper 32 bits) and $r (lower 32 bits) into a binary string format. 479 return pack('NN', $l, $r); 480 } else { 481 // Pack the values into a binary string format. 482 return pack('NN', 0, $int); 483 } 484 } 485 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body