1*70316b84SAndreas Gohr<?php 2*70316b84SAndreas Gohr 3*70316b84SAndreas Gohrnamespace dokuwiki\plugin\dev; 4*70316b84SAndreas Gohr 5*70316b84SAndreas Gohruse RuntimeException; 6*70316b84SAndreas Gohr 7*70316b84SAndreas Gohr/** 8*70316b84SAndreas Gohr * This class holds basic information about a plugin or template and uses the skeleton files to 9*70316b84SAndreas Gohr * create new plugin or template specific versions of them. 10*70316b84SAndreas Gohr * 11*70316b84SAndreas Gohr * This class does not write any files, but only provides the data for the actual file creation. 12*70316b84SAndreas Gohr */ 13*70316b84SAndreas Gohrclass Skeletor 14*70316b84SAndreas Gohr{ 15*70316b84SAndreas Gohr // FIXME this may change upstream we may want to update it via github action 16*70316b84SAndreas Gohr const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli']; 17*70316b84SAndreas Gohr 18*70316b84SAndreas Gohr const TYPE_PLUGIN = 'plugin'; 19*70316b84SAndreas Gohr const TYPE_TEMPLATE = 'template'; 20*70316b84SAndreas Gohr 21*70316b84SAndreas Gohr protected $type; 22*70316b84SAndreas Gohr protected $base; 23*70316b84SAndreas Gohr protected $author; 24*70316b84SAndreas Gohr protected $desc; 25*70316b84SAndreas Gohr protected $name; 26*70316b84SAndreas Gohr protected $email; 27*70316b84SAndreas Gohr protected $url; 28*70316b84SAndreas Gohr protected $dir; 29*70316b84SAndreas Gohr 30*70316b84SAndreas Gohr /** @var array The files to be created in the form of [path => content] */ 31*70316b84SAndreas Gohr protected $files = []; 32*70316b84SAndreas Gohr 33*70316b84SAndreas Gohr /** 34*70316b84SAndreas Gohr * Initialize the skeletor from provided data 35*70316b84SAndreas Gohr * 36*70316b84SAndreas Gohr * @param string $type 37*70316b84SAndreas Gohr * @param string $base 38*70316b84SAndreas Gohr * @param string $desc 39*70316b84SAndreas Gohr * @param string $author 40*70316b84SAndreas Gohr * @param string $email 41*70316b84SAndreas Gohr * @param string $name 42*70316b84SAndreas Gohr * @param string $url 43*70316b84SAndreas Gohr */ 44*70316b84SAndreas Gohr public function __construct($type, $base, $desc, $author, $email, $name = '', $url = '') 45*70316b84SAndreas Gohr { 46*70316b84SAndreas Gohr $this->type = $type; 47*70316b84SAndreas Gohr $this->base = $base; 48*70316b84SAndreas Gohr $this->desc = $desc; 49*70316b84SAndreas Gohr $this->author = $author; 50*70316b84SAndreas Gohr $this->email = $email; 51*70316b84SAndreas Gohr $this->name = $name ?: ucfirst($base . ' ' . $type); 52*70316b84SAndreas Gohr 53*70316b84SAndreas Gohr if ($type == self::TYPE_PLUGIN) { 54*70316b84SAndreas Gohr $this->url = $url ?: 'https://www.dokuwiki.org/plugin:' . $base; 55*70316b84SAndreas Gohr $this->dir = 'lib/plugins/' . $base; 56*70316b84SAndreas Gohr } else { 57*70316b84SAndreas Gohr $this->url = $url ?: 'https://www.dokuwiki.org/template:' . $base; 58*70316b84SAndreas Gohr $this->dir = 'lib/tpl/' . $base; 59*70316b84SAndreas Gohr } 60*70316b84SAndreas Gohr } 61*70316b84SAndreas Gohr 62*70316b84SAndreas Gohr /** 63*70316b84SAndreas Gohr * Create an instance using an existing plugin or template directory 64*70316b84SAndreas Gohr * 65*70316b84SAndreas Gohr * @param string $dir 66*70316b84SAndreas Gohr * @return Skeletor 67*70316b84SAndreas Gohr */ 68*70316b84SAndreas Gohr static public function fromDir($dir) 69*70316b84SAndreas Gohr { 70*70316b84SAndreas Gohr if (file_exists($dir . '/plugin.info.txt')) { 71*70316b84SAndreas Gohr $type = self::TYPE_PLUGIN; 72*70316b84SAndreas Gohr } elseif (file_exists($dir . '/template.info.txt')) { 73*70316b84SAndreas Gohr $type = self::TYPE_TEMPLATE; 74*70316b84SAndreas Gohr } else { 75*70316b84SAndreas Gohr throw new RuntimeException('Not a plugin or template directory'); 76*70316b84SAndreas Gohr } 77*70316b84SAndreas Gohr 78*70316b84SAndreas Gohr $data = file($dir . '/' . $type . '.info.txt', FILE_IGNORE_NEW_LINES); 79*70316b84SAndreas Gohr $data = array_map(function ($item) { 80*70316b84SAndreas Gohr return array_map('trim', explode(' ', $item, 2)); 81*70316b84SAndreas Gohr }, $data); 82*70316b84SAndreas Gohr $data = array_combine(array_column($data, 0), array_column($data, 1)); 83*70316b84SAndreas Gohr 84*70316b84SAndreas Gohr return new self($type, $data['base'], $data['desc'], $data['author'], $data['email'], $data['url']); 85*70316b84SAndreas Gohr } 86*70316b84SAndreas Gohr 87*70316b84SAndreas Gohr /** 88*70316b84SAndreas Gohr * Return the files to be created 89*70316b84SAndreas Gohr * 90*70316b84SAndreas Gohr * @return array [path => content] 91*70316b84SAndreas Gohr */ 92*70316b84SAndreas Gohr public function getFiles() 93*70316b84SAndreas Gohr { 94*70316b84SAndreas Gohr return $this->files; 95*70316b84SAndreas Gohr } 96*70316b84SAndreas Gohr 97*70316b84SAndreas Gohr // region content creators 98*70316b84SAndreas Gohr 99*70316b84SAndreas Gohr /** 100*70316b84SAndreas Gohr * Add the basic files to the plugin 101*70316b84SAndreas Gohr */ 102*70316b84SAndreas Gohr public function addBasics() 103*70316b84SAndreas Gohr { 104*70316b84SAndreas Gohr $this->loadSkeleton('info.txt', $this->type . '.info.txt'); 105*70316b84SAndreas Gohr $this->loadSkeleton('README'); 106*70316b84SAndreas Gohr $this->loadSkeleton('LICENSE'); 107*70316b84SAndreas Gohr $this->loadSkeleton('.gitattributes'); 108*70316b84SAndreas Gohr } 109*70316b84SAndreas Gohr 110*70316b84SAndreas Gohr /** 111*70316b84SAndreas Gohr * Add another component to the plugin 112*70316b84SAndreas Gohr * 113*70316b84SAndreas Gohr * @param string $type 114*70316b84SAndreas Gohr * @param string $component 115*70316b84SAndreas Gohr */ 116*70316b84SAndreas Gohr public function addComponent($type, $component = '') 117*70316b84SAndreas Gohr { 118*70316b84SAndreas Gohr if ($this->type !== self::TYPE_PLUGIN) { 119*70316b84SAndreas Gohr throw new RuntimeException('Components can only be added to plugins'); 120*70316b84SAndreas Gohr } 121*70316b84SAndreas Gohr 122*70316b84SAndreas Gohr if (!in_array($type, self::PLUGIN_TYPES)) { 123*70316b84SAndreas Gohr throw new RuntimeException('Invalid type ' . $type); 124*70316b84SAndreas Gohr } 125*70316b84SAndreas Gohr 126*70316b84SAndreas Gohr $plugin = $this->base; 127*70316b84SAndreas Gohr 128*70316b84SAndreas Gohr if ($component) { 129*70316b84SAndreas Gohr $path = $type . '/' . $component . '.php'; 130*70316b84SAndreas Gohr $class = $type . '_plugin_' . $plugin . '_' . $component; 131*70316b84SAndreas Gohr $self = 'plugin_' . $plugin . '_' . $component; 132*70316b84SAndreas Gohr } else { 133*70316b84SAndreas Gohr $path = $type . '.php'; 134*70316b84SAndreas Gohr $class = $type . '_plugin_' . $plugin; 135*70316b84SAndreas Gohr $self = 'plugin_' . $plugin; 136*70316b84SAndreas Gohr } 137*70316b84SAndreas Gohr 138*70316b84SAndreas Gohr $replacements = $this->actionReplacements('EVENT_NAME'); // FIXME accept multiple optional events 139*70316b84SAndreas Gohr $replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class; 140*70316b84SAndreas Gohr $replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self; 141*70316b84SAndreas Gohr $this->loadSkeleton($type . '.php', $path, $replacements); 142*70316b84SAndreas Gohr } 143*70316b84SAndreas Gohr 144*70316b84SAndreas Gohr /** 145*70316b84SAndreas Gohr * Add test framework optionally with a specific test 146*70316b84SAndreas Gohr * 147*70316b84SAndreas Gohr * @param string $test Name of the Test to add 148*70316b84SAndreas Gohr */ 149*70316b84SAndreas Gohr public function addTest($test = '') 150*70316b84SAndreas Gohr { 151*70316b84SAndreas Gohr $test = ucfirst(strtolower($test)); 152*70316b84SAndreas Gohr $this->loadSkeleton('.github/workflows/phpTestLinux.yml'); 153*70316b84SAndreas Gohr if ($test) { 154*70316b84SAndreas Gohr $replacements = ['@@TEST@@' => $test]; 155*70316b84SAndreas Gohr $this->loadSkeleton('_test/StandardTest.php', '_test/' . $test . 'Test.php', $replacements); 156*70316b84SAndreas Gohr } else { 157*70316b84SAndreas Gohr $this->loadSkeleton('_test/GeneralTest.php'); 158*70316b84SAndreas Gohr } 159*70316b84SAndreas Gohr } 160*70316b84SAndreas Gohr 161*70316b84SAndreas Gohr /** 162*70316b84SAndreas Gohr * Add configuration 163*70316b84SAndreas Gohr * 164*70316b84SAndreas Gohr * @param bool $translate if true the settings language file will be be added, too 165*70316b84SAndreas Gohr */ 166*70316b84SAndreas Gohr public function addConf($translate = false) 167*70316b84SAndreas Gohr { 168*70316b84SAndreas Gohr $this->loadSkeleton('conf/default.php'); 169*70316b84SAndreas Gohr $this->loadSkeleton('conf/metadata.php'); 170*70316b84SAndreas Gohr 171*70316b84SAndreas Gohr if ($translate) { 172*70316b84SAndreas Gohr $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 173*70316b84SAndreas Gohr } 174*70316b84SAndreas Gohr } 175*70316b84SAndreas Gohr 176*70316b84SAndreas Gohr /** 177*70316b84SAndreas Gohr * Add language 178*70316b84SAndreas Gohr * 179*70316b84SAndreas Gohr * Currently only english is added, theoretically this could also copy over the keys from an 180*70316b84SAndreas Gohr * existing english language file. 181*70316b84SAndreas Gohr * 182*70316b84SAndreas Gohr * @param bool $conf if true the settings language file will be be added, too 183*70316b84SAndreas Gohr */ 184*70316b84SAndreas Gohr public function addLang($conf = false) 185*70316b84SAndreas Gohr { 186*70316b84SAndreas Gohr $this->loadSkeleton('lang/lang.php', 'lang/en/lang.php'); 187*70316b84SAndreas Gohr if ($conf) { 188*70316b84SAndreas Gohr $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 189*70316b84SAndreas Gohr } 190*70316b84SAndreas Gohr } 191*70316b84SAndreas Gohr 192*70316b84SAndreas Gohr // endregion 193*70316b84SAndreas Gohr 194*70316b84SAndreas Gohr 195*70316b84SAndreas Gohr /** 196*70316b84SAndreas Gohr * Prepare the string replacements 197*70316b84SAndreas Gohr * 198*70316b84SAndreas Gohr * @param array $replacements override defaults 199*70316b84SAndreas Gohr * @return array 200*70316b84SAndreas Gohr */ 201*70316b84SAndreas Gohr protected function prepareReplacements($replacements = []) 202*70316b84SAndreas Gohr { 203*70316b84SAndreas Gohr // defaults 204*70316b84SAndreas Gohr $data = [ 205*70316b84SAndreas Gohr '@@AUTHOR_NAME@@' => $this->author, 206*70316b84SAndreas Gohr '@@AUTHOR_MAIL@@' => $this->email, 207*70316b84SAndreas Gohr '@@PLUGIN_NAME@@' => $this->base, // FIXME rename to @@PLUGIN_BASE@@ 208*70316b84SAndreas Gohr '@@PLUGIN_DESC@@' => $this->desc, 209*70316b84SAndreas Gohr '@@PLUGIN_URL@@' => $this->url, 210*70316b84SAndreas Gohr '@@PLUGIN_TYPE@@' => $this->type, 211*70316b84SAndreas Gohr '@@INSTALL_DIR@@' => ($this->type == self::TYPE_PLUGIN) ? 'plugins' : 'tpl', 212*70316b84SAndreas Gohr '@@DATE@@' => date('Y-m-d'), 213*70316b84SAndreas Gohr ]; 214*70316b84SAndreas Gohr 215*70316b84SAndreas Gohr // merge given overrides 216*70316b84SAndreas Gohr return array_merge($data, $replacements); 217*70316b84SAndreas Gohr } 218*70316b84SAndreas Gohr 219*70316b84SAndreas Gohr /** 220*70316b84SAndreas Gohr * Replacements needed for action components. 221*70316b84SAndreas Gohr * 222*70316b84SAndreas Gohr * @param string $event FIXME support multiple events 223*70316b84SAndreas Gohr * @return string[] 224*70316b84SAndreas Gohr */ 225*70316b84SAndreas Gohr protected function actionReplacements($event) 226*70316b84SAndreas Gohr { 227*70316b84SAndreas Gohr $event = strtoupper($event); 228*70316b84SAndreas Gohr $fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_')); 229*70316b84SAndreas Gohr $register = ' $controller->register_hook(\'' . $event . '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');'; 230*70316b84SAndreas Gohr $handler = ' public function ' . $fn . '(Doku_Event $event, $param)' . "\n" 231*70316b84SAndreas Gohr . " {\n" 232*70316b84SAndreas Gohr . " }\n"; 233*70316b84SAndreas Gohr 234*70316b84SAndreas Gohr return [ 235*70316b84SAndreas Gohr '@@REGISTER@@' => $register . "\n ", 236*70316b84SAndreas Gohr '@@HANDLERS@@' => $handler, 237*70316b84SAndreas Gohr ]; 238*70316b84SAndreas Gohr } 239*70316b84SAndreas Gohr 240*70316b84SAndreas Gohr /** 241*70316b84SAndreas Gohr * Load a skeleton file, do the replacements and add it to the list of files 242*70316b84SAndreas Gohr * 243*70316b84SAndreas Gohr * @param string $skel Skeleton relative to the skel dir 244*70316b84SAndreas Gohr * @param string $target File name in the final plugin/template, empty for same as skeleton 245*70316b84SAndreas Gohr * @param array $replacements Non-default replacements to use 246*70316b84SAndreas Gohr */ 247*70316b84SAndreas Gohr protected function loadSkeleton($skel, $target = '', $replacements = []) 248*70316b84SAndreas Gohr { 249*70316b84SAndreas Gohr $replacements = $this->prepareReplacements($replacements); 250*70316b84SAndreas Gohr if (!$target) $target = $skel; 251*70316b84SAndreas Gohr 252*70316b84SAndreas Gohr 253*70316b84SAndreas Gohr $file = __DIR__ . '/skel/' . $skel; 254*70316b84SAndreas Gohr if (!file_exists($file)) { 255*70316b84SAndreas Gohr throw new RuntimeException('Skeleton file not found: ' . $skel); 256*70316b84SAndreas Gohr } 257*70316b84SAndreas Gohr $content = file_get_contents($file); 258*70316b84SAndreas Gohr $this->files[$target] = str_replace( 259*70316b84SAndreas Gohr array_keys($replacements), 260*70316b84SAndreas Gohr array_values($replacements), 261*70316b84SAndreas Gohr $content 262*70316b84SAndreas Gohr ); 263*70316b84SAndreas Gohr } 264*70316b84SAndreas Gohr} 265