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