Completed
Push — master ( 06d0a8...6eb73a )
by Seth
04:33
created

Toolbox::resetSession()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 2
eloc 9
nc 2
nop 0
1
<?php
2
3
namespace smtech\ReflexiveCanvasLTI;
4
5
use mysqli;
6
use Serializable;
7
8
use Log;
9
10
use Battis\AppMetadata;
11
use Battis\ConfigXML;
12
use Battis\DataUtilities;
13
14
use smtech\CanvasPest\CanvasPest;
15
use smtech\ReflexiveCanvasLTI\LTI\ToolProvider;
16
use smtech\ReflexiveCanvasLTI\Exception\ConfigurationException;
17
use smtech\LTI\Configuration\Generator;
18
use smtech\LTI\Configuration\LaunchPrivacy;
19
use smtech\LTI\Configuration\Exception\ConfigurationException as LTIConfigGeneratorException;
20
21
/**
22
 * A toolbox of tools for quickly constructing LTI tool providers that hook
23
 * back into the Canvas API reflexively.
24
 *
25
 * The basic idea is that you need an XML configuration file of credentials and
26
 * use that to instantiate the Toolbox. The toolbox can then perform LTI
27
 * authentication, handle API requests, generate an LTI Configuration XML file,
28
 * etc. for you.
29
 *
30
 * @author Seth Battis
31
 * @version v1.0
32
 */
33
class Toolbox implements Serializable
34
{
35
    const
36
        DEFAULT_LAUNCH_PRIVACY = 'public',
37
        TOOL_METADATA_TABLE = 'tool_metadata';
38
39
    /**
40
     * Persistent metadata storage
41
     * @var AppMetadata
42
     * @see Toolbox::config() Toolbox::config()
43
     */
44
    protected $metadata = false;
45
46
    /**
47
     * Object-oriented access to the Canvas API
48
     * @var CanvasPest
49
     */
50
    protected $api = false;
51
52
    /**
53
     * MySQL database connection
54
     * @var mysqli
55
     */
56
    protected $mysql = false;
57
58
    /**
59
     * LTI Tool Provider for handling authentication and consumer/user management
60
     * @var ToolProvider
61
     */
62
    protected $toolProvider;
63
64
    /**
65
     * Generator for LTI Configuration XML files
66
     * @var Generator
67
     */
68
    protected $generator;
69
70
    /**
71
     * Log file manager
72
     * @var Log
73
     */
74
    protected $logger = false;
75
76
    /**
77
     * Provide serialization support for the Toolbox
78
     *
79
     * This allows a Toolbox to be stored in the `$_SESSION` variables.
80
     *
81
     * Caveat emptor: because `mysqli` objects can not be serialized,
82
     * serialization is limited to storing a reference to the configuration file
83
     * that generated this object, which will be reaccessed (along with cached
84
     * configuration metadata) when the object is unserialized.
85
     *
86
     * @return string
87
     */
88
    public function serialize()
89
    {
90
        return serialize([
91
            'config' => $this->metadata['TOOL_CONFIG_FILE']
92
        ]);
93
    }
94
95
    /**
96
     * Provide serialization support for Toolbox
97
     *
98
     * This allows a Toolbox to be stored in the `$_SESSION` variables.
99
     *
100
     * @see Toolbox::serialize() `Toolbox::serialize()` has more information on the
101
     *      specifics of the serialization approach.
102
     *
103
     * @param  string $serialized A Toolbox object serialized by `Toolbox::serialize()`
104
     * @return Toolbox
105
     */
106
    public function unserialize($serialized)
107
    {
108
        $data = unserialize($serialized);
109
        $this->loadConfiguration($data['config']);
110
    }
111
112
    /**
113
     * Create a Toolbox instance from a configuration file
114
     *
115
     * @param  string $configFilePath Path to the configuration file
116
     * @param  boolean $forceRecache Whether or not to rely on cached
117
     *     configuration metadata or to force a refresh from the configuration
118
     *     file
119
     * @return Toolbox
120
     */
121
    public static function fromConfiguration($configFilePath, $forceRecache = false)
122
    {
123
        return new static($configFilePath, $forceRecache);
124
    }
125
126
    /**
127
     * Construct a Toolbox instance from a configuration file
128
     *
129
     * @see Toolbox::fromConfiguration() Use `Toolbox::fromConfiguration()`
130
     *
131
     * @param string $configFilePath
132
     * @param boolean $forceRecache
133
     */
134
    private function __construct($configFilePath, $forceRecache = false)
135
    {
136
        $this->loadConfiguration($configFilePath, $forceRecache);
137
    }
138
139
    /**
140
     * Update a Toolbox instance from a configuration file
141
     *
142
     * @see Toolbox::fromConfiguration() Use `Toolbox::fromConfiguration()`
143
     *
144
     * @param  string $configFilePath
145
     * @param  boolean $forceRecache
146
     * @return void
147
     */
148
    protected function loadConfiguration($configFilePath, $forceRecache = false)
149
    {
150
        $logQueue = [];
151
152
        /* load the configuration file */
153
        $config = new ConfigXML($configFilePath);
154
155
        /* configure database connections */
156
        $this->setMySQL($config->newInstanceOf(mysqli::class, '/config/mysql'));
157
158
        /* configure metadata caching */
159
        $id = $config->toString('/config/tool/id');
160
        if (empty($id)) {
161
            $id = basename(dirname($configFilePath)) . '_' . md5(__DIR__ . file_get_contents($configFilePath));
162
            $logQueue[] = "    Automatically generated ID $id";
163
        }
164
        $this->setMetadata(new AppMetadata($this->mysql, $id, self::TOOL_METADATA_TABLE));
165
166
        /* update metadata */
167
        if ($forceRecache ||
168
            empty($this->metadata['TOOL_ID']) ||
169
            empty($this->metadata['TOOL_LAUNCH_URL']) ||
170
            empty($this->metadata['TOOL_CONFIG_FILE'])) {
171
            $tool = $config->toArray('/config/tool')[0];
172
173
            $this->metadata['TOOL_ID'] = $id;
174
            $this->metadata['TOOL_NAME'] = (empty($tool['name']) ? $id : $tool['name']);
175
            $this->metadata['TOOL_CONFIG_FILE'] = realpath($configFilePath);
176
            $configPath = dirname($this->metadata['TOOL_CONFIG_FILE']);
177
178
            if (!empty($tool['description'])) {
179
                $this->metadata['TOOL_DESCRIPTION'] = $tool['description'];
180
            } elseif (isset($this->metadata['TOOL_DESCRIPTION'])) {
181
                unset($this->metadata['TOOL_DESCRIPTION']);
182
            }
183
184
            if (!empty($tool['icon'])) {
185
                $this->metadata['TOOL_ICON_URL'] = (
186
                    file_exists("$configPath/{$tool['icon']}") ?
187
                        DataUtilities::URLfromPath("$configPath/{$tool['icon']}") :
188
                        $tool[self::ICON]
189
                );
190
            } elseif (isset($this->metadata['TOOL_ICON_URL'])) {
191
                unset($this->metadata['TOOL_ICON_URL']);
192
            }
193
194
            $this->metadata['TOOL_LAUNCH_PRIVACY'] = (
195
                empty($tool['launch-privacy']) ?
196
                    self::DEFAULT_LAUNCH_PRIVACY :
197
                    $tool['launch-privacy']
198
            );
199
200
            if (!empty($tool['domain'])) {
201
                $this->metadata['TOOL_DOMAIN'] = $tool['domain'];
202
            } elseif (isset($this->metadata['TOOL_DOMAIN'])) {
203
                unset($this->metadata['TOOL_DOMAIN']);
204
            }
205
206
            $this->metadata['TOOL_LAUNCH_URL'] = (
207
                empty($tool['authenticate']) ?
208
                    DataUtilities::URLfromPath($_SERVER['SCRIPT_FILENAME']) :
209
                    DataUtilities::URLfromPath("$configPath/{$tool['authenticate']}")
210
            );
211
212
            $logQueue[] = "    Tool metadata configured";
213
        }
214
        $configPath = dirname($this->metadata['TOOL_CONFIG_FILE']);
215
216
        /* configure logging */
217
        if ($forceRecache || empty($this->metadata['TOOL_LOG'])) {
218
            $log = "$configPath/" . $config->toString('/config/tool/log');
219
            shell_exec("touch \"$log\"");
220
            $this->metadata['TOOL_LOG'] = realpath($log);
221
        }
222
        $this->setLog(Log::singleton('file', $this->metadata['TOOL_LOG']));
223
        if ($forceRecache) {
224
            $this->log("Resetting LTI configuration from $configFilePath");
225
        }
226
        if (!empty($logQueue)) {
227
            foreach ($logQueue as $message) {
228
                $this->log($message);
229
            }
230
            unset($logQueue);
231
        }
232
233
        /* configure tool provider */
234
        if ($forceRecache || empty($this->metadata['TOOL_HANDLER_URLS'])) {
235
            $handlers = $config->toArray('/config/tool/handlers')[0];
236
            if (empty($handlers) || !is_array($handlers)) {
237
                throw new ConfigurationException(
238
                    'At least one handler/URL pair must be specified',
239
                    ConfigurationException::TOOL_PROVIDER
240
                );
241
            }
242
            foreach ($handlers as $request => $path) {
243
                $handlers[$request] = DataUtilities::URLfromPath("$configPath/$path");
244
            }
245
            $this->metadata['TOOL_HANDLER_URLS'] = $handlers;
246
            $this->log('    Tool provider handler URLs configured');
247
        }
248
249
        /* configure API access */
250
        if ($forceRecache || empty($this->metadata['TOOL_CANVAS_API'])) {
251
            $this->metadata['TOOL_CANVAS_API'] = $config->toArray('/config/canvas')[0];
252
            if (empty($this->metadata['TOOL_CANVAS_API'])) {
253
                throw new ConfigurationException(
254
                    'Canvas API credentials must be provided',
255
                    ConfigurationException::CANVAS_API_MISSING
256
                );
257
            }
258
            $this->log('    Canvas API credentials configured');
259
        }
260
    }
261
262
    /**
263
     * Update toolbox configuration metadata object
264
     *
265
     * @param AppMetadata $metadata
266
     */
267
    public function setMetadata(AppMetadata $metadata)
268
    {
269
        $this->metadata = $metadata;
270
    }
271
272
    /**
273
     * Get the toolbox configuration metadata object
274
     *
275
     * @return AppMetadata
276
     */
277
    public function getMetadata()
278
    {
279
        return $this->metadata;
280
    }
281
282
    /**
283
     * Access or update a specific configuration metadata key/value pair
284
     *
285
     * The metadata keys used by the toolbox are prefixed `TOOL_`. Currently, the keys of interest are:
286
     *
287
     *   - `TOOL_NAME` is the human-readable name of the tool
288
     *   - `TOOL_ID` is the (ideally globally unique) identifier for the LTI tool provider
289
     *   - `TOOL_DESCRIPTION` is the human-readable description of the tool
290
     *   - `TOOL_ICON_URL` is the URL of the tool's icon image (if present)
291
     *   - `TOOL_DOMAIN` the domain from which Tool Consumer requests may emanate for the tool
292
     *   - `TOOL_LAUNCH_PRIVACY` is the level of information sharing between the LMS and the tool
293
     *   - `TOOL_LAUNCH_URL` is the URL of the script that will handle LTI authentication
294
     *   - `TOOL_HANDLER_URLS` stores an associative array of LTI request types and the URL that handles that request.
295
     *   - `TOOL_CONFIG_FILE` is the path to the configuration file from which this toolbox was generated
296
     *   - `TOOL_CANVAS_API` is an associative array of Canvas API credentials (`url` and `token`)
297
     *   - `TOOL_LOG` is the path to the tool's log file
298
     *
299
     * @param  string $key The metadata key to look up/create/update
300
     * @param  mixed $value (Optional) If not present (or `null`), the current
301
     *     metadata is returned. If present, the metadata is created/updated
302
     * @return mixed If not updating the metadata, the metadata (if any)
303
     *     currently stored
304
     */
305
    public function config($key, $value = null)
306
    {
307
        if ($value !== null) {
308
            $this->metadata[$key] = $value;
309
        } else {
310
            return $this->metadata[$key];
311
        }
312
    }
313
314
    /**
315
     * Update the ToolProvider object
316
     *
317
     * @param ToolProvider $toolProvider
318
     */
319
    public function setToolProvider(ToolProvider $toolProvider)
320
    {
321
        $this->toolProvider = $toolProvider;
322
    }
323
324
    /**
325
     * Get the ToolProvider object
326
     *
327
     * This does some just-in-time initialization, so that if the ToolProvider
328
     * has not yet been accessed, it will be instantiated and initialized by this
329
     * method.
330
     *
331
     * @return ToolProvider
332
     */
333
    public function getToolProvider()
334
    {
335
        if (empty($this->toolProvider)) {
336
            $this->setToolProvider(
337
                new ToolProvider(
338
                    $this->mysql,
339
                    $this->metadata['TOOL_HANDLER_URLS']
340
                )
341
            );
342
        }
343
        return $this->toolProvider;
344
    }
345
346
    /**
347
     * Authenticate an LTI launch request
348
     *
349
     * @return void
350
     */
351
    public function lti_authenticate()
352
    {
353
        $this->getToolProvider()->handle_request();
354
    }
355
356
    /**
357
     * Are we (or should we be) in the midst of authenticating an LTI launch request?
358
     *
359
     * @return boolean
360
     */
361
    public function lti_isLaunching()
362
    {
363
        return !empty($_POST['lti_message_type']);
364
    }
365
366
    /**
367
     * Create a new Tool consumer
368
     *
369
     * @see ToolProvider::createConsumer() Pass-through to `ToolProvider::createConsumer()`
370
     *
371
     * @param  string $name Human-readable name
372
     * @param  string $key (Optional) Consumer key (unique within the tool provider)
373
     * @param  string $secret (Optional) Shared secret
374
     * @return boolean Whether or not the consumer was created
375
     */
376
    public function lti_createConsumer($name, $key = false, $secret = false)
377
    {
378
        if ($this->getToolProvider()->createConsumer($name, $key, $secret)) {
379
            $this->log("Created consumer $name");
380
            return true;
381
        } else {
382
            $this->log("Could not recreate consumer '$name', consumer already exists");
383
            return false;
384
        }
385
    }
386
387
    /**
388
     * Get the list of consumers for this tool
389
     *
390
     * @see ToolProvider::getConsumers() Pass-through to `ToolProvider::getConsumers()`
391
     *
392
     * @return LTI_Consumer[]
393
     */
394
    public function lti_getConsumers()
395
    {
396
        return $this->getToolProvider()->getConsumers();
397
    }
398
399
    /**
400
     * Update the API interaction object
401
     *
402
     * @param CanvasPest $api
403
     */
404
    public function setAPI(CanvasPest $api)
405
    {
406
        $this->api = $api;
407
    }
408
409
    /**
410
     * Get the API interaction object
411
412
     * @return CanvasPest
413
     */
414
    public function getAPI()
415
    {
416
        if (empty($this->api)) {
417
            $canvas = $this->metadata['TOOL_CANVAS_API'];
418
            if (!empty($canvas['url']) && !empty($canvas['token'])) {
419
                $this->setAPI(new CanvasPest(
420
                    "{$canvas['url']}/api/v1", // TODO this seems crude
421
                    $canvas['token']
422
                ));
423
            } else {
424
                throw new ConfigurationException(
425
                    'Canvas URL and Token required',
426
                    ConfigurationException::CANVAS_API_INCORRECT
427
                );
428
            }
429
        }
430
        return $this->api;
431
    }
432
433
    /**
434
     * Make a GET request to the API
435
     *
436
     * @link https://htmlpreview.github.io/?https://raw.githubusercontent.com/smtech/canvaspest/master/doc/classes/smtech.CanvasPest.CanvasPest.html#method_get Pass-through to CanvasPest::get()
437
     * @param  string $url
438
     * @param  string[] $data (Optional)
439
     * @param  string[] $headers (Optional)
440
     * @return \smtech\CanvasPest\CanvasObject|\smtech\CanvasPest\CanvasArray
441
     */
442
    public function api_get($url, $data = [], $headers = [])
443
    {
444
        return $this->getAPI()->get($url, $data, $headers);
445
    }
446
447
    /**
448
     * Make a POST request to the API
449
     *
450
     * @link https://htmlpreview.github.io/?https://raw.githubusercontent.com/smtech/canvaspest/master/doc/classes/smtech.CanvasPest.CanvasPest.html#method_post Pass-through to CanvasPest::post()
451
     * @param  string $url
452
     * @param  string[] $data (Optional)
453
     * @param  string[] $headers (Optional)
454
     * @return \smtech\CanvasPest\CanvasObject|\smtech\CanvasPest\CanvasArray
455
     */
456
    public function api_post($url, $data = [], $headers = [])
457
    {
458
        return $this->getAPI()->post($url, $data, $headers);
459
    }
460
461
    /**
462
     * Make a PUT request to the API
463
     *
464
     * @link https://htmlpreview.github.io/?https://raw.githubusercontent.com/smtech/canvaspest/master/doc/classes/smtech.CanvasPest.CanvasPest.html#method_put Pass-through to CanvasPest::put()
465
     * @param  string $url
466
     * @param  string[] $data (Optional)
467
     * @param  string[] $headers (Optional)
468
     * @return \smtech\CanvasPest\CanvasObject|\smtech\CanvasPest\CanvasArray
469
     */
470
    public function api_put($url, $data = [], $headers = [])
471
    {
472
        return $this->getAPI()->put($url, $data, $headers);
473
    }
474
475
    /**
476
     * Make a DELETE request to the API
477
     *
478
     * @link https://htmlpreview.github.io/?https://raw.githubusercontent.com/smtech/canvaspest/master/doc/classes/smtech.CanvasPest.CanvasPest.html#method_delete Pass-through to CanvasPest::delete()
479
     * @param  string $url
480
     * @param  string[] $data (Optional)
481
     * @param  string[] $headers (Optional)
482
     * @return \smtech\CanvasPest\CanvasObject|\smtech\CanvasPest\CanvasArray
483
     */
484
    public function api_delete($url, $data = [], $headers = [])
485
    {
486
        return $this->getAPI()->delete($url, $data, $headers);
487
    }
488
489
    /**
490
     * Set MySQL connection object
491
     *
492
     * @param mysqli $mysql
493
     */
494
    public function setMySQL(mysqli $mysql)
495
    {
496
        $this->mysql = $mysql;
497
    }
498
499
    /**
500
     * Get MySQL connection object
501
     *
502
     * @return mysqli
503
     */
504
    public function getMySQL()
505
    {
506
        return $this->mysql;
507
    }
508
509
    /**
510
     * Make a MySQL query
511
     *
512
     * @link http://php.net/manual/en/mysqli.query.php Pass-through to `mysqli::query()`
513
     * @param string $query
514
     * @param int $resultMode (Optional, defaults to `MYSQLI_STORE_RESULT`)
515
     * @return mixed
516
     */
517
    public function mysql_query($query, $resultMode = MYSQLI_STORE_RESULT)
518
    {
519
        return $this->getMySQL()->query($query, $resultMode);
520
    }
521
522
    /**
523
     * Set log file manager
524
     *
525
     * @param Log $log
526
     */
527
    public function setLog(Log $log)
528
    {
529
        $this->logger = $log;
530
    }
531
532
    /**
533
     * Get log file manager
534
     *
535
     * @return Log
536
     */
537
    public function getLog()
538
    {
539
        return $this->logger;
540
    }
541
542
    /**
543
     * Add a message to the tool log file
544
     *
545
     * @link https://pear.php.net/package/Log/docs/1.13.1/Log/Log_file.html#methodlog
546
     *      Pass-throgh to `Log_file::log()`
547
     *
548
     * @param string $message
549
     * @param string $priority (Optional, defaults to `PEAR_LOG_INFO`)
550
     * @return boolean Success
551
     */
552
    public function log($message, $priority = null)
553
    {
554
        return $this->getLog()->log($message, $priority);
555
    }
556
557
    /**
558
     * Set the LTI Configuration generator
559
     *
560
     * @param Generator $generator
561
     */
562
    public function setGenerator(Generator $generator)
563
    {
564
        $this->generator = $generator;
565
    }
566
567
    /**
568
     * Get the LTI Configuration generator
569
     *
570
     * @return Generator
571
     */
572
    public function getGenerator()
573
    {
574
        try {
575
            if (empty($this->generator)) {
576
                $this->setGenerator(
577
                    new Generator(
578
                        $this->metadata['TOOL_NAME'],
579
                        $this->metadata['TOOL_ID'],
580
                        $this->metadata['TOOL_LAUNCH_URL'],
581
                        (empty($this->metadata['TOOL_DESCRIPTION']) ? false : $this->metadata['TOOL_DESCRIPTION']),
582
                        (empty($this->metadata['TOOL_ICON_URL']) ? false : $this->metadata['TOOL_ICON_URL']),
583
                        (empty($this->metadata['TOOL_LAUNCH_PRIVACY']) ? LaunchPrivacy::USER_PROFILE() : $this->metadata['TOOL_LAUNCH_PRIVACY']),
584
                        (empty($this->metadata['TOOL_DOMAIN']) ? false : $this->metadata['TOOL_DOMAIN'])
585
                    )
586
                );
587
            }
588
        } catch (LTIConfigGeneratorException $e) {
589
            throw new ConfigurationException(
590
                $e->getMessage(),
591
                ConfigurationException::TOOL_PROVIDER
592
            );
593
        }
594
        return $this->generator;
595
    }
596
597
    /**
598
     * Get the LTI configuration XML
599
     *
600
     * @link https://htmlpreview.github.io/?https://raw.githubusercontent.com/smtech/lti-configuration-xml/master/doc/classes/smtech.LTI.Configuration.Generator.html#method_saveXML Pass-through to `Generator::saveXML()`
601
     *
602
     * @return string
603
     */
604
    public function saveConfigurationXML()
605
    {
606
        try {
607
            return $this->getGenerator()->saveXML();
608
        } catch (LTIConfigGeneratorException $e) {
609
            throw new ConfigurationException(
610
                $e->getMessage(),
611
                ConfigurationException::TOOL_PROVIDER
612
            );
613
        }
614
    }
615
616
    /**
617
     * Reset PHP session
618
     *
619
     * Handy for starting LTI authentication. Resets the session and stores a
620
     * reference to this toolbox object in `$_SESSION[Toolbox::class]`.
621
     *
622
     * @link http://stackoverflow.com/a/14329752 StackOverflow discussion
623
     *
624
     * @return void
625
     */
626
    public function resetSession()
627
    {
628
        /*
629
         * TODO not in love with suppressing errors
630
         */
631
        if (session_status() !== PHP_SESSION_ACTIVE) {
632
            @session_start();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
633
        }
634
        @session_destroy();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
635
        @session_unset();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
636
        @session_start();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
637
        session_regenerate_id(true);
638
        $_SESSION[__CLASS__] =& $this;
639
        session_write_close();
640
    }
641
}
642