Issues (847)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

lib/plugins/extension/helper/extension.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
This use statement conflicts with another class in this namespace, DokuHTTPClient.

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
10
use dokuwiki\Extension\PluginController;
11
12
/**
13
 * Class helper_plugin_extension_extension represents a single extension (plugin or template)
14
 */
15
class helper_plugin_extension_extension extends DokuWiki_Plugin
16
{
17
    private $id;
18
    private $base;
19
    private $is_template = false;
20
    private $localInfo;
21
    private $remoteInfo;
22
    private $managerData;
23
    /** @var helper_plugin_extension_repository $repository */
24
    private $repository = null;
25
26
    /** @var array list of temporary directories */
27
    private $temporary = array();
28
29
    /** @var string where templates are installed to */
30
    private $tpllib = '';
31
32
    /**
33
     * helper_plugin_extension_extension constructor.
34
     */
35
    public function __construct()
36
    {
37
        $this->tpllib = dirname(tpl_incdir()).'/';
38
    }
39
40
    /**
41
     * Destructor
42
     *
43
     * deletes any dangling temporary directories
44
     */
45
    public function __destruct()
46
    {
47
        foreach ($this->temporary as $dir) {
48
            io_rmdir($dir, true);
49
        }
50
    }
51
52
    /**
53
     * @return bool false, this component is not a singleton
54
     */
55
    public function isSingleton()
56
    {
57
        return false;
58
    }
59
60
    /**
61
     * Set the name of the extension this instance shall represents, triggers loading the local and remote data
62
     *
63
     * @param string $id  The id of the extension (prefixed with template: for templates)
64
     * @return bool If some (local or remote) data was found
65
     */
66
    public function setExtension($id)
67
    {
68
        $id = cleanID($id);
69
        $this->id   = $id;
70
        $this->base = $id;
71
72
        if (substr($id, 0, 9) == 'template:') {
73
            $this->base = substr($id, 9);
74
            $this->is_template = true;
75
        } else {
76
            $this->is_template = false;
77
        }
78
79
        $this->localInfo = array();
80
        $this->managerData = array();
81
        $this->remoteInfo = array();
82
83
        if ($this->isInstalled()) {
84
            $this->readLocalData();
85
            $this->readManagerData();
86
        }
87
88
        if ($this->repository == null) {
89
            $this->repository = $this->loadHelper('extension_repository');
90
        }
91
92
        $this->remoteInfo = $this->repository->getData($this->getID());
93
94
        return ($this->localInfo || $this->remoteInfo);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->localInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
95
    }
96
97
    /**
98
     * If the extension is installed locally
99
     *
100
     * @return bool If the extension is installed locally
101
     */
102
    public function isInstalled()
103
    {
104
        return is_dir($this->getInstallDir());
105
    }
106
107
    /**
108
     * If the extension is under git control
109
     *
110
     * @return bool
111
     */
112
    public function isGitControlled()
113
    {
114
        if (!$this->isInstalled()) return false;
115
        return is_dir($this->getInstallDir().'/.git');
116
    }
117
118
    /**
119
     * If the extension is bundled
120
     *
121
     * @return bool If the extension is bundled
122
     */
123
    public function isBundled()
124
    {
125
        if (!empty($this->remoteInfo['bundled'])) return $this->remoteInfo['bundled'];
126
        return in_array(
127
            $this->id,
128
            array(
129
                'authad', 'authldap', 'authpdo', 'authplain',
130
                'acl', 'config', 'extension', 'info', 'popularity', 'revert',
131
                'safefnrecode', 'styling', 'testing', 'usermanager', 'logviewer',
132
                'template:dokuwiki',
133
            )
134
        );
135
    }
136
137
    /**
138
     * If the extension is protected against any modification (disable/uninstall)
139
     *
140
     * @return bool if the extension is protected
141
     */
142
    public function isProtected()
143
    {
144
        // never allow deinstalling the current auth plugin:
145
        global $conf;
146
        if ($this->id == $conf['authtype']) return true;
147
148
        /** @var PluginController $plugin_controller */
149
        global $plugin_controller;
150
        $cascade = $plugin_controller->getCascade();
151
        return (isset($cascade['protected'][$this->id]) && $cascade['protected'][$this->id]);
152
    }
153
154
    /**
155
     * If the extension is installed in the correct directory
156
     *
157
     * @return bool If the extension is installed in the correct directory
158
     */
159
    public function isInWrongFolder()
160
    {
161
        return $this->base != $this->getBase();
162
    }
163
164
    /**
165
     * If the extension is enabled
166
     *
167
     * @return bool If the extension is enabled
168
     */
169
    public function isEnabled()
170
    {
171
        global $conf;
172
        if ($this->isTemplate()) {
173
            return ($conf['template'] == $this->getBase());
174
        }
175
176
        /* @var PluginController $plugin_controller */
177
        global $plugin_controller;
178
        return $plugin_controller->isEnabled($this->base);
179
    }
180
181
    /**
182
     * If the extension should be updated, i.e. if an updated version is available
183
     *
184
     * @return bool If an update is available
185
     */
186
    public function updateAvailable()
187
    {
188
        if (!$this->isInstalled()) return false;
189
        if ($this->isBundled()) return false;
190
        $lastupdate = $this->getLastUpdate();
191
        if ($lastupdate === false) return false;
192
        $installed  = $this->getInstalledVersion();
193
        if ($installed === false || $installed === $this->getLang('unknownversion')) return true;
194
        return $this->getInstalledVersion() < $this->getLastUpdate();
195
    }
196
197
    /**
198
     * If the extension is a template
199
     *
200
     * @return bool If this extension is a template
201
     */
202
    public function isTemplate()
203
    {
204
        return $this->is_template;
205
    }
206
207
    /**
208
     * Get the ID of the extension
209
     *
210
     * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:'
211
     *
212
     * @return string
213
     */
214
    public function getID()
215
    {
216
        return $this->id;
217
    }
218
219
    /**
220
     * Get the name of the installation directory
221
     *
222
     * @return string The name of the installation directory
223
     */
224
    public function getInstallName()
225
    {
226
        return $this->base;
227
    }
228
229
    // Data from plugin.info.txt/template.info.txt or the repo when not available locally
230
    /**
231
     * Get the basename of the extension
232
     *
233
     * @return string The basename
234
     */
235
    public function getBase()
236
    {
237
        if (!empty($this->localInfo['base'])) return $this->localInfo['base'];
238
        return $this->base;
239
    }
240
241
    /**
242
     * Get the display name of the extension
243
     *
244
     * @return string The display name
245
     */
246
    public function getDisplayName()
247
    {
248
        if (!empty($this->localInfo['name'])) return $this->localInfo['name'];
249
        if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name'];
250
        return $this->base;
251
    }
252
253
    /**
254
     * Get the author name of the extension
255
     *
256
     * @return string|bool The name of the author or false if there is none
257
     */
258
    public function getAuthor()
259
    {
260
        if (!empty($this->localInfo['author'])) return $this->localInfo['author'];
261
        if (!empty($this->remoteInfo['author'])) return $this->remoteInfo['author'];
262
        return false;
263
    }
264
265
    /**
266
     * Get the email of the author of the extension if there is any
267
     *
268
     * @return string|bool The email address or false if there is none
269
     */
270
    public function getEmail()
271
    {
272
        // email is only in the local data
273
        if (!empty($this->localInfo['email'])) return $this->localInfo['email'];
274
        return false;
275
    }
276
277
    /**
278
     * Get the email id, i.e. the md5sum of the email
279
     *
280
     * @return string|bool The md5sum of the email if there is any, false otherwise
281
     */
282
    public function getEmailID()
283
    {
284
        if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
285
        if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
286
        return false;
287
    }
288
289
    /**
290
     * Get the description of the extension
291
     *
292
     * @return string The description
293
     */
294
    public function getDescription()
295
    {
296
        if (!empty($this->localInfo['desc'])) return $this->localInfo['desc'];
297
        if (!empty($this->remoteInfo['description'])) return $this->remoteInfo['description'];
298
        return '';
299
    }
300
301
    /**
302
     * Get the URL of the extension, usually a page on dokuwiki.org
303
     *
304
     * @return string The URL
305
     */
306
    public function getURL()
307
    {
308
        if (!empty($this->localInfo['url'])) return $this->localInfo['url'];
309
        return 'https://www.dokuwiki.org/'.
310
            ($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
311
    }
312
313
    /**
314
     * Get the installed version of the extension
315
     *
316
     * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
317
     */
318
    public function getInstalledVersion()
319
    {
320
        if (!empty($this->localInfo['date'])) return $this->localInfo['date'];
321
        if ($this->isInstalled()) return $this->getLang('unknownversion');
322
        return false;
323
    }
324
325
    /**
326
     * Get the install date of the current version
327
     *
328
     * @return string|bool The date of the last update or false if not available
329
     */
330
    public function getUpdateDate()
331
    {
332
        if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
333
        return $this->getInstallDate();
334
    }
335
336
    /**
337
     * Get the date of the installation of the plugin
338
     *
339
     * @return string|bool The date of the installation or false if not available
340
     */
341
    public function getInstallDate()
342
    {
343
        if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
344
        return false;
345
    }
346
347
    /**
348
     * Get the names of the dependencies of this extension
349
     *
350
     * @return array The base names of the dependencies
351
     */
352
    public function getDependencies()
353
    {
354
        if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies'];
355
        return array();
356
    }
357
358
    /**
359
     * Get the names of the missing dependencies
360
     *
361
     * @return array The base names of the missing dependencies
362
     */
363
    public function getMissingDependencies()
364
    {
365
        /* @var PluginController $plugin_controller */
366
        global $plugin_controller;
367
        $dependencies = $this->getDependencies();
368
        $missing_dependencies = array();
369
        foreach ($dependencies as $dependency) {
370
            if (!$plugin_controller->isEnabled($dependency)) {
371
                $missing_dependencies[] = $dependency;
372
            }
373
        }
374
        return $missing_dependencies;
375
    }
376
377
    /**
378
     * Get the names of all conflicting extensions
379
     *
380
     * @return array The names of the conflicting extensions
381
     */
382
    public function getConflicts()
383
    {
384
        if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['conflicts'];
385
        return array();
386
    }
387
388
    /**
389
     * Get the names of similar extensions
390
     *
391
     * @return array The names of similar extensions
392
     */
393
    public function getSimilarExtensions()
394
    {
395
        if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar'];
396
        return array();
397
    }
398
399
    /**
400
     * Get the names of the tags of the extension
401
     *
402
     * @return array The names of the tags of the extension
403
     */
404
    public function getTags()
405
    {
406
        if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags'];
407
        return array();
408
    }
409
410
    /**
411
     * Get the popularity information as floating point number [0,1]
412
     *
413
     * @return float|bool The popularity information or false if it isn't available
414
     */
415
    public function getPopularity()
416
    {
417
        if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity'];
418
        return false;
419
    }
420
421
422
    /**
423
     * Get the text of the security warning if there is any
424
     *
425
     * @return string|bool The security warning if there is any, false otherwise
426
     */
427
    public function getSecurityWarning()
428
    {
429
        if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning'];
430
        return false;
431
    }
432
433
    /**
434
     * Get the text of the security issue if there is any
435
     *
436
     * @return string|bool The security issue if there is any, false otherwise
437
     */
438
    public function getSecurityIssue()
439
    {
440
        if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue'];
441
        return false;
442
    }
443
444
    /**
445
     * Get the URL of the screenshot of the extension if there is any
446
     *
447
     * @return string|bool The screenshot URL if there is any, false otherwise
448
     */
449
    public function getScreenshotURL()
450
    {
451
        if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl'];
452
        return false;
453
    }
454
455
    /**
456
     * Get the URL of the thumbnail of the extension if there is any
457
     *
458
     * @return string|bool The thumbnail URL if there is any, false otherwise
459
     */
460
    public function getThumbnailURL()
461
    {
462
        if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl'];
463
        return false;
464
    }
465
    /**
466
     * Get the last used download URL of the extension if there is any
467
     *
468
     * @return string|bool The previously used download URL, false if the extension has been installed manually
469
     */
470
    public function getLastDownloadURL()
471
    {
472
        if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
473
        return false;
474
    }
475
476
    /**
477
     * Get the download URL of the extension if there is any
478
     *
479
     * @return string|bool The download URL if there is any, false otherwise
480
     */
481
    public function getDownloadURL()
482
    {
483
        if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl'];
484
        return false;
485
    }
486
487
    /**
488
     * If the download URL has changed since the last download
489
     *
490
     * @return bool If the download URL has changed
491
     */
492
    public function hasDownloadURLChanged()
493
    {
494
        $lasturl = $this->getLastDownloadURL();
495
        $currenturl = $this->getDownloadURL();
496
        return ($lasturl && $currenturl && $lasturl != $currenturl);
497
    }
498
499
    /**
500
     * Get the bug tracker URL of the extension if there is any
501
     *
502
     * @return string|bool The bug tracker URL if there is any, false otherwise
503
     */
504
    public function getBugtrackerURL()
505
    {
506
        if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker'];
507
        return false;
508
    }
509
510
    /**
511
     * Get the URL of the source repository if there is any
512
     *
513
     * @return string|bool The URL of the source repository if there is any, false otherwise
514
     */
515
    public function getSourcerepoURL()
516
    {
517
        if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo'];
518
        return false;
519
    }
520
521
    /**
522
     * Get the donation URL of the extension if there is any
523
     *
524
     * @return string|bool The donation URL if there is any, false otherwise
525
     */
526
    public function getDonationURL()
527
    {
528
        if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl'];
529
        return false;
530
    }
531
532
    /**
533
     * Get the extension type(s)
534
     *
535
     * @return array The type(s) as array of strings
536
     */
537
    public function getTypes()
538
    {
539
        if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types'];
540
        if ($this->isTemplate()) return array(32 => 'template');
541
        return array();
542
    }
543
544
    /**
545
     * Get a list of all DokuWiki versions this extension is compatible with
546
     *
547
     * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
548
     */
549
    public function getCompatibleVersions()
550
    {
551
        if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible'];
552
        return array();
553
    }
554
555
    /**
556
     * Get the date of the last available update
557
     *
558
     * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
559
     */
560
    public function getLastUpdate()
561
    {
562
        if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate'];
563
        return false;
564
    }
565
566
    /**
567
     * Get the base path of the extension
568
     *
569
     * @return string The base path of the extension
570
     */
571
    public function getInstallDir()
572
    {
573
        if ($this->isTemplate()) {
574
            return $this->tpllib.$this->base;
575
        } else {
576
            return DOKU_PLUGIN.$this->base;
577
        }
578
    }
579
580
    /**
581
     * The type of extension installation
582
     *
583
     * @return string One of "none", "manual", "git" or "automatic"
584
     */
585
    public function getInstallType()
586
    {
587
        if (!$this->isInstalled()) return 'none';
588
        if (!empty($this->managerData)) return 'automatic';
589
        if (is_dir($this->getInstallDir().'/.git')) return 'git';
590
        return 'manual';
591
    }
592
593
    /**
594
     * If the extension can probably be installed/updated or uninstalled
595
     *
596
     * @return bool|string True or error string
597
     */
598
    public function canModify()
599
    {
600
        if ($this->isInstalled()) {
601
            if (!is_writable($this->getInstallDir())) {
602
                return 'noperms';
603
            }
604
        }
605
606
        if ($this->isTemplate() && !is_writable($this->tpllib)) {
607
            return 'notplperms';
608
        } elseif (!is_writable(DOKU_PLUGIN)) {
609
            return 'nopluginperms';
610
        }
611
        return true;
612
    }
613
614
    /**
615
     * Install an extension from a user upload
616
     *
617
     * @param string $field name of the upload file
618
     * @param boolean $overwrite overwrite folder if the extension name is the same
619
     * @throws Exception when something goes wrong
620
     * @return array The list of installed extensions
621
     */
622
    public function installFromUpload($field, $overwrite = true)
623
    {
624
        if ($_FILES[$field]['error']) {
625
            throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
626
        }
627
628
        $tmp = $this->mkTmpDir();
629
        if (!$tmp) throw new Exception($this->getLang('error_dircreate'));
0 ignored issues
show
Bug Best Practice introduced by
The expression $tmp of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
630
631
        // filename may contain the plugin name for old style plugins...
632
        $basename = basename($_FILES[$field]['name']);
633
        $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
634
        $basename = preg_replace('/[\W]+/', '', $basename);
635
636
        if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
637
            throw new Exception($this->getLang('msg_upload_failed'));
638
        }
639
640
        try {
641
            $installed = $this->installArchive("$tmp/upload.archive", $overwrite, $basename);
642
            $this->updateManagerData('', $installed);
643
            $this->removeDeletedfiles($installed);
644
            // purge cache
645
            $this->purgeCache();
646
        } catch (Exception $e) {
647
            throw $e;
648
        }
649
        return $installed;
650
    }
651
652
    /**
653
     * Install an extension from a remote URL
654
     *
655
     * @param string $url
656
     * @param boolean $overwrite overwrite folder if the extension name is the same
657
     * @throws Exception when something goes wrong
658
     * @return array The list of installed extensions
659
     */
660
    public function installFromURL($url, $overwrite = true)
661
    {
662
        try {
663
            $path      = $this->download($url);
664
            $installed = $this->installArchive($path, $overwrite);
665
            $this->updateManagerData($url, $installed);
666
            $this->removeDeletedfiles($installed);
667
668
            // purge cache
669
            $this->purgeCache();
670
        } catch (Exception $e) {
671
            throw $e;
672
        }
673
        return $installed;
674
    }
675
676
    /**
677
     * Install or update the extension
678
     *
679
     * @throws \Exception when something goes wrong
680
     * @return array The list of installed extensions
681
     */
682
    public function installOrUpdate()
683
    {
684
        $url       = $this->getDownloadURL();
685
        $path      = $this->download($url);
686
        $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
687
        $this->updateManagerData($url, $installed);
688
689
        // refresh extension information
690
        if (!isset($installed[$this->getID()])) {
691
            throw new Exception('Error, the requested extension hasn\'t been installed or updated');
692
        }
693
        $this->removeDeletedfiles($installed);
694
        $this->setExtension($this->getID());
695
        $this->purgeCache();
696
        return $installed;
697
    }
698
699
    /**
700
     * Uninstall the extension
701
     *
702
     * @return bool If the plugin was sucessfully uninstalled
703
     */
704
    public function uninstall()
705
    {
706
        $this->purgeCache();
707
        return io_rmdir($this->getInstallDir(), true);
708
    }
709
710
    /**
711
     * Enable the extension
712
     *
713
     * @return bool|string True or an error message
714
     */
715
    public function enable()
716
    {
717
        if ($this->isTemplate()) return $this->getLang('notimplemented');
718
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
719
        if ($this->isEnabled()) return $this->getLang('alreadyenabled');
720
721
        /* @var PluginController $plugin_controller */
722
        global $plugin_controller;
723
        if ($plugin_controller->enable($this->base)) {
724
            $this->purgeCache();
725
            return true;
726
        } else {
727
            return $this->getLang('pluginlistsaveerror');
728
        }
729
    }
730
731
    /**
732
     * Disable the extension
733
     *
734
     * @return bool|string True or an error message
735
     */
736
    public function disable()
737
    {
738
        if ($this->isTemplate()) return $this->getLang('notimplemented');
739
740
        /* @var PluginController $plugin_controller */
741
        global $plugin_controller;
742
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
743
        if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
744
        if ($plugin_controller->disable($this->base)) {
745
            $this->purgeCache();
746
            return true;
747
        } else {
748
            return $this->getLang('pluginlistsaveerror');
749
        }
750
    }
751
752
    /**
753
     * Purge the cache by touching the main configuration file
754
     */
755
    protected function purgeCache()
756
    {
757
        global $config_cascade;
758
759
        // expire dokuwiki caches
760
        // touching local.php expires wiki page, JS and CSS caches
761
        @touch(reset($config_cascade['main']['local']));
762
    }
763
764
    /**
765
     * Read local extension data either from info.txt or getInfo()
766
     */
767
    protected function readLocalData()
768
    {
769
        if ($this->isTemplate()) {
770
            $infopath = $this->getInstallDir().'/template.info.txt';
771
        } else {
772
            $infopath = $this->getInstallDir().'/plugin.info.txt';
773
        }
774
775
        if (is_readable($infopath)) {
776
            $this->localInfo = confToHash($infopath);
777
        } elseif (!$this->isTemplate() && $this->isEnabled()) {
778
            $path   = $this->getInstallDir().'/';
779
            $plugin = null;
780
781
            foreach (PluginController::PLUGIN_TYPES as $type) {
782
                if (file_exists($path.$type.'.php')) {
783
                    $plugin = plugin_load($type, $this->base);
784
                    if ($plugin) break;
785
                }
786
787
                if ($dh = @opendir($path.$type.'/')) {
788
                    while (false !== ($cp = readdir($dh))) {
789
                        if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
790
791
                        $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
792
                        if ($plugin) break;
793
                    }
794
                    if ($plugin) break;
795
                    closedir($dh);
796
                }
797
            }
798
799
            if ($plugin) {
800
                /* @var DokuWiki_Plugin $plugin */
801
                $this->localInfo = $plugin->getInfo();
802
            }
803
        }
804
    }
805
806
    /**
807
     * Save the given URL and current datetime in the manager.dat file of all installed extensions
808
     *
809
     * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
810
     * @param array  $installed Optional list of installed plugins
811
     */
812
    protected function updateManagerData($url = '', $installed = null)
813
    {
814
        $origID = $this->getID();
815
816
        if (is_null($installed)) {
817
            $installed = array($origID);
818
        }
819
820
        foreach ($installed as $ext => $info) {
821
            if ($this->getID() != $ext) $this->setExtension($ext);
822
            if ($url) {
823
                $this->managerData['downloadurl'] = $url;
824
            } elseif (isset($this->managerData['downloadurl'])) {
825
                unset($this->managerData['downloadurl']);
826
            }
827
            if (isset($this->managerData['installed'])) {
828
                $this->managerData['updated'] = date('r');
829
            } else {
830
                $this->managerData['installed'] = date('r');
831
            }
832
            $this->writeManagerData();
833
        }
834
835
        if ($this->getID() != $origID) $this->setExtension($origID);
836
    }
837
838
    /**
839
     * Read the manager.dat file
840
     */
841
    protected function readManagerData()
842
    {
843
        $managerpath = $this->getInstallDir().'/manager.dat';
844
        if (is_readable($managerpath)) {
845
            $file = @file($managerpath);
846
            if (!empty($file)) {
847
                foreach ($file as $line) {
848
                    list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
849
                    $key = trim($key);
850
                    $value = trim($value);
851
                    // backwards compatible with old plugin manager
852
                    if ($key == 'url') $key = 'downloadurl';
853
                    $this->managerData[$key] = $value;
854
                }
855
            }
856
        }
857
    }
858
859
    /**
860
     * Write the manager.data file
861
     */
862
    protected function writeManagerData()
863
    {
864
        $managerpath = $this->getInstallDir().'/manager.dat';
865
        $data = '';
866
        foreach ($this->managerData as $k => $v) {
867
            $data .= $k.'='.$v.DOKU_LF;
868
        }
869
        io_saveFile($managerpath, $data);
870
    }
871
872
    /**
873
     * Returns a temporary directory
874
     *
875
     * The directory is registered for cleanup when the class is destroyed
876
     *
877
     * @return false|string
878
     */
879
    protected function mkTmpDir()
880
    {
881
        $dir = io_mktmpdir();
882
        if (!$dir) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $dir of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

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

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

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

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