Completed
Push — extension-infosource ( 827712 )
by Henry
04:14
created

isIgnoredSource()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 7
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
    /** $var string $forceSource */
23
    private $forceSource = null;
24
    private $managerData;
25
    /** @var helper_plugin_extension_repository $repository */
26
    private $repository = null;
27
28
    /** @var array list of temporary directories */
29
    private $temporary = array();
30
31
    /** @var string where templates are installed to */
32
    private $tpllib = '';
33
34
    /**
35
     * helper_plugin_extension_extension constructor.
36
     */
37
    public function __construct()
38
    {
39
        $this->tpllib = dirname(tpl_incdir()).'/';
40
    }
41
42
    /**
43
     * Destructor
44
     *
45
     * deletes any dangling temporary directories
46
     */
47
    public function __destruct()
48
    {
49
        foreach ($this->temporary as $dir) {
50
            io_rmdir($dir, true);
51
        }
52
    }
53
54
    /**
55
     * @return bool false, this component is not a singleton
56
     */
57
    public function isSingleton()
58
    {
59
        return false;
60
    }
61
62
    /**
63
     * Set the name of the extension this instance shall represents, triggers loading the local and remote data
64
     *
65
     * @param string $id  The id of the extension (prefixed with template: for templates)
66
     * @return bool If some (local or remote) data was found
67
     */
68
    public function setExtension($id)
69
    {
70
        $id = cleanID($id);
71
        $this->id   = $id;
72
        $this->base = $id;
73
74
        if (substr($id, 0, 9) == 'template:') {
75
            $this->base = substr($id, 9);
76
            $this->is_template = true;
77
        } else {
78
            $this->is_template = false;
79
        }
80
81
        $this->localInfo = array();
82
        $this->managerData = array();
83
        $this->remoteInfo = array();
84
85
        if ($this->isInstalled()) {
86
            $this->readLocalData();
87
            $this->readManagerData();
88
        }
89
90
        if ($this->repository == null) {
91
            $this->repository = $this->loadHelper('extension_repository');
92
        }
93
94
        $this->remoteInfo = $this->repository->getData($this->getID());
95
96
        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...
97
    }
98
99
    /**
100
     * If the extension is installed locally
101
     *
102
     * @return bool If the extension is installed locally
103
     */
104
    public function isInstalled()
105
    {
106
        return is_dir($this->getInstallDir());
107
    }
108
109
    /**
110
     * If the extension is under git control
111
     *
112
     * @return bool
113
     */
114
    public function isGitControlled()
115
    {
116
        if (!$this->isInstalled()) return false;
117
        return is_dir($this->getInstallDir().'/.git');
118
    }
119
120
    /**
121
     * If the extension is bundled
122
     *
123
     * @return bool If the extension is bundled
124
     */
125
    public function isBundled()
126
    {
127
        if (!empty($this->remoteInfo['bundled'])) return $this->remoteInfo['bundled'];
128
        return in_array(
129
            $this->id,
130
            array(
131
                'authad', 'authldap', 'authpdo', 'authplain',
132
                'acl', 'config', 'extension', 'info', 'popularity', 'revert',
133
                'safefnrecode', 'styling', 'testing', 'usermanager',
134
                'template:dokuwiki',
135
            )
136
        );
137
    }
138
139
    /**
140
     * If the extension is protected against any modification (disable/uninstall)
141
     *
142
     * @return bool if the extension is protected
143
     */
144
    public function isProtected()
145
    {
146
        // never allow deinstalling the current auth plugin:
147
        global $conf;
148
        if ($this->id == $conf['authtype']) return true;
149
150
        /** @var PluginController $plugin_controller */
151
        global $plugin_controller;
152
        $cascade = $plugin_controller->getCascade();
153
        return (isset($cascade['protected'][$this->id]) && $cascade['protected'][$this->id]);
154
    }
155
156
    /**
157
     * If the extension is installed in the correct directory
158
     *
159
     * @return bool If the extension is installed in the correct directory
160
     */
161
    public function isInWrongFolder()
162
    {
163
        return $this->base != $this->getBase();
164
    }
165
166
    /**
167
     * If the extension is enabled
168
     *
169
     * @return bool If the extension is enabled
170
     */
171
    public function isEnabled()
172
    {
173
        global $conf;
174
        if ($this->isTemplate()) {
175
            return ($conf['template'] == $this->getBase());
176
        }
177
178
        /* @var PluginController $plugin_controller */
179
        global $plugin_controller;
180
        return !$plugin_controller->isdisabled($this->base);
0 ignored issues
show
Deprecated Code introduced by
The method dokuwiki\Extension\PluginController::isDisabled() has been deprecated with message: in favor of the more sensible isEnabled where the return value matches the enabled state

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
181
    }
182
183
    /**
184
     * If the extension should be updated, i.e. if an updated version is available
185
     *
186
     * @return bool If an update is available
187
     */
188
    public function updateAvailable()
189
    {
190
        if (!$this->isInstalled()) return false;
191
        if ($this->isBundled()) return false;
192
        $lastupdate = $this->getLastUpdate();
193
        if ($lastupdate === false) return false;
194
        $installed  = $this->getInstalledVersion();
195
        if ($installed === false || $installed === $this->getLang('unknownversion')) return true;
196
        return $this->getInstalledVersion() < $this->getLastUpdate();
197
    }
198
199
    /**
200
     * If the extension is a template
201
     *
202
     * @return bool If this extension is a template
203
     */
204
    public function isTemplate()
205
    {
206
        return $this->is_template;
207
    }
208
209
    /**
210
     * Set source (remote/local/null) that getters should be forced on
211
     *
212
     * @var string $source
213
     * @return void
214
     */
215
    public function setForceSource($source)
216
    {
217
        $this->forceSource = $source;
218
    }
219
220
    /**
221
     * If the source should be ignored for getters
222
     *
223
     * @var string $source
224
     * @return bool If this extension is a template
225
     */
226
    public function isIgnoredSource($source)
227
    {
228
        if ($this->forceSource && $this->forceSource !== $source) {
229
            return true;
230
        }
231
        return false;
232
    }
233
234
    /**
235
     * Get the ID of the extension
236
     *
237
     * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:'
238
     *
239
     * @return string
240
     */
241
    public function getID()
242
    {
243
        return $this->id;
244
    }
245
246
    /**
247
     * Get the name of the installation directory
248
     *
249
     * @return string The name of the installation directory
250
     */
251
    public function getInstallName()
252
    {
253
        return $this->base;
254
    }
255
256
    // Data from plugin.info.txt/template.info.txt or the repo when not available locally
257
    /**
258
     * Get the basename of the extension
259
     *
260
     * @return string The basename
261
     */
262
    public function getBase()
263
    {
264
        if (!empty($this->localInfo['base']) && !$this->isIgnoredSource('local'))
265
            return $this->localInfo['base'];
266
        return $this->base;
267
    }
268
269
    /**
270
     * Get the display name of the extension
271
     *
272
     * @return string The display name
273
     */
274
    public function getDisplayName()
275
    {
276
        if (!empty($this->localInfo['name']) && !$this->isIgnoredSource('local'))
277
            return $this->localInfo['name'];
278
        if (!empty($this->remoteInfo['name']) && !$this->isIgnoredSource('remote'))
279
            return $this->remoteInfo['name'];
280
        return $this->base;
281
    }
282
283
    /**
284
     * Get the author name of the extension
285
     *
286
     * @return string|bool The name of the author or false if there is none
287
     */
288
    public function getAuthor()
289
    {
290
        if (!empty($this->localInfo['author']) && !$this->isIgnoredSource('local'))
291
            return $this->localInfo['author'];
292
        if (!empty($this->remoteInfo['author']) && !$this->isIgnoredSource('remote'))
293
            return $this->remoteInfo['author'];
294
        return false;
295
    }
296
297
    /**
298
     * Get the email of the author of the extension if there is any
299
     *
300
     * @return string|bool The email address or false if there is none
301
     */
302
    public function getEmail()
303
    {
304
        // email is only in the local data
305
        if (!empty($this->localInfo['email']) && !$this->isIgnoredSource('local'))
306
            return $this->localInfo['email'];
307
        return false;
308
    }
309
310
    /**
311
     * Get the email id, i.e. the md5sum of the email
312
     *
313
     * @return string|bool The md5sum of the email if there is any, false otherwise
314
     */
315
    public function getEmailID()
316
    {
317
        if (!empty($this->remoteInfo['emailid']) && !$this->isIgnoredSource('remote'))
318
            return $this->remoteInfo['emailid'];
319
        if (!empty($this->localInfo['email']) && !$this->isIgnoredSource('local'))
320
            return md5($this->localInfo['email']);
321
        return false;
322
    }
323
324
    /**
325
     * Get the description of the extension
326
     *
327
     * @return string The description
328
     */
329
    public function getDescription()
330
    {
331
        if (!empty($this->localInfo['desc']) && !$this->isIgnoredSource('local'))
332
            return $this->localInfo['desc'];
333
        if (!empty($this->remoteInfo['description']) && !$this->isIgnoredSource('remote'))
334
            return $this->remoteInfo['description'];
335
        return '';
336
    }
337
338
    /**
339
     * Get the URL of the extension, usually a page on dokuwiki.org
340
     *
341
     * @return string The URL
342
     */
343
    public function getURL()
344
    {
345
        if (!empty($this->localInfo['url']) && !$this->isIgnoredSource('local'))
346
            return $this->localInfo['url'];
347
        return 'https://www.dokuwiki.org/'.
348
            ($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
349
    }
350
351
    /**
352
     * Get the installed version of the extension
353
     *
354
     * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
355
     */
356
    public function getInstalledVersion()
357
    {
358
        if (!empty($this->localInfo['date']))
359
            return $this->localInfo['date'];
360
        if ($this->isInstalled()) return $this->getLang('unknownversion');
361
        return false;
362
    }
363
364
    /*
365
     * Get the install date of the current version
366
     *
367
     * @return string|bool The date of the last update or false if not available
368
     */
369
    public function getUpdateDate()
370
    {
371
        if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
372
        return $this->getInstallDate();
373
    }
374
375
    /**
376
     * Get the date of the installation of the plugin
377
     *
378
     * @return string|bool The date of the installation or false if not available
379
     */
380
    public function getInstallDate()
381
    {
382
        if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
383
        return false;
384
    }
385
386
    /**
387
     * Get the names of the dependencies of this extension
388
     *
389
     * @return array The base names of the dependencies
390
     */
391
    public function getDependencies()
392
    {
393
        if (!empty($this->remoteInfo['dependencies']) && !$this->isIgnoredSource('remote'))
394
            return $this->remoteInfo['dependencies'];
395
        return array();
396
    }
397
398
    /**
399
     * Get the names of the missing dependencies
400
     *
401
     * @return array The base names of the missing dependencies
402
     */
403
    public function getMissingDependencies()
404
    {
405
        /* @var PluginController $plugin_controller */
406
        global $plugin_controller;
407
        $dependencies = $this->getDependencies();
408
        $missing_dependencies = array();
409
        foreach ($dependencies as $dependency) {
410
            if ($plugin_controller->isdisabled($dependency)) {
0 ignored issues
show
Deprecated Code introduced by
The method dokuwiki\Extension\PluginController::isDisabled() has been deprecated with message: in favor of the more sensible isEnabled where the return value matches the enabled state

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
411
                $missing_dependencies[] = $dependency;
412
            }
413
        }
414
        return $missing_dependencies;
415
    }
416
417
    /**
418
     * Get the names of all conflicting extensions
419
     *
420
     * @return array The names of the conflicting extensions
421
     */
422
    public function getConflicts()
423
    {
424
        if (!empty($this->remoteInfo['conflicts']) && !$this->isIgnoredSource('remote'))
425
            return $this->remoteInfo['conflicts'];
426
        return array();
427
    }
428
429
    /**
430
     * Get the names of similar extensions
431
     *
432
     * @return array The names of similar extensions
433
     */
434
    public function getSimilarExtensions()
435
    {
436
        if (!empty($this->remoteInfo['similar']) && !$this->isIgnoredSource('remote'))
437
            return $this->remoteInfo['similar'];
438
        return array();
439
    }
440
441
    /**
442
     * Get the names of the tags of the extension
443
     *
444
     * @return array The names of the tags of the extension
445
     */
446
    public function getTags()
447
    {
448
        if (!empty($this->remoteInfo['tags']) && !$this->isIgnoredSource('remote'))
449
            return $this->remoteInfo['tags'];
450
        return array();
451
    }
452
453
    /**
454
     * Get the popularity information as floating point number [0,1]
455
     *
456
     * @return float|bool The popularity information or false if it isn't available
457
     */
458
    public function getPopularity()
459
    {
460
        if (!empty($this->remoteInfo['popularity']) && !$this->isIgnoredSource('remote'))
461
            return $this->remoteInfo['popularity'];
462
        return false;
463
    }
464
465
466
    /**
467
     * Get the text of the security warning if there is any
468
     *
469
     * @return string|bool The security warning if there is any, false otherwise
470
     */
471
    public function getSecurityWarning()
472
    {
473
        if (!empty($this->remoteInfo['securitywarning']) && !$this->isIgnoredSource('remote'))
474
            return $this->remoteInfo['securitywarning'];
475
        return false;
476
    }
477
478
    /**
479
     * Get the text of the security issue if there is any
480
     *
481
     * @return string|bool The security issue if there is any, false otherwise
482
     */
483
    public function getSecurityIssue()
484
    {
485
        if (!empty($this->remoteInfo['securityissue']) && !$this->isIgnoredSource('remote'))
486
            return $this->remoteInfo['securityissue'];
487
        return false;
488
    }
489
490
    /**
491
     * Get the URL of the screenshot of the extension if there is any
492
     *
493
     * @return string|bool The screenshot URL if there is any, false otherwise
494
     */
495
    public function getScreenshotURL()
496
    {
497
        if (!empty($this->remoteInfo['screenshoturl']) && !$this->isIgnoredSource('remote'))
498
            return $this->remoteInfo['screenshoturl'];
499
        return false;
500
    }
501
502
    /**
503
     * Get the URL of the thumbnail of the extension if there is any
504
     *
505
     * @return string|bool The thumbnail URL if there is any, false otherwise
506
     */
507
    public function getThumbnailURL()
508
    {
509
        if (!empty($this->remoteInfo['thumbnailurl']) && !$this->isIgnoredSource('remote'))
510
            return $this->remoteInfo['thumbnailurl'];
511
        return false;
512
    }
513
    /**
514
     * Get the last used download URL of the extension if there is any
515
     *
516
     * @return string|bool The previously used download URL, false if the extension has been installed manually
517
     */
518
    public function getLastDownloadURL()
519
    {
520
        if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
521
        return false;
522
    }
523
524
    /**
525
     * Get the download URL of the extension if there is any
526
     *
527
     * @return string|bool The download URL if there is any, false otherwise
528
     */
529
    public function getDownloadURL()
530
    {
531
        if (!empty($this->remoteInfo['downloadurl']))
532
            return $this->remoteInfo['downloadurl'];
533
        return false;
534
    }
535
536
    /**
537
     * If the download URL has changed since the last download
538
     *
539
     * @return bool If the download URL has changed
540
     */
541
    public function hasDownloadURLChanged()
542
    {
543
        $lasturl = $this->getLastDownloadURL();
544
        $currenturl = $this->getDownloadURL();
545
        return ($lasturl && $currenturl && $lasturl != $currenturl);
546
    }
547
548
    /**
549
     * Get the bug tracker URL of the extension if there is any
550
     *
551
     * @return string|bool The bug tracker URL if there is any, false otherwise
552
     */
553
    public function getBugtrackerURL()
554
    {
555
        if (!empty($this->remoteInfo['bugtracker']) && !$this->isIgnoredSource('remote'))
556
            return $this->remoteInfo['bugtracker'];
557
        return false;
558
    }
559
560
    /**
561
     * Get the URL of the source repository if there is any
562
     *
563
     * @return string|bool The URL of the source repository if there is any, false otherwise
564
     */
565
    public function getSourcerepoURL()
566
    {
567
        if (!empty($this->remoteInfo['sourcerepo']) && !$this->isIgnoredSource('remote'))
568
            return $this->remoteInfo['sourcerepo'];
569
        return false;
570
    }
571
572
    /**
573
     * Get the donation URL of the extension if there is any
574
     *
575
     * @return string|bool The donation URL if there is any, false otherwise
576
     */
577
    public function getDonationURL()
578
    {
579
        if (!empty($this->remoteInfo['donationurl']) && !$this->isIgnoredSource('remote'))
580
            return $this->remoteInfo['donationurl'];
581
        return false;
582
    }
583
584
    /**
585
     * Get the extension type(s)
586
     *
587
     * @return array The type(s) as array of strings
588
     */
589
    public function getTypes()
590
    {
591
        if (!empty($this->remoteInfo['types']) && !$this->isIgnoredSource('remote'))
592
            return $this->remoteInfo['types'];
593
        if ($this->isTemplate()) return array(32 => 'template');
594
        return array();
595
    }
596
597
    /**
598
     * Get a list of all DokuWiki versions this extension is compatible with
599
     *
600
     * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
601
     */
602
    public function getCompatibleVersions()
603
    {
604
        if (!empty($this->remoteInfo['compatible']))
605
            return $this->remoteInfo['compatible'];
606
        return array();
607
    }
608
609
    /**
610
     * Get the date of the last available update
611
     *
612
     * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
613
     */
614
    public function getLastUpdate()
615
    {
616
        if (!empty($this->remoteInfo['lastupdate']))
617
            return $this->remoteInfo['lastupdate'];
618
        return false;
619
    }
620
621
    /**
622
     * Get the base path of the extension
623
     *
624
     * @return string The base path of the extension
625
     */
626
    public function getInstallDir()
627
    {
628
        if ($this->isTemplate()) {
629
            return $this->tpllib.$this->base;
630
        } else {
631
            return DOKU_PLUGIN.$this->base;
632
        }
633
    }
634
635
    /**
636
     * The type of extension installation
637
     *
638
     * @return string One of "none", "manual", "git" or "automatic"
639
     */
640
    public function getInstallType()
641
    {
642
        if (!$this->isInstalled()) return 'none';
643
        if (!empty($this->managerData)) return 'automatic';
644
        if (is_dir($this->getInstallDir().'/.git')) return 'git';
645
        return 'manual';
646
    }
647
648
    /**
649
     * If the extension can probably be installed/updated or uninstalled
650
     *
651
     * @return bool|string True or error string
652
     */
653
    public function canModify()
654
    {
655
        if ($this->isInstalled()) {
656
            if (!is_writable($this->getInstallDir())) {
657
                return 'noperms';
658
            }
659
        }
660
661
        if ($this->isTemplate() && !is_writable($this->tpllib)) {
662
            return 'notplperms';
663
        } elseif (!is_writable(DOKU_PLUGIN)) {
664
            return 'nopluginperms';
665
        }
666
        return true;
667
    }
668
669
    /**
670
     * Install an extension from a user upload
671
     *
672
     * @param string $field name of the upload file
673
     * @throws Exception when something goes wrong
674
     * @return array The list of installed extensions
675
     */
676
    public function installFromUpload($field)
677
    {
678
        if ($_FILES[$field]['error']) {
679
            throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
680
        }
681
682
        $tmp = $this->mkTmpDir();
683
        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...
684
685
        // filename may contain the plugin name for old style plugins...
686
        $basename = basename($_FILES[$field]['name']);
687
        $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
688
        $basename = preg_replace('/[\W]+/', '', $basename);
689
690
        if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
691
            throw new Exception($this->getLang('msg_upload_failed'));
692
        }
693
694
        try {
695
            $installed = $this->installArchive("$tmp/upload.archive", true, $basename);
696
            $this->updateManagerData('', $installed);
697
            $this->removeDeletedfiles($installed);
698
            // purge cache
699
            $this->purgeCache();
700
        } catch (Exception $e) {
701
            throw $e;
702
        }
703
        return $installed;
704
    }
705
706
    /**
707
     * Install an extension from a remote URL
708
     *
709
     * @param string $url
710
     * @throws Exception when something goes wrong
711
     * @return array The list of installed extensions
712
     */
713
    public function installFromURL($url)
714
    {
715
        try {
716
            $path      = $this->download($url);
717
            $installed = $this->installArchive($path, true);
718
            $this->updateManagerData($url, $installed);
719
            $this->removeDeletedfiles($installed);
720
721
            // purge cache
722
            $this->purgeCache();
723
        } catch (Exception $e) {
724
            throw $e;
725
        }
726
        return $installed;
727
    }
728
729
    /**
730
     * Install or update the extension
731
     *
732
     * @throws \Exception when something goes wrong
733
     * @return array The list of installed extensions
734
     */
735
    public function installOrUpdate()
736
    {
737
        $url       = $this->getDownloadURL();
738
        $path      = $this->download($url);
739
        $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
740
        $this->updateManagerData($url, $installed);
741
742
        // refresh extension information
743
        if (!isset($installed[$this->getID()])) {
744
            throw new Exception('Error, the requested extension hasn\'t been installed or updated');
745
        }
746
        $this->removeDeletedfiles($installed);
747
        $this->setExtension($this->getID());
748
        $this->purgeCache();
749
        return $installed;
750
    }
751
752
    /**
753
     * Uninstall the extension
754
     *
755
     * @return bool If the plugin was sucessfully uninstalled
756
     */
757
    public function uninstall()
758
    {
759
        $this->purgeCache();
760
        return io_rmdir($this->getInstallDir(), true);
761
    }
762
763
    /**
764
     * Enable the extension
765
     *
766
     * @return bool|string True or an error message
767
     */
768
    public function enable()
769
    {
770
        if ($this->isTemplate()) return $this->getLang('notimplemented');
771
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
772
        if ($this->isEnabled()) return $this->getLang('alreadyenabled');
773
774
        /* @var PluginController $plugin_controller */
775
        global $plugin_controller;
776
        if ($plugin_controller->enable($this->base)) {
777
            $this->purgeCache();
778
            return true;
779
        } else {
780
            return $this->getLang('pluginlistsaveerror');
781
        }
782
    }
783
784
    /**
785
     * Disable the extension
786
     *
787
     * @return bool|string True or an error message
788
     */
789
    public function disable()
790
    {
791
        if ($this->isTemplate()) return $this->getLang('notimplemented');
792
793
        /* @var PluginController $plugin_controller */
794
        global $plugin_controller;
795
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
796
        if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
797
        if ($plugin_controller->disable($this->base)) {
798
            $this->purgeCache();
799
            return true;
800
        } else {
801
            return $this->getLang('pluginlistsaveerror');
802
        }
803
    }
804
805
    /**
806
     * Purge the cache by touching the main configuration file
807
     */
808
    protected function purgeCache()
809
    {
810
        global $config_cascade;
811
812
        // expire dokuwiki caches
813
        // touching local.php expires wiki page, JS and CSS caches
814
        @touch(reset($config_cascade['main']['local']));
815
    }
816
817
    /**
818
     * Read local extension data either from info.txt or getInfo()
819
     */
820
    protected function readLocalData()
821
    {
822
        if ($this->isTemplate()) {
823
            $infopath = $this->getInstallDir().'/template.info.txt';
824
        } else {
825
            $infopath = $this->getInstallDir().'/plugin.info.txt';
826
        }
827
828
        if (is_readable($infopath)) {
829
            $this->localInfo = confToHash($infopath);
830
        } elseif (!$this->isTemplate() && $this->isEnabled()) {
831
            $path   = $this->getInstallDir().'/';
832
            $plugin = null;
833
834
            foreach (PluginController::PLUGIN_TYPES as $type) {
835
                if (file_exists($path.$type.'.php')) {
836
                    $plugin = plugin_load($type, $this->base);
837
                    if ($plugin) break;
838
                }
839
840
                if ($dh = @opendir($path.$type.'/')) {
841
                    while (false !== ($cp = readdir($dh))) {
842
                        if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
843
844
                        $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
845
                        if ($plugin) break;
846
                    }
847
                    if ($plugin) break;
848
                    closedir($dh);
849
                }
850
            }
851
852
            if ($plugin) {
853
                /* @var DokuWiki_Plugin $plugin */
854
                $this->localInfo = $plugin->getInfo();
855
            }
856
        }
857
    }
858
859
    /**
860
     * Save the given URL and current datetime in the manager.dat file of all installed extensions
861
     *
862
     * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
863
     * @param array  $installed Optional list of installed plugins
864
     */
865
    protected function updateManagerData($url = '', $installed = null)
866
    {
867
        $origID = $this->getID();
868
869
        if (is_null($installed)) {
870
            $installed = array($origID);
871
        }
872
873
        foreach ($installed as $ext => $info) {
874
            if ($this->getID() != $ext) $this->setExtension($ext);
875
            if ($url) {
876
                $this->managerData['downloadurl'] = $url;
877
            } elseif (isset($this->managerData['downloadurl'])) {
878
                unset($this->managerData['downloadurl']);
879
            }
880
            if (isset($this->managerData['installed'])) {
881
                $this->managerData['updated'] = date('r');
882
            } else {
883
                $this->managerData['installed'] = date('r');
884
            }
885
            $this->writeManagerData();
886
        }
887
888
        if ($this->getID() != $origID) $this->setExtension($origID);
889
    }
890
891
    /**
892
     * Read the manager.dat file
893
     */
894
    protected function readManagerData()
895
    {
896
        $managerpath = $this->getInstallDir().'/manager.dat';
897
        if (is_readable($managerpath)) {
898
            $file = @file($managerpath);
899
            if (!empty($file)) {
900
                foreach ($file as $line) {
901
                    list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
902
                    $key = trim($key);
903
                    $value = trim($value);
904
                    // backwards compatible with old plugin manager
905
                    if ($key == 'url') $key = 'downloadurl';
906
                    $this->managerData[$key] = $value;
907
                }
908
            }
909
        }
910
    }
911
912
    /**
913
     * Write the manager.data file
914
     */
915
    protected function writeManagerData()
916
    {
917
        $managerpath = $this->getInstallDir().'/manager.dat';
918
        $data = '';
919
        foreach ($this->managerData as $k => $v) {
920
            $data .= $k.'='.$v.DOKU_LF;
921
        }
922
        io_saveFile($managerpath, $data);
923
    }
924
925
    /**
926
     * Returns a temporary directory
927
     *
928
     * The directory is registered for cleanup when the class is destroyed
929
     *
930
     * @return false|string
931
     */
932
    protected function mkTmpDir()
933
    {
934
        $dir = io_mktmpdir();
935
        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...
936
        $this->temporary[] = $dir;
937
        return $dir;
938
    }
939
940
    /**
941
     * downloads a file from the net and saves it
942
     *
943
     * - $file is the directory where the file should be saved
944
     * - if successful will return the name used for the saved file, false otherwise
945
     *
946
     * @author Andreas Gohr <[email protected]>
947
     * @author Chris Smith <[email protected]>
948
     *
949
     * @param string $url           url to download
950
     * @param string $file          path to file or directory where to save
951
     * @param string $defaultName   fallback for name of download
952
     * @return bool|string          if failed false, otherwise true or the name of the file in the given dir
953
     */
954
    protected function downloadToFile($url, $file, $defaultName = '')
955
    {
956
        global $conf;
957
        $http = new DokuHTTPClient();
958
        $http->max_bodysize = 0;
959
        $http->timeout = 25; //max. 25 sec
960
        $http->keep_alive = false; // we do single ops here, no need for keep-alive
961
        $http->agent = 'DokuWiki HTTP Client (Extension Manager)';
962
963
        $data = $http->get($url);
964
        if ($data === false) return false;
965
966
        $name = '';
967
        if (isset($http->resp_headers['content-disposition'])) {
968
            $content_disposition = $http->resp_headers['content-disposition'];
969
            $match = array();
970
            if (is_string($content_disposition) &&
971
                preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match)
972
            ) {
973
                $name = \dokuwiki\Utf8\PhpString::basename($match[1]);
974
            }
975
976
        }
977
978
        if (!$name) {
979
            if (!$defaultName) return false;
980
            $name = $defaultName;
981
        }
982
983
        $file = $file.$name;
984
985
        $fileexists = file_exists($file);
986
        $fp = @fopen($file,"w");
987
        if (!$fp) return false;
988
        fwrite($fp, $data);
989
        fclose($fp);
990
        if (!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
991
        return $name;
992
    }
993
994
    /**
995
     * Download an archive to a protected path
996
     *
997
     * @param string $url  The url to get the archive from
998
     * @throws Exception   when something goes wrong
999
     * @return string The path where the archive was saved
1000
     */
1001
    public function download($url)
1002
    {
1003
        // check the url
1004
        if (!preg_match('/https?:\/\//i', $url)) {
1005
            throw new Exception($this->getLang('error_badurl'));
1006
        }
1007
1008
        // try to get the file from the path (used as plugin name fallback)
1009
        $file = parse_url($url, PHP_URL_PATH);
1010
        if (is_null($file)) {
1011
            $file = md5($url);
1012
        } else {
1013
            $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...
1014
        }
1015
1016
        // create tmp directory for download
1017
        if (!($tmp = $this->mkTmpDir())) {
1018
            throw new Exception($this->getLang('error_dircreate'));
1019
        }
1020
1021
        // download
1022
        if (!$file = $this->downloadToFile($url, $tmp.'/', $file)) {
1023
            io_rmdir($tmp, true);
1024
            throw new Exception(sprintf($this->getLang('error_download'),
1025
                '<bdi>'.hsc($url).'</bdi>')
1026
            );
1027
        }
1028
1029
        return $tmp.'/'.$file;
1030
    }
1031
1032
    /**
1033
     * @param string $file      The path to the archive that shall be installed
1034
     * @param bool   $overwrite If an already installed plugin should be overwritten
1035
     * @param string $base      The basename of the plugin if it's known
1036
     * @throws Exception        when something went wrong
1037
     * @return array            list of installed extensions
1038
     */
1039
    public function installArchive($file, $overwrite = false, $base = '')
1040
    {
1041
        $installed_extensions = array();
1042
1043
        // create tmp directory for decompression
1044
        if (!($tmp = $this->mkTmpDir())) {
1045
            throw new Exception($this->getLang('error_dircreate'));
1046
        }
1047
1048
        // add default base folder if specified to handle case where zip doesn't contain this
1049
        if ($base && !@mkdir($tmp.'/'.$base)) {
1050
            throw new Exception($this->getLang('error_dircreate'));
1051
        }
1052
1053
        // decompress
1054
        $this->decompress($file, "$tmp/".$base);
1055
1056
        // search $tmp/$base for the folder(s) that has been created
1057
        // move the folder(s) to lib/..
1058
        $result = array('old'=>array(), 'new'=>array());
1059
        $default = ($this->isTemplate() ? 'template' : 'plugin');
1060
        if (!$this->findFolders($result, $tmp.'/'.$base, $default)) {
1061
            throw new Exception($this->getLang('error_findfolder'));
1062
        }
1063
1064
        // choose correct result array
1065
        if (count($result['new'])) {
1066
            $install = $result['new'];
1067
        } else {
1068
            $install = $result['old'];
1069
        }
1070
1071
        if (!count($install)) {
1072
            throw new Exception($this->getLang('error_findfolder'));
1073
        }
1074
1075
        // now install all found items
1076
        foreach ($install as $item) {
1077
            // where to install?
1078
            if ($item['type'] == 'template') {
1079
                $target_base_dir = $this->tpllib;
1080
            } else {
1081
                $target_base_dir = DOKU_PLUGIN;
1082
            }
1083
1084
            if (!empty($item['base'])) {
1085
                // use base set in info.txt
1086
            } elseif ($base && count($install) == 1) {
1087
                $item['base'] = $base;
1088
            } else {
1089
                // default - use directory as found in zip
1090
                // plugins from github/master without *.info.txt will install in wrong folder
1091
                // but using $info->id will make 'code3' fail (which should install in lib/code/..)
1092
                $item['base'] = basename($item['tmp']);
1093
            }
1094
1095
            // check to make sure we aren't overwriting anything
1096
            $target = $target_base_dir.$item['base'];
1097
            if (!$overwrite && file_exists($target)) {
1098
                // TODO remember our settings, ask the user to confirm overwrite
1099
                continue;
1100
            }
1101
1102
            $action = file_exists($target) ? 'update' : 'install';
1103
1104
            // copy action
1105
            if ($this->dircopy($item['tmp'], $target)) {
1106
                // return info
1107
                $id = $item['base'];
1108
                if ($item['type'] == 'template') {
1109
                    $id = 'template:'.$id;
1110
                }
1111
                $installed_extensions[$id] = array(
1112
                    'base' => $item['base'],
1113
                    'type' => $item['type'],
1114
                    'action' => $action
1115
                );
1116
            } else {
1117
                throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF,
1118
                    '<bdi>'.$item['base'].'</bdi>')
1119
                );
1120
            }
1121
        }
1122
1123
        // cleanup
1124
        if ($tmp) io_rmdir($tmp, true);
1125
1126
        return $installed_extensions;
1127
    }
1128
1129
    /**
1130
     * Find out what was in the extracted directory
1131
     *
1132
     * Correct folders are searched recursively using the "*.info.txt" configs
1133
     * as indicator for a root folder. When such a file is found, it's base
1134
     * setting is used (when set). All folders found by this method are stored
1135
     * in the 'new' key of the $result array.
1136
     *
1137
     * For backwards compatibility all found top level folders are stored as
1138
     * in the 'old' key of the $result array.
1139
     *
1140
     * When no items are found in 'new' the copy mechanism should fall back
1141
     * the 'old' list.
1142
     *
1143
     * @author Andreas Gohr <[email protected]>
1144
     * @param array $result - results are stored here
1145
     * @param string $directory - the temp directory where the package was unpacked to
1146
     * @param string $default_type - type used if no info.txt available
1147
     * @param string $subdir - a subdirectory. do not set. used by recursion
1148
     * @return bool - false on error
1149
     */
1150
    protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '')
1151
    {
1152
        $this_dir = "$directory$subdir";
1153
        $dh       = @opendir($this_dir);
1154
        if (!$dh) return false;
1155
1156
        $found_dirs           = array();
1157
        $found_files          = 0;
1158
        $found_template_parts = 0;
1159
        while (false !== ($f = readdir($dh))) {
1160
            if ($f == '.' || $f == '..') continue;
1161
1162
            if (is_dir("$this_dir/$f")) {
1163
                $found_dirs[] = "$subdir/$f";
1164
            } else {
1165
                // it's a file -> check for config
1166
                $found_files++;
1167
                switch ($f) {
1168
                    case 'plugin.info.txt':
1169
                    case 'template.info.txt':
1170
                        // we have  found a clear marker, save and return
1171
                        $info = array();
1172
                        $type = explode('.', $f, 2);
1173
                        $info['type'] = $type[0];
1174
                        $info['tmp']  = $this_dir;
1175
                        $conf = confToHash("$this_dir/$f");
1176
                        $info['base'] = basename($conf['base']);
1177
                        $result['new'][] = $info;
1178
                        return true;
1179
1180
                    case 'main.php':
1181
                    case 'details.php':
1182
                    case 'mediamanager.php':
1183
                    case 'style.ini':
1184
                        $found_template_parts++;
1185
                        break;
1186
                }
1187
            }
1188
        }
1189
        closedir($dh);
1190
1191
        // files where found but no info.txt - use old method
1192
        if ($found_files) {
1193
            $info        = array();
1194
            $info['tmp'] = $this_dir;
1195
            // does this look like a template or should we use the default type?
1196
            if ($found_template_parts >= 2) {
1197
                $info['type'] = 'template';
1198
            } else {
1199
                $info['type'] = $default_type;
1200
            }
1201
1202
            $result['old'][] = $info;
1203
            return true;
1204
        }
1205
1206
        // we have no files yet -> recurse
1207
        foreach ($found_dirs as $found_dir) {
1208
            $this->findFolders($result, $directory, $default_type, "$found_dir");
1209
        }
1210
        return true;
1211
    }
1212
1213
    /**
1214
     * Decompress a given file to the given target directory
1215
     *
1216
     * Determines the compression type from the file extension
1217
     *
1218
     * @param string $file   archive to extract
1219
     * @param string $target directory to extract to
1220
     * @throws Exception
1221
     * @return bool
1222
     */
1223
    private function decompress($file, $target)
1224
    {
1225
        // decompression library doesn't like target folders ending in "/"
1226
        if (substr($target, -1) == "/") $target = substr($target, 0, -1);
1227
1228
        $ext = $this->guessArchiveType($file);
1229
        if (in_array($ext, array('tar', 'bz', 'gz'))) {
1230
            try {
1231
                $tar = new \splitbrain\PHPArchive\Tar();
1232
                $tar->open($file);
1233
                $tar->extract($target);
1234
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1235
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1236
            }
1237
1238
            return true;
1239
        } elseif ($ext == 'zip') {
1240
            try {
1241
                $zip = new \splitbrain\PHPArchive\Zip();
1242
                $zip->open($file);
1243
                $zip->extract($target);
1244
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1245
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1246
            }
1247
1248
            return true;
1249
        }
1250
1251
        // the only case when we don't get one of the recognized archive types is
1252
        // when the archive file can't be read
1253
        throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
1254
    }
1255
1256
    /**
1257
     * Determine the archive type of the given file
1258
     *
1259
     * Reads the first magic bytes of the given file for content type guessing,
1260
     * if neither bz, gz or zip are recognized, tar is assumed.
1261
     *
1262
     * @author Andreas Gohr <[email protected]>
1263
     * @param string $file The file to analyze
1264
     * @return string|false false if the file can't be read, otherwise an "extension"
1265
     */
1266
    private function guessArchiveType($file)
1267
    {
1268
        $fh = fopen($file, 'rb');
1269
        if (!$fh) return false;
1270
        $magic = fread($fh, 5);
1271
        fclose($fh);
1272
1273
        if (strpos($magic, "\x42\x5a") === 0) return 'bz';
1274
        if (strpos($magic, "\x1f\x8b") === 0) return 'gz';
1275
        if (strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
1276
        return 'tar';
1277
    }
1278
1279
    /**
1280
     * Copy with recursive sub-directory support
1281
     *
1282
     * @param string $src filename path to file
1283
     * @param string $dst filename path to file
1284
     * @return bool|int|string
1285
     */
1286
    private function dircopy($src, $dst)
1287
    {
1288
        global $conf;
1289
1290
        if (is_dir($src)) {
1291
            if (!$dh = @opendir($src)) return false;
1292
1293
            if ($ok = io_mkdir_p($dst)) {
1294
                while ($ok && (false !== ($f = readdir($dh)))) {
1295
                    if ($f == '..' || $f == '.') continue;
1296
                    $ok = $this->dircopy("$src/$f", "$dst/$f");
1297
                }
1298
            }
1299
1300
            closedir($dh);
1301
            return $ok;
1302
        } else {
1303
            $existed = file_exists($dst);
1304
1305
            if (!@copy($src, $dst)) return false;
1306
            if (!$existed && $conf['fperm']) chmod($dst, $conf['fperm']);
1307
            @touch($dst, filemtime($src));
1308
        }
1309
1310
        return true;
1311
    }
1312
1313
    /**
1314
     * Delete outdated files from updated plugins
1315
     *
1316
     * @param array $installed
1317
     */
1318
    private function removeDeletedfiles($installed)
1319
    {
1320
        foreach ($installed as $id => $extension) {
1321
            // only on update
1322
            if ($extension['action'] == 'install') continue;
1323
1324
            // get definition file
1325
            if ($extension['type'] == 'template') {
1326
                $extensiondir = $this->tpllib;
1327
            } else {
1328
                $extensiondir = DOKU_PLUGIN;
1329
            }
1330
            $extensiondir = $extensiondir . $extension['base'] .'/';
1331
            $definitionfile = $extensiondir . 'deleted.files';
1332
            if (!file_exists($definitionfile)) continue;
1333
1334
            // delete the old files
1335
            $list = file($definitionfile);
1336
1337
            foreach ($list as $line) {
1338
                $line = trim(preg_replace('/#.*$/', '', $line));
1339
                if (!$line) continue;
1340
                $file = $extensiondir . $line;
1341
                if (!file_exists($file)) continue;
1342
1343
                io_rmdir($file, true);
1344
            }
1345
        }
1346
    }
1347
}
1348
1349
// vim:ts=4:sw=4:et:
1350