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

lib/plugins/extension/helper/extension.php (1 issue)

Upgrade to new PHP Analysis Engine

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

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