xref: /dokuwiki/lib/plugins/extension/Extension.php (revision b2a05b76de6c1d1e38212dff43776aaa41a22894)
1cf2dcf1bSAndreas Gohr<?php
2cf2dcf1bSAndreas Gohr
3cf2dcf1bSAndreas Gohrnamespace dokuwiki\plugin\extension;
4cf2dcf1bSAndreas Gohr
5cf2dcf1bSAndreas Gohruse dokuwiki\Extension\PluginController;
6cf2dcf1bSAndreas Gohruse dokuwiki\Utf8\PhpString;
7cf2dcf1bSAndreas Gohruse RuntimeException;
8cf2dcf1bSAndreas Gohr
9cf2dcf1bSAndreas Gohrclass Extension
10cf2dcf1bSAndreas Gohr{
11cf2dcf1bSAndreas Gohr    const TYPE_PLUGIN = 'plugin';
12cf2dcf1bSAndreas Gohr    const TYPE_TEMPLATE = 'template';
13cf2dcf1bSAndreas Gohr
14cf2dcf1bSAndreas Gohr    /** @var string "plugin"|"template" */
15cf2dcf1bSAndreas Gohr    protected string $type = self::TYPE_PLUGIN;
16cf2dcf1bSAndreas Gohr
17cf2dcf1bSAndreas Gohr    /** @var string The base name of this extension */
18cf2dcf1bSAndreas Gohr    protected string $base;
19cf2dcf1bSAndreas Gohr
2025d28a01SAndreas Gohr    /** @var string The current location of this extension */
2125d28a01SAndreas Gohr    protected string $currentDir = '';
22cf2dcf1bSAndreas Gohr
23cf2dcf1bSAndreas Gohr    /** @var array The local info array of the extension */
24cf2dcf1bSAndreas Gohr    protected array $localInfo = [];
25cf2dcf1bSAndreas Gohr
26cf2dcf1bSAndreas Gohr    /** @var array The remote info array of the extension */
27cf2dcf1bSAndreas Gohr    protected array $remoteInfo = [];
28cf2dcf1bSAndreas Gohr
297c9966a5SAndreas Gohr    /** @var Manager|null The manager for this extension */
3025d28a01SAndreas Gohr    protected ?Manager $manager = null;
31cf2dcf1bSAndreas Gohr
32cf2dcf1bSAndreas Gohr    // region Constructors
33cf2dcf1bSAndreas Gohr
34cf2dcf1bSAndreas Gohr    /**
35cf2dcf1bSAndreas Gohr     * The main constructor is private to force the use of the factory methods
36cf2dcf1bSAndreas Gohr     */
37cf2dcf1bSAndreas Gohr    protected function __construct()
38cf2dcf1bSAndreas Gohr    {
39cf2dcf1bSAndreas Gohr    }
40cf2dcf1bSAndreas Gohr
41cf2dcf1bSAndreas Gohr    /**
42a1e045f7SAndreas Gohr     * Initializes an extension from an id
43a1e045f7SAndreas Gohr     *
44a1e045f7SAndreas Gohr     * @param string $id The id of the extension
45a1e045f7SAndreas Gohr     * @return Extension
46a1e045f7SAndreas Gohr     */
47a1e045f7SAndreas Gohr    public static function createFromId($id)
48a1e045f7SAndreas Gohr    {
49a1e045f7SAndreas Gohr        $extension = new self();
50a1e045f7SAndreas Gohr        $extension->initFromId($id);
51a1e045f7SAndreas Gohr        return $extension;
52a1e045f7SAndreas Gohr    }
53a1e045f7SAndreas Gohr
54a1e045f7SAndreas Gohr    protected function initFromId($id)
55a1e045f7SAndreas Gohr    {
56a1e045f7SAndreas Gohr        [$type, $base] = $this->idToTypeBase($id);
57a1e045f7SAndreas Gohr        $this->type = $type;
58a1e045f7SAndreas Gohr        $this->base = $base;
59a1e045f7SAndreas Gohr        $this->readLocalInfo();
60a1e045f7SAndreas Gohr    }
61a1e045f7SAndreas Gohr
62a1e045f7SAndreas Gohr    /**
63cf2dcf1bSAndreas Gohr     * Initializes an extension from a directory
64cf2dcf1bSAndreas Gohr     *
65cf2dcf1bSAndreas Gohr     * The given directory might be the one where the extension has already been installed to
66cf2dcf1bSAndreas Gohr     * or it might be the extracted source in some temporary directory.
67cf2dcf1bSAndreas Gohr     *
68cf2dcf1bSAndreas Gohr     * @param string $dir Where the extension code is currently located
69cf2dcf1bSAndreas Gohr     * @param string|null $type TYPE_PLUGIN|TYPE_TEMPLATE, null for auto-detection
70cf2dcf1bSAndreas Gohr     * @param string $base The base name of the extension, null for auto-detection
71cf2dcf1bSAndreas Gohr     * @return Extension
72cf2dcf1bSAndreas Gohr     */
73cf2dcf1bSAndreas Gohr    public static function createFromDirectory($dir, $type = null, $base = null)
74cf2dcf1bSAndreas Gohr    {
75cf2dcf1bSAndreas Gohr        $extension = new self();
76cf2dcf1bSAndreas Gohr        $extension->initFromDirectory($dir, $type, $base);
77cf2dcf1bSAndreas Gohr        return $extension;
78cf2dcf1bSAndreas Gohr    }
79cf2dcf1bSAndreas Gohr
80cf2dcf1bSAndreas Gohr    protected function initFromDirectory($dir, $type = null, $base = null)
81cf2dcf1bSAndreas Gohr    {
82cf2dcf1bSAndreas Gohr        if (!is_dir($dir)) throw new RuntimeException('Directory not found: ' . $dir);
83cf2dcf1bSAndreas Gohr        $this->currentDir = realpath($dir);
84cf2dcf1bSAndreas Gohr
85cf2dcf1bSAndreas Gohr        if ($type === null || $type === self::TYPE_TEMPLATE) {
86cf2dcf1bSAndreas Gohr            if (
87cf2dcf1bSAndreas Gohr                file_exists($dir . '/template.info.php') ||
88cf2dcf1bSAndreas Gohr                file_exists($dir . '/style.ini') ||
89cf2dcf1bSAndreas Gohr                file_exists($dir . '/main.php') ||
90cf2dcf1bSAndreas Gohr                file_exists($dir . '/detail.php') ||
91cf2dcf1bSAndreas Gohr                file_exists($dir . '/mediamanager.php')
92cf2dcf1bSAndreas Gohr            ) {
93cf2dcf1bSAndreas Gohr                $this->type = self::TYPE_TEMPLATE;
94cf2dcf1bSAndreas Gohr            }
95cf2dcf1bSAndreas Gohr        } else {
96cf2dcf1bSAndreas Gohr            $this->type = self::TYPE_PLUGIN;
97cf2dcf1bSAndreas Gohr        }
98cf2dcf1bSAndreas Gohr
99cf2dcf1bSAndreas Gohr        $this->readLocalInfo();
100cf2dcf1bSAndreas Gohr
101cf2dcf1bSAndreas Gohr        if ($base !== null) {
102cf2dcf1bSAndreas Gohr            $this->base = $base;
103cf2dcf1bSAndreas Gohr        } elseif (isset($this->localInfo['base'])) {
104cf2dcf1bSAndreas Gohr            $this->base = $this->localInfo['base'];
105cf2dcf1bSAndreas Gohr        } else {
106cf2dcf1bSAndreas Gohr            $this->base = basename($dir);
107cf2dcf1bSAndreas Gohr        }
108cf2dcf1bSAndreas Gohr    }
109cf2dcf1bSAndreas Gohr
110cf2dcf1bSAndreas Gohr    /**
111cf2dcf1bSAndreas Gohr     * Initializes an extension from remote data
112cf2dcf1bSAndreas Gohr     *
113cf2dcf1bSAndreas Gohr     * @param array $data The data as returned by the repository api
114cf2dcf1bSAndreas Gohr     * @return Extension
115cf2dcf1bSAndreas Gohr     */
116cf2dcf1bSAndreas Gohr    public static function createFromRemoteData($data)
117cf2dcf1bSAndreas Gohr    {
118cf2dcf1bSAndreas Gohr        $extension = new self();
119cf2dcf1bSAndreas Gohr        $extension->initFromRemoteData($data);
120cf2dcf1bSAndreas Gohr        return $extension;
121cf2dcf1bSAndreas Gohr    }
122cf2dcf1bSAndreas Gohr
123cf2dcf1bSAndreas Gohr    protected function initFromRemoteData($data)
124cf2dcf1bSAndreas Gohr    {
125cf2dcf1bSAndreas Gohr        if (!isset($data['plugin'])) throw new RuntimeException('Invalid remote data');
126cf2dcf1bSAndreas Gohr
127a1e045f7SAndreas Gohr        [$type, $base] = $this->idToTypeBase($data['plugin']);
128cf2dcf1bSAndreas Gohr        $this->remoteInfo = $data;
129cf2dcf1bSAndreas Gohr        $this->type = $type;
130cf2dcf1bSAndreas Gohr        $this->base = $base;
131cf2dcf1bSAndreas Gohr
132cf2dcf1bSAndreas Gohr        if ($this->isInstalled()) {
133cf2dcf1bSAndreas Gohr            $this->currentDir = $this->getInstallDir();
134cf2dcf1bSAndreas Gohr            $this->readLocalInfo();
135cf2dcf1bSAndreas Gohr        }
136cf2dcf1bSAndreas Gohr    }
137cf2dcf1bSAndreas Gohr
138cf2dcf1bSAndreas Gohr    // endregion
139cf2dcf1bSAndreas Gohr
140cf2dcf1bSAndreas Gohr    // region Getters
141cf2dcf1bSAndreas Gohr
142cf2dcf1bSAndreas Gohr    /**
143cf2dcf1bSAndreas Gohr     * @return string The extension id (same as base but prefixed with "template:" for templates)
144cf2dcf1bSAndreas Gohr     */
145cf2dcf1bSAndreas Gohr    public function getId()
146cf2dcf1bSAndreas Gohr    {
147cf2dcf1bSAndreas Gohr        if ($this->type === self::TYPE_TEMPLATE) {
148cf2dcf1bSAndreas Gohr            return self::TYPE_TEMPLATE . ':' . $this->base;
149cf2dcf1bSAndreas Gohr        }
150cf2dcf1bSAndreas Gohr        return $this->base;
151cf2dcf1bSAndreas Gohr    }
152cf2dcf1bSAndreas Gohr
153cf2dcf1bSAndreas Gohr    /**
154cf2dcf1bSAndreas Gohr     * Get the base name of this extension
155cf2dcf1bSAndreas Gohr     *
156cf2dcf1bSAndreas Gohr     * @return string
157cf2dcf1bSAndreas Gohr     */
158cf2dcf1bSAndreas Gohr    public function getBase()
159cf2dcf1bSAndreas Gohr    {
160cf2dcf1bSAndreas Gohr        return $this->base;
161cf2dcf1bSAndreas Gohr    }
162cf2dcf1bSAndreas Gohr
163cf2dcf1bSAndreas Gohr    /**
164cf2dcf1bSAndreas Gohr     * Get the type of the extension
165cf2dcf1bSAndreas Gohr     *
166cf2dcf1bSAndreas Gohr     * @return string "plugin"|"template"
167cf2dcf1bSAndreas Gohr     */
168cf2dcf1bSAndreas Gohr    public function getType()
169cf2dcf1bSAndreas Gohr    {
170cf2dcf1bSAndreas Gohr        return $this->type;
171cf2dcf1bSAndreas Gohr    }
172cf2dcf1bSAndreas Gohr
173cf2dcf1bSAndreas Gohr    /**
174cf2dcf1bSAndreas Gohr     * The current directory of the extension
175cf2dcf1bSAndreas Gohr     *
176cf2dcf1bSAndreas Gohr     * @return string|null
177cf2dcf1bSAndreas Gohr     */
178cf2dcf1bSAndreas Gohr    public function getCurrentDir()
179cf2dcf1bSAndreas Gohr    {
180cf2dcf1bSAndreas Gohr        // recheck that the current currentDir is still valid
181cf2dcf1bSAndreas Gohr        if ($this->currentDir && !is_dir($this->currentDir)) {
18225d28a01SAndreas Gohr            $this->currentDir = '';
183cf2dcf1bSAndreas Gohr        }
184cf2dcf1bSAndreas Gohr
185cf2dcf1bSAndreas Gohr        // if the extension is installed, then the currentDir is the install dir!
186cf2dcf1bSAndreas Gohr        if (!$this->currentDir && $this->isInstalled()) {
187cf2dcf1bSAndreas Gohr            $this->currentDir = $this->getInstallDir();
188cf2dcf1bSAndreas Gohr        }
189cf2dcf1bSAndreas Gohr
190cf2dcf1bSAndreas Gohr        return $this->currentDir;
191cf2dcf1bSAndreas Gohr    }
192cf2dcf1bSAndreas Gohr
193cf2dcf1bSAndreas Gohr    /**
194cf2dcf1bSAndreas Gohr     * Get the directory where this extension should be installed in
195cf2dcf1bSAndreas Gohr     *
196cf2dcf1bSAndreas Gohr     * Note: this does not mean that the extension is actually installed there
197cf2dcf1bSAndreas Gohr     *
198cf2dcf1bSAndreas Gohr     * @return string
199cf2dcf1bSAndreas Gohr     */
200cf2dcf1bSAndreas Gohr    public function getInstallDir()
201cf2dcf1bSAndreas Gohr    {
202cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) {
203cf2dcf1bSAndreas Gohr            $dir = dirname(tpl_incdir()) . $this->base;
204cf2dcf1bSAndreas Gohr        } else {
205cf2dcf1bSAndreas Gohr            $dir = DOKU_PLUGIN . $this->base;
206cf2dcf1bSAndreas Gohr        }
207cf2dcf1bSAndreas Gohr
20825d28a01SAndreas Gohr        return fullpath($dir);
209cf2dcf1bSAndreas Gohr    }
210cf2dcf1bSAndreas Gohr
211cf2dcf1bSAndreas Gohr
212cf2dcf1bSAndreas Gohr    /**
213cf2dcf1bSAndreas Gohr     * Get the display name of the extension
214cf2dcf1bSAndreas Gohr     *
215cf2dcf1bSAndreas Gohr     * @return string
216cf2dcf1bSAndreas Gohr     */
217cf2dcf1bSAndreas Gohr    public function getDisplayName()
218cf2dcf1bSAndreas Gohr    {
219cf2dcf1bSAndreas Gohr        return $this->getTag('name', PhpString::ucwords($this->getBase() . ' ' . $this->getType()));
220cf2dcf1bSAndreas Gohr    }
221cf2dcf1bSAndreas Gohr
222cf2dcf1bSAndreas Gohr    /**
223cf2dcf1bSAndreas Gohr     * Get the author name of the extension
224cf2dcf1bSAndreas Gohr     *
225cf2dcf1bSAndreas Gohr     * @return string Returns an empty string if the author info is missing
226cf2dcf1bSAndreas Gohr     */
227cf2dcf1bSAndreas Gohr    public function getAuthor()
228cf2dcf1bSAndreas Gohr    {
229cf2dcf1bSAndreas Gohr        return $this->getTag('author');
230cf2dcf1bSAndreas Gohr    }
231cf2dcf1bSAndreas Gohr
232cf2dcf1bSAndreas Gohr    /**
233cf2dcf1bSAndreas Gohr     * Get the email of the author of the extension if there is any
234cf2dcf1bSAndreas Gohr     *
235cf2dcf1bSAndreas Gohr     * @return string Returns an empty string if the email info is missing
236cf2dcf1bSAndreas Gohr     */
237cf2dcf1bSAndreas Gohr    public function getEmail()
238cf2dcf1bSAndreas Gohr    {
239cf2dcf1bSAndreas Gohr        // email is only in the local data
240cf2dcf1bSAndreas Gohr        return $this->localInfo['email'] ?? '';
241cf2dcf1bSAndreas Gohr    }
242cf2dcf1bSAndreas Gohr
243cf2dcf1bSAndreas Gohr    /**
244cf2dcf1bSAndreas Gohr     * Get the email id, i.e. the md5sum of the email
245cf2dcf1bSAndreas Gohr     *
246cf2dcf1bSAndreas Gohr     * @return string Empty string if no email is available
247cf2dcf1bSAndreas Gohr     */
248cf2dcf1bSAndreas Gohr    public function getEmailID()
249cf2dcf1bSAndreas Gohr    {
250cf2dcf1bSAndreas Gohr        if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
251cf2dcf1bSAndreas Gohr        if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
252cf2dcf1bSAndreas Gohr        return '';
253cf2dcf1bSAndreas Gohr    }
254cf2dcf1bSAndreas Gohr
255cf2dcf1bSAndreas Gohr    /**
256cf2dcf1bSAndreas Gohr     * Get the description of the extension
257cf2dcf1bSAndreas Gohr     *
258cf2dcf1bSAndreas Gohr     * @return string Empty string if no description is available
259cf2dcf1bSAndreas Gohr     */
260cf2dcf1bSAndreas Gohr    public function getDescription()
261cf2dcf1bSAndreas Gohr    {
262cf2dcf1bSAndreas Gohr        return $this->getTag(['desc', 'description']);
263cf2dcf1bSAndreas Gohr    }
264cf2dcf1bSAndreas Gohr
265cf2dcf1bSAndreas Gohr    /**
266cf2dcf1bSAndreas Gohr     * Get the URL of the extension, usually a page on dokuwiki.org
267cf2dcf1bSAndreas Gohr     *
268cf2dcf1bSAndreas Gohr     * @return string
269cf2dcf1bSAndreas Gohr     */
270cf2dcf1bSAndreas Gohr    public function getURL()
271cf2dcf1bSAndreas Gohr    {
272cf2dcf1bSAndreas Gohr        return $this->getTag(
273cf2dcf1bSAndreas Gohr            'url',
274cf2dcf1bSAndreas Gohr            'https://www.dokuwiki.org/' .
275cf2dcf1bSAndreas Gohr            ($this->isTemplate() ? 'template' : 'plugin') . ':' . $this->getBase()
276cf2dcf1bSAndreas Gohr        );
277cf2dcf1bSAndreas Gohr    }
278cf2dcf1bSAndreas Gohr
279cf2dcf1bSAndreas Gohr    /**
2807c9966a5SAndreas Gohr     * Get the version of the extension that is actually installed
2817c9966a5SAndreas Gohr     *
2827c9966a5SAndreas Gohr     * Returns an empty string if the version is not available
2837c9966a5SAndreas Gohr     *
2847c9966a5SAndreas Gohr     * @return string
2857c9966a5SAndreas Gohr     */
2867c9966a5SAndreas Gohr    public function getInstalledVersion()
2877c9966a5SAndreas Gohr    {
2887c9966a5SAndreas Gohr        return $this->localInfo['date'] ?? '';
2897c9966a5SAndreas Gohr    }
2907c9966a5SAndreas Gohr
2917c9966a5SAndreas Gohr    /**
29225d28a01SAndreas Gohr     * Get a list of extension ids this extension depends on
29325d28a01SAndreas Gohr     *
29425d28a01SAndreas Gohr     * @return string[]
29525d28a01SAndreas Gohr     */
29625d28a01SAndreas Gohr    public function getDependencyList()
29725d28a01SAndreas Gohr    {
29825d28a01SAndreas Gohr        return $this->getTag('depends', []);
29925d28a01SAndreas Gohr    }
30025d28a01SAndreas Gohr
30125d28a01SAndreas Gohr    /**
302*b2a05b76SAndreas Gohr     * Return the minimum PHP version required by the extension
303*b2a05b76SAndreas Gohr     *
304*b2a05b76SAndreas Gohr     * Empty if not set
305*b2a05b76SAndreas Gohr     *
306*b2a05b76SAndreas Gohr     * @return string
307*b2a05b76SAndreas Gohr     */
308*b2a05b76SAndreas Gohr    public function getMinimumPHPVersion()
309*b2a05b76SAndreas Gohr    {
310*b2a05b76SAndreas Gohr        return $this->getTag('phpmin', '');
311*b2a05b76SAndreas Gohr    }
312*b2a05b76SAndreas Gohr
313*b2a05b76SAndreas Gohr    /**
314*b2a05b76SAndreas Gohr     * Return the minimum PHP version supported by the extension
315*b2a05b76SAndreas Gohr     *
316*b2a05b76SAndreas Gohr     * @return string
317*b2a05b76SAndreas Gohr     */
318*b2a05b76SAndreas Gohr    public function getMaximumPHPVersion()
319*b2a05b76SAndreas Gohr    {
320*b2a05b76SAndreas Gohr        return $this->getTag('phpmax', '');
321*b2a05b76SAndreas Gohr    }
322*b2a05b76SAndreas Gohr
323*b2a05b76SAndreas Gohr    /**
324cf2dcf1bSAndreas Gohr     * Is this extension a template?
325cf2dcf1bSAndreas Gohr     *
326cf2dcf1bSAndreas Gohr     * @return bool false if it is a plugin
327cf2dcf1bSAndreas Gohr     */
328cf2dcf1bSAndreas Gohr    public function isTemplate()
329cf2dcf1bSAndreas Gohr    {
330cf2dcf1bSAndreas Gohr        return $this->type === self::TYPE_TEMPLATE;
331cf2dcf1bSAndreas Gohr    }
332cf2dcf1bSAndreas Gohr
333cf2dcf1bSAndreas Gohr    /**
334cf2dcf1bSAndreas Gohr     * Is the extension installed locally?
335cf2dcf1bSAndreas Gohr     *
336cf2dcf1bSAndreas Gohr     * @return bool
337cf2dcf1bSAndreas Gohr     */
338cf2dcf1bSAndreas Gohr    public function isInstalled()
339cf2dcf1bSAndreas Gohr    {
340cf2dcf1bSAndreas Gohr        return is_dir($this->getInstallDir());
341cf2dcf1bSAndreas Gohr    }
342cf2dcf1bSAndreas Gohr
343cf2dcf1bSAndreas Gohr    /**
344cf2dcf1bSAndreas Gohr     * Is the extension under git control?
345cf2dcf1bSAndreas Gohr     *
346cf2dcf1bSAndreas Gohr     * @return bool
347cf2dcf1bSAndreas Gohr     */
348cf2dcf1bSAndreas Gohr    public function isGitControlled()
349cf2dcf1bSAndreas Gohr    {
350cf2dcf1bSAndreas Gohr        if (!$this->isInstalled()) return false;
351cf2dcf1bSAndreas Gohr        return file_exists($this->getInstallDir() . '/.git');
352cf2dcf1bSAndreas Gohr    }
353cf2dcf1bSAndreas Gohr
354cf2dcf1bSAndreas Gohr    /**
355cf2dcf1bSAndreas Gohr     * If the extension is bundled
356cf2dcf1bSAndreas Gohr     *
357cf2dcf1bSAndreas Gohr     * @return bool If the extension is bundled
358cf2dcf1bSAndreas Gohr     */
359cf2dcf1bSAndreas Gohr    public function isBundled()
360cf2dcf1bSAndreas Gohr    {
361cf2dcf1bSAndreas Gohr        $this->loadRemoteInfo();
362cf2dcf1bSAndreas Gohr        return $this->remoteInfo['bundled'] ?? in_array(
363cf2dcf1bSAndreas Gohr            $this->getId(),
364cf2dcf1bSAndreas Gohr            [
365cf2dcf1bSAndreas Gohr                'authad',
366cf2dcf1bSAndreas Gohr                'authldap',
367cf2dcf1bSAndreas Gohr                'authpdo',
368cf2dcf1bSAndreas Gohr                'authplain',
369cf2dcf1bSAndreas Gohr                'acl',
370cf2dcf1bSAndreas Gohr                'config',
371cf2dcf1bSAndreas Gohr                'extension',
372cf2dcf1bSAndreas Gohr                'info',
373cf2dcf1bSAndreas Gohr                'popularity',
374cf2dcf1bSAndreas Gohr                'revert',
375cf2dcf1bSAndreas Gohr                'safefnrecode',
376cf2dcf1bSAndreas Gohr                'styling',
377cf2dcf1bSAndreas Gohr                'testing',
378cf2dcf1bSAndreas Gohr                'usermanager',
379cf2dcf1bSAndreas Gohr                'logviewer',
380cf2dcf1bSAndreas Gohr                'template:dokuwiki'
381cf2dcf1bSAndreas Gohr            ]
382cf2dcf1bSAndreas Gohr        );
383cf2dcf1bSAndreas Gohr    }
384cf2dcf1bSAndreas Gohr
385cf2dcf1bSAndreas Gohr    /**
386cf2dcf1bSAndreas Gohr     * Is the extension protected against any modification (disable/uninstall)
387cf2dcf1bSAndreas Gohr     *
388cf2dcf1bSAndreas Gohr     * @return bool if the extension is protected
389cf2dcf1bSAndreas Gohr     */
390cf2dcf1bSAndreas Gohr    public function isProtected()
391cf2dcf1bSAndreas Gohr    {
392cf2dcf1bSAndreas Gohr        // never allow deinstalling the current auth plugin:
393cf2dcf1bSAndreas Gohr        global $conf;
394cf2dcf1bSAndreas Gohr        if ($this->getId() == $conf['authtype']) return true;
395cf2dcf1bSAndreas Gohr
396cf2dcf1bSAndreas Gohr        // FIXME disallow current template to be uninstalled
397cf2dcf1bSAndreas Gohr
398cf2dcf1bSAndreas Gohr        /** @var PluginController $plugin_controller */
399cf2dcf1bSAndreas Gohr        global $plugin_controller;
400cf2dcf1bSAndreas Gohr        $cascade = $plugin_controller->getCascade();
401cf2dcf1bSAndreas Gohr        return ($cascade['protected'][$this->getId()] ?? false);
402cf2dcf1bSAndreas Gohr    }
403cf2dcf1bSAndreas Gohr
404cf2dcf1bSAndreas Gohr    /**
405cf2dcf1bSAndreas Gohr     * Is the extension installed in the correct directory?
406cf2dcf1bSAndreas Gohr     *
407cf2dcf1bSAndreas Gohr     * @return bool
408cf2dcf1bSAndreas Gohr     */
409cf2dcf1bSAndreas Gohr    public function isInWrongFolder()
410cf2dcf1bSAndreas Gohr    {
411cf2dcf1bSAndreas Gohr        return $this->getInstallDir() != $this->currentDir;
412cf2dcf1bSAndreas Gohr    }
413cf2dcf1bSAndreas Gohr
414cf2dcf1bSAndreas Gohr    /**
415cf2dcf1bSAndreas Gohr     * Is the extension enabled?
416cf2dcf1bSAndreas Gohr     *
417cf2dcf1bSAndreas Gohr     * @return bool
418cf2dcf1bSAndreas Gohr     */
419cf2dcf1bSAndreas Gohr    public function isEnabled()
420cf2dcf1bSAndreas Gohr    {
421cf2dcf1bSAndreas Gohr        global $conf;
422cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) {
423cf2dcf1bSAndreas Gohr            return ($conf['template'] == $this->getBase());
424cf2dcf1bSAndreas Gohr        }
425cf2dcf1bSAndreas Gohr
426cf2dcf1bSAndreas Gohr        /* @var PluginController $plugin_controller */
427cf2dcf1bSAndreas Gohr        global $plugin_controller;
428cf2dcf1bSAndreas Gohr        return $plugin_controller->isEnabled($this->base);
429cf2dcf1bSAndreas Gohr    }
430cf2dcf1bSAndreas Gohr
431160d3688SAndreas Gohr    /**
432160d3688SAndreas Gohr     * Has the download URL changed since the last download?
433160d3688SAndreas Gohr     *
434160d3688SAndreas Gohr     * @return bool
435160d3688SAndreas Gohr     */
436160d3688SAndreas Gohr    public function hasChangedURL()
437160d3688SAndreas Gohr    {
438160d3688SAndreas Gohr        $last = $this->getManager()->getDownloadUrl();
439160d3688SAndreas Gohr        if(!$last) return false;
440160d3688SAndreas Gohr        return $last !== $this->getDownloadURL();
441160d3688SAndreas Gohr    }
442160d3688SAndreas Gohr
443160d3688SAndreas Gohr    /**
444160d3688SAndreas Gohr     * Is an update available for this extension?
445160d3688SAndreas Gohr     *
446160d3688SAndreas Gohr     * @return bool
447160d3688SAndreas Gohr     */
448160d3688SAndreas Gohr    public function updateAvailable()
449160d3688SAndreas Gohr    {
450160d3688SAndreas Gohr        if($this->isBundled()) return false; // bundled extensions are never updated
451160d3688SAndreas Gohr        $self = $this->getInstalledVersion();
452160d3688SAndreas Gohr        $remote = $this->getLastUpdate();
453160d3688SAndreas Gohr        return $self < $remote;
454160d3688SAndreas Gohr    }
455160d3688SAndreas Gohr
456cf2dcf1bSAndreas Gohr    // endregion
457cf2dcf1bSAndreas Gohr
4587c9966a5SAndreas Gohr    // region Remote Info
4597c9966a5SAndreas Gohr
4607c9966a5SAndreas Gohr    /**
4617c9966a5SAndreas Gohr     * Get the date of the last available update
4627c9966a5SAndreas Gohr     *
4637c9966a5SAndreas Gohr     * @return string yyyy-mm-dd
4647c9966a5SAndreas Gohr     */
4657c9966a5SAndreas Gohr    public function getLastUpdate()
4667c9966a5SAndreas Gohr    {
4677c9966a5SAndreas Gohr        return $this->getRemoteTag('lastupdate');
4687c9966a5SAndreas Gohr    }
4697c9966a5SAndreas Gohr
4707c9966a5SAndreas Gohr    /**
4717c9966a5SAndreas Gohr     * Get a list of tags this extension is tagged with at dokuwiki.org
4727c9966a5SAndreas Gohr     *
4737c9966a5SAndreas Gohr     * @return string[]
4747c9966a5SAndreas Gohr     */
4757c9966a5SAndreas Gohr    public function getTags()
4767c9966a5SAndreas Gohr    {
4777c9966a5SAndreas Gohr        return $this->getRemoteTag('tags', []);
4787c9966a5SAndreas Gohr    }
4797c9966a5SAndreas Gohr
4807c9966a5SAndreas Gohr    /**
4817c9966a5SAndreas Gohr     * Get the popularity of the extension
4827c9966a5SAndreas Gohr     *
4837c9966a5SAndreas Gohr     * This is a float between 0 and 1
4847c9966a5SAndreas Gohr     *
4857c9966a5SAndreas Gohr     * @return float
4867c9966a5SAndreas Gohr     */
4877c9966a5SAndreas Gohr    public function getPopularity()
4887c9966a5SAndreas Gohr    {
4897c9966a5SAndreas Gohr        return (float)$this->getRemoteTag('popularity', 0);
4907c9966a5SAndreas Gohr    }
4917c9966a5SAndreas Gohr
4927c9966a5SAndreas Gohr    /**
4937c9966a5SAndreas Gohr     * Get the text of the update message if there is any
4947c9966a5SAndreas Gohr     *
4957c9966a5SAndreas Gohr     * @return string
4967c9966a5SAndreas Gohr     */
4977c9966a5SAndreas Gohr    public function getUpdateMessage()
4987c9966a5SAndreas Gohr    {
4997c9966a5SAndreas Gohr        return $this->getRemoteTag('updatemessage');
5007c9966a5SAndreas Gohr    }
5017c9966a5SAndreas Gohr
5027c9966a5SAndreas Gohr    /**
5037c9966a5SAndreas Gohr     * Get the text of the security warning if there is any
5047c9966a5SAndreas Gohr     *
5057c9966a5SAndreas Gohr     * @return string
5067c9966a5SAndreas Gohr     */
5077c9966a5SAndreas Gohr    public function getSecurityWarning()
5087c9966a5SAndreas Gohr    {
5097c9966a5SAndreas Gohr        return $this->getRemoteTag('securitywarning');
5107c9966a5SAndreas Gohr    }
5117c9966a5SAndreas Gohr
5127c9966a5SAndreas Gohr    /**
5137c9966a5SAndreas Gohr     * Get the text of the security issue if there is any
5147c9966a5SAndreas Gohr     *
5157c9966a5SAndreas Gohr     * @return string
5167c9966a5SAndreas Gohr     */
5177c9966a5SAndreas Gohr    public function getSecurityIssue()
5187c9966a5SAndreas Gohr    {
5197c9966a5SAndreas Gohr        return $this->getRemoteTag('securityissue');
5207c9966a5SAndreas Gohr    }
5217c9966a5SAndreas Gohr
5227c9966a5SAndreas Gohr    /**
5237c9966a5SAndreas Gohr     * Get the URL of the screenshot of the extension if there is any
5247c9966a5SAndreas Gohr     *
5257c9966a5SAndreas Gohr     * @return string
5267c9966a5SAndreas Gohr     */
5277c9966a5SAndreas Gohr    public function getScreenshotURL()
5287c9966a5SAndreas Gohr    {
5297c9966a5SAndreas Gohr        return $this->getRemoteTag('screenshoturl');
5307c9966a5SAndreas Gohr    }
5317c9966a5SAndreas Gohr
5327c9966a5SAndreas Gohr    /**
5337c9966a5SAndreas Gohr     * Get the URL of the thumbnail of the extension if there is any
5347c9966a5SAndreas Gohr     *
5357c9966a5SAndreas Gohr     * @return string
5367c9966a5SAndreas Gohr     */
5377c9966a5SAndreas Gohr    public function getThumbnailURL()
5387c9966a5SAndreas Gohr    {
5397c9966a5SAndreas Gohr        return $this->getRemoteTag('thumbnailurl');
5407c9966a5SAndreas Gohr    }
5417c9966a5SAndreas Gohr
5427c9966a5SAndreas Gohr    /**
5437c9966a5SAndreas Gohr     * Get the download URL of the extension if there is any
5447c9966a5SAndreas Gohr     *
5457c9966a5SAndreas Gohr     * @return string
5467c9966a5SAndreas Gohr     */
5477c9966a5SAndreas Gohr    public function getDownloadURL()
5487c9966a5SAndreas Gohr    {
5497c9966a5SAndreas Gohr        return $this->getRemoteTag('downloadurl');
5507c9966a5SAndreas Gohr    }
5517c9966a5SAndreas Gohr
5527c9966a5SAndreas Gohr    /**
5537c9966a5SAndreas Gohr     * Get the bug tracker URL of the extension if there is any
5547c9966a5SAndreas Gohr     *
5557c9966a5SAndreas Gohr     * @return string
5567c9966a5SAndreas Gohr     */
5577c9966a5SAndreas Gohr    public function getBugtrackerURL()
5587c9966a5SAndreas Gohr    {
5597c9966a5SAndreas Gohr        return $this->getRemoteTag('bugtracker');
5607c9966a5SAndreas Gohr    }
5617c9966a5SAndreas Gohr
5627c9966a5SAndreas Gohr    /**
5637c9966a5SAndreas Gohr     * Get the URL of the source repository if there is any
5647c9966a5SAndreas Gohr     *
5657c9966a5SAndreas Gohr     * @return string
5667c9966a5SAndreas Gohr     */
5677c9966a5SAndreas Gohr    public function getSourcerepoURL()
5687c9966a5SAndreas Gohr    {
5697c9966a5SAndreas Gohr        return $this->getRemoteTag('sourcerepo');
5707c9966a5SAndreas Gohr    }
5717c9966a5SAndreas Gohr
5727c9966a5SAndreas Gohr    /**
5737c9966a5SAndreas Gohr     * Get the donation URL of the extension if there is any
5747c9966a5SAndreas Gohr     *
5757c9966a5SAndreas Gohr     * @return string
5767c9966a5SAndreas Gohr     */
5777c9966a5SAndreas Gohr    public function getDonationURL()
5787c9966a5SAndreas Gohr    {
5797c9966a5SAndreas Gohr        return $this->getRemoteTag('donationurl');
5807c9966a5SAndreas Gohr    }
5817c9966a5SAndreas Gohr
5827c9966a5SAndreas Gohr    // endregion
5837c9966a5SAndreas Gohr
584cf2dcf1bSAndreas Gohr    // region Actions
585cf2dcf1bSAndreas Gohr
586cf2dcf1bSAndreas Gohr    /**
587cf2dcf1bSAndreas Gohr     * Install or update the extension
588cf2dcf1bSAndreas Gohr     *
589cf2dcf1bSAndreas Gohr     * @throws Exception
590cf2dcf1bSAndreas Gohr     */
591cf2dcf1bSAndreas Gohr    public function installOrUpdate()
592cf2dcf1bSAndreas Gohr    {
593cf2dcf1bSAndreas Gohr        $installer = new Installer(true);
594160d3688SAndreas Gohr        $installer->installExtension($this);
595cf2dcf1bSAndreas Gohr    }
596cf2dcf1bSAndreas Gohr
597cf2dcf1bSAndreas Gohr    /**
598cf2dcf1bSAndreas Gohr     * Uninstall the extension
599cf2dcf1bSAndreas Gohr     * @throws Exception
600cf2dcf1bSAndreas Gohr     */
601cf2dcf1bSAndreas Gohr    public function uninstall()
602cf2dcf1bSAndreas Gohr    {
603cf2dcf1bSAndreas Gohr        $installer = new Installer(true);
604cf2dcf1bSAndreas Gohr        $installer->uninstall($this);
605cf2dcf1bSAndreas Gohr    }
606cf2dcf1bSAndreas Gohr
607cf2dcf1bSAndreas Gohr    /**
608cf2dcf1bSAndreas Gohr     * Enable the extension
609cf2dcf1bSAndreas Gohr     * @todo I'm unsure if this code should be here or part of Installer
610cf2dcf1bSAndreas Gohr     * @throws Exception
611cf2dcf1bSAndreas Gohr     */
612cf2dcf1bSAndreas Gohr    public function enable()
613cf2dcf1bSAndreas Gohr    {
614cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) throw new Exception('notimplemented');
615160d3688SAndreas Gohr        if (!$this->isInstalled()) throw new Exception('error_notinstalled', [$this->getId()]);
616160d3688SAndreas Gohr        if ($this->isEnabled()) throw new Exception('error_alreadyenabled', [$this->getId()]);
617cf2dcf1bSAndreas Gohr
618cf2dcf1bSAndreas Gohr        /* @var PluginController $plugin_controller */
619cf2dcf1bSAndreas Gohr        global $plugin_controller;
620cf2dcf1bSAndreas Gohr        if (!$plugin_controller->enable($this->base)) {
621cf2dcf1bSAndreas Gohr            throw new Exception('pluginlistsaveerror');
622cf2dcf1bSAndreas Gohr        }
623cf2dcf1bSAndreas Gohr        Installer::purgeCache();
624cf2dcf1bSAndreas Gohr    }
625cf2dcf1bSAndreas Gohr
626cf2dcf1bSAndreas Gohr    /**
627cf2dcf1bSAndreas Gohr     * Disable the extension
628cf2dcf1bSAndreas Gohr     * @todo I'm unsure if this code should be here or part of Installer
629cf2dcf1bSAndreas Gohr     * @throws Exception
630cf2dcf1bSAndreas Gohr     */
631cf2dcf1bSAndreas Gohr    public function disable()
632cf2dcf1bSAndreas Gohr    {
633cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) throw new Exception('notimplemented');
634160d3688SAndreas Gohr        if (!$this->isInstalled()) throw new Exception('error_notinstalled', [$this->getId()]);
635160d3688SAndreas Gohr        if (!$this->isEnabled()) throw new Exception('error_alreadydisabled', [$this->getId()]);
636160d3688SAndreas Gohr        if ($this->isProtected()) throw new Exception('error_disable_protected', [$this->getId()]);
637cf2dcf1bSAndreas Gohr
638cf2dcf1bSAndreas Gohr        /* @var PluginController $plugin_controller */
639cf2dcf1bSAndreas Gohr        global $plugin_controller;
640cf2dcf1bSAndreas Gohr        if (!$plugin_controller->disable($this->base)) {
641cf2dcf1bSAndreas Gohr            throw new Exception('pluginlistsaveerror');
642cf2dcf1bSAndreas Gohr        }
643cf2dcf1bSAndreas Gohr        Installer::purgeCache();
644cf2dcf1bSAndreas Gohr    }
645cf2dcf1bSAndreas Gohr
646cf2dcf1bSAndreas Gohr    // endregion
647cf2dcf1bSAndreas Gohr
648cf2dcf1bSAndreas Gohr    // region Meta Data Management
649cf2dcf1bSAndreas Gohr
650cf2dcf1bSAndreas Gohr    /**
6517c9966a5SAndreas Gohr     * Access the Manager for this extension
652cf2dcf1bSAndreas Gohr     *
6537c9966a5SAndreas Gohr     * @return Manager
654cf2dcf1bSAndreas Gohr     */
6557c9966a5SAndreas Gohr    public function getManager()
656cf2dcf1bSAndreas Gohr    {
6577c9966a5SAndreas Gohr        if ($this->manager === null) {
6587c9966a5SAndreas Gohr            $this->manager = new Manager($this);
659cf2dcf1bSAndreas Gohr        }
6607c9966a5SAndreas Gohr        return $this->manager;
661cf2dcf1bSAndreas Gohr    }
662cf2dcf1bSAndreas Gohr
663cf2dcf1bSAndreas Gohr    /**
664cf2dcf1bSAndreas Gohr     * Reads the info file of the extension if available and fills the localInfo array
665cf2dcf1bSAndreas Gohr     */
666cf2dcf1bSAndreas Gohr    protected function readLocalInfo()
667cf2dcf1bSAndreas Gohr    {
668a1e045f7SAndreas Gohr        if (!$this->getCurrentDir()) return;
669cf2dcf1bSAndreas Gohr        $file = $this->currentDir . '/' . $this->type . '.info.txt';
670cf2dcf1bSAndreas Gohr        if (!is_readable($file)) return;
671cf2dcf1bSAndreas Gohr        $this->localInfo = confToHash($file, true);
672cf2dcf1bSAndreas Gohr        $this->localInfo = array_filter($this->localInfo); // remove all falsy keys
673cf2dcf1bSAndreas Gohr    }
674cf2dcf1bSAndreas Gohr
675cf2dcf1bSAndreas Gohr    /**
676cf2dcf1bSAndreas Gohr     * Fetches the remote info from the repository
677cf2dcf1bSAndreas Gohr     *
678cf2dcf1bSAndreas Gohr     * This ignores any errors coming from the repository and just sets the remoteInfo to an empty array in that case
679cf2dcf1bSAndreas Gohr     */
680cf2dcf1bSAndreas Gohr    protected function loadRemoteInfo()
681cf2dcf1bSAndreas Gohr    {
682cf2dcf1bSAndreas Gohr        if ($this->remoteInfo) return;
683cf2dcf1bSAndreas Gohr        $remote = Repository::getInstance();
684cf2dcf1bSAndreas Gohr        try {
685cf2dcf1bSAndreas Gohr            $this->remoteInfo = (array)$remote->getExtensionData($this->getId());
686cf2dcf1bSAndreas Gohr        } catch (Exception $e) {
687cf2dcf1bSAndreas Gohr            $this->remoteInfo = [];
688cf2dcf1bSAndreas Gohr        }
689cf2dcf1bSAndreas Gohr    }
690cf2dcf1bSAndreas Gohr
691cf2dcf1bSAndreas Gohr    /**
692cf2dcf1bSAndreas Gohr     * Read information from either local or remote info
693cf2dcf1bSAndreas Gohr     *
6947c9966a5SAndreas Gohr     * Always prefers local info over remote info. Giving multiple keys is useful when the
6957c9966a5SAndreas Gohr     * key has been renamed in the past or if local and remote keys might differ.
696cf2dcf1bSAndreas Gohr     *
697cf2dcf1bSAndreas Gohr     * @param string|string[] $tag one or multiple keys to check
698cf2dcf1bSAndreas Gohr     * @param mixed $default
699cf2dcf1bSAndreas Gohr     * @return mixed
700cf2dcf1bSAndreas Gohr     */
701cf2dcf1bSAndreas Gohr    protected function getTag($tag, $default = '')
702cf2dcf1bSAndreas Gohr    {
703cf2dcf1bSAndreas Gohr        foreach ((array)$tag as $t) {
704cf2dcf1bSAndreas Gohr            if (isset($this->localInfo[$t])) return $this->localInfo[$t];
705cf2dcf1bSAndreas Gohr        }
7067c9966a5SAndreas Gohr
7077c9966a5SAndreas Gohr        return $this->getRemoteTag($tag, $default);
7087c9966a5SAndreas Gohr    }
7097c9966a5SAndreas Gohr
7107c9966a5SAndreas Gohr    /**
7117c9966a5SAndreas Gohr     * Read information from remote info
7127c9966a5SAndreas Gohr     *
7137c9966a5SAndreas Gohr     * @param string|string[] $tag one or mutiple keys to check
7147c9966a5SAndreas Gohr     * @param mixed $default
7157c9966a5SAndreas Gohr     * @return mixed
7167c9966a5SAndreas Gohr     */
7177c9966a5SAndreas Gohr    protected function getRemoteTag($tag, $default = '')
7187c9966a5SAndreas Gohr    {
719cf2dcf1bSAndreas Gohr        $this->loadRemoteInfo();
720cf2dcf1bSAndreas Gohr        foreach ((array)$tag as $t) {
721cf2dcf1bSAndreas Gohr            if (isset($this->remoteInfo[$t])) return $this->remoteInfo[$t];
722cf2dcf1bSAndreas Gohr        }
723cf2dcf1bSAndreas Gohr        return $default;
724cf2dcf1bSAndreas Gohr    }
725cf2dcf1bSAndreas Gohr
726cf2dcf1bSAndreas Gohr    // endregion
727a1e045f7SAndreas Gohr
728a1e045f7SAndreas Gohr    // region utilities
729a1e045f7SAndreas Gohr
730a1e045f7SAndreas Gohr    /**
731a1e045f7SAndreas Gohr     * Convert an extension id to a type and base
732a1e045f7SAndreas Gohr     *
733a1e045f7SAndreas Gohr     * @param string $id
734a1e045f7SAndreas Gohr     * @return array [type, base]
735a1e045f7SAndreas Gohr     */
736a1e045f7SAndreas Gohr    protected function idToTypeBase($id)
737a1e045f7SAndreas Gohr    {
738a1e045f7SAndreas Gohr        [$type, $base] = sexplode(':', $id, 2);
739a1e045f7SAndreas Gohr        if ($base === null) {
740a1e045f7SAndreas Gohr            $base = $type;
741a1e045f7SAndreas Gohr            $type = self::TYPE_PLUGIN;
742a1e045f7SAndreas Gohr        } elseif ($type === self::TYPE_TEMPLATE) {
743a1e045f7SAndreas Gohr            $type = self::TYPE_TEMPLATE;
744a1e045f7SAndreas Gohr        } else {
745a1e045f7SAndreas Gohr            throw new RuntimeException('Invalid extension id: ' . $id);
746a1e045f7SAndreas Gohr        }
747a1e045f7SAndreas Gohr
748a1e045f7SAndreas Gohr        return [$type, $base];
749a1e045f7SAndreas Gohr    }
7507c9966a5SAndreas Gohr    /**
7517c9966a5SAndreas Gohr     * @return string
7527c9966a5SAndreas Gohr     */
7537c9966a5SAndreas Gohr    public function __toString()
7547c9966a5SAndreas Gohr    {
7557c9966a5SAndreas Gohr        return $this->getId();
7567c9966a5SAndreas Gohr    }
7577c9966a5SAndreas Gohr
758a1e045f7SAndreas Gohr    // endregion
759cf2dcf1bSAndreas Gohr}
760