Failed Conditions
Pull Request — psr2 (#2725)
by Michael
09:53 queued 06:49
created

helper_plugin_extension_extension::__destruct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
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
use dokuwiki\HTTP\DokuHTTPClient;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, DokuHTTPClient.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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