Issues (4)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Toolbox.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/** Toolbox class */
3
4
namespace smtech\ReflexiveCanvasLTI;
5
6
use mysqli;
7
use Serializable;
8
9
use Log;
10
11
use Battis\AppMetadata;
12
use Battis\ConfigXML;
13
use Battis\DataUtilities;
14
15
use smtech\CanvasPest\CanvasPest;
16
use smtech\ReflexiveCanvasLTI\LTI\ToolProvider;
17
use smtech\ReflexiveCanvasLTI\Exception\ConfigurationException;
18
use smtech\LTI\Configuration\Generator;
19
use smtech\LTI\Configuration\LaunchPrivacy;
20
use smtech\LTI\Configuration\Exception\ConfigurationException as LTIConfigGeneratorException;
21
22
/**
23
 * A toolbox of tools for quickly constructing LTI tool providers that hook
24
 * back into the Canvas API reflexively.
25
 *
26
 * The basic idea is that you need an XML configuration file of credentials and
27
 * use that to instantiate the Toolbox. The toolbox can then perform LTI
28
 * authentication, handle API requests, generate an LTI Configuration XML file,
29
 * etc. for you.
30
 *
31
 * @author Seth Battis
32
 * @version v1.0
33
 */
34
class Toolbox implements Serializable
35
{
36
    /** Default level of information-sharing privacy between consumer and provider */
37
    const DEFAULT_LAUNCH_PRIVACY = 'public';
38
39
    /** Name of the database table backing the tool metadata */
40
    const TOOL_METADATA_TABLE = 'tool_metadata';
41
42
    /** The path to the configuration file from which this toolbox was generated */
43
    const TOOL_CONFIG_FILE = 'TOOL_CONFIG_FILE';
44
45
    /**
46
     * The (ideally globally unique) identifier for the LTI tool provider
47
     */
48
    const TOOL_ID = 'TOOL_ID';
49
50
    /** The human-readable name of the tool */
51
    const TOOL_NAME = 'TOOL_NAME';
52
53
    /** The human-readable description of the tool */
54
    const TOOL_DESCRIPTION = 'TOOL_DESCRIPTION';
55
56
    /** The URL of the tool's icon image (if present) */
57
    const TOOL_ICON_URL = 'TOOL_ICON_URL';
58
59
    /** The domain from which Tool Consumer requests may emanate for the tool */
60
    const TOOL_DOMAIN = 'TOOL_DOMAIN';
61
62
    /** The URL of the script that will handle LTI authentication */
63
    const TOOL_LAUNCH_URL = 'TOOL_LAUNCH_URL';
64
65
    /** The level of information sharing between the LMS and the tool */
66
    const TOOL_LAUNCH_PRIVACY = 'TOOL_LAUNCH_PRIVACY';
67
68
    /** The path to the tool's log file */
69
    const TOOL_LOG = 'TOOL_LOG';
70
71
    /** An associative array of LTI request types and the URL that handles that request. */
72
    const TOOL_HANDLER_URLS = 'TOOL_HANDLER_URLS';
73
74
    /** An associative array of Canvas API credentials (`url` and `token`) */
75
    const TOOL_CANVAS_API = 'TOOL_CANVAS_API';
76
77
    /**
78
     * Persistent metadata storage
79
     * @var AppMetadata
80
     * @see Toolbox::config() Toolbox::config()
81
     */
82
    protected $metadata = false;
83
84
    /**
85
     * Object-oriented access to the Canvas API
86
     * @var CanvasPest
87
     */
88
    protected $api = false;
89
90
    /**
91
     * MySQL database connection
92
     * @var mysqli
93
     */
94
    protected $mysql = false;
95
96
    /**
97
     * LTI Tool Provider for handling authentication and consumer/user management
98
     * @var ToolProvider
99
     */
100
    protected $toolProvider;
101
102
    /**
103
     * Generator for LTI Configuration XML files
104
     * @var Generator
105
     */
106
    protected $generator;
107
108
    /**
109
     * Log file manager
110
     * @var Log
111
     */
112
    protected $logger = false;
113
114
    /**
115
     * Queue of delayed log messages (waiting for a logger instance)
116
     * @return array
117
     */
118
    protected $logQueue = [];
119
120
    /**
121
     * Provide serialization support for the Toolbox
122
     *
123
     * This allows a Toolbox to be stored in the `$_SESSION` variables.
124
     *
125
     * Caveat emptor: because `mysqli` objects can not be serialized,
126
     * serialization is limited to storing a reference to the configuration file
127
     * that generated this object, which will be reaccessed (along with cached
128
     * configuration metadata) when the object is unserialized.
129
     *
130
     * @return string
131
     */
132
    public function serialize()
133
    {
134
        return serialize([
135
            'config' => $this->config(static::TOOL_CONFIG_FILE)
136
        ]);
137
    }
138
139
    /**
140
     * Provide serialization support for Toolbox
141
     *
142
     * This allows a Toolbox to be stored in the `$_SESSION` variables.
143
     *
144
     * @see Toolbox::serialize() `Toolbox::serialize()` has more information on the
145
     *      specifics of the serialization approach.
146
     *
147
     * @param  string $serialized A Toolbox object serialized by `Toolbox::serialize()`
148
     * @return Toolbox
149
     */
150
    public function unserialize($serialized)
151
    {
152
        $data = unserialize($serialized);
153
        $this->loadConfiguration($data['config']);
154
    }
155
156
    /**
157
     * Create a Toolbox instance from a configuration file
158
     *
159
     * @param  string $configFilePath Path to the configuration file
160
     * @param  boolean $forceRecache Whether or not to rely on cached
161
     *     configuration metadata or to force a refresh from the configuration
162
     *     file
163
     * @return Toolbox
164
     */
165
    public static function fromConfiguration($configFilePath, $forceRecache = false)
166
    {
167
        return new static($configFilePath, $forceRecache);
168
    }
169
170
    /**
171
     * Construct a Toolbox instance from a configuration file
172
     *
173
     * @see Toolbox::fromConfiguration() Use `Toolbox::fromConfiguration()`
174
     *
175
     * @param string $configFilePath
176
     * @param boolean $forceRecache
177
     */
178
    private function __construct($configFilePath, $forceRecache = false)
179
    {
180
        $this->loadConfiguration($configFilePath, $forceRecache);
181
    }
182
183
    /**
184
     * Update a Toolbox instance from a configuration file
185
     *
186
     * @see Toolbox::fromConfiguration() Use `Toolbox::fromConfiguration()`
187
     *
188
     * @param  string $configFilePath
189
     * @param  boolean $forceRecache
190
     * @return void
191
     */
192
    protected function loadConfiguration($configFilePath, $forceRecache = false)
193
    {
194
        /* load the configuration file */
195
        $config = new ConfigXML($configFilePath);
196
197
        /* configure database connections */
198
        $this->setMySQL($config->newInstanceOf(mysqli::class, '/config/mysql'));
199
200
        /* configure metadata caching */
201
        $id = $config->toString('/config/tool/id');
202
        if (empty($id)) {
203
            $id = basename(dirname($configFilePath)) . '_' . md5(__DIR__ . file_get_contents($configFilePath));
204
            $this->log("    Automatically generated ID $id");
205
        }
206
        $this->setMetadata(new AppMetadata($this->mysql, $id, self::TOOL_METADATA_TABLE));
207
208
        /* is this the same config file we've been using, or a different one? */
209
        if ($this->config(static::TOOL_CONFIG_FILE) != $configFilePath) {
210
            $this->log('Provided config file path does not match ' . $this->config(static::TOOL_CONFIG_FILE));
211
            $forceRecache = true;
212
        }
213
        if ($forceRecache) {
214
            $this->log("Resetting LTI configuration from provided config $configFilePath");
215
            $this->config(static::TOOL_CONFIG_FILE, realpath($configFilePath));
216
        }
217
218
        /* update metadata */
219
        if ($forceRecache ||
220
            empty($this->config(static::TOOL_ID)) ||
221
            empty($this->config(static::TOOL_LAUNCH_URL)) ||
222
            empty($this->config(static::TOOL_CONFIG_FILE))) {
223
            $this->configToolMetadata($config, $id);
224
        }
225
226
        /* configure logging */
227
        if ($forceRecache || empty($this->config(static::TOOL_LOG))) {
228
            $this->configLog($config);
229
        }
230
        $this->setLog(Log::singleton('file', $this->config(static::TOOL_LOG)));
231
232
        /* configure tool provider */
233
        if ($forceRecache || empty($this->config(static::TOOL_HANDLER_URLS))) {
234
            $this->configToolProvider($config);
235
        }
236
237
        /* configure API access */
238
        if ($forceRecache || empty($this->config(static::TOOL_CANVAS_API))) {
239
            $this->configApi($config);
240
        }
241
    }
242
243
    /**
244
     * Configure the tool metadata from a configuration file
245
     * @param ConfigXML $config Configuration file object
246
     * @param string $id Unique, potentially auto-generated tool ID
247
     * @return void
248
     */
249
    protected function configToolMetadata(ConfigXML $config, $id)
250
    {
251
        $tool = $config->toArray('/config/tool')[0];
252
253
        $this->config(static::TOOL_ID, $id);
254
        $this->config(static::TOOL_NAME, (empty($tool['name']) ? $id : $tool['name']));
255
        $configPath = dirname($this->config(static::TOOL_CONFIG_FILE));
256
257
        $params = [
258
            'description' => static::TOOL_DESCRIPTION,
259
            'icon' => static::TOOL_ICON_URL,
260
            'domain' => static::TOOL_DOMAIN,
261
            'launch-privacy' => static::TOOL_LAUNCH_PRIVACY,
262
            'authenticate' => static::TOOL_LAUNCH_URL
263
        ];
264
265
        foreach ($params as $src => $dest) {
266
            if (empty($tool[$src])) {
267
                $this->clearConfig($dest);
268
            } else {
269
                if (substr($dest, -4) == '_URL') {
270
                    $this->config($dest, DataUtilities::URLfromPath(
271
                        $tool[$src],
272
                        $_SERVER,
273
                        $configPath)
274
                    );
275
                } else {
276
                    $this->config($dest, $tool[$src]);
277
                }
278
            }
279
        }
280
281
        if (empty($this->config(static::TOOL_LAUNCH_PRIVACY))) {
282
            $this->config(static::TOOL_LAUNCH_PRIVACY, static::DEFAULT_LAUNCH_PRIVACY);
283
        }
284
285
        if (empty($this->config(static::TOOL_LAUNCH_URL))) {
286
            $this->config(static::TOOL_LAUNCH_URL, DataUtilities::URLfromPath($_SERVER['SCRIPT_FILENAME']));
287
        }
288
289
        $this->log('Tool metadata configured');
290
    }
291
292
    /**
293
     * Configure the logger object from a configuration file
294
     *
295
     * This will also flush any backlog of queued messages that have been
296
     * waiting for a logger object to be ready.
297
     *
298
     * @param ConfigXML $config Configuration file object
299
     * @return void
300
     */
301
    protected function configLog(ConfigXML $config)
302
    {
303
        $configPath = dirname($this->config(static::TOOL_CONFIG_FILE));
304
        $log = "$configPath/" . $config->toString('/config/tool/log');
305
        shell_exec("touch \"$log\"");
306
        $this->config(static::TOOL_LOG, realpath($log));
307
        $this->flushLogQueue();
308
    }
309
310
    /**
311
     * Configure tool provider object from configuration file
312
     * @param ConfigXML $config Configuration file object
313
     * @return void
314
     */
315
    protected function configToolProvider(ConfigXML $config)
316
    {
317
        $handlers = $config->toArray('/config/tool/handlers')[0];
318
        if (empty($handlers) || !is_array($handlers)) {
319
            throw new ConfigurationException(
320
                'At least one handler/URL pair must be specified',
321
                ConfigurationException::TOOL_PROVIDER
322
            );
323
        }
324
        foreach ($handlers as $request => $path) {
325
            $handlers[$request] = DataUtilities::URLfromPath(
326
                dirname($this->config(static::TOOL_CONFIG_FILE)) . "/$path"
327
            );
328
        }
329
        $this->config(static::TOOL_HANDLER_URLS, $handlers);
330
        $this->log('Tool provider handler URLs configured');
331
    }
332
333
    /**
334
     * Configure API access object from configuration file
335
     * @param ConfigXML $config Configuration file object
336
     * @return void
337
     */
338
    protected function configApi(ConfigXML $config)
339
    {
340
        $this->config(static::TOOL_CANVAS_API, $config->toArray('/config/canvas')[0]);
341
        if (empty($this->config(static::TOOL_CANVAS_API))) {
342
            throw new ConfigurationException(
343
                'Canvas API credentials must be provided',
344
                ConfigurationException::CANVAS_API_MISSING
345
            );
346
        }
347
        $this->log('Canvas API credentials configured');
348
    }
349
350
    /**
351
     * Update toolbox configuration metadata object
352
     *
353
     * @param AppMetadata $metadata
354
     */
355
    public function setMetadata(AppMetadata $metadata)
356
    {
357
        $this->metadata = $metadata;
358
    }
359
360
    /**
361
     * Get the toolbox configuration metadata object
362
     *
363
     * @return AppMetadata
364
     */
365
    public function getMetadata()
366
    {
367
        return $this->metadata;
368
    }
369
370
    /**
371
     * Access or update a specific configuration metadata key/value pair
372
     *
373
     * The `TOOL_*` constants refer to keys used by the Toolbox by default.
374
     *
375
     * @param  string $key The metadata key to look up/create/update
376
     * @param  mixed $value (Optional) If not present (or `null`), the current
377
     *     metadata is returned. If present, the metadata is created/updated
378
     * @return mixed If not updating the metadata, the metadata (if any)
379
     *     currently stored
380
     */
381
    public function config($key, $value = null)
382
    {
383
        if ($value !== null) {
384
            $this->metadata[$key] = $value;
385
        } elseif (isset($this->metadata[$key])) {
386
            return $this->metadata[$key];
387
        } else {
388
            return null;
389
        }
390
    }
391
392
    /**
393
     * Wipe a particular configuration key from storage
394
     * @param string $key
395
     * @return boolean `TRUE` if cleared, `FALSE` if not found
396
     */
397
    public function clearConfig($key)
398
    {
399
        if (isset($this->metadata[$key])) {
400
            unset($this->metadata[$key]);
401
            return true;
402
        }
403
        return false;
404
    }
405
406
407
    /**
408
     * Reset PHP session
409
     *
410
     * Handy for starting LTI authentication. Resets the session and stores a
411
     * reference to this toolbox object in `$_SESSION[Toolbox::class]`.
412
     *
413
     * @link http://stackoverflow.com/a/14329752 StackOverflow discussion
414
     *
415
     * @return void
416
     */
417
    public function resetSession()
418
    {
419
        /*
420
         * TODO not in love with suppressing errors
421
         */
422
        if (session_status() !== PHP_SESSION_ACTIVE) {
423
            @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...
424
        }
425
        @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...
426
        @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...
427
        @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...
428
        session_regenerate_id(true);
429
        $_SESSION[__CLASS__] =& $this;
430
        session_write_close();
431
    }
432
433
    /**
434
     * Update the ToolProvider object
435
     *
436
     * @param ToolProvider $toolProvider
437
     */
438
    public function setToolProvider(ToolProvider $toolProvider)
439
    {
440
        $this->toolProvider = $toolProvider;
441
    }
442
443
    /**
444
     * Get the ToolProvider object
445
     *
446
     * This does some just-in-time initialization, so that if the ToolProvider
447
     * has not yet been accessed, it will be instantiated and initialized by this
448
     * method.
449
     *
450
     * @return ToolProvider
451
     */
452
    public function getToolProvider()
453
    {
454
        if (empty($this->toolProvider)) {
455
            $this->setToolProvider(
456
                new ToolProvider(
457
                    $this->mysql,
458
                    $this->metadata['TOOL_HANDLER_URLS']
459
                )
460
            );
461
        }
462
        return $this->toolProvider;
463
    }
464
465
    /**
466
     * Authenticate an LTI launch request
467
     *
468
     * @return void
469
     * @codingStandardsIgnoreStart
470
     */
471
    public function lti_authenticate()
472
    {
473
        /* @codingStandardsIgnoreEnd */
474
        $this->getToolProvider()->handle_request();
475
    }
476
477
    /**
478
     * Are we (or should we be) in the midst of authenticating an LTI launch request?
479
     *
480
     * @return boolean
481
     * @codingStandardsIgnoreStart
482
     */
483
    public function lti_isLaunching()
484
    {
485
        /* @codingStandardsIgnoreEnd */
486
        return !empty($_POST['lti_message_type']);
487
    }
488
489
    /**
490
     * Create a new Tool consumer
491
     *
492
     * @see ToolProvider::createConsumer() Pass-through to `ToolProvider::createConsumer()`
493
     *
494
     * @param  string $name Human-readable name
495
     * @param  string $key (Optional) Consumer key (unique within the tool provider)
496
     * @param  string $secret (Optional) Shared secret
497
     * @return boolean Whether or not the consumer was created
498
     * @codingStandardsIgnoreStart
499
     */
500
    public function lti_createConsumer($name, $key = false, $secret = false)
501
    {
502
        /* @codingStandardsIgnoreEnd */
503
        if ($this->getToolProvider()->createConsumer($name, $key, $secret)) {
504
            $this->log("Created consumer $name");
505
            return true;
506
        } else {
507
            $this->log("Could not recreate consumer '$name', consumer already exists");
508
            return false;
509
        }
510
    }
511
512
    /**
513
     * Get the list of consumers for this tool
514
     *
515
     * @see ToolProvider::getConsumers() Pass-through to `ToolProvider::getConsumers()`
516
     *
517
     * @return LTI_Consumer[]
518
     * @codingStandardsIgnoreStart
519
     */
520
    public function lti_getConsumers()
521
    {
522
        /* @codingStandardsIgnoreEnd */
523
        return $this->getToolProvider()->getConsumers();
524
    }
525
526
    /**
527
     * Update the API interaction object
528
     *
529
     * @param CanvasPest $api
530
     */
531
    public function setAPI(CanvasPest $api)
532
    {
533
        $this->api = $api;
534
    }
535
536
    /**
537
     * Get the API interaction object
538
539
     * @return CanvasPest
540
     */
541
    public function getAPI()
542
    {
543
        if (empty($this->api)) {
544
            if (!empty($this->config(static::TOOL_CANVAS_API)['token'])) {
545
                $this->setAPI(new CanvasPest(
546
                    'https://' . $_SESSION[ToolProvider::class]['canvas']['api_domain'] . '/api/v1',
547
                    $this->config(static::TOOL_CANVAS_API)['token']
548
                ));
549
            } else {
550
                throw new ConfigurationException(
551
                    'Canvas URL and Token required',
552
                    ConfigurationException::CANVAS_API_INCORRECT
553
                );
554
            }
555
        }
556
        return $this->api;
557
    }
558
559
    /**
560
     * Make a GET request to the API
561
     *
562
     * @codingStandardsIgnoreStart
563
     * @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()
564
     * @codingStandardsIgnoreEnd
565
     *
566
     * @param  string $url
567
     * @param  string[] $data (Optional)
568
     * @param  string[] $headers (Optional)
569
     * @return \smtech\CanvasPest\CanvasObject|\smtech\CanvasPest\CanvasArray
570
     * @codingStandardsIgnoreStart
571
     */
572
    public function api_get($url, $data = [], $headers = [])
573
    {
574
        /* @codingStandardsIgnoreEnd */
575
        return $this->getAPI()->get($url, $data, $headers);
576
    }
577
578
    /**
579
     * Make a POST request to the API
580
     *
581
     * @codingStandardsIgnoreStart
582
     * @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()
583
     * @codingStandardsIgnoreEnd
584
     *
585
     * @param  string $url
586
     * @param  string[] $data (Optional)
587
     * @param  string[] $headers (Optional)
588
     * @return \smtech\CanvasPest\CanvasObject|\smtech\CanvasPest\CanvasArray
589
     * @codingStandardsIgnoreStart
590
     */
591
    public function api_post($url, $data = [], $headers = [])
592
    {
593
        /* @codingStandardsIgnoreEnd */
594
        return $this->getAPI()->post($url, $data, $headers);
595
    }
596
597
    /**
598
     * Make a PUT request to the API
599
     *
600
     * @codingStandardsIgnoreStart
601
     * @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()
602
     * @codingStandardsIgnoreEnd
603
     *
604
     * @param  string $url
605
     * @param  string[] $data (Optional)
606
     * @param  string[] $headers (Optional)
607
     * @return \smtech\CanvasPest\CanvasObject|\smtech\CanvasPest\CanvasArray
608
     * @codingStandardsIgnoreStart
609
     */
610
    public function api_put($url, $data = [], $headers = [])
611
    {
612
        /* @codingStandardsIgnoreEnd */
613
        return $this->getAPI()->put($url, $data, $headers);
614
    }
615
616
    /**
617
     * Make a DELETE request to the API
618
     *
619
     * @codingStandardsIgnoreStart
620
     * @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()
621
     * @codingStandardsIgnoreEnd
622
     *
623
     * @param  string $url
624
     * @param  string[] $data (Optional)
625
     * @param  string[] $headers (Optional)
626
     * @return \smtech\CanvasPest\CanvasObject|\smtech\CanvasPest\CanvasArray
627
     * @codingStandardsIgnoreStart
628
     */
629
    public function api_delete($url, $data = [], $headers = [])
630
    {
631
        /* @codingStandardsIgnoreEnd */
632
        return $this->getAPI()->delete($url, $data, $headers);
633
    }
634
635
    /**
636
     * Set MySQL connection object
637
     *
638
     * @param mysqli $mysql
639
     */
640
    public function setMySQL(mysqli $mysql)
641
    {
642
        $this->mysql = $mysql;
643
    }
644
645
    /**
646
     * Get MySQL connection object
647
     *
648
     * @return mysqli
649
     */
650
    public function getMySQL()
651
    {
652
        return $this->mysql;
653
    }
654
655
    /**
656
     * Make a MySQL query
657
     *
658
     * @link http://php.net/manual/en/mysqli.query.php Pass-through to `mysqli::query()`
659
     * @param string $query
660
     * @param int $resultMode (Optional, defaults to `MYSQLI_STORE_RESULT`)
661
     * @return mixed
662
     * @codingStandardsIgnoreStart
663
     */
664
    public function mysql_query($query, $resultMode = MYSQLI_STORE_RESULT)
665
    {
666
        /* @codingStandardsIgnoreEnd */
667
        return $this->getMySQL()->query($query, $resultMode);
668
    }
669
670
    /**
671
     * Check if the logger object is ready for use
672
     * @return boolean `TRUE` if ready, `FALSE` otherwise
673
     */
674
    protected function logReady()
675
    {
676
        return is_a($this->logger, Log::class);
677
    }
678
679
    /**
680
     * Set log file manager
681
     *
682
     * @param Log $log
683
     */
684
    public function setLog(Log $log)
685
    {
686
        $this->logger = $log;
687
    }
688
689
    /**
690
     * Get log file manager
691
     *
692
     * @return Log
693
     */
694
    public function getLog()
695
    {
696
        return $this->logger;
697
    }
698
699
    /**
700
     * Queue a message for delayed logging
701
     * @param string $message
702
     * @param string $priority
703
     * @return void
704
     */
705
    protected function queueLog($message, $priority = null)
706
    {
707
        $this->logQueue[] = ['message' => $message, 'priority' => $priority];
708
    }
709
710
    /**
711
     * Flush the delayed log queue
712
     * @return void
713
     */
714
    protected function flushLogQueue()
715
    {
716
        if ($this->logReady() && !empty($this->logQueue)) {
717
            foreach ($this->logQueue as $entry) {
718
                $this->getLog()->log($entry['message'], $entry['priority']);
719
            }
720
            $this->logQueue = [];
721
        }
722
    }
723
724
    /**
725
     * Add a message to the tool log file
726
     *
727
     * If no logger object is ready, the message will be queued for delayed
728
     * logging until a logger object is ready.
729
     *
730
     * @link https://pear.php.net/package/Log/docs/1.13.1/Log/Log_file.html#methodlog
731
     *      Pass-throgh to `Log_file::log()`
732
     *
733
     * @param string $message
734
     * @param string $priority (Optional, defaults to `PEAR_LOG_INFO`)
735
     * @return boolean Success
736
     */
737
    public function log($message, $priority = null)
738
    {
739
        if ($this->logReady()) {
740
            $this->flushLogQueue();
741
            return $this->getLog()->log($message, $priority);
742
        } else {
743
            $this->queueLog($message, $priority);
744
        }
745
    }
746
747
    /**
748
     * Set the LTI Configuration generator
749
     *
750
     * @param Generator $generator
751
     */
752
    public function setGenerator(Generator $generator)
753
    {
754
        $this->generator = $generator;
755
    }
756
757
    /**
758
     * Get the LTI Configuration generator
759
     *
760
     * @return Generator
761
     */
762
    public function getGenerator()
763
    {
764
        try {
765
            if (empty($this->generator)) {
766
                $this->setGenerator(
767
                    new Generator(
768
                        $this->config(static::TOOL_NAME),
769
                        $this->config(static::TOOL_ID),
770
                        $this->config(static::TOOL_LAUNCH_URL),
771
                        (empty($this->config(static::TOOL_DESCRIPTION)) ?
772
                            false : $this->config(static::TOOL_DESCRIPTION)),
773
                        (empty($this->config(static::TOOL_ICON_URL)) ?
774
                            false : $this->config(static::TOOL_ICON_URL)),
775
                        (empty($this->config(static::TOOL_LAUNCH_PRIVACY)) ?
776
                            LaunchPrivacy::USER_PROFILE() : $this->config(static::TOOL_LAUNCH_PRIVACY)),
777
                        (empty($this->config(static::TOOL_DOMAIN)) ? false : $this->config(static::TOOL_DOMAIN))
778
                    )
779
                );
780
            }
781
        } catch (LTIConfigGeneratorException $e) {
782
            throw new ConfigurationException(
783
                $e->getMessage(),
784
                ConfigurationException::TOOL_PROVIDER
785
            );
786
        }
787
        return $this->generator;
788
    }
789
790
    /**
791
     * Get the LTI configuration XML
792
     *
793
     * @codingStandardsIgnoreStart
794
     * @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()`
795
     * @codingStandardsIgnoreEnd
796
     *
797
     * @return string
798
     */
799
    public function saveConfigurationXML()
800
    {
801
        try {
802
            return $this->getGenerator()->saveXML();
803
        } catch (LTIConfigGeneratorException $e) {
804
            throw new ConfigurationException(
805
                $e->getMessage(),
806
                ConfigurationException::TOOL_PROVIDER
807
            );
808
        }
809
    }
810
}
811