xref: /dokuwiki/lib/plugins/extension/Extension.php (revision 077f55feebe4a4f38e51ad49cb3c36270f5ee390)
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{
117c184cfcSAndreas Gohr    public const TYPE_PLUGIN = 'plugin';
127c184cfcSAndreas Gohr    public 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);
83e8fd67e9SAndreas Gohr        $this->currentDir = fullpath($dir);
84cf2dcf1bSAndreas Gohr
85cf2dcf1bSAndreas Gohr        if ($type === null || $type === self::TYPE_TEMPLATE) {
86cf2dcf1bSAndreas Gohr            if (
878fe483c9SAndreas Gohr                file_exists($dir . '/template.info.txt') ||
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    /**
1434fd6a1d7SAndreas Gohr     * @param bool $wrap If true, the id is wrapped in backticks
144cf2dcf1bSAndreas Gohr     * @return string The extension id (same as base but prefixed with "template:" for templates)
145cf2dcf1bSAndreas Gohr     */
1464fd6a1d7SAndreas Gohr    public function getId($wrap = false)
147cf2dcf1bSAndreas Gohr    {
148cf2dcf1bSAndreas Gohr        if ($this->type === self::TYPE_TEMPLATE) {
1494fd6a1d7SAndreas Gohr            $id = self::TYPE_TEMPLATE . ':' . $this->base;
1504fd6a1d7SAndreas Gohr        } else {
1514fd6a1d7SAndreas Gohr            $id = $this->base;
152cf2dcf1bSAndreas Gohr        }
1534fd6a1d7SAndreas Gohr        if ($wrap) $id = "`$id`";
1544fd6a1d7SAndreas Gohr        return $id;
155cf2dcf1bSAndreas Gohr    }
156cf2dcf1bSAndreas Gohr
157cf2dcf1bSAndreas Gohr    /**
158cf2dcf1bSAndreas Gohr     * Get the base name of this extension
159cf2dcf1bSAndreas Gohr     *
160cf2dcf1bSAndreas Gohr     * @return string
161cf2dcf1bSAndreas Gohr     */
162cf2dcf1bSAndreas Gohr    public function getBase()
163cf2dcf1bSAndreas Gohr    {
164cf2dcf1bSAndreas Gohr        return $this->base;
165cf2dcf1bSAndreas Gohr    }
166cf2dcf1bSAndreas Gohr
167cf2dcf1bSAndreas Gohr    /**
168cf2dcf1bSAndreas Gohr     * Get the type of the extension
169cf2dcf1bSAndreas Gohr     *
170cf2dcf1bSAndreas Gohr     * @return string "plugin"|"template"
171cf2dcf1bSAndreas Gohr     */
172cf2dcf1bSAndreas Gohr    public function getType()
173cf2dcf1bSAndreas Gohr    {
174cf2dcf1bSAndreas Gohr        return $this->type;
175cf2dcf1bSAndreas Gohr    }
176cf2dcf1bSAndreas Gohr
177cf2dcf1bSAndreas Gohr    /**
178cf2dcf1bSAndreas Gohr     * The current directory of the extension
179cf2dcf1bSAndreas Gohr     *
180cf2dcf1bSAndreas Gohr     * @return string|null
181cf2dcf1bSAndreas Gohr     */
182cf2dcf1bSAndreas Gohr    public function getCurrentDir()
183cf2dcf1bSAndreas Gohr    {
184cf2dcf1bSAndreas Gohr        // recheck that the current currentDir is still valid
185cf2dcf1bSAndreas Gohr        if ($this->currentDir && !is_dir($this->currentDir)) {
18625d28a01SAndreas Gohr            $this->currentDir = '';
187cf2dcf1bSAndreas Gohr        }
188cf2dcf1bSAndreas Gohr
189cf2dcf1bSAndreas Gohr        // if the extension is installed, then the currentDir is the install dir!
190cf2dcf1bSAndreas Gohr        if (!$this->currentDir && $this->isInstalled()) {
191cf2dcf1bSAndreas Gohr            $this->currentDir = $this->getInstallDir();
192cf2dcf1bSAndreas Gohr        }
193cf2dcf1bSAndreas Gohr
194cf2dcf1bSAndreas Gohr        return $this->currentDir;
195cf2dcf1bSAndreas Gohr    }
196cf2dcf1bSAndreas Gohr
197cf2dcf1bSAndreas Gohr    /**
198cf2dcf1bSAndreas Gohr     * Get the directory where this extension should be installed in
199cf2dcf1bSAndreas Gohr     *
200cf2dcf1bSAndreas Gohr     * Note: this does not mean that the extension is actually installed there
201cf2dcf1bSAndreas Gohr     *
202cf2dcf1bSAndreas Gohr     * @return string
203cf2dcf1bSAndreas Gohr     */
204cf2dcf1bSAndreas Gohr    public function getInstallDir()
205cf2dcf1bSAndreas Gohr    {
206cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) {
207176901c2SAndreas Gohr            $dir = dirname(tpl_incdir()) . '/' . $this->base;
208cf2dcf1bSAndreas Gohr        } else {
209cf2dcf1bSAndreas Gohr            $dir = DOKU_PLUGIN . $this->base;
210cf2dcf1bSAndreas Gohr        }
211cf2dcf1bSAndreas Gohr
21225d28a01SAndreas Gohr        return fullpath($dir);
213cf2dcf1bSAndreas Gohr    }
214cf2dcf1bSAndreas Gohr
215cf2dcf1bSAndreas Gohr
216cf2dcf1bSAndreas Gohr    /**
217cf2dcf1bSAndreas Gohr     * Get the display name of the extension
218cf2dcf1bSAndreas Gohr     *
219cf2dcf1bSAndreas Gohr     * @return string
220cf2dcf1bSAndreas Gohr     */
221cf2dcf1bSAndreas Gohr    public function getDisplayName()
222cf2dcf1bSAndreas Gohr    {
223cf2dcf1bSAndreas Gohr        return $this->getTag('name', PhpString::ucwords($this->getBase() . ' ' . $this->getType()));
224cf2dcf1bSAndreas Gohr    }
225cf2dcf1bSAndreas Gohr
226cf2dcf1bSAndreas Gohr    /**
227cf2dcf1bSAndreas Gohr     * Get the author name of the extension
228cf2dcf1bSAndreas Gohr     *
229cf2dcf1bSAndreas Gohr     * @return string Returns an empty string if the author info is missing
230cf2dcf1bSAndreas Gohr     */
231cf2dcf1bSAndreas Gohr    public function getAuthor()
232cf2dcf1bSAndreas Gohr    {
233cf2dcf1bSAndreas Gohr        return $this->getTag('author');
234cf2dcf1bSAndreas Gohr    }
235cf2dcf1bSAndreas Gohr
236cf2dcf1bSAndreas Gohr    /**
237cf2dcf1bSAndreas Gohr     * Get the email of the author of the extension if there is any
238cf2dcf1bSAndreas Gohr     *
239cf2dcf1bSAndreas Gohr     * @return string Returns an empty string if the email info is missing
240cf2dcf1bSAndreas Gohr     */
241cf2dcf1bSAndreas Gohr    public function getEmail()
242cf2dcf1bSAndreas Gohr    {
243cf2dcf1bSAndreas Gohr        // email is only in the local data
244cf2dcf1bSAndreas Gohr        return $this->localInfo['email'] ?? '';
245cf2dcf1bSAndreas Gohr    }
246cf2dcf1bSAndreas Gohr
247cf2dcf1bSAndreas Gohr    /**
248cf2dcf1bSAndreas Gohr     * Get the email id, i.e. the md5sum of the email
249cf2dcf1bSAndreas Gohr     *
250cf2dcf1bSAndreas Gohr     * @return string Empty string if no email is available
251cf2dcf1bSAndreas Gohr     */
252cf2dcf1bSAndreas Gohr    public function getEmailID()
253cf2dcf1bSAndreas Gohr    {
254cf2dcf1bSAndreas Gohr        if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
255cf2dcf1bSAndreas Gohr        if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
256cf2dcf1bSAndreas Gohr        return '';
257cf2dcf1bSAndreas Gohr    }
258cf2dcf1bSAndreas Gohr
259cf2dcf1bSAndreas Gohr    /**
260cf2dcf1bSAndreas Gohr     * Get the description of the extension
261cf2dcf1bSAndreas Gohr     *
262cf2dcf1bSAndreas Gohr     * @return string Empty string if no description is available
263cf2dcf1bSAndreas Gohr     */
264cf2dcf1bSAndreas Gohr    public function getDescription()
265cf2dcf1bSAndreas Gohr    {
266cf2dcf1bSAndreas Gohr        return $this->getTag(['desc', 'description']);
267cf2dcf1bSAndreas Gohr    }
268cf2dcf1bSAndreas Gohr
269cf2dcf1bSAndreas Gohr    /**
270cf2dcf1bSAndreas Gohr     * Get the URL of the extension, usually a page on dokuwiki.org
271cf2dcf1bSAndreas Gohr     *
272cf2dcf1bSAndreas Gohr     * @return string
273cf2dcf1bSAndreas Gohr     */
274cf2dcf1bSAndreas Gohr    public function getURL()
275cf2dcf1bSAndreas Gohr    {
276cf2dcf1bSAndreas Gohr        return $this->getTag(
277cf2dcf1bSAndreas Gohr            'url',
278cf2dcf1bSAndreas Gohr            'https://www.dokuwiki.org/' .
279cf2dcf1bSAndreas Gohr            ($this->isTemplate() ? 'template' : 'plugin') . ':' . $this->getBase()
280cf2dcf1bSAndreas Gohr        );
281cf2dcf1bSAndreas Gohr    }
282cf2dcf1bSAndreas Gohr
283cf2dcf1bSAndreas Gohr    /**
2847c9966a5SAndreas Gohr     * Get the version of the extension that is actually installed
2857c9966a5SAndreas Gohr     *
2867c9966a5SAndreas Gohr     * Returns an empty string if the version is not available
2877c9966a5SAndreas Gohr     *
2887c9966a5SAndreas Gohr     * @return string
2897c9966a5SAndreas Gohr     */
2907c9966a5SAndreas Gohr    public function getInstalledVersion()
2917c9966a5SAndreas Gohr    {
2927c9966a5SAndreas Gohr        return $this->localInfo['date'] ?? '';
2937c9966a5SAndreas Gohr    }
2947c9966a5SAndreas Gohr
2957c9966a5SAndreas Gohr    /**
2964fd6a1d7SAndreas Gohr     * Get the types of components this extension provides
2974fd6a1d7SAndreas Gohr     *
2984fd6a1d7SAndreas Gohr     * @return array int -> type
2994fd6a1d7SAndreas Gohr     */
3004fd6a1d7SAndreas Gohr    public function getComponentTypes()
3014fd6a1d7SAndreas Gohr    {
3028fe483c9SAndreas Gohr        // for installed extensions we can check the files
3038fe483c9SAndreas Gohr        if ($this->isInstalled()) {
3048fe483c9SAndreas Gohr            if ($this->isTemplate()) {
3058fe483c9SAndreas Gohr                return ['Template'];
3068fe483c9SAndreas Gohr            } else {
3078fe483c9SAndreas Gohr                $types = [];
3088fe483c9SAndreas Gohr                foreach (['Admin', 'Action', 'Syntax', 'Renderer', 'Helper', 'CLI'] as $type) {
3098fe483c9SAndreas Gohr                    $check = strtolower($type);
3108fe483c9SAndreas Gohr                    if (
3118fe483c9SAndreas Gohr                        file_exists($this->getInstallDir() . '/' . $check . '.php') ||
3128fe483c9SAndreas Gohr                        is_dir($this->getInstallDir() . '/' . $check)
3138fe483c9SAndreas Gohr                    ) {
3148fe483c9SAndreas Gohr                        $types[] = $type;
3158fe483c9SAndreas Gohr                    }
3168fe483c9SAndreas Gohr                }
3178fe483c9SAndreas Gohr                return $types;
3188fe483c9SAndreas Gohr            }
3198fe483c9SAndreas Gohr        }
3208fe483c9SAndreas Gohr        // still, here? use the remote info
3214fd6a1d7SAndreas Gohr        return $this->getTag('types', []);
3224fd6a1d7SAndreas Gohr    }
3234fd6a1d7SAndreas Gohr
3244fd6a1d7SAndreas Gohr    /**
32525d28a01SAndreas Gohr     * Get a list of extension ids this extension depends on
32625d28a01SAndreas Gohr     *
32725d28a01SAndreas Gohr     * @return string[]
32825d28a01SAndreas Gohr     */
32925d28a01SAndreas Gohr    public function getDependencyList()
33025d28a01SAndreas Gohr    {
33125d28a01SAndreas Gohr        return $this->getTag('depends', []);
33225d28a01SAndreas Gohr    }
33325d28a01SAndreas Gohr
33425d28a01SAndreas Gohr    /**
335b2a05b76SAndreas Gohr     * Return the minimum PHP version required by the extension
336b2a05b76SAndreas Gohr     *
337b2a05b76SAndreas Gohr     * Empty if not set
338b2a05b76SAndreas Gohr     *
339b2a05b76SAndreas Gohr     * @return string
340b2a05b76SAndreas Gohr     */
341b2a05b76SAndreas Gohr    public function getMinimumPHPVersion()
342b2a05b76SAndreas Gohr    {
343b2a05b76SAndreas Gohr        return $this->getTag('phpmin', '');
344b2a05b76SAndreas Gohr    }
345b2a05b76SAndreas Gohr
346b2a05b76SAndreas Gohr    /**
347b2a05b76SAndreas Gohr     * Return the minimum PHP version supported by the extension
348b2a05b76SAndreas Gohr     *
349b2a05b76SAndreas Gohr     * @return string
350b2a05b76SAndreas Gohr     */
351b2a05b76SAndreas Gohr    public function getMaximumPHPVersion()
352b2a05b76SAndreas Gohr    {
353b2a05b76SAndreas Gohr        return $this->getTag('phpmax', '');
354b2a05b76SAndreas Gohr    }
355b2a05b76SAndreas Gohr
356b2a05b76SAndreas Gohr    /**
357cf2dcf1bSAndreas Gohr     * Is this extension a template?
358cf2dcf1bSAndreas Gohr     *
359cf2dcf1bSAndreas Gohr     * @return bool false if it is a plugin
360cf2dcf1bSAndreas Gohr     */
361cf2dcf1bSAndreas Gohr    public function isTemplate()
362cf2dcf1bSAndreas Gohr    {
363cf2dcf1bSAndreas Gohr        return $this->type === self::TYPE_TEMPLATE;
364cf2dcf1bSAndreas Gohr    }
365cf2dcf1bSAndreas Gohr
366cf2dcf1bSAndreas Gohr    /**
367cf2dcf1bSAndreas Gohr     * Is the extension installed locally?
368cf2dcf1bSAndreas Gohr     *
369cf2dcf1bSAndreas Gohr     * @return bool
370cf2dcf1bSAndreas Gohr     */
371cf2dcf1bSAndreas Gohr    public function isInstalled()
372cf2dcf1bSAndreas Gohr    {
373cf2dcf1bSAndreas Gohr        return is_dir($this->getInstallDir());
374cf2dcf1bSAndreas Gohr    }
375cf2dcf1bSAndreas Gohr
376cf2dcf1bSAndreas Gohr    /**
377cf2dcf1bSAndreas Gohr     * Is the extension under git control?
378cf2dcf1bSAndreas Gohr     *
379cf2dcf1bSAndreas Gohr     * @return bool
380cf2dcf1bSAndreas Gohr     */
381cf2dcf1bSAndreas Gohr    public function isGitControlled()
382cf2dcf1bSAndreas Gohr    {
383cf2dcf1bSAndreas Gohr        if (!$this->isInstalled()) return false;
384cf2dcf1bSAndreas Gohr        return file_exists($this->getInstallDir() . '/.git');
385cf2dcf1bSAndreas Gohr    }
386cf2dcf1bSAndreas Gohr
387cf2dcf1bSAndreas Gohr    /**
388cf2dcf1bSAndreas Gohr     * If the extension is bundled
389cf2dcf1bSAndreas Gohr     *
390cf2dcf1bSAndreas Gohr     * @return bool If the extension is bundled
391cf2dcf1bSAndreas Gohr     */
392cf2dcf1bSAndreas Gohr    public function isBundled()
393cf2dcf1bSAndreas Gohr    {
394cf2dcf1bSAndreas Gohr        $this->loadRemoteInfo();
395cf2dcf1bSAndreas Gohr        return $this->remoteInfo['bundled'] ?? in_array(
396cf2dcf1bSAndreas Gohr            $this->getId(),
397cf2dcf1bSAndreas Gohr            [
398cf2dcf1bSAndreas Gohr                'authad',
399cf2dcf1bSAndreas Gohr                'authldap',
400cf2dcf1bSAndreas Gohr                'authpdo',
401cf2dcf1bSAndreas Gohr                'authplain',
402cf2dcf1bSAndreas Gohr                'acl',
403cf2dcf1bSAndreas Gohr                'config',
404cf2dcf1bSAndreas Gohr                'extension',
405cf2dcf1bSAndreas Gohr                'info',
406cf2dcf1bSAndreas Gohr                'popularity',
407cf2dcf1bSAndreas Gohr                'revert',
408cf2dcf1bSAndreas Gohr                'safefnrecode',
409cf2dcf1bSAndreas Gohr                'styling',
410cf2dcf1bSAndreas Gohr                'testing',
411cf2dcf1bSAndreas Gohr                'usermanager',
412cf2dcf1bSAndreas Gohr                'logviewer',
413cf2dcf1bSAndreas Gohr                'template:dokuwiki'
414cf2dcf1bSAndreas Gohr            ]
415cf2dcf1bSAndreas Gohr        );
416cf2dcf1bSAndreas Gohr    }
417cf2dcf1bSAndreas Gohr
418cf2dcf1bSAndreas Gohr    /**
419cf2dcf1bSAndreas Gohr     * Is the extension protected against any modification (disable/uninstall)
420cf2dcf1bSAndreas Gohr     *
421cf2dcf1bSAndreas Gohr     * @return bool if the extension is protected
422cf2dcf1bSAndreas Gohr     */
423cf2dcf1bSAndreas Gohr    public function isProtected()
424cf2dcf1bSAndreas Gohr    {
425cf2dcf1bSAndreas Gohr        // never allow deinstalling the current auth plugin:
426cf2dcf1bSAndreas Gohr        global $conf;
427cf2dcf1bSAndreas Gohr        if ($this->getId() == $conf['authtype']) return true;
428cf2dcf1bSAndreas Gohr
429*077f55feSAndreas Gohr        // disallow current template to be uninstalled
430*077f55feSAndreas Gohr        if($this->isTemplate() && ($this->getBase() === $conf['template'])) return true;
431cf2dcf1bSAndreas Gohr
432cf2dcf1bSAndreas Gohr        /** @var PluginController $plugin_controller */
433cf2dcf1bSAndreas Gohr        global $plugin_controller;
434cf2dcf1bSAndreas Gohr        $cascade = $plugin_controller->getCascade();
435cf2dcf1bSAndreas Gohr        return ($cascade['protected'][$this->getId()] ?? false);
436cf2dcf1bSAndreas Gohr    }
437cf2dcf1bSAndreas Gohr
438cf2dcf1bSAndreas Gohr    /**
439cf2dcf1bSAndreas Gohr     * Is the extension installed in the correct directory?
440cf2dcf1bSAndreas Gohr     *
441cf2dcf1bSAndreas Gohr     * @return bool
442cf2dcf1bSAndreas Gohr     */
443cf2dcf1bSAndreas Gohr    public function isInWrongFolder()
444cf2dcf1bSAndreas Gohr    {
4454fd6a1d7SAndreas Gohr        if (!$this->isInstalled()) return false;
446cf2dcf1bSAndreas Gohr        return $this->getInstallDir() != $this->currentDir;
447cf2dcf1bSAndreas Gohr    }
448cf2dcf1bSAndreas Gohr
449cf2dcf1bSAndreas Gohr    /**
450cf2dcf1bSAndreas Gohr     * Is the extension enabled?
451cf2dcf1bSAndreas Gohr     *
452cf2dcf1bSAndreas Gohr     * @return bool
453cf2dcf1bSAndreas Gohr     */
454cf2dcf1bSAndreas Gohr    public function isEnabled()
455cf2dcf1bSAndreas Gohr    {
456cf2dcf1bSAndreas Gohr        global $conf;
457cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) {
458cf2dcf1bSAndreas Gohr            return ($conf['template'] == $this->getBase());
459cf2dcf1bSAndreas Gohr        }
460cf2dcf1bSAndreas Gohr
461cf2dcf1bSAndreas Gohr        /* @var PluginController $plugin_controller */
462cf2dcf1bSAndreas Gohr        global $plugin_controller;
463cf2dcf1bSAndreas Gohr        return $plugin_controller->isEnabled($this->base);
464cf2dcf1bSAndreas Gohr    }
465cf2dcf1bSAndreas Gohr
466160d3688SAndreas Gohr    /**
467160d3688SAndreas Gohr     * Has the download URL changed since the last download?
468160d3688SAndreas Gohr     *
469160d3688SAndreas Gohr     * @return bool
470160d3688SAndreas Gohr     */
471160d3688SAndreas Gohr    public function hasChangedURL()
472160d3688SAndreas Gohr    {
4734fd6a1d7SAndreas Gohr        $last = $this->getManager()->getDownloadURL();
474160d3688SAndreas Gohr        if (!$last) return false;
475160d3688SAndreas Gohr        return $last !== $this->getDownloadURL();
476160d3688SAndreas Gohr    }
477160d3688SAndreas Gohr
478160d3688SAndreas Gohr    /**
479160d3688SAndreas Gohr     * Is an update available for this extension?
480160d3688SAndreas Gohr     *
481160d3688SAndreas Gohr     * @return bool
482160d3688SAndreas Gohr     */
4834fd6a1d7SAndreas Gohr    public function isUpdateAvailable()
484160d3688SAndreas Gohr    {
485160d3688SAndreas Gohr        if ($this->isBundled()) return false; // bundled extensions are never updated
486160d3688SAndreas Gohr        $self = $this->getInstalledVersion();
487160d3688SAndreas Gohr        $remote = $this->getLastUpdate();
488160d3688SAndreas Gohr        return $self < $remote;
489160d3688SAndreas Gohr    }
490160d3688SAndreas Gohr
491cf2dcf1bSAndreas Gohr    // endregion
492cf2dcf1bSAndreas Gohr
4937c9966a5SAndreas Gohr    // region Remote Info
4947c9966a5SAndreas Gohr
4957c9966a5SAndreas Gohr    /**
4967c9966a5SAndreas Gohr     * Get the date of the last available update
4977c9966a5SAndreas Gohr     *
4987c9966a5SAndreas Gohr     * @return string yyyy-mm-dd
4997c9966a5SAndreas Gohr     */
5007c9966a5SAndreas Gohr    public function getLastUpdate()
5017c9966a5SAndreas Gohr    {
5027c9966a5SAndreas Gohr        return $this->getRemoteTag('lastupdate');
5037c9966a5SAndreas Gohr    }
5047c9966a5SAndreas Gohr
5057c9966a5SAndreas Gohr    /**
5067c9966a5SAndreas Gohr     * Get a list of tags this extension is tagged with at dokuwiki.org
5077c9966a5SAndreas Gohr     *
5087c9966a5SAndreas Gohr     * @return string[]
5097c9966a5SAndreas Gohr     */
5107c9966a5SAndreas Gohr    public function getTags()
5117c9966a5SAndreas Gohr    {
5127c9966a5SAndreas Gohr        return $this->getRemoteTag('tags', []);
5137c9966a5SAndreas Gohr    }
5147c9966a5SAndreas Gohr
5157c9966a5SAndreas Gohr    /**
5167c9966a5SAndreas Gohr     * Get the popularity of the extension
5177c9966a5SAndreas Gohr     *
5187c9966a5SAndreas Gohr     * This is a float between 0 and 1
5197c9966a5SAndreas Gohr     *
5207c9966a5SAndreas Gohr     * @return float
5217c9966a5SAndreas Gohr     */
5227c9966a5SAndreas Gohr    public function getPopularity()
5237c9966a5SAndreas Gohr    {
5247c9966a5SAndreas Gohr        return (float)$this->getRemoteTag('popularity', 0);
5257c9966a5SAndreas Gohr    }
5267c9966a5SAndreas Gohr
5277c9966a5SAndreas Gohr    /**
5287c9966a5SAndreas Gohr     * Get the text of the update message if there is any
5297c9966a5SAndreas Gohr     *
5307c9966a5SAndreas Gohr     * @return string
5317c9966a5SAndreas Gohr     */
5327c9966a5SAndreas Gohr    public function getUpdateMessage()
5337c9966a5SAndreas Gohr    {
5347c9966a5SAndreas Gohr        return $this->getRemoteTag('updatemessage');
5357c9966a5SAndreas Gohr    }
5367c9966a5SAndreas Gohr
5377c9966a5SAndreas Gohr    /**
5387c9966a5SAndreas Gohr     * Get the text of the security warning if there is any
5397c9966a5SAndreas Gohr     *
5407c9966a5SAndreas Gohr     * @return string
5417c9966a5SAndreas Gohr     */
5427c9966a5SAndreas Gohr    public function getSecurityWarning()
5437c9966a5SAndreas Gohr    {
5447c9966a5SAndreas Gohr        return $this->getRemoteTag('securitywarning');
5457c9966a5SAndreas Gohr    }
5467c9966a5SAndreas Gohr
5477c9966a5SAndreas Gohr    /**
5487c9966a5SAndreas Gohr     * Get the text of the security issue if there is any
5497c9966a5SAndreas Gohr     *
5507c9966a5SAndreas Gohr     * @return string
5517c9966a5SAndreas Gohr     */
5527c9966a5SAndreas Gohr    public function getSecurityIssue()
5537c9966a5SAndreas Gohr    {
5547c9966a5SAndreas Gohr        return $this->getRemoteTag('securityissue');
5557c9966a5SAndreas Gohr    }
5567c9966a5SAndreas Gohr
5577c9966a5SAndreas Gohr    /**
5587c9966a5SAndreas Gohr     * Get the URL of the screenshot of the extension if there is any
5597c9966a5SAndreas Gohr     *
5607c9966a5SAndreas Gohr     * @return string
5617c9966a5SAndreas Gohr     */
5627c9966a5SAndreas Gohr    public function getScreenshotURL()
5637c9966a5SAndreas Gohr    {
5647c9966a5SAndreas Gohr        return $this->getRemoteTag('screenshoturl');
5657c9966a5SAndreas Gohr    }
5667c9966a5SAndreas Gohr
5677c9966a5SAndreas Gohr    /**
5687c9966a5SAndreas Gohr     * Get the URL of the thumbnail of the extension if there is any
5697c9966a5SAndreas Gohr     *
5707c9966a5SAndreas Gohr     * @return string
5717c9966a5SAndreas Gohr     */
5727c9966a5SAndreas Gohr    public function getThumbnailURL()
5737c9966a5SAndreas Gohr    {
5747c9966a5SAndreas Gohr        return $this->getRemoteTag('thumbnailurl');
5757c9966a5SAndreas Gohr    }
5767c9966a5SAndreas Gohr
5777c9966a5SAndreas Gohr    /**
5787c9966a5SAndreas Gohr     * Get the download URL of the extension if there is any
5797c9966a5SAndreas Gohr     *
5807c9966a5SAndreas Gohr     * @return string
5817c9966a5SAndreas Gohr     */
5827c9966a5SAndreas Gohr    public function getDownloadURL()
5837c9966a5SAndreas Gohr    {
5847c9966a5SAndreas Gohr        return $this->getRemoteTag('downloadurl');
5857c9966a5SAndreas Gohr    }
5867c9966a5SAndreas Gohr
5877c9966a5SAndreas Gohr    /**
5887c9966a5SAndreas Gohr     * Get the bug tracker URL of the extension if there is any
5897c9966a5SAndreas Gohr     *
5907c9966a5SAndreas Gohr     * @return string
5917c9966a5SAndreas Gohr     */
5927c9966a5SAndreas Gohr    public function getBugtrackerURL()
5937c9966a5SAndreas Gohr    {
5947c9966a5SAndreas Gohr        return $this->getRemoteTag('bugtracker');
5957c9966a5SAndreas Gohr    }
5967c9966a5SAndreas Gohr
5977c9966a5SAndreas Gohr    /**
5987c9966a5SAndreas Gohr     * Get the URL of the source repository if there is any
5997c9966a5SAndreas Gohr     *
6007c9966a5SAndreas Gohr     * @return string
6017c9966a5SAndreas Gohr     */
6027c9966a5SAndreas Gohr    public function getSourcerepoURL()
6037c9966a5SAndreas Gohr    {
6047c9966a5SAndreas Gohr        return $this->getRemoteTag('sourcerepo');
6057c9966a5SAndreas Gohr    }
6067c9966a5SAndreas Gohr
6077c9966a5SAndreas Gohr    /**
6087c9966a5SAndreas Gohr     * Get the donation URL of the extension if there is any
6097c9966a5SAndreas Gohr     *
6107c9966a5SAndreas Gohr     * @return string
6117c9966a5SAndreas Gohr     */
6127c9966a5SAndreas Gohr    public function getDonationURL()
6137c9966a5SAndreas Gohr    {
6147c9966a5SAndreas Gohr        return $this->getRemoteTag('donationurl');
6157c9966a5SAndreas Gohr    }
6167c9966a5SAndreas Gohr
6174fd6a1d7SAndreas Gohr    /**
6184fd6a1d7SAndreas Gohr     * Get a list of extensions that are similar to this one
6194fd6a1d7SAndreas Gohr     *
6204fd6a1d7SAndreas Gohr     * @return string[]
6214fd6a1d7SAndreas Gohr     */
6224fd6a1d7SAndreas Gohr    public function getSimilarList()
6234fd6a1d7SAndreas Gohr    {
6244fd6a1d7SAndreas Gohr        return $this->getRemoteTag('similar', []);
6254fd6a1d7SAndreas Gohr    }
6264fd6a1d7SAndreas Gohr
6274fd6a1d7SAndreas Gohr    /**
6284fd6a1d7SAndreas Gohr     * Get a list of extensions that are marked as conflicting with this one
6294fd6a1d7SAndreas Gohr     *
6304fd6a1d7SAndreas Gohr     * @return string[]
6314fd6a1d7SAndreas Gohr     */
6324fd6a1d7SAndreas Gohr    public function getConflictList()
6334fd6a1d7SAndreas Gohr    {
6344fd6a1d7SAndreas Gohr        return $this->getRemoteTag('conflicts', []);
6354fd6a1d7SAndreas Gohr    }
6364fd6a1d7SAndreas Gohr
6374fd6a1d7SAndreas Gohr    /**
6384fd6a1d7SAndreas Gohr     * Get a list of DokuWiki versions this plugin is marked as compatible with
6394fd6a1d7SAndreas Gohr     *
6404fd6a1d7SAndreas Gohr     * @return string[][] date -> version
6414fd6a1d7SAndreas Gohr     */
6424fd6a1d7SAndreas Gohr    public function getCompatibleVersions()
6434fd6a1d7SAndreas Gohr    {
6444fd6a1d7SAndreas Gohr        return $this->getRemoteTag('compatible', []);
6454fd6a1d7SAndreas Gohr    }
6464fd6a1d7SAndreas Gohr
6477c9966a5SAndreas Gohr    // endregion
6487c9966a5SAndreas Gohr
649cf2dcf1bSAndreas Gohr    // region Actions
650cf2dcf1bSAndreas Gohr
651cf2dcf1bSAndreas Gohr    /**
652cf2dcf1bSAndreas Gohr     * Install or update the extension
653cf2dcf1bSAndreas Gohr     *
654cf2dcf1bSAndreas Gohr     * @throws Exception
655cf2dcf1bSAndreas Gohr     */
656cf2dcf1bSAndreas Gohr    public function installOrUpdate()
657cf2dcf1bSAndreas Gohr    {
658cf2dcf1bSAndreas Gohr        $installer = new Installer(true);
659160d3688SAndreas Gohr        $installer->installExtension($this);
660cf2dcf1bSAndreas Gohr    }
661cf2dcf1bSAndreas Gohr
662cf2dcf1bSAndreas Gohr    /**
663cf2dcf1bSAndreas Gohr     * Uninstall the extension
664cf2dcf1bSAndreas Gohr     * @throws Exception
665cf2dcf1bSAndreas Gohr     */
666cf2dcf1bSAndreas Gohr    public function uninstall()
667cf2dcf1bSAndreas Gohr    {
668cf2dcf1bSAndreas Gohr        $installer = new Installer(true);
669cf2dcf1bSAndreas Gohr        $installer->uninstall($this);
670cf2dcf1bSAndreas Gohr    }
671cf2dcf1bSAndreas Gohr
672cf2dcf1bSAndreas Gohr    /**
6735732c960SAndreas Gohr     * Toggle the extension between enabled and disabled
6745732c960SAndreas Gohr     * @return void
6755732c960SAndreas Gohr     * @throws Exception
6765732c960SAndreas Gohr     */
6775732c960SAndreas Gohr    public function toggle()
6785732c960SAndreas Gohr    {
6795732c960SAndreas Gohr        if ($this->isEnabled()) {
6805732c960SAndreas Gohr            $this->disable();
6815732c960SAndreas Gohr        } else {
6825732c960SAndreas Gohr            $this->enable();
6835732c960SAndreas Gohr        }
6845732c960SAndreas Gohr    }
6855732c960SAndreas Gohr
6865732c960SAndreas Gohr    /**
687cf2dcf1bSAndreas Gohr     * Enable the extension
688cf2dcf1bSAndreas Gohr     * @todo I'm unsure if this code should be here or part of Installer
689cf2dcf1bSAndreas Gohr     * @throws Exception
690cf2dcf1bSAndreas Gohr     */
691cf2dcf1bSAndreas Gohr    public function enable()
692cf2dcf1bSAndreas Gohr    {
693cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) throw new Exception('notimplemented');
694160d3688SAndreas Gohr        if (!$this->isInstalled()) throw new Exception('error_notinstalled', [$this->getId()]);
695160d3688SAndreas Gohr        if ($this->isEnabled()) throw new Exception('error_alreadyenabled', [$this->getId()]);
696cf2dcf1bSAndreas Gohr
697cf2dcf1bSAndreas Gohr        /* @var PluginController $plugin_controller */
698cf2dcf1bSAndreas Gohr        global $plugin_controller;
699cf2dcf1bSAndreas Gohr        if (!$plugin_controller->enable($this->base)) {
700cf2dcf1bSAndreas Gohr            throw new Exception('pluginlistsaveerror');
701cf2dcf1bSAndreas Gohr        }
702cf2dcf1bSAndreas Gohr        Installer::purgeCache();
703cf2dcf1bSAndreas Gohr    }
704cf2dcf1bSAndreas Gohr
705cf2dcf1bSAndreas Gohr    /**
706cf2dcf1bSAndreas Gohr     * Disable the extension
707cf2dcf1bSAndreas Gohr     * @todo I'm unsure if this code should be here or part of Installer
708cf2dcf1bSAndreas Gohr     * @throws Exception
709cf2dcf1bSAndreas Gohr     */
710cf2dcf1bSAndreas Gohr    public function disable()
711cf2dcf1bSAndreas Gohr    {
712cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) throw new Exception('notimplemented');
713160d3688SAndreas Gohr        if (!$this->isInstalled()) throw new Exception('error_notinstalled', [$this->getId()]);
714160d3688SAndreas Gohr        if (!$this->isEnabled()) throw new Exception('error_alreadydisabled', [$this->getId()]);
715160d3688SAndreas Gohr        if ($this->isProtected()) throw new Exception('error_disable_protected', [$this->getId()]);
716cf2dcf1bSAndreas Gohr
717cf2dcf1bSAndreas Gohr        /* @var PluginController $plugin_controller */
718cf2dcf1bSAndreas Gohr        global $plugin_controller;
719cf2dcf1bSAndreas Gohr        if (!$plugin_controller->disable($this->base)) {
720cf2dcf1bSAndreas Gohr            throw new Exception('pluginlistsaveerror');
721cf2dcf1bSAndreas Gohr        }
722cf2dcf1bSAndreas Gohr        Installer::purgeCache();
723cf2dcf1bSAndreas Gohr    }
724cf2dcf1bSAndreas Gohr
725cf2dcf1bSAndreas Gohr    // endregion
726cf2dcf1bSAndreas Gohr
727cf2dcf1bSAndreas Gohr    // region Meta Data Management
728cf2dcf1bSAndreas Gohr
729cf2dcf1bSAndreas Gohr    /**
7307c9966a5SAndreas Gohr     * Access the Manager for this extension
731cf2dcf1bSAndreas Gohr     *
7327c9966a5SAndreas Gohr     * @return Manager
733cf2dcf1bSAndreas Gohr     */
7347c9966a5SAndreas Gohr    public function getManager()
735cf2dcf1bSAndreas Gohr    {
7367c184cfcSAndreas Gohr        if (!$this->manager instanceof Manager) {
7377c9966a5SAndreas Gohr            $this->manager = new Manager($this);
738cf2dcf1bSAndreas Gohr        }
7397c9966a5SAndreas Gohr        return $this->manager;
740cf2dcf1bSAndreas Gohr    }
741cf2dcf1bSAndreas Gohr
742cf2dcf1bSAndreas Gohr    /**
743cf2dcf1bSAndreas Gohr     * Reads the info file of the extension if available and fills the localInfo array
744cf2dcf1bSAndreas Gohr     */
745cf2dcf1bSAndreas Gohr    protected function readLocalInfo()
746cf2dcf1bSAndreas Gohr    {
747a1e045f7SAndreas Gohr        if (!$this->getCurrentDir()) return;
748cf2dcf1bSAndreas Gohr        $file = $this->currentDir . '/' . $this->type . '.info.txt';
749cf2dcf1bSAndreas Gohr        if (!is_readable($file)) return;
750cf2dcf1bSAndreas Gohr        $this->localInfo = confToHash($file, true);
751cf2dcf1bSAndreas Gohr        $this->localInfo = array_filter($this->localInfo); // remove all falsy keys
752cf2dcf1bSAndreas Gohr    }
753cf2dcf1bSAndreas Gohr
754cf2dcf1bSAndreas Gohr    /**
755cf2dcf1bSAndreas Gohr     * Fetches the remote info from the repository
756cf2dcf1bSAndreas Gohr     *
757cf2dcf1bSAndreas Gohr     * This ignores any errors coming from the repository and just sets the remoteInfo to an empty array in that case
758cf2dcf1bSAndreas Gohr     */
759cf2dcf1bSAndreas Gohr    protected function loadRemoteInfo()
760cf2dcf1bSAndreas Gohr    {
761cf2dcf1bSAndreas Gohr        if ($this->remoteInfo) return;
762cf2dcf1bSAndreas Gohr        $remote = Repository::getInstance();
763cf2dcf1bSAndreas Gohr        try {
764cf2dcf1bSAndreas Gohr            $this->remoteInfo = (array)$remote->getExtensionData($this->getId());
765cf2dcf1bSAndreas Gohr        } catch (Exception $e) {
766cf2dcf1bSAndreas Gohr            $this->remoteInfo = [];
767cf2dcf1bSAndreas Gohr        }
768cf2dcf1bSAndreas Gohr    }
769cf2dcf1bSAndreas Gohr
770cf2dcf1bSAndreas Gohr    /**
771cf2dcf1bSAndreas Gohr     * Read information from either local or remote info
772cf2dcf1bSAndreas Gohr     *
7737c9966a5SAndreas Gohr     * Always prefers local info over remote info. Giving multiple keys is useful when the
7747c9966a5SAndreas Gohr     * key has been renamed in the past or if local and remote keys might differ.
775cf2dcf1bSAndreas Gohr     *
776cf2dcf1bSAndreas Gohr     * @param string|string[] $tag one or multiple keys to check
777cf2dcf1bSAndreas Gohr     * @param mixed $default
778cf2dcf1bSAndreas Gohr     * @return mixed
779cf2dcf1bSAndreas Gohr     */
780cf2dcf1bSAndreas Gohr    protected function getTag($tag, $default = '')
781cf2dcf1bSAndreas Gohr    {
782cf2dcf1bSAndreas Gohr        foreach ((array)$tag as $t) {
783cf2dcf1bSAndreas Gohr            if (isset($this->localInfo[$t])) return $this->localInfo[$t];
784cf2dcf1bSAndreas Gohr        }
7857c9966a5SAndreas Gohr
7867c9966a5SAndreas Gohr        return $this->getRemoteTag($tag, $default);
7877c9966a5SAndreas Gohr    }
7887c9966a5SAndreas Gohr
7897c9966a5SAndreas Gohr    /**
7907c9966a5SAndreas Gohr     * Read information from remote info
7917c9966a5SAndreas Gohr     *
7927c9966a5SAndreas Gohr     * @param string|string[] $tag one or mutiple keys to check
7937c9966a5SAndreas Gohr     * @param mixed $default
7947c9966a5SAndreas Gohr     * @return mixed
7957c9966a5SAndreas Gohr     */
7967c9966a5SAndreas Gohr    protected function getRemoteTag($tag, $default = '')
7977c9966a5SAndreas Gohr    {
798cf2dcf1bSAndreas Gohr        $this->loadRemoteInfo();
799cf2dcf1bSAndreas Gohr        foreach ((array)$tag as $t) {
800cf2dcf1bSAndreas Gohr            if (isset($this->remoteInfo[$t])) return $this->remoteInfo[$t];
801cf2dcf1bSAndreas Gohr        }
802cf2dcf1bSAndreas Gohr        return $default;
803cf2dcf1bSAndreas Gohr    }
804cf2dcf1bSAndreas Gohr
805cf2dcf1bSAndreas Gohr    // endregion
806a1e045f7SAndreas Gohr
807a1e045f7SAndreas Gohr    // region utilities
808a1e045f7SAndreas Gohr
809a1e045f7SAndreas Gohr    /**
810a1e045f7SAndreas Gohr     * Convert an extension id to a type and base
811a1e045f7SAndreas Gohr     *
812a1e045f7SAndreas Gohr     * @param string $id
813a1e045f7SAndreas Gohr     * @return array [type, base]
814a1e045f7SAndreas Gohr     */
815a1e045f7SAndreas Gohr    protected function idToTypeBase($id)
816a1e045f7SAndreas Gohr    {
817a1e045f7SAndreas Gohr        [$type, $base] = sexplode(':', $id, 2);
818a1e045f7SAndreas Gohr        if ($base === null) {
819a1e045f7SAndreas Gohr            $base = $type;
820a1e045f7SAndreas Gohr            $type = self::TYPE_PLUGIN;
821a1e045f7SAndreas Gohr        } elseif ($type === self::TYPE_TEMPLATE) {
822a1e045f7SAndreas Gohr            $type = self::TYPE_TEMPLATE;
823a1e045f7SAndreas Gohr        } else {
824a1e045f7SAndreas Gohr            throw new RuntimeException('Invalid extension id: ' . $id);
825a1e045f7SAndreas Gohr        }
826a1e045f7SAndreas Gohr
827a1e045f7SAndreas Gohr        return [$type, $base];
828a1e045f7SAndreas Gohr    }
8297c9966a5SAndreas Gohr    /**
8307c9966a5SAndreas Gohr     * @return string
8317c9966a5SAndreas Gohr     */
8327c9966a5SAndreas Gohr    public function __toString()
8337c9966a5SAndreas Gohr    {
8347c9966a5SAndreas Gohr        return $this->getId();
8357c9966a5SAndreas Gohr    }
8367c9966a5SAndreas Gohr
837a1e045f7SAndreas Gohr    // endregion
838cf2dcf1bSAndreas Gohr}
839