diff options
author | ArthurHoaro <arthur@hoa.ro> | 2017-05-07 19:17:33 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2017-05-07 19:17:33 +0200 |
commit | 01e942d44c7194607649817216aeb5d65c6acad6 (patch) | |
tree | 15777aa1005251f119e6dd680291147117766b5b /application | |
parent | bc22c9a0acb095970e9494cbe8954f0612e05dc0 (diff) | |
parent | 8868f3ca461011a8fb6dd9f90b60ed697ab52fc5 (diff) | |
download | Shaarli-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')
-rw-r--r-- | application/.htaccess | 15 | ||||
-rw-r--r-- | application/ApplicationUtils.php | 32 | ||||
-rw-r--r-- | application/CachedPage.php | 2 | ||||
-rw-r--r-- | application/Config.php | 221 | ||||
-rw-r--r-- | application/FeedBuilder.php | 46 | ||||
-rw-r--r-- | application/FileUtils.php | 8 | ||||
-rw-r--r-- | application/HttpUtils.php | 186 | ||||
-rw-r--r-- | application/Languages.php | 21 | ||||
-rw-r--r-- | application/LinkDB.php | 318 | ||||
-rw-r--r-- | application/LinkFilter.php | 70 | ||||
-rw-r--r-- | application/LinkUtils.php | 91 | ||||
-rw-r--r-- | application/NetscapeBookmarkUtils.php | 139 | ||||
-rw-r--r-- | application/PageBuilder.php | 50 | ||||
-rw-r--r-- | application/PluginManager.php | 83 | ||||
-rw-r--r-- | application/Router.php | 2 | ||||
-rw-r--r-- | application/TimeZone.php | 10 | ||||
-rw-r--r-- | application/Updater.php | 177 | ||||
-rw-r--r-- | application/Url.php | 15 | ||||
-rw-r--r-- | application/Utils.php | 69 | ||||
-rw-r--r-- | application/config/ConfigIO.php | 33 | ||||
-rw-r--r-- | application/config/ConfigJson.php | 78 | ||||
-rw-r--r-- | application/config/ConfigManager.php | 394 | ||||
-rw-r--r-- | application/config/ConfigPhp.php | 132 | ||||
-rw-r--r-- | application/config/ConfigPlugin.php | 124 |
24 files changed, 1763 insertions, 553 deletions
diff --git a/application/.htaccess b/application/.htaccess index b584d98c..f601c1ee 100644 --- a/application/.htaccess +++ b/application/.htaccess | |||
@@ -1,2 +1,13 @@ | |||
1 | Allow from none | 1 | <IfModule version_module> |
2 | Deny from all | 2 | <IfVersion >= 2.4> |
3 | Require all denied | ||
4 | </IfVersion> | ||
5 | <IfVersion < 2.4> | ||
6 | Allow from none | ||
7 | Deny from all | ||
8 | </IfVersion> | ||
9 | </IfModule> | ||
10 | |||
11 | <IfModule !version_module> | ||
12 | Require all denied | ||
13 | </IfModule> | ||
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 978fc9da..7f963e97 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php | |||
@@ -15,6 +15,9 @@ class ApplicationUtils | |||
15 | * | 15 | * |
16 | * The code is read from the raw content of the version file on the Git server. | 16 | * The code is read from the raw content of the version file on the Git server. |
17 | * | 17 | * |
18 | * @param string $url URL to reach to get the latest version. | ||
19 | * @param int $timeout Timeout to check the URL (in seconds). | ||
20 | * | ||
18 | * @return mixed the version code from the repository if available, else 'false' | 21 | * @return mixed the version code from the repository if available, else 'false' |
19 | */ | 22 | */ |
20 | public static function getLatestGitVersionCode($url, $timeout=2) | 23 | public static function getLatestGitVersionCode($url, $timeout=2) |
@@ -49,6 +52,7 @@ class ApplicationUtils | |||
49 | * @param int $checkInterval the minimum interval between update checks (in seconds | 52 | * @param int $checkInterval the minimum interval between update checks (in seconds |
50 | * @param bool $enableCheck whether to check for new versions | 53 | * @param bool $enableCheck whether to check for new versions |
51 | * @param bool $isLoggedIn whether the user is logged in | 54 | * @param bool $isLoggedIn whether the user is logged in |
55 | * @param string $branch check update for the given branch | ||
52 | * | 56 | * |
53 | * @throws Exception an invalid branch has been set for update checks | 57 | * @throws Exception an invalid branch has been set for update checks |
54 | * | 58 | * |
@@ -132,11 +136,11 @@ class ApplicationUtils | |||
132 | /** | 136 | /** |
133 | * Checks Shaarli has the proper access permissions to its resources | 137 | * Checks Shaarli has the proper access permissions to its resources |
134 | * | 138 | * |
135 | * @param array $globalConfig The $GLOBALS['config'] array | 139 | * @param ConfigManager $conf Configuration Manager instance. |
136 | * | 140 | * |
137 | * @return array A list of the detected configuration issues | 141 | * @return array A list of the detected configuration issues |
138 | */ | 142 | */ |
139 | public static function checkResourcePermissions($globalConfig) | 143 | public static function checkResourcePermissions($conf) |
140 | { | 144 | { |
141 | $errors = array(); | 145 | $errors = array(); |
142 | 146 | ||
@@ -145,19 +149,19 @@ class ApplicationUtils | |||
145 | 'application', | 149 | 'application', |
146 | 'inc', | 150 | 'inc', |
147 | 'plugins', | 151 | 'plugins', |
148 | $globalConfig['RAINTPL_TPL'] | 152 | $conf->get('resource.raintpl_tpl'), |
149 | ) as $path) { | 153 | ) as $path) { |
150 | if (! is_readable(realpath($path))) { | 154 | if (! is_readable(realpath($path))) { |
151 | $errors[] = '"'.$path.'" directory is not readable'; | 155 | $errors[] = '"'.$path.'" directory is not readable'; |
152 | } | 156 | } |
153 | } | 157 | } |
154 | 158 | ||
155 | // Check cache and data directories are readable and writeable | 159 | // Check cache and data directories are readable and writable |
156 | foreach (array( | 160 | foreach (array( |
157 | $globalConfig['CACHEDIR'], | 161 | $conf->get('resource.thumbnails_cache'), |
158 | $globalConfig['DATADIR'], | 162 | $conf->get('resource.data_dir'), |
159 | $globalConfig['PAGECACHE'], | 163 | $conf->get('resource.page_cache'), |
160 | $globalConfig['RAINTPL_TMP'] | 164 | $conf->get('resource.raintpl_tmp'), |
161 | ) as $path) { | 165 | ) as $path) { |
162 | if (! is_readable(realpath($path))) { | 166 | if (! is_readable(realpath($path))) { |
163 | $errors[] = '"'.$path.'" directory is not readable'; | 167 | $errors[] = '"'.$path.'" directory is not readable'; |
@@ -167,13 +171,13 @@ class ApplicationUtils | |||
167 | } | 171 | } |
168 | } | 172 | } |
169 | 173 | ||
170 | // Check configuration files are readable and writeable | 174 | // Check configuration files are readable and writable |
171 | foreach (array( | 175 | foreach (array( |
172 | $globalConfig['CONFIG_FILE'], | 176 | $conf->getConfigFileExt(), |
173 | $globalConfig['DATASTORE'], | 177 | $conf->get('resource.datastore'), |
174 | $globalConfig['IPBANS_FILENAME'], | 178 | $conf->get('resource.ban_file'), |
175 | $globalConfig['LOG_FILE'], | 179 | $conf->get('resource.log'), |
176 | $globalConfig['UPDATECHECK_FILENAME'] | 180 | $conf->get('resource.update_check'), |
177 | ) as $path) { | 181 | ) as $path) { |
178 | if (! is_file(realpath($path))) { | 182 | if (! is_file(realpath($path))) { |
179 | # the file may not exist yet | 183 | # the file may not exist yet |
diff --git a/application/CachedPage.php b/application/CachedPage.php index 50cfa9ac..5087d0c4 100644 --- a/application/CachedPage.php +++ b/application/CachedPage.php | |||
@@ -35,7 +35,7 @@ class CachedPage | |||
35 | /** | 35 | /** |
36 | * Returns the cached version of a page, if it exists and should be cached | 36 | * Returns the cached version of a page, if it exists and should be cached |
37 | * | 37 | * |
38 | * @return a cached version of the page if it exists, null otherwise | 38 | * @return string a cached version of the page if it exists, null otherwise |
39 | */ | 39 | */ |
40 | public function cachedVersion() | 40 | public function cachedVersion() |
41 | { | 41 | { |
diff --git a/application/Config.php b/application/Config.php deleted file mode 100644 index 05a59452..00000000 --- a/application/Config.php +++ /dev/null | |||
@@ -1,221 +0,0 @@ | |||
1 | <?php | ||
2 | /** | ||
3 | * Functions related to configuration management. | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * Re-write configuration file according to given array. | ||
8 | * Requires mandatory fields listed in $MANDATORY_FIELDS. | ||
9 | * | ||
10 | * @param array $config contains all configuration fields. | ||
11 | * @param bool $isLoggedIn true if user is logged in. | ||
12 | * | ||
13 | * @return void | ||
14 | * | ||
15 | * @throws MissingFieldConfigException: a mandatory field has not been provided in $config. | ||
16 | * @throws UnauthorizedConfigException: user is not authorize to change configuration. | ||
17 | * @throws Exception: an error occured while writing the new config file. | ||
18 | */ | ||
19 | function writeConfig($config, $isLoggedIn) | ||
20 | { | ||
21 | // These fields are required in configuration. | ||
22 | $MANDATORY_FIELDS = array( | ||
23 | 'login', 'hash', 'salt', 'timezone', 'title', 'titleLink', | ||
24 | 'redirector', 'disablesessionprotection', 'privateLinkByDefault' | ||
25 | ); | ||
26 | |||
27 | if (!isset($config['config']['CONFIG_FILE'])) { | ||
28 | throw new MissingFieldConfigException('CONFIG_FILE'); | ||
29 | } | ||
30 | |||
31 | // Only logged in user can alter config. | ||
32 | if (is_file($config['config']['CONFIG_FILE']) && !$isLoggedIn) { | ||
33 | throw new UnauthorizedConfigException(); | ||
34 | } | ||
35 | |||
36 | // Check that all mandatory fields are provided in $config. | ||
37 | foreach ($MANDATORY_FIELDS as $field) { | ||
38 | if (!isset($config[$field])) { | ||
39 | throw new MissingFieldConfigException($field); | ||
40 | } | ||
41 | } | ||
42 | |||
43 | $configStr = '<?php '. PHP_EOL; | ||
44 | $configStr .= '$GLOBALS[\'login\'] = '.var_export($config['login'], true).';'. PHP_EOL; | ||
45 | $configStr .= '$GLOBALS[\'hash\'] = '.var_export($config['hash'], true).';'. PHP_EOL; | ||
46 | $configStr .= '$GLOBALS[\'salt\'] = '.var_export($config['salt'], true).'; '. PHP_EOL; | ||
47 | $configStr .= '$GLOBALS[\'timezone\'] = '.var_export($config['timezone'], true).';'. PHP_EOL; | ||
48 | $configStr .= 'date_default_timezone_set('.var_export($config['timezone'], true).');'. PHP_EOL; | ||
49 | $configStr .= '$GLOBALS[\'title\'] = '.var_export($config['title'], true).';'. PHP_EOL; | ||
50 | $configStr .= '$GLOBALS[\'titleLink\'] = '.var_export($config['titleLink'], true).'; '. PHP_EOL; | ||
51 | $configStr .= '$GLOBALS[\'redirector\'] = '.var_export($config['redirector'], true).'; '. PHP_EOL; | ||
52 | $configStr .= '$GLOBALS[\'disablesessionprotection\'] = '.var_export($config['disablesessionprotection'], true).'; '. PHP_EOL; | ||
53 | $configStr .= '$GLOBALS[\'privateLinkByDefault\'] = '.var_export($config['privateLinkByDefault'], true).'; '. PHP_EOL; | ||
54 | |||
55 | // Store all $config['config'] | ||
56 | foreach ($config['config'] as $key => $value) { | ||
57 | $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($config['config'][$key], true).';'. PHP_EOL; | ||
58 | } | ||
59 | |||
60 | if (isset($config['plugins'])) { | ||
61 | foreach ($config['plugins'] as $key => $value) { | ||
62 | $configStr .= '$GLOBALS[\'plugins\'][\''. $key .'\'] = '.var_export($config['plugins'][$key], true).';'. PHP_EOL; | ||
63 | } | ||
64 | } | ||
65 | |||
66 | if (!file_put_contents($config['config']['CONFIG_FILE'], $configStr) | ||
67 | || strcmp(file_get_contents($config['config']['CONFIG_FILE']), $configStr) != 0 | ||
68 | ) { | ||
69 | throw new Exception( | ||
70 | 'Shaarli could not create the config file. | ||
71 | Please make sure Shaarli has the right to write in the folder is it installed in.' | ||
72 | ); | ||
73 | } | ||
74 | } | ||
75 | |||
76 | /** | ||
77 | * Process plugin administration form data and save it in an array. | ||
78 | * | ||
79 | * @param array $formData Data sent by the plugin admin form. | ||
80 | * | ||
81 | * @return array New list of enabled plugin, ordered. | ||
82 | * | ||
83 | * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid. | ||
84 | */ | ||
85 | function save_plugin_config($formData) | ||
86 | { | ||
87 | // Make sure there are no duplicates in orders. | ||
88 | if (!validate_plugin_order($formData)) { | ||
89 | throw new PluginConfigOrderException(); | ||
90 | } | ||
91 | |||
92 | $plugins = array(); | ||
93 | $newEnabledPlugins = array(); | ||
94 | foreach ($formData as $key => $data) { | ||
95 | if (startsWith($key, 'order')) { | ||
96 | continue; | ||
97 | } | ||
98 | |||
99 | // If there is no order, it means a disabled plugin has been enabled. | ||
100 | if (isset($formData['order_' . $key])) { | ||
101 | $plugins[(int) $formData['order_' . $key]] = $key; | ||
102 | } | ||
103 | else { | ||
104 | $newEnabledPlugins[] = $key; | ||
105 | } | ||
106 | } | ||
107 | |||
108 | // New enabled plugins will be added at the end of order. | ||
109 | $plugins = array_merge($plugins, $newEnabledPlugins); | ||
110 | |||
111 | // Sort plugins by order. | ||
112 | if (!ksort($plugins)) { | ||
113 | throw new PluginConfigOrderException(); | ||
114 | } | ||
115 | |||
116 | $finalPlugins = array(); | ||
117 | // Make plugins order continuous. | ||
118 | foreach ($plugins as $plugin) { | ||
119 | $finalPlugins[] = $plugin; | ||
120 | } | ||
121 | |||
122 | return $finalPlugins; | ||
123 | } | ||
124 | |||
125 | /** | ||
126 | * Validate plugin array submitted. | ||
127 | * Will fail if there is duplicate orders value. | ||
128 | * | ||
129 | * @param array $formData Data from submitted form. | ||
130 | * | ||
131 | * @return bool true if ok, false otherwise. | ||
132 | */ | ||
133 | function validate_plugin_order($formData) | ||
134 | { | ||
135 | $orders = array(); | ||
136 | foreach ($formData as $key => $value) { | ||
137 | // No duplicate order allowed. | ||
138 | if (in_array($value, $orders)) { | ||
139 | return false; | ||
140 | } | ||
141 | |||
142 | if (startsWith($key, 'order')) { | ||
143 | $orders[] = $value; | ||
144 | } | ||
145 | } | ||
146 | |||
147 | return true; | ||
148 | } | ||
149 | |||
150 | /** | ||
151 | * Affect plugin parameters values into plugins array. | ||
152 | * | ||
153 | * @param mixed $plugins Plugins array ($plugins[<plugin_name>]['parameters']['param_name'] = <value>. | ||
154 | * @param mixed $config Plugins configuration. | ||
155 | * | ||
156 | * @return mixed Updated $plugins array. | ||
157 | */ | ||
158 | function load_plugin_parameter_values($plugins, $config) | ||
159 | { | ||
160 | $out = $plugins; | ||
161 | foreach ($plugins as $name => $plugin) { | ||
162 | if (empty($plugin['parameters'])) { | ||
163 | continue; | ||
164 | } | ||
165 | |||
166 | foreach ($plugin['parameters'] as $key => $param) { | ||
167 | if (!empty($config[$key])) { | ||
168 | $out[$name]['parameters'][$key] = $config[$key]; | ||
169 | } | ||
170 | } | ||
171 | } | ||
172 | |||
173 | return $out; | ||
174 | } | ||
175 | |||
176 | /** | ||
177 | * Exception used if a mandatory field is missing in given configuration. | ||
178 | */ | ||
179 | class MissingFieldConfigException extends Exception | ||
180 | { | ||
181 | public $field; | ||
182 | |||
183 | /** | ||
184 | * Construct exception. | ||
185 | * | ||
186 | * @param string $field field name missing. | ||
187 | */ | ||
188 | public function __construct($field) | ||
189 | { | ||
190 | $this->field = $field; | ||
191 | $this->message = 'Configuration value is required for '. $this->field; | ||
192 | } | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Exception used if an unauthorized attempt to edit configuration has been made. | ||
197 | */ | ||
198 | class UnauthorizedConfigException extends Exception | ||
199 | { | ||
200 | /** | ||
201 | * Construct exception. | ||
202 | */ | ||
203 | public function __construct() | ||
204 | { | ||
205 | $this->message = 'You are not authorized to alter config.'; | ||
206 | } | ||
207 | } | ||
208 | |||
209 | /** | ||
210 | * Exception used if an error occur while saving plugin configuration. | ||
211 | */ | ||
212 | class PluginConfigOrderException extends Exception | ||
213 | { | ||
214 | /** | ||
215 | * Construct exception. | ||
216 | */ | ||
217 | public function __construct() | ||
218 | { | ||
219 | $this->message = 'An error occurred while trying to save plugins loading order.'; | ||
220 | } | ||
221 | } | ||
diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php index ddefe6ce..fedd90e6 100644 --- a/application/FeedBuilder.php +++ b/application/FeedBuilder.php | |||
@@ -124,7 +124,8 @@ class FeedBuilder | |||
124 | $data['last_update'] = $this->getLatestDateFormatted(); | 124 | $data['last_update'] = $this->getLatestDateFormatted(); |
125 | $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; | 125 | $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; |
126 | // Remove leading slash from REQUEST_URI. | 126 | // Remove leading slash from REQUEST_URI. |
127 | $data['self_link'] = $pageaddr . escape(ltrim($this->serverInfo['REQUEST_URI'], '/')); | 127 | $data['self_link'] = escape(server_url($this->serverInfo)) |
128 | . escape($this->serverInfo['REQUEST_URI']); | ||
128 | $data['index_url'] = $pageaddr; | 129 | $data['index_url'] = $pageaddr; |
129 | $data['usepermalinks'] = $this->usePermalinks === true; | 130 | $data['usepermalinks'] = $this->usePermalinks === true; |
130 | $data['links'] = $linkDisplayed; | 131 | $data['links'] = $linkDisplayed; |
@@ -142,7 +143,7 @@ class FeedBuilder | |||
142 | */ | 143 | */ |
143 | protected function buildItem($link, $pageaddr) | 144 | protected function buildItem($link, $pageaddr) |
144 | { | 145 | { |
145 | $link['guid'] = $pageaddr .'?'. smallHash($link['linkdate']); | 146 | $link['guid'] = $pageaddr .'?'. $link['shorturl']; |
146 | // Check for both signs of a note: starting with ? and 7 chars long. | 147 | // Check for both signs of a note: starting with ? and 7 chars long. |
147 | if ($link['url'][0] === '?' && strlen($link['url']) === 7) { | 148 | if ($link['url'][0] === '?' && strlen($link['url']) === 7) { |
148 | $link['url'] = $pageaddr . $link['url']; | 149 | $link['url'] = $pageaddr . $link['url']; |
@@ -152,19 +153,26 @@ class FeedBuilder | |||
152 | } else { | 153 | } else { |
153 | $permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>'; | 154 | $permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>'; |
154 | } | 155 | } |
155 | $link['description'] = format_description($link['description']) . PHP_EOL .'<br>— '. $permalink; | 156 | $link['description'] = format_description($link['description'], '', $pageaddr); |
157 | $link['description'] .= PHP_EOL .'<br>— '. $permalink; | ||
156 | 158 | ||
157 | $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); | 159 | $pubDate = $link['created']; |
160 | $link['pub_iso_date'] = $this->getIsoDate($pubDate); | ||
158 | 161 | ||
159 | if ($this->feedType == self::$FEED_RSS) { | 162 | // atom:entry elements MUST contain exactly one atom:updated element. |
160 | $link['iso_date'] = $date->format(DateTime::RSS); | 163 | if (!empty($link['updated'])) { |
164 | $upDate = $link['updated']; | ||
165 | $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM); | ||
161 | } else { | 166 | } else { |
162 | $link['iso_date'] = $date->format(DateTime::ATOM); | 167 | $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);; |
163 | } | 168 | } |
164 | 169 | ||
165 | // Save the more recent item. | 170 | // Save the more recent item. |
166 | if (empty($this->latestDate) || $this->latestDate < $date) { | 171 | if (empty($this->latestDate) || $this->latestDate < $pubDate) { |
167 | $this->latestDate = $date; | 172 | $this->latestDate = $pubDate; |
173 | } | ||
174 | if (!empty($upDate) && $this->latestDate < $upDate) { | ||
175 | $this->latestDate = $upDate; | ||
168 | } | 176 | } |
169 | 177 | ||
170 | $taglist = array_filter(explode(' ', $link['tags']), 'strlen'); | 178 | $taglist = array_filter(explode(' ', $link['tags']), 'strlen'); |
@@ -250,6 +258,26 @@ class FeedBuilder | |||
250 | } | 258 | } |
251 | 259 | ||
252 | /** | 260 | /** |
261 | * Get ISO date from DateTime according to feed type. | ||
262 | * | ||
263 | * @param DateTime $date Date to format. | ||
264 | * @param string|bool $format Force format. | ||
265 | * | ||
266 | * @return string Formatted date. | ||
267 | */ | ||
268 | protected function getIsoDate(DateTime $date, $format = false) | ||
269 | { | ||
270 | if ($format !== false) { | ||
271 | return $date->format($format); | ||
272 | } | ||
273 | if ($this->feedType == self::$FEED_RSS) { | ||
274 | return $date->format(DateTime::RSS); | ||
275 | |||
276 | } | ||
277 | return $date->format(DateTime::ATOM); | ||
278 | } | ||
279 | |||
280 | /** | ||
253 | * Returns the number of link to display according to 'nb' user input parameter. | 281 | * Returns the number of link to display according to 'nb' user input parameter. |
254 | * | 282 | * |
255 | * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. | 283 | * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. |
diff --git a/application/FileUtils.php b/application/FileUtils.php index 6a12ef0e..6cac9825 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php | |||
@@ -9,11 +9,13 @@ class IOException extends Exception | |||
9 | /** | 9 | /** |
10 | * Construct a new IOException | 10 | * Construct a new IOException |
11 | * | 11 | * |
12 | * @param string $path path to the ressource that cannot be accessed | 12 | * @param string $path path to the resource that cannot be accessed |
13 | * @param string $message Custom exception message. | ||
13 | */ | 14 | */ |
14 | public function __construct($path) | 15 | public function __construct($path, $message = '') |
15 | { | 16 | { |
16 | $this->path = $path; | 17 | $this->path = $path; |
17 | $this->message = 'Error accessing '.$this->path; | 18 | $this->message = empty($message) ? 'Error accessing' : $message; |
19 | $this->message .= PHP_EOL . $this->path; | ||
18 | } | 20 | } |
19 | } | 21 | } |
diff --git a/application/HttpUtils.php b/application/HttpUtils.php index 2e0792f9..e705cfd6 100644 --- a/application/HttpUtils.php +++ b/application/HttpUtils.php | |||
@@ -1,6 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | /** | 2 | /** |
3 | * GET an HTTP URL to retrieve its content | 3 | * GET an HTTP URL to retrieve its content |
4 | * Uses the cURL library or a fallback method | ||
4 | * | 5 | * |
5 | * @param string $url URL to get (http://...) | 6 | * @param string $url URL to get (http://...) |
6 | * @param int $timeout network timeout (in seconds) | 7 | * @param int $timeout network timeout (in seconds) |
@@ -20,38 +21,177 @@ | |||
20 | * echo 'There was an error: '.htmlspecialchars($headers[0]); | 21 | * echo 'There was an error: '.htmlspecialchars($headers[0]); |
21 | * } | 22 | * } |
22 | * | 23 | * |
23 | * @see http://php.net/manual/en/function.file-get-contents.php | 24 | * @see https://secure.php.net/manual/en/ref.curl.php |
24 | * @see http://php.net/manual/en/function.stream-context-create.php | 25 | * @see https://secure.php.net/manual/en/functions.anonymous.php |
25 | * @see http://php.net/manual/en/function.get-headers.php | 26 | * @see https://secure.php.net/manual/en/function.preg-split.php |
27 | * @see https://secure.php.net/manual/en/function.explode.php | ||
28 | * @see http://stackoverflow.com/q/17641073 | ||
29 | * @see http://stackoverflow.com/q/9183178 | ||
30 | * @see http://stackoverflow.com/q/1462720 | ||
26 | */ | 31 | */ |
27 | function get_http_response($url, $timeout = 30, $maxBytes = 4194304) | 32 | function get_http_response($url, $timeout = 30, $maxBytes = 4194304) |
28 | { | 33 | { |
29 | $urlObj = new Url($url); | 34 | $urlObj = new Url($url); |
30 | $cleanUrl = $urlObj->idnToAscii(); | 35 | $cleanUrl = $urlObj->idnToAscii(); |
31 | 36 | ||
32 | if (! filter_var($cleanUrl, FILTER_VALIDATE_URL) || ! $urlObj->isHttp()) { | 37 | if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) { |
33 | return array(array(0 => 'Invalid HTTP Url'), false); | 38 | return array(array(0 => 'Invalid HTTP Url'), false); |
34 | } | 39 | } |
35 | 40 | ||
41 | $userAgent = | ||
42 | 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)' | ||
43 | . ' Gecko/20100101 Firefox/45.0'; | ||
44 | $acceptLanguage = | ||
45 | substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3'; | ||
46 | $maxRedirs = 3; | ||
47 | |||
48 | if (!function_exists('curl_init')) { | ||
49 | return get_http_response_fallback( | ||
50 | $cleanUrl, | ||
51 | $timeout, | ||
52 | $maxBytes, | ||
53 | $userAgent, | ||
54 | $acceptLanguage, | ||
55 | $maxRedirs | ||
56 | ); | ||
57 | } | ||
58 | |||
59 | $ch = curl_init($cleanUrl); | ||
60 | if ($ch === false) { | ||
61 | return array(array(0 => 'curl_init() error'), false); | ||
62 | } | ||
63 | |||
64 | // General cURL settings | ||
65 | curl_setopt($ch, CURLOPT_AUTOREFERER, true); | ||
66 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | ||
67 | curl_setopt($ch, CURLOPT_HEADER, true); | ||
68 | curl_setopt( | ||
69 | $ch, | ||
70 | CURLOPT_HTTPHEADER, | ||
71 | array('Accept-Language: ' . $acceptLanguage) | ||
72 | ); | ||
73 | curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs); | ||
74 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | ||
75 | curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); | ||
76 | curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); | ||
77 | |||
78 | // Max download size management | ||
79 | curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024); | ||
80 | curl_setopt($ch, CURLOPT_NOPROGRESS, false); | ||
81 | curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, | ||
82 | function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) | ||
83 | { | ||
84 | if (version_compare(phpversion(), '5.5', '<')) { | ||
85 | // PHP version lower than 5.5 | ||
86 | // Callback has 4 arguments | ||
87 | $downloaded = $arg1; | ||
88 | } else { | ||
89 | // Callback has 5 arguments | ||
90 | $downloaded = $arg2; | ||
91 | } | ||
92 | // Non-zero return stops downloading | ||
93 | return ($downloaded > $maxBytes) ? 1 : 0; | ||
94 | } | ||
95 | ); | ||
96 | |||
97 | $response = curl_exec($ch); | ||
98 | $errorNo = curl_errno($ch); | ||
99 | $errorStr = curl_error($ch); | ||
100 | $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); | ||
101 | curl_close($ch); | ||
102 | |||
103 | if ($response === false) { | ||
104 | if ($errorNo == CURLE_COULDNT_RESOLVE_HOST) { | ||
105 | /* | ||
106 | * Workaround to match fallback method behaviour | ||
107 | * Removing this would require updating | ||
108 | * GetHttpUrlTest::testGetInvalidRemoteUrl() | ||
109 | */ | ||
110 | return array(false, false); | ||
111 | } | ||
112 | return array(array(0 => 'curl_exec() error: ' . $errorStr), false); | ||
113 | } | ||
114 | |||
115 | // Formatting output like the fallback method | ||
116 | $rawHeaders = substr($response, 0, $headSize); | ||
117 | |||
118 | // Keep only headers from latest redirection | ||
119 | $rawHeadersArrayRedirs = explode("\r\n\r\n", trim($rawHeaders)); | ||
120 | $rawHeadersLastRedir = end($rawHeadersArrayRedirs); | ||
121 | |||
122 | $content = substr($response, $headSize); | ||
123 | $headers = array(); | ||
124 | foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) { | ||
125 | if (empty($line) or ctype_space($line)) { | ||
126 | continue; | ||
127 | } | ||
128 | $splitLine = explode(': ', $line, 2); | ||
129 | if (count($splitLine) > 1) { | ||
130 | $key = $splitLine[0]; | ||
131 | $value = $splitLine[1]; | ||
132 | if (array_key_exists($key, $headers)) { | ||
133 | if (!is_array($headers[$key])) { | ||
134 | $headers[$key] = array(0 => $headers[$key]); | ||
135 | } | ||
136 | $headers[$key][] = $value; | ||
137 | } else { | ||
138 | $headers[$key] = $value; | ||
139 | } | ||
140 | } else { | ||
141 | $headers[] = $splitLine[0]; | ||
142 | } | ||
143 | } | ||
144 | |||
145 | return array($headers, $content); | ||
146 | } | ||
147 | |||
148 | /** | ||
149 | * GET an HTTP URL to retrieve its content (fallback method) | ||
150 | * | ||
151 | * @param string $cleanUrl URL to get (http://... valid and in ASCII form) | ||
152 | * @param int $timeout network timeout (in seconds) | ||
153 | * @param int $maxBytes maximum downloaded bytes | ||
154 | * @param string $userAgent "User-Agent" header | ||
155 | * @param string $acceptLanguage "Accept-Language" header | ||
156 | * @param int $maxRedr maximum amount of redirections followed | ||
157 | * | ||
158 | * @return array HTTP response headers, downloaded content | ||
159 | * | ||
160 | * Output format: | ||
161 | * [0] = associative array containing HTTP response headers | ||
162 | * [1] = URL content (downloaded data) | ||
163 | * | ||
164 | * @see http://php.net/manual/en/function.file-get-contents.php | ||
165 | * @see http://php.net/manual/en/function.stream-context-create.php | ||
166 | * @see http://php.net/manual/en/function.get-headers.php | ||
167 | */ | ||
168 | function get_http_response_fallback( | ||
169 | $cleanUrl, | ||
170 | $timeout, | ||
171 | $maxBytes, | ||
172 | $userAgent, | ||
173 | $acceptLanguage, | ||
174 | $maxRedr | ||
175 | ) { | ||
36 | $options = array( | 176 | $options = array( |
37 | 'http' => array( | 177 | 'http' => array( |
38 | 'method' => 'GET', | 178 | 'method' => 'GET', |
39 | 'timeout' => $timeout, | 179 | 'timeout' => $timeout, |
40 | 'user_agent' => 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)' | 180 | 'user_agent' => $userAgent, |
41 | .' Gecko/20100101 Firefox/45.0', | 181 | 'header' => "Accept: */*\r\n" |
42 | 'accept_language' => substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3', | 182 | . 'Accept-Language: ' . $acceptLanguage |
43 | ) | 183 | ) |
44 | ); | 184 | ); |
45 | 185 | ||
46 | stream_context_set_default($options); | 186 | stream_context_set_default($options); |
47 | list($headers, $finalUrl) = get_redirected_headers($cleanUrl); | 187 | list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); |
48 | if (! $headers || strpos($headers[0], '200 OK') === false) { | 188 | if (! $headers || strpos($headers[0], '200 OK') === false) { |
49 | $options['http']['request_fulluri'] = true; | 189 | $options['http']['request_fulluri'] = true; |
50 | stream_context_set_default($options); | 190 | stream_context_set_default($options); |
51 | list($headers, $finalUrl) = get_redirected_headers($cleanUrl); | 191 | list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); |
52 | } | 192 | } |
53 | 193 | ||
54 | if (! $headers || strpos($headers[0], '200 OK') === false) { | 194 | if (! $headers) { |
55 | return array($headers, false); | 195 | return array($headers, false); |
56 | } | 196 | } |
57 | 197 | ||
@@ -215,3 +355,29 @@ function page_url($server) | |||
215 | } | 355 | } |
216 | return index_url($server); | 356 | return index_url($server); |
217 | } | 357 | } |
358 | |||
359 | /** | ||
360 | * Retrieve the initial IP forwarded by the reverse proxy. | ||
361 | * | ||
362 | * Inspired from: https://github.com/zendframework/zend-http/blob/master/src/PhpEnvironment/RemoteAddress.php | ||
363 | * | ||
364 | * @param array $server $_SERVER array which contains HTTP headers. | ||
365 | * @param array $trustedIps List of trusted IP from the configuration. | ||
366 | * | ||
367 | * @return string|bool The forwarded IP, or false if none could be extracted. | ||
368 | */ | ||
369 | function getIpAddressFromProxy($server, $trustedIps) | ||
370 | { | ||
371 | $forwardedIpHeader = 'HTTP_X_FORWARDED_FOR'; | ||
372 | if (empty($server[$forwardedIpHeader])) { | ||
373 | return false; | ||
374 | } | ||
375 | |||
376 | $ips = preg_split('/\s*,\s*/', $server[$forwardedIpHeader]); | ||
377 | $ips = array_diff($ips, $trustedIps); | ||
378 | if (empty($ips)) { | ||
379 | return false; | ||
380 | } | ||
381 | |||
382 | return array_pop($ips); | ||
383 | } | ||
diff --git a/application/Languages.php b/application/Languages.php new file mode 100644 index 00000000..c8b0a25a --- /dev/null +++ b/application/Languages.php | |||
@@ -0,0 +1,21 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Wrapper function for translation which match the API | ||
5 | * of gettext()/_() and ngettext(). | ||
6 | * | ||
7 | * Not doing translation for now. | ||
8 | * | ||
9 | * @param string $text Text to translate. | ||
10 | * @param string $nText The plural message ID. | ||
11 | * @param int $nb The number of items for plural forms. | ||
12 | * | ||
13 | * @return String Text translated. | ||
14 | */ | ||
15 | function t($text, $nText = '', $nb = 0) { | ||
16 | if (empty($nText)) { | ||
17 | return $text; | ||
18 | } | ||
19 | $actualForm = $nb > 1 ? $nText : $text; | ||
20 | return sprintf($actualForm, $nb); | ||
21 | } | ||
diff --git a/application/LinkDB.php b/application/LinkDB.php index 1cb70de0..1e13286a 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -6,14 +6,15 @@ | |||
6 | * | 6 | * |
7 | * Example: | 7 | * Example: |
8 | * $myLinks = new LinkDB(); | 8 | * $myLinks = new LinkDB(); |
9 | * echo $myLinks['20110826_161819']['title']; | 9 | * echo $myLinks[350]['title']; |
10 | * foreach ($myLinks as $link) | 10 | * foreach ($myLinks as $link) |
11 | * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description']; | 11 | * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description']; |
12 | * | 12 | * |
13 | * Available keys: | 13 | * Available keys: |
14 | * - id: primary key, incremental integer identifier (persistent) | ||
14 | * - description: description of the entry | 15 | * - description: description of the entry |
15 | * - linkdate: date of the creation of this entry, in the form YYYYMMDD_HHMMSS | 16 | * - created: creation date of this entry, DateTime object. |
16 | * (e.g.'20110914_192317') | 17 | * - updated: last modification date of this entry, DateTime object. |
17 | * - private: Is this link private? 0=no, other value=yes | 18 | * - private: Is this link private? 0=no, other value=yes |
18 | * - tags: tags attached to this entry (separated by spaces) | 19 | * - tags: tags attached to this entry (separated by spaces) |
19 | * - title Title of the link | 20 | * - title Title of the link |
@@ -21,16 +22,30 @@ | |||
21 | * Can be absolute or relative. | 22 | * Can be absolute or relative. |
22 | * Relative URLs are permalinks (e.g.'?m-ukcw') | 23 | * Relative URLs are permalinks (e.g.'?m-ukcw') |
23 | * - real_url Absolute processed URL. | 24 | * - real_url Absolute processed URL. |
25 | * - shorturl Permalink smallhash | ||
24 | * | 26 | * |
25 | * Implements 3 interfaces: | 27 | * Implements 3 interfaces: |
26 | * - ArrayAccess: behaves like an associative array; | 28 | * - ArrayAccess: behaves like an associative array; |
27 | * - Countable: there is a count() method; | 29 | * - Countable: there is a count() method; |
28 | * - Iterator: usable in foreach () loops. | 30 | * - Iterator: usable in foreach () loops. |
31 | * | ||
32 | * ID mechanism: | ||
33 | * ArrayAccess is implemented in a way that will allow to access a link | ||
34 | * with the unique identifier ID directly with $link[ID]. | ||
35 | * Note that it's not the real key of the link array attribute. | ||
36 | * This mechanism is in place to have persistent link IDs, | ||
37 | * even though the internal array is reordered by date. | ||
38 | * Example: | ||
39 | * - DB: link #1 (2010-01-01) link #2 (2016-01-01) | ||
40 | * - Order: #2 #1 | ||
41 | * - Import links containing: link #3 (2013-01-01) | ||
42 | * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01) | ||
43 | * - Real order: #2 #3 #1 | ||
29 | */ | 44 | */ |
30 | class LinkDB implements Iterator, Countable, ArrayAccess | 45 | class LinkDB implements Iterator, Countable, ArrayAccess |
31 | { | 46 | { |
32 | // Links are stored as a PHP serialized string | 47 | // Links are stored as a PHP serialized string |
33 | private $_datastore; | 48 | private $datastore; |
34 | 49 | ||
35 | // Link date storage format | 50 | // Link date storage format |
36 | const LINK_DATE_FORMAT = 'Ymd_His'; | 51 | const LINK_DATE_FORMAT = 'Ymd_His'; |
@@ -44,26 +59,32 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
44 | // List of links (associative array) | 59 | // List of links (associative array) |
45 | // - key: link date (e.g. "20110823_124546"), | 60 | // - key: link date (e.g. "20110823_124546"), |
46 | // - value: associative array (keys: title, description...) | 61 | // - value: associative array (keys: title, description...) |
47 | private $_links; | 62 | private $links; |
63 | |||
64 | // List of all recorded URLs (key=url, value=link offset) | ||
65 | // for fast reserve search (url-->link offset) | ||
66 | private $urls; | ||
48 | 67 | ||
49 | // List of all recorded URLs (key=url, value=linkdate) | 68 | /** |
50 | // for fast reserve search (url-->linkdate) | 69 | * @var array List of all links IDS mapped with their array offset. |
51 | private $_urls; | 70 | * Map: id->offset. |
71 | */ | ||
72 | protected $ids; | ||
52 | 73 | ||
53 | // List of linkdate keys (for the Iterator interface implementation) | 74 | // List of offset keys (for the Iterator interface implementation) |
54 | private $_keys; | 75 | private $keys; |
55 | 76 | ||
56 | // Position in the $this->_keys array (for the Iterator interface) | 77 | // Position in the $this->keys array (for the Iterator interface) |
57 | private $_position; | 78 | private $position; |
58 | 79 | ||
59 | // Is the user logged in? (used to filter private links) | 80 | // Is the user logged in? (used to filter private links) |
60 | private $_loggedIn; | 81 | private $loggedIn; |
61 | 82 | ||
62 | // Hide public links | 83 | // Hide public links |
63 | private $_hidePublicLinks; | 84 | private $hidePublicLinks; |
64 | 85 | ||
65 | // link redirector set in user settings. | 86 | // link redirector set in user settings. |
66 | private $_redirector; | 87 | private $redirector; |
67 | 88 | ||
68 | /** | 89 | /** |
69 | * Set this to `true` to urlencode link behind redirector link, `false` to leave it untouched. | 90 | * Set this to `true` to urlencode link behind redirector link, `false` to leave it untouched. |
@@ -86,7 +107,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
86 | * @param string $redirector link redirector set in user settings. | 107 | * @param string $redirector link redirector set in user settings. |
87 | * @param boolean $redirectorEncode Enable urlencode on redirected urls (default: true). | 108 | * @param boolean $redirectorEncode Enable urlencode on redirected urls (default: true). |
88 | */ | 109 | */ |
89 | function __construct( | 110 | public function __construct( |
90 | $datastore, | 111 | $datastore, |
91 | $isLoggedIn, | 112 | $isLoggedIn, |
92 | $hidePublicLinks, | 113 | $hidePublicLinks, |
@@ -94,13 +115,13 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
94 | $redirectorEncode = true | 115 | $redirectorEncode = true |
95 | ) | 116 | ) |
96 | { | 117 | { |
97 | $this->_datastore = $datastore; | 118 | $this->datastore = $datastore; |
98 | $this->_loggedIn = $isLoggedIn; | 119 | $this->loggedIn = $isLoggedIn; |
99 | $this->_hidePublicLinks = $hidePublicLinks; | 120 | $this->hidePublicLinks = $hidePublicLinks; |
100 | $this->_redirector = $redirector; | 121 | $this->redirector = $redirector; |
101 | $this->redirectorEncode = $redirectorEncode === true; | 122 | $this->redirectorEncode = $redirectorEncode === true; |
102 | $this->_checkDB(); | 123 | $this->check(); |
103 | $this->_readDB(); | 124 | $this->read(); |
104 | } | 125 | } |
105 | 126 | ||
106 | /** | 127 | /** |
@@ -108,7 +129,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
108 | */ | 129 | */ |
109 | public function count() | 130 | public function count() |
110 | { | 131 | { |
111 | return count($this->_links); | 132 | return count($this->links); |
112 | } | 133 | } |
113 | 134 | ||
114 | /** | 135 | /** |
@@ -117,17 +138,29 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
117 | public function offsetSet($offset, $value) | 138 | public function offsetSet($offset, $value) |
118 | { | 139 | { |
119 | // TODO: use exceptions instead of "die" | 140 | // TODO: use exceptions instead of "die" |
120 | if (!$this->_loggedIn) { | 141 | if (!$this->loggedIn) { |
121 | die('You are not authorized to add a link.'); | 142 | die('You are not authorized to add a link.'); |
122 | } | 143 | } |
123 | if (empty($value['linkdate']) || empty($value['url'])) { | 144 | if (!isset($value['id']) || empty($value['url'])) { |
124 | die('Internal Error: A link should always have a linkdate and URL.'); | 145 | die('Internal Error: A link should always have an id and URL.'); |
146 | } | ||
147 | if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { | ||
148 | die('You must specify an integer as a key.'); | ||
125 | } | 149 | } |
126 | if (empty($offset)) { | 150 | if (! empty($offset) && $offset !== $value['id']) { |
127 | die('You must specify a key.'); | 151 | die('Array offset and link ID must be equal.'); |
128 | } | 152 | } |
129 | $this->_links[$offset] = $value; | 153 | |
130 | $this->_urls[$value['url']]=$offset; | 154 | // If the link exists, we reuse the real offset, otherwise new entry |
155 | $existing = $this->getLinkOffset($offset); | ||
156 | if ($existing !== null) { | ||
157 | $offset = $existing; | ||
158 | } else { | ||
159 | $offset = count($this->links); | ||
160 | } | ||
161 | $this->links[$offset] = $value; | ||
162 | $this->urls[$value['url']] = $offset; | ||
163 | $this->ids[$value['id']] = $offset; | ||
131 | } | 164 | } |
132 | 165 | ||
133 | /** | 166 | /** |
@@ -135,7 +168,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
135 | */ | 168 | */ |
136 | public function offsetExists($offset) | 169 | public function offsetExists($offset) |
137 | { | 170 | { |
138 | return array_key_exists($offset, $this->_links); | 171 | return array_key_exists($this->getLinkOffset($offset), $this->links); |
139 | } | 172 | } |
140 | 173 | ||
141 | /** | 174 | /** |
@@ -143,13 +176,15 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
143 | */ | 176 | */ |
144 | public function offsetUnset($offset) | 177 | public function offsetUnset($offset) |
145 | { | 178 | { |
146 | if (!$this->_loggedIn) { | 179 | if (!$this->loggedIn) { |
147 | // TODO: raise an exception | 180 | // TODO: raise an exception |
148 | die('You are not authorized to delete a link.'); | 181 | die('You are not authorized to delete a link.'); |
149 | } | 182 | } |
150 | $url = $this->_links[$offset]['url']; | 183 | $realOffset = $this->getLinkOffset($offset); |
151 | unset($this->_urls[$url]); | 184 | $url = $this->links[$realOffset]['url']; |
152 | unset($this->_links[$offset]); | 185 | unset($this->urls[$url]); |
186 | unset($this->ids[$realOffset]); | ||
187 | unset($this->links[$realOffset]); | ||
153 | } | 188 | } |
154 | 189 | ||
155 | /** | 190 | /** |
@@ -157,31 +192,32 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
157 | */ | 192 | */ |
158 | public function offsetGet($offset) | 193 | public function offsetGet($offset) |
159 | { | 194 | { |
160 | return isset($this->_links[$offset]) ? $this->_links[$offset] : null; | 195 | $realOffset = $this->getLinkOffset($offset); |
196 | return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null; | ||
161 | } | 197 | } |
162 | 198 | ||
163 | /** | 199 | /** |
164 | * Iterator - Returns the current element | 200 | * Iterator - Returns the current element |
165 | */ | 201 | */ |
166 | function current() | 202 | public function current() |
167 | { | 203 | { |
168 | return $this->_links[$this->_keys[$this->_position]]; | 204 | return $this[$this->keys[$this->position]]; |
169 | } | 205 | } |
170 | 206 | ||
171 | /** | 207 | /** |
172 | * Iterator - Returns the key of the current element | 208 | * Iterator - Returns the key of the current element |
173 | */ | 209 | */ |
174 | function key() | 210 | public function key() |
175 | { | 211 | { |
176 | return $this->_keys[$this->_position]; | 212 | return $this->keys[$this->position]; |
177 | } | 213 | } |
178 | 214 | ||
179 | /** | 215 | /** |
180 | * Iterator - Moves forward to next element | 216 | * Iterator - Moves forward to next element |
181 | */ | 217 | */ |
182 | function next() | 218 | public function next() |
183 | { | 219 | { |
184 | ++$this->_position; | 220 | ++$this->position; |
185 | } | 221 | } |
186 | 222 | ||
187 | /** | 223 | /** |
@@ -189,19 +225,18 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
189 | * | 225 | * |
190 | * Entries are sorted by date (latest first) | 226 | * Entries are sorted by date (latest first) |
191 | */ | 227 | */ |
192 | function rewind() | 228 | public function rewind() |
193 | { | 229 | { |
194 | $this->_keys = array_keys($this->_links); | 230 | $this->keys = array_keys($this->ids); |
195 | rsort($this->_keys); | 231 | $this->position = 0; |
196 | $this->_position = 0; | ||
197 | } | 232 | } |
198 | 233 | ||
199 | /** | 234 | /** |
200 | * Iterator - Checks if current position is valid | 235 | * Iterator - Checks if current position is valid |
201 | */ | 236 | */ |
202 | function valid() | 237 | public function valid() |
203 | { | 238 | { |
204 | return isset($this->_keys[$this->_position]); | 239 | return isset($this->keys[$this->position]); |
205 | } | 240 | } |
206 | 241 | ||
207 | /** | 242 | /** |
@@ -209,15 +244,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
209 | * | 244 | * |
210 | * If no DB file is found, creates a dummy DB. | 245 | * If no DB file is found, creates a dummy DB. |
211 | */ | 246 | */ |
212 | private function _checkDB() | 247 | private function check() |
213 | { | 248 | { |
214 | if (file_exists($this->_datastore)) { | 249 | if (file_exists($this->datastore)) { |
215 | return; | 250 | return; |
216 | } | 251 | } |
217 | 252 | ||
218 | // Create a dummy database for example | 253 | // Create a dummy database for example |
219 | $this->_links = array(); | 254 | $this->links = array(); |
220 | $link = array( | 255 | $link = array( |
256 | 'id' => 1, | ||
221 | 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', | 257 | 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', |
222 | 'url'=>'https://github.com/shaarli/Shaarli/wiki', | 258 | 'url'=>'https://github.com/shaarli/Shaarli/wiki', |
223 | 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. | 259 | 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. |
@@ -226,77 +262,69 @@ To learn how to use Shaarli, consult the link "Help/documentation" at the bottom | |||
226 | 262 | ||
227 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', | 263 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', |
228 | 'private'=>0, | 264 | 'private'=>0, |
229 | 'linkdate'=> date('Ymd_His'), | 265 | 'created'=> new DateTime(), |
230 | 'tags'=>'opensource software' | 266 | 'tags'=>'opensource software' |
231 | ); | 267 | ); |
232 | $this->_links[$link['linkdate']] = $link; | 268 | $link['shorturl'] = link_small_hash($link['created'], $link['id']); |
269 | $this->links[1] = $link; | ||
233 | 270 | ||
234 | $link = array( | 271 | $link = array( |
272 | 'id' => 0, | ||
235 | 'title'=>'My secret stuff... - Pastebin.com', | 273 | 'title'=>'My secret stuff... - Pastebin.com', |
236 | 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', | 274 | 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', |
237 | 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', | 275 | 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', |
238 | 'private'=>1, | 276 | 'private'=>1, |
239 | 'linkdate'=> date('Ymd_His', strtotime('-1 minute')), | 277 | 'created'=> new DateTime('1 minute ago'), |
240 | 'tags'=>'secretstuff' | 278 | 'tags'=>'secretstuff', |
241 | ); | 279 | ); |
242 | $this->_links[$link['linkdate']] = $link; | 280 | $link['shorturl'] = link_small_hash($link['created'], $link['id']); |
281 | $this->links[0] = $link; | ||
243 | 282 | ||
244 | // Write database to disk | 283 | // Write database to disk |
245 | $this->writeDB(); | 284 | $this->write(); |
246 | } | 285 | } |
247 | 286 | ||
248 | /** | 287 | /** |
249 | * Reads database from disk to memory | 288 | * Reads database from disk to memory |
250 | */ | 289 | */ |
251 | private function _readDB() | 290 | private function read() |
252 | { | 291 | { |
253 | |||
254 | // Public links are hidden and user not logged in => nothing to show | 292 | // Public links are hidden and user not logged in => nothing to show |
255 | if ($this->_hidePublicLinks && !$this->_loggedIn) { | 293 | if ($this->hidePublicLinks && !$this->loggedIn) { |
256 | $this->_links = array(); | 294 | $this->links = array(); |
257 | return; | 295 | return; |
258 | } | 296 | } |
259 | 297 | ||
260 | // Read data | 298 | // Read data |
261 | // Note that gzinflate is faster than gzuncompress. | 299 | // Note that gzinflate is faster than gzuncompress. |
262 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | 300 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 |
263 | $this->_links = array(); | 301 | $this->links = array(); |
264 | 302 | ||
265 | if (file_exists($this->_datastore)) { | 303 | if (file_exists($this->datastore)) { |
266 | $this->_links = unserialize(gzinflate(base64_decode( | 304 | $this->links = unserialize(gzinflate(base64_decode( |
267 | substr(file_get_contents($this->_datastore), | 305 | substr(file_get_contents($this->datastore), |
268 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); | 306 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); |
269 | } | 307 | } |
270 | 308 | ||
271 | // If user is not logged in, filter private links. | 309 | $toremove = array(); |
272 | if (!$this->_loggedIn) { | 310 | foreach ($this->links as $key => &$link) { |
273 | $toremove = array(); | 311 | if (! $this->loggedIn && $link['private'] != 0) { |
274 | foreach ($this->_links as $link) { | 312 | // Transition for not upgraded databases. |
275 | if ($link['private'] != 0) { | 313 | $toremove[] = $key; |
276 | $toremove[] = $link['linkdate']; | 314 | continue; |
277 | } | ||
278 | } | ||
279 | foreach ($toremove as $linkdate) { | ||
280 | unset($this->_links[$linkdate]); | ||
281 | } | 315 | } |
282 | } | ||
283 | |||
284 | $this->_urls = array(); | ||
285 | foreach ($this->_links as &$link) { | ||
286 | // Keep the list of the mapping URLs-->linkdate up-to-date. | ||
287 | $this->_urls[$link['url']] = $link['linkdate']; | ||
288 | 316 | ||
289 | // Sanitize data fields. | 317 | // Sanitize data fields. |
290 | sanitizeLink($link); | 318 | sanitizeLink($link); |
291 | 319 | ||
292 | // Remove private tags if the user is not logged in. | 320 | // Remove private tags if the user is not logged in. |
293 | if (! $this->_loggedIn) { | 321 | if (! $this->loggedIn) { |
294 | $link['tags'] = preg_replace('/(^| )\.[^($| )]+/', '', $link['tags']); | 322 | $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']); |
295 | } | 323 | } |
296 | 324 | ||
297 | // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). | 325 | // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). |
298 | if (!empty($this->_redirector) && !startsWith($link['url'], '?')) { | 326 | if (!empty($this->redirector) && !startsWith($link['url'], '?')) { |
299 | $link['real_url'] = $this->_redirector; | 327 | $link['real_url'] = $this->redirector; |
300 | if ($this->redirectorEncode) { | 328 | if ($this->redirectorEncode) { |
301 | $link['real_url'] .= urlencode(unescape($link['url'])); | 329 | $link['real_url'] .= urlencode(unescape($link['url'])); |
302 | } else { | 330 | } else { |
@@ -306,7 +334,24 @@ You use the community supported version of the original Shaarli project, by Seba | |||
306 | else { | 334 | else { |
307 | $link['real_url'] = $link['url']; | 335 | $link['real_url'] = $link['url']; |
308 | } | 336 | } |
337 | |||
338 | // To be able to load links before running the update, and prepare the update | ||
339 | if (! isset($link['created'])) { | ||
340 | $link['id'] = $link['linkdate']; | ||
341 | $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); | ||
342 | if (! empty($link['updated'])) { | ||
343 | $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']); | ||
344 | } | ||
345 | $link['shorturl'] = smallHash($link['linkdate']); | ||
346 | } | ||
309 | } | 347 | } |
348 | |||
349 | // If user is not logged in, filter private links. | ||
350 | foreach ($toremove as $offset) { | ||
351 | unset($this->links[$offset]); | ||
352 | } | ||
353 | |||
354 | $this->reorder(); | ||
310 | } | 355 | } |
311 | 356 | ||
312 | /** | 357 | /** |
@@ -314,19 +359,19 @@ You use the community supported version of the original Shaarli project, by Seba | |||
314 | * | 359 | * |
315 | * @throws IOException the datastore is not writable | 360 | * @throws IOException the datastore is not writable |
316 | */ | 361 | */ |
317 | private function writeDB() | 362 | private function write() |
318 | { | 363 | { |
319 | if (is_file($this->_datastore) && !is_writeable($this->_datastore)) { | 364 | if (is_file($this->datastore) && !is_writeable($this->datastore)) { |
320 | // The datastore exists but is not writeable | 365 | // The datastore exists but is not writeable |
321 | throw new IOException($this->_datastore); | 366 | throw new IOException($this->datastore); |
322 | } else if (!is_file($this->_datastore) && !is_writeable(dirname($this->_datastore))) { | 367 | } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { |
323 | // The datastore does not exist and its parent directory is not writeable | 368 | // The datastore does not exist and its parent directory is not writeable |
324 | throw new IOException(dirname($this->_datastore)); | 369 | throw new IOException(dirname($this->datastore)); |
325 | } | 370 | } |
326 | 371 | ||
327 | file_put_contents( | 372 | file_put_contents( |
328 | $this->_datastore, | 373 | $this->datastore, |
329 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix | 374 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix |
330 | ); | 375 | ); |
331 | 376 | ||
332 | } | 377 | } |
@@ -336,14 +381,14 @@ You use the community supported version of the original Shaarli project, by Seba | |||
336 | * | 381 | * |
337 | * @param string $pageCacheDir page cache directory | 382 | * @param string $pageCacheDir page cache directory |
338 | */ | 383 | */ |
339 | public function savedb($pageCacheDir) | 384 | public function save($pageCacheDir) |
340 | { | 385 | { |
341 | if (!$this->_loggedIn) { | 386 | if (!$this->loggedIn) { |
342 | // TODO: raise an Exception instead | 387 | // TODO: raise an Exception instead |
343 | die('You are not authorized to change the database.'); | 388 | die('You are not authorized to change the database.'); |
344 | } | 389 | } |
345 | 390 | ||
346 | $this->writeDB(); | 391 | $this->write(); |
347 | 392 | ||
348 | invalidateCaches($pageCacheDir); | 393 | invalidateCaches($pageCacheDir); |
349 | } | 394 | } |
@@ -357,8 +402,8 @@ You use the community supported version of the original Shaarli project, by Seba | |||
357 | */ | 402 | */ |
358 | public function getLinkFromUrl($url) | 403 | public function getLinkFromUrl($url) |
359 | { | 404 | { |
360 | if (isset($this->_urls[$url])) { | 405 | if (isset($this->urls[$url])) { |
361 | return $this->_links[$this->_urls[$url]]; | 406 | return $this->links[$this->urls[$url]]; |
362 | } | 407 | } |
363 | return false; | 408 | return false; |
364 | } | 409 | } |
@@ -375,7 +420,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
375 | public function filterHash($request) | 420 | public function filterHash($request) |
376 | { | 421 | { |
377 | $request = substr($request, 0, 6); | 422 | $request = substr($request, 0, 6); |
378 | $linkFilter = new LinkFilter($this->_links); | 423 | $linkFilter = new LinkFilter($this->links); |
379 | return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request); | 424 | return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request); |
380 | } | 425 | } |
381 | 426 | ||
@@ -387,7 +432,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
387 | * @return array list of shaare found. | 432 | * @return array list of shaare found. |
388 | */ | 433 | */ |
389 | public function filterDay($request) { | 434 | public function filterDay($request) { |
390 | $linkFilter = new LinkFilter($this->_links); | 435 | $linkFilter = new LinkFilter($this->links); |
391 | return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request); | 436 | return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request); |
392 | } | 437 | } |
393 | 438 | ||
@@ -409,7 +454,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
409 | $searchterm = !empty($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; | 454 | $searchterm = !empty($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; |
410 | 455 | ||
411 | // Search tags + fullsearch. | 456 | // Search tags + fullsearch. |
412 | if (empty($type) && ! empty($searchtags) && ! empty($searchterm)) { | 457 | if (! empty($searchtags) && ! empty($searchterm)) { |
413 | $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; | 458 | $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; |
414 | $request = array($searchtags, $searchterm); | 459 | $request = array($searchtags, $searchterm); |
415 | } | 460 | } |
@@ -429,7 +474,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
429 | $request = ''; | 474 | $request = ''; |
430 | } | 475 | } |
431 | 476 | ||
432 | $linkFilter = new LinkFilter($this->_links); | 477 | $linkFilter = new LinkFilter($this); |
433 | return $linkFilter->filter($type, $request, $casesensitive, $privateonly); | 478 | return $linkFilter->filter($type, $request, $casesensitive, $privateonly); |
434 | } | 479 | } |
435 | 480 | ||
@@ -440,11 +485,18 @@ You use the community supported version of the original Shaarli project, by Seba | |||
440 | public function allTags() | 485 | public function allTags() |
441 | { | 486 | { |
442 | $tags = array(); | 487 | $tags = array(); |
443 | foreach ($this->_links as $link) { | 488 | $caseMapping = array(); |
444 | foreach (explode(' ', $link['tags']) as $tag) { | 489 | foreach ($this->links as $link) { |
445 | if (!empty($tag)) { | 490 | foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { |
446 | $tags[$tag] = (empty($tags[$tag]) ? 1 : $tags[$tag] + 1); | 491 | if (empty($tag)) { |
492 | continue; | ||
447 | } | 493 | } |
494 | // The first case found will be displayed. | ||
495 | if (!isset($caseMapping[strtolower($tag)])) { | ||
496 | $caseMapping[strtolower($tag)] = $tag; | ||
497 | $tags[$caseMapping[strtolower($tag)]] = 0; | ||
498 | } | ||
499 | $tags[$caseMapping[strtolower($tag)]]++; | ||
448 | } | 500 | } |
449 | } | 501 | } |
450 | // Sort tags by usage (most used tag first) | 502 | // Sort tags by usage (most used tag first) |
@@ -459,12 +511,64 @@ You use the community supported version of the original Shaarli project, by Seba | |||
459 | public function days() | 511 | public function days() |
460 | { | 512 | { |
461 | $linkDays = array(); | 513 | $linkDays = array(); |
462 | foreach (array_keys($this->_links) as $day) { | 514 | foreach ($this->links as $link) { |
463 | $linkDays[substr($day, 0, 8)] = 0; | 515 | $linkDays[$link['created']->format('Ymd')] = 0; |
464 | } | 516 | } |
465 | $linkDays = array_keys($linkDays); | 517 | $linkDays = array_keys($linkDays); |
466 | sort($linkDays); | 518 | sort($linkDays); |
467 | 519 | ||
468 | return $linkDays; | 520 | return $linkDays; |
469 | } | 521 | } |
522 | |||
523 | /** | ||
524 | * Reorder links by creation date (newest first). | ||
525 | * | ||
526 | * Also update the urls and ids mapping arrays. | ||
527 | * | ||
528 | * @param string $order ASC|DESC | ||
529 | */ | ||
530 | public function reorder($order = 'DESC') | ||
531 | { | ||
532 | $order = $order === 'ASC' ? -1 : 1; | ||
533 | // Reorder array by dates. | ||
534 | usort($this->links, function($a, $b) use ($order) { | ||
535 | return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; | ||
536 | }); | ||
537 | |||
538 | $this->urls = array(); | ||
539 | $this->ids = array(); | ||
540 | foreach ($this->links as $key => $link) { | ||
541 | $this->urls[$link['url']] = $key; | ||
542 | $this->ids[$link['id']] = $key; | ||
543 | } | ||
544 | } | ||
545 | |||
546 | /** | ||
547 | * Return the next key for link creation. | ||
548 | * E.g. If the last ID is 597, the next will be 598. | ||
549 | * | ||
550 | * @return int next ID. | ||
551 | */ | ||
552 | public function getNextId() | ||
553 | { | ||
554 | if (!empty($this->ids)) { | ||
555 | return max(array_keys($this->ids)) + 1; | ||
556 | } | ||
557 | return 0; | ||
558 | } | ||
559 | |||
560 | /** | ||
561 | * Returns a link offset in links array from its unique ID. | ||
562 | * | ||
563 | * @param int $id Persistent ID of a link. | ||
564 | * | ||
565 | * @return int Real offset in local array, or null if doesn't exist. | ||
566 | */ | ||
567 | protected function getLinkOffset($id) | ||
568 | { | ||
569 | if (isset($this->ids[$id])) { | ||
570 | return $this->ids[$id]; | ||
571 | } | ||
572 | return null; | ||
573 | } | ||
470 | } | 574 | } |
diff --git a/application/LinkFilter.php b/application/LinkFilter.php index e693b284..daa6d9cc 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php | |||
@@ -28,12 +28,17 @@ class LinkFilter | |||
28 | public static $FILTER_DAY = 'FILTER_DAY'; | 28 | public static $FILTER_DAY = 'FILTER_DAY'; |
29 | 29 | ||
30 | /** | 30 | /** |
31 | * @var array all available links. | 31 | * @var string Allowed characters for hashtags (regex syntax). |
32 | */ | ||
33 | public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; | ||
34 | |||
35 | /** | ||
36 | * @var LinkDB all available links. | ||
32 | */ | 37 | */ |
33 | private $links; | 38 | private $links; |
34 | 39 | ||
35 | /** | 40 | /** |
36 | * @param array $links initialization. | 41 | * @param LinkDB $links initialization. |
37 | */ | 42 | */ |
38 | public function __construct($links) | 43 | public function __construct($links) |
39 | { | 44 | { |
@@ -89,18 +94,16 @@ class LinkFilter | |||
89 | private function noFilter($privateonly = false) | 94 | private function noFilter($privateonly = false) |
90 | { | 95 | { |
91 | if (! $privateonly) { | 96 | if (! $privateonly) { |
92 | krsort($this->links); | ||
93 | return $this->links; | 97 | return $this->links; |
94 | } | 98 | } |
95 | 99 | ||
96 | $out = array(); | 100 | $out = array(); |
97 | foreach ($this->links as $value) { | 101 | foreach ($this->links as $key => $value) { |
98 | if ($value['private']) { | 102 | if ($value['private']) { |
99 | $out[$value['linkdate']] = $value; | 103 | $out[$key] = $value; |
100 | } | 104 | } |
101 | } | 105 | } |
102 | 106 | ||
103 | krsort($out); | ||
104 | return $out; | 107 | return $out; |
105 | } | 108 | } |
106 | 109 | ||
@@ -116,10 +119,10 @@ class LinkFilter | |||
116 | private function filterSmallHash($smallHash) | 119 | private function filterSmallHash($smallHash) |
117 | { | 120 | { |
118 | $filtered = array(); | 121 | $filtered = array(); |
119 | foreach ($this->links as $l) { | 122 | foreach ($this->links as $key => $l) { |
120 | if ($smallHash == smallHash($l['linkdate'])) { | 123 | if ($smallHash == $l['shorturl']) { |
121 | // Yes, this is ugly and slow | 124 | // Yes, this is ugly and slow |
122 | $filtered[$l['linkdate']] = $l; | 125 | $filtered[$key] = $l; |
123 | return $filtered; | 126 | return $filtered; |
124 | } | 127 | } |
125 | } | 128 | } |
@@ -183,7 +186,7 @@ class LinkFilter | |||
183 | $keys = array('title', 'description', 'url', 'tags'); | 186 | $keys = array('title', 'description', 'url', 'tags'); |
184 | 187 | ||
185 | // Iterate over every stored link. | 188 | // Iterate over every stored link. |
186 | foreach ($this->links as $link) { | 189 | foreach ($this->links as $id => $link) { |
187 | 190 | ||
188 | // ignore non private links when 'privatonly' is on. | 191 | // ignore non private links when 'privatonly' is on. |
189 | if (! $link['private'] && $privateonly === true) { | 192 | if (! $link['private'] && $privateonly === true) { |
@@ -217,11 +220,10 @@ class LinkFilter | |||
217 | } | 220 | } |
218 | 221 | ||
219 | if ($found) { | 222 | if ($found) { |
220 | $filtered[$link['linkdate']] = $link; | 223 | $filtered[$id] = $link; |
221 | } | 224 | } |
222 | } | 225 | } |
223 | 226 | ||
224 | krsort($filtered); | ||
225 | return $filtered; | 227 | return $filtered; |
226 | } | 228 | } |
227 | 229 | ||
@@ -251,7 +253,7 @@ class LinkFilter | |||
251 | return $filtered; | 253 | return $filtered; |
252 | } | 254 | } |
253 | 255 | ||
254 | foreach ($this->links as $link) { | 256 | foreach ($this->links as $key => $link) { |
255 | // ignore non private links when 'privatonly' is on. | 257 | // ignore non private links when 'privatonly' is on. |
256 | if (! $link['private'] && $privateonly === true) { | 258 | if (! $link['private'] && $privateonly === true) { |
257 | continue; | 259 | continue; |
@@ -263,18 +265,19 @@ class LinkFilter | |||
263 | for ($i = 0 ; $i < count($searchtags) && $found; $i++) { | 265 | for ($i = 0 ; $i < count($searchtags) && $found; $i++) { |
264 | // Exclusive search, quit if tag found. | 266 | // Exclusive search, quit if tag found. |
265 | // Or, tag not found in the link, quit. | 267 | // Or, tag not found in the link, quit. |
266 | if (($searchtags[$i][0] == '-' && in_array(substr($searchtags[$i], 1), $linktags)) | 268 | if (($searchtags[$i][0] == '-' |
267 | || ($searchtags[$i][0] != '-') && ! in_array($searchtags[$i], $linktags) | 269 | && $this->searchTagAndHashTag(substr($searchtags[$i], 1), $linktags, $link['description'])) |
270 | || ($searchtags[$i][0] != '-') | ||
271 | && ! $this->searchTagAndHashTag($searchtags[$i], $linktags, $link['description']) | ||
268 | ) { | 272 | ) { |
269 | $found = false; | 273 | $found = false; |
270 | } | 274 | } |
271 | } | 275 | } |
272 | 276 | ||
273 | if ($found) { | 277 | if ($found) { |
274 | $filtered[$link['linkdate']] = $link; | 278 | $filtered[$key] = $link; |
275 | } | 279 | } |
276 | } | 280 | } |
277 | krsort($filtered); | ||
278 | return $filtered; | 281 | return $filtered; |
279 | } | 282 | } |
280 | 283 | ||
@@ -297,13 +300,36 @@ class LinkFilter | |||
297 | } | 300 | } |
298 | 301 | ||
299 | $filtered = array(); | 302 | $filtered = array(); |
300 | foreach ($this->links as $l) { | 303 | foreach ($this->links as $key => $l) { |
301 | if (startsWith($l['linkdate'], $day)) { | 304 | if ($l['created']->format('Ymd') == $day) { |
302 | $filtered[$l['linkdate']] = $l; | 305 | $filtered[$key] = $l; |
303 | } | 306 | } |
304 | } | 307 | } |
305 | ksort($filtered); | 308 | |
306 | return $filtered; | 309 | // sort by date ASC |
310 | return array_reverse($filtered, true); | ||
311 | } | ||
312 | |||
313 | /** | ||
314 | * Check if a tag is found in the taglist, or as an hashtag in the link description. | ||
315 | * | ||
316 | * @param string $tag Tag to search. | ||
317 | * @param array $taglist List of tags for the current link. | ||
318 | * @param string $description Link description. | ||
319 | * | ||
320 | * @return bool True if found, false otherwise. | ||
321 | */ | ||
322 | protected function searchTagAndHashTag($tag, $taglist, $description) | ||
323 | { | ||
324 | if (in_array($tag, $taglist)) { | ||
325 | return true; | ||
326 | } | ||
327 | |||
328 | if (preg_match('/(^| )#'. $tag .'([^'. self::$HASHTAG_CHARS .']|$)/mui', $description) > 0) { | ||
329 | return true; | ||
330 | } | ||
331 | |||
332 | return false; | ||
307 | } | 333 | } |
308 | 334 | ||
309 | /** | 335 | /** |
diff --git a/application/LinkUtils.php b/application/LinkUtils.php index da04ca97..cf58f808 100644 --- a/application/LinkUtils.php +++ b/application/LinkUtils.php | |||
@@ -81,7 +81,7 @@ function html_extract_charset($html) | |||
81 | /** | 81 | /** |
82 | * Count private links in given linklist. | 82 | * Count private links in given linklist. |
83 | * | 83 | * |
84 | * @param array $links Linklist. | 84 | * @param array|Countable $links Linklist. |
85 | * | 85 | * |
86 | * @return int Number of private links. | 86 | * @return int Number of private links. |
87 | */ | 87 | */ |
@@ -91,5 +91,94 @@ function count_private($links) | |||
91 | foreach ($links as $link) { | 91 | foreach ($links as $link) { |
92 | $cpt = $link['private'] == true ? $cpt + 1 : $cpt; | 92 | $cpt = $link['private'] == true ? $cpt + 1 : $cpt; |
93 | } | 93 | } |
94 | |||
94 | return $cpt; | 95 | return $cpt; |
95 | } | 96 | } |
97 | |||
98 | /** | ||
99 | * In a string, converts URLs to clickable links. | ||
100 | * | ||
101 | * @param string $text input string. | ||
102 | * @param string $redirector if a redirector is set, use it to gerenate links. | ||
103 | * | ||
104 | * @return string returns $text with all links converted to HTML links. | ||
105 | * | ||
106 | * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 | ||
107 | */ | ||
108 | function text2clickable($text, $redirector = '') | ||
109 | { | ||
110 | $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si'; | ||
111 | |||
112 | if (empty($redirector)) { | ||
113 | return preg_replace($regex, '<a href="$1">$1</a>', $text); | ||
114 | } | ||
115 | // Redirector is set, urlencode the final URL. | ||
116 | return preg_replace_callback( | ||
117 | $regex, | ||
118 | function ($matches) use ($redirector) { | ||
119 | return '<a href="' . $redirector . urlencode($matches[1]) .'">'. $matches[1] .'</a>'; | ||
120 | }, | ||
121 | $text | ||
122 | ); | ||
123 | } | ||
124 | |||
125 | /** | ||
126 | * Auto-link hashtags. | ||
127 | * | ||
128 | * @param string $description Given description. | ||
129 | * @param string $indexUrl Root URL. | ||
130 | * | ||
131 | * @return string Description with auto-linked hashtags. | ||
132 | */ | ||
133 | function hashtag_autolink($description, $indexUrl = '') | ||
134 | { | ||
135 | /* | ||
136 | * To support unicode: http://stackoverflow.com/a/35498078/1484919 | ||
137 | * \p{Pc} - to match underscore | ||
138 | * \p{N} - numeric character in any script | ||
139 | * \p{L} - letter from any language | ||
140 | * \p{Mn} - any non marking space (accents, umlauts, etc) | ||
141 | */ | ||
142 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; | ||
143 | $replacement = '$1<a href="'. $indexUrl .'?addtag=$2" title="Hashtag $2">#$2</a>'; | ||
144 | return preg_replace($regex, $replacement, $description); | ||
145 | } | ||
146 | |||
147 | /** | ||
148 | * This function inserts where relevant so that multiple spaces are properly displayed in HTML | ||
149 | * even in the absence of <pre> (This is used in description to keep text formatting). | ||
150 | * | ||
151 | * @param string $text input text. | ||
152 | * | ||
153 | * @return string formatted text. | ||
154 | */ | ||
155 | function space2nbsp($text) | ||
156 | { | ||
157 | return preg_replace('/(^| ) /m', '$1 ', $text); | ||
158 | } | ||
159 | |||
160 | /** | ||
161 | * Format Shaarli's description | ||
162 | * | ||
163 | * @param string $description shaare's description. | ||
164 | * @param string $redirector if a redirector is set, use it to gerenate links. | ||
165 | * @param string $indexUrl URL to Shaarli's index. | ||
166 | * | ||
167 | * @return string formatted description. | ||
168 | */ | ||
169 | function format_description($description, $redirector = '', $indexUrl = '') { | ||
170 | return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl))); | ||
171 | } | ||
172 | |||
173 | /** | ||
174 | * Generate a small hash for a link. | ||
175 | * | ||
176 | * @param DateTime $date Link creation date. | ||
177 | * @param int $id Link ID. | ||
178 | * | ||
179 | * @return string the small hash generated from link data. | ||
180 | */ | ||
181 | function link_small_hash($date, $id) | ||
182 | { | ||
183 | return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id); | ||
184 | } | ||
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php index fdbb0ad7..e7148d00 100644 --- a/application/NetscapeBookmarkUtils.php +++ b/application/NetscapeBookmarkUtils.php | |||
@@ -38,7 +38,7 @@ class NetscapeBookmarkUtils | |||
38 | if ($link['private'] == 0 && $selection == 'private') { | 38 | if ($link['private'] == 0 && $selection == 'private') { |
39 | continue; | 39 | continue; |
40 | } | 40 | } |
41 | $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); | 41 | $date = $link['created']; |
42 | $link['timestamp'] = $date->getTimestamp(); | 42 | $link['timestamp'] = $date->getTimestamp(); |
43 | $link['taglist'] = str_replace(' ', ',', $link['tags']); | 43 | $link['taglist'] = str_replace(' ', ',', $link['tags']); |
44 | 44 | ||
@@ -51,4 +51,141 @@ class NetscapeBookmarkUtils | |||
51 | 51 | ||
52 | return $bookmarkLinks; | 52 | return $bookmarkLinks; |
53 | } | 53 | } |
54 | |||
55 | /** | ||
56 | * Generates an import status summary | ||
57 | * | ||
58 | * @param string $filename name of the file to import | ||
59 | * @param int $filesize size of the file to import | ||
60 | * @param int $importCount how many links were imported | ||
61 | * @param int $overwriteCount how many links were overwritten | ||
62 | * @param int $skipCount how many links were skipped | ||
63 | * | ||
64 | * @return string Summary of the bookmark import status | ||
65 | */ | ||
66 | private static function importStatus( | ||
67 | $filename, | ||
68 | $filesize, | ||
69 | $importCount=0, | ||
70 | $overwriteCount=0, | ||
71 | $skipCount=0 | ||
72 | ) | ||
73 | { | ||
74 | $status = 'File '.$filename.' ('.$filesize.' bytes) '; | ||
75 | if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { | ||
76 | $status .= 'has an unknown file format. Nothing was imported.'; | ||
77 | } else { | ||
78 | $status .= 'was successfully processed: '.$importCount.' links imported, '; | ||
79 | $status .= $overwriteCount.' links overwritten, '; | ||
80 | $status .= $skipCount.' links skipped.'; | ||
81 | } | ||
82 | return $status; | ||
83 | } | ||
84 | |||
85 | /** | ||
86 | * Imports Web bookmarks from an uploaded Netscape bookmark dump | ||
87 | * | ||
88 | * @param array $post Server $_POST parameters | ||
89 | * @param array $files Server $_FILES parameters | ||
90 | * @param LinkDB $linkDb Loaded LinkDB instance | ||
91 | * @param string $pagecache Page cache | ||
92 | * | ||
93 | * @return string Summary of the bookmark import status | ||
94 | */ | ||
95 | public static function import($post, $files, $linkDb, $pagecache) | ||
96 | { | ||
97 | $filename = $files['filetoupload']['name']; | ||
98 | $filesize = $files['filetoupload']['size']; | ||
99 | $data = file_get_contents($files['filetoupload']['tmp_name']); | ||
100 | |||
101 | if (strpos($data, '<!DOCTYPE NETSCAPE-Bookmark-file-1>') === false) { | ||
102 | return self::importStatus($filename, $filesize); | ||
103 | } | ||
104 | |||
105 | // Overwrite existing links? | ||
106 | $overwrite = ! empty($post['overwrite']); | ||
107 | |||
108 | // Add tags to all imported links? | ||
109 | if (empty($post['default_tags'])) { | ||
110 | $defaultTags = array(); | ||
111 | } else { | ||
112 | $defaultTags = preg_split( | ||
113 | '/[\s,]+/', | ||
114 | escape($post['default_tags']) | ||
115 | ); | ||
116 | } | ||
117 | |||
118 | // links are imported as public by default | ||
119 | $defaultPrivacy = 0; | ||
120 | |||
121 | $parser = new NetscapeBookmarkParser( | ||
122 | true, // nested tag support | ||
123 | $defaultTags, // additional user-specified tags | ||
124 | strval(1 - $defaultPrivacy) // defaultPub = 1 - defaultPrivacy | ||
125 | ); | ||
126 | $bookmarks = $parser->parseString($data); | ||
127 | |||
128 | $importCount = 0; | ||
129 | $overwriteCount = 0; | ||
130 | $skipCount = 0; | ||
131 | |||
132 | foreach ($bookmarks as $bkm) { | ||
133 | $private = $defaultPrivacy; | ||
134 | if (empty($post['privacy']) || $post['privacy'] == 'default') { | ||
135 | // use value from the imported file | ||
136 | $private = $bkm['pub'] == '1' ? 0 : 1; | ||
137 | } else if ($post['privacy'] == 'private') { | ||
138 | // all imported links are private | ||
139 | $private = 1; | ||
140 | } else if ($post['privacy'] == 'public') { | ||
141 | // all imported links are public | ||
142 | $private = 0; | ||
143 | } | ||
144 | |||
145 | $newLink = array( | ||
146 | 'title' => $bkm['title'], | ||
147 | 'url' => $bkm['uri'], | ||
148 | 'description' => $bkm['note'], | ||
149 | 'private' => $private, | ||
150 | 'tags' => $bkm['tags'] | ||
151 | ); | ||
152 | |||
153 | $existingLink = $linkDb->getLinkFromUrl($bkm['uri']); | ||
154 | |||
155 | if ($existingLink !== false) { | ||
156 | if ($overwrite === false) { | ||
157 | // Do not overwrite an existing link | ||
158 | $skipCount++; | ||
159 | continue; | ||
160 | } | ||
161 | |||
162 | // Overwrite an existing link, keep its date | ||
163 | $newLink['id'] = $existingLink['id']; | ||
164 | $newLink['created'] = $existingLink['created']; | ||
165 | $newLink['updated'] = new DateTime(); | ||
166 | $linkDb[$existingLink['id']] = $newLink; | ||
167 | $importCount++; | ||
168 | $overwriteCount++; | ||
169 | continue; | ||
170 | } | ||
171 | |||
172 | // Add a new link - @ used for UNIX timestamps | ||
173 | $newLinkDate = new DateTime('@'.strval($bkm['time'])); | ||
174 | $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get())); | ||
175 | $newLink['created'] = $newLinkDate; | ||
176 | $newLink['id'] = $linkDb->getNextId(); | ||
177 | $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); | ||
178 | $linkDb[$newLink['id']] = $newLink; | ||
179 | $importCount++; | ||
180 | } | ||
181 | |||
182 | $linkDb->save($pagecache); | ||
183 | return self::importStatus( | ||
184 | $filename, | ||
185 | $filesize, | ||
186 | $importCount, | ||
187 | $overwriteCount, | ||
188 | $skipCount | ||
189 | ); | ||
190 | } | ||
54 | } | 191 | } |
diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 82580787..32c7f9f1 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php | |||
@@ -15,12 +15,20 @@ class PageBuilder | |||
15 | private $tpl; | 15 | private $tpl; |
16 | 16 | ||
17 | /** | 17 | /** |
18 | * @var ConfigManager $conf Configuration Manager instance. | ||
19 | */ | ||
20 | protected $conf; | ||
21 | |||
22 | /** | ||
18 | * PageBuilder constructor. | 23 | * PageBuilder constructor. |
19 | * $tpl is initialized at false for lazy loading. | 24 | * $tpl is initialized at false for lazy loading. |
25 | * | ||
26 | * @param ConfigManager $conf Configuration Manager instance (reference). | ||
20 | */ | 27 | */ |
21 | function __construct() | 28 | function __construct(&$conf) |
22 | { | 29 | { |
23 | $this->tpl = false; | 30 | $this->tpl = false; |
31 | $this->conf = $conf; | ||
24 | } | 32 | } |
25 | 33 | ||
26 | /** | 34 | /** |
@@ -33,17 +41,17 @@ class PageBuilder | |||
33 | try { | 41 | try { |
34 | $version = ApplicationUtils::checkUpdate( | 42 | $version = ApplicationUtils::checkUpdate( |
35 | shaarli_version, | 43 | shaarli_version, |
36 | $GLOBALS['config']['UPDATECHECK_FILENAME'], | 44 | $this->conf->get('resource.update_check'), |
37 | $GLOBALS['config']['UPDATECHECK_INTERVAL'], | 45 | $this->conf->get('updates.check_updates_interval'), |
38 | $GLOBALS['config']['ENABLE_UPDATECHECK'], | 46 | $this->conf->get('updates.check_updates'), |
39 | isLoggedIn(), | 47 | isLoggedIn(), |
40 | $GLOBALS['config']['UPDATECHECK_BRANCH'] | 48 | $this->conf->get('updates.check_updates_branch') |
41 | ); | 49 | ); |
42 | $this->tpl->assign('newVersion', escape($version)); | 50 | $this->tpl->assign('newVersion', escape($version)); |
43 | $this->tpl->assign('versionError', ''); | 51 | $this->tpl->assign('versionError', ''); |
44 | 52 | ||
45 | } catch (Exception $exc) { | 53 | } catch (Exception $exc) { |
46 | logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], $exc->getMessage()); | 54 | logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); |
47 | $this->tpl->assign('newVersion', ''); | 55 | $this->tpl->assign('newVersion', ''); |
48 | $this->tpl->assign('versionError', escape($exc->getMessage())); | 56 | $this->tpl->assign('versionError', escape($exc->getMessage())); |
49 | } | 57 | } |
@@ -60,21 +68,18 @@ class PageBuilder | |||
60 | $this->tpl->assign('source', index_url($_SERVER)); | 68 | $this->tpl->assign('source', index_url($_SERVER)); |
61 | $this->tpl->assign('version', shaarli_version); | 69 | $this->tpl->assign('version', shaarli_version); |
62 | $this->tpl->assign('scripturl', index_url($_SERVER)); | 70 | $this->tpl->assign('scripturl', index_url($_SERVER)); |
63 | $this->tpl->assign('pagetitle', 'Shaarli'); | ||
64 | $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? | 71 | $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? |
65 | if (!empty($GLOBALS['title'])) { | 72 | $this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli')); |
66 | $this->tpl->assign('pagetitle', $GLOBALS['title']); | 73 | if ($this->conf->exists('general.header_link')) { |
67 | } | 74 | $this->tpl->assign('titleLink', $this->conf->get('general.header_link')); |
68 | if (!empty($GLOBALS['titleLink'])) { | ||
69 | $this->tpl->assign('titleLink', $GLOBALS['titleLink']); | ||
70 | } | ||
71 | if (!empty($GLOBALS['pagetitle'])) { | ||
72 | $this->tpl->assign('pagetitle', $GLOBALS['pagetitle']); | ||
73 | } | ||
74 | $this->tpl->assign('shaarlititle', empty($GLOBALS['title']) ? 'Shaarli': $GLOBALS['title']); | ||
75 | if (!empty($GLOBALS['plugin_errors'])) { | ||
76 | $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']); | ||
77 | } | 75 | } |
76 | $this->tpl->assign('shaarlititle', $this->conf->get('general.title', 'Shaarli')); | ||
77 | $this->tpl->assign('openshaarli', $this->conf->get('security.open_shaarli', false)); | ||
78 | $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', false)); | ||
79 | $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); | ||
80 | $this->tpl->assign('token', getToken($this->conf)); | ||
81 | // To be removed with a proper theme configuration. | ||
82 | $this->tpl->assign('conf', $this->conf); | ||
78 | } | 83 | } |
79 | 84 | ||
80 | /** | 85 | /** |
@@ -85,7 +90,6 @@ class PageBuilder | |||
85 | */ | 90 | */ |
86 | public function assign($placeholder, $value) | 91 | public function assign($placeholder, $value) |
87 | { | 92 | { |
88 | // Lazy initialization | ||
89 | if ($this->tpl === false) { | 93 | if ($this->tpl === false) { |
90 | $this->initialize(); | 94 | $this->initialize(); |
91 | } | 95 | } |
@@ -101,7 +105,6 @@ class PageBuilder | |||
101 | */ | 105 | */ |
102 | public function assignAll($data) | 106 | public function assignAll($data) |
103 | { | 107 | { |
104 | // Lazy initialization | ||
105 | if ($this->tpl === false) { | 108 | if ($this->tpl === false) { |
106 | $this->initialize(); | 109 | $this->initialize(); |
107 | } | 110 | } |
@@ -113,6 +116,7 @@ class PageBuilder | |||
113 | foreach ($data as $key => $value) { | 116 | foreach ($data as $key => $value) { |
114 | $this->assign($key, $value); | 117 | $this->assign($key, $value); |
115 | } | 118 | } |
119 | return true; | ||
116 | } | 120 | } |
117 | 121 | ||
118 | /** | 122 | /** |
@@ -123,10 +127,10 @@ class PageBuilder | |||
123 | */ | 127 | */ |
124 | public function renderPage($page) | 128 | public function renderPage($page) |
125 | { | 129 | { |
126 | // Lazy initialization | 130 | if ($this->tpl === false) { |
127 | if ($this->tpl===false) { | ||
128 | $this->initialize(); | 131 | $this->initialize(); |
129 | } | 132 | } |
133 | |||
130 | $this->tpl->draw($page); | 134 | $this->tpl->draw($page); |
131 | } | 135 | } |
132 | 136 | ||
diff --git a/application/PluginManager.php b/application/PluginManager.php index 787ac6a9..59ece4fa 100644 --- a/application/PluginManager.php +++ b/application/PluginManager.php | |||
@@ -4,18 +4,10 @@ | |||
4 | * Class PluginManager | 4 | * Class PluginManager |
5 | * | 5 | * |
6 | * Use to manage, load and execute plugins. | 6 | * Use to manage, load and execute plugins. |
7 | * | ||
8 | * Using Singleton design pattern. | ||
9 | */ | 7 | */ |
10 | class PluginManager | 8 | class PluginManager |
11 | { | 9 | { |
12 | /** | 10 | /** |
13 | * PluginManager singleton instance. | ||
14 | * @var PluginManager $instance | ||
15 | */ | ||
16 | private static $instance; | ||
17 | |||
18 | /** | ||
19 | * List of authorized plugins from configuration file. | 11 | * List of authorized plugins from configuration file. |
20 | * @var array $authorizedPlugins | 12 | * @var array $authorizedPlugins |
21 | */ | 13 | */ |
@@ -28,45 +20,36 @@ class PluginManager | |||
28 | private $loadedPlugins = array(); | 20 | private $loadedPlugins = array(); |
29 | 21 | ||
30 | /** | 22 | /** |
31 | * Plugins subdirectory. | 23 | * @var ConfigManager Configuration Manager instance. |
32 | * @var string $PLUGINS_PATH | ||
33 | */ | 24 | */ |
34 | public static $PLUGINS_PATH = 'plugins'; | 25 | protected $conf; |
35 | 26 | ||
36 | /** | 27 | /** |
37 | * Plugins meta files extension. | 28 | * @var array List of plugin errors. |
38 | * @var string $META_EXT | ||
39 | */ | 29 | */ |
40 | public static $META_EXT = 'meta'; | 30 | protected $errors; |
41 | 31 | ||
42 | /** | 32 | /** |
43 | * Private constructor: new instances not allowed. | 33 | * Plugins subdirectory. |
34 | * @var string $PLUGINS_PATH | ||
44 | */ | 35 | */ |
45 | private function __construct() | 36 | public static $PLUGINS_PATH = 'plugins'; |
46 | { | ||
47 | } | ||
48 | 37 | ||
49 | /** | 38 | /** |
50 | * Cloning isn't allowed either. | 39 | * Plugins meta files extension. |
51 | * | 40 | * @var string $META_EXT |
52 | * @return void | ||
53 | */ | 41 | */ |
54 | private function __clone() | 42 | public static $META_EXT = 'meta'; |
55 | { | ||
56 | } | ||
57 | 43 | ||
58 | /** | 44 | /** |
59 | * Return existing instance of PluginManager, or create it. | 45 | * Constructor. |
60 | * | 46 | * |
61 | * @return PluginManager instance. | 47 | * @param ConfigManager $conf Configuration Manager instance. |
62 | */ | 48 | */ |
63 | public static function getInstance() | 49 | public function __construct(&$conf) |
64 | { | 50 | { |
65 | if (!(self::$instance instanceof self)) { | 51 | $this->conf = $conf; |
66 | self::$instance = new self(); | 52 | $this->errors = array(); |
67 | } | ||
68 | |||
69 | return self::$instance; | ||
70 | } | 53 | } |
71 | 54 | ||
72 | /** | 55 | /** |
@@ -102,9 +85,9 @@ class PluginManager | |||
102 | /** | 85 | /** |
103 | * Execute all plugins registered hook. | 86 | * Execute all plugins registered hook. |
104 | * | 87 | * |
105 | * @param string $hook name of the hook to trigger. | 88 | * @param string $hook name of the hook to trigger. |
106 | * @param array $data list of data to manipulate passed by reference. | 89 | * @param array $data list of data to manipulate passed by reference. |
107 | * @param array $params additional parameters such as page target. | 90 | * @param array $params additional parameters such as page target. |
108 | * | 91 | * |
109 | * @return void | 92 | * @return void |
110 | */ | 93 | */ |
@@ -122,13 +105,14 @@ class PluginManager | |||
122 | $hookFunction = $this->buildHookName($hook, $plugin); | 105 | $hookFunction = $this->buildHookName($hook, $plugin); |
123 | 106 | ||
124 | if (function_exists($hookFunction)) { | 107 | if (function_exists($hookFunction)) { |
125 | $data = call_user_func($hookFunction, $data); | 108 | $data = call_user_func($hookFunction, $data, $this->conf); |
126 | } | 109 | } |
127 | } | 110 | } |
128 | } | 111 | } |
129 | 112 | ||
130 | /** | 113 | /** |
131 | * Load a single plugin from its files. | 114 | * Load a single plugin from its files. |
115 | * Call the init function if it exists, and collect errors. | ||
132 | * Add them in $loadedPlugins if successful. | 116 | * Add them in $loadedPlugins if successful. |
133 | * | 117 | * |
134 | * @param string $dir plugin's directory. | 118 | * @param string $dir plugin's directory. |
@@ -148,8 +132,17 @@ class PluginManager | |||
148 | throw new PluginFileNotFoundException($pluginName); | 132 | throw new PluginFileNotFoundException($pluginName); |
149 | } | 133 | } |
150 | 134 | ||
135 | $conf = $this->conf; | ||
151 | include_once $pluginFilePath; | 136 | include_once $pluginFilePath; |
152 | 137 | ||
138 | $initFunction = $pluginName . '_init'; | ||
139 | if (function_exists($initFunction)) { | ||
140 | $errors = call_user_func($initFunction, $this->conf); | ||
141 | if (!empty($errors)) { | ||
142 | $this->errors = array_merge($this->errors, $errors); | ||
143 | } | ||
144 | } | ||
145 | |||
153 | $this->loadedPlugins[] = $pluginName; | 146 | $this->loadedPlugins[] = $pluginName; |
154 | } | 147 | } |
155 | 148 | ||
@@ -207,12 +200,26 @@ class PluginManager | |||
207 | continue; | 200 | continue; |
208 | } | 201 | } |
209 | 202 | ||
210 | $metaData[$plugin]['parameters'][$param] = ''; | 203 | $metaData[$plugin]['parameters'][$param]['value'] = ''; |
204 | // Optional parameter description in parameter.PARAM_NAME= | ||
205 | if (isset($metaData[$plugin]['parameter.'. $param])) { | ||
206 | $metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param]; | ||
207 | } | ||
211 | } | 208 | } |
212 | } | 209 | } |
213 | 210 | ||
214 | return $metaData; | 211 | return $metaData; |
215 | } | 212 | } |
213 | |||
214 | /** | ||
215 | * Return the list of encountered errors. | ||
216 | * | ||
217 | * @return array List of errors (empty array if none exists). | ||
218 | */ | ||
219 | public function getErrors() | ||
220 | { | ||
221 | return $this->errors; | ||
222 | } | ||
216 | } | 223 | } |
217 | 224 | ||
218 | /** | 225 | /** |
@@ -232,4 +239,4 @@ class PluginFileNotFoundException extends Exception | |||
232 | { | 239 | { |
233 | $this->message = 'Plugin "'. $pluginName .'" files not found.'; | 240 | $this->message = 'Plugin "'. $pluginName .'" files not found.'; |
234 | } | 241 | } |
235 | } \ No newline at end of file | 242 | } |
diff --git a/application/Router.php b/application/Router.php index 2c3934b0..caed4a28 100644 --- a/application/Router.php +++ b/application/Router.php | |||
@@ -138,4 +138,4 @@ class Router | |||
138 | 138 | ||
139 | return self::$PAGE_LINKLIST; | 139 | return self::$PAGE_LINKLIST; |
140 | } | 140 | } |
141 | } \ No newline at end of file | 141 | } |
diff --git a/application/TimeZone.php b/application/TimeZone.php index 26f2232d..36a8fb12 100644 --- a/application/TimeZone.php +++ b/application/TimeZone.php | |||
@@ -7,9 +7,9 @@ | |||
7 | * Example: preselect Europe/Paris | 7 | * Example: preselect Europe/Paris |
8 | * list($htmlform, $js) = generateTimeZoneForm('Europe/Paris'); | 8 | * list($htmlform, $js) = generateTimeZoneForm('Europe/Paris'); |
9 | * | 9 | * |
10 | * @param string $preselected_timezone preselected timezone (optional) | 10 | * @param string $preselectedTimezone preselected timezone (optional) |
11 | * | 11 | * |
12 | * @return an array containing the generated HTML form and Javascript code | 12 | * @return array containing the generated HTML form and Javascript code |
13 | **/ | 13 | **/ |
14 | function generateTimeZoneForm($preselectedTimezone='') | 14 | function generateTimeZoneForm($preselectedTimezone='') |
15 | { | 15 | { |
@@ -27,10 +27,6 @@ function generateTimeZoneForm($preselectedTimezone='') | |||
27 | $pcity = substr($preselectedTimezone, $spos+1); | 27 | $pcity = substr($preselectedTimezone, $spos+1); |
28 | } | 28 | } |
29 | 29 | ||
30 | // Display config form: | ||
31 | $timezoneForm = ''; | ||
32 | $timezoneJs = ''; | ||
33 | |||
34 | // The list is in the form 'Europe/Paris', 'America/Argentina/Buenos_Aires' | 30 | // The list is in the form 'Europe/Paris', 'America/Argentina/Buenos_Aires' |
35 | // We split the list in continents/cities. | 31 | // We split the list in continents/cities. |
36 | $continents = array(); | 32 | $continents = array(); |
@@ -97,7 +93,7 @@ function generateTimeZoneForm($preselectedTimezone='') | |||
97 | * @param string $continent the timezone continent | 93 | * @param string $continent the timezone continent |
98 | * @param string $city the timezone city | 94 | * @param string $city the timezone city |
99 | * | 95 | * |
100 | * @return whether continent/city is a valid timezone | 96 | * @return bool whether continent/city is a valid timezone |
101 | */ | 97 | */ |
102 | function isTimeZoneValid($continent, $city) | 98 | function isTimeZoneValid($continent, $city) |
103 | { | 99 | { |
diff --git a/application/Updater.php b/application/Updater.php index 58c13c07..555d4c25 100644 --- a/application/Updater.php +++ b/application/Updater.php | |||
@@ -13,14 +13,14 @@ class Updater | |||
13 | protected $doneUpdates; | 13 | protected $doneUpdates; |
14 | 14 | ||
15 | /** | 15 | /** |
16 | * @var array Shaarli's configuration array. | 16 | * @var LinkDB instance. |
17 | */ | 17 | */ |
18 | protected $config; | 18 | protected $linkDB; |
19 | 19 | ||
20 | /** | 20 | /** |
21 | * @var LinkDB instance. | 21 | * @var ConfigManager $conf Configuration Manager instance. |
22 | */ | 22 | */ |
23 | protected $linkDB; | 23 | protected $conf; |
24 | 24 | ||
25 | /** | 25 | /** |
26 | * @var bool True if the user is logged in, false otherwise. | 26 | * @var bool True if the user is logged in, false otherwise. |
@@ -35,16 +35,16 @@ class Updater | |||
35 | /** | 35 | /** |
36 | * Object constructor. | 36 | * Object constructor. |
37 | * | 37 | * |
38 | * @param array $doneUpdates Updates which are already done. | 38 | * @param array $doneUpdates Updates which are already done. |
39 | * @param array $config Shaarli's configuration array. | 39 | * @param LinkDB $linkDB LinkDB instance. |
40 | * @param LinkDB $linkDB LinkDB instance. | 40 | * @param ConfigManager $conf Configuration Manager instance. |
41 | * @param boolean $isLoggedIn True if the user is logged in. | 41 | * @param boolean $isLoggedIn True if the user is logged in. |
42 | */ | 42 | */ |
43 | public function __construct($doneUpdates, $config, $linkDB, $isLoggedIn) | 43 | public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) |
44 | { | 44 | { |
45 | $this->doneUpdates = $doneUpdates; | 45 | $this->doneUpdates = $doneUpdates; |
46 | $this->config = $config; | ||
47 | $this->linkDB = $linkDB; | 46 | $this->linkDB = $linkDB; |
47 | $this->conf = $conf; | ||
48 | $this->isLoggedIn = $isLoggedIn; | 48 | $this->isLoggedIn = $isLoggedIn; |
49 | 49 | ||
50 | // Retrieve all update methods. | 50 | // Retrieve all update methods. |
@@ -114,19 +114,19 @@ class Updater | |||
114 | */ | 114 | */ |
115 | public function updateMethodMergeDeprecatedConfigFile() | 115 | public function updateMethodMergeDeprecatedConfigFile() |
116 | { | 116 | { |
117 | $config_file = $this->config['config']['CONFIG_FILE']; | 117 | if (is_file($this->conf->get('resource.data_dir') . '/options.php')) { |
118 | 118 | include $this->conf->get('resource.data_dir') . '/options.php'; | |
119 | if (is_file($this->config['config']['DATADIR'].'/options.php')) { | ||
120 | include $this->config['config']['DATADIR'].'/options.php'; | ||
121 | 119 | ||
122 | // Load GLOBALS into config | 120 | // Load GLOBALS into config |
121 | $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS); | ||
122 | $allowedKeys[] = 'config'; | ||
123 | foreach ($GLOBALS as $key => $value) { | 123 | foreach ($GLOBALS as $key => $value) { |
124 | $this->config[$key] = $value; | 124 | if (in_array($key, $allowedKeys)) { |
125 | $this->conf->set($key, $value); | ||
126 | } | ||
125 | } | 127 | } |
126 | $this->config['config']['CONFIG_FILE'] = $config_file; | 128 | $this->conf->write($this->isLoggedIn); |
127 | writeConfig($this->config, $this->isLoggedIn); | 129 | unlink($this->conf->get('resource.data_dir').'/options.php'); |
128 | |||
129 | unlink($this->config['config']['DATADIR'].'/options.php'); | ||
130 | } | 130 | } |
131 | 131 | ||
132 | return true; | 132 | return true; |
@@ -138,12 +138,144 @@ class Updater | |||
138 | public function updateMethodRenameDashTags() | 138 | public function updateMethodRenameDashTags() |
139 | { | 139 | { |
140 | $linklist = $this->linkDB->filterSearch(); | 140 | $linklist = $this->linkDB->filterSearch(); |
141 | foreach ($linklist as $link) { | 141 | foreach ($linklist as $key => $link) { |
142 | $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']); | 142 | $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']); |
143 | $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true))); | 143 | $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true))); |
144 | $this->linkDB[$link['linkdate']] = $link; | 144 | $this->linkDB[$key] = $link; |
145 | } | 145 | } |
146 | $this->linkDB->savedb($this->config['config']['PAGECACHE']); | 146 | $this->linkDB->save($this->conf->get('resource.page_cache')); |
147 | return true; | ||
148 | } | ||
149 | |||
150 | /** | ||
151 | * Move old configuration in PHP to the new config system in JSON format. | ||
152 | * | ||
153 | * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'. | ||
154 | * It will also convert legacy setting keys to the new ones. | ||
155 | */ | ||
156 | public function updateMethodConfigToJson() | ||
157 | { | ||
158 | // JSON config already exists, nothing to do. | ||
159 | if ($this->conf->getConfigIO() instanceof ConfigJson) { | ||
160 | return true; | ||
161 | } | ||
162 | |||
163 | $configPhp = new ConfigPhp(); | ||
164 | $configJson = new ConfigJson(); | ||
165 | $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php'); | ||
166 | rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php'); | ||
167 | $this->conf->setConfigIO($configJson); | ||
168 | $this->conf->reload(); | ||
169 | |||
170 | $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING); | ||
171 | foreach (ConfigPhp::$ROOT_KEYS as $key) { | ||
172 | $this->conf->set($legacyMap[$key], $oldConfig[$key]); | ||
173 | } | ||
174 | |||
175 | // Set sub config keys (config and plugins) | ||
176 | $subConfig = array('config', 'plugins'); | ||
177 | foreach ($subConfig as $sub) { | ||
178 | foreach ($oldConfig[$sub] as $key => $value) { | ||
179 | if (isset($legacyMap[$sub .'.'. $key])) { | ||
180 | $configKey = $legacyMap[$sub .'.'. $key]; | ||
181 | } else { | ||
182 | $configKey = $sub .'.'. $key; | ||
183 | } | ||
184 | $this->conf->set($configKey, $value); | ||
185 | } | ||
186 | } | ||
187 | |||
188 | try{ | ||
189 | $this->conf->write($this->isLoggedIn); | ||
190 | return true; | ||
191 | } catch (IOException $e) { | ||
192 | error_log($e->getMessage()); | ||
193 | return false; | ||
194 | } | ||
195 | } | ||
196 | |||
197 | /** | ||
198 | * Escape settings which have been manually escaped in every request in previous versions: | ||
199 | * - general.title | ||
200 | * - general.header_link | ||
201 | * - redirector.url | ||
202 | * | ||
203 | * @return bool true if the update is successful, false otherwise. | ||
204 | */ | ||
205 | public function updateMethodEscapeUnescapedConfig() | ||
206 | { | ||
207 | try { | ||
208 | $this->conf->set('general.title', escape($this->conf->get('general.title'))); | ||
209 | $this->conf->set('general.header_link', escape($this->conf->get('general.header_link'))); | ||
210 | $this->conf->set('redirector.url', escape($this->conf->get('redirector.url'))); | ||
211 | $this->conf->write($this->isLoggedIn); | ||
212 | } catch (Exception $e) { | ||
213 | error_log($e->getMessage()); | ||
214 | return false; | ||
215 | } | ||
216 | return true; | ||
217 | } | ||
218 | |||
219 | /** | ||
220 | * Update the database to use the new ID system, which replaces linkdate primary keys. | ||
221 | * Also, creation and update dates are now DateTime objects (done by LinkDB). | ||
222 | * | ||
223 | * Since this update is very sensitve (changing the whole database), the datastore will be | ||
224 | * automatically backed up into the file datastore.<datetime>.php. | ||
225 | * | ||
226 | * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash), | ||
227 | * which will be saved by this method. | ||
228 | * | ||
229 | * @return bool true if the update is successful, false otherwise. | ||
230 | */ | ||
231 | public function updateMethodDatastoreIds() | ||
232 | { | ||
233 | // up to date database | ||
234 | if (isset($this->linkDB[0])) { | ||
235 | return true; | ||
236 | } | ||
237 | |||
238 | $save = $this->conf->get('resource.data_dir') .'/datastore.'. date('YmdHis') .'.php'; | ||
239 | copy($this->conf->get('resource.datastore'), $save); | ||
240 | |||
241 | $links = array(); | ||
242 | foreach ($this->linkDB as $offset => $value) { | ||
243 | $links[] = $value; | ||
244 | unset($this->linkDB[$offset]); | ||
245 | } | ||
246 | $links = array_reverse($links); | ||
247 | $cpt = 0; | ||
248 | foreach ($links as $l) { | ||
249 | unset($l['linkdate']); | ||
250 | $l['id'] = $cpt; | ||
251 | $this->linkDB[$cpt++] = $l; | ||
252 | } | ||
253 | |||
254 | $this->linkDB->save($this->conf->get('resource.page_cache')); | ||
255 | $this->linkDB->reorder(); | ||
256 | |||
257 | return true; | ||
258 | } | ||
259 | |||
260 | /** | ||
261 | * * `markdown_escape` is a new setting, set to true as default. | ||
262 | * | ||
263 | * If the markdown plugin was already enabled, escaping is disabled to avoid | ||
264 | * breaking existing entries. | ||
265 | */ | ||
266 | public function updateMethodEscapeMarkdown() | ||
267 | { | ||
268 | if ($this->conf->exists('security.markdown_escape')) { | ||
269 | return true; | ||
270 | } | ||
271 | |||
272 | if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) { | ||
273 | $this->conf->set('security.markdown_escape', false); | ||
274 | } else { | ||
275 | $this->conf->set('security.markdown_escape', true); | ||
276 | } | ||
277 | $this->conf->write($this->isLoggedIn); | ||
278 | |||
147 | return true; | 279 | return true; |
148 | } | 280 | } |
149 | } | 281 | } |
@@ -203,7 +335,6 @@ class UpdaterException extends Exception | |||
203 | } | 335 | } |
204 | } | 336 | } |
205 | 337 | ||
206 | |||
207 | /** | 338 | /** |
208 | * Read the updates file, and return already done updates. | 339 | * Read the updates file, and return already done updates. |
209 | * | 340 | * |
diff --git a/application/Url.php b/application/Url.php index 77447c8d..c5c7dd18 100644 --- a/application/Url.php +++ b/application/Url.php | |||
@@ -62,21 +62,7 @@ function add_trailing_slash($url) | |||
62 | { | 62 | { |
63 | return $url . (!endsWith($url, '/') ? '/' : ''); | 63 | return $url . (!endsWith($url, '/') ? '/' : ''); |
64 | } | 64 | } |
65 | /** | ||
66 | * Converts an URL with an IDN host to a ASCII one. | ||
67 | * | ||
68 | * @param string $url Input URL. | ||
69 | * | ||
70 | * @return string converted URL. | ||
71 | */ | ||
72 | function url_with_idn_to_ascii($url) | ||
73 | { | ||
74 | $parts = parse_url($url); | ||
75 | $parts['host'] = idn_to_ascii($parts['host']); | ||
76 | 65 | ||
77 | $httpUrl = new \http\Url($parts); | ||
78 | return $httpUrl->toString(); | ||
79 | } | ||
80 | /** | 66 | /** |
81 | * URL representation and cleanup utilities | 67 | * URL representation and cleanup utilities |
82 | * | 68 | * |
@@ -99,6 +85,7 @@ class Url | |||
99 | 'action_type_map=', | 85 | 'action_type_map=', |
100 | 'fb_', | 86 | 'fb_', |
101 | 'fb=', | 87 | 'fb=', |
88 | 'PHPSESSID=', | ||
102 | 89 | ||
103 | // Scoop.it | 90 | // Scoop.it |
104 | '__scoop', | 91 | '__scoop', |
diff --git a/application/Utils.php b/application/Utils.php index da521cce..0a5b476e 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -31,7 +31,15 @@ function logm($logFile, $clientIp, $message) | |||
31 | * - are NOT cryptographically secure (they CAN be forged) | 31 | * - are NOT cryptographically secure (they CAN be forged) |
32 | * | 32 | * |
33 | * In Shaarli, they are used as a tinyurl-like link to individual entries, | 33 | * In Shaarli, they are used as a tinyurl-like link to individual entries, |
34 | * e.g. smallHash('20111006_131924') --> yZH23w | 34 | * built once with the combination of the date and item ID. |
35 | * e.g. smallHash('20111006_131924' . 142) --> eaWxtQ | ||
36 | * | ||
37 | * @warning before v0.8.1, smallhashes were built only with the date, | ||
38 | * and their value has been preserved. | ||
39 | * | ||
40 | * @param string $text Create a hash from this text. | ||
41 | * | ||
42 | * @return string generated small hash. | ||
35 | */ | 43 | */ |
36 | function smallHash($text) | 44 | function smallHash($text) |
37 | { | 45 | { |
@@ -106,7 +114,9 @@ function unescape($str) | |||
106 | } | 114 | } |
107 | 115 | ||
108 | /** | 116 | /** |
109 | * Link sanitization before templating | 117 | * Sanitize link before rendering. |
118 | * | ||
119 | * @param array $link Link to escape. | ||
110 | */ | 120 | */ |
111 | function sanitizeLink(&$link) | 121 | function sanitizeLink(&$link) |
112 | { | 122 | { |
@@ -198,59 +208,6 @@ function is_session_id_valid($sessionId) | |||
198 | } | 208 | } |
199 | 209 | ||
200 | /** | 210 | /** |
201 | * In a string, converts URLs to clickable links. | ||
202 | * | ||
203 | * @param string $text input string. | ||
204 | * @param string $redirector if a redirector is set, use it to gerenate links. | ||
205 | * | ||
206 | * @return string returns $text with all links converted to HTML links. | ||
207 | * | ||
208 | * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 | ||
209 | */ | ||
210 | function text2clickable($text, $redirector) | ||
211 | { | ||
212 | $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si'; | ||
213 | |||
214 | if (empty($redirector)) { | ||
215 | return preg_replace($regex, '<a href="$1">$1</a>', $text); | ||
216 | } | ||
217 | // Redirector is set, urlencode the final URL. | ||
218 | return preg_replace_callback( | ||
219 | $regex, | ||
220 | function ($matches) use ($redirector) { | ||
221 | return '<a href="' . $redirector . urlencode($matches[1]) .'">'. $matches[1] .'</a>'; | ||
222 | }, | ||
223 | $text | ||
224 | ); | ||
225 | } | ||
226 | |||
227 | /** | ||
228 | * This function inserts where relevant so that multiple spaces are properly displayed in HTML | ||
229 | * even in the absence of <pre> (This is used in description to keep text formatting). | ||
230 | * | ||
231 | * @param string $text input text. | ||
232 | * | ||
233 | * @return string formatted text. | ||
234 | */ | ||
235 | function space2nbsp($text) | ||
236 | { | ||
237 | return preg_replace('/(^| ) /m', '$1 ', $text); | ||
238 | } | ||
239 | |||
240 | /** | ||
241 | * Format Shaarli's description | ||
242 | * TODO: Move me to ApplicationUtils when it's ready. | ||
243 | * | ||
244 | * @param string $description shaare's description. | ||
245 | * @param string $redirector if a redirector is set, use it to gerenate links. | ||
246 | * | ||
247 | * @return string formatted description. | ||
248 | */ | ||
249 | function format_description($description, $redirector = false) { | ||
250 | return nl2br(space2nbsp(text2clickable($description, $redirector))); | ||
251 | } | ||
252 | |||
253 | /** | ||
254 | * Sniff browser language to set the locale automatically. | 211 | * Sniff browser language to set the locale automatically. |
255 | * Note that is may not work on your server if the corresponding locale is not installed. | 212 | * Note that is may not work on your server if the corresponding locale is not installed. |
256 | * | 213 | * |
@@ -273,4 +230,4 @@ function autoLocale($headerLocale) | |||
273 | } | 230 | } |
274 | } | 231 | } |
275 | setlocale(LC_ALL, $attempts); | 232 | setlocale(LC_ALL, $attempts); |
276 | } \ No newline at end of file | 233 | } |
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 | */ | ||
8 | interface 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 | */ | ||
8 | class 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... | ||
4 | require_once 'ConfigIO.php'; | ||
5 | require_once 'ConfigJson.php'; | ||
6 | require_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 | */ | ||
16 | class 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 | */ | ||
366 | class 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 | */ | ||
385 | class 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 | */ | ||
9 | class 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 | */ | ||
17 | function 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 | */ | ||
65 | function 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 | */ | ||
94 | function 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 | */ | ||
115 | class 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 | } | ||