From: Arthur Date: Mon, 15 Feb 2016 19:38:31 +0000 (+0100) Subject: Merge pull request #479 from ArthurHoaro/pluginsadmin-error-url X-Git-Tag: v0.6.4~13 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=6e607ca613b47e17f7516e94adfee930d4f3e1e8;hp=59edea42bbd53af627d2d9eca74f95dc4b43bbd0;p=github%2Fshaarli%2FShaarli.git Merge pull request #479 from ArthurHoaro/pluginsadmin-error-url Fixes typo in plugin admin redirection URL --- diff --git a/application/Config.php b/application/Config.php index 9af5a535..05a59452 100644 --- a/application/Config.php +++ b/application/Config.php @@ -173,33 +173,6 @@ function load_plugin_parameter_values($plugins, $config) return $out; } -/** - * Milestone 0.9 - shaarli/Shaarli#41: options.php is not supported anymore. - * ==> if user is loggedIn, merge its content with config.php, then delete options.php. - * - * @param array $config contains all configuration fields. - * @param bool $isLoggedIn true if user is logged in. - * - * @return void - */ -function mergeDeprecatedConfig($config, $isLoggedIn) -{ - $config_file = $config['config']['CONFIG_FILE']; - - if (is_file($config['config']['DATADIR'].'/options.php') && $isLoggedIn) { - include $config['config']['DATADIR'].'/options.php'; - - // Load GLOBALS into config - foreach ($GLOBALS as $key => $value) { - $config[$key] = $value; - } - $config['config']['CONFIG_FILE'] = $config_file; - writeConfig($config, $isLoggedIn); - - unlink($config['config']['DATADIR'].'/options.php'); - } -} - /** * Exception used if a mandatory field is missing in given configuration. */ diff --git a/application/LinkDB.php b/application/LinkDB.php index 19ca6435..a95b3f36 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@ -260,14 +260,11 @@ You use the community supported version of the original Shaarli project, by Seba } } - // Keep the list of the mapping URLs-->linkdate up-to-date. $this->_urls = array(); - foreach ($this->_links as $link) { + foreach ($this->_links as &$link) { + // Keep the list of the mapping URLs-->linkdate up-to-date. $this->_urls[$link['url']] = $link['linkdate']; - } - - // Escape links data - foreach($this->_links as &$link) { + // Sanitize data fields. sanitizeLink($link); // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). if (!empty($this->_redirector) && !startsWith($link['url'], '?')) { @@ -381,6 +378,7 @@ You use the community supported version of the original Shaarli project, by Seba } $linkDays = array_keys($linkDays); sort($linkDays); + return $linkDays; } } diff --git a/application/LinkFilter.php b/application/LinkFilter.php index b2e6530f..096d3b04 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php @@ -260,7 +260,6 @@ class LinkFilter * Convert a list of tags (str) to an array. Also * - handle case sensitivity. * - accepts spaces commas as separator. - * - remove private tags for loggedout users. * * @param string $tags string containing a list of tags. * @param bool $casesensitive will convert everything to lowercase if false. diff --git a/application/Updater.php b/application/Updater.php new file mode 100644 index 00000000..20ae0c4d --- /dev/null +++ b/application/Updater.php @@ -0,0 +1,228 @@ +doneUpdates = $doneUpdates; + $this->config = $config; + $this->linkDB = $linkDB; + $this->isLoggedIn = $isLoggedIn; + + // Retrieve all update methods. + $class = new ReflectionClass($this); + $this->methods = $class->getMethods(); + } + + /** + * Run all new updates. + * Update methods have to start with 'updateMethod' and return true (on success). + * + * @return array An array containing ran updates. + * + * @throws UpdaterException If something went wrong. + */ + public function update() + { + $updatesRan = array(); + + // If the user isn't logged in, exit without updating. + if ($this->isLoggedIn !== true) { + return $updatesRan; + } + + if ($this->methods == null) { + throw new UpdaterException('Couldn\'t retrieve Updater class methods.'); + } + + foreach ($this->methods as $method) { + // Not an update method or already done, pass. + if (! startsWith($method->getName(), 'updateMethod') + || in_array($method->getName(), $this->doneUpdates) + ) { + continue; + } + + try { + $method->setAccessible(true); + $res = $method->invoke($this); + // Update method must return true to be considered processed. + if ($res === true) { + $updatesRan[] = $method->getName(); + } + } catch (Exception $e) { + throw new UpdaterException($method, $e); + } + } + + $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan); + + return $updatesRan; + } + + /** + * @return array Updates methods already processed. + */ + public function getDoneUpdates() + { + return $this->doneUpdates; + } + + /** + * Move deprecated options.php to config.php. + * + * Milestone 0.9 (old versioning) - shaarli/Shaarli#41: + * options.php is not supported anymore. + */ + public function updateMethodMergeDeprecatedConfigFile() + { + $config_file = $this->config['config']['CONFIG_FILE']; + + if (is_file($this->config['config']['DATADIR'].'/options.php')) { + include $this->config['config']['DATADIR'].'/options.php'; + + // Load GLOBALS into config + foreach ($GLOBALS as $key => $value) { + $this->config[$key] = $value; + } + $this->config['config']['CONFIG_FILE'] = $config_file; + writeConfig($this->config, $this->isLoggedIn); + + unlink($this->config['config']['DATADIR'].'/options.php'); + } + + return true; + } +} + +/** + * Class UpdaterException. + */ +class UpdaterException extends Exception +{ + /** + * @var string Method where the error occurred. + */ + protected $method; + + /** + * @var Exception The parent exception. + */ + protected $previous; + + /** + * Constructor. + * + * @param string $message Force the error message if set. + * @param string $method Method where the error occurred. + * @param Exception|bool $previous Parent exception. + */ + public function __construct($message = '', $method = '', $previous = false) + { + $this->method = $method; + $this->previous = $previous; + $this->message = $this->buildMessage($message); + } + + /** + * Build the exception error message. + * + * @param string $message Optional given error message. + * + * @return string The built error message. + */ + private function buildMessage($message) + { + $out = ''; + if (! empty($message)) { + $out .= $message . PHP_EOL; + } + + if (! empty($this->method)) { + $out .= 'An error occurred while running the update '. $this->method . PHP_EOL; + } + + if (! empty($this->previous)) { + $out .= ' '. $this->previous->getMessage(); + } + + return $out; + } +} + + +/** + * Read the updates file, and return already done updates. + * + * @param string $updatesFilepath Updates file path. + * + * @return array Already done update methods. + */ +function read_updates_file($updatesFilepath) +{ + if (! empty($updatesFilepath) && is_file($updatesFilepath)) { + $content = file_get_contents($updatesFilepath); + if (! empty($content)) { + return explode(';', $content); + } + } + return array(); +} + +/** + * Write updates file. + * + * @param string $updatesFilepath Updates file path. + * @param array $updates Updates array to write. + * + * @throws Exception Couldn't write version number. + */ +function write_updates_file($updatesFilepath, $updates) +{ + if (empty($updatesFilepath)) { + throw new Exception('Updates file path is not set, can\'t write updates.'); + } + + $res = file_put_contents($updatesFilepath, implode(';', $updates)); + if ($res === false) { + throw new Exception('Unable to write updates in '. $updatesFilepath . '.'); + } +} diff --git a/index.php b/index.php index 67290f6f..4382bd80 100644 --- a/index.php +++ b/index.php @@ -44,6 +44,9 @@ $GLOBALS['config']['DATASTORE'] = $GLOBALS['config']['DATADIR'].'/datastore.php' // Banned IPs $GLOBALS['config']['IPBANS_FILENAME'] = $GLOBALS['config']['DATADIR'].'/ipbans.php'; +// Processed updates file. +$GLOBALS['config']['UPDATES_FILE'] = $GLOBALS['config']['DATADIR'].'/updates.txt'; + // Access log $GLOBALS['config']['LOG_FILE'] = $GLOBALS['config']['DATADIR'].'/log.txt'; @@ -64,7 +67,6 @@ $GLOBALS['config']['CACHEDIR'] = 'cache'; // Atom & RSS feed cache directory $GLOBALS['config']['PAGECACHE'] = 'pagecache'; - /* * Global configuration */ @@ -163,6 +165,7 @@ require_once 'application/Utils.php'; require_once 'application/Config.php'; require_once 'application/PluginManager.php'; require_once 'application/Router.php'; +require_once 'application/Updater.php'; // Ensure the PHP version is supported try { @@ -1114,6 +1117,25 @@ function renderPage() $GLOBALS['redirector'] ); + $updater = new Updater( + read_updates_file($GLOBALS['config']['UPDATES_FILE']), + $GLOBALS, + $LINKSDB, + isLoggedIn() + ); + try { + $newUpdates = $updater->update(); + if (! empty($newUpdates)) { + write_updates_file( + $GLOBALS['config']['UPDATES_FILE'], + $updater->getDoneUpdates() + ); + } + } + catch(Exception $e) { + die($e->getMessage()); + } + $PAGE = new pageBuilder; // Determine which page will be rendered. @@ -1530,21 +1552,40 @@ function renderPage() // -------- User clicked the "Save" button when editing a link: Save link to database. if (isset($_POST['save_edit'])) { - if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! - $tags = trim(preg_replace('/\s\s+/',' ', $_POST['lf_tags'])); // Remove multiple spaces. - $tags = implode(' ', array_unique(explode(' ', $tags))); // Remove duplicates. - $linkdate=$_POST['lf_linkdate']; + // Go away! + if (! tokenOk($_POST['token'])) { + die('Wrong token.'); + } + // Remove multiple spaces. + $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags'])); + // Remove duplicates. + $tags = implode(' ', array_unique(explode(' ', $tags))); + $linkdate = $_POST['lf_linkdate']; $url = trim($_POST['lf_url']); - if (!startsWith($url,'http:') && !startsWith($url,'https:') && !startsWith($url,'ftp:') && !startsWith($url,'magnet:') && !startsWith($url,'?') && !startsWith($url,'javascript:')) - $url = 'http://'.$url; - $link = array('title'=>trim($_POST['lf_title']),'url'=>$url,'description'=>trim($_POST['lf_description']),'private'=>(isset($_POST['lf_private']) ? 1 : 0), - 'linkdate'=>$linkdate,'tags'=>str_replace(',',' ',$tags)); - if ($link['title']=='') $link['title']=$link['url']; // If title is empty, use the URL as title. + if (! startsWith($url, 'http:') && ! startsWith($url, 'https:') + && ! startsWith($url, 'ftp:') && ! startsWith($url, 'magnet:') + && ! startsWith($url, '?') && ! startsWith($url, 'javascript:') + ) { + $url = 'http://' . $url; + } + + $link = array( + 'title' => trim($_POST['lf_title']), + 'url' => $url, + 'description' => trim($_POST['lf_description']), + 'private' => (isset($_POST['lf_private']) ? 1 : 0), + 'linkdate' => $linkdate, + 'tags' => str_replace(',', ' ', $tags) + ); + // If title is empty, use the URL as title. + if ($link['title'] == '') { + $link['title'] = $link['url']; + } $pluginManager->executeHooks('save_link', $link); $LINKSDB[$linkdate] = $link; - $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk. + $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); pubsubhub(); // If we are called from the bookmarklet, we must close the popup: @@ -1553,10 +1594,12 @@ function renderPage() exit; } - $returnurl = !empty($_POST['returnurl']) ? escape($_POST['returnurl']): '?'; + $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?'; $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link')); - $location .= '#'.smallHash($_POST['lf_linkdate']); // Scroll to the link which has been edited. - header('Location: '. $location); // After saving the link, redirect to the page the user was on. + // Scroll to the link which has been edited. + $location .= '#' . smallHash($_POST['lf_linkdate']); + // After saving the link, redirect to the page the user was on. + header('Location: '. $location); exit; } @@ -2519,15 +2562,6 @@ function resizeImage($filepath) return true; } -try { - mergeDeprecatedConfig($GLOBALS, isLoggedIn()); -} catch(Exception $e) { - error_log( - 'ERROR while merging deprecated options.php file.' . PHP_EOL . - $e->getMessage() - ); -} - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=genthumbnail')) { genThumbnail(); exit; } // Thumbnail generation/cache does not need the link database. if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=rss')) { showRSS(); exit; } if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=atom')) { showATOM(); exit; } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 492ddd3b..7200aae6 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -133,48 +133,6 @@ class ConfigTest extends PHPUnit_Framework_TestCase writeConfig(self::$configFields, false); } - /** - * Test mergeDeprecatedConfig while being logged in: - * 1. init a config file. - * 2. init a options.php file with update value. - * 3. merge. - * 4. check updated value in config file. - */ - public function testMergeDeprecatedConfig() - { - // init - writeConfig(self::$configFields, true); - $configCopy = self::$configFields; - $invert = !$configCopy['privateLinkByDefault']; - $configCopy['privateLinkByDefault'] = $invert; - - // Use writeConfig to create a options.php - $configCopy['config']['CONFIG_FILE'] = 'tests/options.php'; - writeConfig($configCopy, true); - - $this->assertTrue(is_file($configCopy['config']['CONFIG_FILE'])); - - // merge configs - mergeDeprecatedConfig(self::$configFields, true); - - // make sure updated field is changed - include self::$configFields['config']['CONFIG_FILE']; - $this->assertEquals($invert, $GLOBALS['privateLinkByDefault']); - $this->assertFalse(is_file($configCopy['config']['CONFIG_FILE'])); - } - - /** - * Test mergeDeprecatedConfig while being logged in without options file. - */ - public function testMergeDeprecatedConfigNoFile() - { - writeConfig(self::$configFields, true); - mergeDeprecatedConfig(self::$configFields, true); - - include self::$configFields['config']['CONFIG_FILE']; - $this->assertEquals(self::$configFields['login'], $GLOBALS['login']); - } - /** * Test save_plugin_config with valid data. * diff --git a/tests/Updater/DummyUpdater.php b/tests/Updater/DummyUpdater.php new file mode 100644 index 00000000..e9ef2aaa --- /dev/null +++ b/tests/Updater/DummyUpdater.php @@ -0,0 +1,68 @@ +methods = $class->getMethods(ReflectionMethod::IS_FINAL); + } + + /** + * Update method 1. + * + * @return bool true. + */ + private final function updateMethodDummy1() + { + return true; + } + + /** + * Update method 2. + * + * @return bool true. + */ + private final function updateMethodDummy2() + { + return true; + } + + /** + * Update method 3. + * + * @return bool true. + */ + private final function updateMethodDummy3() + { + return true; + } + + /** + * Update method 4, raise an exception. + * + * @throws Exception error. + */ + private final function updateMethodException() + { + throw new Exception('whatever'); + } +} diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php new file mode 100644 index 00000000..63ed5e03 --- /dev/null +++ b/tests/Updater/UpdaterTest.php @@ -0,0 +1,227 @@ + 'login', + 'hash' => 'hash', + 'salt' => 'salt', + 'timezone' => 'Europe/Paris', + 'title' => 'title', + 'titleLink' => 'titleLink', + 'redirector' => '', + 'disablesessionprotection' => false, + 'privateLinkByDefault' => false, + 'config' => array( + 'CONFIG_FILE' => 'tests/Updater/config.php', + 'DATADIR' => 'tests/Updater', + 'config1' => 'config1data', + 'config2' => 'config2data', + ) + ); + } + + /** + * Executed after each test. + * + * @return void + */ + public function tearDown() + { + if (is_file(self::$configFields['config']['CONFIG_FILE'])) { + unlink(self::$configFields['config']['CONFIG_FILE']); + } + + if (is_file(self::$configFields['config']['DATADIR'] . '/options.php')) { + unlink(self::$configFields['config']['DATADIR'] . '/options.php'); + } + + if (is_file(self::$configFields['config']['DATADIR'] . '/updates.json')) { + unlink(self::$configFields['config']['DATADIR'] . '/updates.json'); + } + } + + /** + * Test read_updates_file with an empty/missing file. + */ + public function testReadEmptyUpdatesFile() + { + $this->assertEquals(array(), read_updates_file('')); + $updatesFile = self::$configFields['config']['DATADIR'] . '/updates.json'; + touch($updatesFile); + $this->assertEquals(array(), read_updates_file($updatesFile)); + } + + /** + * Test read/write updates file. + */ + public function testReadWriteUpdatesFile() + { + $updatesFile = self::$configFields['config']['DATADIR'] . '/updates.json'; + $updatesMethods = array('m1', 'm2', 'm3'); + + write_updates_file($updatesFile, $updatesMethods); + $readMethods = read_updates_file($updatesFile); + $this->assertEquals($readMethods, $updatesMethods); + + // Update + $updatesMethods[] = 'm4'; + write_updates_file($updatesFile, $updatesMethods); + $readMethods = read_updates_file($updatesFile); + $this->assertEquals($readMethods, $updatesMethods); + } + + /** + * Test errors in write_updates_file(): empty updates file. + * + * @expectedException Exception + * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/ + */ + public function testWriteEmptyUpdatesFile() + { + write_updates_file('', array('test')); + } + + /** + * Test errors in write_updates_file(): not writable updates file. + * + * @expectedException Exception + * @expectedExceptionMessageRegExp /Unable to write(.*)/ + */ + public function testWriteUpdatesFileNotWritable() + { + $updatesFile = self::$configFields['config']['DATADIR'] . '/updates.json'; + touch($updatesFile); + chmod($updatesFile, 0444); + @write_updates_file($updatesFile, array('test')); + } + + /** + * Test the update() method, with no update to run. + * 1. Everything already run. + * 2. User is logged out. + */ + public function testNoUpdates() + { + $updates = array( + 'updateMethodDummy1', + 'updateMethodDummy2', + 'updateMethodDummy3', + 'updateMethodException', + ); + $updater = new DummyUpdater($updates, array(), array(), true); + $this->assertEquals(array(), $updater->update()); + + $updater = new DummyUpdater(array(), array(), array(), false); + $this->assertEquals(array(), $updater->update()); + } + + /** + * Test the update() method, with all updates to run (except the failing one). + */ + public function testUpdatesFirstTime() + { + $updates = array('updateMethodException',); + $expectedUpdates = array( + 'updateMethodDummy1', + 'updateMethodDummy2', + 'updateMethodDummy3', + ); + $updater = new DummyUpdater($updates, array(), array(), true); + $this->assertEquals($expectedUpdates, $updater->update()); + } + + /** + * Test the update() method, only one update to run. + */ + public function testOneUpdate() + { + $updates = array( + 'updateMethodDummy1', + 'updateMethodDummy3', + 'updateMethodException', + ); + $expectedUpdate = array('updateMethodDummy2'); + + $updater = new DummyUpdater($updates, array(), array(), true); + $this->assertEquals($expectedUpdate, $updater->update()); + } + + /** + * Test Update failed. + * + * @expectedException UpdaterException + */ + public function testUpdateFailed() + { + $updates = array( + 'updateMethodDummy1', + 'updateMethodDummy2', + 'updateMethodDummy3', + ); + + $updater = new DummyUpdater($updates, array(), array(), true); + $updater->update(); + } + + /** + * Test update mergeDeprecatedConfig: + * 1. init a config file. + * 2. init a options.php file with update value. + * 3. merge. + * 4. check updated value in config file. + */ + public function testUpdateMergeDeprecatedConfig() + { + // init + writeConfig(self::$configFields, true); + $configCopy = self::$configFields; + $invert = !$configCopy['privateLinkByDefault']; + $configCopy['privateLinkByDefault'] = $invert; + + // Use writeConfig to create a options.php + $configCopy['config']['CONFIG_FILE'] = 'tests/Updater/options.php'; + writeConfig($configCopy, true); + + $this->assertTrue(is_file($configCopy['config']['CONFIG_FILE'])); + + // merge configs + $updater = new Updater(array(), self::$configFields, array(), true); + $updater->updateMethodMergeDeprecatedConfigFile(); + + // make sure updated field is changed + include self::$configFields['config']['CONFIG_FILE']; + $this->assertEquals($invert, $GLOBALS['privateLinkByDefault']); + $this->assertFalse(is_file($configCopy['config']['CONFIG_FILE'])); + } + + /** + * Test mergeDeprecatedConfig in without options file. + */ + public function testMergeDeprecatedConfigNoFile() + { + writeConfig(self::$configFields, true); + + $updater = new Updater(array(), self::$configFields, array(), true); + $updater->updateMethodMergeDeprecatedConfigFile(); + + include self::$configFields['config']['CONFIG_FILE']; + $this->assertEquals(self::$configFields['login'], $GLOBALS['login']); + } +}