ConfigManager.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <?php
  2. // FIXME! Namespaces...
  3. require_once 'ConfigIO.php';
  4. require_once 'ConfigJson.php';
  5. require_once 'ConfigPhp.php';
  6. /**
  7. * Class ConfigManager
  8. *
  9. * Manages all Shaarli's settings.
  10. * See the documentation for more information on settings:
  11. * - doc/Shaarli-configuration.html
  12. * - https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration
  13. */
  14. class ConfigManager
  15. {
  16. /**
  17. * @var string Flag telling a setting is not found.
  18. */
  19. protected static $NOT_FOUND = 'NOT_FOUND';
  20. /**
  21. * @var string Config folder.
  22. */
  23. protected $configFile;
  24. /**
  25. * @var array Loaded config array.
  26. */
  27. protected $loadedConfig;
  28. /**
  29. * @var ConfigIO implementation instance.
  30. */
  31. protected $configIO;
  32. /**
  33. * Constructor.
  34. */
  35. public function __construct($configFile = 'data/config')
  36. {
  37. $this->configFile = $configFile;
  38. $this->initialize();
  39. }
  40. /**
  41. * Reset the ConfigManager instance.
  42. */
  43. public function reset()
  44. {
  45. $this->initialize();
  46. }
  47. /**
  48. * Rebuild the loaded config array from config files.
  49. */
  50. public function reload()
  51. {
  52. $this->load();
  53. }
  54. /**
  55. * Initialize the ConfigIO and loaded the conf.
  56. */
  57. protected function initialize()
  58. {
  59. if (file_exists($this->configFile . '.php')) {
  60. $this->configIO = new ConfigPhp();
  61. } else {
  62. $this->configIO = new ConfigJson();
  63. }
  64. $this->load();
  65. }
  66. /**
  67. * Load configuration in the ConfigurationManager.
  68. */
  69. protected function load()
  70. {
  71. $this->loadedConfig = $this->configIO->read($this->getConfigFileExt());
  72. $this->setDefaultValues();
  73. }
  74. /**
  75. * Get a setting.
  76. *
  77. * Supports nested settings with dot separated keys.
  78. * Eg. 'config.stuff.option' will find $conf[config][stuff][option],
  79. * or in JSON:
  80. * { "config": { "stuff": {"option": "mysetting" } } } }
  81. *
  82. * @param string $setting Asked setting, keys separated with dots.
  83. * @param string $default Default value if not found.
  84. *
  85. * @return mixed Found setting, or the default value.
  86. */
  87. public function get($setting, $default = '')
  88. {
  89. // During the ConfigIO transition, map legacy settings to the new ones.
  90. if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
  91. $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
  92. }
  93. $settings = explode('.', $setting);
  94. $value = self::getConfig($settings, $this->loadedConfig);
  95. if ($value === self::$NOT_FOUND) {
  96. return $default;
  97. }
  98. return $value;
  99. }
  100. /**
  101. * Set a setting, and eventually write it.
  102. *
  103. * Supports nested settings with dot separated keys.
  104. *
  105. * @param string $setting Asked setting, keys separated with dots.
  106. * @param string $value Value to set.
  107. * @param bool $write Write the new setting in the config file, default false.
  108. * @param bool $isLoggedIn User login state, default false.
  109. *
  110. * @throws Exception Invalid
  111. */
  112. public function set($setting, $value, $write = false, $isLoggedIn = false)
  113. {
  114. if (empty($setting) || ! is_string($setting)) {
  115. throw new Exception('Invalid setting key parameter. String expected, got: '. gettype($setting));
  116. }
  117. // During the ConfigIO transition, map legacy settings to the new ones.
  118. if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
  119. $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
  120. }
  121. $settings = explode('.', $setting);
  122. self::setConfig($settings, $value, $this->loadedConfig);
  123. if ($write) {
  124. $this->write($isLoggedIn);
  125. }
  126. }
  127. /**
  128. * Check if a settings exists.
  129. *
  130. * Supports nested settings with dot separated keys.
  131. *
  132. * @param string $setting Asked setting, keys separated with dots.
  133. *
  134. * @return bool true if the setting exists, false otherwise.
  135. */
  136. public function exists($setting)
  137. {
  138. // During the ConfigIO transition, map legacy settings to the new ones.
  139. if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
  140. $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
  141. }
  142. $settings = explode('.', $setting);
  143. $value = self::getConfig($settings, $this->loadedConfig);
  144. if ($value === self::$NOT_FOUND) {
  145. return false;
  146. }
  147. return true;
  148. }
  149. /**
  150. * Call the config writer.
  151. *
  152. * @param bool $isLoggedIn User login state.
  153. *
  154. * @return bool True if the configuration has been successfully written, false otherwise.
  155. *
  156. * @throws MissingFieldConfigException: a mandatory field has not been provided in $conf.
  157. * @throws UnauthorizedConfigException: user is not authorize to change configuration.
  158. * @throws IOException: an error occurred while writing the new config file.
  159. */
  160. public function write($isLoggedIn)
  161. {
  162. // These fields are required in configuration.
  163. $mandatoryFields = array(
  164. 'credentials.login',
  165. 'credentials.hash',
  166. 'credentials.salt',
  167. 'security.session_protection_disabled',
  168. 'general.timezone',
  169. 'general.title',
  170. 'general.header_link',
  171. 'privacy.default_private_links',
  172. 'redirector.url',
  173. );
  174. // Only logged in user can alter config.
  175. if (is_file($this->getConfigFileExt()) && !$isLoggedIn) {
  176. throw new UnauthorizedConfigException();
  177. }
  178. // Check that all mandatory fields are provided in $conf.
  179. foreach ($mandatoryFields as $field) {
  180. if (! $this->exists($field)) {
  181. throw new MissingFieldConfigException($field);
  182. }
  183. }
  184. return $this->configIO->write($this->getConfigFileExt(), $this->loadedConfig);
  185. }
  186. /**
  187. * Set the config file path (without extension).
  188. *
  189. * @param string $configFile File path.
  190. */
  191. public function setConfigFile($configFile)
  192. {
  193. $this->configFile = $configFile;
  194. }
  195. /**
  196. * Return the configuration file path (without extension).
  197. *
  198. * @return string Config path.
  199. */
  200. public function getConfigFile()
  201. {
  202. return $this->configFile;
  203. }
  204. /**
  205. * Get the configuration file path with its extension.
  206. *
  207. * @return string Config file path.
  208. */
  209. public function getConfigFileExt()
  210. {
  211. return $this->configFile . $this->configIO->getExtension();
  212. }
  213. /**
  214. * Recursive function which find asked setting in the loaded config.
  215. *
  216. * @param array $settings Ordered array which contains keys to find.
  217. * @param array $conf Loaded settings, then sub-array.
  218. *
  219. * @return mixed Found setting or NOT_FOUND flag.
  220. */
  221. protected static function getConfig($settings, $conf)
  222. {
  223. if (!is_array($settings) || count($settings) == 0) {
  224. return self::$NOT_FOUND;
  225. }
  226. $setting = array_shift($settings);
  227. if (!isset($conf[$setting])) {
  228. return self::$NOT_FOUND;
  229. }
  230. if (count($settings) > 0) {
  231. return self::getConfig($settings, $conf[$setting]);
  232. }
  233. return $conf[$setting];
  234. }
  235. /**
  236. * Recursive function which find asked setting in the loaded config.
  237. *
  238. * @param array $settings Ordered array which contains keys to find.
  239. * @param mixed $value
  240. * @param array $conf Loaded settings, then sub-array.
  241. *
  242. * @return mixed Found setting or NOT_FOUND flag.
  243. */
  244. protected static function setConfig($settings, $value, &$conf)
  245. {
  246. if (!is_array($settings) || count($settings) == 0) {
  247. return self::$NOT_FOUND;
  248. }
  249. $setting = array_shift($settings);
  250. if (count($settings) > 0) {
  251. return self::setConfig($settings, $value, $conf[$setting]);
  252. }
  253. $conf[$setting] = $value;
  254. }
  255. /**
  256. * Set a bunch of default values allowing Shaarli to start without a config file.
  257. */
  258. protected function setDefaultValues()
  259. {
  260. $this->setEmpty('resource.data_dir', 'data');
  261. $this->setEmpty('resource.config', 'data/config.php');
  262. $this->setEmpty('resource.datastore', 'data/datastore.php');
  263. $this->setEmpty('resource.ban_file', 'data/ipbans.php');
  264. $this->setEmpty('resource.updates', 'data/updates.txt');
  265. $this->setEmpty('resource.log', 'data/log.txt');
  266. $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
  267. $this->setEmpty('resource.raintpl_tpl', 'tpl/');
  268. $this->setEmpty('resource.raintpl_tmp', 'tmp/');
  269. $this->setEmpty('resource.thumbnails_cache', 'cache');
  270. $this->setEmpty('resource.page_cache', 'pagecache');
  271. $this->setEmpty('security.ban_after', 4);
  272. $this->setEmpty('security.ban_duration', 1800);
  273. $this->setEmpty('security.session_protection_disabled', false);
  274. $this->setEmpty('security.open_shaarli', false);
  275. $this->setEmpty('general.header_link', '?');
  276. $this->setEmpty('general.links_per_page', 20);
  277. $this->setEmpty('general.enabled_plugins', array('qrcode'));
  278. $this->setEmpty('updates.check_updates', false);
  279. $this->setEmpty('updates.check_updates_branch', 'stable');
  280. $this->setEmpty('updates.check_updates_interval', 86400);
  281. $this->setEmpty('feed.rss_permalinks', true);
  282. $this->setEmpty('feed.show_atom', false);
  283. $this->setEmpty('privacy.default_private_links', false);
  284. $this->setEmpty('privacy.hide_public_links', false);
  285. $this->setEmpty('privacy.hide_timestamps', false);
  286. $this->setEmpty('thumbnail.enable_thumbnails', true);
  287. $this->setEmpty('thumbnail.enable_localcache', true);
  288. $this->setEmpty('redirector.url', '');
  289. $this->setEmpty('redirector.encode_url', true);
  290. $this->setEmpty('plugins', array());
  291. }
  292. /**
  293. * Set only if the setting does not exists.
  294. *
  295. * @param string $key Setting key.
  296. * @param mixed $value Setting value.
  297. */
  298. public function setEmpty($key, $value)
  299. {
  300. if (! $this->exists($key)) {
  301. $this->set($key, $value);
  302. }
  303. }
  304. /**
  305. * @return ConfigIO
  306. */
  307. public function getConfigIO()
  308. {
  309. return $this->configIO;
  310. }
  311. /**
  312. * @param ConfigIO $configIO
  313. */
  314. public function setConfigIO($configIO)
  315. {
  316. $this->configIO = $configIO;
  317. }
  318. }
  319. /**
  320. * Exception used if a mandatory field is missing in given configuration.
  321. */
  322. class MissingFieldConfigException extends Exception
  323. {
  324. public $field;
  325. /**
  326. * Construct exception.
  327. *
  328. * @param string $field field name missing.
  329. */
  330. public function __construct($field)
  331. {
  332. $this->field = $field;
  333. $this->message = 'Configuration value is required for '. $this->field;
  334. }
  335. }
  336. /**
  337. * Exception used if an unauthorized attempt to edit configuration has been made.
  338. */
  339. class UnauthorizedConfigException extends Exception
  340. {
  341. /**
  342. * Construct exception.
  343. */
  344. public function __construct()
  345. {
  346. $this->message = 'You are not authorized to alter config.';
  347. }
  348. }