Completed
Push — extensionevent ( 4cd34a...407958 )
by Andreas
04:09
created

helper_plugin_extension_extension::triggerEvent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 6
rs 9.4285
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', 'authpdo', '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
642
        $data = array(
643
            'extension' => $this,
644
            'action' => 'uninstall'
645
        );
646
        $trigger = new Doku_Event('PLUGIN_EXTENSION_CHANGE', $data);
647
648
        if($trigger->advise_before()) {
649
            if(io_rmdir($this->getInstallDir(), true)) {
650
                $trigger->advise_after();
651
                return true;
652
            } else {
653
                return false;
654
            }
655
        }
656
        return (bool) $trigger->result;
657
    }
658
659
    /**
660
     * Enable the extension
661
     *
662
     * @return bool|string True or an error message
663
     */
664
    public function enable() {
665
        if ($this->isTemplate()) return $this->getLang('notimplemented');
666
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
667
        if ($this->isEnabled()) return $this->getLang('alreadyenabled');
668
        /* @var Doku_Plugin_Controller $plugin_controller */
669
        global $plugin_controller;
670
671
        $data = array(
672
            'extension' => $this,
673
            'action' => 'enable'
674
        );
675
        $trigger = new Doku_Event('PLUGIN_EXTENSION_CHANGE', $data);
676
677
        if($trigger->advise_before()) {
678
            if($plugin_controller->enable($this->base)) {
679
                $this->purgeCache();
680
                $trigger->advise_after();
681
                return true;
682
            } else {
683
                return $this->getLang('pluginlistsaveerror');
684
            }
685
        }
686
        if($trigger->result === true) return true;
687
        return (string) $trigger->result;
688
    }
689
690
    /**
691
     * Disable the extension
692
     *
693
     * @return bool|string True or an error message
694
     */
695
    public function disable() {
696
        if ($this->isTemplate()) return $this->getLang('notimplemented');
697
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
698
        if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
699
        /* @var Doku_Plugin_Controller $plugin_controller */
700
        global $plugin_controller;
701
702
        $data = array(
703
            'extension' => $this,
704
            'action' => 'disable'
705
        );
706
        $trigger = new Doku_Event('PLUGIN_EXTENSION_CHANGE', $data);
707
708
        if($trigger->advise_before()) {
709
            if($plugin_controller->disable($this->base)) {
710
                $this->purgeCache();
711
                $trigger->advise_after();
712
                return true;
713
            } else {
714
                return $this->getLang('pluginlistsaveerror');
715
            }
716
        }
717
        if($trigger->result === true) return true;
718
        return (string) $trigger->result;
719
    }
720
721
    /**
722
     * Purge the cache by touching the main configuration file
723
     */
724
    protected function purgeCache() {
725
        global $config_cascade;
726
727
        // expire dokuwiki caches
728
        // touching local.php expires wiki page, JS and CSS caches
729
        @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...
730
    }
731
732
    /**
733
     * Read local extension data either from info.txt or getInfo()
734
     */
735
    protected function readLocalData() {
736
        if ($this->isTemplate()) {
737
            $infopath = $this->getInstallDir().'/template.info.txt';
738
        } else {
739
            $infopath = $this->getInstallDir().'/plugin.info.txt';
740
        }
741
742
        if (is_readable($infopath)) {
743
            $this->localInfo = confToHash($infopath);
744
        } elseif (!$this->isTemplate() && $this->isEnabled()) {
745
            global $plugin_types;
746
            $path       = $this->getInstallDir().'/';
747
            $plugin     = null;
748
749
            foreach($plugin_types as $type) {
750
                if(file_exists($path.$type.'.php')) {
751
                    $plugin = plugin_load($type, $this->base);
752
                    if ($plugin) break;
753
                }
754
755
                if($dh = @opendir($path.$type.'/')) {
756
                    while(false !== ($cp = readdir($dh))) {
757
                        if($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
758
759
                        $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
760
                        if ($plugin) break;
761
                    }
762
                    if ($plugin) break;
763
                    closedir($dh);
764
                }
765
            }
766
767
            if ($plugin) {
768
                /* @var DokuWiki_Plugin $plugin */
769
                $this->localInfo = $plugin->getInfo();
770
            }
771
        }
772
    }
773
774
    /**
775
     * Save the given URL and current datetime in the manager.dat file of all installed extensions
776
     *
777
     * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
778
     * @param array  $installed Optional list of installed plugins
779
     */
780
    protected function updateManagerData($url = '', $installed = null) {
781
        $origID = $this->getID();
782
783
        if(is_null($installed)) {
784
            $installed = array($origID);
785
        }
786
787
        foreach($installed as $ext => $info) {
788
            if($this->getID() != $ext) $this->setExtension($ext);
789
            if($url) {
790
                $this->managerData['downloadurl'] = $url;
791
            } elseif(isset($this->managerData['downloadurl'])) {
792
                unset($this->managerData['downloadurl']);
793
            }
794
            if(isset($this->managerData['installed'])) {
795
                $this->managerData['updated'] = date('r');
796
            } else {
797
                $this->managerData['installed'] = date('r');
798
            }
799
            $this->writeManagerData();
800
        }
801
802
        if($this->getID() != $origID) $this->setExtension($origID);
803
    }
804
805
    /**
806
     * Read the manager.dat file
807
     */
808
    protected function readManagerData() {
809
        $managerpath = $this->getInstallDir().'/manager.dat';
810
        if (is_readable($managerpath)) {
811
            $file = @file($managerpath);
812
            if(!empty($file)) {
813
                foreach($file as $line) {
814
                    list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
815
                    $key = trim($key);
816
                    $value = trim($value);
817
                    // backwards compatible with old plugin manager
818
                    if($key == 'url') $key = 'downloadurl';
819
                    $this->managerData[$key] = $value;
820
                }
821
            }
822
        }
823
    }
824
825
    /**
826
     * Write the manager.data file
827
     */
828
    protected function writeManagerData() {
829
        $managerpath = $this->getInstallDir().'/manager.dat';
830
        $data = '';
831
        foreach ($this->managerData as $k => $v) {
832
            $data .= $k.'='.$v.DOKU_LF;
833
        }
834
        io_saveFile($managerpath, $data);
835
    }
836
837
    /**
838
     * Returns a temporary directory
839
     *
840
     * The directory is registered for cleanup when the class is destroyed
841
     *
842
     * @return false|string
843
     */
844
    protected function mkTmpDir(){
845
        $dir = io_mktmpdir();
846
        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...
847
        $this->temporary[] = $dir;
848
        return $dir;
849
    }
850
851
    /**
852
     * Download an archive to a protected path
853
     *
854
     * @param string $url  The url to get the archive from
855
     * @throws Exception   when something goes wrong
856
     * @return string The path where the archive was saved
857
     */
858
    public function download($url) {
859
        // check the url
860
        if(!preg_match('/https?:\/\//i', $url)){
861
            throw new Exception($this->getLang('error_badurl'));
862
        }
863
864
        // try to get the file from the path (used as plugin name fallback)
865
        $file = parse_url($url, PHP_URL_PATH);
866
        if(is_null($file)){
867
            $file = md5($url);
868
        }else{
869
            $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...
870
        }
871
872
        // create tmp directory for download
873
        if(!($tmp = $this->mkTmpDir())) {
874
            throw new Exception($this->getLang('error_dircreate'));
875
        }
876
877
        // download
878
        if(!$file = io_download($url, $tmp.'/', true, $file, 0)) {
879
            io_rmdir($tmp, true);
880
            throw new Exception(sprintf($this->getLang('error_download'), '<bdi>'.hsc($url).'</bdi>'));
881
        }
882
883
        return $tmp.'/'.$file;
884
    }
885
886
    /**
887
     * @param string $file      The path to the archive that shall be installed
888
     * @param bool   $overwrite If an already installed plugin should be overwritten
889
     * @param string $base      The basename of the plugin if it's known
890
     * @throws Exception        when something went wrong
891
     * @return array            list of installed extensions
892
     */
893
    public function installArchive($file, $overwrite=false, $base = '') {
894
        $installed_extensions = array();
895
896
        // create tmp directory for decompression
897
        if(!($tmp = $this->mkTmpDir())) {
898
            throw new Exception($this->getLang('error_dircreate'));
899
        }
900
901
        // add default base folder if specified to handle case where zip doesn't contain this
902
        if($base && !@mkdir($tmp.'/'.$base)) {
903
            throw new Exception($this->getLang('error_dircreate'));
904
        }
905
906
        // decompress
907
        $this->decompress($file, "$tmp/".$base);
908
909
        // search $tmp/$base for the folder(s) that has been created
910
        // move the folder(s) to lib/..
911
        $result = array('old'=>array(), 'new'=>array());
912
        $default = ($this->isTemplate() ? 'template' : 'plugin');
913
        if(!$this->find_folders($result, $tmp.'/'.$base, $default)) {
914
            throw new Exception($this->getLang('error_findfolder'));
915
        }
916
917
        // choose correct result array
918
        if(count($result['new'])) {
919
            $install = $result['new'];
920
        }else{
921
            $install = $result['old'];
922
        }
923
924
        if(!count($install)){
925
            throw new Exception($this->getLang('error_findfolder'));
926
        }
927
928
        // now install all found items
929
        foreach($install as $item) {
930
            // where to install?
931
            if($item['type'] == 'template') {
932
                $target_base_dir = DOKU_TPLLIB;
933
            }else{
934
                $target_base_dir = DOKU_PLUGIN;
935
            }
936
937
            if(!empty($item['base'])) {
938
                // use base set in info.txt
939
            } elseif($base && count($install) == 1) {
940
                $item['base'] = $base;
941
            } else {
942
                // default - use directory as found in zip
943
                // plugins from github/master without *.info.txt will install in wrong folder
944
                // but using $info->id will make 'code3' fail (which should install in lib/code/..)
945
                $item['base'] = basename($item['tmp']);
946
            }
947
948
            // check to make sure we aren't overwriting anything
949
            $target = $target_base_dir.$item['base'];
950
            if(!$overwrite && file_exists($target)) {
951
                // TODO remember our settings, ask the user to confirm overwrite
952
                continue;
953
            }
954
955
            // gather info
956
            $action = file_exists($target) ? 'update' : 'install';
957
            $extname = $item['base'];
958
            if($item['type'] == 'template') {
959
                $extname = 'template:'.$extname;
960
            }
961
962
            // prepare plugin hook
963
            $extension = new helper_plugin_extension_extension();
964
            $extension->setExtension($extname);
965
            $data = array(
966
                'extension' => $extension,
967
                'action' => $action,
968
                'src' => $item['tmp'],
969
                'dst' => $target
970
            );
971
972
            // execute the copy
973
            $trigger = new Doku_Event('PLUGIN_EXTENSION_CHANGE', $data);
974
            if($trigger->advise_before()) {
975
                // copy action
976
                if($this->dircopy($item['tmp'], $target)) {
977
                    $installed_extensions[$extname] = array(
978
                        'base' => $item['base'],
979
                        'type' => $item['type'],
980
                        'action' => $action
981
                    );
982
                } else {
983
                    throw new Exception(sprintf($this->getLang('error_copy') . DOKU_LF, '<bdi>' . $item['base'] . '</bdi>'));
984
                }
985
            }
986
            $trigger->advise_after();
987
        }
988
989
        // cleanup
990
        if($tmp) io_rmdir($tmp, true);
991
992
        return $installed_extensions;
993
    }
994
995
    /**
996
     * Find out what was in the extracted directory
997
     *
998
     * Correct folders are searched recursively using the "*.info.txt" configs
999
     * as indicator for a root folder. When such a file is found, it's base
1000
     * setting is used (when set). All folders found by this method are stored
1001
     * in the 'new' key of the $result array.
1002
     *
1003
     * For backwards compatibility all found top level folders are stored as
1004
     * in the 'old' key of the $result array.
1005
     *
1006
     * When no items are found in 'new' the copy mechanism should fall back
1007
     * the 'old' list.
1008
     *
1009
     * @author Andreas Gohr <[email protected]>
1010
     * @param array $result - results are stored here
1011
     * @param string $directory - the temp directory where the package was unpacked to
1012
     * @param string $default_type - type used if no info.txt available
1013
     * @param string $subdir - a subdirectory. do not set. used by recursion
1014
     * @return bool - false on error
1015
     */
1016
    protected function find_folders(&$result, $directory, $default_type='plugin', $subdir='') {
1017
        $this_dir = "$directory$subdir";
1018
        $dh       = @opendir($this_dir);
1019
        if(!$dh) return false;
1020
1021
        $found_dirs           = array();
1022
        $found_files          = 0;
1023
        $found_template_parts = 0;
1024
        while (false !== ($f = readdir($dh))) {
1025
            if($f == '.' || $f == '..') continue;
1026
1027
            if(is_dir("$this_dir/$f")) {
1028
                $found_dirs[] = "$subdir/$f";
1029
1030
            } else {
1031
                // it's a file -> check for config
1032
                $found_files++;
1033
                switch ($f) {
1034
                    case 'plugin.info.txt':
1035
                    case 'template.info.txt':
1036
                        // we have  found a clear marker, save and return
1037
                        $info = array();
1038
                        $type = explode('.', $f, 2);
1039
                        $info['type'] = $type[0];
1040
                        $info['tmp']  = $this_dir;
1041
                        $conf = confToHash("$this_dir/$f");
1042
                        $info['base'] = basename($conf['base']);
1043
                        $result['new'][] = $info;
1044
                        return true;
1045
1046
                    case 'main.php':
1047
                    case 'details.php':
1048
                    case 'mediamanager.php':
1049
                    case 'style.ini':
1050
                        $found_template_parts++;
1051
                        break;
1052
                }
1053
            }
1054
        }
1055
        closedir($dh);
1056
1057
        // files where found but no info.txt - use old method
1058
        if($found_files){
1059
            $info            = array();
1060
            $info['tmp']     = $this_dir;
1061
            // does this look like a template or should we use the default type?
1062
            if($found_template_parts >= 2) {
1063
                $info['type']    = 'template';
1064
            } else {
1065
                $info['type']    = $default_type;
1066
            }
1067
1068
            $result['old'][] = $info;
1069
            return true;
1070
        }
1071
1072
        // we have no files yet -> recurse
1073
        foreach ($found_dirs as $found_dir) {
1074
            $this->find_folders($result, $directory, $default_type, "$found_dir");
1075
        }
1076
        return true;
1077
    }
1078
1079
    /**
1080
     * Decompress a given file to the given target directory
1081
     *
1082
     * Determines the compression type from the file extension
1083
     *
1084
     * @param string $file   archive to extract
1085
     * @param string $target directory to extract to
1086
     * @throws Exception
1087
     * @return bool
1088
     */
1089
    private function decompress($file, $target) {
1090
        // decompression library doesn't like target folders ending in "/"
1091
        if(substr($target, -1) == "/") $target = substr($target, 0, -1);
1092
1093
        $ext = $this->guess_archive($file);
1094
        if(in_array($ext, array('tar', 'bz', 'gz'))) {
1095
1096
            try {
1097
                $tar = new \splitbrain\PHPArchive\Tar();
1098
                $tar->open($file);
1099
                $tar->extract($target);
1100
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1101
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1102
            }
1103
1104
            return true;
1105
        } elseif($ext == 'zip') {
1106
1107
            try {
1108
                $zip = new \splitbrain\PHPArchive\Zip();
1109
                $zip->open($file);
1110
                $zip->extract($target);
1111
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1112
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1113
            }
1114
1115
            return true;
1116
        }
1117
1118
        // the only case when we don't get one of the recognized archive types is when the archive file can't be read
1119
        throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
1120
    }
1121
1122
    /**
1123
     * Determine the archive type of the given file
1124
     *
1125
     * Reads the first magic bytes of the given file for content type guessing,
1126
     * if neither bz, gz or zip are recognized, tar is assumed.
1127
     *
1128
     * @author Andreas Gohr <[email protected]>
1129
     * @param string $file The file to analyze
1130
     * @return string|false false if the file can't be read, otherwise an "extension"
1131
     */
1132
    private function guess_archive($file) {
1133
        $fh = fopen($file, 'rb');
1134
        if(!$fh) return false;
1135
        $magic = fread($fh, 5);
1136
        fclose($fh);
1137
1138
        if(strpos($magic, "\x42\x5a") === 0) return 'bz';
1139
        if(strpos($magic, "\x1f\x8b") === 0) return 'gz';
1140
        if(strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
1141
        return 'tar';
1142
    }
1143
1144
    /**
1145
     * Copy with recursive sub-directory support
1146
     *
1147
     * @param string $src filename path to file
1148
     * @param string $dst filename path to file
1149
     * @return bool|int|string
1150
     */
1151
    private function dircopy($src, $dst) {
1152
        global $conf;
1153
1154
        if(is_dir($src)) {
1155
            if(!$dh = @opendir($src)) return false;
1156
1157
            if($ok = io_mkdir_p($dst)) {
1158
                while ($ok && (false !== ($f = readdir($dh)))) {
1159
                    if($f == '..' || $f == '.') continue;
1160
                    $ok = $this->dircopy("$src/$f", "$dst/$f");
1161
                }
1162
            }
1163
1164
            closedir($dh);
1165
            return $ok;
1166
1167
        } else {
1168
            $exists = file_exists($dst);
1169
1170
            if(!@copy($src, $dst)) return false;
1171
            if(!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']);
1172
            @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...
1173
        }
1174
1175
        return true;
1176
    }
1177
1178
    /**
1179
     * Delete outdated files from updated plugins
1180
     *
1181
     * @param array $installed
1182
     */
1183
    private function removeDeletedfiles($installed) {
1184
        foreach($installed as $id => $extension) {
1185
            // only on update
1186
            if($extension['action'] == 'install') continue;
1187
1188
            // get definition file
1189
            if($extension['type'] == 'template') {
1190
                $extensiondir = DOKU_TPLLIB;
1191
            }else{
1192
                $extensiondir = DOKU_PLUGIN;
1193
            }
1194
            $extensiondir = $extensiondir . $extension['base'] .'/';
1195
            $definitionfile = $extensiondir . 'deleted.files';
1196
            if(!file_exists($definitionfile)) continue;
1197
1198
            // delete the old files
1199
            $list = file($definitionfile);
1200
1201
            foreach($list as $line) {
1202
                $line = trim(preg_replace('/#.*$/', '', $line));
1203
                if(!$line) continue;
1204
                $file = $extensiondir . $line;
1205
                if(!file_exists($file)) continue;
1206
1207
                io_rmdir($file, true);
1208
            }
1209
        }
1210
    }
1211
1212
}
1213
1214
// vim:ts=4:sw=4:et:
1215