Adds ConfigJson which handle the configuration in JSON format.

Also use the Updater to make the transition
This commit is contained in:
ArthurHoaro 2016-05-29 12:32:14 +02:00
parent 684e662a58
commit b74b96bfbd
13 changed files with 469 additions and 32 deletions

View file

@ -75,6 +75,7 @@ class PageBuilder
$this->tpl->assign('shaarlititle', $conf->get('title', 'Shaarli'));
$this->tpl->assign('openshaarli', $conf->get('config.OPEN_SHAARLI', false));
$this->tpl->assign('showatom', $conf->get('config.SHOW_ATOM', false));
$this->tpl->assign('hide_timestamps', $conf->get('config.HIDE_TIMESTAMPS', false));
// FIXME! Globals
if (!empty($GLOBALS['plugin_errors'])) {
$this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']);

View file

@ -142,6 +142,48 @@ class Updater
$this->linkDB->savedb($conf->get('config.PAGECACHE'));
return true;
}
/**
* Move old configuration in PHP to the new config system in JSON format.
*
* Will rename 'config.php' into 'config.save.php' and create 'config.json'.
*/
public function updateMethodConfigToJson()
{
$conf = ConfigManager::getInstance();
// JSON config already exists, nothing to do.
if ($conf->getConfigIO() instanceof ConfigJson) {
return true;
}
$configPhp = new ConfigPhp();
$configJson = new ConfigJson();
$oldConfig = $configPhp->read($conf::$CONFIG_FILE . '.php');
rename($conf->getConfigFile(), $conf::$CONFIG_FILE . '.save.php');
$conf->setConfigIO($configJson);
$conf->reload();
foreach (ConfigPhp::$ROOT_KEYS as $key) {
$conf->set($key, $oldConfig[$key]);
}
// Set sub config keys (config and plugins)
$subConfig = array('config', 'plugins');
foreach ($subConfig as $sub) {
foreach ($oldConfig[$sub] as $key => $value) {
$conf->set($sub .'.'. $key, $value);
}
}
try{
$conf->write($this->isLoggedIn);
return true;
} catch (IOException $e) {
error_log($e->getMessage());
return false;
}
}
}
/**
@ -199,7 +241,6 @@ class UpdaterException extends Exception
}
}
/**
* Read the updates file, and return already done updates.
*

View file

@ -21,8 +21,6 @@ interface ConfigIO
*
* @param string $filepath Config file absolute path.
* @param array $conf All configuration in an array.
*
* @return bool True if the configuration has been successfully written, false otherwise.
*/
function write($filepath, $conf);

View file

@ -0,0 +1,66 @@
<?php
/**
* Class ConfigJson (ConfigIO implementation)
*
* Handle Shaarli's JSON configuration file.
*/
class ConfigJson implements ConfigIO
{
/**
* The JSON data is wrapped in a PHP file for security purpose.
* This way, even if the file is accessible, credentials and configuration won't be exposed.
*
* @var string PHP start tag and comment tag.
*/
public static $PHP_HEADER;
public function __construct()
{
// The field can't be initialized directly with concatenation before PHP 5.6.
self::$PHP_HEADER = '<?php /*'. PHP_EOL;
}
/**
* @inheritdoc
*/
function read($filepath)
{
if (! file_exists($filepath) || ! is_readable($filepath)) {
return array();
}
$data = file_get_contents($filepath);
$data = str_replace(self::$PHP_HEADER, '', $data);
$data = json_decode($data, true);
if ($data === null) {
$error = json_last_error();
throw new Exception('An error occured while parsing JSON file: error code #'. $error);
}
return $data;
}
/**
* @inheritdoc
*/
function write($filepath, $conf)
{
// JSON_PRETTY_PRINT is available from PHP 5.4.
$print = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
$data = self::$PHP_HEADER . json_encode($conf, $print);
if (!file_put_contents($filepath, $data)) {
throw new IOException(
$filepath,
'Shaarli could not create the config file.
Please make sure Shaarli has the right to write in the folder is it installed in.'
);
}
}
/**
* @inheritdoc
*/
function getExtension()
{
return '.json.php';
}
}

View file

@ -3,7 +3,7 @@
// FIXME! Namespaces...
require_once 'ConfigIO.php';
require_once 'ConfigPhp.php';
#require_once 'ConfigJson.php';
require_once 'ConfigJson.php';
/**
* Class ConfigManager
@ -84,12 +84,11 @@ class ConfigManager
*/
protected function initialize()
{
/*if (! file_exists(self::$CONFIG_FILE .'.php')) {
if (! file_exists(self::$CONFIG_FILE .'.php')) {
$this->configIO = new ConfigJson();
} else {
$this->configIO = new ConfigPhp();
}*/
$this->configIO = new ConfigPhp();
}
$this->load();
}

View file

@ -20,9 +20,9 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
protected static $testDatastore = 'sandbox/datastore.php';
/**
* @var string Config file path.
* @var string Config file path (without extension).
*/
protected static $configFile = 'tests/Updater/config.php';
protected static $configFile = 'tests/utils/config/configUpdater';
/**
* @var ConfigManager
@ -52,8 +52,9 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
)
);
ConfigManager::$CONFIG_FILE = 'tests/Updater/config';
$this->conf = ConfigManager::getInstance();
ConfigManager::$CONFIG_FILE = self::$configFile;
$this->conf = ConfigManager::reset();
$this->conf->reload();
foreach (self::$configFields as $key => $value) {
$this->conf->set($key, $value);
}
@ -67,8 +68,8 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
*/
public function tearDown()
{
if (is_file(self::$configFile)) {
unlink(self::$configFile);
if (is_file('tests/Updater/config.json')) {
unlink('tests/Updater/config.json');
}
if (is_file(self::$configFields['config']['DATADIR'] . '/options.php')) {
@ -214,6 +215,8 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
{
// Use writeConfig to create a options.php
ConfigManager::$CONFIG_FILE = 'tests/Updater/options';
$this->conf->setConfigIO(new ConfigPhp());
$invert = !$this->conf->get('privateLinkByDefault');
$this->conf->set('privateLinkByDefault', $invert);
$this->conf->write(true);
@ -225,12 +228,15 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
// merge configs
$updater = new Updater(array(), array(), true);
// This writes a new config file in tests/Updater/config.php
$updater->updateMethodMergeDeprecatedConfigFile();
// make sure updated field is changed
$this->conf->reload();
$this->assertEquals($invert, $this->conf->get('privateLinkByDefault'));
$this->assertFalse(is_file($optionsFile));
// Delete the generated file.
unlink($this->conf->getConfigFile());
}
/**
@ -257,4 +263,52 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
$updater->updateMethodRenameDashTags();
$this->assertNotEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
}
/**
* Convert old PHP config file to JSON config.
*/
public function testConfigToJson()
{
$configFile = 'tests/utils/config/configPhp';
ConfigManager::$CONFIG_FILE = $configFile;
$conf = ConfigManager::reset();
// The ConfigIO is initialized with ConfigPhp.
$this->assertTrue($conf->getConfigIO() instanceof ConfigPhp);
$updater = new Updater(array(), array(), false);
$done = $updater->updateMethodConfigToJson();
$this->assertTrue($done);
// The ConfigIO has been updated to ConfigJson.
$this->assertTrue($conf->getConfigIO() instanceof ConfigJson);
$this->assertTrue(file_exists($conf->getConfigFile()));
// Check JSON config data.
$conf->reload();
$this->assertEquals('root', $conf->get('login'));
$this->assertEquals('lala', $conf->get('redirector'));
$this->assertEquals('data/datastore.php', $conf->get('config.DATASTORE'));
$this->assertEquals('1', $conf->get('plugins.WALLABAG_VERSION'));
rename($configFile . '.save.php', $configFile . '.php');
unlink($conf->getConfigFile());
}
/**
* Launch config conversion update with an existing JSON file => nothing to do.
*/
public function testConfigToJsonNothingToDo()
{
$configFile = 'tests/utils/config/configUpdateDone';
ConfigManager::$CONFIG_FILE = $configFile;
$conf = ConfigManager::reset();
$conf->reload();
$filetime = filemtime($conf->getConfigFile());
$updater = new Updater(array(), array(), false);
$done = $updater->updateMethodConfigToJson();
$this->assertTrue($done);
$expected = filemtime($conf->getConfigFile());
$this->assertEquals($expected, $filetime);
}
}

View file

@ -0,0 +1,125 @@
<?php
require_once 'application/config/ConfigJson.php';
/**
* Class ConfigJsonTest
*/
class ConfigJsonTest extends PHPUnit_Framework_TestCase
{
/**
* @var ConfigJson
*/
protected $configIO;
public function setUp()
{
$this->configIO = new ConfigJson();
}
/**
* Read a simple existing config file.
*/
public function testRead()
{
$conf = $this->configIO->read('tests/utils/config/configJson.json.php');
$this->assertEquals('root', $conf['login']);
$this->assertEquals('lala', $conf['redirector']);
$this->assertEquals('data/datastore.php', $conf['config']['DATASTORE']);
$this->assertEquals('1', $conf['plugins']['WALLABAG_VERSION']);
}
/**
* Read a non existent config file -> empty array.
*/
public function testReadNonExistent()
{
$this->assertEquals(array(), $this->configIO->read('nope'));
}
/**
* Read a non existent config file -> empty array.
*
* @expectedException Exception
* @expectedExceptionMessage An error occured while parsing JSON file: error code #4
*/
public function testReadInvalidJson()
{
$this->configIO->read('tests/utils/config/configInvalid.json.php');
}
/**
* Write a new config file.
*/
public function testWriteNew()
{
$dataFile = 'tests/utils/config/configWrite.json.php';
$data = array(
'login' => 'root',
'redirector' => 'lala',
'config' => array(
'DATASTORE' => 'data/datastore.php',
),
'plugins' => array(
'WALLABAG_VERSION' => '1',
)
);
$this->configIO->write($dataFile, $data);
// PHP 5.3 doesn't support json pretty print.
if (defined('JSON_PRETTY_PRINT')) {
$expected = '{
"login": "root",
"redirector": "lala",
"config": {
"DATASTORE": "data\/datastore.php"
},
"plugins": {
"WALLABAG_VERSION": "1"
}
}';
} else {
$expected = '{"login":"root","redirector":"lala","config":{"DATASTORE":"data\/datastore.php"},"plugins":{"WALLABAG_VERSION":"1"}}';
}
$expected = ConfigJson::$PHP_HEADER . $expected;
$this->assertEquals($expected, file_get_contents($dataFile));
unlink($dataFile);
}
/**
* Overwrite an existing setting.
*/
public function testOverwrite()
{
$source = 'tests/utils/config/configJson.json.php';
$dest = 'tests/utils/config/configOverwrite.json.php';
copy($source, $dest);
$conf = $this->configIO->read($dest);
$conf['redirector'] = 'blabla';
$this->configIO->write($dest, $conf);
$conf = $this->configIO->read($dest);
$this->assertEquals('blabla', $conf['redirector']);
unlink($dest);
}
/**
* Write to invalid path.
*
* @expectedException IOException
*/
public function testWriteInvalidArray()
{
$conf = array('conf' => 'value');
@$this->configIO->write(array(), $conf);
}
/**
* Write to invalid path.
*
* @expectedException IOException
*/
public function testWriteInvalidBlank()
{
$conf = array('conf' => 'value');
@$this->configIO->write('', $conf);
}
}

View file

@ -6,7 +6,7 @@
* Note: it only test the manager with ConfigJson,
* ConfigPhp is only a workaround to handle the transition to JSON type.
*/
class ConfigManagerTest extends \PHPUnit_Framework_TestCase
class ConfigManagerTest extends PHPUnit_Framework_TestCase
{
/**
* @var ConfigManager
@ -15,34 +15,160 @@ class ConfigManagerTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
ConfigManager::$CONFIG_FILE = 'tests/config/config';
$this->conf = ConfigManager::getInstance();
ConfigManager::$CONFIG_FILE = 'tests/utils/config/configJson';
$this->conf = ConfigManager::reset();
}
public function tearDown()
/**
* Simple config test:
* 1. Set settings.
* 2. Check settings value.
*/
public function testSetGet()
{
@unlink($this->conf->getConfigFile());
}
public function testSetWriteGet()
{
// This won't work with ConfigPhp.
$this->markTestIncomplete();
$this->conf->set('paramInt', 42);
$this->conf->set('paramString', 'value1');
$this->conf->set('paramBool', false);
$this->conf->set('paramArray', array('foo' => 'bar'));
$this->conf->set('paramNull', null);
$this->conf->write(true);
$this->conf->reload();
$this->assertEquals(42, $this->conf->get('paramInt'));
$this->assertEquals('value1', $this->conf->get('paramString'));
$this->assertFalse($this->conf->get('paramBool'));
$this->assertEquals(array('foo' => 'bar'), $this->conf->get('paramArray'));
$this->assertEquals(null, $this->conf->get('paramNull'));
}
}
/**
* Set/write/get config test:
* 1. Set settings.
* 2. Write it to the config file.
* 3. Read the file.
* 4. Check settings value.
*/
public function testSetWriteGet()
{
$this->conf->set('paramInt', 42);
$this->conf->set('paramString', 'value1');
$this->conf->set('paramBool', false);
$this->conf->set('paramArray', array('foo' => 'bar'));
$this->conf->set('paramNull', null);
ConfigManager::$CONFIG_FILE = 'tests/utils/config/configTmp';
$this->conf->write(true);
$this->conf->reload();
unlink($this->conf->getConfigFile());
$this->assertEquals(42, $this->conf->get('paramInt'));
$this->assertEquals('value1', $this->conf->get('paramString'));
$this->assertFalse($this->conf->get('paramBool'));
$this->assertEquals(array('foo' => 'bar'), $this->conf->get('paramArray'));
$this->assertEquals(null, $this->conf->get('paramNull'));
}
/**
* Test set/write/get with nested keys.
*/
public function testSetWriteGetNested()
{
$this->conf->set('foo.bar.key.stuff', 'testSetWriteGetNested');
ConfigManager::$CONFIG_FILE = 'tests/utils/config/configTmp';
$this->conf->write(true);
$this->conf->reload();
unlink($this->conf->getConfigFile());
$this->assertEquals('testSetWriteGetNested', $this->conf->get('foo.bar.key.stuff'));
}
/**
* Set with an empty key.
*
* @expectedException Exception
* @expectedExceptionMessageRegExp #^Invalid setting key parameter. String expected, got.*#
*/
public function testSetEmptyKey()
{
$this->conf->set('', 'stuff');
}
/**
* Set with an array key.
*
* @expectedException Exception
* @expectedExceptionMessageRegExp #^Invalid setting key parameter. String expected, got.*#
*/
public function testSetArrayKey()
{
$this->conf->set(array('foo' => 'bar'), 'stuff');
}
/**
* Try to write the config without mandatory parameter (e.g. 'login').
*
* @expectedException MissingFieldConfigException
*/
public function testWriteMissingParameter()
{
ConfigManager::$CONFIG_FILE = 'tests/utils/config/configTmp';
$this->assertFalse(file_exists($this->conf->getConfigFile()));
$this->conf->reload();
$this->conf->write(true);
}
/**
* Try to get non existent config keys.
*/
public function testGetNonExistent()
{
$this->assertEquals('', $this->conf->get('nope.test'));
$this->assertEquals('default', $this->conf->get('nope.test', 'default'));
}
/**
* Test the 'exists' method with existent values.
*/
public function testExistsOk()
{
$this->assertTrue($this->conf->exists('login'));
$this->assertTrue($this->conf->exists('config.foo'));
}
/**
* Test the 'exists' method with non existent or invalid values.
*/
public function testExistsKo()
{
$this->assertFalse($this->conf->exists('nope'));
$this->assertFalse($this->conf->exists('nope.nope'));
$this->assertFalse($this->conf->exists(''));
$this->assertFalse($this->conf->exists(false));
}
/**
* Reset the ConfigManager instance.
*/
public function testReset()
{
$conf = $this->conf;
$this->assertTrue($conf === ConfigManager::getInstance());
$this->assertFalse($conf === $this->conf->reset());
$this->assertFalse($conf === ConfigManager::getInstance());
}
/**
* Reload the config from file.
*/
public function testReload()
{
ConfigManager::$CONFIG_FILE = 'tests/utils/config/configTmp';
$newConf = ConfigJson::$PHP_HEADER . '{ "key": "value" }';
file_put_contents($this->conf->getConfigFile(), $newConf);
$this->conf->reload();
unlink($this->conf->getConfigFile());
// Previous conf no longer exists, and new values have been loaded.
$this->assertFalse($this->conf->exists('login'));
$this->assertEquals('value', $this->conf->get('key'));
}
}

View file

@ -0,0 +1,4 @@
<?php /*
{
bad: bad,
}

View file

@ -0,0 +1,19 @@
<?php /*
{
"redirector":"lala",
"login":"root",
"hash":"hash",
"salt":"salt",
"timezone":"Europe\/Paris",
"disablesessionprotection":false,
"privateLinkByDefault":true,
"title": "Shaarli",
"titleLink": "?",
"config": {
"foo": "bar",
"DATASTORE": "data\/datastore.php"
},
"plugins": {
"WALLABAG_VERSION": 1
}
}

View file

@ -0,0 +1,4 @@
<?php /*
{
"login": "root"
}

View file

@ -88,7 +88,7 @@
</span>
<br>
{if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if}
{if="!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()"}
{if="!$hide_timestamps || isLoggedIn()"}
<span class="linkdate" title="Permalink"><a href="?{$value.linkdate|smallHash}">{function="strftime('%c', $value.timestamp)"} - permalink</a> - </span>
{else}
<span class="linkdate" title="Short link here"><a href="?{$value.shorturl}">permalink</a> - </span>

View file

@ -9,7 +9,7 @@
<br><br>
<a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
<br><br>
{if="!$GLOBALS['config']['OPEN_SHAARLI']"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a>
{if="$openshaarli"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a>
<br><br>{/if}
<a href="?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
<br><br>