GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 0e9349...d2bdfd )
by Nicolas
08:54 queued 04:10
created

ExtensionManager::__requiresUpdate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 10
rs 9.4285
cc 3
eloc 5
nc 3
nop 2
1
<?php
2
3
/**
4
 * @package toolkit
5
 */
6
/**
7
 * The ExtensionManager class is responsible for managing all extensions
8
 * in Symphony. Extensions are stored on the file system in the `EXTENSIONS`
9
 * folder. They are autodiscovered where the Extension class name is the same
10
 * as it's folder name (excluding the extension prefix).
11
 */
12
13
include_once FACE . '/interface.fileresource.php';
14
include_once TOOLKIT . '/class.extension.php';
15
16
class ExtensionManager implements FileResource
17
{
18
    /**
19
     * An array of all the objects that the Manager is responsible for.
20
     * Defaults to an empty array.
21
     * @var array
22
     */
23
    protected static $_pool = array();
24
25
    /**
26
     * An array of all extensions whose status is enabled
27
     * @var array
28
     */
29
    private static $_enabled_extensions = array();
30
31
    /**
32
     * An array of all the subscriptions to Symphony delegates made by extensions.
33
     * @var array
34
     */
35
    private static $_subscriptions = array();
36
37
    /**
38
     * An associative array of all the extensions in `tbl_extensions` where
39
     * the key is the extension name and the value is an array
40
     * representation of it's accompanying database row.
41
     * @var array
42
     */
43
    private static $_extensions = array();
44
45
    /**
46
     * An associative array of all the providers from the enabled extensions.
47
     * The key is the type of object, with the value being an associative array
48
     * with the name, classname and path to the object
49
     *
50
     * @since Symphony 2.3
51
     * @var array
52
     */
53
    private static $_providers = array();
54
55
    /**
56
     * The constructor will populate the `$_subscriptions` variable from
57
     * the `tbl_extension` and `tbl_extensions_delegates` tables.
58
     */
59
    public function __construct()
60
    {
61
        if (empty(self::$_subscriptions) && Symphony::Database()->isConnected()) {
62
            $subscriptions = Symphony::Database()->fetch(
63
                "SELECT t1.name, t2.page, t2.delegate, t2.callback
64
                FROM `tbl_extensions` as t1 INNER JOIN `tbl_extensions_delegates` as t2 ON t1.id = t2.extension_id
65
                WHERE t1.status = 'enabled'
66
                ORDER BY t2.delegate, t1.name"
67
            );
68
69
            foreach ($subscriptions as $subscription) {
70
                self::$_subscriptions[$subscription['delegate']][] = $subscription;
71
            }
72
        }
73
    }
74
75
    public static function __getHandleFromFilename($filename)
76
    {
77
        return false;
78
    }
79
80
    /**
81
     * Given a name, returns the full class name of an Extension.
82
     * Extension use an 'extension' prefix.
83
     *
84
     * @param string $name
85
     *  The extension handle
86
     * @return string
87
     */
88
    public static function __getClassName($name)
89
    {
90
        return 'extension_' . $name;
91
    }
92
93
    /**
94
     * Finds an Extension by name by searching the `EXTENSIONS` folder and
95
     * returns the path to the folder.
96
     *
97
     * @param string $name
98
     *  The extension folder
99
     * @return string
100
     */
101
    public static function __getClassPath($name)
102
    {
103
        return EXTENSIONS . strtolower("/$name");
104
    }
105
106
    /**
107
     * Given a name, return the path to the driver of the Extension.
108
     *
109
     * @see toolkit.ExtensionManager#__getClassPath()
110
     * @param string $name
111
     *  The extension folder
112
     * @return string
113
     */
114
    public static function __getDriverPath($name)
115
    {
116
        return self::__getClassPath($name) . '/extension.driver.php';
117
    }
118
119
    /**
120
     * This function returns an instance of an extension from it's name
121
     *
122
     * @param string $name
123
     *  The name of the Extension Class minus the extension prefix.
124
     * @throws SymphonyErrorPage
125
     * @throws Exception
126
     * @return Extension
127
     */
128
    public static function getInstance($name)
129
    {
130
        return (isset(self::$_pool[$name]) ? self::$_pool[$name] : self::create($name));
131
    }
132
133
    /**
134
     * Populates the `ExtensionManager::$_extensions` array with all the
135
     * extensions stored in `tbl_extensions`. If `ExtensionManager::$_extensions`
136
     * isn't empty, passing true as a parameter will force the array to update
137
     *
138
     * @param boolean $update
139
     *  Updates the `ExtensionManager::$_extensions` array even if it was
140
     *  populated, defaults to false.
141
     * @throws DatabaseException
142
     */
143
    private static function __buildExtensionList($update = false)
144
    {
145
        if (empty(self::$_extensions) || $update) {
146
            self::$_extensions = Symphony::Database()->fetch("SELECT * FROM `tbl_extensions`", 'name');
147
        }
148
    }
149
150
    /**
151
     * Returns the status of an Extension given an associative array containing
152
     * the Extension `handle` and `version` where the `version` is the file
153
     * version, not the installed version. This function returns an array
154
     * which may include a maximum of two statuses.
155
     *
156
     * @param array $about
157
     *  An associative array of the extension meta data, typically returned
158
     *  by `ExtensionManager::about()`. At the very least this array needs
159
     *  `handle` and `version` keys.
160
     * @return array
161
     *  An array of extension statuses, with the possible values being
162
     * `EXTENSION_ENABLED`, `EXTENSION_DISABLED`, `EXTENSION_REQUIRES_UPDATE`
163
     *  or `EXTENSION_NOT_INSTALLED`. If an extension doesn't exist,
164
     *  `EXTENSION_NOT_INSTALLED` will be returned.
165
     */
166
    public static function fetchStatus($about)
167
    {
168
        $return = array();
169
        self::__buildExtensionList();
170
171
        if (isset($about['handle']) && array_key_exists($about['handle'], self::$_extensions)) {
172
            if (self::$_extensions[$about['handle']]['status'] == 'enabled') {
173
                $return[] = Extension::EXTENSION_ENABLED;
174
            } else {
175
                $return[] = Extension::EXTENSION_DISABLED;
176
            }
177
        } else {
178
            $return[] = Extension::EXTENSION_NOT_INSTALLED;
179
        }
180
181
        if (isset($about['handle'], $about['version']) && self::__requiresUpdate($about['handle'], $about['version'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::__requiresUpdate($...e'], $about['version']) of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
182
            $return[] = Extension::EXTENSION_REQUIRES_UPDATE;
183
        }
184
185
        return $return;
186
    }
187
188
    /**
189
     * A convenience method that returns an extension version from it's name.
190
     *
191
     * @param string $name
192
     *  The name of the Extension Class minus the extension prefix.
193
     * @return string
194
     */
195
    public static function fetchInstalledVersion($name)
196
    {
197
        self::__buildExtensionList();
198
199
        return (isset(self::$_extensions[$name]) ? self::$_extensions[$name]['version'] : null);
200
    }
201
202
    /**
203
     * A convenience method that returns an extension ID from it's name.
204
     *
205
     * @param string $name
206
     *  The name of the Extension Class minus the extension prefix.
207
     * @return integer
208
     */
209
    public static function fetchExtensionID($name)
210
    {
211
        self::__buildExtensionList();
212
213
        return self::$_extensions[$name]['id'];
214
    }
215
216
    /**
217
     * Return an array all the Provider objects supplied by extensions,
218
     * optionally filtered by a given `$type`.
219
     *
220
     * @since Symphony 2.3
221
     * @todo Add information about the possible types
222
     * @param string $type
223
     *  This will only return Providers of this type. If null, which is
224
     *  default, all providers will be returned.
225
     * @throws Exception
226
     * @throws SymphonyErrorPage
227
     * @return array
228
     *  An array of objects
229
     */
230
    public static function getProvidersOf($type = null)
231
    {
232
        // Loop over all extensions and build an array of providable objects
233
        if (empty(self::$_providers)) {
234
            self::$_providers = array();
235
236
            foreach (self::listInstalledHandles() as $handle) {
237
                $obj = self::getInstance($handle);
238
239
                if (!method_exists($obj, 'providerOf')) {
240
                    continue;
241
                }
242
243
                $providers = $obj->providerOf();
244
245
                if (empty($providers)) {
246
                    continue;
247
                }
248
249
                // For each of the matching objects (by $type), resolve the object path
250
                self::$_providers = array_merge_recursive(self::$_providers, $obj->providerOf());
251
            }
252
        }
253
254
        // Return an array of objects
255
        if (is_null($type)) {
256
            return self::$_providers;
257
        }
258
259
        if (!isset(self::$_providers[$type])) {
260
            return array();
261
        }
262
263
        return self::$_providers[$type];
264
    }
265
266
    /**
267
     * This function will return the `Cacheable` object with the appropriate
268
     * caching layer for the given `$key`. This `$key` should be stored in
269
     * the Symphony configuration in the caching group with a reference
270
     * to the class of the caching object. If the key is not found, this
271
     * will return a default `Cacheable` object created with the MySQL driver.
272
     *
273
     * @since Symphony 2.4
274
     * @param string $key
275
     *  Should be a reference in the Configuration file to the Caching class
276
     * @param boolean $reuse
277
     *  By default true, which will reuse an existing Cacheable object of `$key`
278
     *  if it exists. If false, a new instance will be generated.
279
     * @return Cacheable
280
     */
281
    public static function getCacheProvider($key = null, $reuse = true)
282
    {
283
        $cacheDriver = Symphony::Configuration()->get($key, 'caching');
284
285
        if (in_array($cacheDriver, array_keys(Symphony::ExtensionManager()->getProvidersOf('cache')))) {
286
            $cacheable = new $cacheDriver;
287
        } else {
288
            $cacheable = Symphony::Database();
289
            $cacheDriver = 'CacheDatabase';
290
        }
291
292
        if ($reuse === false) {
293
            return new Cacheable($cacheable);
294
        } elseif (!isset(self::$_pool[$cacheDriver])) {
295
            self::$_pool[$cacheDriver] = new Cacheable($cacheable);
296
        }
297
298
        return self::$_pool[$cacheDriver];
299
    }
300
301
    /**
302
     * Determines whether the current extension is installed or not by checking
303
     * for an id in `tbl_extensions`
304
     *
305
     * @param string $name
306
     *  The name of the Extension Class minus the extension prefix.
307
     * @return boolean
308
     */
309
    private static function __requiresInstallation($name)
310
    {
311
        self::__buildExtensionList();
312
        $id = self::$_extensions[$name]['id'];
313
314
        return (is_numeric($id) ? false : true);
315
    }
316
317
    /**
318
     * Determines whether an extension needs to be updated or not using
319
     * PHP's `version_compare` function. This function will return the
320
     * installed version if the extension requires an update, or
321
     * false otherwise.
322
     *
323
     * @param string $name
324
     *  The name of the Extension Class minus the extension prefix.
325
     * @param string $file_version
326
     *  The version of the extension from the **file**, not the Database.
327
     * @return string|boolean
328
     *  If the given extension (by $name) requires updating, the installed
329
     *  version is returned, otherwise, if the extension doesn't require
330
     *  updating, false.
331
     */
332
    private static function __requiresUpdate($name, $file_version)
333
    {
334
        $installed_version = self::fetchInstalledVersion($name);
335
336
        if (is_null($installed_version)) {
337
            return false;
338
        }
339
340
        return (version_compare($installed_version, $file_version, '<') ? $installed_version : false);
341
    }
342
343
    /**
344
     * Enabling an extension will re-register all it's delegates with Symphony.
345
     * It will also install or update the extension if needs be by calling the
346
     * extensions respective install and update methods. The enable method is
347
     * of the extension object is finally called.
348
     *
349
     * @see toolkit.ExtensionManager#registerDelegates()
350
     * @see toolkit.ExtensionManager#__canUninstallOrDisable()
351
     * @param string $name
352
     *  The name of the Extension Class minus the extension prefix.
353
     * @throws SymphonyErrorPage
354
     * @throws Exception
355
     * @return boolean
356
     */
357
    public static function enable($name)
358
    {
359
        $obj = self::getInstance($name);
360
361
        // If not installed, install it
362
        if (self::__requiresInstallation($name) && $obj->install() === false) {
363
            // If the installation failed, run the uninstall method which
364
            // should rollback the install method. #1326
365
            $obj->uninstall();
366
            return false;
367
368
            // If the extension requires updating before enabling, then update it
369
        } elseif (($about = self::about($name)) && ($previousVersion = self::__requiresUpdate($name, $about['version'])) !== false) {
370
            $obj->update($previousVersion);
371
        }
372
373
        if (!isset($about)) {
374
            $about = self::about($name);
375
        }
376
377
        $id = self::fetchExtensionID($name);
378
379
        $fields = array(
380
            'name' => $name,
381
            'status' => 'enabled',
382
            'version' => $about['version']
383
        );
384
385
        // If there's no $id, the extension needs to be installed
386
        if (is_null($id)) {
387
            Symphony::Database()->insert($fields, 'tbl_extensions');
388
            self::__buildExtensionList(true);
389
390
            // Extension is installed, so update!
391
        } else {
392
            Symphony::Database()->update($fields, 'tbl_extensions', sprintf(" `id` = %d ", $id));
393
        }
394
395
        self::registerDelegates($name);
396
397
        // Now enable the extension
398
        $obj->enable();
399
400
        return true;
401
    }
402
403
    /**
404
     * Disabling an extension will prevent it from executing but retain all it's
405
     * settings in the relevant tables. Symphony checks that an extension can
406
     * be disabled using the `canUninstallorDisable()` before removing
407
     * all delegate subscriptions from the database and calling the extension's
408
     * `disable()` function.
409
     *
410
     * @see toolkit.ExtensionManager#removeDelegates()
411
     * @see toolkit.ExtensionManager#__canUninstallOrDisable()
412
     * @param string $name
413
     *  The name of the Extension Class minus the extension prefix.
414
     * @throws DatabaseException
415
     * @throws SymphonyErrorPage
416
     * @throws Exception
417
     * @return boolean
418
     */
419
    public static function disable($name)
420
    {
421
        $obj = self::getInstance($name);
422
423
        self::__canUninstallOrDisable($obj);
424
425
        $info = self::about($name);
426
        $id = self::fetchExtensionID($name);
427
428
        Symphony::Database()->update(
429
            array(
430
                'name' => $name,
431
                'status' => 'disabled',
432
                'version' => $info['version']
433
            ),
434
            'tbl_extensions',
435
            sprintf(" `id` = %d ", $id)
436
        );
437
438
        $obj->disable();
439
440
        self::removeDelegates($name);
441
442
        return true;
443
    }
444
445
    /**
446
     * Uninstalling an extension will unregister all delegate subscriptions and
447
     * remove all extension settings. Symphony checks that an extension can
448
     * be uninstalled using the `canUninstallorDisable()` before calling
449
     * the extension's `uninstall()` function. Alternatively, if this function
450
     * is called because the extension described by `$name` cannot be found
451
     * it's delegates and extension meta information will just be removed from the
452
     * database.
453
     *
454
     * @see toolkit.ExtensionManager#removeDelegates()
455
     * @see toolkit.ExtensionManager#__canUninstallOrDisable()
456
     * @param string $name
457
     *  The name of the Extension Class minus the extension prefix.
458
     * @throws Exception
459
     * @throws SymphonyErrorPage
460
     * @throws DatabaseException
461
     * @throws Exception
462
     * @return boolean
463
     */
464
    public static function uninstall($name)
465
    {
466
        // If this function is called because the extension doesn't exist,
467
        // then catch the error and just remove from the database. This
468
        // means that the uninstall() function will not run on the extension,
469
        // which may be a blessing in disguise as no entry data will be removed
470
        try {
471
            $obj = self::getInstance($name);
472
            self::__canUninstallOrDisable($obj);
473
            $obj->uninstall();
474
        } catch (SymphonyErrorPage $ex) {
475
            // Create a consistant key
476
            $key = str_replace('-', '_', $ex->getTemplateName());
477
478
            if ($key !== 'missing_extension') {
479
                throw $ex;
480
            }
481
        }
482
483
        self::removeDelegates($name);
484
        Symphony::Database()->delete('tbl_extensions', sprintf(" `name` = '%s' ", $name));
485
486
        return true;
487
    }
488
489
    /**
490
     * This functions registers an extensions delegates in `tbl_extensions_delegates`.
491
     *
492
     * @param string $name
493
     *  The name of the Extension Class minus the extension prefix.
494
     * @throws Exception
495
     * @throws SymphonyErrorPage
496
     * @return integer
497
     *  The Extension ID
498
     */
499
    public static function registerDelegates($name)
500
    {
501
        $obj = self::getInstance($name);
502
        $id = self::fetchExtensionID($name);
503
504
        if (!$id) {
505
            return false;
506
        }
507
508
        Symphony::Database()->delete('tbl_extensions_delegates', sprintf("
509
            `extension_id` = %d ", $id
510
        ));
511
512
        $delegates = $obj->getSubscribedDelegates();
513
514
        if (is_array($delegates) && !empty($delegates)) {
515
            foreach ($delegates as $delegate) {
516
                Symphony::Database()->insert(
517
                    array(
518
                        'extension_id' => $id,
519
                        'page' => $delegate['page'],
520
                        'delegate' => $delegate['delegate'],
521
                        'callback' => $delegate['callback']
522
                    ),
523
                    'tbl_extensions_delegates'
524
                );
525
            }
526
        }
527
528
        // Remove the unused DB records
529
        self::cleanupDatabase();
530
531
        return $id;
532
    }
533
534
    /**
535
     * This function will remove all delegate subscriptions for an extension
536
     * given an extension's name. This triggers `cleanupDatabase()`
537
     *
538
     * @see toolkit.ExtensionManager#cleanupDatabase()
539
     * @param string $name
540
     *  The name of the Extension Class minus the extension prefix.
541
     * @return boolean
542
     */
543
    public static function removeDelegates($name)
544
    {
545
        $delegates = Symphony::Database()->fetchCol('id', sprintf("
546
            SELECT tbl_extensions_delegates.`id`
547
            FROM `tbl_extensions_delegates`
548
            LEFT JOIN `tbl_extensions`
549
            ON (`tbl_extensions`.id = `tbl_extensions_delegates`.extension_id)
550
            WHERE `tbl_extensions`.name = '%s'",
551
            $name
552
        ));
553
554
        if (!empty($delegates)) {
555
            Symphony::Database()->delete('tbl_extensions_delegates', " `id` IN ('". implode("', '", $delegates). "') ");
556
        }
557
558
        // Remove the unused DB records
559
        self::cleanupDatabase();
560
561
        return true;
562
    }
563
564
    /**
565
     * This function checks that if the given extension has provided Fields,
566
     * Data Sources or Events, that they aren't in use before the extension
567
     * is uninstalled or disabled. This prevents exceptions from occurring when
568
     * accessing an object that was using something provided by this Extension
569
     * can't anymore because it has been removed.
570
     *
571
     * @param Extension $obj
572
     *  An extension object
573
     * @throws SymphonyErrorPage
574
     * @throws Exception
575
     * @return boolean
576
     */
577
    private static function __canUninstallOrDisable(Extension $obj)
578
    {
579
        $extension_handle = strtolower(preg_replace('/^extension_/i', null, get_class($obj)));
580
        $about = self::about($extension_handle);
581
582
        // Fields:
583 View Code Duplication
        if (is_dir(EXTENSIONS . "/{$extension_handle}/fields")) {
584
            foreach (glob(EXTENSIONS . "/{$extension_handle}/fields/field.*.php") as $file) {
585
                $type = preg_replace(array('/^field\./i', '/\.php$/i'), null, basename($file));
586
587
                if (FieldManager::isFieldUsed($type)) {
588
                    throw new Exception(
589
                        __('The field ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
590
                        . ' ' . __("Please remove it from your sections prior to uninstalling or disabling.")
591
                    );
592
                }
593
            }
594
        }
595
596
        // Data Sources:
597 View Code Duplication
        if (is_dir(EXTENSIONS . "/{$extension_handle}/data-sources")) {
598
            foreach (glob(EXTENSIONS . "/{$extension_handle}/data-sources/data.*.php") as $file) {
599
                $handle = preg_replace(array('/^data\./i', '/\.php$/i'), null, basename($file));
600
601
                if (PageManager::isDataSourceUsed($handle)) {
602
                    throw new Exception(
603
                        __('The Data Source ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
604
                        . ' ' . __("Please remove it from your pages prior to uninstalling or disabling.")
605
                    );
606
                }
607
            }
608
        }
609
610
        // Events
611 View Code Duplication
        if (is_dir(EXTENSIONS . "/{$extension_handle}/events")) {
612
            foreach (glob(EXTENSIONS . "/{$extension_handle}/events/event.*.php") as $file) {
613
                $handle = preg_replace(array('/^event\./i', '/\.php$/i'), null, basename($file));
614
615
                if (PageManager::isEventUsed($handle)) {
616
                    throw new Exception(
617
                        __('The Event ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
618
                        . ' ' . __("Please remove it from your pages prior to uninstalling or disabling.")
619
                    );
620
                }
621
            }
622
        }
623
624
        // Text Formatters
625 View Code Duplication
        if (is_dir(EXTENSIONS . "/{$extension_handle}/text-formatters")) {
626
            foreach (glob(EXTENSIONS . "/{$extension_handle}/text-formatters/formatter.*.php") as $file) {
627
                $handle = preg_replace(array('/^formatter\./i', '/\.php$/i'), null, basename($file));
628
629
                if (FieldManager::isTextFormatterUsed($handle)) {
630
                    throw new Exception(
631
                        __('The Text Formatter ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
632
                        . ' ' . __("Please remove it from your fields prior to uninstalling or disabling.")
633
                    );
634
                }
635
            }
636
        }
637
    }
638
639
    /**
640
     * Given a delegate name, notify all extensions that have registered to that
641
     * delegate to executing their callbacks with a `$context` array parameter
642
     * that contains information about the current Symphony state.
643
     *
644
     * @param string $delegate
645
     *  The delegate name
646
     * @param string $page
647
     *  The current page namespace that this delegate operates in
648
     * @param array $context
649
     *  The `$context` param is an associative array that at minimum will contain
650
     *  the current Administration class, the current page object and the delegate
651
     *  name. Other context information may be passed to this function when it is
652
     *  called. eg.
653
     *
654
     * array(
655
     *        'parent' =>& $this->Parent,
656
     *        'page' => $page,
657
     *        'delegate' => $delegate
658
     *    );
659
     * @throws Exception
660
     * @throws SymphonyErrorPage
661
     * @return null|void
662
     */
663
    public static function notifyMembers($delegate, $page, array $context = array())
664
    {
665
        // Make sure $page is an array
666
        if (!is_array($page)) {
667
            $page = array($page);
668
        }
669
670
        // Support for global delegate subscription
671
        if (!in_array('*', $page)) {
672
            $page[] = '*';
673
        }
674
675
        $services = array();
676
677
        if (isset(self::$_subscriptions[$delegate])) {
678
            foreach (self::$_subscriptions[$delegate] as $subscription) {
679
                if (!in_array($subscription['page'], $page)) {
680
                    continue;
681
                }
682
683
                $services[] = $subscription;
684
            }
685
        }
686
687
        if (empty($services)) {
688
            return null;
689
        }
690
691
        $context += array('page' => $page, 'delegate' => $delegate);
692
        $profiling = Symphony::Profiler() instanceof Profiler;
693
694
        foreach ($services as $s) {
695
            if ($profiling) {
696
                // Initial seeding and query count
697
                Symphony::Profiler()->seed();
698
                $queries = Symphony::Database()->queryCount();
699
            }
700
701
            // Get instance of extension and execute the callback passing
702
            // the `$context` along
703
            $obj = self::getInstance($s['name']);
704
705
            if (is_object($obj) && method_exists($obj, $s['callback'])) {
706
                $obj->{$s['callback']}($context);
707
            }
708
709
            // Complete the Profiling sample
710
            if ($profiling) {
711
                $queries = Symphony::Database()->queryCount() - $queries;
0 ignored issues
show
Bug introduced by
The variable $queries does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
712
                Symphony::Profiler()->sample($delegate . '|' . $s['name'], PROFILE_LAP, 'Delegate', $queries);
713
            }
714
        }
715
    }
716
717
    /**
718
     * Returns an array of all the enabled extensions available
719
     *
720
     * @return array
721
     */
722
    public static function listInstalledHandles()
723
    {
724
        if (empty(self::$_enabled_extensions) && Symphony::Database()->isConnected()) {
725
            self::$_enabled_extensions = Symphony::Database()->fetchCol(
726
                'name',
727
                "SELECT `name` FROM `tbl_extensions` WHERE `status` = 'enabled'"
728
            );
729
        }
730
        return self::$_enabled_extensions;
731
    }
732
733
    /**
734
     * Will return an associative array of all extensions and their about information
735
     *
736
     * @param string $filter
737
     *  Allows a regular expression to be passed to return only extensions whose
738
     *  folders match the filter.
739
     * @throws SymphonyErrorPage
740
     * @throws Exception
741
     * @return array
742
     *  An associative array with the key being the extension folder and the value
743
     *  being the extension's about information
744
     */
745
    public static function listAll($filter = '/^((?![-^?%:*|"<>]).)*$/')
746
    {
747
        $result = array();
748
        $extensions = General::listDirStructure(EXTENSIONS, $filter, false, EXTENSIONS);
749
750
        if (is_array($extensions) && !empty($extensions)) {
751
            foreach ($extensions as $extension) {
752
                $e = trim($extension, '/');
753
754
                if ($about = self::about($e)) {
755
                    $result[$e] = $about;
756
                }
757
            }
758
        }
759
760
        return $result;
761
    }
762
763
    /**
764
     * Custom user sorting function used inside `fetch` to recursively sort authors
765
     * by their names.
766
     *
767
     * @param array $a
768
     * @param array $b
769
     * @param integer $i
770
     * @return integer
771
     */
772
    private static function sortByAuthor($a, $b, $i = 0)
773
    {
774
        $first = $a;
775
        $second = $b;
776
777
        if (isset($a[$i])) {
778
            $first = $a[$i];
779
        }
780
781
        if (isset($b[$i])) {
782
            $second = $b[$i];
783
        }
784
785
        if ($first == $a && $second == $b && $first['name'] == $second['name']) {
786
            return 1;
787
        } elseif ($first['name'] == $second['name']) {
788
            return self::sortByAuthor($a, $b, $i + 1);
789
        } else {
790
            return ($first['name'] < $second['name']) ? -1 : 1;
791
        }
792
    }
793
794
    /**
795
     * This function will return an associative array of Extension information. The
796
     * information returned is defined by the `$select` parameter, which will allow
797
     * a developer to restrict what information is returned about the Extension.
798
     * Optionally, `$where` (not implemented) and `$order_by` parameters allow a developer to
799
     * further refine their query.
800
     *
801
     * @param array $select (optional)
802
     *  Accepts an array of keys to return from the listAll() method. If omitted, all keys
803
     *  will be returned.
804
     * @param array $where (optional)
805
     *  Not implemented.
806
     * @param string $order_by (optional)
807
     *  Allows a developer to return the extensions in a particular order. The syntax is the
808
     *  same as other `fetch` methods. If omitted this will return resources ordered by `name`.
809
     * @throws Exception
810
     * @throws SymphonyErrorPage
811
     * @return array
812
     *  An associative array of Extension information, formatted in the same way as the
813
     *  listAll() method.
814
     */
815
    public static function fetch(array $select = array(), array $where = array(), $order_by = null)
816
    {
817
        $extensions = self::listAll();
818
        $data = array();
819
820
        if (empty($select) && empty($where) && is_null($order_by)) {
821
            return $extensions;
822
        }
823
824
        if (empty($extensions)) {
825
            return array();
826
        }
827
828
        if (!is_null($order_by)) {
829
            $author = $name = $label = array();
830
            $order_by = array_map('strtolower', explode(' ', $order_by));
831
            $order = ($order_by[1] == 'desc') ? SORT_DESC : SORT_ASC;
832
            $sort = $order_by[0];
833
834
            if ($sort == 'author') {
835
                foreach ($extensions as $key => $about) {
836
                    $author[$key] = $about['author'];
837
                }
838
839
                uasort($author, array('self', 'sortByAuthor'));
840
841
                if ($order == SORT_DESC) {
842
                    $author = array_reverse($author);
843
                }
844
845
                foreach ($author as $key => $value) {
846
                    $data[$key] = $extensions[$key];
847
                }
848
849
                $extensions = $data;
850
            } elseif ($sort == 'name') {
851
                foreach ($extensions as $key => $about) {
852
                    $name[$key] = strtolower($about['name']);
853
                    $label[$key] = $key;
854
                }
855
856
                array_multisort($name, $order, $label, $order, $extensions);
857
            }
858
        }
859
860 View Code Duplication
        foreach ($extensions as $i => $e) {
861
            $data[$i] = array();
862
            foreach ($e as $key => $value) {
863
                // If $select is empty, we assume every field is requested
864
                if (in_array($key, $select) || empty($select)) {
865
                    $data[$i][$key] = $value;
866
                }
867
            }
868
        }
869
870
        return $data;
871
    }
872
873
    /**
874
     * This function will load an extension's meta information given the extension
875
     * `$name`. Since Symphony 2.3, this function will look for an `extension.meta.xml`
876
     * file inside the extension's folder. If this is not found, it will initialise
877
     * the extension and invoke the `about()` function. By default this extension will
878
     * return an associative array display the basic meta data about the given extension.
879
     * If the `$rawXML` parameter is passed true, and the extension has a `extension.meta.xml`
880
     * file, this function will return `DOMDocument` of the file.
881
     *
882
     * @param string $name
883
     *  The name of the Extension Class minus the extension prefix.
884
     * @param boolean $rawXML
885
     *  If passed as true, and is available, this function will return the
886
     *  DOMDocument of representation of the given extension's `extension.meta.xml`
887
     *  file. If the file is not available, the extension will return the normal
888
     *  `about()` results. By default this is false.
889
     * @throws Exception
890
     * @throws SymphonyErrorPage
891
     * @return array
892
     *  An associative array describing this extension
893
     */
894
    public static function about($name, $rawXML = false)
895
    {
896
        // See if the extension has the new meta format
897
        if (file_exists(self::__getClassPath($name) . '/extension.meta.xml')) {
898
            try {
899
                $meta = new DOMDocument;
900
                $meta->load(self::__getClassPath($name) . '/extension.meta.xml');
901
                $xpath = new DOMXPath($meta);
902
                $rootNamespace = $meta->lookupNamespaceUri($meta->namespaceURI);
903
904
                if (is_null($rootNamespace)) {
905
                    throw new Exception(__('Missing default namespace definition.'));
906
                } else {
907
                    $xpath->registerNamespace('ext', $rootNamespace);
908
                }
909
            } catch (Exception $ex) {
910
                Symphony::Engine()->throwCustomError(
911
                    __('The %1$s file for the %2$s extension is not valid XML: %3$s', array(
912
                        '<code>extension.meta.xml</code>',
913
                        '<code>' . $name . '</code>',
914
                        '<br /><code>' . $ex->getMessage() . '</code>'
915
                    ))
916
                );
917
            }
918
919
            // Load <extension>
920
            $extension = $xpath->query('/ext:extension')->item(0);
0 ignored issues
show
Bug introduced by
The variable $xpath does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
921
922
            // Check to see that the extension is named correctly, if it is
923
            // not, then return nothing
924
            if (self::__getClassName($name) !== self::__getClassName($xpath->evaluate('string(@id)', $extension))) {
925
                return array();
926
            }
927
928
            // If `$rawXML` is set, just return our DOMDocument instance
929
            if ($rawXML) {
930
                return $meta;
931
            }
932
933
            $about = array(
934
                'name' => $xpath->evaluate('string(ext:name)', $extension),
935
                'handle' => $name,
936
                'github' => $xpath->evaluate('string(ext:repo)', $extension),
937
                'discuss' => $xpath->evaluate('string(ext:url[@type="discuss"])', $extension),
938
                'homepage' => $xpath->evaluate('string(ext:url[@type="homepage"])', $extension),
939
                'wiki' => $xpath->evaluate('string(ext:url[@type="wiki"])', $extension),
940
                'issues' => $xpath->evaluate('string(ext:url[@type="issues"])', $extension),
941
                'status' => array()
942
            );
943
944
            // find the latest <release> (largest version number)
945
            $latest_release_version = '0.0.0';
946
            foreach ($xpath->query('//ext:release', $extension) as $release) {
947
                $version = $xpath->evaluate('string(@version)', $release);
948
949
                if (version_compare($version, $latest_release_version, '>')) {
950
                    $latest_release_version = $version;
951
                }
952
            }
953
954
            // Load the latest <release> information
955
            if ($release = $xpath->query("//ext:release[@version='$latest_release_version']", $extension)->item(0)) {
956
                $about += array(
957
                    'version' => $xpath->evaluate('string(@version)', $release),
958
                    'release-date' => $xpath->evaluate('string(@date)', $release)
959
                );
960
961
                // If it exists, load in the 'min/max' version data for this release
962
                $required_min_version = $xpath->evaluate('string(@min)', $release);
963
                $required_max_version = $xpath->evaluate('string(@max)', $release);
964
                $current_symphony_version = Symphony::Configuration()->get('version', 'symphony');
965
966
                // Remove pre-release notes from the current Symphony version so that
967
                // we don't get false erros in the backend
968
                $current_symphony_version = preg_replace(array('/dev/i', '/-?beta\.?\d/i', '/-?rc\.?\d/i', '/.0/i'), '', $current_symphony_version);
969
970
                // Munge the version number so that it makes sense in the backend.
971
                // Consider, 2.3.x. As the min version, this means 2.3 onwards,
972
                // for the max it implies any 2.3 release. RE: #1019
973
                // Also remove any .0 when doing the comparison to prevent extensions
974
                // that don't use semver yet. RE: #2146
975
                $required_min_version = preg_replace(array('/\.x/', '/\.0$/'), '', $required_min_version);
976
                $required_max_version = preg_replace(array('/\.x/', '/\.0$/'), 'p', $required_max_version);
977
978
                // Min version
979
                if (!empty($required_min_version) && version_compare($current_symphony_version, $required_min_version, '<')) {
980
                    $about['status'][] = Extension::EXTENSION_NOT_COMPATIBLE;
981
                    $about['required_version'] = $required_min_version;
982
983
                    // Max version
984
                } elseif (!empty($required_max_version) && version_compare($current_symphony_version, $required_max_version, '>')) {
985
                    $about['status'][] = Extension::EXTENSION_NOT_COMPATIBLE;
986
                    $about['required_version'] = str_replace('p', '.x', $required_max_version);
987
                }
988
            }
989
990
            // Add the <author> information
991
            foreach ($xpath->query('//ext:author', $extension) as $author) {
992
                $a = array(
993
                    'name' => $xpath->evaluate('string(ext:name)', $author),
994
                    'website' => $xpath->evaluate('string(ext:website)', $author),
995
                    'github' => $xpath->evaluate('string(ext:name/@github)', $author),
996
                    'email' => $xpath->evaluate('string(ext:email)', $author)
997
                );
998
999
                $about['author'][] = array_filter($a);
1000
            }
1001
1002
            $about['status'] = array_merge($about['status'], self::fetchStatus($about));
1003
            return $about;
1004
        } else {
1005
            Symphony::Log()->pushToLog(sprintf('%s does not have an extension.meta.xml file', $name), E_DEPRECATED, true);
1006
1007
            return array();
1008
        }
1009
    }
1010
1011
    /**
1012
     * Creates an instance of a given class and returns it
1013
     *
1014
     * @param string $name
1015
     *  The name of the Extension Class minus the extension prefix.
1016
     * @throws Exception
1017
     * @throws SymphonyErrorPage
1018
     * @return Extension
1019
     */
1020
    public static function create($name)
1021
    {
1022
        if (!isset(self::$_pool[$name])) {
1023
            $classname = self::__getClassName($name);
1024
            $path = self::__getDriverPath($name);
1025
1026
            if (!is_file($path)) {
1027
                Symphony::Engine()->throwCustomError(
1028
                    __('Could not find extension %s at location %s.', array(
1029
                        '<code>' . $name . '</code>',
1030
                        '<code>' . $path . '</code>'
1031
                    )),
1032
                    __('Symphony Extension Missing Error'),
1033
                    Page::HTTP_STATUS_ERROR,
1034
                    'missing_extension',
1035
                    array(
1036
                        'name' => $name,
1037
                        'path' => $path
1038
                    )
1039
                );
1040
            }
1041
1042
            if (!class_exists($classname)) {
1043
                require_once($path);
1044
            }
1045
1046
            // Create the extension object
1047
            self::$_pool[$name] = new $classname(array());
1048
        }
1049
1050
        return self::$_pool[$name];
1051
    }
1052
1053
    /**
1054
     * A utility function that is used by the ExtensionManager to ensure
1055
     * stray delegates are not in `tbl_extensions_delegates`. It is called when
1056
     * a new Delegate is added or removed.
1057
     */
1058
    public static function cleanupDatabase()
1059
    {
1060
        // Grab any extensions sitting in the database
1061
        $rows = Symphony::Database()->fetch("SELECT `name` FROM `tbl_extensions`");
1062
1063
        // Iterate over each row
1064
        if (is_array($rows) && !empty($rows)) {
1065
            foreach ($rows as $r) {
1066
                $name = $r['name'];
1067
                $status = isset($r['status']) ? $r['status'] : null;
1068
1069
                // Grab the install location
1070
                $path = self::__getClassPath($name);
1071
                $existing_id = self::fetchExtensionID($name);
1072
1073
                // If it doesnt exist, remove the DB rows
1074
                if (!@is_dir($path)) {
1075
                    Symphony::Database()->delete("tbl_extensions_delegates", sprintf(" `extension_id` = %d ", $existing_id));
1076
                    Symphony::Database()->delete('tbl_extensions', sprintf(" `id` = %d LIMIT 1", $existing_id));
1077
                } elseif ($status == 'disabled') {
1078
                    Symphony::Database()->delete("tbl_extensions_delegates", sprintf(" `extension_id` = %d ", $existing_id));
1079
                }
1080
            }
1081
        }
1082
    }
1083
}
1084