symphony/lib/toolkit/class.extensionmanager.php
<?php
/**
* @package toolkit
*/
/**
* The ExtensionManager class is responsible for managing all extensions
* in Symphony. Extensions are stored on the file system in the `EXTENSIONS`
* folder. They are auto-discovered where the Extension class name is the same
* as it's folder name (excluding the extension prefix).
*/
class ExtensionManager implements FileResource
{
/**
* An array of all the objects that the Manager is responsible for.
* Defaults to an empty array.
* @var array
*/
protected static $_pool = array();
/**
* An array of all extensions whose status is enabled
* @var array
*/
private static $_enabled_extensions = array();
/**
* An array of all the subscriptions to Symphony delegates made by extensions.
* @var array
*/
private static $_subscriptions = array();
/**
* An associative array of all the extensions in `tbl_extensions` where
* the key is the extension name and the value is an array
* representation of it's accompanying database row.
* @var array
*/
private static $_extensions = array();
/**
* An associative array of all the providers from the enabled extensions.
* The key is the type of object, with the value being an associative array
* with the name, classname and path to the object
*
* @since Symphony 2.3
* @var array
*/
private static $_providers = array();
/**
* The constructor will populate the `$_subscriptions` variable from
* the `tbl_extension` and `tbl_extensions_delegates` tables.
*/
public function __construct()
{
if (empty(self::$_subscriptions) && Symphony::Database() && Symphony::Database()->isConnected()) {
$subscriptions = $this->getDelegateSubscriptions();
while ($subscription = $subscriptions->next()) {
self::$_subscriptions[$subscription['delegate']][] = $subscription;
}
}
}
public static function __getHandleFromFilename($filename)
{
return false;
}
/**
* Given a name, returns the full class name of an Extension.
* Extension use an 'extension' prefix.
*
* @param string $name
* The extension handle
* @return string
*/
public static function __getClassName($name)
{
return 'extension_' . $name;
}
/**
* Finds an Extension by name by searching the `EXTENSIONS` folder and
* returns the path to the folder.
*
* @param string $name
* The extension folder
* @return string
*/
public static function __getClassPath($name)
{
return EXTENSIONS . strtolower("/$name");
}
/**
* Given a name, return the path to the driver of the Extension.
*
* @see toolkit.ExtensionManager#__getClassPath()
* @param string $name
* The extension folder
* @return string
*/
public static function __getDriverPath($name)
{
return self::__getClassPath($name) . '/extension.driver.php';
}
/**
* This function returns an instance of an extension from it's name
*
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @throws SymphonyException
* @throws Exception
* @return Extension
*/
public static function getInstance($name)
{
return (isset(self::$_pool[$name]) ? self::$_pool[$name] : self::create($name));
}
/**
* Populates the `ExtensionManager::$_extensions` array with all the
* extensions stored in `tbl_extensions`. If `ExtensionManager::$_extensions`
* isn't empty, passing true as a parameter will force the array to update
*
* @param boolean $update
* Updates the `ExtensionManager::$_extensions` array even if it was
* populated, defaults to false.
* @throws DatabaseException
*/
protected static function buildExtensionList($update = false)
{
if (empty(self::$_extensions) || $update) {
self::$_extensions = (new ExtensionManager)
->select()
->execute()
->rowsIndexedByColumn('name');
}
}
/**
* Returns the status of an Extension given an associative array containing
* the Extension `handle` and `version` where the `version` is the file
* version, not the installed version. This function returns an array
* which may include a maximum of two statuses.
*
* @param array $about
* An associative array of the extension meta data, typically returned
* by `ExtensionManager::about()`. At the very least this array needs
* `handle` and `version` keys.
* @return array
* An array of extension statuses, with the possible values being
* `EXTENSION_ENABLED`, `EXTENSION_DISABLED`, `EXTENSION_REQUIRES_UPDATE`
* or `EXTENSION_NOT_INSTALLED`. If an extension doesn't exist,
* `EXTENSION_NOT_INSTALLED` will be returned.
*/
public static function fetchStatus($about)
{
$return = array();
static::buildExtensionList();
if (isset($about['handle']) && array_key_exists($about['handle'], self::$_extensions)) {
if (self::$_extensions[$about['handle']]['status'] == 'enabled') {
$return[] = Extension::EXTENSION_ENABLED;
} else {
$return[] = Extension::EXTENSION_DISABLED;
}
} else {
$return[] = Extension::EXTENSION_NOT_INSTALLED;
}
if (isset($about['handle'], $about['version']) && static::requiresUpdate($about['handle'], $about['version'])) {
$return[] = Extension::EXTENSION_REQUIRES_UPDATE;
}
return $return;
}
/**
* A convenience method that returns an extension version from it's name.
*
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @return string
*/
public static function fetchInstalledVersion($name)
{
static::buildExtensionList();
return (isset(self::$_extensions[$name]) ? self::$_extensions[$name]['version'] : null);
}
/**
* A convenience method that returns an extension ID from it's name.
*
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @return integer
*/
public static function fetchExtensionID($name)
{
static::buildExtensionList();
return self::$_extensions[$name]['id'];
}
/**
* Return an array all the Provider objects supplied by extensions,
* optionally filtered by a given `$type`.
*
* @since Symphony 2.3
* @todo Add information about the possible types
* @param string $type
* This will only return Providers of this type. If null, which is
* default, all providers will be returned.
* @throws Exception
* @throws SymphonyException
* @return array
* An array of objects
*/
public static function getProvidersOf($type = null)
{
// Loop over all extensions and build an array of providable objects
if (empty(self::$_providers)) {
self::$_providers = array();
foreach (self::listInstalledHandles() as $handle) {
$obj = self::getInstance($handle);
if (!method_exists($obj, 'providerOf')) {
continue;
}
$providers = $obj->providerOf();
if (empty($providers)) {
continue;
}
// For each of the matching objects (by $type), resolve the object path
self::$_providers = array_merge_recursive(self::$_providers, $obj->providerOf());
}
}
// Return an array of objects
if (is_null($type)) {
return self::$_providers;
}
if (!isset(self::$_providers[$type])) {
return array();
}
return self::$_providers[$type];
}
/**
* This function will return the `Cacheable` object with the appropriate
* caching layer for the given `$key`. This `$key` should be stored in
* the Symphony configuration in the caching group with a reference
* to the class of the caching object. If the key is not found, this
* will return a default `Cacheable` object created with the Database driver.
*
* @since Symphony 2.4
* @param string $key
* Should be a reference in the Configuration file to the Caching class
* @param boolean $reuse
* By default true, which will reuse an existing Cacheable object of `$key`
* if it exists. If false, a new instance will be generated.
* @return Cacheable
*/
public static function getCacheProvider($key = null, $reuse = true)
{
$cacheDriver = Symphony::Configuration()->get($key, 'caching');
if (in_array($cacheDriver, array_keys(Symphony::ExtensionManager()->getProvidersOf('cache')))) {
$cacheable = new $cacheDriver;
} else {
$cacheable = Symphony::Database();
$cacheDriver = 'CacheDatabase';
}
if ($reuse === false) {
return new Cacheable($cacheable);
} elseif (!isset(self::$_pool[$cacheDriver])) {
self::$_pool[$cacheDriver] = new Cacheable($cacheable);
}
return self::$_pool[$cacheDriver];
}
/**
* Determines whether the current extension is installed or not by checking
* for an id in `tbl_extensions`
*
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @return boolean
*/
protected static function requiresInstallation($name)
{
static::buildExtensionList();
$id = self::$_extensions[$name]['id'];
return (is_numeric($id) ? false : true);
}
/**
* Determines whether an extension needs to be updated or not.
* This function will return the
* installed version if the extension requires an update, or
* false otherwise.
*
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @param string $file_version
* The version of the extension from the **file**, not the Database.
* @return boolean
* true if the given extension (by $name) requires updating.
* If the extension doesn't require updating, false.
*/
protected static function requiresUpdate($name, $file_version)
{
$installed_version = static::fetchInstalledVersion($name);
if (!$installed_version) {
return false;
}
$vc = new VersionComparator($installed_version);
return $vc->lessThan($file_version);
}
/**
* Enabling an extension will re-register all it's delegates with Symphony.
* It will also install or update the extension if needs be by calling the
* extensions respective install and update methods. The enable method is
* of the extension object is finally called.
*
* @see toolkit.ExtensionManager#registerDelegates()
* @see toolkit.ExtensionManager#canUninstallOrDisable()
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @throws SymphonyException
* @throws Exception
* @return boolean
*/
public static function enable($name)
{
$obj = self::getInstance($name);
// If not installed, install it
if (static::requiresInstallation($name) && $obj->install() === false) {
// If the installation failed, run the uninstall method which
// should rollback the install method. #1326
$obj->uninstall();
return false;
// If the extension requires updating before enabling, then update it
} elseif (($about = self::about($name)) && static::requiresUpdate($name, $about['version'])) {
$obj->update(static::fetchInstalledVersion($name));
}
if (!isset($about)) {
$about = self::about($name);
}
$id = self::fetchExtensionID($name);
$fields = array(
'name' => $name,
'status' => 'enabled',
'version' => $about['version']
);
// If there's no $id, the extension needs to be installed
if (is_null($id)) {
Symphony::Database()
->insert('tbl_extensions')
->values($fields)
->execute();
static::buildExtensionList(true);
// Extension is installed, so update!
} else {
Symphony::Database()
->update('tbl_extensions')
->set($fields)
->where(['id' => $id])
->execute();
}
self::registerDelegates($name);
// Now enable the extension
$obj->enable();
return true;
}
/**
* Disabling an extension will prevent it from executing but retain all it's
* settings in the relevant tables. Symphony checks that an extension can
* be disabled using the `canUninstallorDisable()` before removing
* all delegate subscriptions from the database and calling the extension's
* `disable()` function.
*
* @see toolkit.ExtensionManager#removeDelegates()
* @see toolkit.ExtensionManager#canUninstallOrDisable()
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @throws DatabaseException
* @throws SymphonyException
* @throws Exception
* @return boolean
*/
public static function disable($name)
{
$obj = self::getInstance($name);
static::canUninstallOrDisable($obj);
$info = self::about($name);
$id = self::fetchExtensionID($name);
$disabled = Symphony::Database()
->update('tbl_extensions')
->set([
'name' => $name,
'status' => 'disabled',
'version' => $info['version']
])
->where(['id' => $id])
->execute()
->success();
$obj->disable();
self::removeDelegates($name);
return $disabled;
}
/**
* Uninstalling an extension will unregister all delegate subscriptions and
* remove all extension settings. Symphony checks that an extension can
* be uninstalled using the `canUninstallorDisable()` before calling
* the extension's `uninstall()` function. Alternatively, if this function
* is called because the extension described by `$name` cannot be found
* it's delegates and extension meta information will just be removed from the
* database.
*
* @see toolkit.ExtensionManager#removeDelegates()
* @see toolkit.ExtensionManager#canUninstallOrDisable()
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @throws Exception
* @throws SymphonyException
* @throws DatabaseException
* @throws Exception
* @return boolean
*/
public static function uninstall($name)
{
// If this function is called because the extension doesn't exist,
// then catch the error and just remove from the database. This
// means that the uninstall() function will not run on the extension,
// which may be a blessing in disguise as no entry data will be removed
try {
$obj = self::getInstance($name);
static::canUninstallOrDisable($obj);
$obj->uninstall();
} catch (SymphonyException $ex) {
// Create a consistant key
$key = str_replace('-', '_', $ex->getTemplateName());
if ($key !== 'missing_extension') {
throw $ex;
}
}
self::removeDelegates($name);
return Symphony::Database()
->delete('tbl_extensions')
->where(['name' => $name])
->execute()
->success();
}
/**
* Retrieves all subscribed delegates from the database.
*
* @return DatabaseQueryResult
*/
public function getDelegateSubscriptions()
{
$projection = ['t1.name', 't2.page', 't2.delegate', 't2.callback', 't2.order'];
$orderBy = ['t2.delegate', 't2.order', 't1.name'];
$removeOrder = false;
try {
$removeOrder = !(Symphony::Database()
->showColumns()
->from('tbl_extensions_delegates')
->like('order')
->execute()
->next());
} catch (DatabaseException $ex) {
$removeOrder = true;
// Ignore for now
// This catch and check will be removed in Symphony 5.0.0
}
// Remove order col from projection and order by
if ($removeOrder) {
array_pop($projection);
$orderBy = ['t2.delegate', 't1.name'];
}
return Symphony::Database()
->select($projection)
->from('tbl_extensions', 't1')
->innerJoin('tbl_extensions_delegates', 't2')
->on(['t1.id' => '$t2.extension_id'])
->where(['t1.status' => 'enabled'])
->orderBy($orderBy, 'ASC')
->execute();
}
/**
* This functions registers an extensions delegates in `tbl_extensions_delegates`.
*
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @throws Exception
* @throws SymphonyException
* @return integer
* The Extension ID
*/
public static function registerDelegates($name)
{
$obj = self::getInstance($name);
$id = self::fetchExtensionID($name);
if (!$id) {
return false;
}
Symphony::Database()
->delete('tbl_extensions_delegates')
->where(['extension_id' => $id])
->execute();
$delegates = $obj->getSubscribedDelegates();
if (is_array($delegates) && !empty($delegates)) {
foreach ($delegates as $delegate) {
Symphony::Database()
->insert('tbl_extensions_delegates')
->values([
'extension_id' => $id,
'page' => $delegate['page'],
'delegate' => $delegate['delegate'],
'callback' => $delegate['callback'],
'order' => isset($delegate['order']) ? (int)$delegate['order'] : 0
])
->execute();
}
}
// Remove the unused DB records
self::cleanupDatabase();
return $id;
}
/**
* This function will remove all delegate subscriptions for an extension
* given an extension's name. This triggers `cleanupDatabase()`
*
* @see toolkit.ExtensionManager#cleanupDatabase()
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @return boolean
*/
public static function removeDelegates($name)
{
$delegates = Symphony::Database()
->select(['ted.id'])
->from('tbl_extensions_delegates', 'ted')
->leftJoin('tbl_extensions')
->on(['tbl_extensions.id' => '$ted.extension_id'])
->where(['tbl_extensions.name' => $name])
->execute()
->column('id');
if (!empty($delegates)) {
Symphony::Database()
->delete('tbl_extensions_delegates')
->where(['id' => ['in' => $delegates]])
->execute();
}
// Remove the unused DB records
self::cleanupDatabase();
return true;
}
/**
* This function checks that if the given extension has provided Fields,
* Data Sources or Events, that they aren't in use before the extension
* is uninstalled or disabled. This prevents exceptions from occurring when
* accessing an object that was using something provided by this Extension
* can't anymore because it has been removed.
*
* @param Extension $obj
* An extension object
* @throws SymphonyException
* @throws Exception
*/
protected static function canUninstallOrDisable(Extension $obj)
{
$extension_handle = strtolower(preg_replace('/^extension_/i', null, get_class($obj)));
$about = self::about($extension_handle);
// Fields:
if (is_dir(EXTENSIONS . "/{$extension_handle}/fields")) {
foreach (glob(EXTENSIONS . "/{$extension_handle}/fields/field.*.php") as $file) {
$type = preg_replace(array('/^field\./i', '/\.php$/i'), null, basename($file));
if (FieldManager::isFieldUsed($type)) {
throw new Exception(
__('The field ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
. ' ' . __("Please remove it from your sections prior to uninstalling or disabling.")
);
}
}
}
// Data Sources:
if (is_dir(EXTENSIONS . "/{$extension_handle}/data-sources")) {
foreach (glob(EXTENSIONS . "/{$extension_handle}/data-sources/data.*.php") as $file) {
$handle = preg_replace(array('/^data\./i', '/\.php$/i'), null, basename($file));
if (PageManager::isDataSourceUsed($handle)) {
throw new Exception(
__('The Data Source ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
. ' ' . __("Please remove it from your pages prior to uninstalling or disabling.")
);
}
}
}
// Events
if (is_dir(EXTENSIONS . "/{$extension_handle}/events")) {
foreach (glob(EXTENSIONS . "/{$extension_handle}/events/event.*.php") as $file) {
$handle = preg_replace(array('/^event\./i', '/\.php$/i'), null, basename($file));
if (PageManager::isEventUsed($handle)) {
throw new Exception(
__('The Event ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
. ' ' . __("Please remove it from your pages prior to uninstalling or disabling.")
);
}
}
}
// Text Formatters
if (is_dir(EXTENSIONS . "/{$extension_handle}/text-formatters")) {
foreach (glob(EXTENSIONS . "/{$extension_handle}/text-formatters/formatter.*.php") as $file) {
$handle = preg_replace(array('/^formatter\./i', '/\.php$/i'), null, basename($file));
if (FieldManager::isTextFormatterUsed($handle)) {
throw new Exception(
__('The Text Formatter ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
. ' ' . __("Please remove it from your fields prior to uninstalling or disabling.")
);
}
}
}
}
/**
* Given a delegate name, notify all extensions that have registered to that
* delegate to executing their callbacks with a `$context` array parameter
* that contains information about the current Symphony state.
*
* @param string $delegate
* The delegate name
* @param string $page
* The current page namespace that this delegate operates in
* @param array $context
* The `$context` param is an associative array that at minimum will contain
* the current Administration class, the current page object and the delegate
* name. Other context information may be passed to this function when it is
* called. eg.
*
* array(
* 'parent' =>& $this->Parent,
* 'page' => $page,
* 'delegate' => $delegate
* );
* @throws Exception
* @throws SymphonyException
* @return null|void
*/
public static function notifyMembers($delegate, $page, array $context = array())
{
// Make sure $page is an array
if (!is_array($page)) {
$page = array($page);
}
// Support for global delegate subscription
if (!in_array('*', $page)) {
$page[] = '*';
}
$services = array();
if (isset(self::$_subscriptions[$delegate])) {
foreach (self::$_subscriptions[$delegate] as $subscription) {
if (!in_array($subscription['page'], $page)) {
continue;
}
$services[] = $subscription;
}
}
if (empty($services)) {
return null;
}
$context += array('page' => $page, 'delegate' => $delegate);
$profiling = Symphony::Profiler() instanceof Profiler;
foreach ($services as $s) {
if ($profiling) {
// Initial seeding and query count
Symphony::Profiler()->seed();
$queries = Symphony::Database()->queryCount();
}
// Get instance of extension and execute the callback passing
// the `$context` along
$obj = self::getInstance($s['name']);
if (is_object($obj) && method_exists($obj, $s['callback'])) {
$obj->{$s['callback']}($context);
}
// Complete the Profiling sample
if ($profiling) {
$queries = Symphony::Database()->queryCount() - $queries;
Symphony::Profiler()->sample($delegate . '|' . $s['name'], PROFILE_LAP, 'Delegate', $queries);
}
}
}
/**
* Returns an array of all the enabled extensions available
*
* @return array
*/
public static function listInstalledHandles()
{
if (empty(self::$_enabled_extensions) && Symphony::Database()->isConnected()) {
self::$_enabled_extensions = (new ExtensionManager)
->select(['name'])
->enabled()
->execute()
->column('name');
}
return self::$_enabled_extensions;
}
/**
* Returns true if the extension is installed.
*
* @uses listInstalledHandles()
* @since Symphony 3.0.0
* @param string $handle
* The name of the extension
* @return boolean
*/
public static function isInstalled($handle)
{
return in_array($handle, self::listInstalledHandles());
}
/**
* Will return an associative array of all extensions and their about information
*
* @param string $filter
* Allows a regular expression to be passed to return only extensions whose
* folders match the filter.
* @throws SymphonyException
* @throws Exception
* @return array
* An associative array with the key being the extension folder and the value
* being the extension's about information
*/
public static function listAll($filter = '/^((?![-^?%:*|"<>]).)*$/')
{
$result = array();
$extensions = General::listDirStructure(EXTENSIONS, $filter, false, EXTENSIONS);
if (is_array($extensions) && !empty($extensions)) {
foreach ($extensions as $extension) {
$e = trim($extension, '/');
if ($about = self::about($e)) {
$result[$e] = $about;
}
}
}
return $result;
}
/**
* Custom user sorting function used inside `fetch` to recursively sort authors
* by their names.
*
* @param array $a
* @param array $b
* @param integer $i
* @return integer
*/
private static function sortByAuthor($a, $b, $i = 0)
{
$first = $a;
$second = $b;
if (isset($a[$i])) {
$first = $a[$i];
}
if (isset($b[$i])) {
$second = $b[$i];
}
if ($first == $a && $second == $b && $first['name'] == $second['name']) {
return 1;
} elseif ($first['name'] == $second['name']) {
return self::sortByAuthor($a, $b, $i + 1);
} else {
return ($first['name'] < $second['name']) ? -1 : 1;
}
}
/**
* This function will return an associative array of Extension information. The
* information returned is defined by the `$select` parameter, which will allow
* a developer to restrict what information is returned about the Extension.
* Optionally, `$where` (not implemented) and `$order_by` parameters allow a developer to
* further refine their query.
*
* @see listAll()
* @param array $select (optional)
* Accepts an array of keys to return from the listAll() method. If omitted, all keys
* will be returned.
* @param array $where (optional)
* Not implemented.
* @param string $order_by (optional)
* Allows a developer to return the extensions in a particular order. The syntax is the
* same as other `fetch` methods. If omitted this will return resources ordered by `name`.
* @throws Exception
* @throws SymphonyException
* @return array
* An associative array of Extension information, formatted in the same way as the
* listAll() method.
*/
public static function fetch(array $select = array(), array $where = array(), $order_by = null)
{
$extensions = self::listAll();
$data = array();
if (empty($select) && empty($where) && is_null($order_by)) {
return $extensions;
}
if (empty($extensions)) {
return array();
}
if (!is_null($order_by)) {
$author = $name = $label = array();
$order_by = array_map('strtolower', explode(' ', $order_by));
$order = ($order_by[1] == 'desc') ? SORT_DESC : SORT_ASC;
$sort = $order_by[0];
if ($sort == 'author') {
foreach ($extensions as $key => $about) {
$author[$key] = $about['author'];
}
uasort($author, array('self', 'sortByAuthor'));
if ($order == SORT_DESC) {
$author = array_reverse($author);
}
foreach ($author as $key => $value) {
$data[$key] = $extensions[$key];
}
$extensions = $data;
} elseif ($sort == 'name') {
foreach ($extensions as $key => $about) {
$name[$key] = strtolower($about['name']);
$label[$key] = $key;
}
array_multisort($name, $order, $label, $order, $extensions);
}
}
foreach ($extensions as $i => $e) {
$data[$i] = array();
foreach ($e as $key => $value) {
// If $select is empty, we assume every field is requested
if (in_array($key, $select) || empty($select)) {
$data[$i][$key] = $value;
}
}
}
return $data;
}
/**
* This function will load an extension's meta information given the extension
* `$name`. Since Symphony 2.3, this function will look for an `extension.meta.xml`
* file inside the extension's folder. If this is not found, it will initialise
* the extension and invoke the `about()` function. By default this extension will
* return an associative array display the basic meta data about the given extension.
* If the `$rawXML` parameter is passed true, and the extension has a `extension.meta.xml`
* file, this function will return `DOMDocument` of the file.
*
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @param boolean $rawXML
* If passed as true, and is available, this function will return the
* DOMDocument of representation of the given extension's `extension.meta.xml`
* file. If the file is not available, the extension will return the normal
* `about()` results. By default this is false.
* @throws Exception
* @throws SymphonyException
* @return array
* An associative array describing this extension
*/
public static function about($name, $rawXML = false)
{
// See if the extension has the new meta format
if (file_exists(self::__getClassPath($name) . '/extension.meta.xml')) {
try {
$meta = new DOMDocument;
$meta->load(self::__getClassPath($name) . '/extension.meta.xml');
$xpath = new DOMXPath($meta);
$rootNamespace = $meta->lookupNamespaceUri($meta->namespaceURI);
if (is_null($rootNamespace)) {
throw new Exception(__('Missing default namespace definition.'));
} else {
$xpath->registerNamespace('ext', $rootNamespace);
}
} catch (Exception $ex) {
Symphony::Engine()->throwCustomError(
__('The %1$s file for the %2$s extension is not valid XML: %3$s', array(
'<code>extension.meta.xml</code>',
'<code>' . $name . '</code>',
'<br /><code>' . $ex->getMessage() . '</code>'
))
);
}
// Load <extension>
$extension = $xpath->query('/ext:extension')->item(0);
// Check to see that the extension is named correctly, if it is
// not, then return nothing
if (self::__getClassName($name) !== self::__getClassName($xpath->evaluate('string(@id)', $extension))) {
return array();
}
// If `$rawXML` is set, just return our DOMDocument instance
if ($rawXML) {
return $meta;
}
$about = array(
'name' => $xpath->evaluate('string(ext:name)', $extension),
'handle' => $name,
'github' => $xpath->evaluate('string(ext:repo)', $extension),
'discuss' => $xpath->evaluate('string(ext:url[@type="discuss"])', $extension),
'homepage' => $xpath->evaluate('string(ext:url[@type="homepage"])', $extension),
'wiki' => $xpath->evaluate('string(ext:url[@type="wiki"])', $extension),
'issues' => $xpath->evaluate('string(ext:url[@type="issues"])', $extension),
'status' => array()
);
// find the latest <release> (largest version number)
$latest_release_version = '0.0.0';
foreach ($xpath->query('//ext:release', $extension) as $release) {
$version = $xpath->evaluate('string(@version)', $release);
$vc = new VersionComparator($version);
if ($vc->greaterThan($latest_release_version)) {
$latest_release_version = $version;
}
}
// Load the latest <release> information
if ($release = $xpath->query("//ext:release[@version='$latest_release_version']", $extension)->item(0)) {
$about += array(
'version' => $xpath->evaluate('string(@version)', $release),
'release-date' => $xpath->evaluate('string(@date)', $release)
);
// If it exists, load in the 'min/max' version data for this release
$required_min_version = $xpath->evaluate('string(@min)', $release);
$required_max_version = $xpath->evaluate('string(@max)', $release);
$symphonyVc = new VersionComparator(Symphony::Configuration()->get('version', 'symphony'));
// Min version
if (!empty($required_min_version) && $symphonyVc->lessThan($required_min_version)) {
$about['status'][] = Extension::EXTENSION_NOT_COMPATIBLE;
$about['required_version'] = $required_min_version;
// Max version
} elseif (!empty($required_max_version) && $symphonyVc->greaterThan($required_max_version)) {
$about['status'][] = Extension::EXTENSION_NOT_COMPATIBLE;
$about['required_version'] = $required_max_version;
}
}
// Add the <author> information
foreach ($xpath->query('//ext:author', $extension) as $author) {
$a = array(
'name' => $xpath->evaluate('string(ext:name)', $author),
'website' => $xpath->evaluate('string(ext:website)', $author),
'github' => $xpath->evaluate('string(ext:name/@github)', $author),
'email' => $xpath->evaluate('string(ext:email)', $author)
);
$about['author'][] = array_filter($a);
}
$about['status'] = array_merge($about['status'], self::fetchStatus($about));
return $about;
} else {
Symphony::Log()->pushToLog(sprintf('%s does not have an extension.meta.xml file', $name), E_DEPRECATED, true);
return array();
}
}
/**
* Creates an instance of a given class and returns it
*
* @param string $name
* The name of the Extension Class minus the extension prefix.
* @throws Exception
* @throws SymphonyException
* @return Extension
*/
public static function create($name)
{
if (!isset(self::$_pool[$name])) {
$classname = self::__getClassName($name);
$path = self::__getDriverPath($name);
if (!is_file($path)) {
$errMsg = __('Could not find extension %s at location %s.', array(
'<code>' . $name . '</code>',
'<code>' . str_replace(DOCROOT . '/', '', $path) . '</code>'
));
try {
Symphony::Engine()->throwCustomError(
$errMsg,
__('Symphony Extension Missing Error'),
Page::HTTP_STATUS_ERROR,
'missing_extension',
array(
'name' => $name,
'path' => $path
)
);
} catch (Exception $ex) {
throw new Exception($errMsg, 0, $ex);
}
}
// Load optional auto-loader
$autoLoader = basename($path) . '/vendor/autoload.php';
if (file_exists($autoLoader)) {
require_once($autoLoader);
}
// Load missing file if class does not exists
if (!class_exists($classname)) {
require_once($path);
}
// Create the extension object
self::$_pool[$name] = new $classname(array());
}
return self::$_pool[$name];
}
/**
* A utility function that is used by the ExtensionManager to ensure
* stray delegates are not in `tbl_extensions_delegates`. It is called when
* a new Delegate is added or removed.
*/
public static function cleanupDatabase()
{
// Grab any extensions sitting in the database
$rows = Symphony::Database()
->select(['name', 'status'])
->from('tbl_extensions')
->execute()
->rows();
// Iterate over each row
if (!empty($rows)) {
foreach ($rows as $r) {
$name = $r['name'];
$status = $r['status'];
// Grab the install location
$path = self::__getClassPath($name);
$existing_id = self::fetchExtensionID($name);
$removeDelegatesSQL = Symphony::Database()
->delete('tbl_extensions_delegates')
->where(['extension_id' => $existing_id]);
// If it doesn't exist, remove the DB rows
if (!@is_dir($path)) {
$removeDelegatesSQL->execute();
Symphony::Database()
->delete('tbl_extensions')
->wehere(['id' => $existing_id])
->limit(1)
->execute();
} elseif ($status == 'disabled') {
$removeDelegatesSQL->execute();
}
}
}
}
/**
* Factory method that creates a new ExtensionQuery.
*
* @since Symphony 3.0.0
* @param array $projection
* The projection to select.
* If no projection gets added, it defaults to `DatabaseQuery::getDefaultProjection()`.
* @return ExtensionQuery
*/
public function select(array $projection = [])
{
return new ExtensionQuery(Symphony::Database(), $projection);
}
}