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