diff --git a/application/Config.php b/application/Config.php new file mode 100755 index 0000000..0b01b52 --- /dev/null +++ b/application/Config.php @@ -0,0 +1,129 @@ + $value) { + $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($config['config'][$key], true).';'. PHP_EOL; + } + $configStr .= '?>'; + + if (!file_put_contents($config['config']['CONFIG_FILE'], $configStr) + || strcmp(file_get_contents($config['config']['CONFIG_FILE']), $configStr) != 0 + ) { + throw new Exception( + 'Shaarli could not create the config file. + Please make sure Shaarli has the right to write in the folder is it installed in.' + ); + } +} + +/** + * 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. + */ +class MissingFieldConfigException extends Exception +{ + public $field; + + /** + * Construct exception. + * + * @param string $field field name missing. + */ + public function __construct($field) + { + $this->field = $field; + $this->message = 'Configuration value is required for '. $this->field; + } +} + +/** + * Exception used if an unauthorized attempt to edit configuration has been made. + */ +class UnauthorizedConfigException extends Exception +{ + /** + * Construct exception. + */ + public function __construct() + { + $this->message = 'You are not authorized to alter config.'; + } +} \ No newline at end of file diff --git a/index.php b/index.php index 5771dd8..236fd4e 100644 --- a/index.php +++ b/index.php @@ -11,7 +11,8 @@ date_default_timezone_set('UTC'); // ----------------------------------------------------------------------------------------------- -// Hardcoded parameter (These parameters can be overwritten by creating the file /data/options.php) +// Hardcoded parameter (These parameters can be overwritten by editing the file /data/config.php) +// You should not touch any code below (or at your own risks!) $GLOBALS['config']['DATADIR'] = 'data'; // Data subdirectory $GLOBALS['config']['CONFIG_FILE'] = $GLOBALS['config']['DATADIR'].'/config.php'; // Configuration file (user login/password) $GLOBALS['config']['DATASTORE'] = $GLOBALS['config']['DATADIR'].'/datastore.php'; // Data storage file. @@ -36,10 +37,6 @@ $GLOBALS['config']['ARCHIVE_ORG'] = false; // For each link, add a link to an ar $GLOBALS['config']['ENABLE_RSS_PERMALINKS'] = true; // Enable RSS permalinks by default. This corresponds to the default behavior of shaarli before this was added as an option. $GLOBALS['config']['HIDE_PUBLIC_LINKS'] = false; // ----------------------------------------------------------------------------------------------- -// You should not touch below (or at your own risks!) -// Optional config file. -if (is_file($GLOBALS['config']['DATADIR'].'/options.php')) require($GLOBALS['config']['DATADIR'].'/options.php'); - define('shaarli_version','0.0.45beta'); // http://server.com/x/shaarli --> /shaarli/ define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); @@ -69,6 +66,7 @@ error_reporting(E_ALL^E_WARNING); // See all error except warnings. // Shaarli library require_once 'application/LinkDB.php'; require_once 'application/Utils.php'; +require_once 'application/Config.php'; include "inc/rain.tpl.class.php"; //include Rain TPL raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory @@ -100,7 +98,6 @@ if (empty($GLOBALS['title'])) $GLOBALS['title']='Shared links on '.escape(indexU if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get(); if (empty($GLOBALS['redirector'])) $GLOBALS['redirector']=''; if (empty($GLOBALS['disablesessionprotection'])) $GLOBALS['disablesessionprotection']=false; -if (empty($GLOBALS['disablejquery'])) $GLOBALS['disablejquery']=false; if (empty($GLOBALS['privateLinkByDefault'])) $GLOBALS['privateLinkByDefault']=false; if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?'; // I really need to rewrite Shaarli with a proper configuation manager. @@ -1220,7 +1217,19 @@ function renderPage() // Save new password $GLOBALS['salt'] = sha1(uniqid('',true).'_'.mt_rand()); // Salt renders rainbow-tables attacks useless. $GLOBALS['hash'] = sha1($_POST['setpassword'].$GLOBALS['login'].$GLOBALS['salt']); - writeConfig(); + try { + writeConfig($GLOBALS, isLoggedIn()); + } + catch(Exception $e) { + error_log( + 'ERROR while writing config file after changing password.' . PHP_EOL . + $e->getMessage() + ); + + // TODO: do not handle exceptions/errors in JS. + echo ''; + exit; + } echo ''; exit; } @@ -1249,12 +1258,23 @@ function renderPage() $GLOBALS['titleLink']=$_POST['titleLink']; $GLOBALS['redirector']=$_POST['redirector']; $GLOBALS['disablesessionprotection']=!empty($_POST['disablesessionprotection']); - $GLOBALS['disablejquery']=!empty($_POST['disablejquery']); $GLOBALS['privateLinkByDefault']=!empty($_POST['privateLinkByDefault']); $GLOBALS['config']['ENABLE_RSS_PERMALINKS']= !empty($_POST['enableRssPermalinks']); $GLOBALS['config']['ENABLE_UPDATECHECK'] = !empty($_POST['updateCheck']); $GLOBALS['config']['HIDE_PUBLIC_LINKS'] = !empty($_POST['hidePublicLinks']); - writeConfig(); + try { + writeConfig($GLOBALS, isLoggedIn()); + } + catch(Exception $e) { + error_log( + 'ERROR while writing config file after configuration update.' . PHP_EOL . + $e->getMessage() + ); + + // TODO: do not handle exceptions/errors in JS. + echo ''; + exit; + } echo ''; exit; } @@ -2013,7 +2033,19 @@ function install() $GLOBALS['hash'] = sha1($_POST['setpassword'].$GLOBALS['login'].$GLOBALS['salt']); $GLOBALS['title'] = (empty($_POST['title']) ? 'Shared links on '.escape(indexUrl()) : $_POST['title'] ); $GLOBALS['config']['ENABLE_UPDATECHECK'] = !empty($_POST['updateCheck']); - writeConfig(); + try { + writeConfig($GLOBALS, isLoggedIn()); + } + catch(Exception $e) { + error_log( + 'ERROR while writing config file after installation.' . PHP_EOL . + $e->getMessage() + ); + + // TODO: do not handle exceptions/errors in JS. + echo ''; + exit; + } echo ''; exit; } @@ -2127,30 +2159,7 @@ if (!function_exists('json_encode')) { } } -// Re-write configuration file according to globals. -// Requires some $GLOBALS to be set (login,hash,salt,title). -// If the config file cannot be saved, an error message is displayed and the user is redirected to "Tools" menu. -// (otherwise, the function simply returns.) -function writeConfig() -{ - if (is_file($GLOBALS['config']['CONFIG_FILE']) && !isLoggedIn()) die('You are not authorized to alter config.'); // Only logged in user can alter config. - $config=''; - if (!file_put_contents($GLOBALS['config']['CONFIG_FILE'],$config) || strcmp(file_get_contents($GLOBALS['config']['CONFIG_FILE']),$config)!=0) - { - echo ''; - exit; - } -} + /* Because some f*cking services like flickr require an extra HTTP request to get the thumbnail URL, I have deported the thumbnail URL code generation here, otherwise this would slow down page generation. @@ -2379,6 +2388,15 @@ function invalidateCaches() pageCache::purgeCache(); // Purge page cache shared by sessions. } +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 new file mode 100755 index 0000000..4279c57 --- /dev/null +++ b/tests/ConfigTest.php @@ -0,0 +1,177 @@ + 'login', + 'hash' => 'hash', + 'salt' => 'salt', + 'timezone' => 'Europe/Paris', + 'title' => 'title', + 'titleLink' => 'titleLink', + 'redirector' => '', + 'disablesessionprotection' => false, + 'privateLinkByDefault' => false, + 'config' => [ + 'CONFIG_FILE' => 'tests/config.php', + 'DATADIR' => 'tests', + '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']); + } + } + + /** + * Test writeConfig function, valid use case, while being logged in. + */ + public function testWriteConfig() + { + writeConfig(self::$_configFields, true); + + include self::$_configFields['config']['CONFIG_FILE']; + $this->assertEquals(self::$_configFields['login'], $GLOBALS['login']); + $this->assertEquals(self::$_configFields['hash'], $GLOBALS['hash']); + $this->assertEquals(self::$_configFields['salt'], $GLOBALS['salt']); + $this->assertEquals(self::$_configFields['timezone'], $GLOBALS['timezone']); + $this->assertEquals(self::$_configFields['title'], $GLOBALS['title']); + $this->assertEquals(self::$_configFields['titleLink'], $GLOBALS['titleLink']); + $this->assertEquals(self::$_configFields['redirector'], $GLOBALS['redirector']); + $this->assertEquals(self::$_configFields['disablesessionprotection'], $GLOBALS['disablesessionprotection']); + $this->assertEquals(self::$_configFields['privateLinkByDefault'], $GLOBALS['privateLinkByDefault']); + $this->assertEquals(self::$_configFields['config']['config1'], $GLOBALS['config']['config1']); + $this->assertEquals(self::$_configFields['config']['config2'], $GLOBALS['config']['config2']); + } + + /** + * Test writeConfig option while logged in: + * 1. init fields. + * 2. update fields, add new sub config, add new root config. + * 3. rewrite config. + * 4. check result. + */ + public function testWriteConfigFieldUpdate() + { + writeConfig(self::$_configFields, true); + self::$_configFields['title'] = 'ok'; + self::$_configFields['config']['config1'] = 'ok'; + self::$_configFields['config']['config_new'] = 'ok'; + self::$_configFields['new'] = 'should not be saved'; + writeConfig(self::$_configFields, true); + + include self::$_configFields['config']['CONFIG_FILE']; + $this->assertEquals('ok', $GLOBALS['title']); + $this->assertEquals('ok', $GLOBALS['config']['config1']); + $this->assertEquals('ok', $GLOBALS['config']['config_new']); + $this->assertFalse(isset($GLOBALS['new'])); + } + + /** + * Test writeConfig function with an empty array. + * + * @expectedException MissingFieldConfigException + */ + public function testWriteConfigEmpty() + { + writeConfig(array(), true); + } + + /** + * Test writeConfig function with a missing mandatory field. + * + * @expectedException MissingFieldConfigException + */ + public function testWriteConfigMissingField() + { + unset(self::$_configFields['login']); + writeConfig(self::$_configFields, true); + } + + /** + * Test writeConfig function while being logged out, and there is no config file existing. + */ + public function testWriteConfigLoggedOutNoFile() + { + writeConfig(self::$_configFields, false); + } + + /** + * Test writeConfig function while being logged out, and a config file already exists. + * + * @expectedException UnauthorizedConfigException + */ + public function testWriteConfigLoggedOutWithFile() + { + file_put_contents(self::$_configFields['config']['CONFIG_FILE'], ''); + 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']); + } +} \ No newline at end of file