PageRenderTime 56ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

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

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