xref: /dokuwiki/lib/plugins/extension/cli.php (revision 160d3688fe2b70ebe88d464cba3417440127a155)
1a8d2f3cbSAndreas Gohr<?php
2a8d2f3cbSAndreas Gohr
38553d24dSAndreas Gohruse dokuwiki\Extension\CLIPlugin;
47c9966a5SAndreas Gohruse dokuwiki\plugin\extension\Exception as ExtensionException;
57c9966a5SAndreas Gohruse dokuwiki\plugin\extension\Extension;
625d28a01SAndreas Gohruse dokuwiki\plugin\extension\Installer;
77c9966a5SAndreas Gohruse dokuwiki\plugin\extension\Local;
87c9966a5SAndreas Gohruse dokuwiki\plugin\extension\Repository;
97c9966a5SAndreas Gohruse splitbrain\phpcli\Colors;
107c9966a5SAndreas Gohruse splitbrain\phpcli\Exception;
11fe2dcfd5SAndreas Gohruse splitbrain\phpcli\Options;
12fe2dcfd5SAndreas Gohruse splitbrain\phpcli\TableFormatter;
13a8d2f3cbSAndreas Gohr
14a8d2f3cbSAndreas Gohr/**
15a8d2f3cbSAndreas Gohr * Class cli_plugin_extension
16a8d2f3cbSAndreas Gohr *
17a8d2f3cbSAndreas Gohr * Command Line component for the extension manager
18a8d2f3cbSAndreas Gohr *
19a8d2f3cbSAndreas Gohr * @license GPL2
20a8d2f3cbSAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org>
21a8d2f3cbSAndreas Gohr */
228553d24dSAndreas Gohrclass cli_plugin_extension extends CLIPlugin
23a8d2f3cbSAndreas Gohr{
24a8d2f3cbSAndreas Gohr    /** @inheritdoc */
25fe2dcfd5SAndreas Gohr    protected function setup(Options $options)
26a8d2f3cbSAndreas Gohr    {
27a8d2f3cbSAndreas Gohr        // general setup
287c9966a5SAndreas Gohr        $options->useCompactHelp();
29b9daa2f5SAndreas Gohr        $options->setHelp(
30b9daa2f5SAndreas Gohr            "Manage plugins and templates for this DokuWiki instance\n\n" .
31b9daa2f5SAndreas Gohr            "Status codes:\n" .
32*160d3688SAndreas Gohr            "   i - installed                    ☠ - security issue\n" .
33*160d3688SAndreas Gohr            "   b - bundled with DokuWiki        ⚠ - security warning\n" .
34*160d3688SAndreas Gohr            "   g - installed via git            ↯ - update message\n" .
35*160d3688SAndreas Gohr            "   d - disabled                     ⮎ - URL changed\n" .
36*160d3688SAndreas Gohr            "   u - update available\n"
37b9daa2f5SAndreas Gohr        );
38a8d2f3cbSAndreas Gohr
39a8d2f3cbSAndreas Gohr        // search
40a8d2f3cbSAndreas Gohr        $options->registerCommand('search', 'Search for an extension');
41a8d2f3cbSAndreas Gohr        $options->registerOption('max', 'Maximum number of results (default 10)', 'm', 'number', 'search');
42a8d2f3cbSAndreas Gohr        $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'search');
43a8d2f3cbSAndreas Gohr        $options->registerArgument('query', 'The keyword(s) to search for', true, 'search');
44a8d2f3cbSAndreas Gohr
45a8d2f3cbSAndreas Gohr        // list
46a8d2f3cbSAndreas Gohr        $options->registerCommand('list', 'List installed extensions');
47a8d2f3cbSAndreas Gohr        $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'list');
48b9daa2f5SAndreas Gohr        $options->registerOption('filter', 'Filter by this status', 'f', 'status', 'list');
49a8d2f3cbSAndreas Gohr
50a8d2f3cbSAndreas Gohr        // upgrade
51a8d2f3cbSAndreas Gohr        $options->registerCommand('upgrade', 'Update all installed extensions to their latest versions');
52a8d2f3cbSAndreas Gohr
53a8d2f3cbSAndreas Gohr        // install
54a8d2f3cbSAndreas Gohr        $options->registerCommand('install', 'Install or upgrade extensions');
55dccd6b2bSAndreas Gohr        $options->registerArgument(
56dccd6b2bSAndreas Gohr            'extensions...',
57dccd6b2bSAndreas Gohr            'One or more extensions to install. Either by name or download URL',
58dccd6b2bSAndreas Gohr            true,
59dccd6b2bSAndreas Gohr            'install'
60e2170488SAndreas Gohr        );
61a8d2f3cbSAndreas Gohr
62a8d2f3cbSAndreas Gohr        // uninstall
63a8d2f3cbSAndreas Gohr        $options->registerCommand('uninstall', 'Uninstall a new extension');
64a8d2f3cbSAndreas Gohr        $options->registerArgument('extensions...', 'One or more extensions to install', true, 'uninstall');
65a8d2f3cbSAndreas Gohr
66a8d2f3cbSAndreas Gohr        // enable
67a8d2f3cbSAndreas Gohr        $options->registerCommand('enable', 'Enable installed extensions');
68a8d2f3cbSAndreas Gohr        $options->registerArgument('extensions...', 'One or more extensions to enable', true, 'enable');
69a8d2f3cbSAndreas Gohr
70a8d2f3cbSAndreas Gohr        // disable
71a8d2f3cbSAndreas Gohr        $options->registerCommand('disable', 'Disable installed extensions');
72a8d2f3cbSAndreas Gohr        $options->registerArgument('extensions...', 'One or more extensions to disable', true, 'disable');
73a8d2f3cbSAndreas Gohr    }
74a8d2f3cbSAndreas Gohr
75a8d2f3cbSAndreas Gohr    /** @inheritdoc */
76fe2dcfd5SAndreas Gohr    protected function main(Options $options)
77a8d2f3cbSAndreas Gohr    {
787c9966a5SAndreas Gohr        $repo = Repository::getInstance();
797c9966a5SAndreas Gohr        try {
807c9966a5SAndreas Gohr            $repo->checkAccess();
817c9966a5SAndreas Gohr        } catch (ExtensionException $e) {
82ed3520eeSAndreas Gohr            $this->warning('Extension Repository API is not accessible, no remote info available!');
83ed3520eeSAndreas Gohr        }
84ed3520eeSAndreas Gohr
85a8d2f3cbSAndreas Gohr        switch ($options->getCmd()) {
86a8d2f3cbSAndreas Gohr            case 'list':
87b9daa2f5SAndreas Gohr                $ret = $this->cmdList($options->getOpt('verbose'), $options->getOpt('filter', ''));
88a8d2f3cbSAndreas Gohr                break;
89a8d2f3cbSAndreas Gohr            case 'search':
90a8d2f3cbSAndreas Gohr                $ret = $this->cmdSearch(
91a8d2f3cbSAndreas Gohr                    implode(' ', $options->getArgs()),
92a8d2f3cbSAndreas Gohr                    $options->getOpt('verbose'),
93a8d2f3cbSAndreas Gohr                    (int)$options->getOpt('max', 10)
94a8d2f3cbSAndreas Gohr                );
95a8d2f3cbSAndreas Gohr                break;
96a8d2f3cbSAndreas Gohr            case 'install':
97a8d2f3cbSAndreas Gohr                $ret = $this->cmdInstall($options->getArgs());
98a8d2f3cbSAndreas Gohr                break;
99a8d2f3cbSAndreas Gohr            case 'uninstall':
100a8d2f3cbSAndreas Gohr                $ret = $this->cmdUnInstall($options->getArgs());
101a8d2f3cbSAndreas Gohr                break;
102a8d2f3cbSAndreas Gohr            case 'enable':
103a8d2f3cbSAndreas Gohr                $ret = $this->cmdEnable(true, $options->getArgs());
104a8d2f3cbSAndreas Gohr                break;
105a8d2f3cbSAndreas Gohr            case 'disable':
106a8d2f3cbSAndreas Gohr                $ret = $this->cmdEnable(false, $options->getArgs());
107a8d2f3cbSAndreas Gohr                break;
108a8d2f3cbSAndreas Gohr            case 'upgrade':
109a8d2f3cbSAndreas Gohr                $ret = $this->cmdUpgrade();
110a8d2f3cbSAndreas Gohr                break;
111a8d2f3cbSAndreas Gohr            default:
112a8d2f3cbSAndreas Gohr                echo $options->help();
113a8d2f3cbSAndreas Gohr                $ret = 0;
114a8d2f3cbSAndreas Gohr        }
115a8d2f3cbSAndreas Gohr
116a8d2f3cbSAndreas Gohr        exit($ret);
117a8d2f3cbSAndreas Gohr    }
118a8d2f3cbSAndreas Gohr
119a8d2f3cbSAndreas Gohr    /**
120a8d2f3cbSAndreas Gohr     * Upgrade all extensions
121a8d2f3cbSAndreas Gohr     *
122a8d2f3cbSAndreas Gohr     * @return int
123a8d2f3cbSAndreas Gohr     */
124a8d2f3cbSAndreas Gohr    protected function cmdUpgrade()
125a8d2f3cbSAndreas Gohr    {
126*160d3688SAndreas Gohr        $local = new Local();
127*160d3688SAndreas Gohr        $extensions = [];
128*160d3688SAndreas Gohr        foreach ($local->getExtensions() as $ext) {
129*160d3688SAndreas Gohr            if($ext->updateAvailable()) $extensions[] = $ext->getID();
130a8d2f3cbSAndreas Gohr        }
131*160d3688SAndreas Gohr        return $this->cmdInstall($extensions);
132a8d2f3cbSAndreas Gohr    }
133a8d2f3cbSAndreas Gohr
134a8d2f3cbSAndreas Gohr    /**
135a8d2f3cbSAndreas Gohr     * Enable or disable one or more extensions
136a8d2f3cbSAndreas Gohr     *
137a8d2f3cbSAndreas Gohr     * @param bool $set
138a8d2f3cbSAndreas Gohr     * @param string[] $extensions
139a8d2f3cbSAndreas Gohr     * @return int
140a8d2f3cbSAndreas Gohr     */
141a8d2f3cbSAndreas Gohr    protected function cmdEnable($set, $extensions)
142a8d2f3cbSAndreas Gohr    {
143a8d2f3cbSAndreas Gohr        $ok = 0;
144a8d2f3cbSAndreas Gohr        foreach ($extensions as $extname) {
145*160d3688SAndreas Gohr            $ext = Extension::createFromId($extname);
146a8d2f3cbSAndreas Gohr
147*160d3688SAndreas Gohr            try {
148a8d2f3cbSAndreas Gohr                if ($set) {
149*160d3688SAndreas Gohr                    $ext->enable();
150a8d2f3cbSAndreas Gohr                    $msg = 'msg_enabled';
151a8d2f3cbSAndreas Gohr                } else {
152*160d3688SAndreas Gohr                    $ext->disable();
153a8d2f3cbSAndreas Gohr                    $msg = 'msg_disabled';
154a8d2f3cbSAndreas Gohr                }
155*160d3688SAndreas Gohr                $this->success(sprintf($this->getLang($msg), $ext->getID()));
156*160d3688SAndreas Gohr            } catch (ExtensionException $e) {
157*160d3688SAndreas Gohr                $this->error($e->getMessage());
158fe2dcfd5SAndreas Gohr                ++$ok;
159a8d2f3cbSAndreas Gohr                continue;
160a8d2f3cbSAndreas Gohr            }
161a8d2f3cbSAndreas Gohr        }
162a8d2f3cbSAndreas Gohr
163a8d2f3cbSAndreas Gohr        return $ok;
164a8d2f3cbSAndreas Gohr    }
165a8d2f3cbSAndreas Gohr
166a8d2f3cbSAndreas Gohr    /**
167a8d2f3cbSAndreas Gohr     * Uninstall one or more extensions
168a8d2f3cbSAndreas Gohr     *
169a8d2f3cbSAndreas Gohr     * @param string[] $extensions
170a8d2f3cbSAndreas Gohr     * @return int
171a8d2f3cbSAndreas Gohr     */
172a8d2f3cbSAndreas Gohr    protected function cmdUnInstall($extensions)
173a8d2f3cbSAndreas Gohr    {
174*160d3688SAndreas Gohr        $installer = new Installer();
175a8d2f3cbSAndreas Gohr
176a8d2f3cbSAndreas Gohr        $ok = 0;
177a8d2f3cbSAndreas Gohr        foreach ($extensions as $extname) {
178*160d3688SAndreas Gohr            $ext = Extension::createFromId($extname);
179a8d2f3cbSAndreas Gohr
180*160d3688SAndreas Gohr            try {
181*160d3688SAndreas Gohr                $installer->uninstall($ext);
182a8d2f3cbSAndreas Gohr                $this->success(sprintf($this->getLang('msg_delete_success'), $ext->getID()));
183*160d3688SAndreas Gohr            } catch (ExtensionException $e) {
184*160d3688SAndreas Gohr                $this->debug($e->getTraceAsString());
185*160d3688SAndreas Gohr                $this->error($e->getMessage());
186*160d3688SAndreas Gohr                $ok++; // error code is number of failed uninstalls
187a8d2f3cbSAndreas Gohr            }
188a8d2f3cbSAndreas Gohr        }
189a8d2f3cbSAndreas Gohr        return $ok;
190a8d2f3cbSAndreas Gohr    }
191a8d2f3cbSAndreas Gohr
192a8d2f3cbSAndreas Gohr    /**
193a8d2f3cbSAndreas Gohr     * Install one or more extensions
194a8d2f3cbSAndreas Gohr     *
195a8d2f3cbSAndreas Gohr     * @param string[] $extensions
196a8d2f3cbSAndreas Gohr     * @return int
197a8d2f3cbSAndreas Gohr     */
198a8d2f3cbSAndreas Gohr    protected function cmdInstall($extensions)
199a8d2f3cbSAndreas Gohr    {
200a8d2f3cbSAndreas Gohr        $ok = 0;
201a8d2f3cbSAndreas Gohr        foreach ($extensions as $extname) {
202*160d3688SAndreas Gohr            $installer = new Installer(true);
203*160d3688SAndreas Gohr
20425d28a01SAndreas Gohr            try {
205cc16762dSLocness                if (preg_match("/^https?:\/\//i", $extname)) {
20625d28a01SAndreas Gohr                    $installer->installFromURL($extname, true);
207cc16762dSLocness                } else {
20825d28a01SAndreas Gohr                    $installer->installFromId($extname);
209a8d2f3cbSAndreas Gohr                }
21025d28a01SAndreas Gohr            } catch (ExtensionException $e) {
21125d28a01SAndreas Gohr                $this->debug($e->getTraceAsString());
2125aaea2b0SLocness                $this->error($e->getMessage());
21325d28a01SAndreas Gohr                $ok++; // error code is number of failed installs
2145aaea2b0SLocness            }
2155aaea2b0SLocness
21625d28a01SAndreas Gohr            $processed = $installer->getProcessed();
21725d28a01SAndreas Gohr            foreach($processed as $id => $status){
21825d28a01SAndreas Gohr                if($status == Installer::STATUS_INSTALLED) {
21925d28a01SAndreas Gohr                    $this->success(sprintf($this->getLang('msg_install_success'), $id));
22025d28a01SAndreas Gohr                } else if($status == Installer::STATUS_UPDATED) {
22125d28a01SAndreas Gohr                    $this->success(sprintf($this->getLang('msg_update_success'), $id));
222a8d2f3cbSAndreas Gohr                }
223cc16762dSLocness            }
224*160d3688SAndreas Gohr        }
22525d28a01SAndreas Gohr
226a8d2f3cbSAndreas Gohr        return $ok;
227a8d2f3cbSAndreas Gohr    }
228a8d2f3cbSAndreas Gohr
229a8d2f3cbSAndreas Gohr    /**
230a8d2f3cbSAndreas Gohr     * Search for an extension
231a8d2f3cbSAndreas Gohr     *
232a8d2f3cbSAndreas Gohr     * @param string $query
233a8d2f3cbSAndreas Gohr     * @param bool $showdetails
234a8d2f3cbSAndreas Gohr     * @param int $max
235a8d2f3cbSAndreas Gohr     * @return int
2369b36c1fcSsplitbrain     * @throws Exception
237a8d2f3cbSAndreas Gohr     */
238a8d2f3cbSAndreas Gohr    protected function cmdSearch($query, $showdetails, $max)
239a8d2f3cbSAndreas Gohr    {
2407c9966a5SAndreas Gohr        $repo = Repository::getInstance();
2417c9966a5SAndreas Gohr        $result = $repo->searchExtensions($query);
242a8d2f3cbSAndreas Gohr        if ($max) {
243a8d2f3cbSAndreas Gohr            $result = array_slice($result, 0, $max);
244a8d2f3cbSAndreas Gohr        }
245a8d2f3cbSAndreas Gohr
246a8d2f3cbSAndreas Gohr        $this->listExtensions($result, $showdetails);
247a8d2f3cbSAndreas Gohr        return 0;
248a8d2f3cbSAndreas Gohr    }
249a8d2f3cbSAndreas Gohr
250a8d2f3cbSAndreas Gohr    /**
251a8d2f3cbSAndreas Gohr     * @param bool $showdetails
252b9daa2f5SAndreas Gohr     * @param string $filter
253a8d2f3cbSAndreas Gohr     * @return int
2549b36c1fcSsplitbrain     * @throws Exception
255a8d2f3cbSAndreas Gohr     */
256b9daa2f5SAndreas Gohr    protected function cmdList($showdetails, $filter)
257a8d2f3cbSAndreas Gohr    {
2587c9966a5SAndreas Gohr        $this->listExtensions((new Local())->getExtensions(), $showdetails, $filter);
259a8d2f3cbSAndreas Gohr        return 0;
260a8d2f3cbSAndreas Gohr    }
261a8d2f3cbSAndreas Gohr
262a8d2f3cbSAndreas Gohr    /**
263a8d2f3cbSAndreas Gohr     * List the given extensions
264a8d2f3cbSAndreas Gohr     *
2657c9966a5SAndreas Gohr     * @param Extension[] $list
266a8d2f3cbSAndreas Gohr     * @param bool $details display details
267b9daa2f5SAndreas Gohr     * @param string $filter filter for this status
2689b36c1fcSsplitbrain     * @throws Exception
269*160d3688SAndreas Gohr     * @todo break into smaller methods
270a8d2f3cbSAndreas Gohr     */
271b9daa2f5SAndreas Gohr    protected function listExtensions($list, $details, $filter = '')
272a8d2f3cbSAndreas Gohr    {
273fe2dcfd5SAndreas Gohr        $tr = new TableFormatter($this->colors);
2747c9966a5SAndreas Gohr        foreach ($list as $ext) {
275a8d2f3cbSAndreas Gohr
276a8d2f3cbSAndreas Gohr            $status = '';
277a8d2f3cbSAndreas Gohr            if ($ext->isInstalled()) {
278a8d2f3cbSAndreas Gohr                $date = $ext->getInstalledVersion();
279a8d2f3cbSAndreas Gohr                $avail = $ext->getLastUpdate();
280a8d2f3cbSAndreas Gohr                $status = 'i';
281a8d2f3cbSAndreas Gohr                if ($avail && $avail > $date) {
282e5688dc7SAndreas Gohr                    $vcolor = Colors::C_RED;
283b9daa2f5SAndreas Gohr                    $status .= 'u';
284a8d2f3cbSAndreas Gohr                } else {
285e5688dc7SAndreas Gohr                    $vcolor = Colors::C_GREEN;
286a8d2f3cbSAndreas Gohr                }
287a8d2f3cbSAndreas Gohr                if ($ext->isGitControlled()) $status = 'g';
28899fd248dSAndreas Gohr                if ($ext->isBundled()) {
28999fd248dSAndreas Gohr                    $status = 'b';
29099fd248dSAndreas Gohr                    $date = '<bundled>';
29199fd248dSAndreas Gohr                    $vcolor = null;
29299fd248dSAndreas Gohr                }
293e5688dc7SAndreas Gohr                if ($ext->isEnabled()) {
294e5688dc7SAndreas Gohr                    $ecolor = Colors::C_BROWN;
295e5688dc7SAndreas Gohr                } else {
296e5688dc7SAndreas Gohr                    $ecolor = Colors::C_DARKGRAY;
297e5688dc7SAndreas Gohr                    $status .= 'd';
298e5688dc7SAndreas Gohr                }
299a8d2f3cbSAndreas Gohr            } else {
300d915fa09SAndreas Gohr                $ecolor = null;
301a8d2f3cbSAndreas Gohr                $date = $ext->getLastUpdate();
302e5688dc7SAndreas Gohr                $vcolor = null;
303a8d2f3cbSAndreas Gohr            }
304a8d2f3cbSAndreas Gohr
305b9daa2f5SAndreas Gohr            if ($filter && strpos($status, $filter) === false) {
306b9daa2f5SAndreas Gohr                continue;
307b9daa2f5SAndreas Gohr            }
308a8d2f3cbSAndreas Gohr
30999fd248dSAndreas Gohr
31099fd248dSAndreas Gohr            if ($ext->getSecurityIssue()) $status .= '☠';
31199fd248dSAndreas Gohr            if ($ext->getSecurityWarning()) $status .= '⚠';
312*160d3688SAndreas Gohr            if ($ext->getUpdateMessage()) $status .= '↯';
313*160d3688SAndreas Gohr            if ($ext->hasChangedURL()) $status .= '⮎';
31499fd248dSAndreas Gohr
315a8d2f3cbSAndreas Gohr            echo $tr->format(
316*160d3688SAndreas Gohr                [20, 5, 12, '*'],
317a8d2f3cbSAndreas Gohr                [
318a8d2f3cbSAndreas Gohr                    $ext->getID(),
319a8d2f3cbSAndreas Gohr                    $status,
320a8d2f3cbSAndreas Gohr                    $date,
321a8d2f3cbSAndreas Gohr                    strip_tags(sprintf(
322a8d2f3cbSAndreas Gohr                        $this->getLang('extensionby'),
323a8d2f3cbSAndreas Gohr                        $ext->getDisplayName(),
324dccd6b2bSAndreas Gohr                        $this->colors->wrap($ext->getAuthor(), Colors::C_PURPLE)
325dccd6b2bSAndreas Gohr                    ))
326a8d2f3cbSAndreas Gohr                ],
327a8d2f3cbSAndreas Gohr                [
328e5688dc7SAndreas Gohr                    $ecolor,
329a8d2f3cbSAndreas Gohr                    Colors::C_YELLOW,
330e5688dc7SAndreas Gohr                    $vcolor,
331a8d2f3cbSAndreas Gohr                    null,
332a8d2f3cbSAndreas Gohr                ]
333a8d2f3cbSAndreas Gohr            );
334a8d2f3cbSAndreas Gohr
33599fd248dSAndreas Gohr
336a8d2f3cbSAndreas Gohr            if (!$details) continue;
337a8d2f3cbSAndreas Gohr
338a8d2f3cbSAndreas Gohr            echo $tr->format(
339*160d3688SAndreas Gohr                [7, '*'],
340a8d2f3cbSAndreas Gohr                ['', $ext->getDescription()],
341a8d2f3cbSAndreas Gohr                [null, Colors::C_CYAN]
342a8d2f3cbSAndreas Gohr            );
34399fd248dSAndreas Gohr            if ($ext->getSecurityWarning()) {
34499fd248dSAndreas Gohr                echo $tr->format(
345*160d3688SAndreas Gohr                    [7, '*'],
34699fd248dSAndreas Gohr                    ['', '⚠ ' . $ext->getSecurityWarning()],
34799fd248dSAndreas Gohr                    [null, Colors::C_YELLOW]
34899fd248dSAndreas Gohr                );
34999fd248dSAndreas Gohr            }
35099fd248dSAndreas Gohr            if ($ext->getSecurityIssue()) {
35199fd248dSAndreas Gohr                echo $tr->format(
352*160d3688SAndreas Gohr                    [7, '*'],
35399fd248dSAndreas Gohr                    ['', '☠ ' . $ext->getSecurityIssue()],
35499fd248dSAndreas Gohr                    [null, Colors::C_LIGHTRED]
35599fd248dSAndreas Gohr                );
35699fd248dSAndreas Gohr            }
35799fd248dSAndreas Gohr            if ($ext->getUpdateMessage()) {
35899fd248dSAndreas Gohr                echo $tr->format(
359*160d3688SAndreas Gohr                    [7, '*'],
360*160d3688SAndreas Gohr                    ['', '↯ ' . $ext->getUpdateMessage()],
36199fd248dSAndreas Gohr                    [null, Colors::C_LIGHTBLUE]
36299fd248dSAndreas Gohr                );
36399fd248dSAndreas Gohr            }
364*160d3688SAndreas Gohr            if ($ext->hasChangedURL()) {
365*160d3688SAndreas Gohr                $msg = $this->getLang('url_change');
366*160d3688SAndreas Gohr                $msg = str_replace('<br>',"\n", $msg);
367*160d3688SAndreas Gohr                $msg = str_replace('<br/>',"\n", $msg);
368*160d3688SAndreas Gohr                $msg = str_replace('<br />',"\n", $msg);
369*160d3688SAndreas Gohr                $msg = strip_tags($msg);
370*160d3688SAndreas Gohr
371*160d3688SAndreas Gohr                echo $tr->format(
372*160d3688SAndreas Gohr                    [7, '*'],
373*160d3688SAndreas Gohr                    ['', '⮎ ' .  sprintf($msg, $ext->getDownloadURL(), $ext->getManager()->getDownloadUrl())],
374*160d3688SAndreas Gohr                    [null, Colors::C_BLUE]
375*160d3688SAndreas Gohr                );
376*160d3688SAndreas Gohr            }
377a8d2f3cbSAndreas Gohr        }
378a8d2f3cbSAndreas Gohr    }
379a8d2f3cbSAndreas Gohr}
380