Completed
Push — remoteapiGetversions ( 33fe8b...b2f4ab )
by Gerrit
12s
created

helper_plugin_extension_extension::getURL()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 3
eloc 3
nc 3
nop 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
// must be run within Dokuwiki
10
if(!defined('DOKU_INC')) die();
11
if(!defined('DOKU_TPLLIB')) define('DOKU_TPLLIB', DOKU_INC.'lib/tpl/');
12
13
/**
14
 * Class helper_plugin_extension_extension represents a single extension (plugin or template)
15
 */
16
class helper_plugin_extension_extension extends DokuWiki_Plugin {
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
    /**
30
     * Destructor
31
     *
32
     * deletes any dangling temporary directories
33
     */
34
    public function __destruct() {
35
        foreach($this->temporary as $dir){
36
            io_rmdir($dir, true);
37
        }
38
    }
39
40
    /**
41
     * @return bool false, this component is not a singleton
42
     */
43
    public function isSingleton() {
44
        return false;
45
    }
46
47
    /**
48
     * Set the name of the extension this instance shall represents, triggers loading the local and remote data
49
     *
50
     * @param string $id  The id of the extension (prefixed with template: for templates)
51
     * @return bool If some (local or remote) data was found
52
     */
53
    public function setExtension($id) {
54
        $this->id   = $id;
55
        $this->base = $id;
56
57
        if(substr($id, 0 , 9) == 'template:'){
58
            $this->base = substr($id, 9);
59
            $this->is_template = true;
60
        } else {
61
            $this->is_template = false;
62
        }
63
64
        $this->localInfo = array();
65
        $this->managerData = array();
66
        $this->remoteInfo = array();
67
68
        if ($this->isInstalled()) {
69
            $this->readLocalData();
70
            $this->readManagerData();
71
        }
72
73
        if ($this->repository == null) {
74
            $this->repository = $this->loadHelper('extension_repository');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->loadHelper('extension_repository') can also be of type object<DokuWiki_Plugin>. However, the property $repository is declared as type object<helper_plugin_extension_repository>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
75
        }
76
77
        $this->remoteInfo = $this->repository->getData($this->getID());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class DokuWiki_Plugin as the method getData() does only exist in the following sub-classes of DokuWiki_Plugin: helper_plugin_extension_repository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

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

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
693
    }
694
695
    /**
696
     * Read local extension data either from info.txt or getInfo()
697
     */
698
    protected function readLocalData() {
699
        if ($this->isTemplate()) {
700
            $infopath = $this->getInstallDir().'/template.info.txt';
701
        } else {
702
            $infopath = $this->getInstallDir().'/plugin.info.txt';
703
        }
704
705
        if (is_readable($infopath)) {
706
            $this->localInfo = confToHash($infopath);
707
        } elseif (!$this->isTemplate() && $this->isEnabled()) {
708
            global $plugin_types;
709
            $path       = $this->getInstallDir().'/';
710
            $plugin     = null;
711
712
            foreach($plugin_types as $type) {
713
                if(file_exists($path.$type.'.php')) {
714
                    $plugin = plugin_load($type, $this->base);
715
                    if ($plugin) break;
716
                }
717
718
                if($dh = @opendir($path.$type.'/')) {
719
                    while(false !== ($cp = readdir($dh))) {
720
                        if($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
721
722
                        $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
723
                        if ($plugin) break;
724
                    }
725
                    if ($plugin) break;
726
                    closedir($dh);
727
                }
728
            }
729
730
            if ($plugin) {
731
                /* @var DokuWiki_Plugin $plugin */
732
                $this->localInfo = $plugin->getInfo();
733
            }
734
        }
735
    }
736
737
    /**
738
     * Save the given URL and current datetime in the manager.dat file of all installed extensions
739
     *
740
     * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
741
     * @param array  $installed Optional list of installed plugins
742
     */
743
    protected function updateManagerData($url = '', $installed = null) {
744
        $origID = $this->getID();
745
746
        if(is_null($installed)) {
747
            $installed = array($origID);
748
        }
749
750
        foreach($installed as $ext => $info) {
751
            if($this->getID() != $ext) $this->setExtension($ext);
752
            if($url) {
753
                $this->managerData['downloadurl'] = $url;
754
            } elseif(isset($this->managerData['downloadurl'])) {
755
                unset($this->managerData['downloadurl']);
756
            }
757
            if(isset($this->managerData['installed'])) {
758
                $this->managerData['updated'] = date('r');
759
            } else {
760
                $this->managerData['installed'] = date('r');
761
            }
762
            $this->writeManagerData();
763
        }
764
765
        if($this->getID() != $origID) $this->setExtension($origID);
766
    }
767
768
    /**
769
     * Read the manager.dat file
770
     */
771
    protected function readManagerData() {
772
        $managerpath = $this->getInstallDir().'/manager.dat';
773
        if (is_readable($managerpath)) {
774
            $file = @file($managerpath);
775
            if(!empty($file)) {
776
                foreach($file as $line) {
777
                    list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
778
                    $key = trim($key);
779
                    $value = trim($value);
780
                    // backwards compatible with old plugin manager
781
                    if($key == 'url') $key = 'downloadurl';
782
                    $this->managerData[$key] = $value;
783
                }
784
            }
785
        }
786
    }
787
788
    /**
789
     * Write the manager.data file
790
     */
791
    protected function writeManagerData() {
792
        $managerpath = $this->getInstallDir().'/manager.dat';
793
        $data = '';
794
        foreach ($this->managerData as $k => $v) {
795
            $data .= $k.'='.$v.DOKU_LF;
796
        }
797
        io_saveFile($managerpath, $data);
798
    }
799
800
    /**
801
     * Returns a temporary directory
802
     *
803
     * The directory is registered for cleanup when the class is destroyed
804
     *
805
     * @return false|string
806
     */
807
    protected function mkTmpDir(){
808
        $dir = io_mktmpdir();
809
        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...
810
        $this->temporary[] = $dir;
811
        return $dir;
812
    }
813
814
    /**
815
     * Download an archive to a protected path
816
     *
817
     * @param string $url  The url to get the archive from
818
     * @throws Exception   when something goes wrong
819
     * @return string The path where the archive was saved
820
     */
821
    public function download($url) {
822
        // check the url
823
        if(!preg_match('/https?:\/\//i', $url)){
824
            throw new Exception($this->getLang('error_badurl'));
825
        }
826
827
        // try to get the file from the path (used as plugin name fallback)
828
        $file = parse_url($url, PHP_URL_PATH);
829
        if(is_null($file)){
830
            $file = md5($url);
831
        }else{
832
            $file = utf8_basename($file);
0 ignored issues
show
Security Bug introduced by
It seems like $file can also be of type false; however, utf8_basename() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
833
        }
834
835
        // create tmp directory for download
836
        if(!($tmp = $this->mkTmpDir())) {
837
            throw new Exception($this->getLang('error_dircreate'));
838
        }
839
840
        // download
841
        if(!$file = io_download($url, $tmp.'/', true, $file, 0)) {
842
            io_rmdir($tmp, true);
843
            throw new Exception(sprintf($this->getLang('error_download'), '<bdi>'.hsc($url).'</bdi>'));
844
        }
845
846
        return $tmp.'/'.$file;
847
    }
848
849
    /**
850
     * @param string $file      The path to the archive that shall be installed
851
     * @param bool   $overwrite If an already installed plugin should be overwritten
852
     * @param string $base      The basename of the plugin if it's known
853
     * @throws Exception        when something went wrong
854
     * @return array            list of installed extensions
855
     */
856
    public function installArchive($file, $overwrite=false, $base = '') {
857
        $installed_extensions = array();
858
859
        // create tmp directory for decompression
860
        if(!($tmp = $this->mkTmpDir())) {
861
            throw new Exception($this->getLang('error_dircreate'));
862
        }
863
864
        // add default base folder if specified to handle case where zip doesn't contain this
865
        if($base && !@mkdir($tmp.'/'.$base)) {
866
            throw new Exception($this->getLang('error_dircreate'));
867
        }
868
869
        // decompress
870
        $this->decompress($file, "$tmp/".$base);
871
872
        // search $tmp/$base for the folder(s) that has been created
873
        // move the folder(s) to lib/..
874
        $result = array('old'=>array(), 'new'=>array());
875
        $default = ($this->isTemplate() ? 'template' : 'plugin');
876
        if(!$this->find_folders($result, $tmp.'/'.$base, $default)) {
877
            throw new Exception($this->getLang('error_findfolder'));
878
        }
879
880
        // choose correct result array
881
        if(count($result['new'])) {
882
            $install = $result['new'];
883
        }else{
884
            $install = $result['old'];
885
        }
886
887
        if(!count($install)){
888
            throw new Exception($this->getLang('error_findfolder'));
889
        }
890
891
        // now install all found items
892
        foreach($install as $item) {
893
            // where to install?
894
            if($item['type'] == 'template') {
895
                $target_base_dir = DOKU_TPLLIB;
896
            }else{
897
                $target_base_dir = DOKU_PLUGIN;
898
            }
899
900
            if(!empty($item['base'])) {
901
                // use base set in info.txt
902
            } elseif($base && count($install) == 1) {
903
                $item['base'] = $base;
904
            } else {
905
                // default - use directory as found in zip
906
                // plugins from github/master without *.info.txt will install in wrong folder
907
                // but using $info->id will make 'code3' fail (which should install in lib/code/..)
908
                $item['base'] = basename($item['tmp']);
909
            }
910
911
            // check to make sure we aren't overwriting anything
912
            $target = $target_base_dir.$item['base'];
913
            if(!$overwrite && file_exists($target)) {
914
                // TODO remember our settings, ask the user to confirm overwrite
915
                continue;
916
            }
917
918
            $action = file_exists($target) ? 'update' : 'install';
919
920
            // copy action
921
            if($this->dircopy($item['tmp'], $target)) {
922
                // return info
923
                $id = $item['base'];
924
                if($item['type'] == 'template') {
925
                    $id = 'template:'.$id;
926
                }
927
                $installed_extensions[$id] = array(
928
                    'base' => $item['base'],
929
                    'type' => $item['type'],
930
                    'action' => $action
931
                );
932
            } else {
933
                throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, '<bdi>'.$item['base'].'</bdi>'));
934
            }
935
        }
936
937
        // cleanup
938
        if($tmp) io_rmdir($tmp, true);
939
940
        return $installed_extensions;
941
    }
942
943
    /**
944
     * Find out what was in the extracted directory
945
     *
946
     * Correct folders are searched recursively using the "*.info.txt" configs
947
     * as indicator for a root folder. When such a file is found, it's base
948
     * setting is used (when set). All folders found by this method are stored
949
     * in the 'new' key of the $result array.
950
     *
951
     * For backwards compatibility all found top level folders are stored as
952
     * in the 'old' key of the $result array.
953
     *
954
     * When no items are found in 'new' the copy mechanism should fall back
955
     * the 'old' list.
956
     *
957
     * @author Andreas Gohr <[email protected]>
958
     * @param array $result - results are stored here
959
     * @param string $directory - the temp directory where the package was unpacked to
960
     * @param string $default_type - type used if no info.txt available
961
     * @param string $subdir - a subdirectory. do not set. used by recursion
962
     * @return bool - false on error
963
     */
964
    protected function find_folders(&$result, $directory, $default_type='plugin', $subdir='') {
965
        $this_dir = "$directory$subdir";
966
        $dh       = @opendir($this_dir);
967
        if(!$dh) return false;
968
969
        $found_dirs           = array();
970
        $found_files          = 0;
971
        $found_template_parts = 0;
972
        while (false !== ($f = readdir($dh))) {
973
            if($f == '.' || $f == '..') continue;
974
975
            if(is_dir("$this_dir/$f")) {
976
                $found_dirs[] = "$subdir/$f";
977
978
            } else {
979
                // it's a file -> check for config
980
                $found_files++;
981
                switch ($f) {
982
                    case 'plugin.info.txt':
983
                    case 'template.info.txt':
984
                        // we have  found a clear marker, save and return
985
                        $info = array();
986
                        $type = explode('.', $f, 2);
987
                        $info['type'] = $type[0];
988
                        $info['tmp']  = $this_dir;
989
                        $conf = confToHash("$this_dir/$f");
990
                        $info['base'] = basename($conf['base']);
991
                        $result['new'][] = $info;
992
                        return true;
993
994
                    case 'main.php':
995
                    case 'details.php':
996
                    case 'mediamanager.php':
997
                    case 'style.ini':
998
                        $found_template_parts++;
999
                        break;
1000
                }
1001
            }
1002
        }
1003
        closedir($dh);
1004
1005
        // files where found but no info.txt - use old method
1006
        if($found_files){
1007
            $info            = array();
1008
            $info['tmp']     = $this_dir;
1009
            // does this look like a template or should we use the default type?
1010
            if($found_template_parts >= 2) {
1011
                $info['type']    = 'template';
1012
            } else {
1013
                $info['type']    = $default_type;
1014
            }
1015
1016
            $result['old'][] = $info;
1017
            return true;
1018
        }
1019
1020
        // we have no files yet -> recurse
1021
        foreach ($found_dirs as $found_dir) {
1022
            $this->find_folders($result, $directory, $default_type, "$found_dir");
1023
        }
1024
        return true;
1025
    }
1026
1027
    /**
1028
     * Decompress a given file to the given target directory
1029
     *
1030
     * Determines the compression type from the file extension
1031
     *
1032
     * @param string $file   archive to extract
1033
     * @param string $target directory to extract to
1034
     * @throws Exception
1035
     * @return bool
1036
     */
1037
    private function decompress($file, $target) {
1038
        // decompression library doesn't like target folders ending in "/"
1039
        if(substr($target, -1) == "/") $target = substr($target, 0, -1);
1040
1041
        $ext = $this->guess_archive($file);
1042
        if(in_array($ext, array('tar', 'bz', 'gz'))) {
1043
1044
            try {
1045
                $tar = new \splitbrain\PHPArchive\Tar();
1046
                $tar->open($file);
1047
                $tar->extract($target);
1048
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1049
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1050
            }
1051
1052
            return true;
1053
        } elseif($ext == 'zip') {
1054
1055
            try {
1056
                $zip = new \splitbrain\PHPArchive\Zip();
1057
                $zip->open($file);
1058
                $zip->extract($target);
1059
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1060
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1061
            }
1062
1063
            return true;
1064
        }
1065
1066
        // the only case when we don't get one of the recognized archive types is when the archive file can't be read
1067
        throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
1068
    }
1069
1070
    /**
1071
     * Determine the archive type of the given file
1072
     *
1073
     * Reads the first magic bytes of the given file for content type guessing,
1074
     * if neither bz, gz or zip are recognized, tar is assumed.
1075
     *
1076
     * @author Andreas Gohr <[email protected]>
1077
     * @param string $file The file to analyze
1078
     * @return string|false false if the file can't be read, otherwise an "extension"
1079
     */
1080
    private function guess_archive($file) {
1081
        $fh = fopen($file, 'rb');
1082
        if(!$fh) return false;
1083
        $magic = fread($fh, 5);
1084
        fclose($fh);
1085
1086
        if(strpos($magic, "\x42\x5a") === 0) return 'bz';
1087
        if(strpos($magic, "\x1f\x8b") === 0) return 'gz';
1088
        if(strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
1089
        return 'tar';
1090
    }
1091
1092
    /**
1093
     * Copy with recursive sub-directory support
1094
     *
1095
     * @param string $src filename path to file
1096
     * @param string $dst filename path to file
1097
     * @return bool|int|string
1098
     */
1099
    private function dircopy($src, $dst) {
1100
        global $conf;
1101
1102
        if(is_dir($src)) {
1103
            if(!$dh = @opendir($src)) return false;
1104
1105
            if($ok = io_mkdir_p($dst)) {
1106
                while ($ok && (false !== ($f = readdir($dh)))) {
1107
                    if($f == '..' || $f == '.') continue;
1108
                    $ok = $this->dircopy("$src/$f", "$dst/$f");
1109
                }
1110
            }
1111
1112
            closedir($dh);
1113
            return $ok;
1114
1115
        } else {
1116
            $exists = file_exists($dst);
1117
1118
            if(!@copy($src, $dst)) return false;
1119
            if(!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']);
1120
            @touch($dst, filemtime($src));
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1121
        }
1122
1123
        return true;
1124
    }
1125
1126
    /**
1127
     * Delete outdated files from updated plugins
1128
     *
1129
     * @param array $installed
1130
     */
1131
    private function removeDeletedfiles($installed) {
1132
        foreach($installed as $id => $extension) {
1133
            // only on update
1134
            if($extension['action'] == 'install') continue;
1135
1136
            // get definition file
1137
            if($extension['type'] == 'template') {
1138
                $extensiondir = DOKU_TPLLIB;
1139
            }else{
1140
                $extensiondir = DOKU_PLUGIN;
1141
            }
1142
            $extensiondir = $extensiondir . $extension['base'] .'/';
1143
            $definitionfile = $extensiondir . 'deleted.files';
1144
            if(!file_exists($definitionfile)) continue;
1145
1146
            // delete the old files
1147
            $list = file($definitionfile);
1148
1149
            foreach($list as $line) {
1150
                $line = trim(preg_replace('/#.*$/', '', $line));
1151
                if(!$line) continue;
1152
                $file = $extensiondir . $line;
1153
                if(!file_exists($file)) continue;
1154
1155
                io_rmdir($file, true);
1156
            }
1157
        }
1158
    }
1159
}
1160
1161
// vim:ts=4:sw=4:et:
1162