xref: /dokuwiki/lib/plugins/extension/Extension.php (revision a1ef4d6260401c454faedc5d90cb3887bb07a19c)
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
143e63733dSAndreas Gohr    /** @var string[] The types the API uses for plugin components */
153e63733dSAndreas Gohr    public const COMPONENT_TYPES = [
163e63733dSAndreas Gohr        1 => 'Syntax',
173e63733dSAndreas Gohr        2 => 'Admin',
183e63733dSAndreas Gohr        4 => 'Action',
193e63733dSAndreas Gohr        8 => 'Render',
203e63733dSAndreas Gohr        16 => 'Helper',
213e63733dSAndreas Gohr        32 => 'Template',
223e63733dSAndreas Gohr        64 => 'Remote',
233e63733dSAndreas Gohr        128 => 'Auth',
243e63733dSAndreas Gohr        256 => 'CLI',
253e63733dSAndreas Gohr        512 => 'CSS/JS-only',
263e63733dSAndreas Gohr    ];
273e63733dSAndreas Gohr
28*a1ef4d62SAndreas Gohr    /** @var string[] List of plugin component file base names */
29*a1ef4d62SAndreas Gohr    public const COMPONENT_FILES = [
30*a1ef4d62SAndreas Gohr        'syntax',
31*a1ef4d62SAndreas Gohr        'admin',
32*a1ef4d62SAndreas Gohr        'action',
33*a1ef4d62SAndreas Gohr        'render',
34*a1ef4d62SAndreas Gohr        'helper',
35*a1ef4d62SAndreas Gohr        'remote',
36*a1ef4d62SAndreas Gohr        'auth',
37*a1ef4d62SAndreas Gohr        'cli',
38*a1ef4d62SAndreas Gohr    ];
39*a1ef4d62SAndreas Gohr
40cf2dcf1bSAndreas Gohr    /** @var string "plugin"|"template" */
41cf2dcf1bSAndreas Gohr    protected string $type = self::TYPE_PLUGIN;
42cf2dcf1bSAndreas Gohr
43cf2dcf1bSAndreas Gohr    /** @var string The base name of this extension */
44cf2dcf1bSAndreas Gohr    protected string $base;
45cf2dcf1bSAndreas Gohr
4625d28a01SAndreas Gohr    /** @var string The current location of this extension */
4725d28a01SAndreas Gohr    protected string $currentDir = '';
48cf2dcf1bSAndreas Gohr
49cf2dcf1bSAndreas Gohr    /** @var array The local info array of the extension */
50cf2dcf1bSAndreas Gohr    protected array $localInfo = [];
51cf2dcf1bSAndreas Gohr
52cf2dcf1bSAndreas Gohr    /** @var array The remote info array of the extension */
53cf2dcf1bSAndreas Gohr    protected array $remoteInfo = [];
54cf2dcf1bSAndreas Gohr
557c9966a5SAndreas Gohr    /** @var Manager|null The manager for this extension */
5625d28a01SAndreas Gohr    protected ?Manager $manager = null;
57cf2dcf1bSAndreas Gohr
58cf2dcf1bSAndreas Gohr    // region Constructors
59cf2dcf1bSAndreas Gohr
60cf2dcf1bSAndreas Gohr    /**
61cf2dcf1bSAndreas Gohr     * The main constructor is private to force the use of the factory methods
62cf2dcf1bSAndreas Gohr     */
63cf2dcf1bSAndreas Gohr    protected function __construct()
64cf2dcf1bSAndreas Gohr    {
65cf2dcf1bSAndreas Gohr    }
66cf2dcf1bSAndreas Gohr
67cf2dcf1bSAndreas Gohr    /**
68a1e045f7SAndreas Gohr     * Initializes an extension from an id
69a1e045f7SAndreas Gohr     *
70a1e045f7SAndreas Gohr     * @param string $id The id of the extension
71a1e045f7SAndreas Gohr     * @return Extension
72a1e045f7SAndreas Gohr     */
73a1e045f7SAndreas Gohr    public static function createFromId($id)
74a1e045f7SAndreas Gohr    {
75a1e045f7SAndreas Gohr        $extension = new self();
76a1e045f7SAndreas Gohr        $extension->initFromId($id);
77a1e045f7SAndreas Gohr        return $extension;
78a1e045f7SAndreas Gohr    }
79a1e045f7SAndreas Gohr
80a1e045f7SAndreas Gohr    protected function initFromId($id)
81a1e045f7SAndreas Gohr    {
82a1e045f7SAndreas Gohr        [$type, $base] = $this->idToTypeBase($id);
83a1e045f7SAndreas Gohr        $this->type = $type;
84a1e045f7SAndreas Gohr        $this->base = $base;
85a1e045f7SAndreas Gohr        $this->readLocalInfo();
86a1e045f7SAndreas Gohr    }
87a1e045f7SAndreas Gohr
88a1e045f7SAndreas Gohr    /**
89cf2dcf1bSAndreas Gohr     * Initializes an extension from a directory
90cf2dcf1bSAndreas Gohr     *
91cf2dcf1bSAndreas Gohr     * The given directory might be the one where the extension has already been installed to
92cf2dcf1bSAndreas Gohr     * or it might be the extracted source in some temporary directory.
93cf2dcf1bSAndreas Gohr     *
94cf2dcf1bSAndreas Gohr     * @param string $dir Where the extension code is currently located
95cf2dcf1bSAndreas Gohr     * @param string|null $type TYPE_PLUGIN|TYPE_TEMPLATE, null for auto-detection
96cf2dcf1bSAndreas Gohr     * @param string $base The base name of the extension, null for auto-detection
97cf2dcf1bSAndreas Gohr     * @return Extension
98cf2dcf1bSAndreas Gohr     */
99cf2dcf1bSAndreas Gohr    public static function createFromDirectory($dir, $type = null, $base = null)
100cf2dcf1bSAndreas Gohr    {
101cf2dcf1bSAndreas Gohr        $extension = new self();
102cf2dcf1bSAndreas Gohr        $extension->initFromDirectory($dir, $type, $base);
103cf2dcf1bSAndreas Gohr        return $extension;
104cf2dcf1bSAndreas Gohr    }
105cf2dcf1bSAndreas Gohr
106cf2dcf1bSAndreas Gohr    protected function initFromDirectory($dir, $type = null, $base = null)
107cf2dcf1bSAndreas Gohr    {
108cf2dcf1bSAndreas Gohr        if (!is_dir($dir)) throw new RuntimeException('Directory not found: ' . $dir);
109e8fd67e9SAndreas Gohr        $this->currentDir = fullpath($dir);
110cf2dcf1bSAndreas Gohr
111cf2dcf1bSAndreas Gohr        if ($type === null || $type === self::TYPE_TEMPLATE) {
112cf2dcf1bSAndreas Gohr            if (
1138fe483c9SAndreas Gohr                file_exists($dir . '/template.info.txt') ||
114cf2dcf1bSAndreas Gohr                file_exists($dir . '/style.ini') ||
115cf2dcf1bSAndreas Gohr                file_exists($dir . '/main.php') ||
116cf2dcf1bSAndreas Gohr                file_exists($dir . '/detail.php') ||
117cf2dcf1bSAndreas Gohr                file_exists($dir . '/mediamanager.php')
118cf2dcf1bSAndreas Gohr            ) {
119cf2dcf1bSAndreas Gohr                $this->type = self::TYPE_TEMPLATE;
120cf2dcf1bSAndreas Gohr            }
121cf2dcf1bSAndreas Gohr        } else {
122cf2dcf1bSAndreas Gohr            $this->type = self::TYPE_PLUGIN;
123cf2dcf1bSAndreas Gohr        }
124cf2dcf1bSAndreas Gohr
125cf2dcf1bSAndreas Gohr        $this->readLocalInfo();
126cf2dcf1bSAndreas Gohr
127cf2dcf1bSAndreas Gohr        if ($base !== null) {
128cf2dcf1bSAndreas Gohr            $this->base = $base;
129cf2dcf1bSAndreas Gohr        } elseif (isset($this->localInfo['base'])) {
130cf2dcf1bSAndreas Gohr            $this->base = $this->localInfo['base'];
131cf2dcf1bSAndreas Gohr        } else {
132*a1ef4d62SAndreas Gohr            $this->base = $this->getBaseFromClass($dir) ?: basename($dir);
133cf2dcf1bSAndreas Gohr        }
134cf2dcf1bSAndreas Gohr    }
135cf2dcf1bSAndreas Gohr
136cf2dcf1bSAndreas Gohr    /**
137cf2dcf1bSAndreas Gohr     * Initializes an extension from remote data
138cf2dcf1bSAndreas Gohr     *
139cf2dcf1bSAndreas Gohr     * @param array $data The data as returned by the repository api
140cf2dcf1bSAndreas Gohr     * @return Extension
141cf2dcf1bSAndreas Gohr     */
142cf2dcf1bSAndreas Gohr    public static function createFromRemoteData($data)
143cf2dcf1bSAndreas Gohr    {
144cf2dcf1bSAndreas Gohr        $extension = new self();
145cf2dcf1bSAndreas Gohr        $extension->initFromRemoteData($data);
146cf2dcf1bSAndreas Gohr        return $extension;
147cf2dcf1bSAndreas Gohr    }
148cf2dcf1bSAndreas Gohr
149cf2dcf1bSAndreas Gohr    protected function initFromRemoteData($data)
150cf2dcf1bSAndreas Gohr    {
151cf2dcf1bSAndreas Gohr        if (!isset($data['plugin'])) throw new RuntimeException('Invalid remote data');
152cf2dcf1bSAndreas Gohr
153a1e045f7SAndreas Gohr        [$type, $base] = $this->idToTypeBase($data['plugin']);
154cf2dcf1bSAndreas Gohr        $this->remoteInfo = $data;
155cf2dcf1bSAndreas Gohr        $this->type = $type;
156cf2dcf1bSAndreas Gohr        $this->base = $base;
157cf2dcf1bSAndreas Gohr
158cf2dcf1bSAndreas Gohr        if ($this->isInstalled()) {
159cf2dcf1bSAndreas Gohr            $this->currentDir = $this->getInstallDir();
160cf2dcf1bSAndreas Gohr            $this->readLocalInfo();
161cf2dcf1bSAndreas Gohr        }
162cf2dcf1bSAndreas Gohr    }
163cf2dcf1bSAndreas Gohr
164cf2dcf1bSAndreas Gohr    // endregion
165cf2dcf1bSAndreas Gohr
166cf2dcf1bSAndreas Gohr    // region Getters
167cf2dcf1bSAndreas Gohr
168cf2dcf1bSAndreas Gohr    /**
1694fd6a1d7SAndreas Gohr     * @param bool $wrap If true, the id is wrapped in backticks
170cf2dcf1bSAndreas Gohr     * @return string The extension id (same as base but prefixed with "template:" for templates)
171cf2dcf1bSAndreas Gohr     */
1724fd6a1d7SAndreas Gohr    public function getId($wrap = false)
173cf2dcf1bSAndreas Gohr    {
174cf2dcf1bSAndreas Gohr        if ($this->type === self::TYPE_TEMPLATE) {
1754fd6a1d7SAndreas Gohr            $id = self::TYPE_TEMPLATE . ':' . $this->base;
1764fd6a1d7SAndreas Gohr        } else {
1774fd6a1d7SAndreas Gohr            $id = $this->base;
178cf2dcf1bSAndreas Gohr        }
1794fd6a1d7SAndreas Gohr        if ($wrap) $id = "`$id`";
1804fd6a1d7SAndreas Gohr        return $id;
181cf2dcf1bSAndreas Gohr    }
182cf2dcf1bSAndreas Gohr
183cf2dcf1bSAndreas Gohr    /**
184cf2dcf1bSAndreas Gohr     * Get the base name of this extension
185cf2dcf1bSAndreas Gohr     *
186cf2dcf1bSAndreas Gohr     * @return string
187cf2dcf1bSAndreas Gohr     */
188cf2dcf1bSAndreas Gohr    public function getBase()
189cf2dcf1bSAndreas Gohr    {
190cf2dcf1bSAndreas Gohr        return $this->base;
191cf2dcf1bSAndreas Gohr    }
192cf2dcf1bSAndreas Gohr
193cf2dcf1bSAndreas Gohr    /**
194cf2dcf1bSAndreas Gohr     * Get the type of the extension
195cf2dcf1bSAndreas Gohr     *
196cf2dcf1bSAndreas Gohr     * @return string "plugin"|"template"
197cf2dcf1bSAndreas Gohr     */
198cf2dcf1bSAndreas Gohr    public function getType()
199cf2dcf1bSAndreas Gohr    {
200cf2dcf1bSAndreas Gohr        return $this->type;
201cf2dcf1bSAndreas Gohr    }
202cf2dcf1bSAndreas Gohr
203cf2dcf1bSAndreas Gohr    /**
204cf2dcf1bSAndreas Gohr     * The current directory of the extension
205cf2dcf1bSAndreas Gohr     *
206cf2dcf1bSAndreas Gohr     * @return string|null
207cf2dcf1bSAndreas Gohr     */
208cf2dcf1bSAndreas Gohr    public function getCurrentDir()
209cf2dcf1bSAndreas Gohr    {
210cf2dcf1bSAndreas Gohr        // recheck that the current currentDir is still valid
211cf2dcf1bSAndreas Gohr        if ($this->currentDir && !is_dir($this->currentDir)) {
21225d28a01SAndreas Gohr            $this->currentDir = '';
213cf2dcf1bSAndreas Gohr        }
214cf2dcf1bSAndreas Gohr
215cf2dcf1bSAndreas Gohr        // if the extension is installed, then the currentDir is the install dir!
216cf2dcf1bSAndreas Gohr        if (!$this->currentDir && $this->isInstalled()) {
217cf2dcf1bSAndreas Gohr            $this->currentDir = $this->getInstallDir();
218cf2dcf1bSAndreas Gohr        }
219cf2dcf1bSAndreas Gohr
220cf2dcf1bSAndreas Gohr        return $this->currentDir;
221cf2dcf1bSAndreas Gohr    }
222cf2dcf1bSAndreas Gohr
223cf2dcf1bSAndreas Gohr    /**
224cf2dcf1bSAndreas Gohr     * Get the directory where this extension should be installed in
225cf2dcf1bSAndreas Gohr     *
226cf2dcf1bSAndreas Gohr     * Note: this does not mean that the extension is actually installed there
227cf2dcf1bSAndreas Gohr     *
228cf2dcf1bSAndreas Gohr     * @return string
229cf2dcf1bSAndreas Gohr     */
230cf2dcf1bSAndreas Gohr    public function getInstallDir()
231cf2dcf1bSAndreas Gohr    {
232cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) {
233176901c2SAndreas Gohr            $dir = dirname(tpl_incdir()) . '/' . $this->base;
234cf2dcf1bSAndreas Gohr        } else {
235cf2dcf1bSAndreas Gohr            $dir = DOKU_PLUGIN . $this->base;
236cf2dcf1bSAndreas Gohr        }
237cf2dcf1bSAndreas Gohr
23825d28a01SAndreas Gohr        return fullpath($dir);
239cf2dcf1bSAndreas Gohr    }
240cf2dcf1bSAndreas Gohr
241cf2dcf1bSAndreas Gohr
242cf2dcf1bSAndreas Gohr    /**
243cf2dcf1bSAndreas Gohr     * Get the display name of the extension
244cf2dcf1bSAndreas Gohr     *
245cf2dcf1bSAndreas Gohr     * @return string
246cf2dcf1bSAndreas Gohr     */
247cf2dcf1bSAndreas Gohr    public function getDisplayName()
248cf2dcf1bSAndreas Gohr    {
249cf2dcf1bSAndreas Gohr        return $this->getTag('name', PhpString::ucwords($this->getBase() . ' ' . $this->getType()));
250cf2dcf1bSAndreas Gohr    }
251cf2dcf1bSAndreas Gohr
252cf2dcf1bSAndreas Gohr    /**
253cf2dcf1bSAndreas Gohr     * Get the author name of the extension
254cf2dcf1bSAndreas Gohr     *
255cf2dcf1bSAndreas Gohr     * @return string Returns an empty string if the author info is missing
256cf2dcf1bSAndreas Gohr     */
257cf2dcf1bSAndreas Gohr    public function getAuthor()
258cf2dcf1bSAndreas Gohr    {
259cf2dcf1bSAndreas Gohr        return $this->getTag('author');
260cf2dcf1bSAndreas Gohr    }
261cf2dcf1bSAndreas Gohr
262cf2dcf1bSAndreas Gohr    /**
263cf2dcf1bSAndreas Gohr     * Get the email of the author of the extension if there is any
264cf2dcf1bSAndreas Gohr     *
265cf2dcf1bSAndreas Gohr     * @return string Returns an empty string if the email info is missing
266cf2dcf1bSAndreas Gohr     */
267cf2dcf1bSAndreas Gohr    public function getEmail()
268cf2dcf1bSAndreas Gohr    {
269cf2dcf1bSAndreas Gohr        // email is only in the local data
270cf2dcf1bSAndreas Gohr        return $this->localInfo['email'] ?? '';
271cf2dcf1bSAndreas Gohr    }
272cf2dcf1bSAndreas Gohr
273cf2dcf1bSAndreas Gohr    /**
274cf2dcf1bSAndreas Gohr     * Get the email id, i.e. the md5sum of the email
275cf2dcf1bSAndreas Gohr     *
276cf2dcf1bSAndreas Gohr     * @return string Empty string if no email is available
277cf2dcf1bSAndreas Gohr     */
278cf2dcf1bSAndreas Gohr    public function getEmailID()
279cf2dcf1bSAndreas Gohr    {
280cf2dcf1bSAndreas Gohr        if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
281cf2dcf1bSAndreas Gohr        if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
282cf2dcf1bSAndreas Gohr        return '';
283cf2dcf1bSAndreas Gohr    }
284cf2dcf1bSAndreas Gohr
285cf2dcf1bSAndreas Gohr    /**
286cf2dcf1bSAndreas Gohr     * Get the description of the extension
287cf2dcf1bSAndreas Gohr     *
288cf2dcf1bSAndreas Gohr     * @return string Empty string if no description is available
289cf2dcf1bSAndreas Gohr     */
290cf2dcf1bSAndreas Gohr    public function getDescription()
291cf2dcf1bSAndreas Gohr    {
292cf2dcf1bSAndreas Gohr        return $this->getTag(['desc', 'description']);
293cf2dcf1bSAndreas Gohr    }
294cf2dcf1bSAndreas Gohr
295cf2dcf1bSAndreas Gohr    /**
296cf2dcf1bSAndreas Gohr     * Get the URL of the extension, usually a page on dokuwiki.org
297cf2dcf1bSAndreas Gohr     *
298cf2dcf1bSAndreas Gohr     * @return string
299cf2dcf1bSAndreas Gohr     */
300cf2dcf1bSAndreas Gohr    public function getURL()
301cf2dcf1bSAndreas Gohr    {
302cf2dcf1bSAndreas Gohr        return $this->getTag(
303cf2dcf1bSAndreas Gohr            'url',
304cf2dcf1bSAndreas Gohr            'https://www.dokuwiki.org/' .
305cf2dcf1bSAndreas Gohr            ($this->isTemplate() ? 'template' : 'plugin') . ':' . $this->getBase()
306cf2dcf1bSAndreas Gohr        );
307cf2dcf1bSAndreas Gohr    }
308cf2dcf1bSAndreas Gohr
309cf2dcf1bSAndreas Gohr    /**
3107c9966a5SAndreas Gohr     * Get the version of the extension that is actually installed
3117c9966a5SAndreas Gohr     *
3127c9966a5SAndreas Gohr     * Returns an empty string if the version is not available
3137c9966a5SAndreas Gohr     *
3147c9966a5SAndreas Gohr     * @return string
3157c9966a5SAndreas Gohr     */
3167c9966a5SAndreas Gohr    public function getInstalledVersion()
3177c9966a5SAndreas Gohr    {
3187c9966a5SAndreas Gohr        return $this->localInfo['date'] ?? '';
3197c9966a5SAndreas Gohr    }
3207c9966a5SAndreas Gohr
3217c9966a5SAndreas Gohr    /**
3224fd6a1d7SAndreas Gohr     * Get the types of components this extension provides
3234fd6a1d7SAndreas Gohr     *
3244fd6a1d7SAndreas Gohr     * @return array int -> type
3254fd6a1d7SAndreas Gohr     */
3264fd6a1d7SAndreas Gohr    public function getComponentTypes()
3274fd6a1d7SAndreas Gohr    {
3288fe483c9SAndreas Gohr        // for installed extensions we can check the files
3298fe483c9SAndreas Gohr        if ($this->isInstalled()) {
3308fe483c9SAndreas Gohr            if ($this->isTemplate()) {
3318fe483c9SAndreas Gohr                return ['Template'];
3328fe483c9SAndreas Gohr            } else {
3338fe483c9SAndreas Gohr                $types = [];
3343e63733dSAndreas Gohr                foreach (self::COMPONENT_TYPES as $type) {
3358fe483c9SAndreas Gohr                    $check = strtolower($type);
3368fe483c9SAndreas Gohr                    if (
3378fe483c9SAndreas Gohr                        file_exists($this->getInstallDir() . '/' . $check . '.php') ||
3388fe483c9SAndreas Gohr                        is_dir($this->getInstallDir() . '/' . $check)
3398fe483c9SAndreas Gohr                    ) {
3408fe483c9SAndreas Gohr                        $types[] = $type;
3418fe483c9SAndreas Gohr                    }
3428fe483c9SAndreas Gohr                }
3438fe483c9SAndreas Gohr                return $types;
3448fe483c9SAndreas Gohr            }
3458fe483c9SAndreas Gohr        }
3468fe483c9SAndreas Gohr        // still, here? use the remote info
3474fd6a1d7SAndreas Gohr        return $this->getTag('types', []);
3484fd6a1d7SAndreas Gohr    }
3494fd6a1d7SAndreas Gohr
3504fd6a1d7SAndreas Gohr    /**
35125d28a01SAndreas Gohr     * Get a list of extension ids this extension depends on
35225d28a01SAndreas Gohr     *
35325d28a01SAndreas Gohr     * @return string[]
35425d28a01SAndreas Gohr     */
35525d28a01SAndreas Gohr    public function getDependencyList()
35625d28a01SAndreas Gohr    {
35725d28a01SAndreas Gohr        return $this->getTag('depends', []);
35825d28a01SAndreas Gohr    }
35925d28a01SAndreas Gohr
36025d28a01SAndreas Gohr    /**
361b69d74f1SAndreas Gohr     * Get a list of extensions that are currently installed, enabled and depend on this extension
362b69d74f1SAndreas Gohr     *
363b69d74f1SAndreas Gohr     * @return Extension[]
364b69d74f1SAndreas Gohr     */
365b69d74f1SAndreas Gohr    public function getDependants()
366b69d74f1SAndreas Gohr    {
367b69d74f1SAndreas Gohr        $local = new Local();
368b69d74f1SAndreas Gohr        $extensions = $local->getExtensions();
369b69d74f1SAndreas Gohr        $dependants = [];
370b69d74f1SAndreas Gohr        foreach ($extensions as $extension) {
371b69d74f1SAndreas Gohr            if (
372b69d74f1SAndreas Gohr                in_array($this->getId(), $extension->getDependencyList()) &&
373b69d74f1SAndreas Gohr                $extension->isEnabled()
374b69d74f1SAndreas Gohr            ) {
375b69d74f1SAndreas Gohr                $dependants[$extension->getId()] = $extension;
376b69d74f1SAndreas Gohr            }
377b69d74f1SAndreas Gohr        }
378b69d74f1SAndreas Gohr        return $dependants;
379b69d74f1SAndreas Gohr    }
380b69d74f1SAndreas Gohr
381b69d74f1SAndreas Gohr    /**
382b2a05b76SAndreas Gohr     * Return the minimum PHP version required by the extension
383b2a05b76SAndreas Gohr     *
384b2a05b76SAndreas Gohr     * Empty if not set
385b2a05b76SAndreas Gohr     *
386b2a05b76SAndreas Gohr     * @return string
387b2a05b76SAndreas Gohr     */
388b2a05b76SAndreas Gohr    public function getMinimumPHPVersion()
389b2a05b76SAndreas Gohr    {
390b2a05b76SAndreas Gohr        return $this->getTag('phpmin', '');
391b2a05b76SAndreas Gohr    }
392b2a05b76SAndreas Gohr
393b2a05b76SAndreas Gohr    /**
394b2a05b76SAndreas Gohr     * Return the minimum PHP version supported by the extension
395b2a05b76SAndreas Gohr     *
396b2a05b76SAndreas Gohr     * @return string
397b2a05b76SAndreas Gohr     */
398b2a05b76SAndreas Gohr    public function getMaximumPHPVersion()
399b2a05b76SAndreas Gohr    {
400b2a05b76SAndreas Gohr        return $this->getTag('phpmax', '');
401b2a05b76SAndreas Gohr    }
402b2a05b76SAndreas Gohr
403b2a05b76SAndreas Gohr    /**
404cf2dcf1bSAndreas Gohr     * Is this extension a template?
405cf2dcf1bSAndreas Gohr     *
406cf2dcf1bSAndreas Gohr     * @return bool false if it is a plugin
407cf2dcf1bSAndreas Gohr     */
408cf2dcf1bSAndreas Gohr    public function isTemplate()
409cf2dcf1bSAndreas Gohr    {
410cf2dcf1bSAndreas Gohr        return $this->type === self::TYPE_TEMPLATE;
411cf2dcf1bSAndreas Gohr    }
412cf2dcf1bSAndreas Gohr
413cf2dcf1bSAndreas Gohr    /**
414cf2dcf1bSAndreas Gohr     * Is the extension installed locally?
415cf2dcf1bSAndreas Gohr     *
416cf2dcf1bSAndreas Gohr     * @return bool
417cf2dcf1bSAndreas Gohr     */
418cf2dcf1bSAndreas Gohr    public function isInstalled()
419cf2dcf1bSAndreas Gohr    {
420cf2dcf1bSAndreas Gohr        return is_dir($this->getInstallDir());
421cf2dcf1bSAndreas Gohr    }
422cf2dcf1bSAndreas Gohr
423cf2dcf1bSAndreas Gohr    /**
424cf2dcf1bSAndreas Gohr     * Is the extension under git control?
425cf2dcf1bSAndreas Gohr     *
426cf2dcf1bSAndreas Gohr     * @return bool
427cf2dcf1bSAndreas Gohr     */
428cf2dcf1bSAndreas Gohr    public function isGitControlled()
429cf2dcf1bSAndreas Gohr    {
430cf2dcf1bSAndreas Gohr        if (!$this->isInstalled()) return false;
431cf2dcf1bSAndreas Gohr        return file_exists($this->getInstallDir() . '/.git');
432cf2dcf1bSAndreas Gohr    }
433cf2dcf1bSAndreas Gohr
434cf2dcf1bSAndreas Gohr    /**
435cf2dcf1bSAndreas Gohr     * If the extension is bundled
436cf2dcf1bSAndreas Gohr     *
437cf2dcf1bSAndreas Gohr     * @return bool If the extension is bundled
438cf2dcf1bSAndreas Gohr     */
439cf2dcf1bSAndreas Gohr    public function isBundled()
440cf2dcf1bSAndreas Gohr    {
441cf2dcf1bSAndreas Gohr        $this->loadRemoteInfo();
442cf2dcf1bSAndreas Gohr        return $this->remoteInfo['bundled'] ?? in_array(
443cf2dcf1bSAndreas Gohr            $this->getId(),
444cf2dcf1bSAndreas Gohr            [
445cf2dcf1bSAndreas Gohr                'authad',
446cf2dcf1bSAndreas Gohr                'authldap',
447cf2dcf1bSAndreas Gohr                'authpdo',
448cf2dcf1bSAndreas Gohr                'authplain',
449cf2dcf1bSAndreas Gohr                'acl',
450cf2dcf1bSAndreas Gohr                'config',
451cf2dcf1bSAndreas Gohr                'extension',
452cf2dcf1bSAndreas Gohr                'info',
453cf2dcf1bSAndreas Gohr                'popularity',
454cf2dcf1bSAndreas Gohr                'revert',
455cf2dcf1bSAndreas Gohr                'safefnrecode',
456cf2dcf1bSAndreas Gohr                'styling',
457cf2dcf1bSAndreas Gohr                'testing',
458cf2dcf1bSAndreas Gohr                'usermanager',
459cf2dcf1bSAndreas Gohr                'logviewer',
460cf2dcf1bSAndreas Gohr                'template:dokuwiki'
461cf2dcf1bSAndreas Gohr            ]
462cf2dcf1bSAndreas Gohr        );
463cf2dcf1bSAndreas Gohr    }
464cf2dcf1bSAndreas Gohr
465cf2dcf1bSAndreas Gohr    /**
466cf2dcf1bSAndreas Gohr     * Is the extension protected against any modification (disable/uninstall)
467cf2dcf1bSAndreas Gohr     *
468cf2dcf1bSAndreas Gohr     * @return bool if the extension is protected
469cf2dcf1bSAndreas Gohr     */
470cf2dcf1bSAndreas Gohr    public function isProtected()
471cf2dcf1bSAndreas Gohr    {
472cf2dcf1bSAndreas Gohr        // never allow deinstalling the current auth plugin:
473cf2dcf1bSAndreas Gohr        global $conf;
474cf2dcf1bSAndreas Gohr        if ($this->getId() == $conf['authtype']) return true;
475cf2dcf1bSAndreas Gohr
476077f55feSAndreas Gohr        // disallow current template to be uninstalled
477077f55feSAndreas Gohr        if ($this->isTemplate() && ($this->getBase() === $conf['template'])) return true;
478cf2dcf1bSAndreas Gohr
479cf2dcf1bSAndreas Gohr        /** @var PluginController $plugin_controller */
480cf2dcf1bSAndreas Gohr        global $plugin_controller;
481cf2dcf1bSAndreas Gohr        $cascade = $plugin_controller->getCascade();
482cf2dcf1bSAndreas Gohr        return ($cascade['protected'][$this->getId()] ?? false);
483cf2dcf1bSAndreas Gohr    }
484cf2dcf1bSAndreas Gohr
485cf2dcf1bSAndreas Gohr    /**
486cf2dcf1bSAndreas Gohr     * Is the extension installed in the correct directory?
487cf2dcf1bSAndreas Gohr     *
488cf2dcf1bSAndreas Gohr     * @return bool
489cf2dcf1bSAndreas Gohr     */
490cf2dcf1bSAndreas Gohr    public function isInWrongFolder()
491cf2dcf1bSAndreas Gohr    {
4924fd6a1d7SAndreas Gohr        if (!$this->isInstalled()) return false;
493cf2dcf1bSAndreas Gohr        return $this->getInstallDir() != $this->currentDir;
494cf2dcf1bSAndreas Gohr    }
495cf2dcf1bSAndreas Gohr
496cf2dcf1bSAndreas Gohr    /**
497cf2dcf1bSAndreas Gohr     * Is the extension enabled?
498cf2dcf1bSAndreas Gohr     *
499cf2dcf1bSAndreas Gohr     * @return bool
500cf2dcf1bSAndreas Gohr     */
501cf2dcf1bSAndreas Gohr    public function isEnabled()
502cf2dcf1bSAndreas Gohr    {
503cf2dcf1bSAndreas Gohr        global $conf;
504cf2dcf1bSAndreas Gohr        if ($this->isTemplate()) {
505cf2dcf1bSAndreas Gohr            return ($conf['template'] == $this->getBase());
506cf2dcf1bSAndreas Gohr        }
507cf2dcf1bSAndreas Gohr
508cf2dcf1bSAndreas Gohr        /* @var PluginController $plugin_controller */
509cf2dcf1bSAndreas Gohr        global $plugin_controller;
510cf2dcf1bSAndreas Gohr        return $plugin_controller->isEnabled($this->base);
511cf2dcf1bSAndreas Gohr    }
512cf2dcf1bSAndreas Gohr
513160d3688SAndreas Gohr    /**
514160d3688SAndreas Gohr     * Has the download URL changed since the last download?
515160d3688SAndreas Gohr     *
516160d3688SAndreas Gohr     * @return bool
517160d3688SAndreas Gohr     */
518160d3688SAndreas Gohr    public function hasChangedURL()
519160d3688SAndreas Gohr    {
5204fd6a1d7SAndreas Gohr        $last = $this->getManager()->getDownloadURL();
521160d3688SAndreas Gohr        if (!$last) return false;
5222ee9c305Sfiwswe        $url = $this->getDownloadURL();
5232ee9c305Sfiwswe        if (!$url) return false;
5242ee9c305Sfiwswe        return $last !== $url;
525160d3688SAndreas Gohr    }
526160d3688SAndreas Gohr
527160d3688SAndreas Gohr    /**
528160d3688SAndreas Gohr     * Is an update available for this extension?
529160d3688SAndreas Gohr     *
530160d3688SAndreas Gohr     * @return bool
531160d3688SAndreas Gohr     */
5324fd6a1d7SAndreas Gohr    public function isUpdateAvailable()
533160d3688SAndreas Gohr    {
534160d3688SAndreas Gohr        if ($this->isBundled()) return false; // bundled extensions are never updated
535160d3688SAndreas Gohr        $self = $this->getInstalledVersion();
536160d3688SAndreas Gohr        $remote = $this->getLastUpdate();
537160d3688SAndreas Gohr        return $self < $remote;
538160d3688SAndreas Gohr    }
539160d3688SAndreas Gohr
540cf2dcf1bSAndreas Gohr    // endregion
541cf2dcf1bSAndreas Gohr
5427c9966a5SAndreas Gohr    // region Remote Info
5437c9966a5SAndreas Gohr
5447c9966a5SAndreas Gohr    /**
5457c9966a5SAndreas Gohr     * Get the date of the last available update
5467c9966a5SAndreas Gohr     *
5477c9966a5SAndreas Gohr     * @return string yyyy-mm-dd
5487c9966a5SAndreas Gohr     */
5497c9966a5SAndreas Gohr    public function getLastUpdate()
5507c9966a5SAndreas Gohr    {
5517c9966a5SAndreas Gohr        return $this->getRemoteTag('lastupdate');
5527c9966a5SAndreas Gohr    }
5537c9966a5SAndreas Gohr
5547c9966a5SAndreas Gohr    /**
5557c9966a5SAndreas Gohr     * Get a list of tags this extension is tagged with at dokuwiki.org
5567c9966a5SAndreas Gohr     *
5577c9966a5SAndreas Gohr     * @return string[]
5587c9966a5SAndreas Gohr     */
5597c9966a5SAndreas Gohr    public function getTags()
5607c9966a5SAndreas Gohr    {
5617c9966a5SAndreas Gohr        return $this->getRemoteTag('tags', []);
5627c9966a5SAndreas Gohr    }
5637c9966a5SAndreas Gohr
5647c9966a5SAndreas Gohr    /**
5657c9966a5SAndreas Gohr     * Get the popularity of the extension
5667c9966a5SAndreas Gohr     *
5677c9966a5SAndreas Gohr     * This is a float between 0 and 1
5687c9966a5SAndreas Gohr     *
5697c9966a5SAndreas Gohr     * @return float
5707c9966a5SAndreas Gohr     */
5717c9966a5SAndreas Gohr    public function getPopularity()
5727c9966a5SAndreas Gohr    {
5737c9966a5SAndreas Gohr        return (float)$this->getRemoteTag('popularity', 0);
5747c9966a5SAndreas Gohr    }
5757c9966a5SAndreas Gohr
5767c9966a5SAndreas Gohr    /**
5777c9966a5SAndreas Gohr     * Get the text of the update message if there is any
5787c9966a5SAndreas Gohr     *
5797c9966a5SAndreas Gohr     * @return string
5807c9966a5SAndreas Gohr     */
5817c9966a5SAndreas Gohr    public function getUpdateMessage()
5827c9966a5SAndreas Gohr    {
5837c9966a5SAndreas Gohr        return $this->getRemoteTag('updatemessage');
5847c9966a5SAndreas Gohr    }
5857c9966a5SAndreas Gohr
5867c9966a5SAndreas Gohr    /**
5877c9966a5SAndreas Gohr     * Get the text of the security warning if there is any
5887c9966a5SAndreas Gohr     *
5897c9966a5SAndreas Gohr     * @return string
5907c9966a5SAndreas Gohr     */
5917c9966a5SAndreas Gohr    public function getSecurityWarning()
5927c9966a5SAndreas Gohr    {
5937c9966a5SAndreas Gohr        return $this->getRemoteTag('securitywarning');
5947c9966a5SAndreas Gohr    }
5957c9966a5SAndreas Gohr
5967c9966a5SAndreas Gohr    /**
5977c9966a5SAndreas Gohr     * Get the text of the security issue if there is any
5987c9966a5SAndreas Gohr     *
5997c9966a5SAndreas Gohr     * @return string
6007c9966a5SAndreas Gohr     */
6017c9966a5SAndreas Gohr    public function getSecurityIssue()
6027c9966a5SAndreas Gohr    {
6037c9966a5SAndreas Gohr        return $this->getRemoteTag('securityissue');
6047c9966a5SAndreas Gohr    }
6057c9966a5SAndreas Gohr
6067c9966a5SAndreas Gohr    /**
6077c9966a5SAndreas Gohr     * Get the URL of the screenshot of the extension if there is any
6087c9966a5SAndreas Gohr     *
6097c9966a5SAndreas Gohr     * @return string
6107c9966a5SAndreas Gohr     */
6117c9966a5SAndreas Gohr    public function getScreenshotURL()
6127c9966a5SAndreas Gohr    {
6137c9966a5SAndreas Gohr        return $this->getRemoteTag('screenshoturl');
6147c9966a5SAndreas Gohr    }
6157c9966a5SAndreas Gohr
6167c9966a5SAndreas Gohr    /**
6177c9966a5SAndreas Gohr     * Get the URL of the thumbnail of the extension if there is any
6187c9966a5SAndreas Gohr     *
6197c9966a5SAndreas Gohr     * @return string
6207c9966a5SAndreas Gohr     */
6217c9966a5SAndreas Gohr    public function getThumbnailURL()
6227c9966a5SAndreas Gohr    {
6237c9966a5SAndreas Gohr        return $this->getRemoteTag('thumbnailurl');
6247c9966a5SAndreas Gohr    }
6257c9966a5SAndreas Gohr
6267c9966a5SAndreas Gohr    /**
6277c9966a5SAndreas Gohr     * Get the download URL of the extension if there is any
6287c9966a5SAndreas Gohr     *
6297c9966a5SAndreas Gohr     * @return string
6307c9966a5SAndreas Gohr     */
6317c9966a5SAndreas Gohr    public function getDownloadURL()
6327c9966a5SAndreas Gohr    {
6337c9966a5SAndreas Gohr        return $this->getRemoteTag('downloadurl');
6347c9966a5SAndreas Gohr    }
6357c9966a5SAndreas Gohr
6367c9966a5SAndreas Gohr    /**
6377c9966a5SAndreas Gohr     * Get the bug tracker URL of the extension if there is any
6387c9966a5SAndreas Gohr     *
6397c9966a5SAndreas Gohr     * @return string
6407c9966a5SAndreas Gohr     */
6417c9966a5SAndreas Gohr    public function getBugtrackerURL()
6427c9966a5SAndreas Gohr    {
6437c9966a5SAndreas Gohr        return $this->getRemoteTag('bugtracker');
6447c9966a5SAndreas Gohr    }
6457c9966a5SAndreas Gohr
6467c9966a5SAndreas Gohr    /**
6477c9966a5SAndreas Gohr     * Get the URL of the source repository if there is any
6487c9966a5SAndreas Gohr     *
6497c9966a5SAndreas Gohr     * @return string
6507c9966a5SAndreas Gohr     */
6517c9966a5SAndreas Gohr    public function getSourcerepoURL()
6527c9966a5SAndreas Gohr    {
6537c9966a5SAndreas Gohr        return $this->getRemoteTag('sourcerepo');
6547c9966a5SAndreas Gohr    }
6557c9966a5SAndreas Gohr
6567c9966a5SAndreas Gohr    /**
6577c9966a5SAndreas Gohr     * Get the donation URL of the extension if there is any
6587c9966a5SAndreas Gohr     *
6597c9966a5SAndreas Gohr     * @return string
6607c9966a5SAndreas Gohr     */
6617c9966a5SAndreas Gohr    public function getDonationURL()
6627c9966a5SAndreas Gohr    {
6637c9966a5SAndreas Gohr        return $this->getRemoteTag('donationurl');
6647c9966a5SAndreas Gohr    }
6657c9966a5SAndreas Gohr
6664fd6a1d7SAndreas Gohr    /**
6674fd6a1d7SAndreas Gohr     * Get a list of extensions that are similar to this one
6684fd6a1d7SAndreas Gohr     *
6694fd6a1d7SAndreas Gohr     * @return string[]
6704fd6a1d7SAndreas Gohr     */
6714fd6a1d7SAndreas Gohr    public function getSimilarList()
6724fd6a1d7SAndreas Gohr    {
6734fd6a1d7SAndreas Gohr        return $this->getRemoteTag('similar', []);
6744fd6a1d7SAndreas Gohr    }
6754fd6a1d7SAndreas Gohr
6764fd6a1d7SAndreas Gohr    /**
6774fd6a1d7SAndreas Gohr     * Get a list of extensions that are marked as conflicting with this one
6784fd6a1d7SAndreas Gohr     *
6794fd6a1d7SAndreas Gohr     * @return string[]
6804fd6a1d7SAndreas Gohr     */
6814fd6a1d7SAndreas Gohr    public function getConflictList()
6824fd6a1d7SAndreas Gohr    {
6834fd6a1d7SAndreas Gohr        return $this->getRemoteTag('conflicts', []);
6844fd6a1d7SAndreas Gohr    }
6854fd6a1d7SAndreas Gohr
6864fd6a1d7SAndreas Gohr    /**
6874fd6a1d7SAndreas Gohr     * Get a list of DokuWiki versions this plugin is marked as compatible with
6884fd6a1d7SAndreas Gohr     *
6894fd6a1d7SAndreas Gohr     * @return string[][] date -> version
6904fd6a1d7SAndreas Gohr     */
6914fd6a1d7SAndreas Gohr    public function getCompatibleVersions()
6924fd6a1d7SAndreas Gohr    {
6934fd6a1d7SAndreas Gohr        return $this->getRemoteTag('compatible', []);
6944fd6a1d7SAndreas Gohr    }
6954fd6a1d7SAndreas Gohr
6967c9966a5SAndreas Gohr    // endregion
6977c9966a5SAndreas Gohr
698cf2dcf1bSAndreas Gohr    // region Actions
699cf2dcf1bSAndreas Gohr
700cf2dcf1bSAndreas Gohr    /**
701cf2dcf1bSAndreas Gohr     * Install or update the extension
702cf2dcf1bSAndreas Gohr     *
703cf2dcf1bSAndreas Gohr     * @throws Exception
704cf2dcf1bSAndreas Gohr     */
705cf2dcf1bSAndreas Gohr    public function installOrUpdate()
706cf2dcf1bSAndreas Gohr    {
707cf2dcf1bSAndreas Gohr        $installer = new Installer(true);
708160d3688SAndreas Gohr        $installer->installExtension($this);
709cf2dcf1bSAndreas Gohr    }
710cf2dcf1bSAndreas Gohr
711cf2dcf1bSAndreas Gohr    /**
712cf2dcf1bSAndreas Gohr     * Uninstall the extension
713cf2dcf1bSAndreas Gohr     * @throws Exception
714cf2dcf1bSAndreas Gohr     */
715cf2dcf1bSAndreas Gohr    public function uninstall()
716cf2dcf1bSAndreas Gohr    {
717cf2dcf1bSAndreas Gohr        $installer = new Installer(true);
718cf2dcf1bSAndreas Gohr        $installer->uninstall($this);
719cf2dcf1bSAndreas Gohr    }
720cf2dcf1bSAndreas Gohr
721cf2dcf1bSAndreas Gohr    /**
7225732c960SAndreas Gohr     * Toggle the extension between enabled and disabled
7235732c960SAndreas Gohr     * @return void
7245732c960SAndreas Gohr     * @throws Exception
7255732c960SAndreas Gohr     */
7265732c960SAndreas Gohr    public function toggle()
7275732c960SAndreas Gohr    {
7285732c960SAndreas Gohr        if ($this->isEnabled()) {
7295732c960SAndreas Gohr            $this->disable();
7305732c960SAndreas Gohr        } else {
7315732c960SAndreas Gohr            $this->enable();
7325732c960SAndreas Gohr        }
7335732c960SAndreas Gohr    }
7345732c960SAndreas Gohr
7355732c960SAndreas Gohr    /**
736cf2dcf1bSAndreas Gohr     * Enable the extension
737b69d74f1SAndreas Gohr     *
738cf2dcf1bSAndreas Gohr     * @throws Exception
739cf2dcf1bSAndreas Gohr     */
740cf2dcf1bSAndreas Gohr    public function enable()
741cf2dcf1bSAndreas Gohr    {
742b69d74f1SAndreas Gohr        (new Installer())->enable($this);
743cf2dcf1bSAndreas Gohr    }
744cf2dcf1bSAndreas Gohr
745cf2dcf1bSAndreas Gohr    /**
746cf2dcf1bSAndreas Gohr     * Disable the extension
747b69d74f1SAndreas Gohr     *
748cf2dcf1bSAndreas Gohr     * @throws Exception
749cf2dcf1bSAndreas Gohr     */
750cf2dcf1bSAndreas Gohr    public function disable()
751cf2dcf1bSAndreas Gohr    {
752b69d74f1SAndreas Gohr        (new Installer())->disable($this);
753cf2dcf1bSAndreas Gohr    }
754cf2dcf1bSAndreas Gohr
755cf2dcf1bSAndreas Gohr    // endregion
756cf2dcf1bSAndreas Gohr
757cf2dcf1bSAndreas Gohr    // region Meta Data Management
758cf2dcf1bSAndreas Gohr
759cf2dcf1bSAndreas Gohr    /**
7607c9966a5SAndreas Gohr     * Access the Manager for this extension
761cf2dcf1bSAndreas Gohr     *
7627c9966a5SAndreas Gohr     * @return Manager
763cf2dcf1bSAndreas Gohr     */
7647c9966a5SAndreas Gohr    public function getManager()
765cf2dcf1bSAndreas Gohr    {
7667c184cfcSAndreas Gohr        if (!$this->manager instanceof Manager) {
7677c9966a5SAndreas Gohr            $this->manager = new Manager($this);
768cf2dcf1bSAndreas Gohr        }
7697c9966a5SAndreas Gohr        return $this->manager;
770cf2dcf1bSAndreas Gohr    }
771cf2dcf1bSAndreas Gohr
772cf2dcf1bSAndreas Gohr    /**
773cf2dcf1bSAndreas Gohr     * Reads the info file of the extension if available and fills the localInfo array
774cf2dcf1bSAndreas Gohr     */
775cf2dcf1bSAndreas Gohr    protected function readLocalInfo()
776cf2dcf1bSAndreas Gohr    {
777a1e045f7SAndreas Gohr        if (!$this->getCurrentDir()) return;
778cf2dcf1bSAndreas Gohr        $file = $this->currentDir . '/' . $this->type . '.info.txt';
779cf2dcf1bSAndreas Gohr        if (!is_readable($file)) return;
780cf2dcf1bSAndreas Gohr        $this->localInfo = confToHash($file, true);
781cf2dcf1bSAndreas Gohr        $this->localInfo = array_filter($this->localInfo); // remove all falsy keys
782cf2dcf1bSAndreas Gohr    }
783cf2dcf1bSAndreas Gohr
784cf2dcf1bSAndreas Gohr    /**
785*a1ef4d62SAndreas Gohr     * Try to determine the extension's base name from a plugin class name
786*a1ef4d62SAndreas Gohr     *
787*a1ef4d62SAndreas Gohr     * We use this as a fallback for old plugins without an info file
788*a1ef4d62SAndreas Gohr     *
789*a1ef4d62SAndreas Gohr     * @param string $dir The directory where the extension is located
790*a1ef4d62SAndreas Gohr     * @return string
791*a1ef4d62SAndreas Gohr     */
792*a1ef4d62SAndreas Gohr    protected function getBaseFromClass($dir)
793*a1ef4d62SAndreas Gohr    {
794*a1ef4d62SAndreas Gohr        foreach (Extension::COMPONENT_FILES as $type) {
795*a1ef4d62SAndreas Gohr            $file = $dir . '/' . $type . '.php';
796*a1ef4d62SAndreas Gohr            if (!is_readable($file)) continue;
797*a1ef4d62SAndreas Gohr            $class = $this->getClassNameFromFile($file);
798*a1ef4d62SAndreas Gohr            if ($class === null) continue;
799*a1ef4d62SAndreas Gohr            if (preg_match('/' . $type . '_plugin_(\w+)/', $class, $matches)) {
800*a1ef4d62SAndreas Gohr                return $matches[1];
801*a1ef4d62SAndreas Gohr            }
802*a1ef4d62SAndreas Gohr        }
803*a1ef4d62SAndreas Gohr        return '';
804*a1ef4d62SAndreas Gohr    }
805*a1ef4d62SAndreas Gohr
806*a1ef4d62SAndreas Gohr    /**
807cf2dcf1bSAndreas Gohr     * Fetches the remote info from the repository
808cf2dcf1bSAndreas Gohr     *
809cf2dcf1bSAndreas Gohr     * This ignores any errors coming from the repository and just sets the remoteInfo to an empty array in that case
810cf2dcf1bSAndreas Gohr     */
811cf2dcf1bSAndreas Gohr    protected function loadRemoteInfo()
812cf2dcf1bSAndreas Gohr    {
813cf2dcf1bSAndreas Gohr        if ($this->remoteInfo) return;
814cf2dcf1bSAndreas Gohr        $remote = Repository::getInstance();
815cf2dcf1bSAndreas Gohr        try {
816cf2dcf1bSAndreas Gohr            $this->remoteInfo = (array)$remote->getExtensionData($this->getId());
817cf2dcf1bSAndreas Gohr        } catch (Exception $e) {
818cf2dcf1bSAndreas Gohr            $this->remoteInfo = [];
819cf2dcf1bSAndreas Gohr        }
820cf2dcf1bSAndreas Gohr    }
821cf2dcf1bSAndreas Gohr
822cf2dcf1bSAndreas Gohr    /**
823cf2dcf1bSAndreas Gohr     * Read information from either local or remote info
824cf2dcf1bSAndreas Gohr     *
8257c9966a5SAndreas Gohr     * Always prefers local info over remote info. Giving multiple keys is useful when the
8267c9966a5SAndreas Gohr     * key has been renamed in the past or if local and remote keys might differ.
827cf2dcf1bSAndreas Gohr     *
828cf2dcf1bSAndreas Gohr     * @param string|string[] $tag one or multiple keys to check
829cf2dcf1bSAndreas Gohr     * @param mixed $default
830cf2dcf1bSAndreas Gohr     * @return mixed
831cf2dcf1bSAndreas Gohr     */
832cf2dcf1bSAndreas Gohr    protected function getTag($tag, $default = '')
833cf2dcf1bSAndreas Gohr    {
834cf2dcf1bSAndreas Gohr        foreach ((array)$tag as $t) {
835cf2dcf1bSAndreas Gohr            if (isset($this->localInfo[$t])) return $this->localInfo[$t];
836cf2dcf1bSAndreas Gohr        }
8377c9966a5SAndreas Gohr
8387c9966a5SAndreas Gohr        return $this->getRemoteTag($tag, $default);
8397c9966a5SAndreas Gohr    }
8407c9966a5SAndreas Gohr
8417c9966a5SAndreas Gohr    /**
8427c9966a5SAndreas Gohr     * Read information from remote info
8437c9966a5SAndreas Gohr     *
8447c9966a5SAndreas Gohr     * @param string|string[] $tag one or mutiple keys to check
8457c9966a5SAndreas Gohr     * @param mixed $default
8467c9966a5SAndreas Gohr     * @return mixed
8477c9966a5SAndreas Gohr     */
8487c9966a5SAndreas Gohr    protected function getRemoteTag($tag, $default = '')
8497c9966a5SAndreas Gohr    {
850cf2dcf1bSAndreas Gohr        $this->loadRemoteInfo();
851cf2dcf1bSAndreas Gohr        foreach ((array)$tag as $t) {
852cf2dcf1bSAndreas Gohr            if (isset($this->remoteInfo[$t])) return $this->remoteInfo[$t];
853cf2dcf1bSAndreas Gohr        }
854cf2dcf1bSAndreas Gohr        return $default;
855cf2dcf1bSAndreas Gohr    }
856cf2dcf1bSAndreas Gohr
857cf2dcf1bSAndreas Gohr    // endregion
858a1e045f7SAndreas Gohr
859a1e045f7SAndreas Gohr    // region utilities
860a1e045f7SAndreas Gohr
861a1e045f7SAndreas Gohr    /**
862a1e045f7SAndreas Gohr     * Convert an extension id to a type and base
863a1e045f7SAndreas Gohr     *
864a1e045f7SAndreas Gohr     * @param string $id
865a1e045f7SAndreas Gohr     * @return array [type, base]
866a1e045f7SAndreas Gohr     */
867a1e045f7SAndreas Gohr    protected function idToTypeBase($id)
868a1e045f7SAndreas Gohr    {
869a1e045f7SAndreas Gohr        [$type, $base] = sexplode(':', $id, 2);
870a1e045f7SAndreas Gohr        if ($base === null) {
871a1e045f7SAndreas Gohr            $base = $type;
872a1e045f7SAndreas Gohr            $type = self::TYPE_PLUGIN;
873a1e045f7SAndreas Gohr        } elseif ($type === self::TYPE_TEMPLATE) {
874a1e045f7SAndreas Gohr            $type = self::TYPE_TEMPLATE;
875a1e045f7SAndreas Gohr        } else {
876a1e045f7SAndreas Gohr            throw new RuntimeException('Invalid extension id: ' . $id);
877a1e045f7SAndreas Gohr        }
878a1e045f7SAndreas Gohr
879a1e045f7SAndreas Gohr        return [$type, $base];
880a1e045f7SAndreas Gohr    }
881b69d74f1SAndreas Gohr
8827c9966a5SAndreas Gohr    /**
883*a1ef4d62SAndreas Gohr     * Extract the class name from a file
884*a1ef4d62SAndreas Gohr     *
885*a1ef4d62SAndreas Gohr     * @param string $filePath
886*a1ef4d62SAndreas Gohr     * @return string|null
887*a1ef4d62SAndreas Gohr     */
888*a1ef4d62SAndreas Gohr    protected function getClassNameFromFile($filePath)
889*a1ef4d62SAndreas Gohr    {
890*a1ef4d62SAndreas Gohr        $code = file_get_contents($filePath);
891*a1ef4d62SAndreas Gohr        $tokens = token_get_all($code);
892*a1ef4d62SAndreas Gohr
893*a1ef4d62SAndreas Gohr        for ($i = 0, $count = count($tokens); $i < $count; $i++) {
894*a1ef4d62SAndreas Gohr            if (is_array($tokens[$i]) && $tokens[$i][0] === T_CLASS) {
895*a1ef4d62SAndreas Gohr                // Skip whitespace/comments after T_CLASS
896*a1ef4d62SAndreas Gohr                $j = $i + 1;
897*a1ef4d62SAndreas Gohr                while (
898*a1ef4d62SAndreas Gohr                    isset($tokens[$j]) &&
899*a1ef4d62SAndreas Gohr                    is_array($tokens[$j]) &&
900*a1ef4d62SAndreas Gohr                    in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])
901*a1ef4d62SAndreas Gohr                ) {
902*a1ef4d62SAndreas Gohr                    $j++;
903*a1ef4d62SAndreas Gohr                }
904*a1ef4d62SAndreas Gohr
905*a1ef4d62SAndreas Gohr                // The next token should be the class name
906*a1ef4d62SAndreas Gohr                if (isset($tokens[$j]) && is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
907*a1ef4d62SAndreas Gohr                    return $tokens[$j][1]; // Return class name
908*a1ef4d62SAndreas Gohr                }
909*a1ef4d62SAndreas Gohr            }
910*a1ef4d62SAndreas Gohr        }
911*a1ef4d62SAndreas Gohr
912*a1ef4d62SAndreas Gohr        return null; // No class found
913*a1ef4d62SAndreas Gohr    }
914*a1ef4d62SAndreas Gohr
915*a1ef4d62SAndreas Gohr    /**
9167c9966a5SAndreas Gohr     * @return string
9177c9966a5SAndreas Gohr     */
9187c9966a5SAndreas Gohr    public function __toString()
9197c9966a5SAndreas Gohr    {
9207c9966a5SAndreas Gohr        return $this->getId();
9217c9966a5SAndreas Gohr    }
9227c9966a5SAndreas Gohr
923a1e045f7SAndreas Gohr    // endregion
924cf2dcf1bSAndreas Gohr}
925