PageRenderTime 43ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/system/classes/plugins.php

https://github.com/HabariMag/habarimag-old
PHP | 688 lines | 441 code | 60 blank | 187 comment | 73 complexity | 750adeb0632479d8cdd71c445295743e MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * @package Habari
  4. *
  5. */
  6. /**
  7. * Habari Plugins Class
  8. *
  9. * Provides an interface for the code to access plugins
  10. */
  11. class Plugins
  12. {
  13. private static $hooks = array();
  14. private static $plugins = array();
  15. private static $plugin_files = array();
  16. private static $plugin_classes = array();
  17. /**
  18. * function __construct
  19. * A private constructor method to prevent this class from being instantiated.
  20. * Don't ever create this class as an object for any reason. It is not a singleton.
  21. */
  22. private function __construct()
  23. {
  24. }
  25. /**
  26. * Autoload function to load plugin file from classname
  27. */
  28. public static function _autoload( $class )
  29. {
  30. if ( isset( self::$plugin_files[$class] ) ) {
  31. require( self::$plugin_files[$class] );
  32. }
  33. }
  34. /**
  35. * function register
  36. * Registers a plugin action for possible execution
  37. * @param mixed A reference to the function to register by string or array(object, string)
  38. * @param string Usually either 'filter' or 'action' depending on the hook type.
  39. * @param string The plugin hook to register
  40. * @param hex An optional execution priority, in hex. The lower the priority, the earlier the function will execute in the chain. Default value = 8.
  41. */
  42. public static function register( $fn, $type, $hook, $priority = 8 )
  43. {
  44. // add the plugin function to the appropriate array
  45. $index = array( $type, $hook, $priority );
  46. $ref =& self::$hooks;
  47. foreach ( $index as $bit ) {
  48. if ( !isset( $ref["{$bit}"] ) ) {
  49. $ref["{$bit}"] = array();
  50. }
  51. $ref =& $ref["{$bit}"];
  52. }
  53. $ref[] = $fn;
  54. ksort( self::$hooks[$type][$hook] );
  55. }
  56. /**
  57. * Call to execute a plugin action
  58. * @param string The name of the action to execute
  59. * @param mixed Optional arguments needed for action
  60. */
  61. public static function act()
  62. {
  63. $args = func_get_args();
  64. $hookname = array_shift( $args );
  65. if ( ! isset( self::$hooks['action'][$hookname] ) ) {
  66. return false;
  67. }
  68. foreach ( self::$hooks['action'][$hookname] as $priority ) {
  69. foreach ( $priority as $action ) {
  70. // $action is an array of object reference
  71. // and method name
  72. call_user_func_array( $action, $args );
  73. }
  74. }
  75. }
  76. /**
  77. * Call to execute a plugin action, by id
  78. * @param string The name of the action to execute
  79. * @param mixed Optional arguments needed for action
  80. */
  81. public static function act_id()
  82. {
  83. $args = func_get_args();
  84. list( $hookname, $id ) = $args;
  85. $args = array_slice( func_get_args(), 2 );
  86. $hookname = $hookname . ':' . $id;
  87. if ( ! isset( self::$hooks['action'][$hookname] ) ) {
  88. return false;
  89. }
  90. foreach ( self::$hooks['action'][$hookname] as $priority ) {
  91. foreach ( $priority as $action ) {
  92. // $action is an array of object reference
  93. // and method name
  94. call_user_func_array( $action, $args );
  95. }
  96. }
  97. }
  98. /**
  99. * Call to execute a plugin filter
  100. * @param string The name of the filter to execute
  101. * @param mixed The value to filter.
  102. */
  103. public static function filter()
  104. {
  105. list( $hookname, $return ) = func_get_args();
  106. if ( ! isset( self::$hooks['filter'][$hookname] ) ) {
  107. return $return;
  108. }
  109. $filterargs = array_slice( func_get_args(), 2 );
  110. foreach ( self::$hooks['filter'][$hookname] as $priority ) {
  111. foreach ( $priority as $filter ) {
  112. // $filter is an array of object reference and method name
  113. $callargs = $filterargs;
  114. array_unshift( $callargs, $return );
  115. $return = call_user_func_array( $filter, $callargs );
  116. }
  117. }
  118. return $return;
  119. }
  120. /**
  121. * Call to execute a plugin filter on a specific plugin, by id
  122. * @param string The name of the filter to execute
  123. * @param string The id of the only plugin on which to execute
  124. * @param mixed The value to filter.
  125. */
  126. public static function filter_id()
  127. {
  128. list( $hookname, $id, $return ) = func_get_args();
  129. $hookname = $hookname . ':' . $id;
  130. if ( ! isset( self::$hooks['filter'][$hookname] ) ) {
  131. return $return;
  132. }
  133. $filterargs = array_slice( func_get_args(), 3 );
  134. foreach ( self::$hooks['filter'][$hookname] as $priority ) {
  135. foreach ( $priority as $filter ) {
  136. // $filter is an array of object reference and method name
  137. $callargs = $filterargs;
  138. array_unshift( $callargs, $return );
  139. $return = call_user_func_array( $filter, $callargs );
  140. }
  141. }
  142. return $return;
  143. }
  144. /**
  145. * Call to execute an XMLRPC function
  146. * @param string The name of the filter to execute
  147. * @param mixed The value to filter.
  148. */
  149. public static function xmlrpc()
  150. {
  151. list( $hookname, $return ) = func_get_args();
  152. if ( ! isset( self::$hooks['xmlrpc'][$hookname] ) ) {
  153. return false;
  154. }
  155. $filterargs = array_slice( func_get_args(), 2 );
  156. foreach ( self::$hooks['xmlrpc'][$hookname] as $priority ) {
  157. foreach ( $priority as $filter ) {
  158. // $filter is an array of object reference and method name
  159. return call_user_func_array( $filter, $filterargs );
  160. }
  161. }
  162. return false;
  163. }
  164. /**
  165. * Call to execute a theme function
  166. * @param string The name of the filter to execute
  167. * @param mixed The value to filter
  168. * @return The filtered value
  169. */
  170. public static function theme()
  171. {
  172. $filter_args = func_get_args();
  173. $hookname = array_shift( $filter_args );
  174. $filtersets = array();
  175. if ( !isset( self::$hooks['theme'][$hookname] ) ) {
  176. if ( substr( $hookname, -6 ) != '_empty' ) {
  177. array_unshift( $filter_args, $hookname . '_empty' );
  178. return call_user_func_array( array( 'Plugins', 'theme' ), $filter_args );
  179. }
  180. return array();
  181. }
  182. $return = array();
  183. foreach ( self::$hooks['theme'][$hookname] as $priority ) {
  184. foreach ( $priority as $filter ) {
  185. // $filter is an array of object reference and method name
  186. $callargs = $filter_args;
  187. if ( is_array( $filter ) ) {
  188. if ( is_string( $filter[0] ) ) {
  189. $module = $filter[0];
  190. }
  191. else {
  192. $module = get_class( $filter[0] );
  193. if ( $filter[0] instanceof Theme && $module != get_class( $callargs[0] ) ) {
  194. continue;
  195. }
  196. }
  197. }
  198. else {
  199. $module = $filter;
  200. }
  201. $return[$module] = call_user_func_array( $filter, $callargs );
  202. }
  203. }
  204. if ( count( $return ) == 0 && substr( $hookname, -6 ) != '_empty' ) {
  205. array_unshift( $filter_args, $hookname . '_empty' );
  206. $result = call_user_func_array( array( 'Plugins', 'theme' ), $filter_args );
  207. }
  208. array_unshift( $filter_args, 'theme_call_' . $hookname, $return );
  209. $result = call_user_func_array( array( 'Plugins', 'filter' ), $filter_args );
  210. return $result;
  211. }
  212. /**
  213. * Determine if any plugin implements the indicated theme hook
  214. *
  215. * @param string $hookname The name of the hook to check for
  216. * @return boolean True if the hook is implemented
  217. */
  218. public static function theme_implemented( $hookname )
  219. {
  220. return isset( self::$hooks['theme'][$hookname] );
  221. }
  222. /**
  223. * function list_active
  224. * Gets a list of active plugin filenames to be included
  225. * @param boolean Whether to refresh the cached array. Default false
  226. * @return array An array of filenames
  227. */
  228. public static function list_active( $refresh = false )
  229. {
  230. if ( empty( self::$plugin_files ) || $refresh ) {
  231. $plugins = Options::get( 'active_plugins' );
  232. if ( is_array( $plugins ) ) {
  233. foreach ( $plugins as $class => $filename ) {
  234. // add base path to stored path
  235. $filename = HABARI_PATH . $filename;
  236. // if class is somehow empty we'll throw an error when trying to load it - deactivate the plugin instead
  237. if ( $class == '' ) {
  238. self::deactivate_plugin( $filename, true );
  239. EventLog::log( _t( 'An empty plugin definition pointing to file "%1$s" was removed.', array( $filename ) ), 'err', 'plugin', 'habari' );
  240. // and skip adding it to the active stack
  241. continue;
  242. }
  243. if ( file_exists( $filename ) ) {
  244. self::$plugin_files[$class] = $filename;
  245. }
  246. else {
  247. // file does not exist, deactivate plugin
  248. self::deactivate_plugin( $filename, true );
  249. EventLog::log( _t( 'Plugin "%1$s" deactivated because it could no longer be found.', array( $class ) ), 'err', 'plugin', 'habari', $filename );
  250. }
  251. }
  252. }
  253. // make sure things work on Windows
  254. self::$plugin_files = array_map( create_function( '$s', 'return str_replace(\'\\\\\', \'/\', $s);' ), self::$plugin_files );
  255. }
  256. return self::$plugin_files;
  257. }
  258. /**
  259. * Returns the internally stored references to all loaded plugins
  260. * @return array An array of plugin objects
  261. */
  262. public static function get_active()
  263. {
  264. return self::$plugins;
  265. }
  266. /**
  267. * Get references to plugin objects that implement a specific interface
  268. * @param string $interface The interface to check for
  269. * @return array An array of matching plugins
  270. */
  271. public static function get_by_interface( $interface )
  272. {
  273. return array_filter( self::$plugins, create_function( '$a', 'return $a instanceof ' . $interface . ';' ) );
  274. }
  275. /**
  276. * function list_all
  277. * Gets a list of all plugin filenames that are available
  278. * @return array An array of filenames
  279. */
  280. public static function list_all()
  281. {
  282. $plugins = array();
  283. $plugindirs = array( HABARI_PATH . '/system/plugins/', HABARI_PATH . '/3rdparty/plugins/', HABARI_PATH . '/user/plugins/' );
  284. if ( Site::CONFIG_LOCAL != Site::$config_type ) {
  285. // include site-specific plugins
  286. $plugindirs[] = Site::get_dir( 'config' ) . '/plugins/';
  287. }
  288. $dirs = array();
  289. foreach ( $plugindirs as $plugindir ) {
  290. if ( file_exists( $plugindir ) ) {
  291. $dirs = array_merge( $dirs, Utils::glob( $plugindir . '*', GLOB_ONLYDIR | GLOB_MARK ) );
  292. }
  293. }
  294. foreach ( $dirs as $dir ) {
  295. $dirfiles = Utils::glob( $dir . '*.plugin.php' );
  296. if ( ! empty( $dirfiles ) ) {
  297. $dirfiles = array_combine(
  298. // Use the basename of the file as the index to use the named plugin from the last directory in $dirs
  299. array_map( 'basename', $dirfiles ),
  300. // massage the filenames so that this works on Windows
  301. array_map( create_function( '$s', 'return str_replace(\'\\\\\', \'/\', $s);' ), $dirfiles )
  302. );
  303. $plugins = array_merge( $plugins, $dirfiles );
  304. }
  305. }
  306. ksort( $plugins );
  307. return $plugins;
  308. }
  309. /**
  310. * Get classes that extend Plugin.
  311. * @param $class string A class name
  312. * @return boolean true if the class extends Plugin
  313. */
  314. public static function extends_plugin( $class )
  315. {
  316. $parents = class_parents( $class, false );
  317. return in_array( 'Plugin', $parents );
  318. }
  319. /**
  320. * function class_from_filename
  321. * returns the class name from a plugin's filename
  322. * @param string $file the full path to a plugin file
  323. * @param bool $check_realpath whether or not to try realpath resolution
  324. * @return string the class name
  325. */
  326. public static function class_from_filename( $file, $check_realpath = false )
  327. {
  328. if ( $check_realpath ) {
  329. $file = realpath( $file );
  330. }
  331. foreach ( self::get_plugin_classes() as $plugin ) {
  332. $class = new ReflectionClass( $plugin );
  333. $classfile = str_replace( '\\', '/', $class->getFileName() );
  334. if ( $classfile == $file ) {
  335. return $plugin;
  336. }
  337. }
  338. // if we haven't found the plugin class, try again with realpath resolution:
  339. if ( $check_realpath ) {
  340. // really can't find it
  341. return false;
  342. }
  343. else {
  344. return self::class_from_filename( $file, true );
  345. }
  346. }
  347. public static function get_plugin_classes()
  348. {
  349. $classes = get_declared_classes();
  350. return array_filter( $classes, array( 'Plugins', 'extends_plugin' ) );
  351. }
  352. /**
  353. * Initialize all loaded plugins by calling their load() method
  354. * @param string $file the class name to load
  355. * @param boolean $activate True if the plugin's load() method should be called
  356. * @return Plugin The instantiated plugin class
  357. */
  358. public static function load_from_file( $file, $activate = true )
  359. {
  360. $class = self::class_from_filename( $file );
  361. return self::load( $class, $activate );
  362. }
  363. /**
  364. * Return the info XML for a plugin based on a filename
  365. *
  366. * @param string $file The filename of the plugin file
  367. * @return SimpleXMLElement The info structure for the plugin, or null if no info could be loaded
  368. */
  369. public static function load_info( $file )
  370. {
  371. $info = null;
  372. $xml_file = preg_replace( '%\.plugin\.php$%i', '.plugin.xml', $file );
  373. if ( file_exists( $xml_file ) && $xml_content = file_get_contents( $xml_file ) ) {
  374. // tell libxml to throw exceptions and let us check for errors
  375. $old_error = libxml_use_internal_errors( true );
  376. try {
  377. $info = new SimpleXMLElement( $xml_content );
  378. // if the xml file uses a theme element name instead of pluggable, it's old
  379. if ( $info->getName() != 'pluggable' ) {
  380. $info = 'legacy';
  381. }
  382. }
  383. catch ( Exception $e ) {
  384. EventLog::log( _t( 'Invalid plugin XML file: %1$s', array( $xml_file ) ), 'err', 'plugin' );
  385. $info = 'broken';
  386. }
  387. // restore the old error level
  388. libxml_use_internal_errors( $old_error );
  389. }
  390. return $info;
  391. }
  392. /**
  393. * Load a pluign into memory by class name
  394. *
  395. * @param string $class The name of the class of the plugin to load
  396. * @param boolean $activate True to run the load routine of the plugin and add it to the loaded plugins list
  397. * @return Plugin The instance of the created plugin
  398. */
  399. public static function load( $class, $activate = true )
  400. {
  401. $plugin = new $class;
  402. if ( $activate ) {
  403. self::$plugins[$plugin->plugin_id] = $plugin;
  404. $plugin->load();
  405. $plugin->upgrade();
  406. }
  407. return $plugin;
  408. }
  409. /**
  410. * Upgrade all loaded plugins
  411. */
  412. public static function upgrade( )
  413. {
  414. foreach(self::$plugins as $plugin) {
  415. $plugin->upgrade();
  416. }
  417. }
  418. /**
  419. * Instatiate and load all active plugins
  420. */
  421. public static function load_active()
  422. {
  423. foreach ( self::list_active() as $class => $filename ) {
  424. if ( file_exists( $filename ) ) {
  425. self::load( $class );
  426. }
  427. }
  428. }
  429. /**
  430. * Returns a plugin id for the filename specified.
  431. * Used to unify the way plugin ids are generated, rather than spreading the
  432. * calls internal to this function over several files.
  433. *
  434. * @param string $file The filename to generate an id for
  435. * @return string A plugin id.
  436. */
  437. public static function id_from_file( $file )
  438. {
  439. $file = str_replace( array( '\\', '/' ), PATH_SEPARATOR, realpath( $file ) );
  440. return sprintf( '%x', crc32( $file ) );
  441. }
  442. /**
  443. * Activates a plugin file
  444. */
  445. public static function activate_plugin( $file )
  446. {
  447. $ok = true;
  448. // strip base path from stored path
  449. $short_file = MultiByte::substr( $file, strlen( HABARI_PATH ) );
  450. $activated = Options::get( 'active_plugins' );
  451. if ( !is_array( $activated ) || !in_array( $short_file, $activated ) ) {
  452. include_once( $file );
  453. $class = Plugins::class_from_filename( $file );
  454. $plugin = Plugins::load( $class );
  455. $ok = Plugins::filter( 'activate_plugin', $ok, $file ); // Allow plugins to reject activation
  456. }
  457. else if ( is_array( $activated) && in_array( $short_file, $activated ) ) {
  458. $ok = false;
  459. }
  460. if ( $ok ) {
  461. $activated[$class] = $short_file;
  462. Options::set( 'active_plugins', $activated );
  463. $versions = Options::get( 'pluggable_versions' );
  464. if(!isset($versions[$class])) {
  465. $versions[$class] = $plugin->get_version();
  466. Options::set( 'pluggable_versions', $versions );
  467. }
  468. if ( method_exists( $plugin, 'action_plugin_activation' ) ) {
  469. $plugin->action_plugin_activation( $file ); // For the plugin to install itself
  470. }
  471. Plugins::act( 'plugin_activated', $file ); // For other plugins to react to a plugin install
  472. EventLog::log( _t( 'Activated Plugin: %s', array( $plugin->info->name ) ), 'notice', 'plugin', 'habari' );
  473. }
  474. return $ok;
  475. }
  476. /**
  477. * Deactivates a plugin file
  478. */
  479. public static function deactivate_plugin( $file, $force = false )
  480. {
  481. $ok = true;
  482. $name = '';
  483. $ok = Plugins::filter( 'deactivate_plugin', $ok, $file ); // Allow plugins to reject deactivation
  484. if ( $ok || $force == true ) {
  485. // normalize directory separator
  486. $file = str_replace( '\\', '/', $file );
  487. // strip base path from stored path
  488. $short_file = MultiByte::substr( $file, MultiByte::strlen( HABARI_PATH ) );
  489. $activated = Options::get( 'active_plugins' );
  490. $index = array_search( $short_file, $activated );
  491. if ( is_array( $activated ) && ( false !== $index ) ) {
  492. if ( $force != true ) {
  493. // Get plugin name for logging
  494. $name = self::$plugins[Plugins::id_from_file( $file )]->info->name;
  495. if ( method_exists( self::$plugins[Plugins::id_from_file( $file )], 'action_plugin_deactivation' ) ) {
  496. self::$plugins[Plugins::id_from_file( $file )]->action_plugin_deactivation( $file ); // For the plugin to uninstall itself
  497. }
  498. }
  499. unset( $activated[$index] );
  500. Options::set( 'active_plugins', $activated );
  501. if ( $force != true ) {
  502. Plugins::act( 'plugin_deactivated', $file ); // For other plugins to react to a plugin uninstallation
  503. EventLog::log( _t( 'Deactivated Plugin: %s', array( $name ) ), 'notice', 'plugin', 'habari' );
  504. }
  505. }
  506. }
  507. if ( $force == true ) {
  508. // always return true for forced deactivations
  509. return true;
  510. }
  511. else {
  512. return $ok;
  513. }
  514. }
  515. /**
  516. * Detects whether the plugins that exist have changed since they were last
  517. * activated.
  518. * @return boolean true if the plugins have changed, false if not.
  519. */
  520. public static function changed_since_last_activation()
  521. {
  522. $old_plugins = Options::get( 'plugins_present' );
  523. //self::set_present();
  524. // If the plugin list was never stored, then they've changed.
  525. if ( !is_array( $old_plugins ) ) {
  526. return true;
  527. }
  528. // add base path onto stored path
  529. foreach ( $old_plugins as $old_plugin ) {
  530. $old_plugin = HABARI_PATH . $old_plugin;
  531. }
  532. // If the file list is not identical, then they've changed.
  533. $new_plugin_files = Plugins::list_all();
  534. $old_plugin_files = array_map( create_function( '$a', 'return $a["file"];' ), $old_plugins );
  535. if ( count( array_intersect( $new_plugin_files, $old_plugin_files ) ) != count( $new_plugin_files ) ) {
  536. return true;
  537. }
  538. // If the files are not identical, then they've changed.
  539. $old_plugin_checksums = array_map( create_function( '$a', 'return $a["checksum"];' ), $old_plugins );
  540. $new_plugin_checksums = array_map( 'md5_file', $new_plugin_files );
  541. if ( count( array_intersect( $old_plugin_checksums, $new_plugin_checksums ) ) != count( $new_plugin_checksums ) ) {
  542. return true;
  543. }
  544. return false;
  545. }
  546. /**
  547. * Stores the list of plugins that are present (not necessarily active) in
  548. * the Options table for future comparison.
  549. */
  550. public static function set_present()
  551. {
  552. $plugin_files = Plugins::list_all();
  553. // strip base path
  554. foreach ( $plugin_files as $plugin_file ) {
  555. $plugin_file = MultiByte::substr( $file, MultiByte::strlen( HABARI_PATH ) );
  556. }
  557. $plugin_data = array_map( create_function( '$a', 'return array( "file" => $a, "checksum" => md5_file( $a ) );' ), $plugin_files );
  558. Options::set( 'plugins_present', $plugin_data );
  559. }
  560. /**
  561. * Verify if a plugin is loaded.
  562. * You may supply an optional argument $version as a minimum version requirement.
  563. *
  564. * @param string $name Name or class name of the plugin to find.
  565. * @param string $version Optional minimal version of the plugin.
  566. * @return bool Returns true if name is found and version is equal or higher than required.
  567. */
  568. public static function is_loaded( $name, $version = null )
  569. {
  570. foreach ( self::$plugins as $plugin ) {
  571. if ( is_null( $plugin->info ) || $plugin->info == 'broken' || $plugin->info == 'invalid' ) {
  572. continue;
  573. }
  574. if ( MultiByte::strtolower( $plugin->info->name ) == MultiByte::strtolower( $name ) || $plugin instanceof $name || ( isset( $plugin->info->guid ) && MultiByte::strtolower( $plugin->info->guid ) == MultiByte::strtolower( $name ) ) ) {
  575. if ( isset( $version ) ) {
  576. if ( isset( $plugin->info->version ) ) {
  577. return version_compare( $plugin->info->version, $version, '>=' );
  578. }
  579. else {
  580. return $version == null;
  581. }
  582. }
  583. else {
  584. return true;
  585. }
  586. }
  587. }
  588. return false;
  589. }
  590. /**
  591. * Check the PHP syntax of every plugin available, activated or not.
  592. *
  593. * @see Utils::php_check_file_syntax()
  594. * @return bool Returns true if all plugins were valid, return false if a plugin (or more) failed.
  595. */
  596. public static function check_every_plugin_syntax()
  597. {
  598. $failed_plugins = array();
  599. $all_plugins = self::list_all();
  600. foreach ( $all_plugins as $file ) {
  601. $error = '';
  602. if ( !Utils::php_check_file_syntax( $file, $error ) ) {
  603. Session::error( sprintf( _t( 'Attempted to load the plugin file "%s", but it failed with syntax errors. <div class="reveal">%s</div>' ), basename( $file ), $error ) );
  604. $failed_plugins[] = $file;
  605. }
  606. }
  607. Options::set( 'failed_plugins', $failed_plugins );
  608. Plugins::set_present();
  609. return ( count( $failed_plugins ) > 0 ) ? false : true;
  610. }
  611. /**
  612. * Produce the UI for a plugin based on the user's selected config option
  613. *
  614. * @param string $configure The id of the configured plugin
  615. * @param string $configuration The selected configuration option
  616. **/
  617. public static function plugin_ui( $configure, $configaction )
  618. {
  619. Plugins::act_id( 'plugin_ui_' . $configaction, $configure, $configure, $configaction );
  620. Plugins::act( 'plugin_ui_any_' . $configaction, $configure, $configaction );
  621. Plugins::act_id( 'plugin_ui', $configure, $configure, $configaction );
  622. Plugins::act( 'plugin_ui_any', $configure, $configaction );
  623. }
  624. }
  625. ?>