Completed
Push — cli ( eb8d78 )
by Andreas
03:57
created

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

Upgrade to new PHP Analysis Engine

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

1
<?php
2
/**
3
 * DokuWiki Plugin extension (Helper Component)
4
 *
5
 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6
 * @author  Michael Hamann <[email protected]>
7
 */
8
9
// 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
        $id = cleanID($id);
55
        $this->id   = $id;
56
        $this->base = $id;
57
58
        if(substr($id, 0 , 9) == 'template:'){
59
            $this->base = substr($id, 9);
60
            $this->is_template = true;
61
        } else {
62
            $this->is_template = false;
63
        }
64
65
        $this->localInfo = array();
66
        $this->managerData = array();
67
        $this->remoteInfo = array();
68
69
        if ($this->isInstalled()) {
70
            $this->readLocalData();
71
            $this->readManagerData();
72
        }
73
74
        if ($this->repository == null) {
75
            $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_PluginInterface>. 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...
76
        }
77
78
        $this->remoteInfo = $this->repository->getData($this->getID());
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface DokuWiki_PluginInterface as the method getData() does only exist in the following implementations of said interface: helper_plugin_extension_repository.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
79
80
        return ($this->localInfo || $this->remoteInfo);
81
    }
82
83
    /**
84
     * If the extension is installed locally
85
     *
86
     * @return bool If the extension is installed locally
87
     */
88
    public function isInstalled() {
89
        return is_dir($this->getInstallDir());
90
    }
91
92
    /**
93
     * If the extension is under git control
94
     *
95
     * @return bool
96
     */
97
    public function isGitControlled() {
98
        if(!$this->isInstalled()) return false;
99
        return is_dir($this->getInstallDir().'/.git');
100
    }
101
102
    /**
103
     * If the extension is bundled
104
     *
105
     * @return bool If the extension is bundled
106
     */
107
    public function isBundled() {
108
        if (!empty($this->remoteInfo['bundled'])) return $this->remoteInfo['bundled'];
109
        return in_array($this->id,
110
                        array(
111
                            'authad', 'authldap', 'authmysql', 'authpdo', 'authpgsql', 'authplain', 'acl', 'info', 'extension',
112
                            'revert', 'popularity', 'config', 'safefnrecode', 'styling', 'testing', 'template:dokuwiki'
113
                        )
114
        );
115
    }
116
117
    /**
118
     * If the extension is protected against any modification (disable/uninstall)
119
     *
120
     * @return bool if the extension is protected
121
     */
122
    public function isProtected() {
123
        // never allow deinstalling the current auth plugin:
124
        global $conf;
125
        if ($this->id == $conf['authtype']) return true;
126
127
        /** @var Doku_Plugin_Controller $plugin_controller */
128
        global $plugin_controller;
129
        $cascade = $plugin_controller->getCascade();
130
        return (isset($cascade['protected'][$this->id]) && $cascade['protected'][$this->id]);
131
    }
132
133
    /**
134
     * If the extension is installed in the correct directory
135
     *
136
     * @return bool If the extension is installed in the correct directory
137
     */
138
    public function isInWrongFolder() {
139
        return $this->base != $this->getBase();
140
    }
141
142
    /**
143
     * If the extension is enabled
144
     *
145
     * @return bool If the extension is enabled
146
     */
147
    public function isEnabled() {
148
        global $conf;
149
        if($this->isTemplate()){
150
            return ($conf['template'] == $this->getBase());
151
        }
152
153
        /* @var Doku_Plugin_Controller $plugin_controller */
154
        global $plugin_controller;
155
        return !$plugin_controller->isdisabled($this->base);
156
    }
157
158
    /**
159
     * If the extension should be updated, i.e. if an updated version is available
160
     *
161
     * @return bool If an update is available
162
     */
163
    public function updateAvailable() {
164
        if(!$this->isInstalled()) return false;
165
        if($this->isBundled()) return false;
166
        $lastupdate = $this->getLastUpdate();
167
        if ($lastupdate === false) return false;
168
        $installed  = $this->getInstalledVersion();
169
        if ($installed === false || $installed === $this->getLang('unknownversion')) return true;
170
        return $this->getInstalledVersion() < $this->getLastUpdate();
171
    }
172
173
    /**
174
     * If the extension is a template
175
     *
176
     * @return bool If this extension is a template
177
     */
178
    public function isTemplate() {
179
        return $this->is_template;
180
    }
181
182
    /**
183
     * Get the ID of the extension
184
     *
185
     * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:'
186
     *
187
     * @return string
188
     */
189
    public function getID() {
190
        return $this->id;
191
    }
192
193
    /**
194
     * Get the name of the installation directory
195
     *
196
     * @return string The name of the installation directory
197
     */
198
    public function getInstallName() {
199
        return $this->base;
200
    }
201
202
    // Data from plugin.info.txt/template.info.txt or the repo when not available locally
203
    /**
204
     * Get the basename of the extension
205
     *
206
     * @return string The basename
207
     */
208
    public function getBase() {
209
        if (!empty($this->localInfo['base'])) return $this->localInfo['base'];
210
        return $this->base;
211
    }
212
213
    /**
214
     * Get the display name of the extension
215
     *
216
     * @return string The display name
217
     */
218
    public function getDisplayName() {
219
        if (!empty($this->localInfo['name'])) return $this->localInfo['name'];
220
        if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name'];
221
        return $this->base;
222
    }
223
224
    /**
225
     * Get the author name of the extension
226
     *
227
     * @return string|bool The name of the author or false if there is none
228
     */
229
    public function getAuthor() {
230
        if (!empty($this->localInfo['author'])) return $this->localInfo['author'];
231
        if (!empty($this->remoteInfo['author'])) return $this->remoteInfo['author'];
232
        return false;
233
    }
234
235
    /**
236
     * Get the email of the author of the extension if there is any
237
     *
238
     * @return string|bool The email address or false if there is none
239
     */
240
    public function getEmail() {
241
        // email is only in the local data
242
        if (!empty($this->localInfo['email'])) return $this->localInfo['email'];
243
        return false;
244
    }
245
246
    /**
247
     * Get the email id, i.e. the md5sum of the email
248
     *
249
     * @return string|bool The md5sum of the email if there is any, false otherwise
250
     */
251
    public function getEmailID() {
252
        if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
253
        if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
254
        return false;
255
    }
256
257
    /**
258
     * Get the description of the extension
259
     *
260
     * @return string The description
261
     */
262
    public function getDescription() {
263
        if (!empty($this->localInfo['desc'])) return $this->localInfo['desc'];
264
        if (!empty($this->remoteInfo['description'])) return $this->remoteInfo['description'];
265
        return '';
266
    }
267
268
    /**
269
     * Get the URL of the extension, usually a page on dokuwiki.org
270
     *
271
     * @return string The URL
272
     */
273
    public function getURL() {
274
        if (!empty($this->localInfo['url'])) return $this->localInfo['url'];
275
        return 'https://www.dokuwiki.org/'.($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
276
    }
277
278
    /**
279
     * Get the installed version of the extension
280
     *
281
     * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
282
     */
283
    public function getInstalledVersion() {
284
        if (!empty($this->localInfo['date'])) return $this->localInfo['date'];
285
        if ($this->isInstalled()) return $this->getLang('unknownversion');
286
        return false;
287
    }
288
289
    /**
290
     * Get the install date of the current version
291
     *
292
     * @return string|bool The date of the last update or false if not available
293
     */
294
    public function getUpdateDate() {
295
        if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
296
        return $this->getInstallDate();
297
    }
298
299
    /**
300
     * Get the date of the installation of the plugin
301
     *
302
     * @return string|bool The date of the installation or false if not available
303
     */
304
    public function getInstallDate() {
305
        if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
306
        return false;
307
    }
308
309
    /**
310
     * Get the names of the dependencies of this extension
311
     *
312
     * @return array The base names of the dependencies
313
     */
314
    public function getDependencies() {
315
        if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies'];
316
        return array();
317
    }
318
319
    /**
320
     * Get the names of the missing dependencies
321
     *
322
     * @return array The base names of the missing dependencies
323
     */
324
    public function getMissingDependencies() {
325
        /* @var Doku_Plugin_Controller $plugin_controller */
326
        global $plugin_controller;
327
        $dependencies = $this->getDependencies();
328
        $missing_dependencies = array();
329
        foreach ($dependencies as $dependency) {
330
            if ($plugin_controller->isdisabled($dependency)) {
331
                $missing_dependencies[] = $dependency;
332
            }
333
        }
334
        return $missing_dependencies;
335
    }
336
337
    /**
338
     * Get the names of all conflicting extensions
339
     *
340
     * @return array The names of the conflicting extensions
341
     */
342
    public function getConflicts() {
343
        if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['conflicts'];
344
        return array();
345
    }
346
347
    /**
348
     * Get the names of similar extensions
349
     *
350
     * @return array The names of similar extensions
351
     */
352
    public function getSimilarExtensions() {
353
        if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar'];
354
        return array();
355
    }
356
357
    /**
358
     * Get the names of the tags of the extension
359
     *
360
     * @return array The names of the tags of the extension
361
     */
362
    public function getTags() {
363
        if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags'];
364
        return array();
365
    }
366
367
    /**
368
     * Get the popularity information as floating point number [0,1]
369
     *
370
     * @return float|bool The popularity information or false if it isn't available
371
     */
372
    public function getPopularity() {
373
        if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity'];
374
        return false;
375
    }
376
377
378
    /**
379
     * Get the text of the security warning if there is any
380
     *
381
     * @return string|bool The security warning if there is any, false otherwise
382
     */
383
    public function getSecurityWarning() {
384
        if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning'];
385
        return false;
386
    }
387
388
    /**
389
     * Get the text of the security issue if there is any
390
     *
391
     * @return string|bool The security issue if there is any, false otherwise
392
     */
393
    public function getSecurityIssue() {
394
        if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue'];
395
        return false;
396
    }
397
398
    /**
399
     * Get the URL of the screenshot of the extension if there is any
400
     *
401
     * @return string|bool The screenshot URL if there is any, false otherwise
402
     */
403
    public function getScreenshotURL() {
404
        if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl'];
405
        return false;
406
    }
407
408
    /**
409
     * Get the URL of the thumbnail of the extension if there is any
410
     *
411
     * @return string|bool The thumbnail URL if there is any, false otherwise
412
     */
413
    public function getThumbnailURL() {
414
        if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl'];
415
        return false;
416
    }
417
    /**
418
     * Get the last used download URL of the extension if there is any
419
     *
420
     * @return string|bool The previously used download URL, false if the extension has been installed manually
421
     */
422
    public function getLastDownloadURL() {
423
        if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
424
        return false;
425
    }
426
427
    /**
428
     * Get the download URL of the extension if there is any
429
     *
430
     * @return string|bool The download URL if there is any, false otherwise
431
     */
432
    public function getDownloadURL() {
433
        if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl'];
434
        return false;
435
    }
436
437
    /**
438
     * If the download URL has changed since the last download
439
     *
440
     * @return bool If the download URL has changed
441
     */
442
    public function hasDownloadURLChanged() {
443
        $lasturl = $this->getLastDownloadURL();
444
        $currenturl = $this->getDownloadURL();
445
        return ($lasturl && $currenturl && $lasturl != $currenturl);
446
    }
447
448
    /**
449
     * Get the bug tracker URL of the extension if there is any
450
     *
451
     * @return string|bool The bug tracker URL if there is any, false otherwise
452
     */
453
    public function getBugtrackerURL() {
454
        if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker'];
455
        return false;
456
    }
457
458
    /**
459
     * Get the URL of the source repository if there is any
460
     *
461
     * @return string|bool The URL of the source repository if there is any, false otherwise
462
     */
463
    public function getSourcerepoURL() {
464
        if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo'];
465
        return false;
466
    }
467
468
    /**
469
     * Get the donation URL of the extension if there is any
470
     *
471
     * @return string|bool The donation URL if there is any, false otherwise
472
     */
473
    public function getDonationURL() {
474
        if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl'];
475
        return false;
476
    }
477
478
    /**
479
     * Get the extension type(s)
480
     *
481
     * @return array The type(s) as array of strings
482
     */
483
    public function getTypes() {
484
        if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types'];
485
        if ($this->isTemplate()) return array(32 => 'template');
486
        return array();
487
    }
488
489
    /**
490
     * Get a list of all DokuWiki versions this extension is compatible with
491
     *
492
     * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
493
     */
494
    public function getCompatibleVersions() {
495
        if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible'];
496
        return array();
497
    }
498
499
    /**
500
     * Get the date of the last available update
501
     *
502
     * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
503
     */
504
    public function getLastUpdate() {
505
        if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate'];
506
        return false;
507
    }
508
509
    /**
510
     * Get the base path of the extension
511
     *
512
     * @return string The base path of the extension
513
     */
514
    public function getInstallDir() {
515
        if ($this->isTemplate()) {
516
            return DOKU_TPLLIB.$this->base;
517
        } else {
518
            return DOKU_PLUGIN.$this->base;
519
        }
520
    }
521
522
    /**
523
     * The type of extension installation
524
     *
525
     * @return string One of "none", "manual", "git" or "automatic"
526
     */
527
    public function getInstallType() {
528
        if (!$this->isInstalled()) return 'none';
529
        if (!empty($this->managerData)) return 'automatic';
530
        if (is_dir($this->getInstallDir().'/.git')) return 'git';
531
        return 'manual';
532
    }
533
534
    /**
535
     * If the extension can probably be installed/updated or uninstalled
536
     *
537
     * @return bool|string True or error string
538
     */
539
    public function canModify() {
540
        if($this->isInstalled()) {
541
            if(!is_writable($this->getInstallDir())) {
542
                return 'noperms';
543
            }
544
        }
545
546
        if($this->isTemplate() && !is_writable(DOKU_TPLLIB)) {
547
            return 'notplperms';
548
549
        } elseif(!is_writable(DOKU_PLUGIN)) {
550
            return 'nopluginperms';
551
        }
552
        return true;
553
    }
554
555
    /**
556
     * Install an extension from a user upload
557
     *
558
     * @param string $field name of the upload file
559
     * @throws Exception when something goes wrong
560
     * @return array The list of installed extensions
561
     */
562
    public function installFromUpload($field){
563
        if($_FILES[$field]['error']){
564
            throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
565
        }
566
567
        $tmp = $this->mkTmpDir();
568
        if(!$tmp) throw new Exception($this->getLang('error_dircreate'));
569
570
        // filename may contain the plugin name for old style plugins...
571
        $basename = basename($_FILES[$field]['name']);
572
        $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
573
        $basename = preg_replace('/[\W]+/', '', $basename);
574
575
        if(!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")){
576
            throw new Exception($this->getLang('msg_upload_failed'));
577
        }
578
579
        try {
580
            $installed = $this->installArchive("$tmp/upload.archive", true, $basename);
581
            $this->updateManagerData('', $installed);
582
            $this->removeDeletedfiles($installed);
583
            // purge cache
584
            $this->purgeCache();
585
        }catch (Exception $e){
586
            throw $e;
587
        }
588
        return $installed;
589
    }
590
591
    /**
592
     * Install an extension from a remote URL
593
     *
594
     * @param string $url
595
     * @throws Exception when something goes wrong
596
     * @return array The list of installed extensions
597
     */
598
    public function installFromURL($url){
599
        try {
600
            $path      = $this->download($url);
601
            $installed = $this->installArchive($path, true);
602
            $this->updateManagerData($url, $installed);
603
            $this->removeDeletedfiles($installed);
604
605
            // purge cache
606
            $this->purgeCache();
607
        }catch (Exception $e){
608
            throw $e;
609
        }
610
        return $installed;
611
    }
612
613
    /**
614
     * Install or update the extension
615
     *
616
     * @throws \Exception when something goes wrong
617
     * @return array The list of installed extensions
618
     */
619
    public function installOrUpdate() {
620
        $url       = $this->getDownloadURL();
621
        $path      = $this->download($url);
622
        $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
623
        $this->updateManagerData($url, $installed);
624
625
        // refresh extension information
626
        if (!isset($installed[$this->getID()])) {
627
            throw new Exception('Error, the requested extension hasn\'t been installed or updated');
628
        }
629
        $this->removeDeletedfiles($installed);
630
        $this->setExtension($this->getID());
631
        $this->purgeCache();
632
        return $installed;
633
    }
634
635
    /**
636
     * Uninstall the extension
637
     *
638
     * @return bool If the plugin was sucessfully uninstalled
639
     */
640
    public function uninstall() {
641
        $this->purgeCache();
642
        return io_rmdir($this->getInstallDir(), true);
643
    }
644
645
    /**
646
     * Enable the extension
647
     *
648
     * @return bool|string True or an error message
649
     */
650
    public function enable() {
651
        if ($this->isTemplate()) return $this->getLang('notimplemented');
652
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
653
        if ($this->isEnabled()) return $this->getLang('alreadyenabled');
654
655
        /* @var Doku_Plugin_Controller $plugin_controller */
656
        global $plugin_controller;
657
        if ($plugin_controller->enable($this->base)) {
658
            $this->purgeCache();
659
            return true;
660
        } else {
661
            return $this->getLang('pluginlistsaveerror');
662
        }
663
    }
664
665
    /**
666
     * Disable the extension
667
     *
668
     * @return bool|string True or an error message
669
     */
670
    public function disable() {
671
        if ($this->isTemplate()) return $this->getLang('notimplemented');
672
673
        /* @var Doku_Plugin_Controller $plugin_controller */
674
        global $plugin_controller;
675
        if (!$this->isInstalled()) return $this->getLang('notinstalled');
676
        if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
677
        if ($plugin_controller->disable($this->base)) {
678
            $this->purgeCache();
679
            return true;
680
        } else {
681
            return $this->getLang('pluginlistsaveerror');
682
        }
683
    }
684
685
    /**
686
     * Purge the cache by touching the main configuration file
687
     */
688
    protected function purgeCache() {
689
        global $config_cascade;
690
691
        // expire dokuwiki caches
692
        // touching local.php expires wiki page, JS and CSS caches
693
        @touch(reset($config_cascade['main']['local']));
694
    }
695
696
    /**
697
     * Read local extension data either from info.txt or getInfo()
698
     */
699
    protected function readLocalData() {
700
        if ($this->isTemplate()) {
701
            $infopath = $this->getInstallDir().'/template.info.txt';
702
        } else {
703
            $infopath = $this->getInstallDir().'/plugin.info.txt';
704
        }
705
706
        if (is_readable($infopath)) {
707
            $this->localInfo = confToHash($infopath);
708
        } elseif (!$this->isTemplate() && $this->isEnabled()) {
709
            global $plugin_types;
710
            $path       = $this->getInstallDir().'/';
711
            $plugin     = null;
712
713
            foreach($plugin_types as $type) {
714
                if(file_exists($path.$type.'.php')) {
715
                    $plugin = plugin_load($type, $this->base);
716
                    if ($plugin) break;
717
                }
718
719
                if($dh = @opendir($path.$type.'/')) {
720
                    while(false !== ($cp = readdir($dh))) {
721
                        if($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
722
723
                        $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
724
                        if ($plugin) break;
725
                    }
726
                    if ($plugin) break;
727
                    closedir($dh);
728
                }
729
            }
730
731
            if ($plugin) {
732
                /* @var DokuWiki_Plugin $plugin */
733
                $this->localInfo = $plugin->getInfo();
734
            }
735
        }
736
    }
737
738
    /**
739
     * Save the given URL and current datetime in the manager.dat file of all installed extensions
740
     *
741
     * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
742
     * @param array  $installed Optional list of installed plugins
743
     */
744
    protected function updateManagerData($url = '', $installed = null) {
745
        $origID = $this->getID();
746
747
        if(is_null($installed)) {
748
            $installed = array($origID);
749
        }
750
751
        foreach($installed as $ext => $info) {
752
            if($this->getID() != $ext) $this->setExtension($ext);
753
            if($url) {
754
                $this->managerData['downloadurl'] = $url;
755
            } elseif(isset($this->managerData['downloadurl'])) {
756
                unset($this->managerData['downloadurl']);
757
            }
758
            if(isset($this->managerData['installed'])) {
759
                $this->managerData['updated'] = date('r');
760
            } else {
761
                $this->managerData['installed'] = date('r');
762
            }
763
            $this->writeManagerData();
764
        }
765
766
        if($this->getID() != $origID) $this->setExtension($origID);
767
    }
768
769
    /**
770
     * Read the manager.dat file
771
     */
772
    protected function readManagerData() {
773
        $managerpath = $this->getInstallDir().'/manager.dat';
774
        if (is_readable($managerpath)) {
775
            $file = @file($managerpath);
776
            if(!empty($file)) {
777
                foreach($file as $line) {
778
                    list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
779
                    $key = trim($key);
780
                    $value = trim($value);
781
                    // backwards compatible with old plugin manager
782
                    if($key == 'url') $key = 'downloadurl';
783
                    $this->managerData[$key] = $value;
784
                }
785
            }
786
        }
787
    }
788
789
    /**
790
     * Write the manager.data file
791
     */
792
    protected function writeManagerData() {
793
        $managerpath = $this->getInstallDir().'/manager.dat';
794
        $data = '';
795
        foreach ($this->managerData as $k => $v) {
796
            $data .= $k.'='.$v.DOKU_LF;
797
        }
798
        io_saveFile($managerpath, $data);
799
    }
800
801
    /**
802
     * Returns a temporary directory
803
     *
804
     * The directory is registered for cleanup when the class is destroyed
805
     *
806
     * @return false|string
807
     */
808
    protected function mkTmpDir(){
809
        $dir = io_mktmpdir();
810
        if(!$dir) return false;
811
        $this->temporary[] = $dir;
812
        return $dir;
813
    }
814
815
    /**
816
     * Download an archive to a protected path
817
     *
818
     * @param string $url  The url to get the archive from
819
     * @throws Exception   when something goes wrong
820
     * @return string The path where the archive was saved
821
     */
822
    public function download($url) {
823
        // check the url
824
        if(!preg_match('/https?:\/\//i', $url)){
825
            throw new Exception($this->getLang('error_badurl'));
826
        }
827
828
        // try to get the file from the path (used as plugin name fallback)
829
        $file = parse_url($url, PHP_URL_PATH);
830
        if(is_null($file)){
831
            $file = md5($url);
832
        }else{
833
            $file = utf8_basename($file);
834
        }
835
836
        // create tmp directory for download
837
        if(!($tmp = $this->mkTmpDir())) {
838
            throw new Exception($this->getLang('error_dircreate'));
839
        }
840
841
        // download
842
        if(!$file = io_download($url, $tmp.'/', true, $file, 0)) {
843
            io_rmdir($tmp, true);
844
            throw new Exception(sprintf($this->getLang('error_download'), '<bdi>'.hsc($url).'</bdi>'));
845
        }
846
847
        return $tmp.'/'.$file;
848
    }
849
850
    /**
851
     * @param string $file      The path to the archive that shall be installed
852
     * @param bool   $overwrite If an already installed plugin should be overwritten
853
     * @param string $base      The basename of the plugin if it's known
854
     * @throws Exception        when something went wrong
855
     * @return array            list of installed extensions
856
     */
857
    public function installArchive($file, $overwrite=false, $base = '') {
858
        $installed_extensions = array();
859
860
        // create tmp directory for decompression
861
        if(!($tmp = $this->mkTmpDir())) {
862
            throw new Exception($this->getLang('error_dircreate'));
863
        }
864
865
        // add default base folder if specified to handle case where zip doesn't contain this
866
        if($base && !@mkdir($tmp.'/'.$base)) {
867
            throw new Exception($this->getLang('error_dircreate'));
868
        }
869
870
        // decompress
871
        $this->decompress($file, "$tmp/".$base);
872
873
        // search $tmp/$base for the folder(s) that has been created
874
        // move the folder(s) to lib/..
875
        $result = array('old'=>array(), 'new'=>array());
876
        $default = ($this->isTemplate() ? 'template' : 'plugin');
877
        if(!$this->find_folders($result, $tmp.'/'.$base, $default)) {
878
            throw new Exception($this->getLang('error_findfolder'));
879
        }
880
881
        // choose correct result array
882
        if(count($result['new'])) {
883
            $install = $result['new'];
884
        }else{
885
            $install = $result['old'];
886
        }
887
888
        if(!count($install)){
889
            throw new Exception($this->getLang('error_findfolder'));
890
        }
891
892
        // now install all found items
893
        foreach($install as $item) {
894
            // where to install?
895
            if($item['type'] == 'template') {
896
                $target_base_dir = DOKU_TPLLIB;
897
            }else{
898
                $target_base_dir = DOKU_PLUGIN;
899
            }
900
901
            if(!empty($item['base'])) {
902
                // use base set in info.txt
903
            } elseif($base && count($install) == 1) {
904
                $item['base'] = $base;
905
            } else {
906
                // default - use directory as found in zip
907
                // plugins from github/master without *.info.txt will install in wrong folder
908
                // but using $info->id will make 'code3' fail (which should install in lib/code/..)
909
                $item['base'] = basename($item['tmp']);
910
            }
911
912
            // check to make sure we aren't overwriting anything
913
            $target = $target_base_dir.$item['base'];
914
            if(!$overwrite && file_exists($target)) {
915
                // TODO remember our settings, ask the user to confirm overwrite
916
                continue;
917
            }
918
919
            $action = file_exists($target) ? 'update' : 'install';
920
921
            // copy action
922
            if($this->dircopy($item['tmp'], $target)) {
923
                // return info
924
                $id = $item['base'];
925
                if($item['type'] == 'template') {
926
                    $id = 'template:'.$id;
927
                }
928
                $installed_extensions[$id] = array(
929
                    'base' => $item['base'],
930
                    'type' => $item['type'],
931
                    'action' => $action
932
                );
933
            } else {
934
                throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, '<bdi>'.$item['base'].'</bdi>'));
935
            }
936
        }
937
938
        // cleanup
939
        if($tmp) io_rmdir($tmp, true);
940
941
        return $installed_extensions;
942
    }
943
944
    /**
945
     * Find out what was in the extracted directory
946
     *
947
     * Correct folders are searched recursively using the "*.info.txt" configs
948
     * as indicator for a root folder. When such a file is found, it's base
949
     * setting is used (when set). All folders found by this method are stored
950
     * in the 'new' key of the $result array.
951
     *
952
     * For backwards compatibility all found top level folders are stored as
953
     * in the 'old' key of the $result array.
954
     *
955
     * When no items are found in 'new' the copy mechanism should fall back
956
     * the 'old' list.
957
     *
958
     * @author Andreas Gohr <[email protected]>
959
     * @param array $result - results are stored here
960
     * @param string $directory - the temp directory where the package was unpacked to
961
     * @param string $default_type - type used if no info.txt available
962
     * @param string $subdir - a subdirectory. do not set. used by recursion
963
     * @return bool - false on error
964
     */
965
    protected function find_folders(&$result, $directory, $default_type='plugin', $subdir='') {
966
        $this_dir = "$directory$subdir";
967
        $dh       = @opendir($this_dir);
968
        if(!$dh) return false;
969
970
        $found_dirs           = array();
971
        $found_files          = 0;
972
        $found_template_parts = 0;
973
        while (false !== ($f = readdir($dh))) {
974
            if($f == '.' || $f == '..') continue;
975
976
            if(is_dir("$this_dir/$f")) {
977
                $found_dirs[] = "$subdir/$f";
978
979
            } else {
980
                // it's a file -> check for config
981
                $found_files++;
982
                switch ($f) {
983
                    case 'plugin.info.txt':
984
                    case 'template.info.txt':
985
                        // we have  found a clear marker, save and return
986
                        $info = array();
987
                        $type = explode('.', $f, 2);
988
                        $info['type'] = $type[0];
989
                        $info['tmp']  = $this_dir;
990
                        $conf = confToHash("$this_dir/$f");
991
                        $info['base'] = basename($conf['base']);
992
                        $result['new'][] = $info;
993
                        return true;
994
995
                    case 'main.php':
996
                    case 'details.php':
997
                    case 'mediamanager.php':
998
                    case 'style.ini':
999
                        $found_template_parts++;
1000
                        break;
1001
                }
1002
            }
1003
        }
1004
        closedir($dh);
1005
1006
        // files where found but no info.txt - use old method
1007
        if($found_files){
1008
            $info            = array();
1009
            $info['tmp']     = $this_dir;
1010
            // does this look like a template or should we use the default type?
1011
            if($found_template_parts >= 2) {
1012
                $info['type']    = 'template';
1013
            } else {
1014
                $info['type']    = $default_type;
1015
            }
1016
1017
            $result['old'][] = $info;
1018
            return true;
1019
        }
1020
1021
        // we have no files yet -> recurse
1022
        foreach ($found_dirs as $found_dir) {
1023
            $this->find_folders($result, $directory, $default_type, "$found_dir");
1024
        }
1025
        return true;
1026
    }
1027
1028
    /**
1029
     * Decompress a given file to the given target directory
1030
     *
1031
     * Determines the compression type from the file extension
1032
     *
1033
     * @param string $file   archive to extract
1034
     * @param string $target directory to extract to
1035
     * @throws Exception
1036
     * @return bool
1037
     */
1038
    private function decompress($file, $target) {
1039
        // decompression library doesn't like target folders ending in "/"
1040
        if(substr($target, -1) == "/") $target = substr($target, 0, -1);
1041
1042
        $ext = $this->guess_archive($file);
1043
        if(in_array($ext, array('tar', 'bz', 'gz'))) {
1044
1045
            try {
1046
                $tar = new \splitbrain\PHPArchive\Tar();
1047
                $tar->open($file);
1048
                $tar->extract($target);
1049
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1050
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1051
            }
1052
1053
            return true;
1054
        } elseif($ext == 'zip') {
1055
1056
            try {
1057
                $zip = new \splitbrain\PHPArchive\Zip();
1058
                $zip->open($file);
1059
                $zip->extract($target);
1060
            } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1061
                throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1062
            }
1063
1064
            return true;
1065
        }
1066
1067
        // the only case when we don't get one of the recognized archive types is when the archive file can't be read
1068
        throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
1069
    }
1070
1071
    /**
1072
     * Determine the archive type of the given file
1073
     *
1074
     * Reads the first magic bytes of the given file for content type guessing,
1075
     * if neither bz, gz or zip are recognized, tar is assumed.
1076
     *
1077
     * @author Andreas Gohr <[email protected]>
1078
     * @param string $file The file to analyze
1079
     * @return string|false false if the file can't be read, otherwise an "extension"
1080
     */
1081
    private function guess_archive($file) {
1082
        $fh = fopen($file, 'rb');
1083
        if(!$fh) return false;
1084
        $magic = fread($fh, 5);
1085
        fclose($fh);
1086
1087
        if(strpos($magic, "\x42\x5a") === 0) return 'bz';
1088
        if(strpos($magic, "\x1f\x8b") === 0) return 'gz';
1089
        if(strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
1090
        return 'tar';
1091
    }
1092
1093
    /**
1094
     * Copy with recursive sub-directory support
1095
     *
1096
     * @param string $src filename path to file
1097
     * @param string $dst filename path to file
1098
     * @return bool|int|string
1099
     */
1100
    private function dircopy($src, $dst) {
1101
        global $conf;
1102
1103
        if(is_dir($src)) {
1104
            if(!$dh = @opendir($src)) return false;
1105
1106
            if($ok = io_mkdir_p($dst)) {
1107
                while ($ok && (false !== ($f = readdir($dh)))) {
1108
                    if($f == '..' || $f == '.') continue;
1109
                    $ok = $this->dircopy("$src/$f", "$dst/$f");
1110
                }
1111
            }
1112
1113
            closedir($dh);
1114
            return $ok;
1115
1116
        } else {
1117
            $exists = file_exists($dst);
1118
1119
            if(!@copy($src, $dst)) return false;
1120
            if(!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']);
1121
            @touch($dst, filemtime($src));
1122
        }
1123
1124
        return true;
1125
    }
1126
1127
    /**
1128
     * Delete outdated files from updated plugins
1129
     *
1130
     * @param array $installed
1131
     */
1132
    private function removeDeletedfiles($installed) {
1133
        foreach($installed as $id => $extension) {
1134
            // only on update
1135
            if($extension['action'] == 'install') continue;
1136
1137
            // get definition file
1138
            if($extension['type'] == 'template') {
1139
                $extensiondir = DOKU_TPLLIB;
1140
            }else{
1141
                $extensiondir = DOKU_PLUGIN;
1142
            }
1143
            $extensiondir = $extensiondir . $extension['base'] .'/';
1144
            $definitionfile = $extensiondir . 'deleted.files';
1145
            if(!file_exists($definitionfile)) continue;
1146
1147
            // delete the old files
1148
            $list = file($definitionfile);
1149
1150
            foreach($list as $line) {
1151
                $line = trim(preg_replace('/#.*$/', '', $line));
1152
                if(!$line) continue;
1153
                $file = $extensiondir . $line;
1154
                if(!file_exists($file)) continue;
1155
1156
                io_rmdir($file, true);
1157
            }
1158
        }
1159
    }
1160
}
1161
1162
// vim:ts=4:sw=4:et:
1163