dokuwiki/lib/plugins/extension/Extension.php
<?php
namespace dokuwiki\plugin\extension;
use dokuwiki\Extension\PluginController;
use dokuwiki\Utf8\PhpString;
use RuntimeException;
class Extension
{
public const TYPE_PLUGIN = 'plugin';
public const TYPE_TEMPLATE = 'template';
/** @var string[] The types the API uses for plugin components */
public const COMPONENT_TYPES = [
1 => 'Syntax',
2 => 'Admin',
4 => 'Action',
8 => 'Render',
16 => 'Helper',
32 => 'Template',
64 => 'Remote',
128 => 'Auth',
256 => 'CLI',
512 => 'CSS/JS-only',
];
/** @var string "plugin"|"template" */
protected string $type = self::TYPE_PLUGIN;
/** @var string The base name of this extension */
protected string $base;
/** @var string The current location of this extension */
protected string $currentDir = '';
/** @var array The local info array of the extension */
protected array $localInfo = [];
/** @var array The remote info array of the extension */
protected array $remoteInfo = [];
/** @var Manager|null The manager for this extension */
protected ?Manager $manager = null;
// region Constructors
/**
* The main constructor is private to force the use of the factory methods
*/
protected function __construct()
{
}
/**
* Initializes an extension from an id
*
* @param string $id The id of the extension
* @return Extension
*/
public static function createFromId($id)
{
$extension = new self();
$extension->initFromId($id);
return $extension;
}
protected function initFromId($id)
{
[$type, $base] = $this->idToTypeBase($id);
$this->type = $type;
$this->base = $base;
$this->readLocalInfo();
}
/**
* Initializes an extension from a directory
*
* The given directory might be the one where the extension has already been installed to
* or it might be the extracted source in some temporary directory.
*
* @param string $dir Where the extension code is currently located
* @param string|null $type TYPE_PLUGIN|TYPE_TEMPLATE, null for auto-detection
* @param string $base The base name of the extension, null for auto-detection
* @return Extension
*/
public static function createFromDirectory($dir, $type = null, $base = null)
{
$extension = new self();
$extension->initFromDirectory($dir, $type, $base);
return $extension;
}
protected function initFromDirectory($dir, $type = null, $base = null)
{
if (!is_dir($dir)) throw new RuntimeException('Directory not found: ' . $dir);
$this->currentDir = fullpath($dir);
if ($type === null || $type === self::TYPE_TEMPLATE) {
if (
file_exists($dir . '/template.info.txt') ||
file_exists($dir . '/style.ini') ||
file_exists($dir . '/main.php') ||
file_exists($dir . '/detail.php') ||
file_exists($dir . '/mediamanager.php')
) {
$this->type = self::TYPE_TEMPLATE;
}
} else {
$this->type = self::TYPE_PLUGIN;
}
$this->readLocalInfo();
if ($base !== null) {
$this->base = $base;
} elseif (isset($this->localInfo['base'])) {
$this->base = $this->localInfo['base'];
} else {
$this->base = basename($dir);
}
}
/**
* Initializes an extension from remote data
*
* @param array $data The data as returned by the repository api
* @return Extension
*/
public static function createFromRemoteData($data)
{
$extension = new self();
$extension->initFromRemoteData($data);
return $extension;
}
protected function initFromRemoteData($data)
{
if (!isset($data['plugin'])) throw new RuntimeException('Invalid remote data');
[$type, $base] = $this->idToTypeBase($data['plugin']);
$this->remoteInfo = $data;
$this->type = $type;
$this->base = $base;
if ($this->isInstalled()) {
$this->currentDir = $this->getInstallDir();
$this->readLocalInfo();
}
}
// endregion
// region Getters
/**
* @param bool $wrap If true, the id is wrapped in backticks
* @return string The extension id (same as base but prefixed with "template:" for templates)
*/
public function getId($wrap = false)
{
if ($this->type === self::TYPE_TEMPLATE) {
$id = self::TYPE_TEMPLATE . ':' . $this->base;
} else {
$id = $this->base;
}
if ($wrap) $id = "`$id`";
return $id;
}
/**
* Get the base name of this extension
*
* @return string
*/
public function getBase()
{
return $this->base;
}
/**
* Get the type of the extension
*
* @return string "plugin"|"template"
*/
public function getType()
{
return $this->type;
}
/**
* The current directory of the extension
*
* @return string|null
*/
public function getCurrentDir()
{
// recheck that the current currentDir is still valid
if ($this->currentDir && !is_dir($this->currentDir)) {
$this->currentDir = '';
}
// if the extension is installed, then the currentDir is the install dir!
if (!$this->currentDir && $this->isInstalled()) {
$this->currentDir = $this->getInstallDir();
}
return $this->currentDir;
}
/**
* Get the directory where this extension should be installed in
*
* Note: this does not mean that the extension is actually installed there
*
* @return string
*/
public function getInstallDir()
{
if ($this->isTemplate()) {
$dir = dirname(tpl_incdir()) . '/' . $this->base;
} else {
$dir = DOKU_PLUGIN . $this->base;
}
return fullpath($dir);
}
/**
* Get the display name of the extension
*
* @return string
*/
public function getDisplayName()
{
return $this->getTag('name', PhpString::ucwords($this->getBase() . ' ' . $this->getType()));
}
/**
* Get the author name of the extension
*
* @return string Returns an empty string if the author info is missing
*/
public function getAuthor()
{
return $this->getTag('author');
}
/**
* Get the email of the author of the extension if there is any
*
* @return string Returns an empty string if the email info is missing
*/
public function getEmail()
{
// email is only in the local data
return $this->localInfo['email'] ?? '';
}
/**
* Get the email id, i.e. the md5sum of the email
*
* @return string Empty string if no email is available
*/
public function getEmailID()
{
if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
return '';
}
/**
* Get the description of the extension
*
* @return string Empty string if no description is available
*/
public function getDescription()
{
return $this->getTag(['desc', 'description']);
}
/**
* Get the URL of the extension, usually a page on dokuwiki.org
*
* @return string
*/
public function getURL()
{
return $this->getTag(
'url',
'https://www.dokuwiki.org/' .
($this->isTemplate() ? 'template' : 'plugin') . ':' . $this->getBase()
);
}
/**
* Get the version of the extension that is actually installed
*
* Returns an empty string if the version is not available
*
* @return string
*/
public function getInstalledVersion()
{
return $this->localInfo['date'] ?? '';
}
/**
* Get the types of components this extension provides
*
* @return array int -> type
*/
public function getComponentTypes()
{
// for installed extensions we can check the files
if ($this->isInstalled()) {
if ($this->isTemplate()) {
return ['Template'];
} else {
$types = [];
foreach (self::COMPONENT_TYPES as $type) {
$check = strtolower($type);
if (
file_exists($this->getInstallDir() . '/' . $check . '.php') ||
is_dir($this->getInstallDir() . '/' . $check)
) {
$types[] = $type;
}
}
return $types;
}
}
// still, here? use the remote info
return $this->getTag('types', []);
}
/**
* Get a list of extension ids this extension depends on
*
* @return string[]
*/
public function getDependencyList()
{
return $this->getTag('depends', []);
}
/**
* Get a list of extensions that are currently installed, enabled and depend on this extension
*
* @return Extension[]
*/
public function getDependants()
{
$local = new Local();
$extensions = $local->getExtensions();
$dependants = [];
foreach ($extensions as $extension) {
if (
in_array($this->getId(), $extension->getDependencyList()) &&
$extension->isEnabled()
) {
$dependants[$extension->getId()] = $extension;
}
}
return $dependants;
}
/**
* Return the minimum PHP version required by the extension
*
* Empty if not set
*
* @return string
*/
public function getMinimumPHPVersion()
{
return $this->getTag('phpmin', '');
}
/**
* Return the minimum PHP version supported by the extension
*
* @return string
*/
public function getMaximumPHPVersion()
{
return $this->getTag('phpmax', '');
}
/**
* Is this extension a template?
*
* @return bool false if it is a plugin
*/
public function isTemplate()
{
return $this->type === self::TYPE_TEMPLATE;
}
/**
* Is the extension installed locally?
*
* @return bool
*/
public function isInstalled()
{
return is_dir($this->getInstallDir());
}
/**
* Is the extension under git control?
*
* @return bool
*/
public function isGitControlled()
{
if (!$this->isInstalled()) return false;
return file_exists($this->getInstallDir() . '/.git');
}
/**
* If the extension is bundled
*
* @return bool If the extension is bundled
*/
public function isBundled()
{
$this->loadRemoteInfo();
return $this->remoteInfo['bundled'] ?? in_array(
$this->getId(),
[
'authad',
'authldap',
'authpdo',
'authplain',
'acl',
'config',
'extension',
'info',
'popularity',
'revert',
'safefnrecode',
'styling',
'testing',
'usermanager',
'logviewer',
'template:dokuwiki'
]
);
}
/**
* Is the extension protected against any modification (disable/uninstall)
*
* @return bool if the extension is protected
*/
public function isProtected()
{
// never allow deinstalling the current auth plugin:
global $conf;
if ($this->getId() == $conf['authtype']) return true;
// disallow current template to be uninstalled
if ($this->isTemplate() && ($this->getBase() === $conf['template'])) return true;
/** @var PluginController $plugin_controller */
global $plugin_controller;
$cascade = $plugin_controller->getCascade();
return ($cascade['protected'][$this->getId()] ?? false);
}
/**
* Is the extension installed in the correct directory?
*
* @return bool
*/
public function isInWrongFolder()
{
if (!$this->isInstalled()) return false;
return $this->getInstallDir() != $this->currentDir;
}
/**
* Is the extension enabled?
*
* @return bool
*/
public function isEnabled()
{
global $conf;
if ($this->isTemplate()) {
return ($conf['template'] == $this->getBase());
}
/* @var PluginController $plugin_controller */
global $plugin_controller;
return $plugin_controller->isEnabled($this->base);
}
/**
* Has the download URL changed since the last download?
*
* @return bool
*/
public function hasChangedURL()
{
$last = $this->getManager()->getDownloadURL();
if (!$last) return false;
return $last !== $this->getDownloadURL();
}
/**
* Is an update available for this extension?
*
* @return bool
*/
public function isUpdateAvailable()
{
if ($this->isBundled()) return false; // bundled extensions are never updated
$self = $this->getInstalledVersion();
$remote = $this->getLastUpdate();
return $self < $remote;
}
// endregion
// region Remote Info
/**
* Get the date of the last available update
*
* @return string yyyy-mm-dd
*/
public function getLastUpdate()
{
return $this->getRemoteTag('lastupdate');
}
/**
* Get a list of tags this extension is tagged with at dokuwiki.org
*
* @return string[]
*/
public function getTags()
{
return $this->getRemoteTag('tags', []);
}
/**
* Get the popularity of the extension
*
* This is a float between 0 and 1
*
* @return float
*/
public function getPopularity()
{
return (float)$this->getRemoteTag('popularity', 0);
}
/**
* Get the text of the update message if there is any
*
* @return string
*/
public function getUpdateMessage()
{
return $this->getRemoteTag('updatemessage');
}
/**
* Get the text of the security warning if there is any
*
* @return string
*/
public function getSecurityWarning()
{
return $this->getRemoteTag('securitywarning');
}
/**
* Get the text of the security issue if there is any
*
* @return string
*/
public function getSecurityIssue()
{
return $this->getRemoteTag('securityissue');
}
/**
* Get the URL of the screenshot of the extension if there is any
*
* @return string
*/
public function getScreenshotURL()
{
return $this->getRemoteTag('screenshoturl');
}
/**
* Get the URL of the thumbnail of the extension if there is any
*
* @return string
*/
public function getThumbnailURL()
{
return $this->getRemoteTag('thumbnailurl');
}
/**
* Get the download URL of the extension if there is any
*
* @return string
*/
public function getDownloadURL()
{
return $this->getRemoteTag('downloadurl');
}
/**
* Get the bug tracker URL of the extension if there is any
*
* @return string
*/
public function getBugtrackerURL()
{
return $this->getRemoteTag('bugtracker');
}
/**
* Get the URL of the source repository if there is any
*
* @return string
*/
public function getSourcerepoURL()
{
return $this->getRemoteTag('sourcerepo');
}
/**
* Get the donation URL of the extension if there is any
*
* @return string
*/
public function getDonationURL()
{
return $this->getRemoteTag('donationurl');
}
/**
* Get a list of extensions that are similar to this one
*
* @return string[]
*/
public function getSimilarList()
{
return $this->getRemoteTag('similar', []);
}
/**
* Get a list of extensions that are marked as conflicting with this one
*
* @return string[]
*/
public function getConflictList()
{
return $this->getRemoteTag('conflicts', []);
}
/**
* Get a list of DokuWiki versions this plugin is marked as compatible with
*
* @return string[][] date -> version
*/
public function getCompatibleVersions()
{
return $this->getRemoteTag('compatible', []);
}
// endregion
// region Actions
/**
* Install or update the extension
*
* @throws Exception
*/
public function installOrUpdate()
{
$installer = new Installer(true);
$installer->installExtension($this);
}
/**
* Uninstall the extension
* @throws Exception
*/
public function uninstall()
{
$installer = new Installer(true);
$installer->uninstall($this);
}
/**
* Toggle the extension between enabled and disabled
* @return void
* @throws Exception
*/
public function toggle()
{
if ($this->isEnabled()) {
$this->disable();
} else {
$this->enable();
}
}
/**
* Enable the extension
*
* @throws Exception
*/
public function enable()
{
(new Installer())->enable($this);
}
/**
* Disable the extension
*
* @throws Exception
*/
public function disable()
{
(new Installer())->disable($this);
}
// endregion
// region Meta Data Management
/**
* Access the Manager for this extension
*
* @return Manager
*/
public function getManager()
{
if (!$this->manager instanceof Manager) {
$this->manager = new Manager($this);
}
return $this->manager;
}
/**
* Reads the info file of the extension if available and fills the localInfo array
*/
protected function readLocalInfo()
{
if (!$this->getCurrentDir()) return;
$file = $this->currentDir . '/' . $this->type . '.info.txt';
if (!is_readable($file)) return;
$this->localInfo = confToHash($file, true);
$this->localInfo = array_filter($this->localInfo); // remove all falsy keys
}
/**
* Fetches the remote info from the repository
*
* This ignores any errors coming from the repository and just sets the remoteInfo to an empty array in that case
*/
protected function loadRemoteInfo()
{
if ($this->remoteInfo) return;
$remote = Repository::getInstance();
try {
$this->remoteInfo = (array)$remote->getExtensionData($this->getId());
} catch (Exception $e) {
$this->remoteInfo = [];
}
}
/**
* Read information from either local or remote info
*
* Always prefers local info over remote info. Giving multiple keys is useful when the
* key has been renamed in the past or if local and remote keys might differ.
*
* @param string|string[] $tag one or multiple keys to check
* @param mixed $default
* @return mixed
*/
protected function getTag($tag, $default = '')
{
foreach ((array)$tag as $t) {
if (isset($this->localInfo[$t])) return $this->localInfo[$t];
}
return $this->getRemoteTag($tag, $default);
}
/**
* Read information from remote info
*
* @param string|string[] $tag one or mutiple keys to check
* @param mixed $default
* @return mixed
*/
protected function getRemoteTag($tag, $default = '')
{
$this->loadRemoteInfo();
foreach ((array)$tag as $t) {
if (isset($this->remoteInfo[$t])) return $this->remoteInfo[$t];
}
return $default;
}
// endregion
// region utilities
/**
* Convert an extension id to a type and base
*
* @param string $id
* @return array [type, base]
*/
protected function idToTypeBase($id)
{
[$type, $base] = sexplode(':', $id, 2);
if ($base === null) {
$base = $type;
$type = self::TYPE_PLUGIN;
} elseif ($type === self::TYPE_TEMPLATE) {
$type = self::TYPE_TEMPLATE;
} else {
throw new RuntimeException('Invalid extension id: ' . $id);
}
return [$type, $base];
}
/**
* @return string
*/
public function __toString()
{
return $this->getId();
}
// endregion
}