xref: /plugin/dev/Skeletor.php (revision fe060d0d4c4c42403a614910f1d8cdcd8d360b96)
170316b84SAndreas Gohr<?php
270316b84SAndreas Gohr
370316b84SAndreas Gohrnamespace dokuwiki\plugin\dev;
470316b84SAndreas Gohr
570316b84SAndreas Gohruse RuntimeException;
670316b84SAndreas Gohr
770316b84SAndreas Gohr/**
870316b84SAndreas Gohr * This class holds basic information about a plugin or template and uses the skeleton files to
970316b84SAndreas Gohr * create new plugin or template specific versions of them.
1070316b84SAndreas Gohr *
1170316b84SAndreas Gohr * This class does not write any files, but only provides the data for the actual file creation.
1270316b84SAndreas Gohr */
1370316b84SAndreas Gohrclass Skeletor
1470316b84SAndreas Gohr{
1570316b84SAndreas Gohr    // FIXME this may change upstream we may want to update it via github action
16*fe060d0dSAndreas Gohr    public const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli'];
1770316b84SAndreas Gohr
18*fe060d0dSAndreas Gohr    public const TYPE_PLUGIN = 'plugin';
19*fe060d0dSAndreas Gohr    public const TYPE_TEMPLATE = 'template';
2070316b84SAndreas Gohr
2170316b84SAndreas Gohr    protected $type;
2270316b84SAndreas Gohr    protected $base;
2370316b84SAndreas Gohr    protected $author;
2470316b84SAndreas Gohr    protected $desc;
2570316b84SAndreas Gohr    protected $name;
2670316b84SAndreas Gohr    protected $email;
2770316b84SAndreas Gohr    protected $url;
2870316b84SAndreas Gohr    protected $dir;
2970316b84SAndreas Gohr
3070316b84SAndreas Gohr    /** @var array The files to be created in the form of [path => content] */
3170316b84SAndreas Gohr    protected $files = [];
3270316b84SAndreas Gohr
3370316b84SAndreas Gohr    /**
3470316b84SAndreas Gohr     * Initialize the skeletor from provided data
3570316b84SAndreas Gohr     *
3670316b84SAndreas Gohr     * @param string $type
3770316b84SAndreas Gohr     * @param string $base
3870316b84SAndreas Gohr     * @param string $desc
3970316b84SAndreas Gohr     * @param string $author
4070316b84SAndreas Gohr     * @param string $email
4170316b84SAndreas Gohr     * @param string $name
4270316b84SAndreas Gohr     * @param string $url
4370316b84SAndreas Gohr     */
4470316b84SAndreas Gohr    public function __construct($type, $base, $desc, $author, $email, $name = '', $url = '')
4570316b84SAndreas Gohr    {
4670316b84SAndreas Gohr        $this->type = $type;
4770316b84SAndreas Gohr        $this->base = $base;
4870316b84SAndreas Gohr        $this->desc = $desc;
4970316b84SAndreas Gohr        $this->author = $author;
5070316b84SAndreas Gohr        $this->email = $email;
5170316b84SAndreas Gohr        $this->name = $name ?: ucfirst($base . ' ' . $type);
5270316b84SAndreas Gohr
5370316b84SAndreas Gohr        if ($type == self::TYPE_PLUGIN) {
5470316b84SAndreas Gohr            $this->url = $url ?: 'https://www.dokuwiki.org/plugin:' . $base;
5570316b84SAndreas Gohr            $this->dir = 'lib/plugins/' . $base;
5670316b84SAndreas Gohr        } else {
5770316b84SAndreas Gohr            $this->url = $url ?: 'https://www.dokuwiki.org/template:' . $base;
5870316b84SAndreas Gohr            $this->dir = 'lib/tpl/' . $base;
5970316b84SAndreas Gohr        }
6070316b84SAndreas Gohr    }
6170316b84SAndreas Gohr
6270316b84SAndreas Gohr    /**
6370316b84SAndreas Gohr     * Create an instance using an existing plugin or template directory
6470316b84SAndreas Gohr     *
6570316b84SAndreas Gohr     * @param string $dir
6670316b84SAndreas Gohr     * @return Skeletor
6770316b84SAndreas Gohr     */
68*fe060d0dSAndreas Gohr    public static function fromDir($dir)
6970316b84SAndreas Gohr    {
7070316b84SAndreas Gohr        if (file_exists($dir . '/plugin.info.txt')) {
7170316b84SAndreas Gohr            $type = self::TYPE_PLUGIN;
7270316b84SAndreas Gohr        } elseif (file_exists($dir . '/template.info.txt')) {
7370316b84SAndreas Gohr            $type = self::TYPE_TEMPLATE;
7470316b84SAndreas Gohr        } else {
7570316b84SAndreas Gohr            throw new RuntimeException('Not a plugin or template directory');
7670316b84SAndreas Gohr        }
7770316b84SAndreas Gohr
7870316b84SAndreas Gohr        $data = file($dir . '/' . $type . '.info.txt', FILE_IGNORE_NEW_LINES);
79*fe060d0dSAndreas Gohr        $data = array_map(fn($item) => array_map(trim(...), sexplode(' ', $item, 2, '')), $data);
8070316b84SAndreas Gohr        $data = array_combine(array_column($data, 0), array_column($data, 1));
8170316b84SAndreas Gohr
8270316b84SAndreas Gohr        return new self($type, $data['base'], $data['desc'], $data['author'], $data['email'], $data['url']);
8370316b84SAndreas Gohr    }
8470316b84SAndreas Gohr
8570316b84SAndreas Gohr    /**
8670316b84SAndreas Gohr     * Return the files to be created
8770316b84SAndreas Gohr     *
8870316b84SAndreas Gohr     * @return array [path => content]
8970316b84SAndreas Gohr     */
9070316b84SAndreas Gohr    public function getFiles()
9170316b84SAndreas Gohr    {
9270316b84SAndreas Gohr        return $this->files;
9370316b84SAndreas Gohr    }
9470316b84SAndreas Gohr
9570316b84SAndreas Gohr    // region content creators
9670316b84SAndreas Gohr
9770316b84SAndreas Gohr    /**
9870316b84SAndreas Gohr     * Add the basic files to the plugin
9970316b84SAndreas Gohr     */
10070316b84SAndreas Gohr    public function addBasics()
10170316b84SAndreas Gohr    {
10270316b84SAndreas Gohr        $this->loadSkeleton('info.txt', $this->type . '.info.txt');
10370316b84SAndreas Gohr        $this->loadSkeleton('README');
10470316b84SAndreas Gohr        $this->loadSkeleton('LICENSE');
10501bb0631SAndreas Gohr        $this->loadSkeleton('_gitattributes', '.gitattributes');
10670316b84SAndreas Gohr    }
10770316b84SAndreas Gohr
10870316b84SAndreas Gohr    /**
10970316b84SAndreas Gohr     * Add another component to the plugin
11070316b84SAndreas Gohr     *
11170316b84SAndreas Gohr     * @param string $type
11270316b84SAndreas Gohr     * @param string $component
11370316b84SAndreas Gohr     */
11489e2f9d1SAndreas Gohr    public function addComponent($type, $component = '', $options = [])
11570316b84SAndreas Gohr    {
11670316b84SAndreas Gohr        if ($this->type !== self::TYPE_PLUGIN) {
11770316b84SAndreas Gohr            throw new RuntimeException('Components can only be added to plugins');
11870316b84SAndreas Gohr        }
11970316b84SAndreas Gohr
12070316b84SAndreas Gohr        if (!in_array($type, self::PLUGIN_TYPES)) {
12170316b84SAndreas Gohr            throw new RuntimeException('Invalid type ' . $type);
12270316b84SAndreas Gohr        }
12370316b84SAndreas Gohr
12470316b84SAndreas Gohr        $plugin = $this->base;
12570316b84SAndreas Gohr
12670316b84SAndreas Gohr        if ($component) {
12770316b84SAndreas Gohr            $path = $type . '/' . $component . '.php';
12870316b84SAndreas Gohr            $class = $type . '_plugin_' . $plugin . '_' . $component;
12970316b84SAndreas Gohr            $self = 'plugin_' . $plugin . '_' . $component;
13070316b84SAndreas Gohr        } else {
13170316b84SAndreas Gohr            $path = $type . '.php';
13270316b84SAndreas Gohr            $class = $type . '_plugin_' . $plugin;
13370316b84SAndreas Gohr            $self = 'plugin_' . $plugin;
13470316b84SAndreas Gohr        }
13570316b84SAndreas Gohr
13689e2f9d1SAndreas Gohr        if ($type === 'action') {
13789e2f9d1SAndreas Gohr            $replacements = $this->actionReplacements($options);
13889e2f9d1SAndreas Gohr        }
139cde324c2SAndreas Gohr        if ($type === 'renderer' && isset($options[0]) && $options[0] === 'Doku_Renderer_xhtml') {
140cde324c2SAndreas Gohr            $type = 'renderer_xhtml'; // different template then
141cde324c2SAndreas Gohr        }
14289e2f9d1SAndreas Gohr
14370316b84SAndreas Gohr        $replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class;
14470316b84SAndreas Gohr        $replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self;
14570316b84SAndreas Gohr        $this->loadSkeleton($type . '.php', $path, $replacements);
14670316b84SAndreas Gohr    }
14770316b84SAndreas Gohr
14870316b84SAndreas Gohr    /**
14970316b84SAndreas Gohr     * Add test framework optionally with a specific test
15070316b84SAndreas Gohr     *
15170316b84SAndreas Gohr     * @param string $test Name of the Test to add
15270316b84SAndreas Gohr     */
15370316b84SAndreas Gohr    public function addTest($test = '')
15470316b84SAndreas Gohr    {
15553bec4caSAndreas Gohr        // pick a random day and time for the cron job
15653bec4caSAndreas Gohr        $cron = sprintf(
15753bec4caSAndreas Gohr            '%d %d %d * *',
15853bec4caSAndreas Gohr            random_int(0, 59),
15953bec4caSAndreas Gohr            random_int(0, 23),
16053bec4caSAndreas Gohr            random_int(1, 28)
16153bec4caSAndreas Gohr        );
16253bec4caSAndreas Gohr
16353bec4caSAndreas Gohr        $test = ucfirst($test);
16453bec4caSAndreas Gohr        $this->loadSkeleton('.github/workflows/dokuwiki.yml', '', ['@@CRON@@' => $cron]);
16570316b84SAndreas Gohr        if ($test) {
16670316b84SAndreas Gohr            $replacements = ['@@TEST@@' => $test];
16770316b84SAndreas Gohr            $this->loadSkeleton('_test/StandardTest.php', '_test/' . $test . 'Test.php', $replacements);
16870316b84SAndreas Gohr        } else {
16970316b84SAndreas Gohr            $this->loadSkeleton('_test/GeneralTest.php');
17070316b84SAndreas Gohr        }
17170316b84SAndreas Gohr    }
17270316b84SAndreas Gohr
17370316b84SAndreas Gohr    /**
17470316b84SAndreas Gohr     * Add configuration
17570316b84SAndreas Gohr     *
17670316b84SAndreas Gohr     * @param bool $translate if true the settings language file will be be added, too
17770316b84SAndreas Gohr     */
17870316b84SAndreas Gohr    public function addConf($translate = false)
17970316b84SAndreas Gohr    {
18070316b84SAndreas Gohr        $this->loadSkeleton('conf/default.php');
18170316b84SAndreas Gohr        $this->loadSkeleton('conf/metadata.php');
18270316b84SAndreas Gohr
18370316b84SAndreas Gohr        if ($translate) {
18470316b84SAndreas Gohr            $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php');
18570316b84SAndreas Gohr        }
18670316b84SAndreas Gohr    }
18770316b84SAndreas Gohr
18870316b84SAndreas Gohr    /**
18970316b84SAndreas Gohr     * Add language
19070316b84SAndreas Gohr     *
19170316b84SAndreas Gohr     * Currently only english is added, theoretically this could also copy over the keys from an
19270316b84SAndreas Gohr     * existing english language file.
19370316b84SAndreas Gohr     *
19470316b84SAndreas Gohr     * @param bool $conf if true the settings language file will be be added, too
19570316b84SAndreas Gohr     */
19670316b84SAndreas Gohr    public function addLang($conf = false)
19770316b84SAndreas Gohr    {
19870316b84SAndreas Gohr        $this->loadSkeleton('lang/lang.php', 'lang/en/lang.php');
19970316b84SAndreas Gohr        if ($conf) {
20070316b84SAndreas Gohr            $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php');
20170316b84SAndreas Gohr        }
20270316b84SAndreas Gohr    }
20370316b84SAndreas Gohr
204719b4841SAndreas Gohr    /**
205719b4841SAndreas Gohr     * Add the AGENTS.md file
206719b4841SAndreas Gohr     *
207719b4841SAndreas Gohr     * @return void
208719b4841SAndreas Gohr     */
209719b4841SAndreas Gohr    public function addAgents()
210719b4841SAndreas Gohr    {
211719b4841SAndreas Gohr        $this->loadSkeleton('AGENTS.md');
212719b4841SAndreas Gohr    }
213719b4841SAndreas Gohr
21470316b84SAndreas Gohr    // endregion
21570316b84SAndreas Gohr
21670316b84SAndreas Gohr
21770316b84SAndreas Gohr    /**
21870316b84SAndreas Gohr     * Prepare the string replacements
21970316b84SAndreas Gohr     *
22070316b84SAndreas Gohr     * @param array $replacements override defaults
22170316b84SAndreas Gohr     * @return array
22270316b84SAndreas Gohr     */
22370316b84SAndreas Gohr    protected function prepareReplacements($replacements = [])
22470316b84SAndreas Gohr    {
22570316b84SAndreas Gohr        // defaults
22670316b84SAndreas Gohr        $data = [
22770316b84SAndreas Gohr            '@@AUTHOR_NAME@@' => $this->author,
22870316b84SAndreas Gohr            '@@AUTHOR_MAIL@@' => $this->email,
22970316b84SAndreas Gohr            '@@PLUGIN_NAME@@' => $this->base, // FIXME rename to @@PLUGIN_BASE@@
23070316b84SAndreas Gohr            '@@PLUGIN_DESC@@' => $this->desc,
23170316b84SAndreas Gohr            '@@PLUGIN_URL@@' => $this->url,
23270316b84SAndreas Gohr            '@@PLUGIN_TYPE@@' => $this->type,
23370316b84SAndreas Gohr            '@@INSTALL_DIR@@' => ($this->type == self::TYPE_PLUGIN) ? 'plugins' : 'tpl',
23470316b84SAndreas Gohr            '@@DATE@@' => date('Y-m-d'),
23570316b84SAndreas Gohr        ];
23670316b84SAndreas Gohr
23770316b84SAndreas Gohr        // merge given overrides
23870316b84SAndreas Gohr        return array_merge($data, $replacements);
23970316b84SAndreas Gohr    }
24070316b84SAndreas Gohr
24170316b84SAndreas Gohr    /**
24270316b84SAndreas Gohr     * Replacements needed for action components.
24370316b84SAndreas Gohr     *
24489e2f9d1SAndreas Gohr     * @param string[] $event Event names to handle
24570316b84SAndreas Gohr     * @return string[]
24670316b84SAndreas Gohr     */
24789e2f9d1SAndreas Gohr    protected function actionReplacements($events = [])
24870316b84SAndreas Gohr    {
24989e2f9d1SAndreas Gohr        if (!$events) $events = ['EXAMPLE_EVENT'];
25089e2f9d1SAndreas Gohr
25189e2f9d1SAndreas Gohr        $register = '';
25289e2f9d1SAndreas Gohr        $handler = '';
25389e2f9d1SAndreas Gohr
25489e2f9d1SAndreas Gohr        $template = file_get_contents(__DIR__ . '/skel/action_handler.php');
25589e2f9d1SAndreas Gohr
25689e2f9d1SAndreas Gohr        foreach ($events as $event) {
25770316b84SAndreas Gohr            $event = strtoupper($event);
25870316b84SAndreas Gohr            $fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_'));
25989e2f9d1SAndreas Gohr
26089e2f9d1SAndreas Gohr            $register .= '        $controller->register_hook(\'' . $event .
26189e2f9d1SAndreas Gohr                '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');' . "\n";
26289e2f9d1SAndreas Gohr
26389e2f9d1SAndreas Gohr            $handler .= str_replace(['@@EVENT@@', '@@HANDLER@@'], [$event, $fn], $template);
26489e2f9d1SAndreas Gohr        }
26570316b84SAndreas Gohr
26670316b84SAndreas Gohr        return [
26716cb46d5SAnna Dabrowska            '@@REGISTER@@' => rtrim($register, "\n"),
26816cb46d5SAnna Dabrowska            '@@HANDLERS@@' => rtrim($handler, "\n"),
26970316b84SAndreas Gohr        ];
27070316b84SAndreas Gohr    }
27170316b84SAndreas Gohr
27270316b84SAndreas Gohr    /**
27370316b84SAndreas Gohr     * Load a skeleton file, do the replacements and add it to the list of files
27470316b84SAndreas Gohr     *
27570316b84SAndreas Gohr     * @param string $skel Skeleton relative to the skel dir
27670316b84SAndreas Gohr     * @param string $target File name in the final plugin/template, empty for same as skeleton
27770316b84SAndreas Gohr     * @param array $replacements Non-default replacements to use
27870316b84SAndreas Gohr     */
27970316b84SAndreas Gohr    protected function loadSkeleton($skel, $target = '', $replacements = [])
28070316b84SAndreas Gohr    {
28170316b84SAndreas Gohr        $replacements = $this->prepareReplacements($replacements);
28270316b84SAndreas Gohr        if (!$target) $target = $skel;
28370316b84SAndreas Gohr
28470316b84SAndreas Gohr
28570316b84SAndreas Gohr        $file = __DIR__ . '/skel/' . $skel;
28670316b84SAndreas Gohr        if (!file_exists($file)) {
28770316b84SAndreas Gohr            throw new RuntimeException('Skeleton file not found: ' . $skel);
28870316b84SAndreas Gohr        }
28970316b84SAndreas Gohr        $content = file_get_contents($file);
29070316b84SAndreas Gohr        $this->files[$target] = str_replace(
29170316b84SAndreas Gohr            array_keys($replacements),
29270316b84SAndreas Gohr            array_values($replacements),
29370316b84SAndreas Gohr            $content
29470316b84SAndreas Gohr        );
29570316b84SAndreas Gohr    }
29670316b84SAndreas Gohr}
297