From 59ed39769893f44cb6f17242a2daf184c5d7c1d9 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 15 Jul 2015 11:42:15 +0200 Subject: [PATCH] Plugin system - CORE I retrieved @nodiscc plugin system proposed in #164 and changed it to create PHP plugin system. It relies on hooks triggered by certain actions (only template rendering for now). **It's only a proposition, let me know what you think of it**. * I think that an 'only template' plugin system might be too restrictive, and doesn't allow a lot of extension. * I raised concerns in #44 and don't blend too well with user made templates. * @nodiscc lacks of time to finish this. * I'd like to see 0.9beta release one day. :) PluginManager class includes enabled plugin PHP files at loading and register their hook function. When we want to trigger a hook, 'PluginManager::executeHooks()' is called, eventually with rendering data. It will call every plugin function registered under the standardized name: hook__ Rendering data can be altered and/or completed. This is exactly what @nodiscc did. Templates contain plugin display at specific location, which are populated by the plugin functions. Here is what's has been done: * hook_render_linklist: when linklist is rendered, all links data passed to plugins. It allows plugins to alter link rendering (such as Markdown parsing). They can also add a linklist icon for every link (QRCode, etc.) * hook_render_header: every page builder triggers this hook. Plugins can add specific data to header if the current page is concerned (toolbar). * hook_render_footer: : every page builder triggers this hook. Plugins can add specific data to header if the current page is concerned (JS file). * hook_render_includes: : every page builder triggers this hook. Plugins can add specific data to header if the current page is concerned (CSS file). We can easily add hooks to whatever is pertinent (link add, picwal rendering, etc.). * Strong documentation, especially for plugin developers. * Unit tests for PluginManger and Router. * Test this heavily. Later: * finish Markdown plugin. * Add a plugin page in administration. --- .gitignore | 3 + application/Config.php | 7 ++- application/Plugin.php | 118 +++++++++++++++++++++++++++++++++++++++ application/Router.php | 100 +++++++++++++++++++++++++++++++++ index.php | 122 ++++++++++++++++++++++++++--------------- 5 files changed, 306 insertions(+), 44 deletions(-) create mode 100644 application/Plugin.php create mode 100644 application/Router.php diff --git a/.gitignore b/.gitignore index 6fd0ccd80..68cc81563 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ composer.lock coverage tests/datastore.php phpmd.html + +# Ignore user plugin configuration +plugins/*/config.php \ No newline at end of file diff --git a/application/Config.php b/application/Config.php index ec799d7f7..08b2a368a 100755 --- a/application/Config.php +++ b/application/Config.php @@ -56,7 +56,12 @@ function writeConfig($config, $isLoggedIn) foreach ($config['config'] as $key => $value) { $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($config['config'][$key], true).';'. PHP_EOL; } - $configStr .= '?>'; + + if (isset($config['plugins'])) { + foreach ($config['plugins'] as $key => $value) { + $configStr .= '$GLOBALS[\'plugins\'][\''. $key .'\'] = '.var_export($config['plugins'][$key], true).';'. PHP_EOL; + } + } if (!file_put_contents($config['config']['CONFIG_FILE'], $configStr) || strcmp(file_get_contents($config['config']['CONFIG_FILE']), $configStr) != 0 diff --git a/application/Plugin.php b/application/Plugin.php new file mode 100644 index 000000000..e05a75a6c --- /dev/null +++ b/application/Plugin.php @@ -0,0 +1,118 @@ +getMessage()); + } + } + } + } + + /** + * Load a single plugin from its files. + * Add them in $__LOADED_PLUGINS if successful. + * + * @param string $dir - plugin's directory. + * @param string $pluginName - plugin's name. + * @throws PluginFileNotFoundException - plugin files not found. + */ + private static function loadPlugin($dir, $pluginName) { + if (!is_dir($dir)) { + throw new PluginFileNotFoundException($pluginName); + } + + $pluginFilePath = $dir . '/' . $pluginName . '.php'; + if (!is_file($pluginFilePath)) { + throw new PluginFileNotFoundException($pluginName); + } + + include $pluginFilePath; + + self::$_LOADED_PLUGINS[] = $pluginName; + } + + /** + * Execute all plugins registered hook. + * + * @param string $hook - name of the hook to trigger. + * @param array $data - list of data to manipulate passed by reference. + * @param array $params - additional parameters such as page target for common templates. + */ + public static function executeHooks($hook, &$data, $params = array()) { + if (!empty($params['target'])) { + $data['_PAGE_'] = $params['target']; + } + + if (isset($params['loggedin'])) { + $data['_LOGGEDIN_'] = $params['loggedin']; + } + + foreach (self::$_LOADED_PLUGINS as $plugin) { + $hookFunction = self::buildHookName($hook, $plugin); + + if (function_exists($hookFunction)) { + $data = call_user_func($hookFunction, $data); + } + } + } + + /** + * Construct normalize hook name for a specific plugin. + * + * Format: + * hook__ + * + * @param $hook - hook name. + * @param $pluginName - plugin name. + * + * @return string - plugin's hook name. + */ + public static function buildHookName($hook, $pluginName) { + return 'hook_' . $pluginName . '_' . $hook; + } +} + +/** + * Class PluginFileNotFoundException + * + * Raise when plugin files can't be found. + */ +class PluginFileNotFoundException extends Exception +{ + public function __construct($pluginName) { + $this->message = 'Plugin "'. $pluginName .'" files not found.'; + } +} \ No newline at end of file diff --git a/application/Router.php b/application/Router.php new file mode 100644 index 000000000..c73153815 --- /dev/null +++ b/application/Router.php @@ -0,0 +1,100 @@ + /shaarli/ @@ -72,6 +81,8 @@ require_once 'application/TimeZone.php'; require_once 'application/Utils.php'; require_once 'application/Config.php'; +require_once 'application/Plugin.php'; +require_once 'application/Router.php'; // Ensure the PHP version is supported try { @@ -86,6 +97,9 @@ raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory raintpl::$cache_dir = $GLOBALS['config']['RAINTPL_TMP']; // cache directory +PluginManager::$AUTHORIZED_PLUGINS = $GLOBALS['config']['ENABLED_PLUGINS']; +PluginManager::load(); + ob_start(); // Output buffering for the page cache. @@ -1024,6 +1038,12 @@ function showDaily() exit; } +// Renders the linklist +function showLinkList($PAGE, $LINKSDB) { + buildLinkList($PAGE,$LINKSDB); // Compute list of links to display + $PAGE->renderPage('linklist'); +} + // ------------------------------------------------------------------------------------------ // Render HTML page (according to URL parameters and user rights) @@ -1035,12 +1055,35 @@ function renderPage() $GLOBALS['config']['HIDE_PUBLIC_LINKS'] ); + $PAGE = new pageBuilder; + + // Determine which page will be rendered. + $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; + $target = Router::findPage($query, $_GET, isLoggedIn()); + + // Call plugin hooks for header, footer and includes, specifying which page will be rendered. + // Then asign generated data to RainTPL. + $common_hooks = array( + 'header', + 'footer', + 'includes', + ); + foreach($common_hooks as $name) { + $plugin_data = array(); + PluginManager::executeHooks('render_' . $name, $plugin_data, + array( + 'target' => $target, + 'loggedin' => isLoggedIn() + ) + ); + $PAGE->assign('plugins_' . $name, $plugin_data); + } + // -------- Display login form. - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=login')) + if ($target == Router::$PAGE_LOGIN) { if ($GLOBALS['config']['OPEN_SHAARLI']) { header('Location: ?'); exit; } // No need to login for open Shaarli $token=''; if (ban_canLogin()) $token=getToken(); // Do not waste token generation if not useful. - $PAGE = new pageBuilder; $PAGE->assign('token',$token); $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):'')); $PAGE->renderPage('loginform'); @@ -1056,7 +1099,7 @@ function renderPage() } // -------- Picture wall - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=picwall')) + if ($target == Router::$PAGE_PICWALL) { // Optionally filter the results: $links=array(); @@ -1079,7 +1122,6 @@ function renderPage() } } - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('linksToDisplay',$linksToDisplay); $PAGE->renderPage('picwall'); @@ -1087,7 +1129,7 @@ function renderPage() } // -------- Tag cloud - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=tagcloud')) + if ($target == Router::$PAGE_TAGCLOUD) { $tags= $LINKSDB->allTags(); @@ -1101,7 +1143,6 @@ function renderPage() { $tagList[$key] = array('count'=>$value,'size'=>log($value, 15) / log($maxcount, 30) * (22-6) + 6); } - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('tags',$tagList); $PAGE->renderPage('tagcloud'); @@ -1216,19 +1257,15 @@ function renderPage() header('Location: ?do=login&post='); exit; } - - $PAGE = new pageBuilder; - buildLinkList($PAGE,$LINKSDB); // Compute list of links to display - $PAGE->renderPage('linklist'); + showLinkList($PAGE, $LINKSDB); exit; // Never remove this one! All operations below are reserved for logged in user. } // -------- All other functions are reserved for the registered user: // -------- Display the Tools menu if requested (import/export/bookmarklet...) - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=tools')) + if ($target == Router::$PAGE_TOOLS) { - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('pageabsaddr',indexUrl()); $PAGE->renderPage('tools'); @@ -1236,7 +1273,7 @@ function renderPage() } // -------- User wants to change his/her password. - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=changepasswd')) + if ($target == Router::$PAGE_CHANGEPASSWORD) { if ($GLOBALS['config']['OPEN_SHAARLI']) die('You are not supposed to change a password on an Open Shaarli.'); if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) @@ -1267,7 +1304,6 @@ function renderPage() } else // show the change password form. { - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('token',getToken()); $PAGE->renderPage('changepassword'); @@ -1276,7 +1312,7 @@ function renderPage() } // -------- User wants to change configuration - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=configure')) + if ($target == Router::$PAGE_CONFIGURE) { if (!empty($_POST['title']) ) { @@ -1312,7 +1348,6 @@ function renderPage() } else // Show the configuration form. { - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('token',getToken()); $PAGE->assign('title', empty($GLOBALS['title']) ? '' : $GLOBALS['title'] ); @@ -1326,11 +1361,10 @@ function renderPage() } // -------- User wants to rename a tag or delete it - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=changetag')) + if ($target == Router::$PAGE_CHANGETAG) { if (empty($_POST['fromtag'])) { - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('token',getToken()); $PAGE->assign('tags', $LINKSDB->allTags()); @@ -1375,9 +1409,8 @@ function renderPage() } // -------- User wants to add a link without using the bookmarklet: Show form. - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=addlink')) + if ($target == Router::$PAGE_ADDLINK) { - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->renderPage('addlink'); exit; @@ -1470,7 +1503,6 @@ function renderPage() { $link = $LINKSDB[$_GET['edit_link']]; // Read database if (!$link) { header('Location: ?'); exit; } // Link not found in database. - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('link',$link); $PAGE->assign('link_is_new',false); @@ -1555,7 +1587,6 @@ function renderPage() $link = array('linkdate'=>$linkdate,'title'=>$title,'url'=>$url,'description'=>$description,'tags'=>$tags,'private'=>$private); } - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('link',$link); $PAGE->assign('link_is_new',$link_is_new); @@ -1568,11 +1599,10 @@ function renderPage() } // -------- Export as Netscape Bookmarks HTML file. - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=export')) + if ($target == Router::$PAGE_EXPORT) { if (empty($_GET['what'])) { - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->renderPage('export'); exit; @@ -1624,9 +1654,8 @@ function renderPage() } // -------- Show upload/import dialog: - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=import')) + if ($target == Router::$PAGE_IMPORT) { - $PAGE = new pageBuilder; $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('token',getToken()); $PAGE->assign('maxfilesize',getMaxFileSize()); @@ -1635,9 +1664,7 @@ function renderPage() } // -------- Otherwise, simply display search form and links: - $PAGE = new pageBuilder; - buildLinkList($PAGE,$LINKSDB); // Compute list of links to display - $PAGE->renderPage('linklist'); + showLinkList($PAGE, $LINKSDB); exit; } @@ -1808,7 +1835,7 @@ function buildLinkList($PAGE,$LINKSDB) $taglist = explode(' ',$link['tags']); uasort($taglist, 'strcasecmp'); $link['taglist']=$taglist; - + $link['shorturl'] = smallHash($link['linkdate']); if ($link["url"][0] === '?' && // Check for both signs of a note: starting with ? and 7 chars long. I doubt that you'll post any links that look like this. strlen($link["url"]) === 7) { $link["url"] = indexUrl() . $link["url"]; @@ -1828,18 +1855,27 @@ function buildLinkList($PAGE,$LINKSDB) $token = ''; if (isLoggedIn()) $token=getToken(); // Fill all template fields. - $PAGE->assign('linkcount',count($LINKSDB)); - $PAGE->assign('previous_page_url',$previous_page_url); - $PAGE->assign('next_page_url',$next_page_url); - $PAGE->assign('page_current',$page); - $PAGE->assign('page_max',$pagecount); - $PAGE->assign('result_count',count($linksToDisplay)); - $PAGE->assign('search_type',$search_type); - $PAGE->assign('search_crits',$search_crits); - $PAGE->assign('redirector',empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector']); // Optional redirector URL. - $PAGE->assign('token',$token); - $PAGE->assign('links',$linkDisp); - $PAGE->assign('tags', $LINKSDB->allTags()); + $data = array( + 'linkcount' => count($LINKSDB), + 'previous_page_url' => $previous_page_url, + 'next_page_url' => $next_page_url, + 'page_current' => $page, + 'page_max' => $pagecount, + 'result_count' => count($linksToDisplay), + 'search_type' => $search_type, + 'search_crits' => $search_crits, + 'redirector' => empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'], // Optional redirector URL. + 'token' => $token, + 'links' => $linkDisp, + 'tags' => $LINKSDB->allTags(), + ); + + PluginManager::executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn())); + + foreach ($data as $key => $value) { + $PAGE->assign($key, $value); + } + return; }