aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/HttpUtils.php160
-rw-r--r--application/Languages.php21
-rw-r--r--application/LinkDB.php4
-rw-r--r--application/NetscapeBookmarkUtils.php141
-rw-r--r--application/PageBuilder.php1
-rw-r--r--application/PluginManager.php2
-rw-r--r--application/Router.php2
-rw-r--r--application/Updater.php4
8 files changed, 319 insertions, 16 deletions
diff --git a/application/HttpUtils.php b/application/HttpUtils.php
index 2e0792f9..27a39d3d 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
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 929a6b0f..d80434bf 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -291,7 +291,7 @@ You use the community supported version of the original Shaarli project, by Seba
291 291
292 // Remove private tags if the user is not logged in. 292 // Remove private tags if the user is not logged in.
293 if (! $this->_loggedIn) { 293 if (! $this->_loggedIn) {
294 $link['tags'] = preg_replace('/(^| )\.[^($| )]+/', '', $link['tags']); 294 $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
295 } 295 }
296 296
297 // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). 297 // Do not use the redirector for internal links (Shaarli note URL starting with a '?').
@@ -442,7 +442,7 @@ You use the community supported version of the original Shaarli project, by Seba
442 $tags = array(); 442 $tags = array();
443 $caseMapping = array(); 443 $caseMapping = array();
444 foreach ($this->_links as $link) { 444 foreach ($this->_links as $link) {
445 foreach (explode(' ', $link['tags']) as $tag) { 445 foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
446 if (empty($tag)) { 446 if (empty($tag)) {
447 continue; 447 continue;
448 } 448 }
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
index fdbb0ad7..c3181254 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/NetscapeBookmarkUtils.php
@@ -51,4 +51,145 @@ 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 $file 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 'linkdate'=> '',
151 'tags' => $bkm['tags']
152 );
153
154 $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
155
156 if ($existingLink !== false) {
157 if ($overwrite === false) {
158 // Do not overwrite an existing link
159 $skipCount++;
160 continue;
161 }
162
163 // Overwrite an existing link, keep its date
164 $newLink['linkdate'] = $existingLink['linkdate'];
165 $linkDb[$existingLink['linkdate']] = $newLink;
166 $importCount++;
167 $overwriteCount++;
168 continue;
169 }
170
171 // Add a new link
172 $newLinkDate = new DateTime('@'.strval($bkm['time']));
173 while (!empty($linkDb[$newLinkDate->format(LinkDB::LINK_DATE_FORMAT)])) {
174 // Ensure the date/time is not already used
175 // - this hack is necessary as the date/time acts as a primary key
176 // - apply 1 second increments until an unused index is found
177 // See https://github.com/shaarli/Shaarli/issues/351
178 $newLinkDate->add(new DateInterval('PT1S'));
179 }
180 $linkDbDate = $newLinkDate->format(LinkDB::LINK_DATE_FORMAT);
181 $newLink['linkdate'] = $linkDbDate;
182 $linkDb[$linkDbDate] = $newLink;
183 $importCount++;
184 }
185
186 $linkDb->savedb($pagecache);
187 return self::importStatus(
188 $filename,
189 $filesize,
190 $importCount,
191 $overwriteCount,
192 $skipCount
193 );
194 }
54} 195}
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index 1ca0260a..42932f32 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -80,6 +80,7 @@ class PageBuilder
80 if (!empty($GLOBALS['plugin_errors'])) { 80 if (!empty($GLOBALS['plugin_errors'])) {
81 $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']); 81 $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']);
82 } 82 }
83 $this->tpl->assign('token', getToken($this->conf));
83 // To be removed with a proper theme configuration. 84 // To be removed with a proper theme configuration.
84 $this->tpl->assign('conf', $this->conf); 85 $this->tpl->assign('conf', $this->conf);
85 } 86 }
diff --git a/application/PluginManager.php b/application/PluginManager.php
index 07bc1da9..1e132a7f 100644
--- a/application/PluginManager.php
+++ b/application/PluginManager.php
@@ -214,4 +214,4 @@ class PluginFileNotFoundException extends Exception
214 { 214 {
215 $this->message = 'Plugin "'. $pluginName .'" files not found.'; 215 $this->message = 'Plugin "'. $pluginName .'" files not found.';
216 } 216 }
217} \ No newline at end of file 217}
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/Updater.php b/application/Updater.php
index fd45d17f..b6cbc56c 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -198,11 +198,11 @@ class Updater
198 * Escape settings which have been manually escaped in every request in previous versions: 198 * Escape settings which have been manually escaped in every request in previous versions:
199 * - general.title 199 * - general.title
200 * - general.header_link 200 * - general.header_link
201 * - extras.redirector 201 * - redirector.url
202 * 202 *
203 * @return bool true if the update is successful, false otherwise. 203 * @return bool true if the update is successful, false otherwise.
204 */ 204 */
205 public function escapeUnescapedConfig() 205 public function updateMethodEscapeUnescapedConfig()
206 { 206 {
207 try { 207 try {
208 $this->conf->set('general.title', escape($this->conf->get('general.title'))); 208 $this->conf->set('general.title', escape($this->conf->get('general.title')));