PageRenderTime 49ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/symphony/lib/toolkit/class.extensionmanager.php

https://bitbucket.org/Twisted/symphony-2
PHP | 988 lines | 466 code | 138 blank | 384 comment | 93 complexity | e020fa9e9ff8ef7000ec1b94146bf4b6 MD5 | raw file
  1. <?php
  2. /**
  3. * @package toolkit
  4. */
  5. /**
  6. * The ExtensionManager class is responsible for managing all extensions
  7. * in Symphony. Extensions are stored on the file system in the `EXTENSIONS`
  8. * folder. They are autodiscovered where the Extension class name is the same
  9. * as it's folder name (excluding the extension prefix).
  10. */
  11. include_once(FACE . '/interface.fileresource.php');
  12. include_once(TOOLKIT . '/class.extension.php');
  13. Class ExtensionManager implements FileResource {
  14. /**
  15. * An array of all the objects that the Manager is responsible for.
  16. * Defaults to an empty array.
  17. * @var array
  18. */
  19. protected static $_pool = array();
  20. /**
  21. * An array of all extensions whose status is enabled
  22. * @var array
  23. */
  24. private static $_enabled_extensions = null;
  25. /**
  26. * An array of all the subscriptions to Symphony delegates made by extensions.
  27. * @var array
  28. */
  29. private static $_subscriptions = array();
  30. /**
  31. * An associative array of all the extensions in `tbl_extensions` where
  32. * the key is the extension name and the value is an array
  33. * representation of it's accompanying database row.
  34. * @var array
  35. */
  36. private static $_extensions = array();
  37. /**
  38. * An associative array of all the providers from the enabled extensions.
  39. * The key is the type of object, with the value being an associative array
  40. * with the name, classname and path to the object
  41. *
  42. * @since Symphony 2.3
  43. * @var array
  44. */
  45. private static $_providers = array();
  46. /**
  47. * The constructor will populate the `$_subscriptions` variable from
  48. * the `tbl_extension` and `tbl_extensions_delegates` tables.
  49. */
  50. public function __construct() {
  51. if (empty(self::$_subscriptions)) {
  52. $subscriptions = Symphony::Database()->fetch("
  53. SELECT t1.name, t2.page, t2.delegate, t2.callback
  54. FROM `tbl_extensions` as t1 INNER JOIN `tbl_extensions_delegates` as t2 ON t1.id = t2.extension_id
  55. WHERE t1.status = 'enabled'
  56. ORDER BY t2.delegate, t1.name
  57. ");
  58. foreach($subscriptions as $subscription) {
  59. self::$_subscriptions[$subscription['delegate']][] = $subscription;
  60. }
  61. }
  62. }
  63. public static function __getHandleFromFilename($filename) {
  64. return false;
  65. }
  66. /**
  67. * Given a name, returns the full class name of an Extension.
  68. * Extension use an 'extension' prefix.
  69. *
  70. * @param string $name
  71. * The extension handle
  72. * @return string
  73. */
  74. public static function __getClassName($name){
  75. return 'extension_' . $name;
  76. }
  77. /**
  78. * Finds an Extension by name by searching the `EXTENSIONS` folder and
  79. * returns the path to the folder.
  80. *
  81. * @param string $name
  82. * The extension folder
  83. * @return string
  84. */
  85. public static function __getClassPath($name){
  86. return EXTENSIONS . strtolower("/$name");
  87. }
  88. /**
  89. * Given a name, return the path to the driver of the Extension.
  90. *
  91. * @see toolkit.ExtensionManager#__getClassPath()
  92. * @param string $name
  93. * The extension folder
  94. * @return string
  95. */
  96. public static function __getDriverPath($name){
  97. return self::__getClassPath($name) . '/extension.driver.php';
  98. }
  99. /**
  100. * This function returns an instance of an extension from it's name
  101. *
  102. * @param string $name
  103. * The name of the Extension Class minus the extension prefix.
  104. * @return Extension
  105. */
  106. public static function getInstance($name){
  107. return isset(self::$_pool[$name])
  108. ? self::$_pool[$name]
  109. : self::create($name);
  110. }
  111. /**
  112. * Populates the `ExtensionManager::$_extensions` array with all the
  113. * extensions stored in `tbl_extensions`. If `ExtensionManager::$_extensions`
  114. * isn't empty, passing true as a parameter will force the array to update
  115. *
  116. * @param boolean $update
  117. * Updates the `ExtensionManager::$_extensions` array even if it was
  118. * populated, defaults to false.
  119. */
  120. private static function __buildExtensionList($update=false) {
  121. if (empty(self::$_extensions) || $update) {
  122. self::$_extensions = Symphony::Database()->fetch("SELECT * FROM `tbl_extensions`", 'name');
  123. }
  124. }
  125. /**
  126. * Returns the status of an Extension given an associative array containing
  127. * the Extension `handle` and `version` where the `version` is the file
  128. * version, not the installed version. This function returns an array
  129. * which may include a maximum of two statuses.
  130. *
  131. * @param array $about
  132. * An associative array of the extension meta data, typically returned
  133. * by `ExtensionManager::about()`. At the very least this array needs
  134. * `handle` and `version` keys.
  135. * @return array
  136. * An array of extension statuses, with the possible values being
  137. * `EXTENSION_ENABLED`, `EXTENSION_DISABLED`, `EXTENSION_REQUIRES_UPDATE`
  138. * or `EXTENSION_NOT_INSTALLED`. If an extension doesn't exist,
  139. * `EXTENSION_NOT_INSTALLED` will be returned.
  140. */
  141. public static function fetchStatus($about){
  142. $return = array();
  143. self::__buildExtensionList();
  144. if(isset($about['handle']) && array_key_exists($about['handle'], self::$_extensions)) {
  145. if(self::$_extensions[$about['handle']]['status'] == 'enabled')
  146. $return[] = EXTENSION_ENABLED;
  147. else
  148. $return[] = EXTENSION_DISABLED;
  149. }
  150. else {
  151. $return[] = EXTENSION_NOT_INSTALLED;
  152. }
  153. if(isset($about['handle']) && self::__requiresUpdate($about['handle'], $about['version'])) {
  154. $return[] = EXTENSION_REQUIRES_UPDATE;
  155. }
  156. return $return;
  157. }
  158. /**
  159. * A convenience method that returns an extension version from it's name.
  160. *
  161. * @param string $name
  162. * The name of the Extension Class minus the extension prefix.
  163. * @return string
  164. */
  165. public static function fetchInstalledVersion($name){
  166. self::__buildExtensionList();
  167. return isset(self::$_extensions[$name]) ? self::$_extensions[$name]['version'] : null;
  168. }
  169. /**
  170. * A convenience method that returns an extension ID from it's name.
  171. *
  172. * @param string $name
  173. * The name of the Extension Class minus the extension prefix.
  174. * @return integer
  175. */
  176. public static function fetchExtensionID($name){
  177. self::__buildExtensionList();
  178. return self::$_extensions[$name]['id'];
  179. }
  180. /**
  181. * Return an array all the Provider objects supplied by extensions,
  182. * optionally filtered by a given `$type`.
  183. *
  184. * @since Symphony 2.3
  185. * @todo Add information about the possible types
  186. * @param string $type
  187. * This will only return Providers of this type. If null, which is
  188. * default, all providers will be returned.
  189. * @return array
  190. * An array of objects
  191. */
  192. public static function getProvidersOf($type = null) {
  193. // Loop over all extensions and build an array of providable objects
  194. if(empty(self::$_providers)) {
  195. self::$_providers = array();
  196. foreach(self::listInstalledHandles() as $handle) {
  197. $obj = self::getInstance($handle);
  198. if(!method_exists($obj, 'providerOf')) continue;
  199. $providers = $obj->providerOf();
  200. if(empty($providers)) continue;
  201. // For each of the matching objects (by $type), resolve the object path
  202. self::$_providers = array_merge_recursive(self::$_providers, $obj->providerOf());
  203. }
  204. }
  205. // Return an array of objects
  206. if(is_null($type)) return self::$_providers;
  207. if(!isset(self::$_providers[$type])) return array();
  208. return self::$_providers[$type];
  209. }
  210. /**
  211. * Determines whether the current extension is installed or not by checking
  212. * for an id in `tbl_extensions`
  213. *
  214. * @param string $name
  215. * The name of the Extension Class minus the extension prefix.
  216. * @return boolean
  217. */
  218. private static function __requiresInstallation($name){
  219. self::__buildExtensionList();
  220. $id = self::$_extensions[$name]['id'];
  221. return (is_numeric($id) ? false : true);
  222. }
  223. /**
  224. * Determines whether an extension needs to be updated or not using
  225. * PHP's `version_compare` function. This function will return the
  226. * installed version if the extension requires an update, or
  227. * false otherwise.
  228. *
  229. * @param string $name
  230. * The name of the Extension Class minus the extension prefix.
  231. * @param string $file_version
  232. * The version of the extension from the **file**, not the Database.
  233. * @return string|boolean
  234. * If the given extension (by $name) requires updating, the installed
  235. * version is returned, otherwise, if the extension doesn't require
  236. * updating, false.
  237. */
  238. private static function __requiresUpdate($name, $file_version){
  239. $installed_version = self::fetchInstalledVersion($name);
  240. if(is_null($installed_version)) return false;
  241. return (version_compare($installed_version, $file_version, '<') ? $installed_version : false);
  242. }
  243. /**
  244. * Enabling an extension will re-register all it's delegates with Symphony.
  245. * It will also install or update the extension if needs be by calling the
  246. * extensions respective install and update methods. The enable method is
  247. * of the extension object is finally called.
  248. *
  249. * @see toolkit.ExtensionManager#registerDelegates()
  250. * @see toolkit.ExtensionManager#__canUninstallOrDisable()
  251. * @param string $name
  252. * The name of the Extension Class minus the extension prefix.
  253. * @return boolean
  254. */
  255. public static function enable($name){
  256. $obj = self::getInstance($name);
  257. // If not installed, install it
  258. if(self::__requiresInstallation($name) && $obj->install() === false) {
  259. // If the installation failed, run the uninstall method which
  260. // should rollback the install method. #1326
  261. $obj->uninstall();
  262. return false;
  263. }
  264. // If the extension requires updating before enabling, then update it
  265. elseif(($about = self::about($name)) && ($previousVersion = self::__requiresUpdate($name, $about['version'])) !== false) {
  266. $obj->update($previousVersion);
  267. }
  268. if(!isset($about)) $about = self::about($name);
  269. $id = self::fetchExtensionID($name);
  270. $fields = array(
  271. 'name' => $name,
  272. 'status' => 'enabled',
  273. 'version' => $about['version']
  274. );
  275. // If there's no $id, the extension needs to be installed
  276. if(is_null($id)) {
  277. Symphony::Database()->insert($fields, 'tbl_extensions');
  278. self::__buildExtensionList(true);
  279. }
  280. // Extension is installed, so update!
  281. else {
  282. Symphony::Database()->update($fields, 'tbl_extensions', " `id` = '$id '");
  283. }
  284. self::registerDelegates($name);
  285. // Now enable the extension
  286. $obj->enable();
  287. return true;
  288. }
  289. /**
  290. * Disabling an extension will prevent it from executing but retain all it's
  291. * settings in the relevant tables. Symphony checks that an extension can
  292. * be disabled using the `canUninstallorDisable()` before removing
  293. * all delegate subscriptions from the database and calling the extension's
  294. * `disable()` function.
  295. *
  296. * @see toolkit.ExtensionManager#removeDelegates()
  297. * @see toolkit.ExtensionManager#__canUninstallOrDisable()
  298. * @param string $name
  299. * The name of the Extension Class minus the extension prefix.
  300. * @return boolean
  301. */
  302. public static function disable($name){
  303. $obj = self::getInstance($name);
  304. self::__canUninstallOrDisable($obj);
  305. $info = self::about($name);
  306. $id = self::fetchExtensionID($name);
  307. Symphony::Database()->update(array(
  308. 'name' => $name,
  309. 'status' => 'disabled',
  310. 'version' => $info['version']
  311. ),
  312. 'tbl_extensions',
  313. " `id` = '$id '"
  314. );
  315. $obj->disable();
  316. self::removeDelegates($name);
  317. return true;
  318. }
  319. /**
  320. * Uninstalling an extension will unregister all delegate subscriptions and
  321. * remove all extension settings. Symphony checks that an extension can
  322. * be uninstalled using the `canUninstallorDisable()` before calling
  323. * the extension's `uninstall()` function. Alternatively, if this function
  324. * is called because the extension described by `$name` cannot be found
  325. * it's delegates and extension meta information will just be removed from the
  326. * database.
  327. *
  328. * @see toolkit.ExtensionManager#removeDelegates()
  329. * @see toolkit.ExtensionManager#__canUninstallOrDisable()
  330. * @param string $name
  331. * The name of the Extension Class minus the extension prefix.
  332. * @return boolean
  333. */
  334. public static function uninstall($name) {
  335. // If this function is called because the extension doesn't exist,
  336. // then catch the error and just remove from the database. This
  337. // means that the uninstall() function will not run on the extension,
  338. // which may be a blessing in disguise as no entry data will be removed
  339. try {
  340. $obj = self::getInstance($name);
  341. self::__canUninstallOrDisable($obj);
  342. $obj->uninstall();
  343. }
  344. catch(SymphonyErrorPage $ex) {
  345. // Create a consistant key
  346. $key = str_replace('-', '_', $ex->getTemplateName());
  347. if($key !== 'missing_extension') {
  348. throw $ex;
  349. }
  350. }
  351. self::removeDelegates($name);
  352. Symphony::Database()->delete('tbl_extensions', sprintf(" `name` = '%s' ", $name));
  353. return true;
  354. }
  355. /**
  356. * This functions registers an extensions delegates in `tbl_extensions_delegates`.
  357. *
  358. * @param string $name
  359. * The name of the Extension Class minus the extension prefix.
  360. * @return integer
  361. * The Extension ID
  362. */
  363. public static function registerDelegates($name){
  364. $obj = self::getInstance($name);
  365. $id = self::fetchExtensionID($name);
  366. if(!$id) return false;
  367. Symphony::Database()->delete('tbl_extensions_delegates', " `extension_id` = '$id ' ");
  368. $delegates = $obj->getSubscribedDelegates();
  369. if(is_array($delegates) && !empty($delegates)){
  370. foreach($delegates as $delegate){
  371. Symphony::Database()->insert(
  372. array(
  373. 'extension_id' => $id ,
  374. 'page' => $delegate['page'],
  375. 'delegate' => $delegate['delegate'],
  376. 'callback' => $delegate['callback']
  377. ),
  378. 'tbl_extensions_delegates'
  379. );
  380. }
  381. }
  382. // Remove the unused DB records
  383. self::cleanupDatabase();
  384. return $id;
  385. }
  386. /**
  387. * This function will remove all delegate subscriptions for an extension
  388. * given an extension's name. This triggers `cleanupDatabase()`
  389. *
  390. * @see toolkit.ExtensionManager#cleanupDatabase()
  391. * @param string $name
  392. * The name of the Extension Class minus the extension prefix.
  393. * @return boolean
  394. */
  395. public static function removeDelegates($name){
  396. $classname = self::__getClassName($name);
  397. $path = self::__getDriverPath($name);
  398. if(!file_exists($path)) return false;
  399. $delegates = Symphony::Database()->fetchCol('id', sprintf("
  400. SELECT tbl_extensions_delegates.`id`
  401. FROM `tbl_extensions_delegates`
  402. LEFT JOIN `tbl_extensions`
  403. ON (`tbl_extensions`.id = `tbl_extensions_delegates`.extension_id)
  404. WHERE `tbl_extensions`.name = '%s'
  405. ", $name
  406. ));
  407. if(!empty($delegates)) {
  408. Symphony::Database()->delete('tbl_extensions_delegates', " `id` IN ('". implode("', '", $delegates). "') ");
  409. }
  410. // Remove the unused DB records
  411. self::cleanupDatabase();
  412. return true;
  413. }
  414. /**
  415. * This function checks that if the given extension has provided Fields,
  416. * Data Sources or Events, that they aren't in use before the extension
  417. * is uninstalled or disabled. This prevents exceptions from occurring when
  418. * accessing an object that was using something provided by this Extension
  419. * can't anymore because it has been removed.
  420. *
  421. * @param Extension $obj
  422. * An extension object
  423. * @return boolean
  424. */
  425. private static function __canUninstallOrDisable(Extension $obj){
  426. $extension_handle = strtolower(preg_replace('/^extension_/i', NULL, get_class($obj)));
  427. $about = self::about($extension_handle);
  428. // Fields:
  429. if(is_dir(EXTENSIONS . "/{$extension_handle}/fields")){
  430. foreach(glob(EXTENSIONS . "/{$extension_handle}/fields/field.*.php") as $file){
  431. $type = preg_replace(array('/^field\./i', '/\.php$/i'), NULL, basename($file));
  432. if(FieldManager::isFieldUsed($type)){
  433. throw new Exception(
  434. __('The field ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
  435. . ' ' . __("Please remove it from your sections prior to uninstalling or disabling.")
  436. );
  437. }
  438. }
  439. }
  440. // Data Sources:
  441. if(is_dir(EXTENSIONS . "/{$extension_handle}/data-sources")){
  442. foreach(glob(EXTENSIONS . "/{$extension_handle}/data-sources/data.*.php") as $file){
  443. $handle = preg_replace(array('/^data\./i', '/\.php$/i'), NULL, basename($file));
  444. if(PageManager::isDataSourceUsed($handle)){
  445. throw new Exception(
  446. __('The Data Source ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
  447. . ' ' . __("Please remove it from your pages prior to uninstalling or disabling.")
  448. );
  449. }
  450. }
  451. }
  452. // Events
  453. if(is_dir(EXTENSIONS . "/{$extension_handle}/events")){
  454. foreach(glob(EXTENSIONS . "/{$extension_handle}/events/event.*.php") as $file){
  455. $handle = preg_replace(array('/^event\./i', '/\.php$/i'), NULL, basename($file));
  456. if(PageManager::isEventUsed($handle)){
  457. throw new Exception(
  458. __('The Event ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
  459. . ' ' . __("Please remove it from your pages prior to uninstalling or disabling.")
  460. );
  461. }
  462. }
  463. }
  464. // Text Formatters
  465. if(is_dir(EXTENSIONS . "/{$extension_handle}/text-formatters")){
  466. foreach(glob(EXTENSIONS . "/{$extension_handle}/text-formatters/formatter.*.php") as $file){
  467. $handle = preg_replace(array('/^formatter\./i', '/\.php$/i'), NULL, basename($file));
  468. if(FieldManager::isTextFormatterUsed($handle)) {
  469. throw new Exception(
  470. __('The Text Formatter ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
  471. . ' ' . __("Please remove it from your fields prior to uninstalling or disabling.")
  472. );
  473. }
  474. }
  475. }
  476. }
  477. /**
  478. * Given a delegate name, notify all extensions that have registered to that
  479. * delegate to executing their callbacks with a `$context` array parameter
  480. * that contains information about the current Symphony state.
  481. *
  482. * @param string $delegate
  483. * The delegate name
  484. * @param string $page
  485. * The current page namespace that this delegate operates in
  486. * @param array $context
  487. * The `$context` param is an associative array that at minimum will contain
  488. * the current Administration class, the current page object and the delegate
  489. * name. Other context information may be passed to this function when it is
  490. * called. eg.
  491. *
  492. * array(
  493. * 'parent' =>& $this->Parent,
  494. * 'page' => $page,
  495. * 'delegate' => $delegate
  496. * );
  497. */
  498. public static function notifyMembers($delegate, $page, array $context=array()){
  499. // Make sure $page is an array
  500. if(!is_array($page)){
  501. $page = array($page);
  502. }
  503. // Support for global delegate subscription
  504. if(!in_array('*', $page)){
  505. $page[] = '*';
  506. }
  507. $services = array();
  508. if(isset(self::$_subscriptions[$delegate])) foreach(self::$_subscriptions[$delegate] as $subscription) {
  509. if(!in_array($subscription['page'], $page)) continue;
  510. $services[] = $subscription;
  511. }
  512. if(empty($services)) return null;
  513. $context += array('page' => $page, 'delegate' => $delegate);
  514. foreach($services as $s) {
  515. // Initial seeding and query count
  516. Symphony::Profiler()->seed();
  517. $queries = Symphony::Database()->queryCount();
  518. // Get instance of extension and execute the callback passing
  519. // the `$context` along
  520. $obj = self::getInstance($s['name']);
  521. if(is_object($obj) && method_exists($obj, $s['callback'])) {
  522. $obj->{$s['callback']}($context);
  523. }
  524. // Complete the Profiling sample
  525. $queries = Symphony::Database()->queryCount() - $queries;
  526. Symphony::Profiler()->sample($delegate . '|' . $s['name'], PROFILE_LAP, 'Delegate', $queries);
  527. }
  528. }
  529. /**
  530. * Returns an array of all the enabled extensions available
  531. *
  532. * @return array
  533. */
  534. public static function listInstalledHandles(){
  535. if(is_null(self::$_enabled_extensions)) {
  536. self::$_enabled_extensions = Symphony::Database()->fetchCol('name',
  537. "SELECT `name` FROM `tbl_extensions` WHERE `status` = 'enabled'"
  538. );
  539. }
  540. return self::$_enabled_extensions;
  541. }
  542. /**
  543. * Will return an associative array of all extensions and their about information
  544. *
  545. * @param string $filter
  546. * Allows a regular expression to be passed to return only extensions whose
  547. * folders match the filter.
  548. * @return array
  549. * An associative array with the key being the extension folder and the value
  550. * being the extension's about information
  551. */
  552. public static function listAll($filter='/^((?![-^?%:*|"<>]).)*$/') {
  553. $result = array();
  554. $extensions = General::listDirStructure(EXTENSIONS, $filter, false, EXTENSIONS);
  555. if(is_array($extensions) && !empty($extensions)){
  556. foreach($extensions as $extension){
  557. $e = trim($extension, '/');
  558. if($about = self::about($e)) $result[$e] = $about;
  559. }
  560. }
  561. return $result;
  562. }
  563. /**
  564. * Custom user sorting function used inside `fetch` to recursively sort authors
  565. * by their names.
  566. *
  567. * @param array $a
  568. * @param array $b
  569. * @return integer
  570. */
  571. private static function sortByAuthor($a, $b, $i = 0) {
  572. $first = $a; $second = $b;
  573. if(isset($a[$i]))$first = $a[$i];
  574. if(isset($b[$i])) $second = $b[$i];
  575. if ($first == $a && $second == $b && $first['name'] == $second['name'])
  576. return 1;
  577. else if ($first['name'] == $second['name'])
  578. return self::sortByAuthor($a, $b, $i + 1);
  579. else
  580. return ($first['name'] < $second['name']) ? -1 : 1;
  581. }
  582. /**
  583. * This function will return an associative array of Extension information. The
  584. * information returned is defined by the `$select` parameter, which will allow
  585. * a developer to restrict what information is returned about the Extension.
  586. * Optionally, `$where` (not implemented) and `$order_by` parameters allow a developer to
  587. * further refine their query.
  588. *
  589. * @param array $select (optional)
  590. * Accepts an array of keys to return from the listAll() method. If omitted, all keys
  591. * will be returned.
  592. * @param array $where (optional)
  593. * Not implemented.
  594. * @param string $order_by (optional)
  595. * Allows a developer to return the extensions in a particular order. The syntax is the
  596. * same as other `fetch` methods. If omitted this will return resources ordered by `name`.
  597. * @return array
  598. * An associative array of Extension information, formatted in the same way as the
  599. * listAll() method.
  600. */
  601. public static function fetch(array $select = array(), array $where = array(), $order_by = null){
  602. $extensions = self::listAll();
  603. if(empty($select) && empty($where) && is_null($order_by)) return $extensions;
  604. if(empty($extensions)) return array();
  605. if(!is_null($order_by)){
  606. $order_by = array_map('strtolower', explode(' ', $order_by));
  607. $order = ($order_by[1] == 'desc') ? SORT_DESC : SORT_ASC;
  608. $sort = $order_by[0];
  609. if($sort == 'author'){
  610. foreach($extensions as $key => $about){
  611. $author[$key] = $about['author'];
  612. }
  613. $data = array();
  614. uasort($author, array('self', 'sortByAuthor'));
  615. if($order == SORT_DESC){
  616. $author = array_reverse($author);
  617. }
  618. foreach($author as $key => $value){
  619. $data[$key] = $extensions[$key];
  620. }
  621. $extensions = $data;
  622. }
  623. else if($sort == 'name'){
  624. foreach($extensions as $key => $about){
  625. $name[$key] = strtolower($about['name']);
  626. $label[$key] = $key;
  627. }
  628. array_multisort($name, $order, $label, $order, $extensions);
  629. }
  630. }
  631. $data = array();
  632. foreach($extensions as $i => $e){
  633. $data[$i] = array();
  634. foreach($e as $key => $value) {
  635. // If $select is empty, we assume every field is requested
  636. if(in_array($key, $select) || empty($select)) $data[$i][$key] = $value;
  637. }
  638. }
  639. return $data;
  640. }
  641. /**
  642. * This function will load an extension's meta information given the extension
  643. * `$name`. Since Symphony 2.3, this function will look for an `extension.meta.xml`
  644. * file inside the extension's folder. If this is not found, it will initialise
  645. * the extension and invoke the `about()` function. By default this extension will
  646. * return an associative array display the basic meta data about the given extension.
  647. * If the `$rawXML` parameter is passed true, and the extension has a `extension.meta.xml`
  648. * file, this function will return `DOMDocument` of the file.
  649. *
  650. * @deprecated Since Symphony 2.3, the `about()` function is deprecated for extensions
  651. * in favour of the `extension.meta.xml` file.
  652. * @param string $name
  653. * The name of the Extension Class minus the extension prefix.
  654. * @param boolean $rawXML
  655. * If passed as true, and is available, this function will return the
  656. * DOMDocument of representation of the given extension's `extension.meta.xml`
  657. * file. If the file is not available, the extension will return the normal
  658. * `about()` results. By default this is false.
  659. * @return array
  660. * An associative array describing this extension
  661. */
  662. public static function about($name, $rawXML = false) {
  663. // See if the extension has the new meta format
  664. if(file_exists(self::__getClassPath($name) . '/extension.meta.xml')) {
  665. try {
  666. $meta = new DOMDocument;
  667. $meta->load(self::__getClassPath($name) . '/extension.meta.xml');
  668. $xpath = new DOMXPath($meta);
  669. $rootNamespace = $meta->lookupNamespaceUri($meta->namespaceURI);
  670. if(is_null($rootNamespace)) {
  671. throw new Exception(__('Missing default namespace definition.'));
  672. }
  673. else {
  674. $xpath->registerNamespace('ext', $rootNamespace);
  675. }
  676. }
  677. catch (Exception $ex) {
  678. Symphony::Engine()->throwCustomError(
  679. __('The %1$s file for the %2$s extension is not valid XML: %3$s', array(
  680. '<code>extension.meta.xml</code>',
  681. '<code>' . $name . '</code>',
  682. '<br /><code>' . $ex->getMessage() . '</code>'
  683. ))
  684. );
  685. }
  686. // Load <extension>
  687. $extension = $xpath->query('/ext:extension')->item(0);
  688. // Check to see that the extension is named correctly, if it is
  689. // not, then return nothing
  690. if(self::__getClassName($name) != self::__getClassName($xpath->evaluate('string(@id)', $extension))) {
  691. return array();
  692. }
  693. // If `$rawXML` is set, just return our DOMDocument instance
  694. if($rawXML) return $meta;
  695. $about = array(
  696. 'name' => $xpath->evaluate('string(ext:name)', $extension),
  697. 'status' => array()
  698. );
  699. // find the latest <release> (largest version number)
  700. $latest_release_version = '0.0.0';
  701. foreach($xpath->query('//ext:release', $extension) as $release) {
  702. $version = $xpath->evaluate('string(@version)', $release);
  703. if(version_compare($version, $latest_release_version, '>')) {
  704. $latest_release_version = $version;
  705. }
  706. }
  707. // Load the latest <release> information
  708. if($release = $xpath->query("//ext:release[@version='$latest_release_version']", $extension)->item(0)) {
  709. $about += array(
  710. 'version' => $xpath->evaluate('string(@version)', $release),
  711. 'release-date' => $xpath->evaluate('string(@date)', $release)
  712. );
  713. // If it exists, load in the 'min/max' version data for this release
  714. $required_version = null;
  715. $required_min_version = $xpath->evaluate('string(@min)', $release);
  716. $required_max_version = $xpath->evaluate('string(@max)', $release);
  717. $current_symphony_version = Symphony::Configuration()->get('version', 'symphony');
  718. // Munge the version number so that it makes sense in the backend.
  719. // Consider, 2.3.x. As the min version, this means 2.3 onwards,
  720. // for the max it implies any 2.3 release. RE: #1019
  721. $required_min_version = str_replace('.x', '', $required_min_version);
  722. $required_max_version = str_replace('.x', 'p', $required_max_version);
  723. // Min version
  724. if(!empty($required_min_version) &&
  725. version_compare($current_symphony_version, $required_min_version, '<')
  726. ) {
  727. $about['status'][] = EXTENSION_NOT_COMPATIBLE;
  728. $about['required_version'] = $required_min_version;
  729. }
  730. // Max version
  731. else if(!empty($required_max_version) &&
  732. version_compare($current_symphony_version, $required_max_version, '>')
  733. ) {
  734. $about['status'][] = EXTENSION_NOT_COMPATIBLE;
  735. $about['required_version'] = $required_max_version;
  736. }
  737. }
  738. // Add the <author> information
  739. foreach($xpath->query('//ext:author', $extension) as $author) {
  740. $a = array(
  741. 'name' => $xpath->evaluate('string(ext:name)', $author),
  742. 'website' => $xpath->evaluate('string(ext:website)', $author),
  743. 'email' => $xpath->evaluate('string(ext:email)', $author)
  744. );
  745. $about['author'][] = array_filter($a);
  746. }
  747. }
  748. // It doesn't, fallback to loading the extension using the built in
  749. // `about()` array.
  750. else {
  751. $obj = self::getInstance($name);
  752. $about = $obj->about();
  753. // If this is empty then the extension has managed to not provide
  754. // an `about()` function or an `extension.meta.xml` file. So
  755. // ignore this extension even exists
  756. if(empty($about)) return array();
  757. $about['status'] = array();
  758. }
  759. $about['handle'] = $name;
  760. $about['status'] = array_merge($about['status'], self::fetchStatus($about));
  761. return $about;
  762. }
  763. /**
  764. * Creates an instance of a given class and returns it
  765. *
  766. * @param string $name
  767. * The name of the Extension Class minus the extension prefix.
  768. * @return Extension
  769. */
  770. public static function create($name){
  771. if(!isset(self::$_pool[$name])){
  772. $classname = self::__getClassName($name);
  773. $path = self::__getDriverPath($name);
  774. if(!is_file($path)) {
  775. Symphony::Engine()->throwCustomError(
  776. __('Could not find extension %s at location %s.', array(
  777. '<code>' . $name . '</code>',
  778. '<code>' . $path . '</code>'
  779. )),
  780. __('Symphony Extension Missing Error'),
  781. Page::HTTP_STATUS_ERROR,
  782. 'missing_extension',
  783. array(
  784. 'name' => $name,
  785. 'path' => $path
  786. )
  787. );
  788. }
  789. if(!class_exists($classname)) require_once($path);
  790. // Create the extension object
  791. self::$_pool[$name] = new $classname(array());
  792. }
  793. return self::$_pool[$name];
  794. }
  795. /**
  796. * A utility function that is used by the ExtensionManager to ensure
  797. * stray delegates are not in `tbl_extensions_delegates`. It is called when
  798. * a new Delegate is added or removed.
  799. */
  800. public static function cleanupDatabase() {
  801. // Grab any extensions sitting in the database
  802. $rows = Symphony::Database()->fetch("SELECT `name` FROM `tbl_extensions`");
  803. // Iterate over each row
  804. if(is_array($rows) && !empty($rows)){
  805. foreach($rows as $r){
  806. $name = $r['name'];
  807. $status = isset($r['status']) ? $r['status'] : null;
  808. // Grab the install location
  809. $path = self::__getClassPath($name);
  810. $existing_id = self::fetchExtensionID($name);
  811. // If it doesnt exist, remove the DB rows
  812. if(!@is_dir($path)){
  813. Symphony::Database()->delete("tbl_extensions_delegates", " `extension_id` = $existing_id ");
  814. Symphony::Database()->delete('tbl_extensions', " `id` = '$existing_id' LIMIT 1");
  815. }
  816. elseif ($status == 'disabled') {
  817. Symphony::Database()->delete("tbl_extensions_delegates", " `extension_id` = $existing_id ");
  818. }
  819. }
  820. }
  821. }
  822. }
  823. /**
  824. * Status when an extension is installed and enabled
  825. * @var integer
  826. */
  827. define_safe('EXTENSION_ENABLED', 10);
  828. /**
  829. * Status when an extension is disabled
  830. * @var integer
  831. */
  832. define_safe('EXTENSION_DISABLED', 11);
  833. /**
  834. * Status when an extension is in the file system, but has not been installed.
  835. * @var integer
  836. */
  837. define_safe('EXTENSION_NOT_INSTALLED', 12);
  838. /**
  839. * Status when an extension version in the file system is different to
  840. * the version stored in the database for the extension
  841. * @var integer
  842. */
  843. define_safe('EXTENSION_REQUIRES_UPDATE', 13);
  844. /**
  845. * Status when the extension is not compatible with the current version of
  846. * Symphony
  847. * @since Symphony 2.3
  848. * @var integer
  849. */
  850. define_safe('EXTENSION_NOT_COMPATIBLE', 14);