Completed
Push — HTTPClientAgent ( d2b05e...bf209a )
by
unknown
17:44 queued 13:15
created

downloadToFile()   B

Complexity

Conditions 10
Paths 22

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 22
nop 3
dl 0
loc 38
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
Bug introduced by
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);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->localInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
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'));
0 ignored issues
show
Bug Best Practice introduced by
The expression $tmp of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression $dir of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

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

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

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

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