getDescription()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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