Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
        return $this->validate($sender);
439
    }
440
 
441
    /**
442
     * Process the message against the relevant handler.
443
     *
444
     * @param \stdClass $messagedata The data for the current message being processed.
445
     * @return mixed The result of the handler's message processor. A truthy result suggests a successful send.
446
     */
447
    public function handle_message(\stdClass $messagedata) {
448
        $this->record = $this->get_data();
449
        return $this->handler->process_message($this->record, $messagedata);
450
    }
451
 
452
    /**
453
     * Pack an integer into a pair of 32-bit numbers.
454
     *
455
     * @param int $int The integer to pack
456
     * @return string The encoded binary data
457
     */
458
    protected function pack_int($int) {
459
        // If PHP environment is running on a 64-bit.
460
        if (PHP_INT_SIZE === 8) {
461
            // Will be used to ensures that the result remains as a 32-bit unsigned integer and
462
            // doesn't extend beyond 32 bits.
463
            $notation = 0xffffffff;
464
 
465
            if ($int < 0) {
466
                // If the given integer is negative, set it to -1.
467
                $l = -1;
468
            } else {
469
                // Otherwise, calculate the upper 32 bits of the 64-bit integer.
470
                $l = ($int >> 32) & $notation;
471
            }
472
 
473
            // Calculate the lower 32 bits of the 64-bit integer.
474
            $r = $int & $notation;
475
 
476
            // Pack the values of $l (upper 32 bits) and $r (lower 32 bits) into a binary string format.
477
            return pack('NN', $l, $r);
478
        } else {
479
            // Pack the values into a binary string format.
480
            return pack('NN', 0, $int);
481
        }
482
    }
483
}