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{ 11cf2dcf1bSAndreas Gohr const TYPE_PLUGIN = 'plugin'; 12cf2dcf1bSAndreas Gohr const TYPE_TEMPLATE = 'template'; 13cf2dcf1bSAndreas Gohr 14cf2dcf1bSAndreas Gohr /** @var string "plugin"|"template" */ 15cf2dcf1bSAndreas Gohr protected string $type = self::TYPE_PLUGIN; 16cf2dcf1bSAndreas Gohr 17cf2dcf1bSAndreas Gohr /** @var string The base name of this extension */ 18cf2dcf1bSAndreas Gohr protected string $base; 19cf2dcf1bSAndreas Gohr 2025d28a01SAndreas Gohr /** @var string The current location of this extension */ 2125d28a01SAndreas Gohr protected string $currentDir = ''; 22cf2dcf1bSAndreas Gohr 23cf2dcf1bSAndreas Gohr /** @var array The local info array of the extension */ 24cf2dcf1bSAndreas Gohr protected array $localInfo = []; 25cf2dcf1bSAndreas Gohr 26cf2dcf1bSAndreas Gohr /** @var array The remote info array of the extension */ 27cf2dcf1bSAndreas Gohr protected array $remoteInfo = []; 28cf2dcf1bSAndreas Gohr 297c9966a5SAndreas Gohr /** @var Manager|null The manager for this extension */ 3025d28a01SAndreas Gohr protected ?Manager $manager = null; 31cf2dcf1bSAndreas Gohr 32cf2dcf1bSAndreas Gohr // region Constructors 33cf2dcf1bSAndreas Gohr 34cf2dcf1bSAndreas Gohr /** 35cf2dcf1bSAndreas Gohr * The main constructor is private to force the use of the factory methods 36cf2dcf1bSAndreas Gohr */ 37cf2dcf1bSAndreas Gohr protected function __construct() 38cf2dcf1bSAndreas Gohr { 39cf2dcf1bSAndreas Gohr } 40cf2dcf1bSAndreas Gohr 41cf2dcf1bSAndreas Gohr /** 42a1e045f7SAndreas Gohr * Initializes an extension from an id 43a1e045f7SAndreas Gohr * 44a1e045f7SAndreas Gohr * @param string $id The id of the extension 45a1e045f7SAndreas Gohr * @return Extension 46a1e045f7SAndreas Gohr */ 47a1e045f7SAndreas Gohr public static function createFromId($id) 48a1e045f7SAndreas Gohr { 49a1e045f7SAndreas Gohr $extension = new self(); 50a1e045f7SAndreas Gohr $extension->initFromId($id); 51a1e045f7SAndreas Gohr return $extension; 52a1e045f7SAndreas Gohr } 53a1e045f7SAndreas Gohr 54a1e045f7SAndreas Gohr protected function initFromId($id) 55a1e045f7SAndreas Gohr { 56a1e045f7SAndreas Gohr [$type, $base] = $this->idToTypeBase($id); 57a1e045f7SAndreas Gohr $this->type = $type; 58a1e045f7SAndreas Gohr $this->base = $base; 59a1e045f7SAndreas Gohr $this->readLocalInfo(); 60a1e045f7SAndreas Gohr } 61a1e045f7SAndreas Gohr 62a1e045f7SAndreas Gohr /** 63cf2dcf1bSAndreas Gohr * Initializes an extension from a directory 64cf2dcf1bSAndreas Gohr * 65cf2dcf1bSAndreas Gohr * The given directory might be the one where the extension has already been installed to 66cf2dcf1bSAndreas Gohr * or it might be the extracted source in some temporary directory. 67cf2dcf1bSAndreas Gohr * 68cf2dcf1bSAndreas Gohr * @param string $dir Where the extension code is currently located 69cf2dcf1bSAndreas Gohr * @param string|null $type TYPE_PLUGIN|TYPE_TEMPLATE, null for auto-detection 70cf2dcf1bSAndreas Gohr * @param string $base The base name of the extension, null for auto-detection 71cf2dcf1bSAndreas Gohr * @return Extension 72cf2dcf1bSAndreas Gohr */ 73cf2dcf1bSAndreas Gohr public static function createFromDirectory($dir, $type = null, $base = null) 74cf2dcf1bSAndreas Gohr { 75cf2dcf1bSAndreas Gohr $extension = new self(); 76cf2dcf1bSAndreas Gohr $extension->initFromDirectory($dir, $type, $base); 77cf2dcf1bSAndreas Gohr return $extension; 78cf2dcf1bSAndreas Gohr } 79cf2dcf1bSAndreas Gohr 80cf2dcf1bSAndreas Gohr protected function initFromDirectory($dir, $type = null, $base = null) 81cf2dcf1bSAndreas Gohr { 82cf2dcf1bSAndreas Gohr if (!is_dir($dir)) throw new RuntimeException('Directory not found: ' . $dir); 83cf2dcf1bSAndreas Gohr $this->currentDir = realpath($dir); 84cf2dcf1bSAndreas Gohr 85cf2dcf1bSAndreas Gohr if ($type === null || $type === self::TYPE_TEMPLATE) { 86cf2dcf1bSAndreas Gohr if ( 87cf2dcf1bSAndreas Gohr file_exists($dir . '/template.info.php') || 88cf2dcf1bSAndreas Gohr file_exists($dir . '/style.ini') || 89cf2dcf1bSAndreas Gohr file_exists($dir . '/main.php') || 90cf2dcf1bSAndreas Gohr file_exists($dir . '/detail.php') || 91cf2dcf1bSAndreas Gohr file_exists($dir . '/mediamanager.php') 92cf2dcf1bSAndreas Gohr ) { 93cf2dcf1bSAndreas Gohr $this->type = self::TYPE_TEMPLATE; 94cf2dcf1bSAndreas Gohr } 95cf2dcf1bSAndreas Gohr } else { 96cf2dcf1bSAndreas Gohr $this->type = self::TYPE_PLUGIN; 97cf2dcf1bSAndreas Gohr } 98cf2dcf1bSAndreas Gohr 99cf2dcf1bSAndreas Gohr $this->readLocalInfo(); 100cf2dcf1bSAndreas Gohr 101cf2dcf1bSAndreas Gohr if ($base !== null) { 102cf2dcf1bSAndreas Gohr $this->base = $base; 103cf2dcf1bSAndreas Gohr } elseif (isset($this->localInfo['base'])) { 104cf2dcf1bSAndreas Gohr $this->base = $this->localInfo['base']; 105cf2dcf1bSAndreas Gohr } else { 106cf2dcf1bSAndreas Gohr $this->base = basename($dir); 107cf2dcf1bSAndreas Gohr } 108cf2dcf1bSAndreas Gohr } 109cf2dcf1bSAndreas Gohr 110cf2dcf1bSAndreas Gohr /** 111cf2dcf1bSAndreas Gohr * Initializes an extension from remote data 112cf2dcf1bSAndreas Gohr * 113cf2dcf1bSAndreas Gohr * @param array $data The data as returned by the repository api 114cf2dcf1bSAndreas Gohr * @return Extension 115cf2dcf1bSAndreas Gohr */ 116cf2dcf1bSAndreas Gohr public static function createFromRemoteData($data) 117cf2dcf1bSAndreas Gohr { 118cf2dcf1bSAndreas Gohr $extension = new self(); 119cf2dcf1bSAndreas Gohr $extension->initFromRemoteData($data); 120cf2dcf1bSAndreas Gohr return $extension; 121cf2dcf1bSAndreas Gohr } 122cf2dcf1bSAndreas Gohr 123cf2dcf1bSAndreas Gohr protected function initFromRemoteData($data) 124cf2dcf1bSAndreas Gohr { 125cf2dcf1bSAndreas Gohr if (!isset($data['plugin'])) throw new RuntimeException('Invalid remote data'); 126cf2dcf1bSAndreas Gohr 127a1e045f7SAndreas Gohr [$type, $base] = $this->idToTypeBase($data['plugin']); 128cf2dcf1bSAndreas Gohr $this->remoteInfo = $data; 129cf2dcf1bSAndreas Gohr $this->type = $type; 130cf2dcf1bSAndreas Gohr $this->base = $base; 131cf2dcf1bSAndreas Gohr 132cf2dcf1bSAndreas Gohr if ($this->isInstalled()) { 133cf2dcf1bSAndreas Gohr $this->currentDir = $this->getInstallDir(); 134cf2dcf1bSAndreas Gohr $this->readLocalInfo(); 135cf2dcf1bSAndreas Gohr } 136cf2dcf1bSAndreas Gohr } 137cf2dcf1bSAndreas Gohr 138cf2dcf1bSAndreas Gohr // endregion 139cf2dcf1bSAndreas Gohr 140cf2dcf1bSAndreas Gohr // region Getters 141cf2dcf1bSAndreas Gohr 142cf2dcf1bSAndreas Gohr /** 143*4fd6a1d7SAndreas Gohr * @param bool $wrap If true, the id is wrapped in backticks 144cf2dcf1bSAndreas Gohr * @return string The extension id (same as base but prefixed with "template:" for templates) 145cf2dcf1bSAndreas Gohr */ 146*4fd6a1d7SAndreas Gohr public function getId($wrap=false) 147cf2dcf1bSAndreas Gohr { 148cf2dcf1bSAndreas Gohr if ($this->type === self::TYPE_TEMPLATE) { 149*4fd6a1d7SAndreas Gohr $id = self::TYPE_TEMPLATE . ':' . $this->base; 150*4fd6a1d7SAndreas Gohr } else { 151*4fd6a1d7SAndreas Gohr $id = $this->base; 152cf2dcf1bSAndreas Gohr } 153*4fd6a1d7SAndreas Gohr if($wrap) $id = "`$id`"; 154*4fd6a1d7SAndreas Gohr return $id; 155cf2dcf1bSAndreas Gohr } 156cf2dcf1bSAndreas Gohr 157cf2dcf1bSAndreas Gohr /** 158cf2dcf1bSAndreas Gohr * Get the base name of this extension 159cf2dcf1bSAndreas Gohr * 160cf2dcf1bSAndreas Gohr * @return string 161cf2dcf1bSAndreas Gohr */ 162cf2dcf1bSAndreas Gohr public function getBase() 163cf2dcf1bSAndreas Gohr { 164cf2dcf1bSAndreas Gohr return $this->base; 165cf2dcf1bSAndreas Gohr } 166cf2dcf1bSAndreas Gohr 167cf2dcf1bSAndreas Gohr /** 168cf2dcf1bSAndreas Gohr * Get the type of the extension 169cf2dcf1bSAndreas Gohr * 170cf2dcf1bSAndreas Gohr * @return string "plugin"|"template" 171cf2dcf1bSAndreas Gohr */ 172cf2dcf1bSAndreas Gohr public function getType() 173cf2dcf1bSAndreas Gohr { 174cf2dcf1bSAndreas Gohr return $this->type; 175cf2dcf1bSAndreas Gohr } 176cf2dcf1bSAndreas Gohr 177cf2dcf1bSAndreas Gohr /** 178cf2dcf1bSAndreas Gohr * The current directory of the extension 179cf2dcf1bSAndreas Gohr * 180cf2dcf1bSAndreas Gohr * @return string|null 181cf2dcf1bSAndreas Gohr */ 182cf2dcf1bSAndreas Gohr public function getCurrentDir() 183cf2dcf1bSAndreas Gohr { 184cf2dcf1bSAndreas Gohr // recheck that the current currentDir is still valid 185cf2dcf1bSAndreas Gohr if ($this->currentDir && !is_dir($this->currentDir)) { 18625d28a01SAndreas Gohr $this->currentDir = ''; 187cf2dcf1bSAndreas Gohr } 188cf2dcf1bSAndreas Gohr 189cf2dcf1bSAndreas Gohr // if the extension is installed, then the currentDir is the install dir! 190cf2dcf1bSAndreas Gohr if (!$this->currentDir && $this->isInstalled()) { 191cf2dcf1bSAndreas Gohr $this->currentDir = $this->getInstallDir(); 192cf2dcf1bSAndreas Gohr } 193cf2dcf1bSAndreas Gohr 194cf2dcf1bSAndreas Gohr return $this->currentDir; 195cf2dcf1bSAndreas Gohr } 196cf2dcf1bSAndreas Gohr 197cf2dcf1bSAndreas Gohr /** 198cf2dcf1bSAndreas Gohr * Get the directory where this extension should be installed in 199cf2dcf1bSAndreas Gohr * 200cf2dcf1bSAndreas Gohr * Note: this does not mean that the extension is actually installed there 201cf2dcf1bSAndreas Gohr * 202cf2dcf1bSAndreas Gohr * @return string 203cf2dcf1bSAndreas Gohr */ 204cf2dcf1bSAndreas Gohr public function getInstallDir() 205cf2dcf1bSAndreas Gohr { 206cf2dcf1bSAndreas Gohr if ($this->isTemplate()) { 207cf2dcf1bSAndreas Gohr $dir = dirname(tpl_incdir()) . $this->base; 208cf2dcf1bSAndreas Gohr } else { 209cf2dcf1bSAndreas Gohr $dir = DOKU_PLUGIN . $this->base; 210cf2dcf1bSAndreas Gohr } 211cf2dcf1bSAndreas Gohr 21225d28a01SAndreas Gohr return fullpath($dir); 213cf2dcf1bSAndreas Gohr } 214cf2dcf1bSAndreas Gohr 215cf2dcf1bSAndreas Gohr 216cf2dcf1bSAndreas Gohr /** 217cf2dcf1bSAndreas Gohr * Get the display name of the extension 218cf2dcf1bSAndreas Gohr * 219cf2dcf1bSAndreas Gohr * @return string 220cf2dcf1bSAndreas Gohr */ 221cf2dcf1bSAndreas Gohr public function getDisplayName() 222cf2dcf1bSAndreas Gohr { 223cf2dcf1bSAndreas Gohr return $this->getTag('name', PhpString::ucwords($this->getBase() . ' ' . $this->getType())); 224cf2dcf1bSAndreas Gohr } 225cf2dcf1bSAndreas Gohr 226cf2dcf1bSAndreas Gohr /** 227cf2dcf1bSAndreas Gohr * Get the author name of the extension 228cf2dcf1bSAndreas Gohr * 229cf2dcf1bSAndreas Gohr * @return string Returns an empty string if the author info is missing 230cf2dcf1bSAndreas Gohr */ 231cf2dcf1bSAndreas Gohr public function getAuthor() 232cf2dcf1bSAndreas Gohr { 233cf2dcf1bSAndreas Gohr return $this->getTag('author'); 234cf2dcf1bSAndreas Gohr } 235cf2dcf1bSAndreas Gohr 236cf2dcf1bSAndreas Gohr /** 237cf2dcf1bSAndreas Gohr * Get the email of the author of the extension if there is any 238cf2dcf1bSAndreas Gohr * 239cf2dcf1bSAndreas Gohr * @return string Returns an empty string if the email info is missing 240cf2dcf1bSAndreas Gohr */ 241cf2dcf1bSAndreas Gohr public function getEmail() 242cf2dcf1bSAndreas Gohr { 243cf2dcf1bSAndreas Gohr // email is only in the local data 244cf2dcf1bSAndreas Gohr return $this->localInfo['email'] ?? ''; 245cf2dcf1bSAndreas Gohr } 246cf2dcf1bSAndreas Gohr 247cf2dcf1bSAndreas Gohr /** 248cf2dcf1bSAndreas Gohr * Get the email id, i.e. the md5sum of the email 249cf2dcf1bSAndreas Gohr * 250cf2dcf1bSAndreas Gohr * @return string Empty string if no email is available 251cf2dcf1bSAndreas Gohr */ 252cf2dcf1bSAndreas Gohr public function getEmailID() 253cf2dcf1bSAndreas Gohr { 254cf2dcf1bSAndreas Gohr if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid']; 255cf2dcf1bSAndreas Gohr if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']); 256cf2dcf1bSAndreas Gohr return ''; 257cf2dcf1bSAndreas Gohr } 258cf2dcf1bSAndreas Gohr 259cf2dcf1bSAndreas Gohr /** 260cf2dcf1bSAndreas Gohr * Get the description of the extension 261cf2dcf1bSAndreas Gohr * 262cf2dcf1bSAndreas Gohr * @return string Empty string if no description is available 263cf2dcf1bSAndreas Gohr */ 264cf2dcf1bSAndreas Gohr public function getDescription() 265cf2dcf1bSAndreas Gohr { 266cf2dcf1bSAndreas Gohr return $this->getTag(['desc', 'description']); 267cf2dcf1bSAndreas Gohr } 268cf2dcf1bSAndreas Gohr 269cf2dcf1bSAndreas Gohr /** 270cf2dcf1bSAndreas Gohr * Get the URL of the extension, usually a page on dokuwiki.org 271cf2dcf1bSAndreas Gohr * 272cf2dcf1bSAndreas Gohr * @return string 273cf2dcf1bSAndreas Gohr */ 274cf2dcf1bSAndreas Gohr public function getURL() 275cf2dcf1bSAndreas Gohr { 276cf2dcf1bSAndreas Gohr return $this->getTag( 277cf2dcf1bSAndreas Gohr 'url', 278cf2dcf1bSAndreas Gohr 'https://www.dokuwiki.org/' . 279cf2dcf1bSAndreas Gohr ($this->isTemplate() ? 'template' : 'plugin') . ':' . $this->getBase() 280cf2dcf1bSAndreas Gohr ); 281cf2dcf1bSAndreas Gohr } 282cf2dcf1bSAndreas Gohr 283cf2dcf1bSAndreas Gohr /** 2847c9966a5SAndreas Gohr * Get the version of the extension that is actually installed 2857c9966a5SAndreas Gohr * 2867c9966a5SAndreas Gohr * Returns an empty string if the version is not available 2877c9966a5SAndreas Gohr * 2887c9966a5SAndreas Gohr * @return string 2897c9966a5SAndreas Gohr */ 2907c9966a5SAndreas Gohr public function getInstalledVersion() 2917c9966a5SAndreas Gohr { 2927c9966a5SAndreas Gohr return $this->localInfo['date'] ?? ''; 2937c9966a5SAndreas Gohr } 2947c9966a5SAndreas Gohr 2957c9966a5SAndreas Gohr /** 296*4fd6a1d7SAndreas Gohr * Get the types of components this extension provides 297*4fd6a1d7SAndreas Gohr * 298*4fd6a1d7SAndreas Gohr * @todo for installed extensions this could be read from the filesystem instead of relying on the meta data 299*4fd6a1d7SAndreas Gohr * @return array int -> type 300*4fd6a1d7SAndreas Gohr */ 301*4fd6a1d7SAndreas Gohr public function getComponentTypes () 302*4fd6a1d7SAndreas Gohr { 303*4fd6a1d7SAndreas Gohr return $this->getTag('types', []); 304*4fd6a1d7SAndreas Gohr } 305*4fd6a1d7SAndreas Gohr 306*4fd6a1d7SAndreas Gohr /** 30725d28a01SAndreas Gohr * Get a list of extension ids this extension depends on 30825d28a01SAndreas Gohr * 30925d28a01SAndreas Gohr * @return string[] 31025d28a01SAndreas Gohr */ 31125d28a01SAndreas Gohr public function getDependencyList() 31225d28a01SAndreas Gohr { 31325d28a01SAndreas Gohr return $this->getTag('depends', []); 31425d28a01SAndreas Gohr } 31525d28a01SAndreas Gohr 31625d28a01SAndreas Gohr /** 317b2a05b76SAndreas Gohr * Return the minimum PHP version required by the extension 318b2a05b76SAndreas Gohr * 319b2a05b76SAndreas Gohr * Empty if not set 320b2a05b76SAndreas Gohr * 321b2a05b76SAndreas Gohr * @return string 322b2a05b76SAndreas Gohr */ 323b2a05b76SAndreas Gohr public function getMinimumPHPVersion() 324b2a05b76SAndreas Gohr { 325b2a05b76SAndreas Gohr return $this->getTag('phpmin', ''); 326b2a05b76SAndreas Gohr } 327b2a05b76SAndreas Gohr 328b2a05b76SAndreas Gohr /** 329b2a05b76SAndreas Gohr * Return the minimum PHP version supported by the extension 330b2a05b76SAndreas Gohr * 331b2a05b76SAndreas Gohr * @return string 332b2a05b76SAndreas Gohr */ 333b2a05b76SAndreas Gohr public function getMaximumPHPVersion() 334b2a05b76SAndreas Gohr { 335b2a05b76SAndreas Gohr return $this->getTag('phpmax', ''); 336b2a05b76SAndreas Gohr } 337b2a05b76SAndreas Gohr 338b2a05b76SAndreas Gohr /** 339cf2dcf1bSAndreas Gohr * Is this extension a template? 340cf2dcf1bSAndreas Gohr * 341cf2dcf1bSAndreas Gohr * @return bool false if it is a plugin 342cf2dcf1bSAndreas Gohr */ 343cf2dcf1bSAndreas Gohr public function isTemplate() 344cf2dcf1bSAndreas Gohr { 345cf2dcf1bSAndreas Gohr return $this->type === self::TYPE_TEMPLATE; 346cf2dcf1bSAndreas Gohr } 347cf2dcf1bSAndreas Gohr 348cf2dcf1bSAndreas Gohr /** 349cf2dcf1bSAndreas Gohr * Is the extension installed locally? 350cf2dcf1bSAndreas Gohr * 351cf2dcf1bSAndreas Gohr * @return bool 352cf2dcf1bSAndreas Gohr */ 353cf2dcf1bSAndreas Gohr public function isInstalled() 354cf2dcf1bSAndreas Gohr { 355cf2dcf1bSAndreas Gohr return is_dir($this->getInstallDir()); 356cf2dcf1bSAndreas Gohr } 357cf2dcf1bSAndreas Gohr 358cf2dcf1bSAndreas Gohr /** 359cf2dcf1bSAndreas Gohr * Is the extension under git control? 360cf2dcf1bSAndreas Gohr * 361cf2dcf1bSAndreas Gohr * @return bool 362cf2dcf1bSAndreas Gohr */ 363cf2dcf1bSAndreas Gohr public function isGitControlled() 364cf2dcf1bSAndreas Gohr { 365cf2dcf1bSAndreas Gohr if (!$this->isInstalled()) return false; 366cf2dcf1bSAndreas Gohr return file_exists($this->getInstallDir() . '/.git'); 367cf2dcf1bSAndreas Gohr } 368cf2dcf1bSAndreas Gohr 369cf2dcf1bSAndreas Gohr /** 370cf2dcf1bSAndreas Gohr * If the extension is bundled 371cf2dcf1bSAndreas Gohr * 372cf2dcf1bSAndreas Gohr * @return bool If the extension is bundled 373cf2dcf1bSAndreas Gohr */ 374cf2dcf1bSAndreas Gohr public function isBundled() 375cf2dcf1bSAndreas Gohr { 376cf2dcf1bSAndreas Gohr $this->loadRemoteInfo(); 377cf2dcf1bSAndreas Gohr return $this->remoteInfo['bundled'] ?? in_array( 378cf2dcf1bSAndreas Gohr $this->getId(), 379cf2dcf1bSAndreas Gohr [ 380cf2dcf1bSAndreas Gohr 'authad', 381cf2dcf1bSAndreas Gohr 'authldap', 382cf2dcf1bSAndreas Gohr 'authpdo', 383cf2dcf1bSAndreas Gohr 'authplain', 384cf2dcf1bSAndreas Gohr 'acl', 385cf2dcf1bSAndreas Gohr 'config', 386cf2dcf1bSAndreas Gohr 'extension', 387cf2dcf1bSAndreas Gohr 'info', 388cf2dcf1bSAndreas Gohr 'popularity', 389cf2dcf1bSAndreas Gohr 'revert', 390cf2dcf1bSAndreas Gohr 'safefnrecode', 391cf2dcf1bSAndreas Gohr 'styling', 392cf2dcf1bSAndreas Gohr 'testing', 393cf2dcf1bSAndreas Gohr 'usermanager', 394cf2dcf1bSAndreas Gohr 'logviewer', 395cf2dcf1bSAndreas Gohr 'template:dokuwiki' 396cf2dcf1bSAndreas Gohr ] 397cf2dcf1bSAndreas Gohr ); 398cf2dcf1bSAndreas Gohr } 399cf2dcf1bSAndreas Gohr 400cf2dcf1bSAndreas Gohr /** 401cf2dcf1bSAndreas Gohr * Is the extension protected against any modification (disable/uninstall) 402cf2dcf1bSAndreas Gohr * 403cf2dcf1bSAndreas Gohr * @return bool if the extension is protected 404cf2dcf1bSAndreas Gohr */ 405cf2dcf1bSAndreas Gohr public function isProtected() 406cf2dcf1bSAndreas Gohr { 407cf2dcf1bSAndreas Gohr // never allow deinstalling the current auth plugin: 408cf2dcf1bSAndreas Gohr global $conf; 409cf2dcf1bSAndreas Gohr if ($this->getId() == $conf['authtype']) return true; 410cf2dcf1bSAndreas Gohr 411cf2dcf1bSAndreas Gohr // FIXME disallow current template to be uninstalled 412cf2dcf1bSAndreas Gohr 413cf2dcf1bSAndreas Gohr /** @var PluginController $plugin_controller */ 414cf2dcf1bSAndreas Gohr global $plugin_controller; 415cf2dcf1bSAndreas Gohr $cascade = $plugin_controller->getCascade(); 416cf2dcf1bSAndreas Gohr return ($cascade['protected'][$this->getId()] ?? false); 417cf2dcf1bSAndreas Gohr } 418cf2dcf1bSAndreas Gohr 419cf2dcf1bSAndreas Gohr /** 420cf2dcf1bSAndreas Gohr * Is the extension installed in the correct directory? 421cf2dcf1bSAndreas Gohr * 422cf2dcf1bSAndreas Gohr * @return bool 423cf2dcf1bSAndreas Gohr */ 424cf2dcf1bSAndreas Gohr public function isInWrongFolder() 425cf2dcf1bSAndreas Gohr { 426*4fd6a1d7SAndreas Gohr if(!$this->isInstalled()) return false; 427cf2dcf1bSAndreas Gohr return $this->getInstallDir() != $this->currentDir; 428cf2dcf1bSAndreas Gohr } 429cf2dcf1bSAndreas Gohr 430cf2dcf1bSAndreas Gohr /** 431cf2dcf1bSAndreas Gohr * Is the extension enabled? 432cf2dcf1bSAndreas Gohr * 433cf2dcf1bSAndreas Gohr * @return bool 434cf2dcf1bSAndreas Gohr */ 435cf2dcf1bSAndreas Gohr public function isEnabled() 436cf2dcf1bSAndreas Gohr { 437cf2dcf1bSAndreas Gohr global $conf; 438cf2dcf1bSAndreas Gohr if ($this->isTemplate()) { 439cf2dcf1bSAndreas Gohr return ($conf['template'] == $this->getBase()); 440cf2dcf1bSAndreas Gohr } 441cf2dcf1bSAndreas Gohr 442cf2dcf1bSAndreas Gohr /* @var PluginController $plugin_controller */ 443cf2dcf1bSAndreas Gohr global $plugin_controller; 444cf2dcf1bSAndreas Gohr return $plugin_controller->isEnabled($this->base); 445cf2dcf1bSAndreas Gohr } 446cf2dcf1bSAndreas Gohr 447160d3688SAndreas Gohr /** 448160d3688SAndreas Gohr * Has the download URL changed since the last download? 449160d3688SAndreas Gohr * 450160d3688SAndreas Gohr * @return bool 451160d3688SAndreas Gohr */ 452160d3688SAndreas Gohr public function hasChangedURL() 453160d3688SAndreas Gohr { 454*4fd6a1d7SAndreas Gohr $last = $this->getManager()->getDownloadURL(); 455160d3688SAndreas Gohr if(!$last) return false; 456160d3688SAndreas Gohr return $last !== $this->getDownloadURL(); 457160d3688SAndreas Gohr } 458160d3688SAndreas Gohr 459160d3688SAndreas Gohr /** 460160d3688SAndreas Gohr * Is an update available for this extension? 461160d3688SAndreas Gohr * 462160d3688SAndreas Gohr * @return bool 463160d3688SAndreas Gohr */ 464*4fd6a1d7SAndreas Gohr public function isUpdateAvailable() 465160d3688SAndreas Gohr { 466160d3688SAndreas Gohr if($this->isBundled()) return false; // bundled extensions are never updated 467160d3688SAndreas Gohr $self = $this->getInstalledVersion(); 468160d3688SAndreas Gohr $remote = $this->getLastUpdate(); 469160d3688SAndreas Gohr return $self < $remote; 470160d3688SAndreas Gohr } 471160d3688SAndreas Gohr 472cf2dcf1bSAndreas Gohr // endregion 473cf2dcf1bSAndreas Gohr 4747c9966a5SAndreas Gohr // region Remote Info 4757c9966a5SAndreas Gohr 4767c9966a5SAndreas Gohr /** 4777c9966a5SAndreas Gohr * Get the date of the last available update 4787c9966a5SAndreas Gohr * 4797c9966a5SAndreas Gohr * @return string yyyy-mm-dd 4807c9966a5SAndreas Gohr */ 4817c9966a5SAndreas Gohr public function getLastUpdate() 4827c9966a5SAndreas Gohr { 4837c9966a5SAndreas Gohr return $this->getRemoteTag('lastupdate'); 4847c9966a5SAndreas Gohr } 4857c9966a5SAndreas Gohr 4867c9966a5SAndreas Gohr /** 4877c9966a5SAndreas Gohr * Get a list of tags this extension is tagged with at dokuwiki.org 4887c9966a5SAndreas Gohr * 4897c9966a5SAndreas Gohr * @return string[] 4907c9966a5SAndreas Gohr */ 4917c9966a5SAndreas Gohr public function getTags() 4927c9966a5SAndreas Gohr { 4937c9966a5SAndreas Gohr return $this->getRemoteTag('tags', []); 4947c9966a5SAndreas Gohr } 4957c9966a5SAndreas Gohr 4967c9966a5SAndreas Gohr /** 4977c9966a5SAndreas Gohr * Get the popularity of the extension 4987c9966a5SAndreas Gohr * 4997c9966a5SAndreas Gohr * This is a float between 0 and 1 5007c9966a5SAndreas Gohr * 5017c9966a5SAndreas Gohr * @return float 5027c9966a5SAndreas Gohr */ 5037c9966a5SAndreas Gohr public function getPopularity() 5047c9966a5SAndreas Gohr { 5057c9966a5SAndreas Gohr return (float)$this->getRemoteTag('popularity', 0); 5067c9966a5SAndreas Gohr } 5077c9966a5SAndreas Gohr 5087c9966a5SAndreas Gohr /** 5097c9966a5SAndreas Gohr * Get the text of the update message if there is any 5107c9966a5SAndreas Gohr * 5117c9966a5SAndreas Gohr * @return string 5127c9966a5SAndreas Gohr */ 5137c9966a5SAndreas Gohr public function getUpdateMessage() 5147c9966a5SAndreas Gohr { 5157c9966a5SAndreas Gohr return $this->getRemoteTag('updatemessage'); 5167c9966a5SAndreas Gohr } 5177c9966a5SAndreas Gohr 5187c9966a5SAndreas Gohr /** 5197c9966a5SAndreas Gohr * Get the text of the security warning if there is any 5207c9966a5SAndreas Gohr * 5217c9966a5SAndreas Gohr * @return string 5227c9966a5SAndreas Gohr */ 5237c9966a5SAndreas Gohr public function getSecurityWarning() 5247c9966a5SAndreas Gohr { 5257c9966a5SAndreas Gohr return $this->getRemoteTag('securitywarning'); 5267c9966a5SAndreas Gohr } 5277c9966a5SAndreas Gohr 5287c9966a5SAndreas Gohr /** 5297c9966a5SAndreas Gohr * Get the text of the security issue if there is any 5307c9966a5SAndreas Gohr * 5317c9966a5SAndreas Gohr * @return string 5327c9966a5SAndreas Gohr */ 5337c9966a5SAndreas Gohr public function getSecurityIssue() 5347c9966a5SAndreas Gohr { 5357c9966a5SAndreas Gohr return $this->getRemoteTag('securityissue'); 5367c9966a5SAndreas Gohr } 5377c9966a5SAndreas Gohr 5387c9966a5SAndreas Gohr /** 5397c9966a5SAndreas Gohr * Get the URL of the screenshot of the extension if there is any 5407c9966a5SAndreas Gohr * 5417c9966a5SAndreas Gohr * @return string 5427c9966a5SAndreas Gohr */ 5437c9966a5SAndreas Gohr public function getScreenshotURL() 5447c9966a5SAndreas Gohr { 5457c9966a5SAndreas Gohr return $this->getRemoteTag('screenshoturl'); 5467c9966a5SAndreas Gohr } 5477c9966a5SAndreas Gohr 5487c9966a5SAndreas Gohr /** 5497c9966a5SAndreas Gohr * Get the URL of the thumbnail of the extension if there is any 5507c9966a5SAndreas Gohr * 5517c9966a5SAndreas Gohr * @return string 5527c9966a5SAndreas Gohr */ 5537c9966a5SAndreas Gohr public function getThumbnailURL() 5547c9966a5SAndreas Gohr { 5557c9966a5SAndreas Gohr return $this->getRemoteTag('thumbnailurl'); 5567c9966a5SAndreas Gohr } 5577c9966a5SAndreas Gohr 5587c9966a5SAndreas Gohr /** 5597c9966a5SAndreas Gohr * Get the download URL of the extension if there is any 5607c9966a5SAndreas Gohr * 5617c9966a5SAndreas Gohr * @return string 5627c9966a5SAndreas Gohr */ 5637c9966a5SAndreas Gohr public function getDownloadURL() 5647c9966a5SAndreas Gohr { 5657c9966a5SAndreas Gohr return $this->getRemoteTag('downloadurl'); 5667c9966a5SAndreas Gohr } 5677c9966a5SAndreas Gohr 5687c9966a5SAndreas Gohr /** 5697c9966a5SAndreas Gohr * Get the bug tracker URL of the extension if there is any 5707c9966a5SAndreas Gohr * 5717c9966a5SAndreas Gohr * @return string 5727c9966a5SAndreas Gohr */ 5737c9966a5SAndreas Gohr public function getBugtrackerURL() 5747c9966a5SAndreas Gohr { 5757c9966a5SAndreas Gohr return $this->getRemoteTag('bugtracker'); 5767c9966a5SAndreas Gohr } 5777c9966a5SAndreas Gohr 5787c9966a5SAndreas Gohr /** 5797c9966a5SAndreas Gohr * Get the URL of the source repository if there is any 5807c9966a5SAndreas Gohr * 5817c9966a5SAndreas Gohr * @return string 5827c9966a5SAndreas Gohr */ 5837c9966a5SAndreas Gohr public function getSourcerepoURL() 5847c9966a5SAndreas Gohr { 5857c9966a5SAndreas Gohr return $this->getRemoteTag('sourcerepo'); 5867c9966a5SAndreas Gohr } 5877c9966a5SAndreas Gohr 5887c9966a5SAndreas Gohr /** 5897c9966a5SAndreas Gohr * Get the donation URL of the extension if there is any 5907c9966a5SAndreas Gohr * 5917c9966a5SAndreas Gohr * @return string 5927c9966a5SAndreas Gohr */ 5937c9966a5SAndreas Gohr public function getDonationURL() 5947c9966a5SAndreas Gohr { 5957c9966a5SAndreas Gohr return $this->getRemoteTag('donationurl'); 5967c9966a5SAndreas Gohr } 5977c9966a5SAndreas Gohr 598*4fd6a1d7SAndreas Gohr /** 599*4fd6a1d7SAndreas Gohr * Get a list of extensions that are similar to this one 600*4fd6a1d7SAndreas Gohr * 601*4fd6a1d7SAndreas Gohr * @return string[] 602*4fd6a1d7SAndreas Gohr */ 603*4fd6a1d7SAndreas Gohr public function getSimilarList() 604*4fd6a1d7SAndreas Gohr { 605*4fd6a1d7SAndreas Gohr return $this->getRemoteTag('similar', []); 606*4fd6a1d7SAndreas Gohr } 607*4fd6a1d7SAndreas Gohr 608*4fd6a1d7SAndreas Gohr /** 609*4fd6a1d7SAndreas Gohr * Get a list of extensions that are marked as conflicting with this one 610*4fd6a1d7SAndreas Gohr * 611*4fd6a1d7SAndreas Gohr * @return string[] 612*4fd6a1d7SAndreas Gohr */ 613*4fd6a1d7SAndreas Gohr public function getConflictList() 614*4fd6a1d7SAndreas Gohr { 615*4fd6a1d7SAndreas Gohr return $this->getRemoteTag('conflicts', []); 616*4fd6a1d7SAndreas Gohr } 617*4fd6a1d7SAndreas Gohr 618*4fd6a1d7SAndreas Gohr /** 619*4fd6a1d7SAndreas Gohr * Get a list of DokuWiki versions this plugin is marked as compatible with 620*4fd6a1d7SAndreas Gohr * 621*4fd6a1d7SAndreas Gohr * @return string[][] date -> version 622*4fd6a1d7SAndreas Gohr */ 623*4fd6a1d7SAndreas Gohr public function getCompatibleVersions() 624*4fd6a1d7SAndreas Gohr { 625*4fd6a1d7SAndreas Gohr return $this->getRemoteTag('compatible', []); 626*4fd6a1d7SAndreas Gohr } 627*4fd6a1d7SAndreas Gohr 6287c9966a5SAndreas Gohr // endregion 6297c9966a5SAndreas Gohr 630cf2dcf1bSAndreas Gohr // region Actions 631cf2dcf1bSAndreas Gohr 632cf2dcf1bSAndreas Gohr /** 633cf2dcf1bSAndreas Gohr * Install or update the extension 634cf2dcf1bSAndreas Gohr * 635cf2dcf1bSAndreas Gohr * @throws Exception 636cf2dcf1bSAndreas Gohr */ 637cf2dcf1bSAndreas Gohr public function installOrUpdate() 638cf2dcf1bSAndreas Gohr { 639cf2dcf1bSAndreas Gohr $installer = new Installer(true); 640160d3688SAndreas Gohr $installer->installExtension($this); 641cf2dcf1bSAndreas Gohr } 642cf2dcf1bSAndreas Gohr 643cf2dcf1bSAndreas Gohr /** 644cf2dcf1bSAndreas Gohr * Uninstall the extension 645cf2dcf1bSAndreas Gohr * @throws Exception 646cf2dcf1bSAndreas Gohr */ 647cf2dcf1bSAndreas Gohr public function uninstall() 648cf2dcf1bSAndreas Gohr { 649cf2dcf1bSAndreas Gohr $installer = new Installer(true); 650cf2dcf1bSAndreas Gohr $installer->uninstall($this); 651cf2dcf1bSAndreas Gohr } 652cf2dcf1bSAndreas Gohr 653cf2dcf1bSAndreas Gohr /** 654cf2dcf1bSAndreas Gohr * Enable the extension 655cf2dcf1bSAndreas Gohr * @todo I'm unsure if this code should be here or part of Installer 656cf2dcf1bSAndreas Gohr * @throws Exception 657cf2dcf1bSAndreas Gohr */ 658cf2dcf1bSAndreas Gohr public function enable() 659cf2dcf1bSAndreas Gohr { 660cf2dcf1bSAndreas Gohr if ($this->isTemplate()) throw new Exception('notimplemented'); 661160d3688SAndreas Gohr if (!$this->isInstalled()) throw new Exception('error_notinstalled', [$this->getId()]); 662160d3688SAndreas Gohr if ($this->isEnabled()) throw new Exception('error_alreadyenabled', [$this->getId()]); 663cf2dcf1bSAndreas Gohr 664cf2dcf1bSAndreas Gohr /* @var PluginController $plugin_controller */ 665cf2dcf1bSAndreas Gohr global $plugin_controller; 666cf2dcf1bSAndreas Gohr if (!$plugin_controller->enable($this->base)) { 667cf2dcf1bSAndreas Gohr throw new Exception('pluginlistsaveerror'); 668cf2dcf1bSAndreas Gohr } 669cf2dcf1bSAndreas Gohr Installer::purgeCache(); 670cf2dcf1bSAndreas Gohr } 671cf2dcf1bSAndreas Gohr 672cf2dcf1bSAndreas Gohr /** 673cf2dcf1bSAndreas Gohr * Disable the extension 674cf2dcf1bSAndreas Gohr * @todo I'm unsure if this code should be here or part of Installer 675cf2dcf1bSAndreas Gohr * @throws Exception 676cf2dcf1bSAndreas Gohr */ 677cf2dcf1bSAndreas Gohr public function disable() 678cf2dcf1bSAndreas Gohr { 679cf2dcf1bSAndreas Gohr if ($this->isTemplate()) throw new Exception('notimplemented'); 680160d3688SAndreas Gohr if (!$this->isInstalled()) throw new Exception('error_notinstalled', [$this->getId()]); 681160d3688SAndreas Gohr if (!$this->isEnabled()) throw new Exception('error_alreadydisabled', [$this->getId()]); 682160d3688SAndreas Gohr if ($this->isProtected()) throw new Exception('error_disable_protected', [$this->getId()]); 683cf2dcf1bSAndreas Gohr 684cf2dcf1bSAndreas Gohr /* @var PluginController $plugin_controller */ 685cf2dcf1bSAndreas Gohr global $plugin_controller; 686cf2dcf1bSAndreas Gohr if (!$plugin_controller->disable($this->base)) { 687cf2dcf1bSAndreas Gohr throw new Exception('pluginlistsaveerror'); 688cf2dcf1bSAndreas Gohr } 689cf2dcf1bSAndreas Gohr Installer::purgeCache(); 690cf2dcf1bSAndreas Gohr } 691cf2dcf1bSAndreas Gohr 692cf2dcf1bSAndreas Gohr // endregion 693cf2dcf1bSAndreas Gohr 694cf2dcf1bSAndreas Gohr // region Meta Data Management 695cf2dcf1bSAndreas Gohr 696cf2dcf1bSAndreas Gohr /** 6977c9966a5SAndreas Gohr * Access the Manager for this extension 698cf2dcf1bSAndreas Gohr * 6997c9966a5SAndreas Gohr * @return Manager 700cf2dcf1bSAndreas Gohr */ 7017c9966a5SAndreas Gohr public function getManager() 702cf2dcf1bSAndreas Gohr { 7037c9966a5SAndreas Gohr if ($this->manager === null) { 7047c9966a5SAndreas Gohr $this->manager = new Manager($this); 705cf2dcf1bSAndreas Gohr } 7067c9966a5SAndreas Gohr return $this->manager; 707cf2dcf1bSAndreas Gohr } 708cf2dcf1bSAndreas Gohr 709cf2dcf1bSAndreas Gohr /** 710cf2dcf1bSAndreas Gohr * Reads the info file of the extension if available and fills the localInfo array 711cf2dcf1bSAndreas Gohr */ 712cf2dcf1bSAndreas Gohr protected function readLocalInfo() 713cf2dcf1bSAndreas Gohr { 714a1e045f7SAndreas Gohr if (!$this->getCurrentDir()) return; 715cf2dcf1bSAndreas Gohr $file = $this->currentDir . '/' . $this->type . '.info.txt'; 716cf2dcf1bSAndreas Gohr if (!is_readable($file)) return; 717cf2dcf1bSAndreas Gohr $this->localInfo = confToHash($file, true); 718cf2dcf1bSAndreas Gohr $this->localInfo = array_filter($this->localInfo); // remove all falsy keys 719cf2dcf1bSAndreas Gohr } 720cf2dcf1bSAndreas Gohr 721cf2dcf1bSAndreas Gohr /** 722cf2dcf1bSAndreas Gohr * Fetches the remote info from the repository 723cf2dcf1bSAndreas Gohr * 724cf2dcf1bSAndreas Gohr * This ignores any errors coming from the repository and just sets the remoteInfo to an empty array in that case 725cf2dcf1bSAndreas Gohr */ 726cf2dcf1bSAndreas Gohr protected function loadRemoteInfo() 727cf2dcf1bSAndreas Gohr { 728cf2dcf1bSAndreas Gohr if ($this->remoteInfo) return; 729cf2dcf1bSAndreas Gohr $remote = Repository::getInstance(); 730cf2dcf1bSAndreas Gohr try { 731cf2dcf1bSAndreas Gohr $this->remoteInfo = (array)$remote->getExtensionData($this->getId()); 732cf2dcf1bSAndreas Gohr } catch (Exception $e) { 733cf2dcf1bSAndreas Gohr $this->remoteInfo = []; 734cf2dcf1bSAndreas Gohr } 735cf2dcf1bSAndreas Gohr } 736cf2dcf1bSAndreas Gohr 737cf2dcf1bSAndreas Gohr /** 738cf2dcf1bSAndreas Gohr * Read information from either local or remote info 739cf2dcf1bSAndreas Gohr * 7407c9966a5SAndreas Gohr * Always prefers local info over remote info. Giving multiple keys is useful when the 7417c9966a5SAndreas Gohr * key has been renamed in the past or if local and remote keys might differ. 742cf2dcf1bSAndreas Gohr * 743cf2dcf1bSAndreas Gohr * @param string|string[] $tag one or multiple keys to check 744cf2dcf1bSAndreas Gohr * @param mixed $default 745cf2dcf1bSAndreas Gohr * @return mixed 746cf2dcf1bSAndreas Gohr */ 747cf2dcf1bSAndreas Gohr protected function getTag($tag, $default = '') 748cf2dcf1bSAndreas Gohr { 749cf2dcf1bSAndreas Gohr foreach ((array)$tag as $t) { 750cf2dcf1bSAndreas Gohr if (isset($this->localInfo[$t])) return $this->localInfo[$t]; 751cf2dcf1bSAndreas Gohr } 7527c9966a5SAndreas Gohr 7537c9966a5SAndreas Gohr return $this->getRemoteTag($tag, $default); 7547c9966a5SAndreas Gohr } 7557c9966a5SAndreas Gohr 7567c9966a5SAndreas Gohr /** 7577c9966a5SAndreas Gohr * Read information from remote info 7587c9966a5SAndreas Gohr * 7597c9966a5SAndreas Gohr * @param string|string[] $tag one or mutiple keys to check 7607c9966a5SAndreas Gohr * @param mixed $default 7617c9966a5SAndreas Gohr * @return mixed 7627c9966a5SAndreas Gohr */ 7637c9966a5SAndreas Gohr protected function getRemoteTag($tag, $default = '') 7647c9966a5SAndreas Gohr { 765cf2dcf1bSAndreas Gohr $this->loadRemoteInfo(); 766cf2dcf1bSAndreas Gohr foreach ((array)$tag as $t) { 767cf2dcf1bSAndreas Gohr if (isset($this->remoteInfo[$t])) return $this->remoteInfo[$t]; 768cf2dcf1bSAndreas Gohr } 769cf2dcf1bSAndreas Gohr return $default; 770cf2dcf1bSAndreas Gohr } 771cf2dcf1bSAndreas Gohr 772cf2dcf1bSAndreas Gohr // endregion 773a1e045f7SAndreas Gohr 774a1e045f7SAndreas Gohr // region utilities 775a1e045f7SAndreas Gohr 776a1e045f7SAndreas Gohr /** 777a1e045f7SAndreas Gohr * Convert an extension id to a type and base 778a1e045f7SAndreas Gohr * 779a1e045f7SAndreas Gohr * @param string $id 780a1e045f7SAndreas Gohr * @return array [type, base] 781a1e045f7SAndreas Gohr */ 782a1e045f7SAndreas Gohr protected function idToTypeBase($id) 783a1e045f7SAndreas Gohr { 784a1e045f7SAndreas Gohr [$type, $base] = sexplode(':', $id, 2); 785a1e045f7SAndreas Gohr if ($base === null) { 786a1e045f7SAndreas Gohr $base = $type; 787a1e045f7SAndreas Gohr $type = self::TYPE_PLUGIN; 788a1e045f7SAndreas Gohr } elseif ($type === self::TYPE_TEMPLATE) { 789a1e045f7SAndreas Gohr $type = self::TYPE_TEMPLATE; 790a1e045f7SAndreas Gohr } else { 791a1e045f7SAndreas Gohr throw new RuntimeException('Invalid extension id: ' . $id); 792a1e045f7SAndreas Gohr } 793a1e045f7SAndreas Gohr 794a1e045f7SAndreas Gohr return [$type, $base]; 795a1e045f7SAndreas Gohr } 7967c9966a5SAndreas Gohr /** 7977c9966a5SAndreas Gohr * @return string 7987c9966a5SAndreas Gohr */ 7997c9966a5SAndreas Gohr public function __toString() 8007c9966a5SAndreas Gohr { 8017c9966a5SAndreas Gohr return $this->getId(); 8027c9966a5SAndreas Gohr } 8037c9966a5SAndreas Gohr 804a1e045f7SAndreas Gohr // endregion 805cf2dcf1bSAndreas Gohr} 806