Failed Conditions
Push — psr2 ( 957f36...e79ce3 )
by Andreas
03:40
created

helper_plugin_extension_extension::findFolders()   C

Complexity

Conditions 15
Paths 35

Size

Total Lines 62
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 43
nc 35
nop 4
dl 0
loc 62
rs 6.1517
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * DokuWiki Plugin extension (Helper Component)
4
 *
5
 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6
 * @author  Michael Hamann <[email protected]>
7
 */
8
9
/**
10
 * Class helper_plugin_extension_extension represents a single extension (plugin or template)
11
 */
12
class helper_plugin_extension_extension extends DokuWiki_Plugin
13
{
14
    private $id;
15
    private $base;
16
    private $is_template = false;
17
    private $localInfo;
18
    private $remoteInfo;
19
    private $managerData;
20
    /** @var helper_plugin_extension_repository $repository */
21
    private $repository = null;
22
23
    /** @var array list of temporary directories */
24
    private $temporary = array();
25
26
    /** @var string where templates are installed to */
27
    private $tpllib = '';
28
29
    /**
30
     * helper_plugin_extension_extension constructor.
31
     */
32
    public function __construct()
33
    {
34
        $this->tpllib = dirname(tpl_incdir()).'/';
35
    }
36
37
    /**
38
     * Destructor
39
     *
40
     * deletes any dangling temporary directories
41
     */
42
    public function __destruct()
43
    {
44
        foreach ($this->temporary as $dir) {
45
            io_rmdir($dir, true);
46
        }
47
    }
48
49
    /**
50
     * @return bool false, this component is not a singleton
51
     */
52
    public function isSingleton()
53
    {
54
        return false;
55
    }
56
57
    /**
58
     * Set the name of the extension this instance shall represents, triggers loading the local and remote data
59
     *
60
     * @param string $id  The id of the extension (prefixed with template: for templates)
61
     * @return bool If some (local or remote) data was found
62
     */
63
    public function setExtension($id)
64
    {
65
        $id = cleanID($id);
66
        $this->id   = $id;
67
        $this->base = $id;
68
69
        if (substr($id, 0, 9) == 'template:') {
70
            $this->base = substr($id, 9);
71
            $this->is_template = true;
72
        } else {
73
            $this->is_template = false;
74
        }
75
76
        $this->localInfo = array();
77
        $this->managerData = array();
78
        $this->remoteInfo = array();
79
80
        if ($this->isInstalled()) {
81
            $this->readLocalData();
82
            $this->readManagerData();
83
        }
84
85
        if ($this->repository == null) {
86
            $this->repository = $this->loadHelper('extension_repository');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->loadHelper('extension_repository') can also be of type object<DokuWiki_PluginInterface>. However, the property $repository is declared as type object<helper_plugin_extension_repository>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
87
        }
88
89
        $this->remoteInfo = $this->repository->getData($this->getID());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface DokuWiki_PluginInterface as the method getData() does only exist in the following implementations of said interface: helper_plugin_extension_repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
90
91
        return ($this->localInfo || $this->remoteInfo);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->localInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
92
    }
93
94
    /**
95
     * If the extension is installed locally
96
     *
97
     * @return bool If the extension is installed locally
98
     */
99
    public function isInstalled()
100
    {
101
        return is_dir($this->getInstallDir());
102
    }
103
104
    /**
105
     * If the extension is under git control
106
     *
107
     * @return bool
108
     */
109
    public function isGitControlled()
110
    {
111
        if (!$this->isInstalled()) return false;
112
        return is_dir($this->getInstallDir().'/.git');
113
    }
114
115
    /**
116
     * If the extension is bundled
117
     *
118
     * @return bool If the extension is bundled
119
     */
120
    public function isBundled()
121
    {
122
        if (!empty($this->remoteInfo['bundled'])) return $this->remoteInfo['bundled'];
123
        return in_array(
124
            $this->id,
125
            array(
126
                            'authad', 'authldap', 'authmysql', 'authpdo',
127
                            'authpgsql', 'authplain', 'acl', 'info', 'extension',
128
                            'revert', 'popularity', 'config', 'safefnrecode', 'styling',
129
                            'testing', 'template:dokuwiki'
130
                        )
131
        );
132
    }
133
134
    /**
135
     * If the extension is protected against any modification (disable/uninstall)
136
     *
137
     * @return bool if the extension is protected
138
     */
139
    public function isProtected()
140
    {
141
        // never allow deinstalling the current auth plugin:
142
        global $conf;
143
        if ($this->id == $conf['authtype']) return true;
144
145
        /** @var Doku_Plugin_Controller $plugin_controller */
146
        global $plugin_controller;
147
        $cascade = $plugin_controller->getCascade();
148
        return (isset($cascade['protected'][$this->id]) && $cascade['protected'][$this->id]);
149
    }
150
151
    /**
152
     * If the extension is installed in the correct directory
153
     *
154
     * @return bool If the extension is installed in the correct directory
155
     */
156
    public function isInWrongFolder()
157
    {
158
        return $this->base != $this->getBase();
159
    }
160
161
    /**
162
     * If the extension is enabled
163
     *
164
     * @return bool If the extension is enabled
165
     */
166
    public function isEnabled()
167
    {
168
        global $conf;
169
        if ($this->isTemplate()) {
170
            return ($conf['template'] == $this->getBase());
171
        }
172
173
        /* @var Doku_Plugin_Controller $plugin_controller */
174
        global $plugin_controller;
175
        return !$plugin_controller->isdisabled($this->base);
176
    }
177
178
    /**
179
     * If the extension should be updated, i.e. if an updated version is available
180
     *
181
     * @return bool If an update is available
182
     */
183
    public function updateAvailable()
184
    {
185
        if (!$this->isInstalled()) return false;
186
        if ($this->isBundled()) return false;
187
        $lastupdate = $this->getLastUpdate();
188
        if ($lastupdate === false) return false;
189
        $installed  = $this->getInstalledVersion();
190
        if ($installed === false || $installed === $this->getLang('unknownversion')) return true;
191
        return $this->getInstalledVersion() < $this->getLastUpdate();
192
    }
193
194
    /**
195
     * If the extension is a template
196
     *
197
     * @return bool If this extension is a template
198
     */
199
    public function isTemplate()
200
    {
201
        return $this->is_template;
202
    }
203
204
    /**
205
     * Get the ID of the extension
206
     *
207
     * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:'
208
     *
209
     * @return string
210
     */
211
    public function getID()
212
    {
213
        return $this->id;
214
    }
215
216
    /**
217
     * Get the name of the installation directory
218
     *
219
     * @return string The name of the installation directory
220
     */
221
    public function getInstallName()
222
    {
223
        return $this->base;
224
    }
225
226
    // Data from plugin.info.txt/template.info.txt or the repo when not available locally
227
    /**
228
     * Get the basename of the extension
229
     *
230
     * @return string The basename
231
     */
232
    public function getBase()
233
    {
234
        if (!empty($this->localInfo['base'])) return $this->localInfo['base'];
235
        return $this->base;
236
    }
237
238
    /**
239
     * Get the display name of the extension
240
     *
241
     * @return string The display name
242
     */
243
    public function getDisplayName()
244
    {
245
        if (!empty($this->localInfo['name'])) return $this->localInfo['name'];
246
        if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name'];
247
        return $this->base;
248
    }
249
250
    /**
251
     * Get the author name of the extension
252
     *
253
     * @return string|bool The name of the author or false if there is none
254
     */
255
    public function getAuthor()
256
    {
257
        if (!empty($this->localInfo['author'])) return $this->localInfo['author'];
258
        if (!empty($this->remoteInfo['author'])) return $this->remoteInfo['author'];
259
        return false;
260
    }
261
262
    /**
263
     * Get the email of the author of the extension if there is any
264
     *
265
     * @return string|bool The email address or false if there is none
266
     */
267
    public function getEmail()
268
    {
269
        // email is only in the local data
270
        if (!empty($this->localInfo['email'])) return $this->localInfo['email'];
271
        return false;
272
    }
273
274
    /**
275
     * Get the email id, i.e. the md5sum of the email
276
     *
277
     * @return string|bool The md5sum of the email if there is any, false otherwise
278
     */
279
    public function getEmailID()
280
    {
281
        if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
282
        if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
283
        return false;
284
    }
285
286
    /**
287
     * Get the description of the extension
288
     *
289
     * @return string The description
290
     */
291
    public function getDescription()
292
    {
293
        if (!empty($this->localInfo['desc'])) return $this->localInfo['desc'];
294
        if (!empty($this->remoteInfo['description'])) return $this->remoteInfo['description'];
295
        return '';
296
    }
297
298
    /**
299
     * Get the URL of the extension, usually a page on dokuwiki.org
300
     *
301
     * @return string The URL
302
     */
303
    public function getURL()
304
    {
305
        if (!empty($this->localInfo['url'])) return $this->localInfo['url'];
306
        return 'https://www.dokuwiki.org/'.($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
307
    }
308
309
    /**
310
     * Get the installed version of the extension
311
     *
312
     * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
313
     */
314
    public function getInstalledVersion()
315
    {
316
        if (!empty($this->localInfo['date'])) return $this->localInfo['date'];
317
        if ($this->isInstalled()) return $this->getLang('unknownversion');
318
        return false;
319
    }
320
321
    /**
322
     * Get the install date of the current version
323
     *
324
     * @return string|bool The date of the last update or false if not available
325
     */
326
    public function getUpdateDate()
327
    {
328
        if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
329
        return $this->getInstallDate();
330
    }
331
332
    /**
333
     * Get the date of the installation of the plugin
334
     *
335
     * @return string|bool The date of the installation or false if not available
336
     */
337
    public function getInstallDate()
338
    {
339
        if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
340
        return false;
341
    }
342
343
    /**
344
     * Get the names of the dependencies of this extension
345
     *
346
     * @return array The base names of the dependencies
347
     */
348
    public function getDependencies()
349
    {
350
        if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies'];
351
        return array();
352
    }
353
354
    /**
355
     * Get the names of the missing dependencies
356
     *
357
     * @return array The base names of the missing dependencies
358
     */
359
    public function getMissingDependencies()
360
    {
361
        /* @var Doku_Plugin_Controller $plugin_controller */
362
        global $plugin_controller;
363
        $dependencies = $this->getDependencies();
364
        $missing_dependencies = array();
365
        foreach ($dependencies as $dependency) {
366
            if ($plugin_controller->isdisabled($dependency)) {
367
                $missing_dependencies[] = $dependency;
368
            }
369
        }
370
        return $missing_dependencies;
371
    }
372
373
    /**
374
     * Get the names of all conflicting extensions
375
     *
376
     * @return array The names of the conflicting extensions
377
     */
378
    public function getConflicts()
379
    {
380
        if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['conflicts'];
381
        return array();
382
    }
383
384
    /**
385
     * Get the names of similar extensions
386
     *
387
     * @return array The names of similar extensions
388
     */
389
    public function getSimilarExtensions()
390
    {
391
        if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar'];
392
        return array();
393
    }
394
395
    /**
396
     * Get the names of the tags of the extension
397
     *
398
     * @return array The names of the tags of the extension
399
     */
400
    public function getTags()
401
    {
402
        if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags'];
403
        return array();
404
    }
405
406
    /**
407
     * Get the popularity information as floating point number [0,1]
408
     *
409
     * @return float|bool The popularity information or false if it isn't available
410
     */
411
    public function getPopularity()
412
    {
413
        if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity'];
414
        return false;
415
    }
416
417
418
    /**
419
     * Get the text of the security warning if there is any
420
     *
421
     * @return string|bool The security warning if there is any, false otherwise
422
     */
423
    public function getSecurityWarning()
424
    {
425
        if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning'];
426
        return false;
427
    }
428
429
    /**
430
     * Get the text of the security issue if there is any
431
     *
432
     * @return string|bool The security issue if there is any, false otherwise
433
     */
434
    public function getSecurityIssue()
435
    {
436
        if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue'];
437
        return false;
438
    }
439
440
    /**
441
     * Get the URL of the screenshot of the extension if there is any
442
     *
443
     * @return string|bool The screenshot URL if there is any, false otherwise
444
     */
445
    public function getScreenshotURL()
446
    {
447
        if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl'];
448
        return false;
449
    }
450
451
    /**
452
     * Get the URL of the thumbnail of the extension if there is any
453
     *
454
     * @return string|bool The thumbnail URL if there is any, false otherwise
455
     */
456
    public function getThumbnailURL()
457
    {
458
        if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl'];
459
        return false;
460
    }
461
    /**
462
     * Get the last used download URL of the extension if there is any
463
     *
464
     * @return string|bool The previously used download URL, false if the extension has been installed manually
465
     */
466
    public function getLastDownloadURL()
467
    {
468
        if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
469
        return false;
470
    }
471
472
    /**
473
     * Get the download URL of the extension if there is any
474
     *
475
     * @return string|bool The download URL if there is any, false otherwise
476
     */
477
    public function getDownloadURL()
478
    {
479
        if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl'];
480
        return false;
481
    }
482
483
    /**
484
     * If the download URL has changed since the last download
485
     *
486
     * @return bool If the download URL has changed
487
     */
488
    public function hasDownloadURLChanged()
489
    {
490
        $lasturl = $this->getLastDownloadURL();
491
        $currenturl = $this->getDownloadURL();
492
        return ($lasturl && $currenturl && $lasturl != $currenturl);
493
    }
494
495
    /**
496
     * Get the bug tracker URL of the extension if there is any
497
     *
498
     * @return string|bool The bug tracker URL if there is any, false otherwise
499
     */
500
    public function getBugtrackerURL()
501
    {
502
        if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker'];
503
        return false;
504
    }
505
506
    /**
507
     * Get the URL of the source repository if there is any
508
     *
509
     * @return string|bool The URL of the source repository if there is any, false otherwise
510
     */
511
    public function getSourcerepoURL()
512
    {
513
        if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo'];
514
        return false;
515
    }
516
517
    /**
518
     * Get the donation URL of the extension if there is any
519
     *
520
     * @return string|bool The donation URL if there is any, false otherwise
521
     */
522
    public function getDonationURL()
523
    {
524
        if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl'];
525
        return false;
526
    }
527
528
    /**
529
     * Get the extension type(s)
530
     *
531
     * @return array The type(s) as array of strings
532
     */
533
    public function getTypes()
534
    {
535
        if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types'];
536
        if ($this->isTemplate()) return array(32 => 'template');
537
        return array();
538
    }
539
540
    /**
541
     * Get a list of all DokuWiki versions this extension is compatible with
542
     *
543
     * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
544
     */
545
    public function getCompatibleVersions()
546
    {
547
        if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible'];
548
        return array();
549
    }
550
551
    /**
552
     * Get the date of the last available update
553
     *
554
     * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
555
     */
556
    public function getLastUpdate()
557
    {
558
        if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate'];
559
        return false;
560
    }
561
562
    /**
563
     * Get the base path of the extension
564
     *
565
     * @return string The base path of the extension
566
     */
567
    public function getInstallDir()
568
    {
569
        if ($this->isTemplate()) {
570
            return $this->tpllib.$this->base;
571
        } else {
572
            return DOKU_PLUGIN.$this->base;
573
        }
574
    }
575
576
    /**
577
     * The type of extension installation
578
     *
579
     * @return string One of "none", "manual", "git" or "automatic"
580
     */
581
    public function getInstallType()
582
    {
583
        if (!$this->isInstalled()) return 'none';
584
        if (!empty($this->managerData)) return 'automatic';
585
        if (is_dir($this->getInstallDir().'/.git')) return 'git';
586
        return 'manual';
587
    }
588
589
    /**
590
     * If the extension can probably be installed/updated or uninstalled
591
     *
592
     * @return bool|string True or error string
593
     */
594
    public function canModify()
595
    {
596
        if ($this->isInstalled()) {
597
            if (!is_writable($this->getInstallDir())) {
598
                return 'noperms';
599
            }
600
        }
601
602
        if ($this->isTemplate() && !is_writable($this->tpllib)) {
603
            return 'notplperms';
604
        } elseif (!is_writable(DOKU_PLUGIN)) {
605
            return 'nopluginperms';
606
        }
607
        return true;
608
    }
609
610
    /**
611
     * Install an extension from a user upload
612
     *
613
     * @param string $field name of the upload file
614
     * @throws Exception when something goes wrong
615
     * @return array The list of installed extensions
616
     */
617
    public function installFromUpload($field)
618
    {
619
        if ($_FILES[$field]['error']) {
620
            throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
621
        }
622
623
        $tmp = $this->mkTmpDir();
624
        if (!$tmp) throw new Exception($this->getLang('error_dircreate'));
0 ignored issues
show
Bug Best Practice introduced by
The expression $tmp of type false|string is loosely compared to false; 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...
625
626
        // filename may contain the plugin name for old style plugins...
627
        $basename = basename($_FILES[$field]['name']);
628
        $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
629
        $basename = preg_replace('/[\W]+/', '', $basename);
630
631
        if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
632
            throw new Exception($this->getLang('msg_upload_failed'));
633
        }
634
635
        try {
636
            $installed = $this->installArchive("$tmp/upload.archive", true, $basename);
637
            $this->updateManagerData('', $installed);
638
            $this->removeDeletedfiles($installed);
639
            // purge cache
640
            $this->purgeCache();
641
        } catch (Exception $e) {
642
            throw $e;
643
        }
644
        return $installed;
645
    }
646
647
    /**
648
     * Install an extension from a remote URL
649
     *
650
     * @param string $url
651
     * @throws Exception when something goes wrong
652
     * @return array The list of installed extensions
653
     */
654
    public function installFromURL($url)
655
    {
656
        try {
657
            $path      = $this->download($url);
658
            $installed = $this->installArchive($path, true);
659
            $this->updateManagerData($url, $installed);
660
            $this->removeDeletedfiles($installed);
661
662
            // purge cache
663
            $this->purgeCache();
664
        } catch (Exception $e) {
665
            throw $e;
666
        }
667
        return $installed;
668
    }
669
670
    /**
671
     * Install or update the extension
672
     *
673
     * @throws \Exception when something goes wrong
674
     * @return array The list of installed extensions
675
     */
676
    public function installOrUpdate()
677
    {
678
        $url       = $this->getDownloadURL();
679
        $path      = $this->download($url);
680
        $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
681
        $this->updateManagerData($url, $installed);
682
683
        // refresh extension information
684
        if (!isset($installed[$this->getID()])) {
685
            throw new Exception('Error, the requested extension hasn\'t been installed or updated');
686
        }
687
        $this->removeDeletedfiles($installed);
688
        $this->setExtension($this->getID());
689
        $this->purgeCache();
690
        return $installed;
691
    }
692
693
    /**
694
     * Uninstall the extension
695
     *
696
     * @return bool If the plugin was sucessfully uninstalled
697
     */
698
    public function uninstall()
699
    {
700
        $this->purgeCache();
701
        return io_rmdir($this->getInstallDir(), true);
702
    }
703
704
    /**
705
     * Enable the extension
706
     *
707
     * @return bool|string True or an error message
708
     */
709
    public function enable()
710
    {
711
        if ($this->isTemplate()) return $this->getLang('notimplemented');
712
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
713
        if ($this->isEnabled()) return $this->getLang('alreadyenabled');
714
715
        /* @var Doku_Plugin_Controller $plugin_controller */
716
        global $plugin_controller;
717
        if ($plugin_controller->enable($this->base)) {
718
            $this->purgeCache();
719
            return true;
720
        } else {
721
            return $this->getLang('pluginlistsaveerror');
722
        }
723
    }
724
725
    /**
726
     * Disable the extension
727
     *
728
     * @return bool|string True or an error message
729
     */
730
    public function disable()
731
    {
732
        if ($this->isTemplate()) return $this->getLang('notimplemented');
733
734
        /* @var Doku_Plugin_Controller $plugin_controller */
735
        global $plugin_controller;
736
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
737
        if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
738
        if ($plugin_controller->disable($this->base)) {
739
            $this->purgeCache();
740
            return true;
741
        } else {
742
            return $this->getLang('pluginlistsaveerror');
743
        }
744
    }
745
746
    /**
747
     * Purge the cache by touching the main configuration file
748
     */
749
    protected function purgeCache()
750
    {
751
        global $config_cascade;
752
753
        // expire dokuwiki caches
754
        // touching local.php expires wiki page, JS and CSS caches
755
        @touch(reset($config_cascade['main']['local']));
1 ignored issue
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...
756
    }
757
758
    /**
759
     * Read local extension data either from info.txt or getInfo()
760
     */
761
    protected function readLocalData()
762
    {
763
        if ($this->isTemplate()) {
764
            $infopath = $this->getInstallDir().'/template.info.txt';
765
        } else {
766
            $infopath = $this->getInstallDir().'/plugin.info.txt';
767
        }
768
769
        if (is_readable($infopath)) {
770
            $this->localInfo = confToHash($infopath);
771
        } elseif (!$this->isTemplate() && $this->isEnabled()) {
772
            global $plugin_types;
773
            $path       = $this->getInstallDir().'/';
774
            $plugin     = null;
775
776
            foreach ($plugin_types as $type) {
777
                if (file_exists($path.$type.'.php')) {
778
                    $plugin = plugin_load($type, $this->base);
779
                    if ($plugin) break;
780
                }
781
782
                if ($dh = @opendir($path.$type.'/')) {
783
                    while (false !== ($cp = readdir($dh))) {
784
                        if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
785
786
                        $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
787
                        if ($plugin) break;
788
                    }
789
                    if ($plugin) break;
790
                    closedir($dh);
791
                }
792
            }
793
794
            if ($plugin) {
795
                /* @var DokuWiki_Plugin $plugin */
796
                $this->localInfo = $plugin->getInfo();
797
            }
798
        }
799
    }
800
801
    /**
802
     * Save the given URL and current datetime in the manager.dat file of all installed extensions
803
     *
804
     * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
805
     * @param array  $installed Optional list of installed plugins
806
     */
807
    protected function updateManagerData($url = '', $installed = null)
808
    {
809
        $origID = $this->getID();
810
811
        if (is_null($installed)) {
812
            $installed = array($origID);
813
        }
814
815
        foreach ($installed as $ext => $info) {
816
            if ($this->getID() != $ext) $this->setExtension($ext);
817
            if ($url) {
818
                $this->managerData['downloadurl'] = $url;
819
            } elseif (isset($this->managerData['downloadurl'])) {
820
                unset($this->managerData['downloadurl']);
821
            }
822
            if (isset($this->managerData['installed'])) {
823
                $this->managerData['updated'] = date('r');
824
            } else {
825
                $this->managerData['installed'] = date('r');
826
            }
827
            $this->writeManagerData();
828
        }
829
830
        if ($this->getID() != $origID) $this->setExtension($origID);
831
    }
832
833
    /**
834
     * Read the manager.dat file
835
     */
836
    protected function readManagerData()
837
    {
838
        $managerpath = $this->getInstallDir().'/manager.dat';
839
        if (is_readable($managerpath)) {
840
            $file = @file($managerpath);
841
            if (!empty($file)) {
842
                foreach ($file as $line) {
843
                    list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
844
                    $key = trim($key);
845
                    $value = trim($value);
846
                    // backwards compatible with old plugin manager
847
                    if ($key == 'url') $key = 'downloadurl';
848
                    $this->managerData[$key] = $value;
849
                }
850
            }
851
        }
852
    }
853
854
    /**
855
     * Write the manager.data file
856
     */
857
    protected function writeManagerData()
858
    {
859
        $managerpath = $this->getInstallDir().'/manager.dat';
860
        $data = '';
861
        foreach ($this->managerData as $k => $v) {
862
            $data .= $k.'='.$v.DOKU_LF;
863
        }
864
        io_saveFile($managerpath, $data);
865
    }
866
867
    /**
868
     * Returns a temporary directory
869
     *
870
     * The directory is registered for cleanup when the class is destroyed
871
     *
872
     * @return false|string
873
     */
874
    protected function mkTmpDir()
875
    {
876
        $dir = io_mktmpdir();
877
        if (!$dir) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $dir of type string|false is loosely compared to false; 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...
878
        $this->temporary[] = $dir;
879
        return $dir;
880
    }
881
882
    /**
883
     * Download an archive to a protected path
884
     *
885
     * @param string $url  The url to get the archive from
886
     * @throws Exception   when something goes wrong
887
     * @return string The path where the archive was saved
888
     */
889
    public function download($url)
890
    {
891
        // check the url
892
        if (!preg_match('/https?:\/\//i', $url)) {
893
            throw new Exception($this->getLang('error_badurl'));
894
        }
895
896
        // try to get the file from the path (used as plugin name fallback)
897
        $file = parse_url($url, PHP_URL_PATH);
898
        if (is_null($file)) {
899
            $file = md5($url);
900
        } else {
901
            $file = utf8_basename($file);
0 ignored issues
show
Security Bug introduced by
It seems like $file can also be of type false; however, utf8_basename() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
902
        }
903
904
        // create tmp directory for download
905
        if (!($tmp = $this->mkTmpDir())) {
906
            throw new Exception($this->getLang('error_dircreate'));
907
        }
908
909
        // download
910
        if (!$file = io_download($url, $tmp.'/', true, $file, 0)) {
911
            io_rmdir($tmp, true);
912
            throw new Exception(sprintf($this->getLang('error_download'), '<bdi>'.hsc($url).'</bdi>'));
913
        }
914
915
        return $tmp.'/'.$file;
916
    }
917
918
    /**
919
     * @param string $file      The path to the archive that shall be installed
920
     * @param bool   $overwrite If an already installed plugin should be overwritten
921
     * @param string $base      The basename of the plugin if it's known
922
     * @throws Exception        when something went wrong
923
     * @return array            list of installed extensions
924
     */
925
    public function installArchive($file, $overwrite = false, $base = '')
926
    {
927
        $installed_extensions = array();
928
929
        // create tmp directory for decompression
930
        if (!($tmp = $this->mkTmpDir())) {
931
            throw new Exception($this->getLang('error_dircreate'));
932
        }
933
934
        // add default base folder if specified to handle case where zip doesn't contain this
935
        if ($base && !@mkdir($tmp.'/'.$base)) {
936
            throw new Exception($this->getLang('error_dircreate'));
937
        }
938
939
        // decompress
940
        $this->decompress($file, "$tmp/".$base);
941
942
        // search $tmp/$base for the folder(s) that has been created
943
        // move the folder(s) to lib/..
944
        $result = array('old'=>array(), 'new'=>array());
945
        $default = ($this->isTemplate() ? 'template' : 'plugin');
946
        if (!$this->findFolders($result, $tmp.'/'.$base, $default)) {
947
            throw new Exception($this->getLang('error_findfolder'));
948
        }
949
950
        // choose correct result array
951
        if (count($result['new'])) {
952
            $install = $result['new'];
953
        } else {
954
            $install = $result['old'];
955
        }
956
957
        if (!count($install)) {
958
            throw new Exception($this->getLang('error_findfolder'));
959
        }
960
961
        // now install all found items
962
        foreach ($install as $item) {
963
            // where to install?
964
            if ($item['type'] == 'template') {
965
                $target_base_dir = $this->tpllib;
966
            } else {
967
                $target_base_dir = DOKU_PLUGIN;
968
            }
969
970
            if (!empty($item['base'])) {
971
                // use base set in info.txt
972
            } elseif ($base && count($install) == 1) {
973
                $item['base'] = $base;
974
            } else {
975
                // default - use directory as found in zip
976
                // plugins from github/master without *.info.txt will install in wrong folder
977
                // but using $info->id will make 'code3' fail (which should install in lib/code/..)
978
                $item['base'] = basename($item['tmp']);
979
            }
980
981
            // check to make sure we aren't overwriting anything
982
            $target = $target_base_dir.$item['base'];
983
            if (!$overwrite && file_exists($target)) {
984
                // TODO remember our settings, ask the user to confirm overwrite
985
                continue;
986
            }
987
988
            $action = file_exists($target) ? 'update' : 'install';
989
990
            // copy action
991
            if ($this->dircopy($item['tmp'], $target)) {
992
                // return info
993
                $id = $item['base'];
994
                if ($item['type'] == 'template') {
995
                    $id = 'template:'.$id;
996
                }
997
                $installed_extensions[$id] = array(
998
                    'base' => $item['base'],
999
                    'type' => $item['type'],
1000
                    'action' => $action
1001
                );
1002
            } else {
1003
                throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, '<bdi>'.$item['base'].'</bdi>'));
1004
            }
1005
        }
1006
1007
        // cleanup
1008
        if ($tmp) io_rmdir($tmp, true);
1009
1010
        return $installed_extensions;
1011
    }
1012
1013
    /**
1014
     * Find out what was in the extracted directory
1015
     *
1016
     * Correct folders are searched recursively using the "*.info.txt" configs
1017
     * as indicator for a root folder. When such a file is found, it's base
1018
     * setting is used (when set). All folders found by this method are stored
1019
     * in the 'new' key of the $result array.
1020
     *
1021
     * For backwards compatibility all found top level folders are stored as
1022
     * in the 'old' key of the $result array.
1023
     *
1024
     * When no items are found in 'new' the copy mechanism should fall back
1025
     * the 'old' list.
1026
     *
1027
     * @author Andreas Gohr <[email protected]>
1028
     * @param array $result - results are stored here
1029
     * @param string $directory - the temp directory where the package was unpacked to
1030
     * @param string $default_type - type used if no info.txt available
1031
     * @param string $subdir - a subdirectory. do not set. used by recursion
1032
     * @return bool - false on error
1033
     */
1034
    protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '')
1035
    {
1036
        $this_dir = "$directory$subdir";
1037
        $dh       = @opendir($this_dir);
1038
        if (!$dh) return false;
1039
1040
        $found_dirs           = array();
1041
        $found_files          = 0;
1042
        $found_template_parts = 0;
1043
        while (false !== ($f = readdir($dh))) {
1044
            if ($f == '.' || $f == '..') continue;
1045
1046
            if (is_dir("$this_dir/$f")) {
1047
                $found_dirs[] = "$subdir/$f";
1048
            } else {
1049
                // it's a file -> check for config
1050
                $found_files++;
1051
                switch ($f) {
1052
                    case 'plugin.info.txt':
1053
                    case 'template.info.txt':
1054
                        // we have  found a clear marker, save and return
1055
                        $info = array();
1056
                        $type = explode('.', $f, 2);
1057
                        $info['type'] = $type[0];
1058
                        $info['tmp']  = $this_dir;
1059
                        $conf = confToHash("$this_dir/$f");
1060
                        $info['base'] = basename($conf['base']);
1061
                        $result['new'][] = $info;
1062
                        return true;
1063
1064
                    case 'main.php':
1065
                    case 'details.php':
1066
                    case 'mediamanager.php':
1067
                    case 'style.ini':
1068
                        $found_template_parts++;
1069
                        break;
1070
                }
1071
            }
1072
        }
1073
        closedir($dh);
1074
1075
        // files where found but no info.txt - use old method
1076
        if ($found_files) {
1077
            $info            = array();
1078
            $info['tmp']     = $this_dir;
1079
            // does this look like a template or should we use the default type?
1080
            if ($found_template_parts >= 2) {
1081
                $info['type']    = 'template';
1082
            } else {
1083
                $info['type']    = $default_type;
1084
            }
1085
1086
            $result['old'][] = $info;
1087
            return true;
1088
        }
1089
1090
        // we have no files yet -> recurse
1091
        foreach ($found_dirs as $found_dir) {
1092
            $this->findFolders($result, $directory, $default_type, "$found_dir");
1093
        }
1094
        return true;
1095
    }
1096
1097
    /**
1098
     * Decompress a given file to the given target directory
1099
     *
1100
     * Determines the compression type from the file extension
1101
     *
1102
     * @param string $file   archive to extract
1103
     * @param string $target directory to extract to
1104
     * @throws Exception
1105
     * @return bool
1106
     */
1107
    private function decompress($file, $target)
1108
    {
1109
        // decompression library doesn't like target folders ending in "/"
1110
        if (substr($target, -1) == "/") $target = substr($target, 0, -1);
1111
1112
        $ext = $this->guessArchiveType($file);
1113
        if (in_array($ext, array('tar', 'bz', 'gz'))) {
1114
            try {
1115
                $tar = new \splitbrain\PHPArchive\Tar();
1116
                $tar->open($file);
1117
                $tar->extract($target);
1118
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1119
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1120
            }
1121
1122
            return true;
1123
        } elseif ($ext == 'zip') {
1124
            try {
1125
                $zip = new \splitbrain\PHPArchive\Zip();
1126
                $zip->open($file);
1127
                $zip->extract($target);
1128
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1129
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1130
            }
1131
1132
            return true;
1133
        }
1134
1135
        // the only case when we don't get one of the recognized archive types is when the archive file can't be read
1136
        throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
1137
    }
1138
1139
    /**
1140
     * Determine the archive type of the given file
1141
     *
1142
     * Reads the first magic bytes of the given file for content type guessing,
1143
     * if neither bz, gz or zip are recognized, tar is assumed.
1144
     *
1145
     * @author Andreas Gohr <[email protected]>
1146
     * @param string $file The file to analyze
1147
     * @return string|false false if the file can't be read, otherwise an "extension"
1148
     */
1149
    private function guessArchiveType($file)
1150
    {
1151
        $fh = fopen($file, 'rb');
1152
        if (!$fh) return false;
1153
        $magic = fread($fh, 5);
1154
        fclose($fh);
1155
1156
        if (strpos($magic, "\x42\x5a") === 0) return 'bz';
1157
        if (strpos($magic, "\x1f\x8b") === 0) return 'gz';
1158
        if (strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
1159
        return 'tar';
1160
    }
1161
1162
    /**
1163
     * Copy with recursive sub-directory support
1164
     *
1165
     * @param string $src filename path to file
1166
     * @param string $dst filename path to file
1167
     * @return bool|int|string
1168
     */
1169
    private function dircopy($src, $dst)
1170
    {
1171
        global $conf;
1172
1173
        if (is_dir($src)) {
1174
            if (!$dh = @opendir($src)) return false;
1175
1176
            if ($ok = io_mkdir_p($dst)) {
1177
                while ($ok && (false !== ($f = readdir($dh)))) {
1178
                    if ($f == '..' || $f == '.') continue;
1179
                    $ok = $this->dircopy("$src/$f", "$dst/$f");
1180
                }
1181
            }
1182
1183
            closedir($dh);
1184
            return $ok;
1185
        } else {
1186
            $exists = file_exists($dst);
1187
1188
            if (!@copy($src, $dst)) return false;
1189
            if (!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']);
1190
            @touch($dst, filemtime($src));
1 ignored issue
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...
1191
        }
1192
1193
        return true;
1194
    }
1195
1196
    /**
1197
     * Delete outdated files from updated plugins
1198
     *
1199
     * @param array $installed
1200
     */
1201
    private function removeDeletedfiles($installed)
1202
    {
1203
        foreach ($installed as $id => $extension) {
1204
            // only on update
1205
            if ($extension['action'] == 'install') continue;
1206
1207
            // get definition file
1208
            if ($extension['type'] == 'template') {
1209
                $extensiondir = $this->tpllib;
1210
            } else {
1211
                $extensiondir = DOKU_PLUGIN;
1212
            }
1213
            $extensiondir = $extensiondir . $extension['base'] .'/';
1214
            $definitionfile = $extensiondir . 'deleted.files';
1215
            if (!file_exists($definitionfile)) continue;
1216
1217
            // delete the old files
1218
            $list = file($definitionfile);
1219
1220
            foreach ($list as $line) {
1221
                $line = trim(preg_replace('/#.*$/', '', $line));
1222
                if (!$line) continue;
1223
                $file = $extensiondir . $line;
1224
                if (!file_exists($file)) continue;
1225
1226
                io_rmdir($file, true);
1227
            }
1228
        }
1229
    }
1230
}
1231
1232
// vim:ts=4:sw=4:et:
1233