Failed Conditions
Push — utf8refactor ( 0da4ba...8cbc5e )
by Andreas
06:03
created

helper_plugin_extension_extension::canModify()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 7
nop 0
dl 0
loc 15
rs 9.2222
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', 'authmysql', 'authpdo',
130
                            'authpgsql', 'authplain', 'acl', 'info', 'extension',
131
                            'revert', 'popularity', 'config', 'safefnrecode', 'styling',
132
                            'testing', '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->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...
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/'.($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
310
    }
311
312
    /**
313
     * Get the installed version of the extension
314
     *
315
     * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
316
     */
317
    public function getInstalledVersion()
318
    {
319
        if (!empty($this->localInfo['date'])) return $this->localInfo['date'];
320
        if ($this->isInstalled()) return $this->getLang('unknownversion');
321
        return false;
322
    }
323
324
    /**
325
     * Get the install date of the current version
326
     *
327
     * @return string|bool The date of the last update or false if not available
328
     */
329
    public function getUpdateDate()
330
    {
331
        if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
332
        return $this->getInstallDate();
333
    }
334
335
    /**
336
     * Get the date of the installation of the plugin
337
     *
338
     * @return string|bool The date of the installation or false if not available
339
     */
340
    public function getInstallDate()
341
    {
342
        if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
343
        return false;
344
    }
345
346
    /**
347
     * Get the names of the dependencies of this extension
348
     *
349
     * @return array The base names of the dependencies
350
     */
351
    public function getDependencies()
352
    {
353
        if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies'];
354
        return array();
355
    }
356
357
    /**
358
     * Get the names of the missing dependencies
359
     *
360
     * @return array The base names of the missing dependencies
361
     */
362
    public function getMissingDependencies()
363
    {
364
        /* @var PluginController $plugin_controller */
365
        global $plugin_controller;
366
        $dependencies = $this->getDependencies();
367
        $missing_dependencies = array();
368
        foreach ($dependencies as $dependency) {
369
            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...
370
                $missing_dependencies[] = $dependency;
371
            }
372
        }
373
        return $missing_dependencies;
374
    }
375
376
    /**
377
     * Get the names of all conflicting extensions
378
     *
379
     * @return array The names of the conflicting extensions
380
     */
381
    public function getConflicts()
382
    {
383
        if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['conflicts'];
384
        return array();
385
    }
386
387
    /**
388
     * Get the names of similar extensions
389
     *
390
     * @return array The names of similar extensions
391
     */
392
    public function getSimilarExtensions()
393
    {
394
        if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar'];
395
        return array();
396
    }
397
398
    /**
399
     * Get the names of the tags of the extension
400
     *
401
     * @return array The names of the tags of the extension
402
     */
403
    public function getTags()
404
    {
405
        if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags'];
406
        return array();
407
    }
408
409
    /**
410
     * Get the popularity information as floating point number [0,1]
411
     *
412
     * @return float|bool The popularity information or false if it isn't available
413
     */
414
    public function getPopularity()
415
    {
416
        if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity'];
417
        return false;
418
    }
419
420
421
    /**
422
     * Get the text of the security warning if there is any
423
     *
424
     * @return string|bool The security warning if there is any, false otherwise
425
     */
426
    public function getSecurityWarning()
427
    {
428
        if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning'];
429
        return false;
430
    }
431
432
    /**
433
     * Get the text of the security issue if there is any
434
     *
435
     * @return string|bool The security issue if there is any, false otherwise
436
     */
437
    public function getSecurityIssue()
438
    {
439
        if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue'];
440
        return false;
441
    }
442
443
    /**
444
     * Get the URL of the screenshot of the extension if there is any
445
     *
446
     * @return string|bool The screenshot URL if there is any, false otherwise
447
     */
448
    public function getScreenshotURL()
449
    {
450
        if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl'];
451
        return false;
452
    }
453
454
    /**
455
     * Get the URL of the thumbnail of the extension if there is any
456
     *
457
     * @return string|bool The thumbnail URL if there is any, false otherwise
458
     */
459
    public function getThumbnailURL()
460
    {
461
        if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl'];
462
        return false;
463
    }
464
    /**
465
     * Get the last used download URL of the extension if there is any
466
     *
467
     * @return string|bool The previously used download URL, false if the extension has been installed manually
468
     */
469
    public function getLastDownloadURL()
470
    {
471
        if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
472
        return false;
473
    }
474
475
    /**
476
     * Get the download URL of the extension if there is any
477
     *
478
     * @return string|bool The download URL if there is any, false otherwise
479
     */
480
    public function getDownloadURL()
481
    {
482
        if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl'];
483
        return false;
484
    }
485
486
    /**
487
     * If the download URL has changed since the last download
488
     *
489
     * @return bool If the download URL has changed
490
     */
491
    public function hasDownloadURLChanged()
492
    {
493
        $lasturl = $this->getLastDownloadURL();
494
        $currenturl = $this->getDownloadURL();
495
        return ($lasturl && $currenturl && $lasturl != $currenturl);
496
    }
497
498
    /**
499
     * Get the bug tracker URL of the extension if there is any
500
     *
501
     * @return string|bool The bug tracker URL if there is any, false otherwise
502
     */
503
    public function getBugtrackerURL()
504
    {
505
        if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker'];
506
        return false;
507
    }
508
509
    /**
510
     * Get the URL of the source repository if there is any
511
     *
512
     * @return string|bool The URL of the source repository if there is any, false otherwise
513
     */
514
    public function getSourcerepoURL()
515
    {
516
        if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo'];
517
        return false;
518
    }
519
520
    /**
521
     * Get the donation URL of the extension if there is any
522
     *
523
     * @return string|bool The donation URL if there is any, false otherwise
524
     */
525
    public function getDonationURL()
526
    {
527
        if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl'];
528
        return false;
529
    }
530
531
    /**
532
     * Get the extension type(s)
533
     *
534
     * @return array The type(s) as array of strings
535
     */
536
    public function getTypes()
537
    {
538
        if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types'];
539
        if ($this->isTemplate()) return array(32 => 'template');
540
        return array();
541
    }
542
543
    /**
544
     * Get a list of all DokuWiki versions this extension is compatible with
545
     *
546
     * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
547
     */
548
    public function getCompatibleVersions()
549
    {
550
        if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible'];
551
        return array();
552
    }
553
554
    /**
555
     * Get the date of the last available update
556
     *
557
     * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
558
     */
559
    public function getLastUpdate()
560
    {
561
        if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate'];
562
        return false;
563
    }
564
565
    /**
566
     * Get the base path of the extension
567
     *
568
     * @return string The base path of the extension
569
     */
570
    public function getInstallDir()
571
    {
572
        if ($this->isTemplate()) {
573
            return $this->tpllib.$this->base;
574
        } else {
575
            return DOKU_PLUGIN.$this->base;
576
        }
577
    }
578
579
    /**
580
     * The type of extension installation
581
     *
582
     * @return string One of "none", "manual", "git" or "automatic"
583
     */
584
    public function getInstallType()
585
    {
586
        if (!$this->isInstalled()) return 'none';
587
        if (!empty($this->managerData)) return 'automatic';
588
        if (is_dir($this->getInstallDir().'/.git')) return 'git';
589
        return 'manual';
590
    }
591
592
    /**
593
     * If the extension can probably be installed/updated or uninstalled
594
     *
595
     * @return bool|string True or error string
596
     */
597
    public function canModify()
598
    {
599
        if ($this->isInstalled()) {
600
            if (!is_writable($this->getInstallDir())) {
601
                return 'noperms';
602
            }
603
        }
604
605
        if ($this->isTemplate() && !is_writable($this->tpllib)) {
606
            return 'notplperms';
607
        } elseif (!is_writable(DOKU_PLUGIN)) {
608
            return 'nopluginperms';
609
        }
610
        return true;
611
    }
612
613
    /**
614
     * Install an extension from a user upload
615
     *
616
     * @param string $field name of the upload file
617
     * @throws Exception when something goes wrong
618
     * @return array The list of installed extensions
619
     */
620
    public function installFromUpload($field)
621
    {
622
        if ($_FILES[$field]['error']) {
623
            throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
624
        }
625
626
        $tmp = $this->mkTmpDir();
627
        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...
628
629
        // filename may contain the plugin name for old style plugins...
630
        $basename = basename($_FILES[$field]['name']);
631
        $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
632
        $basename = preg_replace('/[\W]+/', '', $basename);
633
634
        if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
635
            throw new Exception($this->getLang('msg_upload_failed'));
636
        }
637
638
        try {
639
            $installed = $this->installArchive("$tmp/upload.archive", true, $basename);
640
            $this->updateManagerData('', $installed);
641
            $this->removeDeletedfiles($installed);
642
            // purge cache
643
            $this->purgeCache();
644
        } catch (Exception $e) {
645
            throw $e;
646
        }
647
        return $installed;
648
    }
649
650
    /**
651
     * Install an extension from a remote URL
652
     *
653
     * @param string $url
654
     * @throws Exception when something goes wrong
655
     * @return array The list of installed extensions
656
     */
657
    public function installFromURL($url)
658
    {
659
        try {
660
            $path      = $this->download($url);
661
            $installed = $this->installArchive($path, true);
662
            $this->updateManagerData($url, $installed);
663
            $this->removeDeletedfiles($installed);
664
665
            // purge cache
666
            $this->purgeCache();
667
        } catch (Exception $e) {
668
            throw $e;
669
        }
670
        return $installed;
671
    }
672
673
    /**
674
     * Install or update the extension
675
     *
676
     * @throws \Exception when something goes wrong
677
     * @return array The list of installed extensions
678
     */
679
    public function installOrUpdate()
680
    {
681
        $url       = $this->getDownloadURL();
682
        $path      = $this->download($url);
683
        $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
684
        $this->updateManagerData($url, $installed);
685
686
        // refresh extension information
687
        if (!isset($installed[$this->getID()])) {
688
            throw new Exception('Error, the requested extension hasn\'t been installed or updated');
689
        }
690
        $this->removeDeletedfiles($installed);
691
        $this->setExtension($this->getID());
692
        $this->purgeCache();
693
        return $installed;
694
    }
695
696
    /**
697
     * Uninstall the extension
698
     *
699
     * @return bool If the plugin was sucessfully uninstalled
700
     */
701
    public function uninstall()
702
    {
703
        $this->purgeCache();
704
        return io_rmdir($this->getInstallDir(), true);
705
    }
706
707
    /**
708
     * Enable the extension
709
     *
710
     * @return bool|string True or an error message
711
     */
712
    public function enable()
713
    {
714
        if ($this->isTemplate()) return $this->getLang('notimplemented');
715
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
716
        if ($this->isEnabled()) return $this->getLang('alreadyenabled');
717
718
        /* @var PluginController $plugin_controller */
719
        global $plugin_controller;
720
        if ($plugin_controller->enable($this->base)) {
721
            $this->purgeCache();
722
            return true;
723
        } else {
724
            return $this->getLang('pluginlistsaveerror');
725
        }
726
    }
727
728
    /**
729
     * Disable the extension
730
     *
731
     * @return bool|string True or an error message
732
     */
733
    public function disable()
734
    {
735
        if ($this->isTemplate()) return $this->getLang('notimplemented');
736
737
        /* @var PluginController $plugin_controller */
738
        global $plugin_controller;
739
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
740
        if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
741
        if ($plugin_controller->disable($this->base)) {
742
            $this->purgeCache();
743
            return true;
744
        } else {
745
            return $this->getLang('pluginlistsaveerror');
746
        }
747
    }
748
749
    /**
750
     * Purge the cache by touching the main configuration file
751
     */
752
    protected function purgeCache()
753
    {
754
        global $config_cascade;
755
756
        // expire dokuwiki caches
757
        // touching local.php expires wiki page, JS and CSS caches
758
        @touch(reset($config_cascade['main']['local']));
759
    }
760
761
    /**
762
     * Read local extension data either from info.txt or getInfo()
763
     */
764
    protected function readLocalData()
765
    {
766
        if ($this->isTemplate()) {
767
            $infopath = $this->getInstallDir().'/template.info.txt';
768
        } else {
769
            $infopath = $this->getInstallDir().'/plugin.info.txt';
770
        }
771
772
        if (is_readable($infopath)) {
773
            $this->localInfo = confToHash($infopath);
774
        } elseif (!$this->isTemplate() && $this->isEnabled()) {
775
            $path       = $this->getInstallDir().'/';
776
            $plugin     = null;
777
778
            foreach (PluginController::PLUGIN_TYPES as $type) {
779
                if (file_exists($path.$type.'.php')) {
780
                    $plugin = plugin_load($type, $this->base);
781
                    if ($plugin) break;
782
                }
783
784
                if ($dh = @opendir($path.$type.'/')) {
785
                    while (false !== ($cp = readdir($dh))) {
786
                        if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
787
788
                        $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
789
                        if ($plugin) break;
790
                    }
791
                    if ($plugin) break;
792
                    closedir($dh);
793
                }
794
            }
795
796
            if ($plugin) {
797
                /* @var DokuWiki_Plugin $plugin */
798
                $this->localInfo = $plugin->getInfo();
799
            }
800
        }
801
    }
802
803
    /**
804
     * Save the given URL and current datetime in the manager.dat file of all installed extensions
805
     *
806
     * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
807
     * @param array  $installed Optional list of installed plugins
808
     */
809
    protected function updateManagerData($url = '', $installed = null)
810
    {
811
        $origID = $this->getID();
812
813
        if (is_null($installed)) {
814
            $installed = array($origID);
815
        }
816
817
        foreach ($installed as $ext => $info) {
818
            if ($this->getID() != $ext) $this->setExtension($ext);
819
            if ($url) {
820
                $this->managerData['downloadurl'] = $url;
821
            } elseif (isset($this->managerData['downloadurl'])) {
822
                unset($this->managerData['downloadurl']);
823
            }
824
            if (isset($this->managerData['installed'])) {
825
                $this->managerData['updated'] = date('r');
826
            } else {
827
                $this->managerData['installed'] = date('r');
828
            }
829
            $this->writeManagerData();
830
        }
831
832
        if ($this->getID() != $origID) $this->setExtension($origID);
833
    }
834
835
    /**
836
     * Read the manager.dat file
837
     */
838
    protected function readManagerData()
839
    {
840
        $managerpath = $this->getInstallDir().'/manager.dat';
841
        if (is_readable($managerpath)) {
842
            $file = @file($managerpath);
843
            if (!empty($file)) {
844
                foreach ($file as $line) {
845
                    list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
846
                    $key = trim($key);
847
                    $value = trim($value);
848
                    // backwards compatible with old plugin manager
849
                    if ($key == 'url') $key = 'downloadurl';
850
                    $this->managerData[$key] = $value;
851
                }
852
            }
853
        }
854
    }
855
856
    /**
857
     * Write the manager.data file
858
     */
859
    protected function writeManagerData()
860
    {
861
        $managerpath = $this->getInstallDir().'/manager.dat';
862
        $data = '';
863
        foreach ($this->managerData as $k => $v) {
864
            $data .= $k.'='.$v.DOKU_LF;
865
        }
866
        io_saveFile($managerpath, $data);
867
    }
868
869
    /**
870
     * Returns a temporary directory
871
     *
872
     * The directory is registered for cleanup when the class is destroyed
873
     *
874
     * @return false|string
875
     */
876
    protected function mkTmpDir()
877
    {
878
        $dir = io_mktmpdir();
879
        if (!$dir) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $dir of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
880
        $this->temporary[] = $dir;
881
        return $dir;
882
    }
883
884
    /**
885
     * downloads a file from the net and saves it
886
     *
887
     * - $file is the directory where the file should be saved
888
     * - if successful will return the name used for the saved file, false otherwise
889
     *
890
     * @author Andreas Gohr <[email protected]>
891
     * @author Chris Smith <[email protected]>
892
     *
893
     * @param string $url           url to download
894
     * @param string $file          path to file or directory where to save
895
     * @param string $defaultName   fallback for name of download
896
     * @return bool|string          if failed false, otherwise true or the name of the file in the given dir
897
     */
898
    protected function downloadToFile($url,$file,$defaultName=''){
899
        global $conf;
900
        $http = new DokuHTTPClient();
901
        $http->max_bodysize = 0;
902
        $http->timeout = 25; //max. 25 sec
903
        $http->keep_alive = false; // we do single ops here, no need for keep-alive
904
        $http->agent = 'DokuWiki HTTP Client (Extension Manager)';
905
906
        $data = $http->get($url);
907
        if ($data === false) return false;
908
909
        $name = '';
910
        if (isset($http->resp_headers['content-disposition'])) {
911
            $content_disposition = $http->resp_headers['content-disposition'];
912
            $match=array();
913
            if (is_string($content_disposition) &&
914
                    preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match)) {
915
916
                $name = \dokuwiki\Utf8\PhpString::basename($match[1]);
917
            }
918
919
        }
920
921
        if (!$name) {
922
            if (!$defaultName) return false;
923
            $name = $defaultName;
924
        }
925
926
        $file = $file.$name;
927
928
        $fileexists = file_exists($file);
929
        $fp = @fopen($file,"w");
930
        if(!$fp) return false;
931
        fwrite($fp,$data);
932
        fclose($fp);
933
        if(!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
934
        return $name;
935
    }
936
937
    /**
938
     * Download an archive to a protected path
939
     *
940
     * @param string $url  The url to get the archive from
941
     * @throws Exception   when something goes wrong
942
     * @return string The path where the archive was saved
943
     */
944
    public function download($url)
945
    {
946
        // check the url
947
        if (!preg_match('/https?:\/\//i', $url)) {
948
            throw new Exception($this->getLang('error_badurl'));
949
        }
950
951
        // try to get the file from the path (used as plugin name fallback)
952
        $file = parse_url($url, PHP_URL_PATH);
953
        if (is_null($file)) {
954
            $file = md5($url);
955
        } else {
956
            $file = \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...
957
        }
958
959
        // create tmp directory for download
960
        if (!($tmp = $this->mkTmpDir())) {
961
            throw new Exception($this->getLang('error_dircreate'));
962
        }
963
964
        // download
965
        if (!$file = $this->downloadToFile($url, $tmp.'/', $file)) {
966
            io_rmdir($tmp, true);
967
            throw new Exception(sprintf($this->getLang('error_download'), '<bdi>'.hsc($url).'</bdi>'));
968
        }
969
970
        return $tmp.'/'.$file;
971
    }
972
973
    /**
974
     * @param string $file      The path to the archive that shall be installed
975
     * @param bool   $overwrite If an already installed plugin should be overwritten
976
     * @param string $base      The basename of the plugin if it's known
977
     * @throws Exception        when something went wrong
978
     * @return array            list of installed extensions
979
     */
980
    public function installArchive($file, $overwrite = false, $base = '')
981
    {
982
        $installed_extensions = array();
983
984
        // create tmp directory for decompression
985
        if (!($tmp = $this->mkTmpDir())) {
986
            throw new Exception($this->getLang('error_dircreate'));
987
        }
988
989
        // add default base folder if specified to handle case where zip doesn't contain this
990
        if ($base && !@mkdir($tmp.'/'.$base)) {
991
            throw new Exception($this->getLang('error_dircreate'));
992
        }
993
994
        // decompress
995
        $this->decompress($file, "$tmp/".$base);
996
997
        // search $tmp/$base for the folder(s) that has been created
998
        // move the folder(s) to lib/..
999
        $result = array('old'=>array(), 'new'=>array());
1000
        $default = ($this->isTemplate() ? 'template' : 'plugin');
1001
        if (!$this->findFolders($result, $tmp.'/'.$base, $default)) {
1002
            throw new Exception($this->getLang('error_findfolder'));
1003
        }
1004
1005
        // choose correct result array
1006
        if (count($result['new'])) {
1007
            $install = $result['new'];
1008
        } else {
1009
            $install = $result['old'];
1010
        }
1011
1012
        if (!count($install)) {
1013
            throw new Exception($this->getLang('error_findfolder'));
1014
        }
1015
1016
        // now install all found items
1017
        foreach ($install as $item) {
1018
            // where to install?
1019
            if ($item['type'] == 'template') {
1020
                $target_base_dir = $this->tpllib;
1021
            } else {
1022
                $target_base_dir = DOKU_PLUGIN;
1023
            }
1024
1025
            if (!empty($item['base'])) {
1026
                // use base set in info.txt
1027
            } elseif ($base && count($install) == 1) {
1028
                $item['base'] = $base;
1029
            } else {
1030
                // default - use directory as found in zip
1031
                // plugins from github/master without *.info.txt will install in wrong folder
1032
                // but using $info->id will make 'code3' fail (which should install in lib/code/..)
1033
                $item['base'] = basename($item['tmp']);
1034
            }
1035
1036
            // check to make sure we aren't overwriting anything
1037
            $target = $target_base_dir.$item['base'];
1038
            if (!$overwrite && file_exists($target)) {
1039
                // TODO remember our settings, ask the user to confirm overwrite
1040
                continue;
1041
            }
1042
1043
            $action = file_exists($target) ? 'update' : 'install';
1044
1045
            // copy action
1046
            if ($this->dircopy($item['tmp'], $target)) {
1047
                // return info
1048
                $id = $item['base'];
1049
                if ($item['type'] == 'template') {
1050
                    $id = 'template:'.$id;
1051
                }
1052
                $installed_extensions[$id] = array(
1053
                    'base' => $item['base'],
1054
                    'type' => $item['type'],
1055
                    'action' => $action
1056
                );
1057
            } else {
1058
                throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, '<bdi>'.$item['base'].'</bdi>'));
1059
            }
1060
        }
1061
1062
        // cleanup
1063
        if ($tmp) io_rmdir($tmp, true);
1064
1065
        return $installed_extensions;
1066
    }
1067
1068
    /**
1069
     * Find out what was in the extracted directory
1070
     *
1071
     * Correct folders are searched recursively using the "*.info.txt" configs
1072
     * as indicator for a root folder. When such a file is found, it's base
1073
     * setting is used (when set). All folders found by this method are stored
1074
     * in the 'new' key of the $result array.
1075
     *
1076
     * For backwards compatibility all found top level folders are stored as
1077
     * in the 'old' key of the $result array.
1078
     *
1079
     * When no items are found in 'new' the copy mechanism should fall back
1080
     * the 'old' list.
1081
     *
1082
     * @author Andreas Gohr <[email protected]>
1083
     * @param array $result - results are stored here
1084
     * @param string $directory - the temp directory where the package was unpacked to
1085
     * @param string $default_type - type used if no info.txt available
1086
     * @param string $subdir - a subdirectory. do not set. used by recursion
1087
     * @return bool - false on error
1088
     */
1089
    protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '')
1090
    {
1091
        $this_dir = "$directory$subdir";
1092
        $dh       = @opendir($this_dir);
1093
        if (!$dh) return false;
1094
1095
        $found_dirs           = array();
1096
        $found_files          = 0;
1097
        $found_template_parts = 0;
1098
        while (false !== ($f = readdir($dh))) {
1099
            if ($f == '.' || $f == '..') continue;
1100
1101
            if (is_dir("$this_dir/$f")) {
1102
                $found_dirs[] = "$subdir/$f";
1103
            } else {
1104
                // it's a file -> check for config
1105
                $found_files++;
1106
                switch ($f) {
1107
                    case 'plugin.info.txt':
1108
                    case 'template.info.txt':
1109
                        // we have  found a clear marker, save and return
1110
                        $info = array();
1111
                        $type = explode('.', $f, 2);
1112
                        $info['type'] = $type[0];
1113
                        $info['tmp']  = $this_dir;
1114
                        $conf = confToHash("$this_dir/$f");
1115
                        $info['base'] = basename($conf['base']);
1116
                        $result['new'][] = $info;
1117
                        return true;
1118
1119
                    case 'main.php':
1120
                    case 'details.php':
1121
                    case 'mediamanager.php':
1122
                    case 'style.ini':
1123
                        $found_template_parts++;
1124
                        break;
1125
                }
1126
            }
1127
        }
1128
        closedir($dh);
1129
1130
        // files where found but no info.txt - use old method
1131
        if ($found_files) {
1132
            $info            = array();
1133
            $info['tmp']     = $this_dir;
1134
            // does this look like a template or should we use the default type?
1135
            if ($found_template_parts >= 2) {
1136
                $info['type']    = 'template';
1137
            } else {
1138
                $info['type']    = $default_type;
1139
            }
1140
1141
            $result['old'][] = $info;
1142
            return true;
1143
        }
1144
1145
        // we have no files yet -> recurse
1146
        foreach ($found_dirs as $found_dir) {
1147
            $this->findFolders($result, $directory, $default_type, "$found_dir");
1148
        }
1149
        return true;
1150
    }
1151
1152
    /**
1153
     * Decompress a given file to the given target directory
1154
     *
1155
     * Determines the compression type from the file extension
1156
     *
1157
     * @param string $file   archive to extract
1158
     * @param string $target directory to extract to
1159
     * @throws Exception
1160
     * @return bool
1161
     */
1162
    private function decompress($file, $target)
1163
    {
1164
        // decompression library doesn't like target folders ending in "/"
1165
        if (substr($target, -1) == "/") $target = substr($target, 0, -1);
1166
1167
        $ext = $this->guessArchiveType($file);
1168
        if (in_array($ext, array('tar', 'bz', 'gz'))) {
1169
            try {
1170
                $tar = new \splitbrain\PHPArchive\Tar();
1171
                $tar->open($file);
1172
                $tar->extract($target);
1173
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1174
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1175
            }
1176
1177
            return true;
1178
        } elseif ($ext == 'zip') {
1179
            try {
1180
                $zip = new \splitbrain\PHPArchive\Zip();
1181
                $zip->open($file);
1182
                $zip->extract($target);
1183
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1184
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1185
            }
1186
1187
            return true;
1188
        }
1189
1190
        // the only case when we don't get one of the recognized archive types is when the archive file can't be read
1191
        throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
1192
    }
1193
1194
    /**
1195
     * Determine the archive type of the given file
1196
     *
1197
     * Reads the first magic bytes of the given file for content type guessing,
1198
     * if neither bz, gz or zip are recognized, tar is assumed.
1199
     *
1200
     * @author Andreas Gohr <[email protected]>
1201
     * @param string $file The file to analyze
1202
     * @return string|false false if the file can't be read, otherwise an "extension"
1203
     */
1204
    private function guessArchiveType($file)
1205
    {
1206
        $fh = fopen($file, 'rb');
1207
        if (!$fh) return false;
1208
        $magic = fread($fh, 5);
1209
        fclose($fh);
1210
1211
        if (strpos($magic, "\x42\x5a") === 0) return 'bz';
1212
        if (strpos($magic, "\x1f\x8b") === 0) return 'gz';
1213
        if (strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
1214
        return 'tar';
1215
    }
1216
1217
    /**
1218
     * Copy with recursive sub-directory support
1219
     *
1220
     * @param string $src filename path to file
1221
     * @param string $dst filename path to file
1222
     * @return bool|int|string
1223
     */
1224
    private function dircopy($src, $dst)
1225
    {
1226
        global $conf;
1227
1228
        if (is_dir($src)) {
1229
            if (!$dh = @opendir($src)) return false;
1230
1231
            if ($ok = io_mkdir_p($dst)) {
1232
                while ($ok && (false !== ($f = readdir($dh)))) {
1233
                    if ($f == '..' || $f == '.') continue;
1234
                    $ok = $this->dircopy("$src/$f", "$dst/$f");
1235
                }
1236
            }
1237
1238
            closedir($dh);
1239
            return $ok;
1240
        } else {
1241
            $exists = file_exists($dst);
1242
1243
            if (!@copy($src, $dst)) return false;
1244
            if (!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']);
1245
            @touch($dst, filemtime($src));
1246
        }
1247
1248
        return true;
1249
    }
1250
1251
    /**
1252
     * Delete outdated files from updated plugins
1253
     *
1254
     * @param array $installed
1255
     */
1256
    private function removeDeletedfiles($installed)
1257
    {
1258
        foreach ($installed as $id => $extension) {
1259
            // only on update
1260
            if ($extension['action'] == 'install') continue;
1261
1262
            // get definition file
1263
            if ($extension['type'] == 'template') {
1264
                $extensiondir = $this->tpllib;
1265
            } else {
1266
                $extensiondir = DOKU_PLUGIN;
1267
            }
1268
            $extensiondir = $extensiondir . $extension['base'] .'/';
1269
            $definitionfile = $extensiondir . 'deleted.files';
1270
            if (!file_exists($definitionfile)) continue;
1271
1272
            // delete the old files
1273
            $list = file($definitionfile);
1274
1275
            foreach ($list as $line) {
1276
                $line = trim(preg_replace('/#.*$/', '', $line));
1277
                if (!$line) continue;
1278
                $file = $extensiondir . $line;
1279
                if (!file_exists($file)) continue;
1280
1281
                io_rmdir($file, true);
1282
            }
1283
        }
1284
    }
1285
}
1286
1287
// vim:ts=4:sw=4:et:
1288