aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/config
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2017-05-07 19:17:33 +0200
committerArthurHoaro <arthur@hoa.ro>2017-05-07 19:17:33 +0200
commit01e942d44c7194607649817216aeb5d65c6acad6 (patch)
tree15777aa1005251f119e6dd680291147117766b5b /application/config
parentbc22c9a0acb095970e9494cbe8954f0612e05dc0 (diff)
parent8868f3ca461011a8fb6dd9f90b60ed697ab52fc5 (diff)
downloadShaarli-01e942d44c7194607649817216aeb5d65c6acad6.tar.gz
Shaarli-01e942d44c7194607649817216aeb5d65c6acad6.tar.zst
Shaarli-01e942d44c7194607649817216aeb5d65c6acad6.zip
Merge tag 'v0.8.4' into stable
Release v0.8.4
Diffstat (limited to 'application/config')
-rw-r--r--application/config/ConfigIO.php33
-rw-r--r--application/config/ConfigJson.php78
-rw-r--r--application/config/ConfigManager.php394
-rw-r--r--application/config/ConfigPhp.php132
-rw-r--r--application/config/ConfigPlugin.php124
5 files changed, 761 insertions, 0 deletions
diff --git a/application/config/ConfigIO.php b/application/config/ConfigIO.php
new file mode 100644
index 00000000..2b68fe6a
--- /dev/null
+++ b/application/config/ConfigIO.php
@@ -0,0 +1,33 @@
1<?php
2
3/**
4 * Interface ConfigIO
5 *
6 * This describes how Config types should store their configuration.
7 */
8interface ConfigIO
9{
10 /**
11 * Read configuration.
12 *
13 * @param string $filepath Config file absolute path.
14 *
15 * @return array All configuration in an array.
16 */
17 function read($filepath);
18
19 /**
20 * Write configuration.
21 *
22 * @param string $filepath Config file absolute path.
23 * @param array $conf All configuration in an array.
24 */
25 function write($filepath, $conf);
26
27 /**
28 * Get config file extension according to config type.
29 *
30 * @return string Config file extension.
31 */
32 function getExtension();
33}
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php
new file mode 100644
index 00000000..30007eb4
--- /dev/null
+++ b/application/config/ConfigJson.php
@@ -0,0 +1,78 @@
1<?php
2
3/**
4 * Class ConfigJson (ConfigIO implementation)
5 *
6 * Handle Shaarli's JSON configuration file.
7 */
8class ConfigJson implements ConfigIO
9{
10 /**
11 * @inheritdoc
12 */
13 function read($filepath)
14 {
15 if (! is_readable($filepath)) {
16 return array();
17 }
18 $data = file_get_contents($filepath);
19 $data = str_replace(self::getPhpHeaders(), '', $data);
20 $data = str_replace(self::getPhpSuffix(), '', $data);
21 $data = json_decode($data, true);
22 if ($data === null) {
23 $error = json_last_error();
24 throw new Exception('An error occurred while parsing JSON file: error code #'. $error);
25 }
26 return $data;
27 }
28
29 /**
30 * @inheritdoc
31 */
32 function write($filepath, $conf)
33 {
34 // JSON_PRETTY_PRINT is available from PHP 5.4.
35 $print = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
36 $data = self::getPhpHeaders() . json_encode($conf, $print) . self::getPhpSuffix();
37 if (!file_put_contents($filepath, $data)) {
38 throw new IOException(
39 $filepath,
40 'Shaarli could not create the config file.
41 Please make sure Shaarli has the right to write in the folder is it installed in.'
42 );
43 }
44 }
45
46 /**
47 * @inheritdoc
48 */
49 function getExtension()
50 {
51 return '.json.php';
52 }
53
54 /**
55 * The JSON data is wrapped in a PHP file for security purpose.
56 * This way, even if the file is accessible, credentials and configuration won't be exposed.
57 *
58 * Note: this isn't a static field because concatenation isn't supported in field declaration before PHP 5.6.
59 *
60 * @return string PHP start tag and comment tag.
61 */
62 public static function getPhpHeaders()
63 {
64 return '<?php /*'. PHP_EOL;
65 }
66
67 /**
68 * Get PHP comment closing tags.
69 *
70 * Static method for consistency with getPhpHeaders.
71 *
72 * @return string PHP comment closing.
73 */
74 public static function getPhpSuffix()
75 {
76 return PHP_EOL . '*/ ?>';
77 }
78}
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
new file mode 100644
index 00000000..f5f753f8
--- /dev/null
+++ b/application/config/ConfigManager.php
@@ -0,0 +1,394 @@
1<?php
2
3// FIXME! Namespaces...
4require_once 'ConfigIO.php';
5require_once 'ConfigJson.php';
6require_once 'ConfigPhp.php';
7
8/**
9 * Class ConfigManager
10 *
11 * Manages all Shaarli's settings.
12 * See the documentation for more information on settings:
13 * - doc/Shaarli-configuration.html
14 * - https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration
15 */
16class ConfigManager
17{
18 /**
19 * @var string Flag telling a setting is not found.
20 */
21 protected static $NOT_FOUND = 'NOT_FOUND';
22
23 /**
24 * @var string Config folder.
25 */
26 protected $configFile;
27
28 /**
29 * @var array Loaded config array.
30 */
31 protected $loadedConfig;
32
33 /**
34 * @var ConfigIO implementation instance.
35 */
36 protected $configIO;
37
38 /**
39 * Constructor.
40 *
41 * @param string $configFile Configuration file path without extension.
42 */
43 public function __construct($configFile = 'data/config')
44 {
45 $this->configFile = $configFile;
46 $this->initialize();
47 }
48
49 /**
50 * Reset the ConfigManager instance.
51 */
52 public function reset()
53 {
54 $this->initialize();
55 }
56
57 /**
58 * Rebuild the loaded config array from config files.
59 */
60 public function reload()
61 {
62 $this->load();
63 }
64
65 /**
66 * Initialize the ConfigIO and loaded the conf.
67 */
68 protected function initialize()
69 {
70 if (file_exists($this->configFile . '.php')) {
71 $this->configIO = new ConfigPhp();
72 } else {
73 $this->configIO = new ConfigJson();
74 }
75 $this->load();
76 }
77
78 /**
79 * Load configuration in the ConfigurationManager.
80 */
81 protected function load()
82 {
83 $this->loadedConfig = $this->configIO->read($this->getConfigFileExt());
84 $this->setDefaultValues();
85 }
86
87 /**
88 * Get a setting.
89 *
90 * Supports nested settings with dot separated keys.
91 * Eg. 'config.stuff.option' will find $conf[config][stuff][option],
92 * or in JSON:
93 * { "config": { "stuff": {"option": "mysetting" } } } }
94 *
95 * @param string $setting Asked setting, keys separated with dots.
96 * @param string $default Default value if not found.
97 *
98 * @return mixed Found setting, or the default value.
99 */
100 public function get($setting, $default = '')
101 {
102 // During the ConfigIO transition, map legacy settings to the new ones.
103 if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
104 $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
105 }
106
107 $settings = explode('.', $setting);
108 $value = self::getConfig($settings, $this->loadedConfig);
109 if ($value === self::$NOT_FOUND) {
110 return $default;
111 }
112 return $value;
113 }
114
115 /**
116 * Set a setting, and eventually write it.
117 *
118 * Supports nested settings with dot separated keys.
119 *
120 * @param string $setting Asked setting, keys separated with dots.
121 * @param string $value Value to set.
122 * @param bool $write Write the new setting in the config file, default false.
123 * @param bool $isLoggedIn User login state, default false.
124 *
125 * @throws Exception Invalid
126 */
127 public function set($setting, $value, $write = false, $isLoggedIn = false)
128 {
129 if (empty($setting) || ! is_string($setting)) {
130 throw new Exception('Invalid setting key parameter. String expected, got: '. gettype($setting));
131 }
132
133 // During the ConfigIO transition, map legacy settings to the new ones.
134 if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
135 $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
136 }
137
138 $settings = explode('.', $setting);
139 self::setConfig($settings, $value, $this->loadedConfig);
140 if ($write) {
141 $this->write($isLoggedIn);
142 }
143 }
144
145 /**
146 * Check if a settings exists.
147 *
148 * Supports nested settings with dot separated keys.
149 *
150 * @param string $setting Asked setting, keys separated with dots.
151 *
152 * @return bool true if the setting exists, false otherwise.
153 */
154 public function exists($setting)
155 {
156 // During the ConfigIO transition, map legacy settings to the new ones.
157 if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
158 $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
159 }
160
161 $settings = explode('.', $setting);
162 $value = self::getConfig($settings, $this->loadedConfig);
163 if ($value === self::$NOT_FOUND) {
164 return false;
165 }
166 return true;
167 }
168
169 /**
170 * Call the config writer.
171 *
172 * @param bool $isLoggedIn User login state.
173 *
174 * @return bool True if the configuration has been successfully written, false otherwise.
175 *
176 * @throws MissingFieldConfigException: a mandatory field has not been provided in $conf.
177 * @throws UnauthorizedConfigException: user is not authorize to change configuration.
178 * @throws IOException: an error occurred while writing the new config file.
179 */
180 public function write($isLoggedIn)
181 {
182 // These fields are required in configuration.
183 $mandatoryFields = array(
184 'credentials.login',
185 'credentials.hash',
186 'credentials.salt',
187 'security.session_protection_disabled',
188 'general.timezone',
189 'general.title',
190 'general.header_link',
191 'privacy.default_private_links',
192 'redirector.url',
193 );
194
195 // Only logged in user can alter config.
196 if (is_file($this->getConfigFileExt()) && !$isLoggedIn) {
197 throw new UnauthorizedConfigException();
198 }
199
200 // Check that all mandatory fields are provided in $conf.
201 foreach ($mandatoryFields as $field) {
202 if (! $this->exists($field)) {
203 throw new MissingFieldConfigException($field);
204 }
205 }
206
207 return $this->configIO->write($this->getConfigFileExt(), $this->loadedConfig);
208 }
209
210 /**
211 * Set the config file path (without extension).
212 *
213 * @param string $configFile File path.
214 */
215 public function setConfigFile($configFile)
216 {
217 $this->configFile = $configFile;
218 }
219
220 /**
221 * Return the configuration file path (without extension).
222 *
223 * @return string Config path.
224 */
225 public function getConfigFile()
226 {
227 return $this->configFile;
228 }
229
230 /**
231 * Get the configuration file path with its extension.
232 *
233 * @return string Config file path.
234 */
235 public function getConfigFileExt()
236 {
237 return $this->configFile . $this->configIO->getExtension();
238 }
239
240 /**
241 * Recursive function which find asked setting in the loaded config.
242 *
243 * @param array $settings Ordered array which contains keys to find.
244 * @param array $conf Loaded settings, then sub-array.
245 *
246 * @return mixed Found setting or NOT_FOUND flag.
247 */
248 protected static function getConfig($settings, $conf)
249 {
250 if (!is_array($settings) || count($settings) == 0) {
251 return self::$NOT_FOUND;
252 }
253
254 $setting = array_shift($settings);
255 if (!isset($conf[$setting])) {
256 return self::$NOT_FOUND;
257 }
258
259 if (count($settings) > 0) {
260 return self::getConfig($settings, $conf[$setting]);
261 }
262 return $conf[$setting];
263 }
264
265 /**
266 * Recursive function which find asked setting in the loaded config.
267 *
268 * @param array $settings Ordered array which contains keys to find.
269 * @param mixed $value
270 * @param array $conf Loaded settings, then sub-array.
271 *
272 * @return mixed Found setting or NOT_FOUND flag.
273 */
274 protected static function setConfig($settings, $value, &$conf)
275 {
276 if (!is_array($settings) || count($settings) == 0) {
277 return self::$NOT_FOUND;
278 }
279
280 $setting = array_shift($settings);
281 if (count($settings) > 0) {
282 return self::setConfig($settings, $value, $conf[$setting]);
283 }
284 $conf[$setting] = $value;
285 }
286
287 /**
288 * Set a bunch of default values allowing Shaarli to start without a config file.
289 */
290 protected function setDefaultValues()
291 {
292 $this->setEmpty('resource.data_dir', 'data');
293 $this->setEmpty('resource.config', 'data/config.php');
294 $this->setEmpty('resource.datastore', 'data/datastore.php');
295 $this->setEmpty('resource.ban_file', 'data/ipbans.php');
296 $this->setEmpty('resource.updates', 'data/updates.txt');
297 $this->setEmpty('resource.log', 'data/log.txt');
298 $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
299 $this->setEmpty('resource.raintpl_tpl', 'tpl/');
300 $this->setEmpty('resource.raintpl_tmp', 'tmp/');
301 $this->setEmpty('resource.thumbnails_cache', 'cache');
302 $this->setEmpty('resource.page_cache', 'pagecache');
303
304 $this->setEmpty('security.ban_after', 4);
305 $this->setEmpty('security.ban_duration', 1800);
306 $this->setEmpty('security.session_protection_disabled', false);
307 $this->setEmpty('security.open_shaarli', false);
308
309 $this->setEmpty('general.header_link', '?');
310 $this->setEmpty('general.links_per_page', 20);
311 $this->setEmpty('general.enabled_plugins', array('qrcode'));
312
313 $this->setEmpty('updates.check_updates', false);
314 $this->setEmpty('updates.check_updates_branch', 'stable');
315 $this->setEmpty('updates.check_updates_interval', 86400);
316
317 $this->setEmpty('feed.rss_permalinks', true);
318 $this->setEmpty('feed.show_atom', false);
319
320 $this->setEmpty('privacy.default_private_links', false);
321 $this->setEmpty('privacy.hide_public_links', false);
322 $this->setEmpty('privacy.hide_timestamps', false);
323
324 $this->setEmpty('thumbnail.enable_thumbnails', true);
325 $this->setEmpty('thumbnail.enable_localcache', true);
326
327 $this->setEmpty('redirector.url', '');
328 $this->setEmpty('redirector.encode_url', true);
329
330 $this->setEmpty('plugins', array());
331 }
332
333 /**
334 * Set only if the setting does not exists.
335 *
336 * @param string $key Setting key.
337 * @param mixed $value Setting value.
338 */
339 public function setEmpty($key, $value)
340 {
341 if (! $this->exists($key)) {
342 $this->set($key, $value);
343 }
344 }
345
346 /**
347 * @return ConfigIO
348 */
349 public function getConfigIO()
350 {
351 return $this->configIO;
352 }
353
354 /**
355 * @param ConfigIO $configIO
356 */
357 public function setConfigIO($configIO)
358 {
359 $this->configIO = $configIO;
360 }
361}
362
363/**
364 * Exception used if a mandatory field is missing in given configuration.
365 */
366class MissingFieldConfigException extends Exception
367{
368 public $field;
369
370 /**
371 * Construct exception.
372 *
373 * @param string $field field name missing.
374 */
375 public function __construct($field)
376 {
377 $this->field = $field;
378 $this->message = 'Configuration value is required for '. $this->field;
379 }
380}
381
382/**
383 * Exception used if an unauthorized attempt to edit configuration has been made.
384 */
385class UnauthorizedConfigException extends Exception
386{
387 /**
388 * Construct exception.
389 */
390 public function __construct()
391 {
392 $this->message = 'You are not authorized to alter config.';
393 }
394}
diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php
new file mode 100644
index 00000000..27187b66
--- /dev/null
+++ b/application/config/ConfigPhp.php
@@ -0,0 +1,132 @@
1<?php
2
3/**
4 * Class ConfigPhp (ConfigIO implementation)
5 *
6 * Handle Shaarli's legacy PHP configuration file.
7 * Note: this is only designed to support the transition to JSON configuration.
8 */
9class ConfigPhp implements ConfigIO
10{
11 /**
12 * @var array List of config key without group.
13 */
14 public static $ROOT_KEYS = array(
15 'login',
16 'hash',
17 'salt',
18 'timezone',
19 'title',
20 'titleLink',
21 'redirector',
22 'disablesessionprotection',
23 'privateLinkByDefault',
24 );
25
26 /**
27 * Map legacy config keys with the new ones.
28 * If ConfigPhp is used, getting <newkey> will actually look for <legacykey>.
29 * The Updater will use this array to transform keys when switching to JSON.
30 *
31 * @var array current key => legacy key.
32 */
33 public static $LEGACY_KEYS_MAPPING = array(
34 'credentials.login' => 'login',
35 'credentials.hash' => 'hash',
36 'credentials.salt' => 'salt',
37 'resource.data_dir' => 'config.DATADIR',
38 'resource.config' => 'config.CONFIG_FILE',
39 'resource.datastore' => 'config.DATASTORE',
40 'resource.updates' => 'config.UPDATES_FILE',
41 'resource.log' => 'config.LOG_FILE',
42 'resource.update_check' => 'config.UPDATECHECK_FILENAME',
43 'resource.raintpl_tpl' => 'config.RAINTPL_TPL',
44 'resource.raintpl_tmp' => 'config.RAINTPL_TMP',
45 'resource.thumbnails_cache' => 'config.CACHEDIR',
46 'resource.page_cache' => 'config.PAGECACHE',
47 'resource.ban_file' => 'config.IPBANS_FILENAME',
48 'security.session_protection_disabled' => 'disablesessionprotection',
49 'security.ban_after' => 'config.BAN_AFTER',
50 'security.ban_duration' => 'config.BAN_DURATION',
51 'general.title' => 'title',
52 'general.timezone' => 'timezone',
53 'general.header_link' => 'titleLink',
54 'updates.check_updates' => 'config.ENABLE_UPDATECHECK',
55 'updates.check_updates_branch' => 'config.UPDATECHECK_BRANCH',
56 'updates.check_updates_interval' => 'config.UPDATECHECK_INTERVAL',
57 'privacy.default_private_links' => 'privateLinkByDefault',
58 'feed.rss_permalinks' => 'config.ENABLE_RSS_PERMALINKS',
59 'general.links_per_page' => 'config.LINKS_PER_PAGE',
60 'thumbnail.enable_thumbnails' => 'config.ENABLE_THUMBNAILS',
61 'thumbnail.enable_localcache' => 'config.ENABLE_LOCALCACHE',
62 'general.enabled_plugins' => 'config.ENABLED_PLUGINS',
63 'redirector.url' => 'redirector',
64 'redirector.encode_url' => 'config.REDIRECTOR_URLENCODE',
65 'feed.show_atom' => 'config.SHOW_ATOM',
66 'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS',
67 'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS',
68 'security.open_shaarli' => 'config.OPEN_SHAARLI',
69 );
70
71 /**
72 * @inheritdoc
73 */
74 function read($filepath)
75 {
76 if (! file_exists($filepath) || ! is_readable($filepath)) {
77 return array();
78 }
79
80 include $filepath;
81
82 $out = array();
83 foreach (self::$ROOT_KEYS as $key) {
84 $out[$key] = $GLOBALS[$key];
85 }
86 $out['config'] = $GLOBALS['config'];
87 $out['plugins'] = !empty($GLOBALS['plugins']) ? $GLOBALS['plugins'] : array();
88 return $out;
89 }
90
91 /**
92 * @inheritdoc
93 */
94 function write($filepath, $conf)
95 {
96 $configStr = '<?php '. PHP_EOL;
97 foreach (self::$ROOT_KEYS as $key) {
98 if (isset($conf[$key])) {
99 $configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL;
100 }
101 }
102
103 // Store all $conf['config']
104 foreach ($conf['config'] as $key => $value) {
105 $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($conf['config'][$key], true).';'. PHP_EOL;
106 }
107
108 if (isset($conf['plugins'])) {
109 foreach ($conf['plugins'] as $key => $value) {
110 $configStr .= '$GLOBALS[\'plugins\'][\''. $key .'\'] = '.var_export($conf['plugins'][$key], true).';'. PHP_EOL;
111 }
112 }
113
114 if (!file_put_contents($filepath, $configStr)
115 || strcmp(file_get_contents($filepath), $configStr) != 0
116 ) {
117 throw new IOException(
118 $filepath,
119 'Shaarli could not create the config file.
120 Please make sure Shaarli has the right to write in the folder is it installed in.'
121 );
122 }
123 }
124
125 /**
126 * @inheritdoc
127 */
128 function getExtension()
129 {
130 return '.php';
131 }
132}
diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php
new file mode 100644
index 00000000..cb0b6fce
--- /dev/null
+++ b/application/config/ConfigPlugin.php
@@ -0,0 +1,124 @@
1<?php
2/**
3 * Plugin configuration helper functions.
4 *
5 * Note: no access to configuration files here.
6 */
7
8/**
9 * Process plugin administration form data and save it in an array.
10 *
11 * @param array $formData Data sent by the plugin admin form.
12 *
13 * @return array New list of enabled plugin, ordered.
14 *
15 * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid.
16 */
17function save_plugin_config($formData)
18{
19 // Make sure there are no duplicates in orders.
20 if (!validate_plugin_order($formData)) {
21 throw new PluginConfigOrderException();
22 }
23
24 $plugins = array();
25 $newEnabledPlugins = array();
26 foreach ($formData as $key => $data) {
27 if (startsWith($key, 'order')) {
28 continue;
29 }
30
31 // If there is no order, it means a disabled plugin has been enabled.
32 if (isset($formData['order_' . $key])) {
33 $plugins[(int) $formData['order_' . $key]] = $key;
34 }
35 else {
36 $newEnabledPlugins[] = $key;
37 }
38 }
39
40 // New enabled plugins will be added at the end of order.
41 $plugins = array_merge($plugins, $newEnabledPlugins);
42
43 // Sort plugins by order.
44 if (!ksort($plugins)) {
45 throw new PluginConfigOrderException();
46 }
47
48 $finalPlugins = array();
49 // Make plugins order continuous.
50 foreach ($plugins as $plugin) {
51 $finalPlugins[] = $plugin;
52 }
53
54 return $finalPlugins;
55}
56
57/**
58 * Validate plugin array submitted.
59 * Will fail if there is duplicate orders value.
60 *
61 * @param array $formData Data from submitted form.
62 *
63 * @return bool true if ok, false otherwise.
64 */
65function validate_plugin_order($formData)
66{
67 $orders = array();
68 foreach ($formData as $key => $value) {
69 // No duplicate order allowed.
70 if (in_array($value, $orders)) {
71 return false;
72 }
73
74 if (startsWith($key, 'order')) {
75 $orders[] = $value;
76 }
77 }
78
79 return true;
80}
81
82/**
83 * Affect plugin parameters values from the ConfigManager into plugins array.
84 *
85 * @param mixed $plugins Plugins array:
86 * $plugins[<plugin_name>]['parameters'][<param_name>] = [
87 * 'value' => <value>,
88 * 'desc' => <description>
89 * ]
90 * @param mixed $conf Plugins configuration.
91 *
92 * @return mixed Updated $plugins array.
93 */
94function load_plugin_parameter_values($plugins, $conf)
95{
96 $out = $plugins;
97 foreach ($plugins as $name => $plugin) {
98 if (empty($plugin['parameters'])) {
99 continue;
100 }
101
102 foreach ($plugin['parameters'] as $key => $param) {
103 if (!empty($conf[$key])) {
104 $out[$name]['parameters'][$key]['value'] = $conf[$key];
105 }
106 }
107 }
108
109 return $out;
110}
111
112/**
113 * Exception used if an error occur while saving plugin configuration.
114 */
115class PluginConfigOrderException extends Exception
116{
117 /**
118 * Construct exception.
119 */
120 public function __construct()
121 {
122 $this->message = 'An error occurred while trying to save plugins loading order.';
123 }
124}