diff options
48 files changed, 1711 insertions, 555 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..aaf6a39e --- /dev/null +++ b/.gitattributes | |||
@@ -0,0 +1,31 @@ | |||
1 | # Set default behavior | ||
2 | * text=auto eol=lf | ||
3 | |||
4 | # Ensure sources are processed | ||
5 | *.conf text | ||
6 | *.css text | ||
7 | *.html text diff=html | ||
8 | *.js text | ||
9 | *.md text | ||
10 | *.php text diff=php | ||
11 | Dockerfile text | ||
12 | |||
13 | # Do not alter images nor minified scripts | ||
14 | *.ico binary | ||
15 | *.jpg binary | ||
16 | *.png binary | ||
17 | *.min.css binary | ||
18 | *.min.js binary | ||
19 | |||
20 | # Exclude from Git archives | ||
21 | .gitattributes export-ignore | ||
22 | .gitignore export-ignore | ||
23 | .travis.yml export-ignore | ||
24 | composer.json export-ignore | ||
25 | doc/**/*.json export-ignore | ||
26 | doc/**/*.md export-ignore | ||
27 | docker/ export-ignore | ||
28 | Doxyfile export-ignore | ||
29 | Makefile export-ignore | ||
30 | phpunit.xml export-ignore | ||
31 | tests/ export-ignore | ||
diff --git a/.travis.yml b/.travis.yml index a3038c13..7408b2e2 100644 --- a/.travis.yml +++ b/.travis.yml | |||
@@ -11,4 +11,5 @@ install: | |||
11 | - composer install | 11 | - composer install |
12 | script: | 12 | script: |
13 | - make clean | 13 | - make clean |
14 | - make check_permissions | ||
14 | - make test | 15 | - make test |
@@ -16,7 +16,7 @@ BIN = vendor/bin | |||
16 | PHP_SOURCE = index.php application tests plugins | 16 | PHP_SOURCE = index.php application tests plugins |
17 | PHP_COMMA_SOURCE = index.php,application,tests,plugins | 17 | PHP_COMMA_SOURCE = index.php,application,tests,plugins |
18 | 18 | ||
19 | all: static_analysis_summary test | 19 | all: static_analysis_summary check_permissions test |
20 | 20 | ||
21 | ## | 21 | ## |
22 | # Concise status of the project | 22 | # Concise status of the project |
@@ -99,6 +99,20 @@ mess_detector_summary: mess_title | |||
99 | done; | 99 | done; |
100 | 100 | ||
101 | ## | 101 | ## |
102 | # Checks source file & script permissions | ||
103 | ## | ||
104 | check_permissions: | ||
105 | @echo "----------------------" | ||
106 | @echo "Check file permissions" | ||
107 | @echo "----------------------" | ||
108 | @for file in `git ls-files`; do \ | ||
109 | if [ -x $$file ]; then \ | ||
110 | errors=true; \ | ||
111 | echo "$${file} is executable"; \ | ||
112 | fi \ | ||
113 | done; [ -z $$errors ] || false | ||
114 | |||
115 | ## | ||
102 | # PHPUnit | 116 | # PHPUnit |
103 | # Runs unitary and functional tests | 117 | # Runs unitary and functional tests |
104 | # Generates an HTML coverage report if Xdebug is enabled | 118 | # Generates an HTML coverage report if Xdebug is enabled |
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 274331e1..978fc9da 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php | |||
@@ -19,7 +19,7 @@ class ApplicationUtils | |||
19 | */ | 19 | */ |
20 | public static function getLatestGitVersionCode($url, $timeout=2) | 20 | public static function getLatestGitVersionCode($url, $timeout=2) |
21 | { | 21 | { |
22 | list($headers, $data) = get_http_url($url, $timeout); | 22 | list($headers, $data) = get_http_response($url, $timeout); |
23 | 23 | ||
24 | if (strpos($headers[0], '200 OK') === false) { | 24 | if (strpos($headers[0], '200 OK') === false) { |
25 | error_log('Failed to retrieve ' . $url); | 25 | error_log('Failed to retrieve ' . $url); |
diff --git a/application/HttpUtils.php b/application/HttpUtils.php index 499220c5..e2c1cb47 100644 --- a/application/HttpUtils.php +++ b/application/HttpUtils.php | |||
@@ -13,7 +13,7 @@ | |||
13 | * [1] = URL content (downloaded data) | 13 | * [1] = URL content (downloaded data) |
14 | * | 14 | * |
15 | * Example: | 15 | * Example: |
16 | * list($headers, $data) = get_http_url('http://sebauvage.net/'); | 16 | * list($headers, $data) = get_http_response('http://sebauvage.net/'); |
17 | * if (strpos($headers[0], '200 OK') !== false) { | 17 | * if (strpos($headers[0], '200 OK') !== false) { |
18 | * echo 'Data type: '.htmlspecialchars($headers['Content-Type']); | 18 | * echo 'Data type: '.htmlspecialchars($headers['Content-Type']); |
19 | * } else { | 19 | * } else { |
@@ -24,31 +24,66 @@ | |||
24 | * @see http://php.net/manual/en/function.stream-context-create.php | 24 | * @see http://php.net/manual/en/function.stream-context-create.php |
25 | * @see http://php.net/manual/en/function.get-headers.php | 25 | * @see http://php.net/manual/en/function.get-headers.php |
26 | */ | 26 | */ |
27 | function get_http_url($url, $timeout = 30, $maxBytes = 4194304) | 27 | function get_http_response($url, $timeout = 30, $maxBytes = 4194304) |
28 | { | 28 | { |
29 | $urlObj = new Url($url); | ||
30 | if (! filter_var($url, FILTER_VALIDATE_URL) || ! $urlObj->isHttp()) { | ||
31 | return array(array(0 => 'Invalid HTTP Url'), false); | ||
32 | } | ||
33 | |||
29 | $options = array( | 34 | $options = array( |
30 | 'http' => array( | 35 | 'http' => array( |
31 | 'method' => 'GET', | 36 | 'method' => 'GET', |
32 | 'timeout' => $timeout, | 37 | 'timeout' => $timeout, |
33 | 'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0)' | 38 | 'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0)' |
34 | .' Gecko/20100101 Firefox/23.0' | 39 | .' Gecko/20100101 Firefox/23.0', |
40 | 'request_fulluri' => true, | ||
35 | ) | 41 | ) |
36 | ); | 42 | ); |
37 | 43 | ||
38 | $context = stream_context_create($options); | 44 | $context = stream_context_create($options); |
45 | stream_context_set_default($options); | ||
46 | |||
47 | list($headers, $finalUrl) = get_redirected_headers($urlObj->cleanup()); | ||
48 | if (! $headers || strpos($headers[0], '200 OK') === false) { | ||
49 | return array($headers, false); | ||
50 | } | ||
39 | 51 | ||
40 | try { | 52 | try { |
41 | // TODO: catch Exception in calling code (thumbnailer) | 53 | // TODO: catch Exception in calling code (thumbnailer) |
42 | $content = file_get_contents($url, false, $context, -1, $maxBytes); | 54 | $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes); |
43 | } catch (Exception $exc) { | 55 | } catch (Exception $exc) { |
44 | return array(array(0 => 'HTTP Error'), $exc->getMessage()); | 56 | return array(array(0 => 'HTTP Error'), $exc->getMessage()); |
45 | } | 57 | } |
46 | 58 | ||
47 | if (!$content) { | 59 | return array($headers, $content); |
48 | return array(array(0 => 'HTTP Error'), ''); | 60 | } |
61 | |||
62 | /** | ||
63 | * Retrieve HTTP headers, following n redirections (temporary and permanent). | ||
64 | * | ||
65 | * @param string $url initial URL to reach. | ||
66 | * @param int $redirectionLimit max redirection follow.. | ||
67 | * | ||
68 | * @return array | ||
69 | */ | ||
70 | function get_redirected_headers($url, $redirectionLimit = 3) | ||
71 | { | ||
72 | $headers = get_headers($url, 1); | ||
73 | |||
74 | // Headers found, redirection found, and limit not reached. | ||
75 | if ($redirectionLimit-- > 0 | ||
76 | && !empty($headers) | ||
77 | && (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false) | ||
78 | && !empty($headers['Location'])) { | ||
79 | |||
80 | $redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location']; | ||
81 | if ($redirection != $url) { | ||
82 | return get_redirected_headers($redirection, $redirectionLimit); | ||
83 | } | ||
49 | } | 84 | } |
50 | 85 | ||
51 | return array(get_headers($url, 1), $content); | 86 | return array($headers, $url); |
52 | } | 87 | } |
53 | 88 | ||
54 | /** | 89 | /** |
diff --git a/application/LinkDB.php b/application/LinkDB.php index f771ac8b..19ca6435 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -17,8 +17,10 @@ | |||
17 | * - private: Is this link private? 0=no, other value=yes | 17 | * - private: Is this link private? 0=no, other value=yes |
18 | * - tags: tags attached to this entry (separated by spaces) | 18 | * - tags: tags attached to this entry (separated by spaces) |
19 | * - title Title of the link | 19 | * - title Title of the link |
20 | * - url URL of the link. Can be absolute or relative. | 20 | * - url URL of the link. Used for displayable links (no redirector, relative, etc.). |
21 | * Can be absolute or relative. | ||
21 | * Relative URLs are permalinks (e.g.'?m-ukcw') | 22 | * Relative URLs are permalinks (e.g.'?m-ukcw') |
23 | * - real_url Absolute processed URL. | ||
22 | * | 24 | * |
23 | * Implements 3 interfaces: | 25 | * Implements 3 interfaces: |
24 | * - ArrayAccess: behaves like an associative array; | 26 | * - ArrayAccess: behaves like an associative array; |
@@ -332,114 +334,20 @@ You use the community supported version of the original Shaarli project, by Seba | |||
332 | } | 334 | } |
333 | 335 | ||
334 | /** | 336 | /** |
335 | * Returns the list of links corresponding to a full-text search | 337 | * Filter links. |
336 | * | 338 | * |
337 | * Searches: | 339 | * @param string $type Type of filter. |
338 | * - in the URLs, title and description; | 340 | * @param mixed $request Search request, string or array. |
339 | * - are case-insensitive. | 341 | * @param bool $casesensitive Optional: Perform case sensitive filter |
342 | * @param bool $privateonly Optional: Returns private links only if true. | ||
340 | * | 343 | * |
341 | * Example: | 344 | * @return array filtered links |
342 | * print_r($mydb->filterFulltext('hollandais')); | ||
343 | * | ||
344 | * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8') | ||
345 | * - allows to perform searches on Unicode text | ||
346 | * - see https://github.com/shaarli/Shaarli/issues/75 for examples | ||
347 | */ | ||
348 | public function filterFulltext($searchterms) | ||
349 | { | ||
350 | // FIXME: explode(' ',$searchterms) and perform a AND search. | ||
351 | // FIXME: accept double-quotes to search for a string "as is"? | ||
352 | $filtered = array(); | ||
353 | $search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8'); | ||
354 | $keys = array('title', 'description', 'url', 'tags'); | ||
355 | |||
356 | foreach ($this->_links as $link) { | ||
357 | $found = false; | ||
358 | |||
359 | foreach ($keys as $key) { | ||
360 | if (strpos(mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'), | ||
361 | $search) !== false) { | ||
362 | $found = true; | ||
363 | } | ||
364 | } | ||
365 | |||
366 | if ($found) { | ||
367 | $filtered[$link['linkdate']] = $link; | ||
368 | } | ||
369 | } | ||
370 | krsort($filtered); | ||
371 | return $filtered; | ||
372 | } | ||
373 | |||
374 | /** | ||
375 | * Returns the list of links associated with a given list of tags | ||
376 | * | ||
377 | * You can specify one or more tags, separated by space or a comma, e.g. | ||
378 | * print_r($mydb->filterTags('linux programming')); | ||
379 | */ | 345 | */ |
380 | public function filterTags($tags, $casesensitive=false) | 346 | public function filter($type, $request, $casesensitive = false, $privateonly = false) |
381 | { | 347 | { |
382 | // Same as above, we use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) | 348 | $linkFilter = new LinkFilter($this->_links); |
383 | // FIXME: is $casesensitive ever true? | 349 | $requestFilter = is_array($request) ? implode(' ', $request) : $request; |
384 | $t = str_replace( | 350 | return $linkFilter->filter($type, trim($requestFilter), $casesensitive, $privateonly); |
385 | ',', ' ', | ||
386 | ($casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8')) | ||
387 | ); | ||
388 | |||
389 | $searchtags = explode(' ', $t); | ||
390 | $filtered = array(); | ||
391 | |||
392 | foreach ($this->_links as $l) { | ||
393 | $linktags = explode( | ||
394 | ' ', | ||
395 | ($casesensitive ? $l['tags']:mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8')) | ||
396 | ); | ||
397 | |||
398 | if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) { | ||
399 | $filtered[$l['linkdate']] = $l; | ||
400 | } | ||
401 | } | ||
402 | krsort($filtered); | ||
403 | return $filtered; | ||
404 | } | ||
405 | |||
406 | |||
407 | /** | ||
408 | * Returns the list of articles for a given day, chronologically sorted | ||
409 | * | ||
410 | * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g. | ||
411 | * print_r($mydb->filterDay('20120125')); | ||
412 | */ | ||
413 | public function filterDay($day) | ||
414 | { | ||
415 | if (! checkDateFormat('Ymd', $day)) { | ||
416 | throw new Exception('Invalid date format'); | ||
417 | } | ||
418 | |||
419 | $filtered = array(); | ||
420 | foreach ($this->_links as $l) { | ||
421 | if (startsWith($l['linkdate'], $day)) { | ||
422 | $filtered[$l['linkdate']] = $l; | ||
423 | } | ||
424 | } | ||
425 | ksort($filtered); | ||
426 | return $filtered; | ||
427 | } | ||
428 | |||
429 | /** | ||
430 | * Returns the article corresponding to a smallHash | ||
431 | */ | ||
432 | public function filterSmallHash($smallHash) | ||
433 | { | ||
434 | $filtered = array(); | ||
435 | foreach ($this->_links as $l) { | ||
436 | if ($smallHash == smallHash($l['linkdate'])) { | ||
437 | // Yes, this is ugly and slow | ||
438 | $filtered[$l['linkdate']] = $l; | ||
439 | return $filtered; | ||
440 | } | ||
441 | } | ||
442 | return $filtered; | ||
443 | } | 351 | } |
444 | 352 | ||
445 | /** | 353 | /** |
diff --git a/application/LinkFilter.php b/application/LinkFilter.php new file mode 100644 index 00000000..cf647371 --- /dev/null +++ b/application/LinkFilter.php | |||
@@ -0,0 +1,259 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Class LinkFilter. | ||
5 | * | ||
6 | * Perform search and filter operation on link data list. | ||
7 | */ | ||
8 | class LinkFilter | ||
9 | { | ||
10 | /** | ||
11 | * @var string permalinks. | ||
12 | */ | ||
13 | public static $FILTER_HASH = 'permalink'; | ||
14 | |||
15 | /** | ||
16 | * @var string text search. | ||
17 | */ | ||
18 | public static $FILTER_TEXT = 'fulltext'; | ||
19 | |||
20 | /** | ||
21 | * @var string tag filter. | ||
22 | */ | ||
23 | public static $FILTER_TAG = 'tags'; | ||
24 | |||
25 | /** | ||
26 | * @var string filter by day. | ||
27 | */ | ||
28 | public static $FILTER_DAY = 'FILTER_DAY'; | ||
29 | |||
30 | /** | ||
31 | * @var array all available links. | ||
32 | */ | ||
33 | private $links; | ||
34 | |||
35 | /** | ||
36 | * @param array $links initialization. | ||
37 | */ | ||
38 | public function __construct($links) | ||
39 | { | ||
40 | $this->links = $links; | ||
41 | } | ||
42 | |||
43 | /** | ||
44 | * Filter links according to parameters. | ||
45 | * | ||
46 | * @param string $type Type of filter (eg. tags, permalink, etc.). | ||
47 | * @param string $request Filter content. | ||
48 | * @param bool $casesensitive Optional: Perform case sensitive filter if true. | ||
49 | * @param bool $privateonly Optional: Only returns private links if true. | ||
50 | * | ||
51 | * @return array filtered link list. | ||
52 | */ | ||
53 | public function filter($type, $request, $casesensitive = false, $privateonly = false) | ||
54 | { | ||
55 | switch($type) { | ||
56 | case self::$FILTER_HASH: | ||
57 | return $this->filterSmallHash($request); | ||
58 | break; | ||
59 | case self::$FILTER_TEXT: | ||
60 | return $this->filterFulltext($request, $privateonly); | ||
61 | break; | ||
62 | case self::$FILTER_TAG: | ||
63 | return $this->filterTags($request, $casesensitive, $privateonly); | ||
64 | break; | ||
65 | case self::$FILTER_DAY: | ||
66 | return $this->filterDay($request); | ||
67 | break; | ||
68 | default: | ||
69 | return $this->noFilter($privateonly); | ||
70 | } | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * Unknown filter, but handle private only. | ||
75 | * | ||
76 | * @param bool $privateonly returns private link only if true. | ||
77 | * | ||
78 | * @return array filtered links. | ||
79 | */ | ||
80 | private function noFilter($privateonly = false) | ||
81 | { | ||
82 | if (! $privateonly) { | ||
83 | krsort($this->links); | ||
84 | return $this->links; | ||
85 | } | ||
86 | |||
87 | $out = array(); | ||
88 | foreach ($this->links as $value) { | ||
89 | if ($value['private']) { | ||
90 | $out[$value['linkdate']] = $value; | ||
91 | } | ||
92 | } | ||
93 | |||
94 | krsort($out); | ||
95 | return $out; | ||
96 | } | ||
97 | |||
98 | /** | ||
99 | * Returns the shaare corresponding to a smallHash. | ||
100 | * | ||
101 | * @param string $smallHash permalink hash. | ||
102 | * | ||
103 | * @return array $filtered array containing permalink data. | ||
104 | */ | ||
105 | private function filterSmallHash($smallHash) | ||
106 | { | ||
107 | $filtered = array(); | ||
108 | foreach ($this->links as $l) { | ||
109 | if ($smallHash == smallHash($l['linkdate'])) { | ||
110 | // Yes, this is ugly and slow | ||
111 | $filtered[$l['linkdate']] = $l; | ||
112 | return $filtered; | ||
113 | } | ||
114 | } | ||
115 | return $filtered; | ||
116 | } | ||
117 | |||
118 | /** | ||
119 | * Returns the list of links corresponding to a full-text search | ||
120 | * | ||
121 | * Searches: | ||
122 | * - in the URLs, title and description; | ||
123 | * - are case-insensitive. | ||
124 | * | ||
125 | * Example: | ||
126 | * print_r($mydb->filterFulltext('hollandais')); | ||
127 | * | ||
128 | * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8') | ||
129 | * - allows to perform searches on Unicode text | ||
130 | * - see https://github.com/shaarli/Shaarli/issues/75 for examples | ||
131 | * | ||
132 | * @param string $searchterms search query. | ||
133 | * @param bool $privateonly return only private links if true. | ||
134 | * | ||
135 | * @return array search results. | ||
136 | */ | ||
137 | private function filterFulltext($searchterms, $privateonly = false) | ||
138 | { | ||
139 | // FIXME: explode(' ',$searchterms) and perform a AND search. | ||
140 | // FIXME: accept double-quotes to search for a string "as is"? | ||
141 | $filtered = array(); | ||
142 | $search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8'); | ||
143 | $explodedSearch = explode(' ', trim($search)); | ||
144 | $keys = array('title', 'description', 'url', 'tags'); | ||
145 | |||
146 | // Iterate over every stored link. | ||
147 | foreach ($this->links as $link) { | ||
148 | $found = false; | ||
149 | |||
150 | // ignore non private links when 'privatonly' is on. | ||
151 | if (! $link['private'] && $privateonly === true) { | ||
152 | continue; | ||
153 | } | ||
154 | |||
155 | // Iterate over searchable link fields. | ||
156 | foreach ($keys as $key) { | ||
157 | // Search full expression. | ||
158 | if (strpos( | ||
159 | mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'), | ||
160 | $search | ||
161 | ) !== false) { | ||
162 | $found = true; | ||
163 | } | ||
164 | |||
165 | if ($found) { | ||
166 | break; | ||
167 | } | ||
168 | } | ||
169 | |||
170 | if ($found) { | ||
171 | $filtered[$link['linkdate']] = $link; | ||
172 | } | ||
173 | } | ||
174 | |||
175 | krsort($filtered); | ||
176 | return $filtered; | ||
177 | } | ||
178 | |||
179 | /** | ||
180 | * Returns the list of links associated with a given list of tags | ||
181 | * | ||
182 | * You can specify one or more tags, separated by space or a comma, e.g. | ||
183 | * print_r($mydb->filterTags('linux programming')); | ||
184 | * | ||
185 | * @param string $tags list of tags separated by commas or blank spaces. | ||
186 | * @param bool $casesensitive ignore case if false. | ||
187 | * @param bool $privateonly returns private links only. | ||
188 | * | ||
189 | * @return array filtered links. | ||
190 | */ | ||
191 | public function filterTags($tags, $casesensitive = false, $privateonly = false) | ||
192 | { | ||
193 | $searchtags = $this->tagsStrToArray($tags, $casesensitive); | ||
194 | $filtered = array(); | ||
195 | |||
196 | foreach ($this->links as $l) { | ||
197 | // ignore non private links when 'privatonly' is on. | ||
198 | if (! $l['private'] && $privateonly === true) { | ||
199 | continue; | ||
200 | } | ||
201 | |||
202 | $linktags = $this->tagsStrToArray($l['tags'], $casesensitive); | ||
203 | |||
204 | if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) { | ||
205 | $filtered[$l['linkdate']] = $l; | ||
206 | } | ||
207 | } | ||
208 | krsort($filtered); | ||
209 | return $filtered; | ||
210 | } | ||
211 | |||
212 | /** | ||
213 | * Returns the list of articles for a given day, chronologically sorted | ||
214 | * | ||
215 | * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g. | ||
216 | * print_r($mydb->filterDay('20120125')); | ||
217 | * | ||
218 | * @param string $day day to filter. | ||
219 | * | ||
220 | * @return array all link matching given day. | ||
221 | * | ||
222 | * @throws Exception if date format is invalid. | ||
223 | */ | ||
224 | public function filterDay($day) | ||
225 | { | ||
226 | if (! checkDateFormat('Ymd', $day)) { | ||
227 | throw new Exception('Invalid date format'); | ||
228 | } | ||
229 | |||
230 | $filtered = array(); | ||
231 | foreach ($this->links as $l) { | ||
232 | if (startsWith($l['linkdate'], $day)) { | ||
233 | $filtered[$l['linkdate']] = $l; | ||
234 | } | ||
235 | } | ||
236 | ksort($filtered); | ||
237 | return $filtered; | ||
238 | } | ||
239 | |||
240 | /** | ||
241 | * Convert a list of tags (str) to an array. Also | ||
242 | * - handle case sensitivity. | ||
243 | * - accepts spaces commas as separator. | ||
244 | * - remove private tags for loggedout users. | ||
245 | * | ||
246 | * @param string $tags string containing a list of tags. | ||
247 | * @param bool $casesensitive will convert everything to lowercase if false. | ||
248 | * | ||
249 | * @return array filtered tags string. | ||
250 | */ | ||
251 | public function tagsStrToArray($tags, $casesensitive) | ||
252 | { | ||
253 | // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) | ||
254 | $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); | ||
255 | $tagsOut = str_replace(',', ' ', $tagsOut); | ||
256 | |||
257 | return explode(' ', trim($tagsOut)); | ||
258 | } | ||
259 | } | ||
diff --git a/application/LinkUtils.php b/application/LinkUtils.php new file mode 100644 index 00000000..26dd6b67 --- /dev/null +++ b/application/LinkUtils.php | |||
@@ -0,0 +1,79 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Extract title from an HTML document. | ||
5 | * | ||
6 | * @param string $html HTML content where to look for a title. | ||
7 | * | ||
8 | * @return bool|string Extracted title if found, false otherwise. | ||
9 | */ | ||
10 | function html_extract_title($html) | ||
11 | { | ||
12 | if (preg_match('!<title>(.*)</title>!is', $html, $matches)) { | ||
13 | return trim(str_replace("\n", ' ', $matches[1])); | ||
14 | } | ||
15 | return false; | ||
16 | } | ||
17 | |||
18 | /** | ||
19 | * Determine charset from downloaded page. | ||
20 | * Priority: | ||
21 | * 1. HTTP headers (Content type). | ||
22 | * 2. HTML content page (tag <meta charset>). | ||
23 | * 3. Use a default charset (default: UTF-8). | ||
24 | * | ||
25 | * @param array $headers HTTP headers array. | ||
26 | * @param string $htmlContent HTML content where to look for charset. | ||
27 | * @param string $defaultCharset Default charset to apply if other methods failed. | ||
28 | * | ||
29 | * @return string Determined charset. | ||
30 | */ | ||
31 | function get_charset($headers, $htmlContent, $defaultCharset = 'utf-8') | ||
32 | { | ||
33 | if ($charset = headers_extract_charset($headers)) { | ||
34 | return $charset; | ||
35 | } | ||
36 | |||
37 | if ($charset = html_extract_charset($htmlContent)) { | ||
38 | return $charset; | ||
39 | } | ||
40 | |||
41 | return $defaultCharset; | ||
42 | } | ||
43 | |||
44 | /** | ||
45 | * Extract charset from HTTP headers if it's defined. | ||
46 | * | ||
47 | * @param array $headers HTTP headers array. | ||
48 | * | ||
49 | * @return bool|string Charset string if found (lowercase), false otherwise. | ||
50 | */ | ||
51 | function headers_extract_charset($headers) | ||
52 | { | ||
53 | if (! empty($headers['Content-Type']) && strpos($headers['Content-Type'], 'charset=') !== false) { | ||
54 | preg_match('/charset="?([^; ]+)/i', $headers['Content-Type'], $match); | ||
55 | if (! empty($match[1])) { | ||
56 | return strtolower(trim($match[1])); | ||
57 | } | ||
58 | } | ||
59 | |||
60 | return false; | ||
61 | } | ||
62 | |||
63 | /** | ||
64 | * Extract charset HTML content (tag <meta charset>). | ||
65 | * | ||
66 | * @param string $html HTML content where to look for charset. | ||
67 | * | ||
68 | * @return bool|string Charset string if found, false otherwise. | ||
69 | */ | ||
70 | function html_extract_charset($html) | ||
71 | { | ||
72 | // Get encoding specified in HTML header. | ||
73 | preg_match('#<meta .*charset="?([^">/]+)"? */?>#Usi', $html, $enc); | ||
74 | if (!empty($enc[1])) { | ||
75 | return strtolower($enc[1]); | ||
76 | } | ||
77 | |||
78 | return false; | ||
79 | } | ||
diff --git a/application/Url.php b/application/Url.php index af43b457..a4ac2e73 100644 --- a/application/Url.php +++ b/application/Url.php | |||
@@ -52,6 +52,18 @@ function get_url_scheme($url) | |||
52 | } | 52 | } |
53 | 53 | ||
54 | /** | 54 | /** |
55 | * Adds a trailing slash at the end of URL if necessary. | ||
56 | * | ||
57 | * @param string $url URL to check/edit. | ||
58 | * | ||
59 | * @return string $url URL with a end trailing slash. | ||
60 | */ | ||
61 | function add_trailing_slash($url) | ||
62 | { | ||
63 | return $url . (!endsWith($url, '/') ? '/' : ''); | ||
64 | } | ||
65 | |||
66 | /** | ||
55 | * URL representation and cleanup utilities | 67 | * URL representation and cleanup utilities |
56 | * | 68 | * |
57 | * Form | 69 | * Form |
@@ -106,7 +118,7 @@ class Url | |||
106 | */ | 118 | */ |
107 | public function __construct($url) | 119 | public function __construct($url) |
108 | { | 120 | { |
109 | $this->parts = parse_url($url); | 121 | $this->parts = parse_url(trim($url)); |
110 | 122 | ||
111 | if (!empty($url) && empty($this->parts['scheme'])) { | 123 | if (!empty($url) && empty($this->parts['scheme'])) { |
112 | $this->parts['scheme'] = 'http'; | 124 | $this->parts['scheme'] = 'http'; |
@@ -189,4 +201,13 @@ class Url | |||
189 | } | 201 | } |
190 | return $this->parts['scheme']; | 202 | return $this->parts['scheme']; |
191 | } | 203 | } |
204 | |||
205 | /** | ||
206 | * Test if the Url is an HTTP one. | ||
207 | * | ||
208 | * @return true is HTTP, false otherwise. | ||
209 | */ | ||
210 | public function isHttp() { | ||
211 | return strpos(strtolower($this->parts['scheme']), 'http') !== false; | ||
212 | } | ||
192 | } | 213 | } |
diff --git a/application/Utils.php b/application/Utils.php index ac8bfbfc..10d60698 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -4,6 +4,24 @@ | |||
4 | */ | 4 | */ |
5 | 5 | ||
6 | /** | 6 | /** |
7 | * Logs a message to a text file | ||
8 | * | ||
9 | * The log format is compatible with fail2ban. | ||
10 | * | ||
11 | * @param string $logFile where to write the logs | ||
12 | * @param string $clientIp the client's remote IPv4/IPv6 address | ||
13 | * @param string $message the message to log | ||
14 | */ | ||
15 | function logm($logFile, $clientIp, $message) | ||
16 | { | ||
17 | file_put_contents( | ||
18 | $logFile, | ||
19 | date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL, | ||
20 | FILE_APPEND | ||
21 | ); | ||
22 | } | ||
23 | |||
24 | /** | ||
7 | * Returns the small hash of a string, using RFC 4648 base64url format | 25 | * Returns the small hash of a string, using RFC 4648 base64url format |
8 | * | 26 | * |
9 | * Small hashes: | 27 | * Small hashes: |
@@ -64,12 +82,14 @@ function sanitizeLink(&$link) | |||
64 | 82 | ||
65 | /** | 83 | /** |
66 | * Checks if a string represents a valid date | 84 | * Checks if a string represents a valid date |
85 | |||
86 | * @param string $format The expected DateTime format of the string | ||
87 | * @param string $string A string-formatted date | ||
88 | * | ||
89 | * @return bool whether the string is a valid date | ||
67 | * | 90 | * |
68 | * @param string a string-formatted date | 91 | * @see http://php.net/manual/en/class.datetime.php |
69 | * @param format the expected DateTime format of the string | 92 | * @see http://php.net/manual/en/datetime.createfromformat.php |
70 | * @return whether the string is a valid date | ||
71 | * @see http://php.net/manual/en/class.datetime.php | ||
72 | * @see http://php.net/manual/en/datetime.createfromformat.php | ||
73 | */ | 93 | */ |
74 | function checkDateFormat($format, $string) | 94 | function checkDateFormat($format, $string) |
75 | { | 95 | { |
diff --git a/docker/.htaccess b/docker/.htaccess new file mode 100644 index 00000000..b584d98c --- /dev/null +++ b/docker/.htaccess | |||
@@ -0,0 +1,2 @@ | |||
1 | Allow from none | ||
2 | Deny from all | ||
diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile new file mode 100644 index 00000000..2ed59b89 --- /dev/null +++ b/docker/development/Dockerfile | |||
@@ -0,0 +1,28 @@ | |||
1 | FROM debian:jessie | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | RUN apt-get update \ | ||
5 | && apt-get install -y \ | ||
6 | nginx-light php5-fpm php5-gd supervisor \ | ||
7 | git nano | ||
8 | |||
9 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer | ||
10 | RUN chmod 755 /usr/local/bin/composer | ||
11 | |||
12 | COPY nginx.conf /etc/nginx/nginx.conf | ||
13 | COPY supervised.conf /etc/supervisor/conf.d/supervised.conf | ||
14 | RUN echo "<?php phpinfo(); ?>" > /var/www/index.php | ||
15 | |||
16 | WORKDIR /var/www | ||
17 | RUN rm -rf html \ | ||
18 | && git clone https://github.com/shaarli/Shaarli.git shaarli \ | ||
19 | && chown -R www-data:www-data . | ||
20 | |||
21 | WORKDIR /var/www/shaarli | ||
22 | RUN composer install | ||
23 | |||
24 | VOLUME /var/www/shaarli/data | ||
25 | |||
26 | EXPOSE 80 | ||
27 | |||
28 | CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"] | ||
diff --git a/docker/development/IMAGE.md b/docker/development/IMAGE.md new file mode 100644 index 00000000..e2ff0f0e --- /dev/null +++ b/docker/development/IMAGE.md | |||
@@ -0,0 +1,10 @@ | |||
1 | ## shaarli:dev | ||
2 | - [Debian 8 Jessie](https://hub.docker.com/_/debian/) | ||
3 | - [PHP5-FPM](http://php-fpm.org/) | ||
4 | - [Nginx](http://nginx.org/) | ||
5 | - [Shaarli](https://github.com/shaarli/Shaarli) | ||
6 | |||
7 | ### Development tools | ||
8 | - [composer](https://getcomposer.org/) | ||
9 | - [git](http://git-scm.com/) | ||
10 | - [nano](http://www.nano-editor.org/) | ||
diff --git a/docker/development/nginx.conf b/docker/development/nginx.conf new file mode 100644 index 00000000..cda09b56 --- /dev/null +++ b/docker/development/nginx.conf | |||
@@ -0,0 +1,64 @@ | |||
1 | user www-data www-data; | ||
2 | daemon off; | ||
3 | worker_processes 4; | ||
4 | |||
5 | events { | ||
6 | worker_connections 768; | ||
7 | } | ||
8 | |||
9 | http { | ||
10 | include mime.types; | ||
11 | default_type application/octet-stream; | ||
12 | keepalive_timeout 20; | ||
13 | |||
14 | index index.html index.php; | ||
15 | |||
16 | server { | ||
17 | listen 80; | ||
18 | root /var/www/shaarli; | ||
19 | |||
20 | access_log /var/log/nginx/shaarli.access.log; | ||
21 | error_log /var/log/nginx/shaarli.error.log; | ||
22 | |||
23 | location /phpinfo/ { | ||
24 | # add a PHP info page for convenience | ||
25 | fastcgi_pass unix:/var/run/php5-fpm.sock; | ||
26 | fastcgi_index index.php; | ||
27 | fastcgi_param SCRIPT_FILENAME /var/www/index.php; | ||
28 | include fastcgi_params; | ||
29 | } | ||
30 | |||
31 | location ~ /\. { | ||
32 | # deny access to dotfiles | ||
33 | access_log off; | ||
34 | log_not_found off; | ||
35 | deny all; | ||
36 | } | ||
37 | |||
38 | location ~ ~$ { | ||
39 | # deny access to temp editor files, e.g. "script.php~" | ||
40 | access_log off; | ||
41 | log_not_found off; | ||
42 | deny all; | ||
43 | } | ||
44 | |||
45 | location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { | ||
46 | # cache static assets | ||
47 | expires max; | ||
48 | add_header Pragma public; | ||
49 | add_header Cache-Control "public, must-revalidate, proxy-revalidate"; | ||
50 | } | ||
51 | |||
52 | location ~ (index)\.php$ { | ||
53 | # filter and proxy PHP requests to PHP-FPM | ||
54 | fastcgi_pass unix:/var/run/php5-fpm.sock; | ||
55 | fastcgi_index index.php; | ||
56 | include fastcgi.conf; | ||
57 | } | ||
58 | |||
59 | location ~ \.php$ { | ||
60 | # deny access to all other PHP scripts | ||
61 | deny all; | ||
62 | } | ||
63 | } | ||
64 | } | ||
diff --git a/docker/development/supervised.conf b/docker/development/supervised.conf new file mode 100644 index 00000000..5acd9795 --- /dev/null +++ b/docker/development/supervised.conf | |||
@@ -0,0 +1,13 @@ | |||
1 | [program:php5-fpm] | ||
2 | command=/usr/sbin/php5-fpm -F | ||
3 | priority=5 | ||
4 | autostart=true | ||
5 | autorestart=true | ||
6 | |||
7 | [program:nginx] | ||
8 | command=/usr/sbin/nginx | ||
9 | priority=10 | ||
10 | autostart=true | ||
11 | autorestart=true | ||
12 | stdout_events_enabled=true | ||
13 | stderr_events_enabled=true | ||
diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile new file mode 100644 index 00000000..3db4eb56 --- /dev/null +++ b/docker/production/Dockerfile | |||
@@ -0,0 +1,20 @@ | |||
1 | FROM debian:jessie | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | RUN apt-get update \ | ||
5 | && apt-get install -y curl nginx-light php5-fpm php5-gd supervisor | ||
6 | |||
7 | COPY nginx.conf /etc/nginx/nginx.conf | ||
8 | COPY supervised.conf /etc/supervisor/conf.d/supervised.conf | ||
9 | |||
10 | WORKDIR /var/www | ||
11 | RUN rm -rf html \ | ||
12 | && curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xvzf - \ | ||
13 | && mv Shaarli-master shaarli \ | ||
14 | && chown -R www-data:www-data shaarli | ||
15 | |||
16 | VOLUME /var/www/shaarli/data | ||
17 | |||
18 | EXPOSE 80 | ||
19 | |||
20 | CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"] | ||
diff --git a/docker/production/IMAGE.md b/docker/production/IMAGE.md new file mode 100644 index 00000000..6f827b35 --- /dev/null +++ b/docker/production/IMAGE.md | |||
@@ -0,0 +1,5 @@ | |||
1 | ## shaarli:latest | ||
2 | - [Debian 8 Jessie](https://hub.docker.com/_/debian/) | ||
3 | - [PHP5-FPM](http://php-fpm.org/) | ||
4 | - [Nginx](http://nginx.org/) | ||
5 | - [Shaarli](https://github.com/shaarli/Shaarli) | ||
diff --git a/docker/production/nginx.conf b/docker/production/nginx.conf new file mode 100644 index 00000000..e23c4587 --- /dev/null +++ b/docker/production/nginx.conf | |||
@@ -0,0 +1,56 @@ | |||
1 | user www-data www-data; | ||
2 | daemon off; | ||
3 | worker_processes 4; | ||
4 | |||
5 | events { | ||
6 | worker_connections 768; | ||
7 | } | ||
8 | |||
9 | http { | ||
10 | include mime.types; | ||
11 | default_type application/octet-stream; | ||
12 | keepalive_timeout 20; | ||
13 | |||
14 | index index.html index.php; | ||
15 | |||
16 | server { | ||
17 | listen 80; | ||
18 | root /var/www/shaarli; | ||
19 | |||
20 | access_log /var/log/nginx/shaarli.access.log; | ||
21 | error_log /var/log/nginx/shaarli.error.log; | ||
22 | |||
23 | location ~ /\. { | ||
24 | # deny access to dotfiles | ||
25 | access_log off; | ||
26 | log_not_found off; | ||
27 | deny all; | ||
28 | } | ||
29 | |||
30 | location ~ ~$ { | ||
31 | # deny access to temp editor files, e.g. "script.php~" | ||
32 | access_log off; | ||
33 | log_not_found off; | ||
34 | deny all; | ||
35 | } | ||
36 | |||
37 | location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { | ||
38 | # cache static assets | ||
39 | expires max; | ||
40 | add_header Pragma public; | ||
41 | add_header Cache-Control "public, must-revalidate, proxy-revalidate"; | ||
42 | } | ||
43 | |||
44 | location ~ (index)\.php$ { | ||
45 | # filter and proxy PHP requests to PHP-FPM | ||
46 | fastcgi_pass unix:/var/run/php5-fpm.sock; | ||
47 | fastcgi_index index.php; | ||
48 | include fastcgi.conf; | ||
49 | } | ||
50 | |||
51 | location ~ \.php$ { | ||
52 | # deny access to all other PHP scripts | ||
53 | deny all; | ||
54 | } | ||
55 | } | ||
56 | } | ||
diff --git a/docker/production/stable/Dockerfile b/docker/production/stable/Dockerfile new file mode 100644 index 00000000..2bb3948c --- /dev/null +++ b/docker/production/stable/Dockerfile | |||
@@ -0,0 +1,20 @@ | |||
1 | FROM debian:jessie | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | RUN apt-get update \ | ||
5 | && apt-get install -y curl nginx-light php5-fpm php5-gd supervisor | ||
6 | |||
7 | COPY nginx.conf /etc/nginx/nginx.conf | ||
8 | COPY supervised.conf /etc/supervisor/conf.d/supervised.conf | ||
9 | |||
10 | WORKDIR /var/www | ||
11 | RUN rm -rf html \ | ||
12 | && curl -L https://github.com/shaarli/Shaarli/archive/stable.tar.gz | tar xvzf - \ | ||
13 | && mv Shaarli-stable shaarli \ | ||
14 | && chown -R www-data:www-data shaarli | ||
15 | |||
16 | VOLUME /var/www/shaarli/data | ||
17 | |||
18 | EXPOSE 80 | ||
19 | |||
20 | CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"] | ||
diff --git a/docker/production/stable/IMAGE.md b/docker/production/stable/IMAGE.md new file mode 100644 index 00000000..d85b1d7a --- /dev/null +++ b/docker/production/stable/IMAGE.md | |||
@@ -0,0 +1,5 @@ | |||
1 | ## shaarli:stable | ||
2 | - [Debian 8 Jessie](https://hub.docker.com/_/debian/) | ||
3 | - [PHP5-FPM](http://php-fpm.org/) | ||
4 | - [Nginx](http://nginx.org/) | ||
5 | - [Shaarli (stable)](https://github.com/shaarli/Shaarli/tree/stable) | ||
diff --git a/docker/production/stable/nginx.conf b/docker/production/stable/nginx.conf new file mode 100644 index 00000000..e23c4587 --- /dev/null +++ b/docker/production/stable/nginx.conf | |||
@@ -0,0 +1,56 @@ | |||
1 | user www-data www-data; | ||
2 | daemon off; | ||
3 | worker_processes 4; | ||
4 | |||
5 | events { | ||
6 | worker_connections 768; | ||
7 | } | ||
8 | |||
9 | http { | ||
10 | include mime.types; | ||
11 | default_type application/octet-stream; | ||
12 | keepalive_timeout 20; | ||
13 | |||
14 | index index.html index.php; | ||
15 | |||
16 | server { | ||
17 | listen 80; | ||
18 | root /var/www/shaarli; | ||
19 | |||
20 | access_log /var/log/nginx/shaarli.access.log; | ||
21 | error_log /var/log/nginx/shaarli.error.log; | ||
22 | |||
23 | location ~ /\. { | ||
24 | # deny access to dotfiles | ||
25 | access_log off; | ||
26 | log_not_found off; | ||
27 | deny all; | ||
28 | } | ||
29 | |||
30 | location ~ ~$ { | ||
31 | # deny access to temp editor files, e.g. "script.php~" | ||
32 | access_log off; | ||
33 | log_not_found off; | ||
34 | deny all; | ||
35 | } | ||
36 | |||
37 | location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { | ||
38 | # cache static assets | ||
39 | expires max; | ||
40 | add_header Pragma public; | ||
41 | add_header Cache-Control "public, must-revalidate, proxy-revalidate"; | ||
42 | } | ||
43 | |||
44 | location ~ (index)\.php$ { | ||
45 | # filter and proxy PHP requests to PHP-FPM | ||
46 | fastcgi_pass unix:/var/run/php5-fpm.sock; | ||
47 | fastcgi_index index.php; | ||
48 | include fastcgi.conf; | ||
49 | } | ||
50 | |||
51 | location ~ \.php$ { | ||
52 | # deny access to all other PHP scripts | ||
53 | deny all; | ||
54 | } | ||
55 | } | ||
56 | } | ||
diff --git a/docker/production/stable/supervised.conf b/docker/production/stable/supervised.conf new file mode 100644 index 00000000..5acd9795 --- /dev/null +++ b/docker/production/stable/supervised.conf | |||
@@ -0,0 +1,13 @@ | |||
1 | [program:php5-fpm] | ||
2 | command=/usr/sbin/php5-fpm -F | ||
3 | priority=5 | ||
4 | autostart=true | ||
5 | autorestart=true | ||
6 | |||
7 | [program:nginx] | ||
8 | command=/usr/sbin/nginx | ||
9 | priority=10 | ||
10 | autostart=true | ||
11 | autorestart=true | ||
12 | stdout_events_enabled=true | ||
13 | stderr_events_enabled=true | ||
diff --git a/docker/production/supervised.conf b/docker/production/supervised.conf new file mode 100644 index 00000000..5acd9795 --- /dev/null +++ b/docker/production/supervised.conf | |||
@@ -0,0 +1,13 @@ | |||
1 | [program:php5-fpm] | ||
2 | command=/usr/sbin/php5-fpm -F | ||
3 | priority=5 | ||
4 | autostart=true | ||
5 | autorestart=true | ||
6 | |||
7 | [program:nginx] | ||
8 | command=/usr/sbin/nginx | ||
9 | priority=10 | ||
10 | autostart=true | ||
11 | autorestart=true | ||
12 | stdout_events_enabled=true | ||
13 | stderr_events_enabled=true | ||
diff --git a/inc/shaarli.css b/inc/shaarli.css index 79ba1d69..7a69d575 100644 --- a/inc/shaarli.css +++ b/inc/shaarli.css | |||
@@ -738,25 +738,6 @@ h1 { | |||
738 | background: #ffffff; | 738 | background: #ffffff; |
739 | } | 739 | } |
740 | 740 | ||
741 | div#permalinkQrcode { | ||
742 | padding: 20px; | ||
743 | width: 220px; | ||
744 | height: 220px; | ||
745 | background-color: #ffffff; | ||
746 | border: 1px solid black; | ||
747 | position: absolute; | ||
748 | top: -100px; | ||
749 | left: -100px; | ||
750 | text-align: center; | ||
751 | font-size: 8pt; | ||
752 | z-index: 50; | ||
753 | -webkit-box-shadow: 2px 2px 20px 2px #333333; | ||
754 | -moz-box-shadow: 2px 2px 20px 2px #333333; | ||
755 | -o-box-shadow: 2px 2px 20px 2px #333333; | ||
756 | -ms-box-shadow: 2px 2px 20px 2px #333333; | ||
757 | box-shadow: 2px 2px 20px 2px #333333; | ||
758 | } | ||
759 | |||
760 | div.daily { | 741 | div.daily { |
761 | font-family: Georgia, 'DejaVu Serif', Norasi, serif; | 742 | font-family: Georgia, 'DejaVu Serif', Norasi, serif; |
762 | background-color: #E6D6BE; | 743 | background-color: #E6D6BE; |
@@ -1119,4 +1100,17 @@ div.dailyNoEntry { | |||
1119 | ul.errors { | 1100 | ul.errors { |
1120 | color: red; | 1101 | color: red; |
1121 | float: left; | 1102 | float: left; |
1122 | } \ No newline at end of file | 1103 | } |
1104 | |||
1105 | /* 404 page */ | ||
1106 | .error-container { | ||
1107 | |||
1108 | margin: 50px; | ||
1109 | margin-top: 20px; | ||
1110 | } | ||
1111 | |||
1112 | .error-container h1 { | ||
1113 | text-decoration: none; | ||
1114 | font-style: normal; | ||
1115 | color: #80AD48; | ||
1116 | } | ||
@@ -1,6 +1,6 @@ | |||
1 | <?php | 1 | <?php |
2 | /** | 2 | /** |
3 | * Shaarli v0.6.1 - Shaare your links... | 3 | * Shaarli v0.6.2 - Shaare your links... |
4 | * | 4 | * |
5 | * The personal, minimalist, super-fast, no-database Delicious clone. | 5 | * The personal, minimalist, super-fast, no-database Delicious clone. |
6 | * | 6 | * |
@@ -119,7 +119,7 @@ $GLOBALS['config']['PUBSUBHUB_URL'] = ''; | |||
119 | /* | 119 | /* |
120 | * PHP configuration | 120 | * PHP configuration |
121 | */ | 121 | */ |
122 | define('shaarli_version', '0.6.1'); | 122 | define('shaarli_version', '0.6.2'); |
123 | 123 | ||
124 | // http://server.com/x/shaarli --> /shaarli/ | 124 | // http://server.com/x/shaarli --> /shaarli/ |
125 | define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); | 125 | define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); |
@@ -151,6 +151,8 @@ require_once 'application/CachedPage.php'; | |||
151 | require_once 'application/FileUtils.php'; | 151 | require_once 'application/FileUtils.php'; |
152 | require_once 'application/HttpUtils.php'; | 152 | require_once 'application/HttpUtils.php'; |
153 | require_once 'application/LinkDB.php'; | 153 | require_once 'application/LinkDB.php'; |
154 | require_once 'application/LinkFilter.php'; | ||
155 | require_once 'application/LinkUtils.php'; | ||
154 | require_once 'application/TimeZone.php'; | 156 | require_once 'application/TimeZone.php'; |
155 | require_once 'application/Url.php'; | 157 | require_once 'application/Url.php'; |
156 | require_once 'application/Utils.php'; | 158 | require_once 'application/Utils.php'; |
@@ -307,14 +309,6 @@ function setup_login_state() { | |||
307 | $userIsLoggedIn = setup_login_state(); | 309 | $userIsLoggedIn = setup_login_state(); |
308 | 310 | ||
309 | 311 | ||
310 | // ----------------------------------------------------------------------------------------------- | ||
311 | // Log to text file | ||
312 | function logm($message) | ||
313 | { | ||
314 | $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n"; | ||
315 | file_put_contents($GLOBALS['config']['LOG_FILE'], $t, FILE_APPEND); | ||
316 | } | ||
317 | |||
318 | // ------------------------------------------------------------------------------------------ | 312 | // ------------------------------------------------------------------------------------------ |
319 | // Sniff browser language to display dates in the right format automatically. | 313 | // Sniff browser language to display dates in the right format automatically. |
320 | // (Note that is may not work on your server if the corresponding local is not installed.) | 314 | // (Note that is may not work on your server if the corresponding local is not installed.) |
@@ -378,10 +372,10 @@ function check_auth($login,$password) | |||
378 | if ($login==$GLOBALS['login'] && $hash==$GLOBALS['hash']) | 372 | if ($login==$GLOBALS['login'] && $hash==$GLOBALS['hash']) |
379 | { // Login/password is correct. | 373 | { // Login/password is correct. |
380 | fillSessionInfo(); | 374 | fillSessionInfo(); |
381 | logm('Login successful'); | 375 | logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Login successful'); |
382 | return True; | 376 | return True; |
383 | } | 377 | } |
384 | logm('Login failed for user '.$login); | 378 | logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login); |
385 | return False; | 379 | return False; |
386 | } | 380 | } |
387 | 381 | ||
@@ -418,7 +412,7 @@ function ban_loginFailed() | |||
418 | if ($gb['FAILURES'][$ip]>($GLOBALS['config']['BAN_AFTER']-1)) | 412 | if ($gb['FAILURES'][$ip]>($GLOBALS['config']['BAN_AFTER']-1)) |
419 | { | 413 | { |
420 | $gb['BANS'][$ip]=time()+$GLOBALS['config']['BAN_DURATION']; | 414 | $gb['BANS'][$ip]=time()+$GLOBALS['config']['BAN_DURATION']; |
421 | logm('IP address banned from login'); | 415 | logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'IP address banned from login'); |
422 | } | 416 | } |
423 | $GLOBALS['IPBANS'] = $gb; | 417 | $GLOBALS['IPBANS'] = $gb; |
424 | file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"); | 418 | file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"); |
@@ -442,7 +436,7 @@ function ban_canLogin() | |||
442 | // User is banned. Check if the ban has expired: | 436 | // User is banned. Check if the ban has expired: |
443 | if ($gb['BANS'][$ip]<=time()) | 437 | if ($gb['BANS'][$ip]<=time()) |
444 | { // Ban expired, user can try to login again. | 438 | { // Ban expired, user can try to login again. |
445 | logm('Ban lifted.'); | 439 | logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Ban lifted.'); |
446 | unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]); | 440 | unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]); |
447 | file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"); | 441 | file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"); |
448 | return true; // Ban has expired, user can login. | 442 | return true; // Ban has expired, user can login. |
@@ -478,7 +472,7 @@ if (isset($_POST['login'])) | |||
478 | session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes" | 472 | session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes" |
479 | session_regenerate_id(true); | 473 | session_regenerate_id(true); |
480 | } | 474 | } |
481 | 475 | ||
482 | // Optional redirect after login: | 476 | // Optional redirect after login: |
483 | if (isset($_GET['post'])) { | 477 | if (isset($_GET['post'])) { |
484 | $uri = '?post='. urlencode($_GET['post']); | 478 | $uri = '?post='. urlencode($_GET['post']); |
@@ -577,13 +571,6 @@ function linkdate2iso8601($linkdate) | |||
577 | return date('c',linkdate2timestamp($linkdate)); // 'c' is for ISO 8601 date format. | 571 | return date('c',linkdate2timestamp($linkdate)); // 'c' is for ISO 8601 date format. |
578 | } | 572 | } |
579 | 573 | ||
580 | // Extract title from an HTML document. | ||
581 | // (Returns an empty string if not found.) | ||
582 | function html_extract_title($html) | ||
583 | { | ||
584 | return preg_match('!<title>(.*?)</title>!is', $html, $matches) ? trim(str_replace("\n",' ', $matches[1])) : '' ; | ||
585 | } | ||
586 | |||
587 | // ------------------------------------------------------------------------------------------ | 574 | // ------------------------------------------------------------------------------------------ |
588 | // Token management for XSRF protection | 575 | // Token management for XSRF protection |
589 | // Token should be used in any form which acts on data (create,update,delete,import...). | 576 | // Token should be used in any form which acts on data (create,update,delete,import...). |
@@ -646,7 +633,7 @@ class pageBuilder | |||
646 | $this->tpl->assign('versionError', ''); | 633 | $this->tpl->assign('versionError', ''); |
647 | 634 | ||
648 | } catch (Exception $exc) { | 635 | } catch (Exception $exc) { |
649 | logm($exc->getMessage()); | 636 | logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], $exc->getMessage()); |
650 | $this->tpl->assign('newVersion', ''); | 637 | $this->tpl->assign('newVersion', ''); |
651 | $this->tpl->assign('versionError', escape($exc->getMessage())); | 638 | $this->tpl->assign('versionError', escape($exc->getMessage())); |
652 | } | 639 | } |
@@ -694,6 +681,18 @@ class pageBuilder | |||
694 | if ($this->tpl===false) $this->initialize(); // Lazy initialization | 681 | if ($this->tpl===false) $this->initialize(); // Lazy initialization |
695 | $this->tpl->draw($page); | 682 | $this->tpl->draw($page); |
696 | } | 683 | } |
684 | |||
685 | /** | ||
686 | * Render a 404 page (uses the template : tpl/404.tpl) | ||
687 | * | ||
688 | * usage : $PAGE->render404('The link was deleted') | ||
689 | * @param string $message A messate to display what is not found | ||
690 | */ | ||
691 | public function render404($message='The page you are trying to reach does not exist or has been deleted.') { | ||
692 | header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); | ||
693 | $this->tpl->assign('error_message', $message); | ||
694 | $this->renderPage('404'); | ||
695 | } | ||
697 | } | 696 | } |
698 | 697 | ||
699 | // ------------------------------------------------------------------------------------------ | 698 | // ------------------------------------------------------------------------------------------ |
@@ -730,18 +729,23 @@ function showRSS() | |||
730 | // Read links from database (and filter private links if user it not logged in). | 729 | // Read links from database (and filter private links if user it not logged in). |
731 | 730 | ||
732 | // Optionally filter the results: | 731 | // Optionally filter the results: |
733 | $linksToDisplay=array(); | 732 | if (!empty($_GET['searchterm'])) { |
734 | if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']); | 733 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); |
735 | else if (!empty($_GET['searchtags'])) $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); | 734 | } |
736 | else $linksToDisplay = $LINKSDB; | 735 | elseif (!empty($_GET['searchtags'])) { |
736 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); | ||
737 | } | ||
738 | else { | ||
739 | $linksToDisplay = $LINKSDB; | ||
740 | } | ||
737 | 741 | ||
738 | $nblinksToDisplay = 50; // Number of links to display. | 742 | $nblinksToDisplay = 50; // Number of links to display. |
739 | if (!empty($_GET['nb'])) // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. | 743 | // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. |
740 | { | 744 | if (!empty($_GET['nb'])) { |
741 | $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ; | 745 | $nblinksToDisplay = $_GET['nb'] == 'all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1); |
742 | } | 746 | } |
743 | 747 | ||
744 | $pageaddr=escape(index_url($_SERVER)); | 748 | $pageaddr = escape(index_url($_SERVER)); |
745 | echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">'; | 749 | echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">'; |
746 | echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>'; | 750 | echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>'; |
747 | echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n"; | 751 | echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n"; |
@@ -821,15 +825,20 @@ function showATOM() | |||
821 | ); | 825 | ); |
822 | 826 | ||
823 | // Optionally filter the results: | 827 | // Optionally filter the results: |
824 | $linksToDisplay=array(); | 828 | if (!empty($_GET['searchterm'])) { |
825 | if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']); | 829 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); |
826 | else if (!empty($_GET['searchtags'])) $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); | 830 | } |
827 | else $linksToDisplay = $LINKSDB; | 831 | else if (!empty($_GET['searchtags'])) { |
832 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); | ||
833 | } | ||
834 | else { | ||
835 | $linksToDisplay = $LINKSDB; | ||
836 | } | ||
828 | 837 | ||
829 | $nblinksToDisplay = 50; // Number of links to display. | 838 | $nblinksToDisplay = 50; // Number of links to display. |
830 | if (!empty($_GET['nb'])) // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. | 839 | // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. |
831 | { | 840 | if (!empty($_GET['nb'])) { |
832 | $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ; | 841 | $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1); |
833 | } | 842 | } |
834 | 843 | ||
835 | $pageaddr=escape(index_url($_SERVER)); | 844 | $pageaddr=escape(index_url($_SERVER)); |
@@ -1024,7 +1033,7 @@ function showDaily($pageBuilder) | |||
1024 | } | 1033 | } |
1025 | 1034 | ||
1026 | try { | 1035 | try { |
1027 | $linksToDisplay = $LINKSDB->filterDay($day); | 1036 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_DAY, $day); |
1028 | } catch (Exception $exc) { | 1037 | } catch (Exception $exc) { |
1029 | error_log($exc); | 1038 | error_log($exc); |
1030 | $linksToDisplay = array(); | 1039 | $linksToDisplay = array(); |
@@ -1149,13 +1158,17 @@ function renderPage() | |||
1149 | if ($targetPage == Router::$PAGE_PICWALL) | 1158 | if ($targetPage == Router::$PAGE_PICWALL) |
1150 | { | 1159 | { |
1151 | // Optionally filter the results: | 1160 | // Optionally filter the results: |
1152 | $links=array(); | 1161 | if (!empty($_GET['searchterm'])) { |
1153 | if (!empty($_GET['searchterm'])) $links = $LINKSDB->filterFulltext($_GET['searchterm']); | 1162 | $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); |
1154 | elseif (!empty($_GET['searchtags'])) $links = $LINKSDB->filterTags(trim($_GET['searchtags'])); | 1163 | } |
1155 | else $links = $LINKSDB; | 1164 | elseif (! empty($_GET['searchtags'])) { |
1165 | $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); | ||
1166 | } | ||
1167 | else { | ||
1168 | $links = $LINKSDB; | ||
1169 | } | ||
1156 | 1170 | ||
1157 | $body=''; | 1171 | $linksToDisplay = array(); |
1158 | $linksToDisplay=array(); | ||
1159 | 1172 | ||
1160 | // Get only links which have a thumbnail. | 1173 | // Get only links which have a thumbnail. |
1161 | foreach($links as $link) | 1174 | foreach($links as $link) |
@@ -1282,13 +1295,15 @@ function renderPage() | |||
1282 | } | 1295 | } |
1283 | 1296 | ||
1284 | if (isset($params['searchtags'])) { | 1297 | if (isset($params['searchtags'])) { |
1285 | $tags = explode(' ',$params['searchtags']); | 1298 | $tags = explode(' ', $params['searchtags']); |
1286 | $tags=array_diff($tags, array($_GET['removetag'])); // Remove value from array $tags. | 1299 | // Remove value from array $tags. |
1287 | if (count($tags)==0) { | 1300 | $tags = array_diff($tags, array($_GET['removetag'])); |
1301 | $params['searchtags'] = implode(' ',$tags); | ||
1302 | |||
1303 | if (empty($params['searchtags'])) { | ||
1288 | unset($params['searchtags']); | 1304 | unset($params['searchtags']); |
1289 | } else { | ||
1290 | $params['searchtags'] = implode(' ',$tags); | ||
1291 | } | 1305 | } |
1306 | |||
1292 | unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different) | 1307 | unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different) |
1293 | } | 1308 | } |
1294 | header('Location: ?'.http_build_query($params)); | 1309 | header('Location: ?'.http_build_query($params)); |
@@ -1453,21 +1468,23 @@ function renderPage() | |||
1453 | // -------- User wants to rename a tag or delete it | 1468 | // -------- User wants to rename a tag or delete it |
1454 | if ($targetPage == Router::$PAGE_CHANGETAG) | 1469 | if ($targetPage == Router::$PAGE_CHANGETAG) |
1455 | { | 1470 | { |
1456 | if (empty($_POST['fromtag'])) | 1471 | if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { |
1457 | { | 1472 | $PAGE->assign('linkcount', count($LINKSDB)); |
1458 | $PAGE->assign('linkcount',count($LINKSDB)); | 1473 | $PAGE->assign('token', getToken()); |
1459 | $PAGE->assign('token',getToken()); | ||
1460 | $PAGE->assign('tags', $LINKSDB->allTags()); | 1474 | $PAGE->assign('tags', $LINKSDB->allTags()); |
1461 | $PAGE->renderPage('changetag'); | 1475 | $PAGE->renderPage('changetag'); |
1462 | exit; | 1476 | exit; |
1463 | } | 1477 | } |
1464 | if (!tokenOk($_POST['token'])) die('Wrong token.'); | 1478 | |
1479 | if (!tokenOk($_POST['token'])) { | ||
1480 | die('Wrong token.'); | ||
1481 | } | ||
1465 | 1482 | ||
1466 | // Delete a tag: | 1483 | // Delete a tag: |
1467 | if (!empty($_POST['deletetag']) && !empty($_POST['fromtag'])) | 1484 | if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) { |
1468 | { | ||
1469 | $needle=trim($_POST['fromtag']); | 1485 | $needle=trim($_POST['fromtag']); |
1470 | $linksToAlter = $LINKSDB->filterTags($needle,true); // True for case-sensitive tag search. | 1486 | // True for case-sensitive tag search. |
1487 | $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true); | ||
1471 | foreach($linksToAlter as $key=>$value) | 1488 | foreach($linksToAlter as $key=>$value) |
1472 | { | 1489 | { |
1473 | $tags = explode(' ',trim($value['tags'])); | 1490 | $tags = explode(' ',trim($value['tags'])); |
@@ -1481,10 +1498,10 @@ function renderPage() | |||
1481 | } | 1498 | } |
1482 | 1499 | ||
1483 | // Rename a tag: | 1500 | // Rename a tag: |
1484 | if (!empty($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) | 1501 | if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) { |
1485 | { | ||
1486 | $needle=trim($_POST['fromtag']); | 1502 | $needle=trim($_POST['fromtag']); |
1487 | $linksToAlter = $LINKSDB->filterTags($needle,true); // true for case-sensitive tag search. | 1503 | // True for case-sensitive tag search. |
1504 | $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true); | ||
1488 | foreach($linksToAlter as $key=>$value) | 1505 | foreach($linksToAlter as $key=>$value) |
1489 | { | 1506 | { |
1490 | $tags = explode(' ',trim($value['tags'])); | 1507 | $tags = explode(' ',trim($value['tags'])); |
@@ -1623,7 +1640,7 @@ function renderPage() | |||
1623 | 1640 | ||
1624 | // -------- User want to post a new link: Display link edit form. | 1641 | // -------- User want to post a new link: Display link edit form. |
1625 | if (isset($_GET['post'])) { | 1642 | if (isset($_GET['post'])) { |
1626 | $url = cleanup_url($_GET['post']); | 1643 | $url = cleanup_url(escape($_GET['post'])); |
1627 | 1644 | ||
1628 | $link_is_new = false; | 1645 | $link_is_new = false; |
1629 | // Check if URL is not already in database (in this case, we will edit the existing link) | 1646 | // Check if URL is not already in database (in this case, we will edit the existing link) |
@@ -1641,35 +1658,24 @@ function renderPage() | |||
1641 | // If this is an HTTP(S) link, we try go get the page to extract the title (otherwise we will to straight to the edit form.) | 1658 | // If this is an HTTP(S) link, we try go get the page to extract the title (otherwise we will to straight to the edit form.) |
1642 | if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) { | 1659 | if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) { |
1643 | // Short timeout to keep the application responsive | 1660 | // Short timeout to keep the application responsive |
1644 | list($headers, $data) = get_http_url($url, 4); | 1661 | list($headers, $content) = get_http_response($url, 4); |
1645 | // FIXME: Decode charset according to specified in either 1) HTTP response headers or 2) <head> in html | ||
1646 | if (strpos($headers[0], '200 OK') !== false) { | 1662 | if (strpos($headers[0], '200 OK') !== false) { |
1647 | // Look for charset in html header. | 1663 | // Retrieve charset. |
1648 | preg_match('#<meta .*charset=.*>#Usi', $data, $meta); | 1664 | $charset = get_charset($headers, $content); |
1649 | 1665 | // Extract title. | |
1650 | // If found, extract encoding. | 1666 | $title = html_extract_title($content); |
1651 | if (!empty($meta[0])) { | 1667 | // Re-encode title in utf-8 if necessary. |
1652 | // Get encoding specified in header. | 1668 | if (! empty($title) && $charset != 'utf-8') { |
1653 | preg_match('#charset="?(.*)"#si', $meta[0], $enc); | 1669 | $title = mb_convert_encoding($title, $charset, 'utf-8'); |
1654 | // If charset not found, use utf-8. | ||
1655 | $html_charset = (!empty($enc[1])) ? strtolower($enc[1]) : 'utf-8'; | ||
1656 | } | ||
1657 | else { | ||
1658 | $html_charset = 'utf-8'; | ||
1659 | } | ||
1660 | |||
1661 | // Extract title | ||
1662 | $title = html_extract_title($data); | ||
1663 | if (!empty($title)) { | ||
1664 | // Re-encode title in utf-8 if necessary. | ||
1665 | $title = ($html_charset == 'iso-8859-1') ? utf8_encode($title) : $title; | ||
1666 | } | 1670 | } |
1667 | } | 1671 | } |
1668 | } | 1672 | } |
1673 | |||
1669 | if ($url == '') { | 1674 | if ($url == '') { |
1670 | $url = '?' . smallHash($linkdate); | 1675 | $url = '?' . smallHash($linkdate); |
1671 | $title = 'Note: '; | 1676 | $title = 'Note: '; |
1672 | } | 1677 | } |
1678 | |||
1673 | $link = array( | 1679 | $link = array( |
1674 | 'linkdate' => $linkdate, | 1680 | 'linkdate' => $linkdate, |
1675 | 'title' => $title, | 1681 | 'title' => $title, |
@@ -1865,81 +1871,75 @@ function importFile() | |||
1865 | function buildLinkList($PAGE,$LINKSDB) | 1871 | function buildLinkList($PAGE,$LINKSDB) |
1866 | { | 1872 | { |
1867 | // ---- Filter link database according to parameters | 1873 | // ---- Filter link database according to parameters |
1868 | $linksToDisplay=array(); | 1874 | $search_type = ''; |
1869 | $search_type=''; | 1875 | $search_crits = ''; |
1870 | $search_crits=''; | 1876 | $privateonly = !empty($_SESSION['privateonly']) ? true : false; |
1871 | if (isset($_GET['searchterm'])) // Fulltext search | 1877 | |
1872 | { | 1878 | // Fulltext search |
1873 | $linksToDisplay = $LINKSDB->filterFulltext(trim($_GET['searchterm'])); | 1879 | if (isset($_GET['searchterm'])) { |
1874 | $search_crits=escape(trim($_GET['searchterm'])); | 1880 | $search_crits = escape(trim($_GET['searchterm'])); |
1875 | $search_type='fulltext'; | 1881 | $search_type = LinkFilter::$FILTER_TEXT; |
1876 | } | 1882 | $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly); |
1877 | elseif (isset($_GET['searchtags'])) // Search by tag | 1883 | } |
1878 | { | 1884 | // Search by tag |
1879 | $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); | 1885 | elseif (isset($_GET['searchtags'])) { |
1880 | $search_crits=explode(' ',escape(trim($_GET['searchtags']))); | 1886 | $search_crits = explode(' ', escape(trim($_GET['searchtags']))); |
1881 | $search_type='tags'; | 1887 | $search_type = LinkFilter::$FILTER_TAG; |
1882 | } | 1888 | $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly); |
1883 | elseif (isset($_SERVER['QUERY_STRING']) && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/',$_SERVER['QUERY_STRING'])) // Detect smallHashes in URL | 1889 | } |
1884 | { | 1890 | // Detect smallHashes in URL. |
1885 | $linksToDisplay = $LINKSDB->filterSmallHash(substr(trim($_SERVER["QUERY_STRING"], '/'),0,6)); | 1891 | elseif (isset($_SERVER['QUERY_STRING']) |
1886 | if (count($linksToDisplay)==0) | 1892 | && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])) { |
1887 | { | 1893 | $search_type = LinkFilter::$FILTER_HASH; |
1888 | header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); | 1894 | $search_crits = substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6); |
1889 | echo '<h1>404 Not found.</h1>Oh crap. The link you are trying to reach does not exist or has been deleted.'; | 1895 | $linksToDisplay = $LINKSDB->filter($search_type, $search_crits); |
1890 | echo '<br>Would you mind <a href="?">clicking here</a>?'; | 1896 | |
1897 | if (count($linksToDisplay) == 0) { | ||
1898 | $PAGE->render404('The link you are trying to reach does not exist or has been deleted.'); | ||
1891 | exit; | 1899 | exit; |
1892 | } | 1900 | } |
1893 | $search_type='permalink'; | ||
1894 | } | 1901 | } |
1895 | else | 1902 | // Otherwise, display without filtering. |
1896 | $linksToDisplay = $LINKSDB; // Otherwise, display without filtering. | 1903 | else { |
1897 | 1904 | $linksToDisplay = $LINKSDB->filter('', '', false, $privateonly); | |
1898 | |||
1899 | // Option: Show only private links | ||
1900 | if (!empty($_SESSION['privateonly'])) | ||
1901 | { | ||
1902 | $tmp = array(); | ||
1903 | foreach($linksToDisplay as $linkdate=>$link) | ||
1904 | { | ||
1905 | if ($link['private']!=0) $tmp[$linkdate]=$link; | ||
1906 | } | ||
1907 | $linksToDisplay=$tmp; | ||
1908 | } | 1905 | } |
1909 | 1906 | ||
1910 | // ---- Handle paging. | 1907 | // ---- Handle paging. |
1911 | /* Can someone explain to me why you get the following error when using array_keys() on an object which implements the interface ArrayAccess??? | 1908 | $keys = array(); |
1912 | "Warning: array_keys() expects parameter 1 to be array, object given in ... " | 1909 | foreach ($linksToDisplay as $key => $value) { |
1913 | If my class implements ArrayAccess, why won't array_keys() accept it ? ( $keys=array_keys($linksToDisplay); ) | 1910 | $keys[] = $key; |
1914 | */ | 1911 | } |
1915 | $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // Stupid and ugly. Thanks PHP. | ||
1916 | 1912 | ||
1917 | // If there is only a single link, we change on-the-fly the title of the page. | 1913 | // If there is only a single link, we change on-the-fly the title of the page. |
1918 | if (count($linksToDisplay)==1) $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title']; | 1914 | if (count($linksToDisplay) == 1) { |
1915 | $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title']; | ||
1916 | } | ||
1919 | 1917 | ||
1920 | // Select articles according to paging. | 1918 | // Select articles according to paging. |
1921 | $pagecount = ceil(count($keys)/$_SESSION['LINKS_PER_PAGE']); | 1919 | $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']); |
1922 | $pagecount = ($pagecount==0 ? 1 : $pagecount); | 1920 | $pagecount = $pagecount == 0 ? 1 : $pagecount; |
1923 | $page=( empty($_GET['page']) ? 1 : intval($_GET['page'])); | 1921 | $page= empty($_GET['page']) ? 1 : intval($_GET['page']); |
1924 | $page = ( $page<1 ? 1 : $page ); | 1922 | $page = $page < 1 ? 1 : $page; |
1925 | $page = ( $page>$pagecount ? $pagecount : $page ); | 1923 | $page = $page > $pagecount ? $pagecount : $page; |
1926 | $i = ($page-1)*$_SESSION['LINKS_PER_PAGE']; // Start index. | 1924 | // Start index. |
1927 | $end = $i+$_SESSION['LINKS_PER_PAGE']; | 1925 | $i = ($page-1) * $_SESSION['LINKS_PER_PAGE']; |
1928 | $linkDisp=array(); // Links to display | 1926 | $end = $i + $_SESSION['LINKS_PER_PAGE']; |
1927 | $linkDisp = array(); | ||
1929 | while ($i<$end && $i<count($keys)) | 1928 | while ($i<$end && $i<count($keys)) |
1930 | { | 1929 | { |
1931 | $link = $linksToDisplay[$keys[$i]]; | 1930 | $link = $linksToDisplay[$keys[$i]]; |
1932 | $link['description'] = format_description($link['description'], $GLOBALS['redirector']); | 1931 | $link['description'] = format_description($link['description'], $GLOBALS['redirector']); |
1933 | $classLi = $i%2!=0 ? '' : 'publicLinkHightLight'; | 1932 | $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight'; |
1934 | $link['class'] = ($link['private']==0 ? $classLi : 'private'); | 1933 | $link['class'] = $link['private'] == 0 ? $classLi : 'private'; |
1935 | $link['timestamp']=linkdate2timestamp($link['linkdate']); | 1934 | $link['timestamp'] = linkdate2timestamp($link['linkdate']); |
1936 | $taglist = explode(' ',$link['tags']); | 1935 | $taglist = explode(' ', $link['tags']); |
1937 | uasort($taglist, 'strcasecmp'); | 1936 | uasort($taglist, 'strcasecmp'); |
1938 | $link['taglist']=$taglist; | 1937 | $link['taglist'] = $taglist; |
1939 | $link['shorturl'] = smallHash($link['linkdate']); | 1938 | $link['shorturl'] = smallHash($link['linkdate']); |
1940 | if ($link["url"][0] === '?' && // Check for both signs of a note: starting with ? and 7 chars long. I doubt that you'll post any links that look like this. | 1939 | // Check for both signs of a note: starting with ? and 7 chars long. |
1941 | strlen($link["url"]) === 7) { | 1940 | if ($link['url'][0] === '?' && |
1942 | $link["url"] = index_url($_SERVER) . $link["url"]; | 1941 | strlen($link['url']) === 7) { |
1942 | $link['url'] = index_url($_SERVER) . $link['url']; | ||
1943 | } | 1943 | } |
1944 | 1944 | ||
1945 | $linkDisp[$keys[$i]] = $link; | 1945 | $linkDisp[$keys[$i]] = $link; |
@@ -1947,13 +1947,21 @@ function buildLinkList($PAGE,$LINKSDB) | |||
1947 | } | 1947 | } |
1948 | 1948 | ||
1949 | // Compute paging navigation | 1949 | // Compute paging navigation |
1950 | $searchterm= ( empty($_GET['searchterm']) ? '' : '&searchterm='.$_GET['searchterm'] ); | 1950 | $searchterm = empty($_GET['searchterm']) ? '' : '&searchterm=' . $_GET['searchterm']; |
1951 | $searchtags= ( empty($_GET['searchtags']) ? '' : '&searchtags='.$_GET['searchtags'] ); | 1951 | $searchtags = empty($_GET['searchtags']) ? '' : '&searchtags=' . $_GET['searchtags']; |
1952 | $paging=''; | 1952 | $previous_page_url = ''; |
1953 | $previous_page_url=''; if ($i!=count($keys)) $previous_page_url='?page='.($page+1).$searchterm.$searchtags; | 1953 | if ($i != count($keys)) { |
1954 | $next_page_url='';if ($page>1) $next_page_url='?page='.($page-1).$searchterm.$searchtags; | 1954 | $previous_page_url = '?page=' . ($page+1) . $searchterm . $searchtags; |
1955 | } | ||
1956 | $next_page_url=''; | ||
1957 | if ($page>1) { | ||
1958 | $next_page_url = '?page=' . ($page-1) . $searchterm . $searchtags; | ||
1959 | } | ||
1955 | 1960 | ||
1956 | $token = ''; if (isLoggedIn()) $token=getToken(); | 1961 | $token = ''; |
1962 | if (isLoggedIn()) { | ||
1963 | $token = getToken(); | ||
1964 | } | ||
1957 | 1965 | ||
1958 | // Fill all template fields. | 1966 | // Fill all template fields. |
1959 | $data = array( | 1967 | $data = array( |
@@ -2290,11 +2298,11 @@ function genThumbnail() | |||
2290 | else // This is a flickr page (html) | 2298 | else // This is a flickr page (html) |
2291 | { | 2299 | { |
2292 | // Get the flickr html page. | 2300 | // Get the flickr html page. |
2293 | list($headers, $data) = get_http_url($url, 20); | 2301 | list($headers, $content) = get_http_response($url, 20); |
2294 | if (strpos($headers[0], '200 OK') !== false) | 2302 | if (strpos($headers[0], '200 OK') !== false) |
2295 | { | 2303 | { |
2296 | // flickr now nicely provides the URL of the thumbnail in each flickr page. | 2304 | // flickr now nicely provides the URL of the thumbnail in each flickr page. |
2297 | preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!',$data,$matches); | 2305 | preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!', $content, $matches); |
2298 | if (!empty($matches[1])) $imageurl=$matches[1]; | 2306 | if (!empty($matches[1])) $imageurl=$matches[1]; |
2299 | 2307 | ||
2300 | // In albums (and some other pages), the link rel="image_src" is not provided, | 2308 | // In albums (and some other pages), the link rel="image_src" is not provided, |
@@ -2302,7 +2310,7 @@ function genThumbnail() | |||
2302 | // <meta property="og:image" content="http://farm4.staticflickr.com/3398/3239339068_25d13535ff_z.jpg" /> | 2310 | // <meta property="og:image" content="http://farm4.staticflickr.com/3398/3239339068_25d13535ff_z.jpg" /> |
2303 | if ($imageurl=='') | 2311 | if ($imageurl=='') |
2304 | { | 2312 | { |
2305 | preg_match('!<meta property=\"og:image\" content=\"(.+?)\"!',$data,$matches); | 2313 | preg_match('!<meta property=\"og:image\" content=\"(.+?)\"!', $content, $matches); |
2306 | if (!empty($matches[1])) $imageurl=$matches[1]; | 2314 | if (!empty($matches[1])) $imageurl=$matches[1]; |
2307 | } | 2315 | } |
2308 | } | 2316 | } |
@@ -2311,11 +2319,12 @@ function genThumbnail() | |||
2311 | if ($imageurl!='') | 2319 | if ($imageurl!='') |
2312 | { // Let's download the image. | 2320 | { // Let's download the image. |
2313 | // Image is 240x120, so 10 seconds to download should be enough. | 2321 | // Image is 240x120, so 10 seconds to download should be enough. |
2314 | list($headers, $data) = get_http_url($imageurl, 10); | 2322 | list($headers, $content) = get_http_response($imageurl, 10); |
2315 | if (strpos($headers[0], '200 OK') !== false) { | 2323 | if (strpos($headers[0], '200 OK') !== false) { |
2316 | file_put_contents($GLOBALS['config']['CACHEDIR'].'/'.$thumbname,$data); // Save image to cache. | 2324 | // Save image to cache. |
2325 | file_put_contents($GLOBALS['config']['CACHEDIR'].'/' . $thumbname, $content); | ||
2317 | header('Content-Type: image/jpeg'); | 2326 | header('Content-Type: image/jpeg'); |
2318 | echo $data; | 2327 | echo $content; |
2319 | return; | 2328 | return; |
2320 | } | 2329 | } |
2321 | } | 2330 | } |
@@ -2326,16 +2335,17 @@ function genThumbnail() | |||
2326 | // This is more complex: we have to perform a HTTP request, then parse the result. | 2335 | // This is more complex: we have to perform a HTTP request, then parse the result. |
2327 | // Maybe we should deport this to JavaScript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098 | 2336 | // Maybe we should deport this to JavaScript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098 |
2328 | $vid = substr(parse_url($url,PHP_URL_PATH),1); | 2337 | $vid = substr(parse_url($url,PHP_URL_PATH),1); |
2329 | list($headers, $data) = get_http_url('https://vimeo.com/api/v2/video/'.escape($vid).'.php', 5); | 2338 | list($headers, $content) = get_http_response('https://vimeo.com/api/v2/video/'.escape($vid).'.php', 5); |
2330 | if (strpos($headers[0], '200 OK') !== false) { | 2339 | if (strpos($headers[0], '200 OK') !== false) { |
2331 | $t = unserialize($data); | 2340 | $t = unserialize($content); |
2332 | $imageurl = $t[0]['thumbnail_medium']; | 2341 | $imageurl = $t[0]['thumbnail_medium']; |
2333 | // Then we download the image and serve it to our client. | 2342 | // Then we download the image and serve it to our client. |
2334 | list($headers, $data) = get_http_url($imageurl, 10); | 2343 | list($headers, $content) = get_http_response($imageurl, 10); |
2335 | if (strpos($headers[0], '200 OK') !== false) { | 2344 | if (strpos($headers[0], '200 OK') !== false) { |
2336 | file_put_contents($GLOBALS['config']['CACHEDIR'].'/'.$thumbname,$data); // Save image to cache. | 2345 | // Save image to cache. |
2346 | file_put_contents($GLOBALS['config']['CACHEDIR'] . '/' . $thumbname, $content); | ||
2337 | header('Content-Type: image/jpeg'); | 2347 | header('Content-Type: image/jpeg'); |
2338 | echo $data; | 2348 | echo $content; |
2339 | return; | 2349 | return; |
2340 | } | 2350 | } |
2341 | } | 2351 | } |
@@ -2346,18 +2356,18 @@ function genThumbnail() | |||
2346 | // The thumbnail for TED talks is located in the <link rel="image_src" [...]> tag on that page | 2356 | // The thumbnail for TED talks is located in the <link rel="image_src" [...]> tag on that page |
2347 | // http://www.ted.com/talks/mikko_hypponen_fighting_viruses_defending_the_net.html | 2357 | // http://www.ted.com/talks/mikko_hypponen_fighting_viruses_defending_the_net.html |
2348 | // <link rel="image_src" href="http://images.ted.com/images/ted/28bced335898ba54d4441809c5b1112ffaf36781_389x292.jpg" /> | 2358 | // <link rel="image_src" href="http://images.ted.com/images/ted/28bced335898ba54d4441809c5b1112ffaf36781_389x292.jpg" /> |
2349 | list($headers, $data) = get_http_url($url, 5); | 2359 | list($headers, $content) = get_http_response($url, 5); |
2350 | if (strpos($headers[0], '200 OK') !== false) { | 2360 | if (strpos($headers[0], '200 OK') !== false) { |
2351 | // Extract the link to the thumbnail | 2361 | // Extract the link to the thumbnail |
2352 | preg_match('!link rel="image_src" href="(http://images.ted.com/images/ted/.+_\d+x\d+\.jpg)"!',$data,$matches); | 2362 | preg_match('!link rel="image_src" href="(http://images.ted.com/images/ted/.+_\d+x\d+\.jpg)"!', $content, $matches); |
2353 | if (!empty($matches[1])) | 2363 | if (!empty($matches[1])) |
2354 | { // Let's download the image. | 2364 | { // Let's download the image. |
2355 | $imageurl=$matches[1]; | 2365 | $imageurl=$matches[1]; |
2356 | // No control on image size, so wait long enough | 2366 | // No control on image size, so wait long enough |
2357 | list($headers, $data) = get_http_url($imageurl, 20); | 2367 | list($headers, $content) = get_http_response($imageurl, 20); |
2358 | if (strpos($headers[0], '200 OK') !== false) { | 2368 | if (strpos($headers[0], '200 OK') !== false) { |
2359 | $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname; | 2369 | $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname; |
2360 | file_put_contents($filepath,$data); // Save image to cache. | 2370 | file_put_contents($filepath, $content); // Save image to cache. |
2361 | if (resizeImage($filepath)) | 2371 | if (resizeImage($filepath)) |
2362 | { | 2372 | { |
2363 | header('Content-Type: image/jpeg'); | 2373 | header('Content-Type: image/jpeg'); |
@@ -2374,18 +2384,19 @@ function genThumbnail() | |||
2374 | // There is no thumbnail available for xkcd comics, so download the whole image and resize it. | 2384 | // There is no thumbnail available for xkcd comics, so download the whole image and resize it. |
2375 | // http://xkcd.com/327/ | 2385 | // http://xkcd.com/327/ |
2376 | // <img src="http://imgs.xkcd.com/comics/exploits_of_a_mom.png" title="<BLABLA>" alt="<BLABLA>" /> | 2386 | // <img src="http://imgs.xkcd.com/comics/exploits_of_a_mom.png" title="<BLABLA>" alt="<BLABLA>" /> |
2377 | list($headers, $data) = get_http_url($url, 5); | 2387 | list($headers, $content) = get_http_response($url, 5); |
2378 | if (strpos($headers[0], '200 OK') !== false) { | 2388 | if (strpos($headers[0], '200 OK') !== false) { |
2379 | // Extract the link to the thumbnail | 2389 | // Extract the link to the thumbnail |
2380 | preg_match('!<img src="(http://imgs.xkcd.com/comics/.*)" title="[^s]!',$data,$matches); | 2390 | preg_match('!<img src="(http://imgs.xkcd.com/comics/.*)" title="[^s]!', $content, $matches); |
2381 | if (!empty($matches[1])) | 2391 | if (!empty($matches[1])) |
2382 | { // Let's download the image. | 2392 | { // Let's download the image. |
2383 | $imageurl=$matches[1]; | 2393 | $imageurl=$matches[1]; |
2384 | // No control on image size, so wait long enough | 2394 | // No control on image size, so wait long enough |
2385 | list($headers, $data) = get_http_url($imageurl, 20); | 2395 | list($headers, $content) = get_http_response($imageurl, 20); |
2386 | if (strpos($headers[0], '200 OK') !== false) { | 2396 | if (strpos($headers[0], '200 OK') !== false) { |
2387 | $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname; | 2397 | $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname; |
2388 | file_put_contents($filepath,$data); // Save image to cache. | 2398 | // Save image to cache. |
2399 | file_put_contents($filepath, $content); | ||
2389 | if (resizeImage($filepath)) | 2400 | if (resizeImage($filepath)) |
2390 | { | 2401 | { |
2391 | header('Content-Type: image/jpeg'); | 2402 | header('Content-Type: image/jpeg'); |
@@ -2401,10 +2412,11 @@ function genThumbnail() | |||
2401 | { | 2412 | { |
2402 | // For all other domains, we try to download the image and make a thumbnail. | 2413 | // For all other domains, we try to download the image and make a thumbnail. |
2403 | // We allow 30 seconds max to download (and downloads are limited to 4 Mb) | 2414 | // We allow 30 seconds max to download (and downloads are limited to 4 Mb) |
2404 | list($headers, $data) = get_http_url($url, 30); | 2415 | list($headers, $content) = get_http_response($url, 30); |
2405 | if (strpos($headers[0], '200 OK') !== false) { | 2416 | if (strpos($headers[0], '200 OK') !== false) { |
2406 | $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname; | 2417 | $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname; |
2407 | file_put_contents($filepath,$data); // Save image to cache. | 2418 | // Save image to cache. |
2419 | file_put_contents($filepath, $content); | ||
2408 | if (resizeImage($filepath)) | 2420 | if (resizeImage($filepath)) |
2409 | { | 2421 | { |
2410 | header('Content-Type: image/jpeg'); | 2422 | header('Content-Type: image/jpeg'); |
diff --git a/plugins/qrcode/qrcode.css b/plugins/qrcode/qrcode.css new file mode 100644 index 00000000..0d514a0e --- /dev/null +++ b/plugins/qrcode/qrcode.css | |||
@@ -0,0 +1,23 @@ | |||
1 | .linkqrcode { | ||
2 | display: inline; | ||
3 | position: relative; | ||
4 | } | ||
5 | |||
6 | #permalinkQrcode { | ||
7 | position: absolute; | ||
8 | z-index: 200; | ||
9 | padding: 20px; | ||
10 | width: 220px; | ||
11 | height: 220px; | ||
12 | background-color: #ffffff; | ||
13 | border: 1px solid black; | ||
14 | top: -110px; | ||
15 | left: -110px; | ||
16 | text-align: center; | ||
17 | font-size: 8pt; | ||
18 | box-shadow: 2px 2px 20px 2px #333333; | ||
19 | } | ||
20 | |||
21 | #permalinkQrcode img { | ||
22 | margin-bottom: 5px; | ||
23 | } | ||
diff --git a/plugins/qrcode/qrcode.html b/plugins/qrcode/qrcode.html index 58ac5007..ffdaf3b8 100644 --- a/plugins/qrcode/qrcode.html +++ b/plugins/qrcode/qrcode.html | |||
@@ -1,3 +1,5 @@ | |||
1 | <a href="http://qrfree.kaywa.com/?l=1&s=8&d=%s" onclick="showQrCode(this); return false;" class="qrcode" data-permalink="%s"> | 1 | <div class="linkqrcode"> |
2 | <img src="%s/qrcode/qrcode.png" width="13" height="13" title="QR-Code"> | 2 | <a href="http://qrfree.kaywa.com/?l=1&s=8&d=%s" onclick="showQrCode(this); return false;" class="qrcode" data-permalink="%s"> |
3 | </a> | 3 | <img src="%s/qrcode/qrcode.png" width="13" height="13" title="QR-Code"> |
4 | </a> | ||
5 | </div> | ||
diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php index 5f6e76a2..8bc610d1 100644 --- a/plugins/qrcode/qrcode.php +++ b/plugins/qrcode/qrcode.php | |||
@@ -17,7 +17,11 @@ function hook_qrcode_render_linklist($data) | |||
17 | $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); | 17 | $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); |
18 | 18 | ||
19 | foreach ($data['links'] as &$value) { | 19 | foreach ($data['links'] as &$value) { |
20 | $qrcode = sprintf($qrcode_html, $value['real_url'], $value['real_url'], PluginManager::$PLUGINS_PATH); | 20 | $qrcode = sprintf($qrcode_html, |
21 | urlencode($value['url']), | ||
22 | $value['url'], | ||
23 | PluginManager::$PLUGINS_PATH | ||
24 | ); | ||
21 | $value['link_plugin'][] = $qrcode; | 25 | $value['link_plugin'][] = $qrcode; |
22 | } | 26 | } |
23 | 27 | ||
@@ -39,3 +43,19 @@ function hook_qrcode_render_footer($data) | |||
39 | 43 | ||
40 | return $data; | 44 | return $data; |
41 | } | 45 | } |
46 | |||
47 | /** | ||
48 | * When linklist is displayed, include qrcode CSS file. | ||
49 | * | ||
50 | * @param array $data - header data. | ||
51 | * | ||
52 | * @return mixed - header data with qrcode CSS file added. | ||
53 | */ | ||
54 | function hook_qrcode_render_includes($data) | ||
55 | { | ||
56 | if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { | ||
57 | $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.css'; | ||
58 | } | ||
59 | |||
60 | return $data; | ||
61 | } | ||
diff --git a/plugins/qrcode/shaarli-qrcode.js b/plugins/qrcode/shaarli-qrcode.js index 0a8de21d..615f54c7 100644 --- a/plugins/qrcode/shaarli-qrcode.js +++ b/plugins/qrcode/shaarli-qrcode.js | |||
@@ -19,7 +19,7 @@ function showQrCode(caller,loading) | |||
19 | 19 | ||
20 | // Build the div which contains the QR-Code: | 20 | // Build the div which contains the QR-Code: |
21 | var element = document.createElement('div'); | 21 | var element = document.createElement('div'); |
22 | element.id="permalinkQrcode"; | 22 | element.id = 'permalinkQrcode'; |
23 | 23 | ||
24 | // Make QR-Code div commit sepuku when clicked: | 24 | // Make QR-Code div commit sepuku when clicked: |
25 | if ( element.attachEvent ){ | 25 | if ( element.attachEvent ){ |
@@ -37,6 +37,12 @@ function showQrCode(caller,loading) | |||
37 | element.appendChild(image); | 37 | element.appendChild(image); |
38 | element.innerHTML += "<br>Click to close"; | 38 | element.innerHTML += "<br>Click to close"; |
39 | caller.parentNode.appendChild(element); | 39 | caller.parentNode.appendChild(element); |
40 | |||
41 | // Show the QRCode | ||
42 | qrcodeImage = document.getElementById('permalinkQrcode'); | ||
43 | // Workaround to deal with newly created element lag for transition. | ||
44 | window.getComputedStyle(qrcodeImage).opacity; | ||
45 | qrcodeImage.className = 'show'; | ||
40 | } | 46 | } |
41 | else | 47 | else |
42 | { | 48 | { |
@@ -48,7 +54,7 @@ function showQrCode(caller,loading) | |||
48 | // Remove any displayed QR-Code | 54 | // Remove any displayed QR-Code |
49 | function removeQrcode() | 55 | function removeQrcode() |
50 | { | 56 | { |
51 | var elem = document.getElementById("permalinkQrcode"); | 57 | var elem = document.getElementById('permalinkQrcode'); |
52 | if (elem) { | 58 | if (elem) { |
53 | elem.parentNode.removeChild(elem); | 59 | elem.parentNode.removeChild(elem); |
54 | } | 60 | } |
diff --git a/plugins/wallabag/README.md b/plugins/wallabag/README.md index 08e0d44a..5bc35be1 100644 --- a/plugins/wallabag/README.md +++ b/plugins/wallabag/README.md | |||
@@ -2,7 +2,8 @@ | |||
2 | 2 | ||
3 | For each link in your Shaarli, adds a button to save the target page in your [wallabag](https://www.wallabag.org/). | 3 | For each link in your Shaarli, adds a button to save the target page in your [wallabag](https://www.wallabag.org/). |
4 | 4 | ||
5 | ### Installation/configuration | 5 | ### Installation |
6 | |||
6 | Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there. | 7 | Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there. |
7 | The directory structure should look like: | 8 | The directory structure should look like: |
8 | 9 | ||
@@ -11,19 +12,31 @@ The directory structure should look like: | |||
11 | └── plugins | 12 | └── plugins |
12 |   └── wallabag | 13 |   └── wallabag |
13 |   ├── README.md | 14 |   ├── README.md |
15 | ├── config.php.dist | ||
14 |   ├── wallabag.html | 16 |   ├── wallabag.html |
17 |   ├── wallabag.php | ||
15 |   └── wallabag.png | 18 |   └── wallabag.png |
16 | ``` | 19 | ``` |
17 | 20 | ||
18 | To enable the plugin, add `'wallabag'` to your list of enabled plugins in `data/options.php` (`PLUGINS` array) | 21 | To enable the plugin, add `'wallabag'` to your list of enabled plugins in `data/options.php` (`PLUGINS` array). |
19 | . This should look like: | 22 | This should look like: |
20 | 23 | ||
21 | ``` | 24 | ``` |
22 | $GLOBALS['config']['PLUGINS'] = array('qrcode', 'any_other_plugin', 'wallabag') | 25 | $GLOBALS['config']['PLUGINS'] = array('qrcode', 'any_other_plugin', 'wallabag') |
23 | ``` | 26 | ``` |
24 | 27 | ||
25 | Then, set the `WALLABAG_URL` variable in `data/options.php` pointing to your wallabag URL. Example: | 28 | ### Configuration |
29 | |||
30 | Copy `config.php.dist` into `config.php` and setup your instance. | ||
26 | 31 | ||
32 | *Wallabag instance URL* | ||
27 | ``` | 33 | ``` |
28 | $GLOBALS['config']['WALLABAG_URL'] = 'http://demo.wallabag.org' ; //Base URL of your wallabag installation | 34 | $GLOBALS['config']['WALLABAG_URL'] = 'http://v2.wallabag.org' ; |
29 | ``` \ No newline at end of file | 35 | ``` |
36 | |||
37 | *Wallabag version*: either `1` (for 1.x) or `2` (for 2.x) | ||
38 | ``` | ||
39 | $GLOBALS['config']['WALLABAG_VERSION'] = 2; | ||
40 | ``` | ||
41 | |||
42 | > Note: these settings can also be set in `data/config.php`. \ No newline at end of file | ||
diff --git a/plugins/wallabag/WallabagInstance.php b/plugins/wallabag/WallabagInstance.php new file mode 100644 index 00000000..72cc2e5e --- /dev/null +++ b/plugins/wallabag/WallabagInstance.php | |||
@@ -0,0 +1,71 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Class WallabagInstance. | ||
5 | */ | ||
6 | class WallabagInstance | ||
7 | { | ||
8 | /** | ||
9 | * @var array Static reference to differrent WB API versions. | ||
10 | * - key: version ID, must match plugin settings. | ||
11 | * - value: version name. | ||
12 | */ | ||
13 | private static $wallabagVersions = array( | ||
14 | 1 => '1.x', | ||
15 | 2 => '2.x', | ||
16 | ); | ||
17 | |||
18 | /** | ||
19 | * @var array Static reference to WB endpoint according to the API version. | ||
20 | * - key: version name. | ||
21 | * - value: endpoint. | ||
22 | */ | ||
23 | private static $wallabagEndpoints = array( | ||
24 | '1.x' => '?plainurl=', | ||
25 | '2.x' => 'bookmarklet?url=', | ||
26 | ); | ||
27 | |||
28 | /** | ||
29 | * @var string Wallabag user instance URL. | ||
30 | */ | ||
31 | private $instanceUrl; | ||
32 | |||
33 | /** | ||
34 | * @var string Wallabag user instance API version. | ||
35 | */ | ||
36 | private $apiVersion; | ||
37 | |||
38 | function __construct($instance, $version) | ||
39 | { | ||
40 | if ($this->isVersionAllowed($version)) { | ||
41 | $this->apiVersion = self::$wallabagVersions[$version]; | ||
42 | } else { | ||
43 | // Default API version: 1.x. | ||
44 | $this->apiVersion = self::$wallabagVersions[1]; | ||
45 | } | ||
46 | |||
47 | $this->instanceUrl = add_trailing_slash($instance); | ||
48 | } | ||
49 | |||
50 | /** | ||
51 | * Build the Wallabag URL to reach from instance URL and API version endpoint. | ||
52 | * | ||
53 | * @return string wallabag url. | ||
54 | */ | ||
55 | public function getWallabagUrl() | ||
56 | { | ||
57 | return $this->instanceUrl . self::$wallabagEndpoints[$this->apiVersion]; | ||
58 | } | ||
59 | |||
60 | /** | ||
61 | * Checks version configuration. | ||
62 | * | ||
63 | * @param mixed $version given version ID. | ||
64 | * | ||
65 | * @return bool true if it's valid, false otherwise. | ||
66 | */ | ||
67 | private function isVersionAllowed($version) | ||
68 | { | ||
69 | return isset(self::$wallabagVersions[$version]); | ||
70 | } | ||
71 | } | ||
diff --git a/plugins/wallabag/config.php.dist b/plugins/wallabag/config.php.dist index 7cf0d303..a602708f 100644 --- a/plugins/wallabag/config.php.dist +++ b/plugins/wallabag/config.php.dist | |||
@@ -1,3 +1,4 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | $GLOBALS['plugins']['WALLABAG_URL'] = 'https://demo.wallabag.org/'; \ No newline at end of file | 3 | $GLOBALS['plugins']['WALLABAG_URL'] = 'https://demo.wallabag.org'; |
4 | $GLOBALS['plugins']['WALLABAG_VERSION'] = 1; \ No newline at end of file | ||
diff --git a/plugins/wallabag/wallabag.html b/plugins/wallabag/wallabag.html index ddcf8126..d0382adc 100644 --- a/plugins/wallabag/wallabag.html +++ b/plugins/wallabag/wallabag.html | |||
@@ -1 +1 @@ | |||
<span><a href="%s/?plainurl=%s" target="_blank"><img width="13" height="13" src="%s/wallabag/wallabag.png" title="Save to wallabag" /></a></span> | <span><a href="%s%s" target="_blank"><img width="13" height="13" src="%s/wallabag/wallabag.png" title="Save to wallabag" /></a></span> | ||
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php index 37969c97..e3c399a9 100644 --- a/plugins/wallabag/wallabag.php +++ b/plugins/wallabag/wallabag.php | |||
@@ -4,6 +4,8 @@ | |||
4 | * Plugin Wallabag. | 4 | * Plugin Wallabag. |
5 | */ | 5 | */ |
6 | 6 | ||
7 | require_once 'WallabagInstance.php'; | ||
8 | |||
7 | // don't raise unnecessary warnings | 9 | // don't raise unnecessary warnings |
8 | if (is_file(PluginManager::$PLUGINS_PATH . '/wallabag/config.php')) { | 10 | if (is_file(PluginManager::$PLUGINS_PATH . '/wallabag/config.php')) { |
9 | include PluginManager::$PLUGINS_PATH . '/wallabag/config.php'; | 11 | include PluginManager::$PLUGINS_PATH . '/wallabag/config.php'; |
@@ -28,12 +30,23 @@ function hook_wallabag_render_linklist($data) | |||
28 | return $data; | 30 | return $data; |
29 | } | 31 | } |
30 | 32 | ||
31 | $wallabag_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); | 33 | $version = isset($GLOBALS['plugins']['WALLABAG_VERSION']) |
34 | ? $GLOBALS['plugins']['WALLABAG_VERSION'] | ||
35 | : ''; | ||
36 | $wallabagInstance = new WallabagInstance($GLOBALS['plugins']['WALLABAG_URL'], $version); | ||
37 | |||
38 | $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); | ||
32 | 39 | ||
33 | foreach ($data['links'] as &$value) { | 40 | foreach ($data['links'] as &$value) { |
34 | $wallabag = sprintf($wallabag_html, $GLOBALS['plugins']['WALLABAG_URL'], $value['url'], PluginManager::$PLUGINS_PATH); | 41 | $wallabag = sprintf( |
42 | $wallabagHtml, | ||
43 | $wallabagInstance->getWallabagUrl(), | ||
44 | urlencode($value['url']), | ||
45 | PluginManager::$PLUGINS_PATH | ||
46 | ); | ||
35 | $value['link_plugin'][] = $wallabag; | 47 | $value['link_plugin'][] = $wallabag; |
36 | } | 48 | } |
37 | 49 | ||
38 | return $data; | 50 | return $data; |
39 | } | 51 | } |
52 | |||
diff --git a/shaarli_version.php b/shaarli_version.php index 11ad87d7..fe5f3896 100644 --- a/shaarli_version.php +++ b/shaarli_version.php | |||
@@ -1 +1 @@ | |||
<?php /* 0.6.1 */ ?> | <?php /* 0.6.2 */ ?> | ||
diff --git a/tests/HttpUtils/GetHttpUrlTest.php b/tests/HttpUtils/GetHttpUrlTest.php index 76092b80..fd293505 100644 --- a/tests/HttpUtils/GetHttpUrlTest.php +++ b/tests/HttpUtils/GetHttpUrlTest.php | |||
@@ -6,7 +6,7 @@ | |||
6 | require_once 'application/HttpUtils.php'; | 6 | require_once 'application/HttpUtils.php'; |
7 | 7 | ||
8 | /** | 8 | /** |
9 | * Unitary tests for get_http_url() | 9 | * Unitary tests for get_http_response() |
10 | */ | 10 | */ |
11 | class GetHttpUrlTest extends PHPUnit_Framework_TestCase | 11 | class GetHttpUrlTest extends PHPUnit_Framework_TestCase |
12 | { | 12 | { |
@@ -15,12 +15,15 @@ class GetHttpUrlTest extends PHPUnit_Framework_TestCase | |||
15 | */ | 15 | */ |
16 | public function testGetInvalidLocalUrl() | 16 | public function testGetInvalidLocalUrl() |
17 | { | 17 | { |
18 | list($headers, $content) = get_http_url('/non/existent', 1); | 18 | // Local |
19 | $this->assertEquals('HTTP Error', $headers[0]); | 19 | list($headers, $content) = get_http_response('/non/existent', 1); |
20 | $this->assertRegexp( | 20 | $this->assertEquals('Invalid HTTP Url', $headers[0]); |
21 | '/failed to open stream: No such file or directory/', | 21 | $this->assertFalse($content); |
22 | $content | 22 | |
23 | ); | 23 | // Non HTTP |
24 | list($headers, $content) = get_http_response('ftp://save.tld/mysave', 1); | ||
25 | $this->assertEquals('Invalid HTTP Url', $headers[0]); | ||
26 | $this->assertFalse($content); | ||
24 | } | 27 | } |
25 | 28 | ||
26 | /** | 29 | /** |
@@ -28,11 +31,8 @@ class GetHttpUrlTest extends PHPUnit_Framework_TestCase | |||
28 | */ | 31 | */ |
29 | public function testGetInvalidRemoteUrl() | 32 | public function testGetInvalidRemoteUrl() |
30 | { | 33 | { |
31 | list($headers, $content) = get_http_url('http://non.existent', 1); | 34 | list($headers, $content) = @get_http_response('http://non.existent', 1); |
32 | $this->assertEquals('HTTP Error', $headers[0]); | 35 | $this->assertFalse($headers); |
33 | $this->assertRegexp( | 36 | $this->assertFalse($content); |
34 | '/Name or service not known/', | ||
35 | $content | ||
36 | ); | ||
37 | } | 37 | } |
38 | } | 38 | } |
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 7b22b270..3b1a2057 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php | |||
@@ -302,236 +302,49 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
302 | } | 302 | } |
303 | 303 | ||
304 | /** | 304 | /** |
305 | * Filter links using a tag | 305 | * Test real_url without redirector. |
306 | */ | ||
307 | public function testFilterOneTag() | ||
308 | { | ||
309 | $this->assertEquals( | ||
310 | 3, | ||
311 | sizeof(self::$publicLinkDB->filterTags('web', false)) | ||
312 | ); | ||
313 | |||
314 | $this->assertEquals( | ||
315 | 4, | ||
316 | sizeof(self::$privateLinkDB->filterTags('web', false)) | ||
317 | ); | ||
318 | } | ||
319 | |||
320 | /** | ||
321 | * Filter links using a tag - case-sensitive | ||
322 | */ | ||
323 | public function testFilterCaseSensitiveTag() | ||
324 | { | ||
325 | $this->assertEquals( | ||
326 | 0, | ||
327 | sizeof(self::$privateLinkDB->filterTags('mercurial', true)) | ||
328 | ); | ||
329 | |||
330 | $this->assertEquals( | ||
331 | 1, | ||
332 | sizeof(self::$privateLinkDB->filterTags('Mercurial', true)) | ||
333 | ); | ||
334 | } | ||
335 | |||
336 | /** | ||
337 | * Filter links using a tag combination | ||
338 | */ | ||
339 | public function testFilterMultipleTags() | ||
340 | { | ||
341 | $this->assertEquals( | ||
342 | 1, | ||
343 | sizeof(self::$publicLinkDB->filterTags('dev cartoon', false)) | ||
344 | ); | ||
345 | |||
346 | $this->assertEquals( | ||
347 | 2, | ||
348 | sizeof(self::$privateLinkDB->filterTags('dev cartoon', false)) | ||
349 | ); | ||
350 | } | ||
351 | |||
352 | /** | ||
353 | * Filter links using a non-existent tag | ||
354 | */ | ||
355 | public function testFilterUnknownTag() | ||
356 | { | ||
357 | $this->assertEquals( | ||
358 | 0, | ||
359 | sizeof(self::$publicLinkDB->filterTags('null', false)) | ||
360 | ); | ||
361 | } | ||
362 | |||
363 | /** | ||
364 | * Return links for a given day | ||
365 | */ | ||
366 | public function testFilterDay() | ||
367 | { | ||
368 | $this->assertEquals( | ||
369 | 2, | ||
370 | sizeof(self::$publicLinkDB->filterDay('20121206')) | ||
371 | ); | ||
372 | |||
373 | $this->assertEquals( | ||
374 | 3, | ||
375 | sizeof(self::$privateLinkDB->filterDay('20121206')) | ||
376 | ); | ||
377 | } | ||
378 | |||
379 | /** | ||
380 | * 404 - day not found | ||
381 | */ | ||
382 | public function testFilterUnknownDay() | ||
383 | { | ||
384 | $this->assertEquals( | ||
385 | 0, | ||
386 | sizeof(self::$publicLinkDB->filterDay('19700101')) | ||
387 | ); | ||
388 | |||
389 | $this->assertEquals( | ||
390 | 0, | ||
391 | sizeof(self::$privateLinkDB->filterDay('19700101')) | ||
392 | ); | ||
393 | } | ||
394 | |||
395 | /** | ||
396 | * Use an invalid date format | ||
397 | * @expectedException Exception | ||
398 | * @expectedExceptionMessageRegExp /Invalid date format/ | ||
399 | */ | ||
400 | public function testFilterInvalidDayWithChars() | ||
401 | { | ||
402 | self::$privateLinkDB->filterDay('Rainy day, dream away'); | ||
403 | } | ||
404 | |||
405 | /** | ||
406 | * Use an invalid date format | ||
407 | * @expectedException Exception | ||
408 | * @expectedExceptionMessageRegExp /Invalid date format/ | ||
409 | */ | ||
410 | public function testFilterInvalidDayDigits() | ||
411 | { | ||
412 | self::$privateLinkDB->filterDay('20'); | ||
413 | } | ||
414 | |||
415 | /** | ||
416 | * Retrieve a link entry with its hash | ||
417 | */ | ||
418 | public function testFilterSmallHash() | ||
419 | { | ||
420 | $links = self::$privateLinkDB->filterSmallHash('IuWvgA'); | ||
421 | |||
422 | $this->assertEquals( | ||
423 | 1, | ||
424 | sizeof($links) | ||
425 | ); | ||
426 | |||
427 | $this->assertEquals( | ||
428 | 'MediaGoblin', | ||
429 | $links['20130614_184135']['title'] | ||
430 | ); | ||
431 | |||
432 | } | ||
433 | |||
434 | /** | ||
435 | * No link for this hash | ||
436 | */ | ||
437 | public function testFilterUnknownSmallHash() | ||
438 | { | ||
439 | $this->assertEquals( | ||
440 | 0, | ||
441 | sizeof(self::$privateLinkDB->filterSmallHash('Iblaah')) | ||
442 | ); | ||
443 | } | ||
444 | |||
445 | /** | ||
446 | * Full-text search - result from a link's URL | ||
447 | */ | ||
448 | public function testFilterFullTextURL() | ||
449 | { | ||
450 | $this->assertEquals( | ||
451 | 2, | ||
452 | sizeof(self::$publicLinkDB->filterFullText('ars.userfriendly.org')) | ||
453 | ); | ||
454 | } | ||
455 | |||
456 | /** | ||
457 | * Full-text search - result from a link's title only | ||
458 | */ | 306 | */ |
459 | public function testFilterFullTextTitle() | 307 | public function testLinkRealUrlWithoutRedirector() |
460 | { | 308 | { |
461 | // use miscellaneous cases | 309 | $db = new LinkDB(self::$testDatastore, false, false); |
462 | $this->assertEquals( | 310 | foreach($db as $link) { |
463 | 2, | 311 | $this->assertEquals($link['url'], $link['real_url']); |
464 | sizeof(self::$publicLinkDB->filterFullText('userfriendly -')) | 312 | } |
465 | ); | ||
466 | $this->assertEquals( | ||
467 | 2, | ||
468 | sizeof(self::$publicLinkDB->filterFullText('UserFriendly -')) | ||
469 | ); | ||
470 | $this->assertEquals( | ||
471 | 2, | ||
472 | sizeof(self::$publicLinkDB->filterFullText('uSeRFrIendlY -')) | ||
473 | ); | ||
474 | |||
475 | // use miscellaneous case and offset | ||
476 | $this->assertEquals( | ||
477 | 2, | ||
478 | sizeof(self::$publicLinkDB->filterFullText('RFrIendL')) | ||
479 | ); | ||
480 | } | 313 | } |
481 | 314 | ||
482 | /** | 315 | /** |
483 | * Full-text search - result from the link's description only | 316 | * Test real_url with redirector. |
484 | */ | 317 | */ |
485 | public function testFilterFullTextDescription() | 318 | public function testLinkRealUrlWithRedirector() |
486 | { | 319 | { |
487 | $this->assertEquals( | 320 | $redirector = 'http://redirector.to?'; |
488 | 1, | 321 | $db = new LinkDB(self::$testDatastore, false, false, $redirector); |
489 | sizeof(self::$publicLinkDB->filterFullText('media publishing')) | 322 | foreach($db as $link) { |
490 | ); | 323 | $this->assertStringStartsWith($redirector, $link['real_url']); |
324 | } | ||
491 | } | 325 | } |
492 | 326 | ||
493 | /** | 327 | /** |
494 | * Full-text search - result from the link's tags only | 328 | * Test filter with string. |
495 | */ | 329 | */ |
496 | public function testFilterFullTextTags() | 330 | public function testFilterString() |
497 | { | 331 | { |
332 | $tags = 'dev cartoon'; | ||
498 | $this->assertEquals( | 333 | $this->assertEquals( |
499 | 2, | 334 | 2, |
500 | sizeof(self::$publicLinkDB->filterFullText('gnu')) | 335 | count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) |
501 | ); | 336 | ); |
502 | } | 337 | } |
503 | 338 | ||
504 | /** | 339 | /** |
505 | * Full-text search - result set from mixed sources | 340 | * Test filter with string. |
506 | */ | 341 | */ |
507 | public function testFilterFullTextMixed() | 342 | public function testFilterArray() |
508 | { | 343 | { |
344 | $tags = array('dev', 'cartoon'); | ||
509 | $this->assertEquals( | 345 | $this->assertEquals( |
510 | 2, | 346 | 2, |
511 | sizeof(self::$publicLinkDB->filterFullText('free software')) | 347 | count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) |
512 | ); | 348 | ); |
513 | } | 349 | } |
514 | |||
515 | /** | ||
516 | * Test real_url without redirector. | ||
517 | */ | ||
518 | public function testLinkRealUrlWithoutRedirector() | ||
519 | { | ||
520 | $db = new LinkDB(self::$testDatastore, false, false); | ||
521 | foreach($db as $link) { | ||
522 | $this->assertEquals($link['url'], $link['real_url']); | ||
523 | } | ||
524 | } | ||
525 | |||
526 | /** | ||
527 | * Test real_url with redirector. | ||
528 | */ | ||
529 | public function testLinkRealUrlWithRedirector() | ||
530 | { | ||
531 | $redirector = 'http://redirector.to?'; | ||
532 | $db = new LinkDB(self::$testDatastore, false, false, $redirector); | ||
533 | foreach($db as $link) { | ||
534 | $this->assertStringStartsWith($redirector, $link['real_url']); | ||
535 | } | ||
536 | } | ||
537 | } | 350 | } |
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php new file mode 100644 index 00000000..5107ab72 --- /dev/null +++ b/tests/LinkFilterTest.php | |||
@@ -0,0 +1,242 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'application/LinkFilter.php'; | ||
4 | |||
5 | /** | ||
6 | * Class LinkFilterTest. | ||
7 | */ | ||
8 | class LinkFilterTest extends PHPUnit_Framework_TestCase | ||
9 | { | ||
10 | /** | ||
11 | * @var LinkFilter instance. | ||
12 | */ | ||
13 | protected static $linkFilter; | ||
14 | |||
15 | /** | ||
16 | * Instanciate linkFilter with ReferenceLinkDB data. | ||
17 | */ | ||
18 | public static function setUpBeforeClass() | ||
19 | { | ||
20 | $refDB = new ReferenceLinkDB(); | ||
21 | self::$linkFilter = new LinkFilter($refDB->getLinks()); | ||
22 | } | ||
23 | |||
24 | /** | ||
25 | * Blank filter. | ||
26 | */ | ||
27 | public function testFilter() | ||
28 | { | ||
29 | $this->assertEquals( | ||
30 | 6, | ||
31 | count(self::$linkFilter->filter('', '')) | ||
32 | ); | ||
33 | |||
34 | // Private only. | ||
35 | $this->assertEquals( | ||
36 | 2, | ||
37 | count(self::$linkFilter->filter('', '', false, true)) | ||
38 | ); | ||
39 | } | ||
40 | |||
41 | /** | ||
42 | * Filter links using a tag | ||
43 | */ | ||
44 | public function testFilterOneTag() | ||
45 | { | ||
46 | $this->assertEquals( | ||
47 | 4, | ||
48 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false)) | ||
49 | ); | ||
50 | |||
51 | // Private only. | ||
52 | $this->assertEquals( | ||
53 | 1, | ||
54 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, true)) | ||
55 | ); | ||
56 | } | ||
57 | |||
58 | /** | ||
59 | * Filter links using a tag - case-sensitive | ||
60 | */ | ||
61 | public function testFilterCaseSensitiveTag() | ||
62 | { | ||
63 | $this->assertEquals( | ||
64 | 0, | ||
65 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'mercurial', true)) | ||
66 | ); | ||
67 | |||
68 | $this->assertEquals( | ||
69 | 1, | ||
70 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'Mercurial', true)) | ||
71 | ); | ||
72 | } | ||
73 | |||
74 | /** | ||
75 | * Filter links using a tag combination | ||
76 | */ | ||
77 | public function testFilterMultipleTags() | ||
78 | { | ||
79 | $this->assertEquals( | ||
80 | 2, | ||
81 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'dev cartoon', false)) | ||
82 | ); | ||
83 | } | ||
84 | |||
85 | /** | ||
86 | * Filter links using a non-existent tag | ||
87 | */ | ||
88 | public function testFilterUnknownTag() | ||
89 | { | ||
90 | $this->assertEquals( | ||
91 | 0, | ||
92 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'null', false)) | ||
93 | ); | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * Return links for a given day | ||
98 | */ | ||
99 | public function testFilterDay() | ||
100 | { | ||
101 | $this->assertEquals( | ||
102 | 3, | ||
103 | count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206')) | ||
104 | ); | ||
105 | } | ||
106 | |||
107 | /** | ||
108 | * 404 - day not found | ||
109 | */ | ||
110 | public function testFilterUnknownDay() | ||
111 | { | ||
112 | $this->assertEquals( | ||
113 | 0, | ||
114 | count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '19700101')) | ||
115 | ); | ||
116 | } | ||
117 | |||
118 | /** | ||
119 | * Use an invalid date format | ||
120 | * @expectedException Exception | ||
121 | * @expectedExceptionMessageRegExp /Invalid date format/ | ||
122 | */ | ||
123 | public function testFilterInvalidDayWithChars() | ||
124 | { | ||
125 | self::$linkFilter->filter(LinkFilter::$FILTER_DAY, 'Rainy day, dream away'); | ||
126 | } | ||
127 | |||
128 | /** | ||
129 | * Use an invalid date format | ||
130 | * @expectedException Exception | ||
131 | * @expectedExceptionMessageRegExp /Invalid date format/ | ||
132 | */ | ||
133 | public function testFilterInvalidDayDigits() | ||
134 | { | ||
135 | self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20'); | ||
136 | } | ||
137 | |||
138 | /** | ||
139 | * Retrieve a link entry with its hash | ||
140 | */ | ||
141 | public function testFilterSmallHash() | ||
142 | { | ||
143 | $links = self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'IuWvgA'); | ||
144 | |||
145 | $this->assertEquals( | ||
146 | 1, | ||
147 | count($links) | ||
148 | ); | ||
149 | |||
150 | $this->assertEquals( | ||
151 | 'MediaGoblin', | ||
152 | $links['20130614_184135']['title'] | ||
153 | ); | ||
154 | } | ||
155 | |||
156 | /** | ||
157 | * No link for this hash | ||
158 | */ | ||
159 | public function testFilterUnknownSmallHash() | ||
160 | { | ||
161 | $this->assertEquals( | ||
162 | 0, | ||
163 | count(self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah')) | ||
164 | ); | ||
165 | } | ||
166 | |||
167 | /** | ||
168 | * Full-text search - result from a link's URL | ||
169 | */ | ||
170 | public function testFilterFullTextURL() | ||
171 | { | ||
172 | $this->assertEquals( | ||
173 | 2, | ||
174 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars.userfriendly.org')) | ||
175 | ); | ||
176 | } | ||
177 | |||
178 | /** | ||
179 | * Full-text search - result from a link's title only | ||
180 | */ | ||
181 | public function testFilterFullTextTitle() | ||
182 | { | ||
183 | // use miscellaneous cases | ||
184 | $this->assertEquals( | ||
185 | 2, | ||
186 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'userfriendly -')) | ||
187 | ); | ||
188 | $this->assertEquals( | ||
189 | 2, | ||
190 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'UserFriendly -')) | ||
191 | ); | ||
192 | $this->assertEquals( | ||
193 | 2, | ||
194 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -')) | ||
195 | ); | ||
196 | |||
197 | // use miscellaneous case and offset | ||
198 | $this->assertEquals( | ||
199 | 2, | ||
200 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'RFrIendL')) | ||
201 | ); | ||
202 | } | ||
203 | |||
204 | /** | ||
205 | * Full-text search - result from the link's description only | ||
206 | */ | ||
207 | public function testFilterFullTextDescription() | ||
208 | { | ||
209 | $this->assertEquals( | ||
210 | 1, | ||
211 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'media publishing')) | ||
212 | ); | ||
213 | } | ||
214 | |||
215 | /** | ||
216 | * Full-text search - result from the link's tags only | ||
217 | */ | ||
218 | public function testFilterFullTextTags() | ||
219 | { | ||
220 | $this->assertEquals( | ||
221 | 2, | ||
222 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'gnu')) | ||
223 | ); | ||
224 | |||
225 | // Private only. | ||
226 | $this->assertEquals( | ||
227 | 1, | ||
228 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, true)) | ||
229 | ); | ||
230 | } | ||
231 | |||
232 | /** | ||
233 | * Full-text search - result set from mixed sources | ||
234 | */ | ||
235 | public function testFilterFullTextMixed() | ||
236 | { | ||
237 | $this->assertEquals( | ||
238 | 2, | ||
239 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software')) | ||
240 | ); | ||
241 | } | ||
242 | } | ||
diff --git a/tests/LinkUtilsTest.php b/tests/LinkUtilsTest.php new file mode 100644 index 00000000..c2257590 --- /dev/null +++ b/tests/LinkUtilsTest.php | |||
@@ -0,0 +1,85 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'application/LinkUtils.php'; | ||
4 | |||
5 | /** | ||
6 | * Class LinkUtilsTest. | ||
7 | */ | ||
8 | class LinkUtilsTest extends PHPUnit_Framework_TestCase | ||
9 | { | ||
10 | /** | ||
11 | * Test html_extract_title() when the title is found. | ||
12 | */ | ||
13 | public function testHtmlExtractExistentTitle() | ||
14 | { | ||
15 | $title = 'Read me please.'; | ||
16 | $html = '<html><meta>stuff</meta><title>'. $title .'</title></html>'; | ||
17 | $this->assertEquals($title, html_extract_title($html)); | ||
18 | } | ||
19 | |||
20 | /** | ||
21 | * Test html_extract_title() when the title is not found. | ||
22 | */ | ||
23 | public function testHtmlExtractNonExistentTitle() | ||
24 | { | ||
25 | $html = '<html><meta>stuff</meta></html>'; | ||
26 | $this->assertFalse(html_extract_title($html)); | ||
27 | } | ||
28 | |||
29 | /** | ||
30 | * Test get_charset() with all priorities. | ||
31 | */ | ||
32 | public function testGetCharset() | ||
33 | { | ||
34 | $headers = array('Content-Type' => 'text/html; charset=Headers'); | ||
35 | $html = '<html><meta>stuff</meta><meta charset="Html"/></html>'; | ||
36 | $default = 'default'; | ||
37 | $this->assertEquals('headers', get_charset($headers, $html, $default)); | ||
38 | $this->assertEquals('html', get_charset(array(), $html, $default)); | ||
39 | $this->assertEquals($default, get_charset(array(), '', $default)); | ||
40 | $this->assertEquals('utf-8', get_charset(array(), '')); | ||
41 | } | ||
42 | |||
43 | /** | ||
44 | * Test headers_extract_charset() when the charset is found. | ||
45 | */ | ||
46 | public function testHeadersExtractExistentCharset() | ||
47 | { | ||
48 | $charset = 'x-MacCroatian'; | ||
49 | $headers = array('Content-Type' => 'text/html; charset='. $charset); | ||
50 | $this->assertEquals(strtolower($charset), headers_extract_charset($headers)); | ||
51 | } | ||
52 | |||
53 | /** | ||
54 | * Test headers_extract_charset() when the charset is not found. | ||
55 | */ | ||
56 | public function testHeadersExtractNonExistentCharset() | ||
57 | { | ||
58 | $headers = array(); | ||
59 | $this->assertFalse(headers_extract_charset($headers)); | ||
60 | |||
61 | $headers = array('Content-Type' => 'text/html'); | ||
62 | $this->assertFalse(headers_extract_charset($headers)); | ||
63 | } | ||
64 | |||
65 | /** | ||
66 | * Test html_extract_charset() when the charset is found. | ||
67 | */ | ||
68 | public function testHtmlExtractExistentCharset() | ||
69 | { | ||
70 | $charset = 'x-MacCroatian'; | ||
71 | $html = '<html><meta>stuff2</meta><meta charset="'. $charset .'"/></html>'; | ||
72 | $this->assertEquals(strtolower($charset), html_extract_charset($html)); | ||
73 | } | ||
74 | |||
75 | /** | ||
76 | * Test html_extract_charset() when the charset is not found. | ||
77 | */ | ||
78 | public function testHtmlExtractNonExistentCharset() | ||
79 | { | ||
80 | $html = '<html><meta>stuff</meta></html>'; | ||
81 | $this->assertFalse(html_extract_charset($html)); | ||
82 | $html = '<html><meta>stuff</meta><meta charset=""/></html>'; | ||
83 | $this->assertFalse(html_extract_charset($html)); | ||
84 | } | ||
85 | } | ||
diff --git a/tests/Url/UrlTest.php b/tests/Url/UrlTest.php index e498d79e..425327ed 100644 --- a/tests/Url/UrlTest.php +++ b/tests/Url/UrlTest.php | |||
@@ -145,4 +145,33 @@ class UrlTest extends PHPUnit_Framework_TestCase | |||
145 | $url = new Url('git://domain.tld/push?pull=clone#checkout'); | 145 | $url = new Url('git://domain.tld/push?pull=clone#checkout'); |
146 | $this->assertEquals('git', $url->getScheme()); | 146 | $this->assertEquals('git', $url->getScheme()); |
147 | } | 147 | } |
148 | |||
149 | /** | ||
150 | * Test add trailing slash. | ||
151 | */ | ||
152 | function testAddTrailingSlash() | ||
153 | { | ||
154 | $strOn = 'http://randomstr.com/test/'; | ||
155 | $strOff = 'http://randomstr.com/test'; | ||
156 | $this->assertEquals($strOn, add_trailing_slash($strOn)); | ||
157 | $this->assertEquals($strOn, add_trailing_slash($strOff)); | ||
158 | } | ||
159 | |||
160 | /** | ||
161 | * Test valid HTTP url. | ||
162 | */ | ||
163 | function testUrlIsHttp() | ||
164 | { | ||
165 | $url = new Url(self::$baseUrl); | ||
166 | $this->assertTrue($url->isHttp()); | ||
167 | } | ||
168 | |||
169 | /** | ||
170 | * Test non HTTP url. | ||
171 | */ | ||
172 | function testUrlIsNotHttp() | ||
173 | { | ||
174 | $url = new Url('ftp://save.tld/mysave'); | ||
175 | $this->assertFalse($url->isHttp()); | ||
176 | } | ||
148 | } | 177 | } |
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 02eecda2..3073b5eb 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php | |||
@@ -18,6 +18,13 @@ class UtilsTest extends PHPUnit_Framework_TestCase | |||
18 | // Session ID hashes | 18 | // Session ID hashes |
19 | protected static $sidHashes = null; | 19 | protected static $sidHashes = null; |
20 | 20 | ||
21 | // Log file | ||
22 | protected static $testLogFile = 'tests.log'; | ||
23 | |||
24 | // Expected log date format | ||
25 | protected static $dateFormat = 'Y/m/d H:i:s'; | ||
26 | |||
27 | |||
21 | /** | 28 | /** |
22 | * Assign reference data | 29 | * Assign reference data |
23 | */ | 30 | */ |
@@ -27,6 +34,65 @@ class UtilsTest extends PHPUnit_Framework_TestCase | |||
27 | } | 34 | } |
28 | 35 | ||
29 | /** | 36 | /** |
37 | * Resets test data before each test | ||
38 | */ | ||
39 | protected function setUp() | ||
40 | { | ||
41 | if (file_exists(self::$testLogFile)) { | ||
42 | unlink(self::$testLogFile); | ||
43 | } | ||
44 | } | ||
45 | |||
46 | /** | ||
47 | * Returns a list of the elements from the last logged entry | ||
48 | * | ||
49 | * @return list (date, ip address, message) | ||
50 | */ | ||
51 | protected function getLastLogEntry() | ||
52 | { | ||
53 | $logFile = file(self::$testLogFile); | ||
54 | return explode(' - ', trim(array_pop($logFile), PHP_EOL)); | ||
55 | } | ||
56 | |||
57 | /** | ||
58 | * Log a message to a file - IPv4 client address | ||
59 | */ | ||
60 | public function testLogmIp4() | ||
61 | { | ||
62 | $logMessage = 'IPv4 client connected'; | ||
63 | logm(self::$testLogFile, '127.0.0.1', $logMessage); | ||
64 | list($date, $ip, $message) = $this->getLastLogEntry(); | ||
65 | |||
66 | $this->assertInstanceOf( | ||
67 | 'DateTime', | ||
68 | DateTime::createFromFormat(self::$dateFormat, $date) | ||
69 | ); | ||
70 | $this->assertTrue( | ||
71 | filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false | ||
72 | ); | ||
73 | $this->assertEquals($logMessage, $message); | ||
74 | } | ||
75 | |||
76 | /** | ||
77 | * Log a message to a file - IPv6 client address | ||
78 | */ | ||
79 | public function testLogmIp6() | ||
80 | { | ||
81 | $logMessage = 'IPv6 client connected'; | ||
82 | logm(self::$testLogFile, '2001:db8::ff00:42:8329', $logMessage); | ||
83 | list($date, $ip, $message) = $this->getLastLogEntry(); | ||
84 | |||
85 | $this->assertInstanceOf( | ||
86 | 'DateTime', | ||
87 | DateTime::createFromFormat(self::$dateFormat, $date) | ||
88 | ); | ||
89 | $this->assertTrue( | ||
90 | filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false | ||
91 | ); | ||
92 | $this->assertEquals($logMessage, $message); | ||
93 | } | ||
94 | |||
95 | /** | ||
30 | * Represent a link by its hash | 96 | * Represent a link by its hash |
31 | */ | 97 | */ |
32 | public function testSmallHash() | 98 | public function testSmallHash() |
diff --git a/tests/plugins/PlugQrcodeTest.php b/tests/plugins/PlugQrcodeTest.php index c749fa86..86dc7f29 100644 --- a/tests/plugins/PlugQrcodeTest.php +++ b/tests/plugins/PlugQrcodeTest.php | |||
@@ -30,7 +30,7 @@ class PlugQrcodeTest extends PHPUnit_Framework_TestCase | |||
30 | 'title' => $str, | 30 | 'title' => $str, |
31 | 'links' => array( | 31 | 'links' => array( |
32 | array( | 32 | array( |
33 | 'real_url' => $str, | 33 | 'url' => $str, |
34 | ) | 34 | ) |
35 | ) | 35 | ) |
36 | ); | 36 | ); |
@@ -39,7 +39,7 @@ class PlugQrcodeTest extends PHPUnit_Framework_TestCase | |||
39 | $link = $data['links'][0]; | 39 | $link = $data['links'][0]; |
40 | // data shouldn't be altered | 40 | // data shouldn't be altered |
41 | $this->assertEquals($str, $data['title']); | 41 | $this->assertEquals($str, $data['title']); |
42 | $this->assertEquals($str, $link['real_url']); | 42 | $this->assertEquals($str, $link['url']); |
43 | 43 | ||
44 | // plugin data | 44 | // plugin data |
45 | $this->assertEquals(1, count($link['link_plugin'])); | 45 | $this->assertEquals(1, count($link['link_plugin'])); |
diff --git a/tests/plugins/PluginWallabagTest.php b/tests/plugins/PluginWallabagTest.php index 7cc83f4f..5d3a60e0 100644 --- a/tests/plugins/PluginWallabagTest.php +++ b/tests/plugins/PluginWallabagTest.php | |||
@@ -44,6 +44,8 @@ class PluginWallabagTest extends PHPUnit_Framework_TestCase | |||
44 | 44 | ||
45 | // plugin data | 45 | // plugin data |
46 | $this->assertEquals(1, count($link['link_plugin'])); | 46 | $this->assertEquals(1, count($link['link_plugin'])); |
47 | $this->assertNotFalse(strpos($link['link_plugin'][0], $str)); | 47 | $this->assertNotFalse(strpos($link['link_plugin'][0], urlencode($str))); |
48 | $this->assertNotFalse(strpos($link['link_plugin'][0], $GLOBALS['plugins']['WALLABAG_URL'])); | ||
48 | } | 49 | } |
49 | } | 50 | } |
51 | |||
diff --git a/tests/plugins/WallabagInstanceTest.php b/tests/plugins/WallabagInstanceTest.php new file mode 100644 index 00000000..7c14c1df --- /dev/null +++ b/tests/plugins/WallabagInstanceTest.php | |||
@@ -0,0 +1,60 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'plugins/wallabag/WallabagInstance.php'; | ||
4 | |||
5 | /** | ||
6 | * Class WallabagInstanceTest | ||
7 | */ | ||
8 | class WallabagInstanceTest extends PHPUnit_Framework_TestCase | ||
9 | { | ||
10 | /** | ||
11 | * @var string wallabag url. | ||
12 | */ | ||
13 | private $instance; | ||
14 | |||
15 | /** | ||
16 | * Reset plugin path | ||
17 | */ | ||
18 | function setUp() | ||
19 | { | ||
20 | $this->instance = 'http://some.url'; | ||
21 | } | ||
22 | |||
23 | /** | ||
24 | * Test WallabagInstance with API V1. | ||
25 | */ | ||
26 | function testWallabagInstanceV1() | ||
27 | { | ||
28 | $instance = new WallabagInstance($this->instance, 1); | ||
29 | $expected = $this->instance . '/?plainurl='; | ||
30 | $result = $instance->getWallabagUrl(); | ||
31 | $this->assertEquals($expected, $result); | ||
32 | } | ||
33 | |||
34 | /** | ||
35 | * Test WallabagInstance with API V2. | ||
36 | */ | ||
37 | function testWallabagInstanceV2() | ||
38 | { | ||
39 | $instance = new WallabagInstance($this->instance, 2); | ||
40 | $expected = $this->instance . '/bookmarklet?url='; | ||
41 | $result = $instance->getWallabagUrl(); | ||
42 | $this->assertEquals($expected, $result); | ||
43 | } | ||
44 | |||
45 | /** | ||
46 | * Test WallabagInstance with an invalid API version. | ||
47 | */ | ||
48 | function testWallabagInstanceInvalidVersion() | ||
49 | { | ||
50 | $instance = new WallabagInstance($this->instance, false); | ||
51 | $expected = $this->instance . '/?plainurl='; | ||
52 | $result = $instance->getWallabagUrl(); | ||
53 | $this->assertEquals($expected, $result); | ||
54 | |||
55 | $instance = new WallabagInstance($this->instance, 3); | ||
56 | $expected = $this->instance . '/?plainurl='; | ||
57 | $result = $instance->getWallabagUrl(); | ||
58 | $this->assertEquals($expected, $result); | ||
59 | } | ||
60 | } | ||
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index 47b51829..011317ef 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php | |||
@@ -124,4 +124,9 @@ class ReferenceLinkDB | |||
124 | { | 124 | { |
125 | return $this->_privateCount; | 125 | return $this->_privateCount; |
126 | } | 126 | } |
127 | |||
128 | public function getLinks() | ||
129 | { | ||
130 | return $this->_links; | ||
131 | } | ||
127 | } | 132 | } |
diff --git a/tpl/404.html b/tpl/404.html new file mode 100644 index 00000000..53e98e2e --- /dev/null +++ b/tpl/404.html | |||
@@ -0,0 +1,17 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html> | ||
3 | <head> | ||
4 | {include="includes"} | ||
5 | </head> | ||
6 | <body> | ||
7 | <div id="pageheader"> | ||
8 | {include="page.header"} | ||
9 | </div> | ||
10 | <div class="error-container"> | ||
11 | <h1>404 Not found <small>Oh crap!</small></h1> | ||
12 | <p>{$error_message}</p> | ||
13 | <p>Would you mind <a href="?">clicking here</a>?</p> | ||
14 | </div> | ||
15 | {include="page.footer"} | ||
16 | </body> | ||
17 | </html> | ||
diff --git a/tpl/linklist.html b/tpl/linklist.html index 666748a7..ca91699e 100644 --- a/tpl/linklist.html +++ b/tpl/linklist.html | |||
@@ -7,15 +7,24 @@ | |||
7 | <body> | 7 | <body> |
8 | <div id="pageheader"> | 8 | <div id="pageheader"> |
9 | {include="page.header"} | 9 | {include="page.header"} |
10 | |||
10 | <div id="headerform" class="search"> | 11 | <div id="headerform" class="search"> |
11 | <form method="GET" class="searchform" name="searchform"> | 12 | <form method="GET" class="searchform" name="searchform"> |
12 | <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text" value=""> | 13 | <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text" |
14 | {if="!empty($search_crits) && $search_type=='fulltext'"} | ||
15 | value="{$search_crits}" | ||
16 | {/if} | ||
17 | > | ||
13 | <input type="submit" value="Search" class="bigbutton"> | 18 | <input type="submit" value="Search" class="bigbutton"> |
14 | </form> | 19 | </form> |
15 | <form method="GET" class="tagfilter" name="tagfilter"> | 20 | <form method="GET" class="tagfilter" name="tagfilter"> |
16 | <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" value="" | 21 | <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" |
17 | autocomplete="off" class="awesomplete" data-multiple data-minChars="1" | 22 | {if="!empty($search_crits) && $search_type=='tags'"} |
18 | data-list="{loop="$tags"}{$key}, {/loop}"> | 23 | value="{function="implode(' ', $search_crits)"}" |
24 | {/if} | ||
25 | autocomplete="off" class="awesomplete" data-multiple data-minChars="1" | ||
26 | data-list="{loop="$tags"}{$key}, {/loop}" | ||
27 | > | ||
19 | <input type="submit" value="Search" class="bigbutton"> | 28 | <input type="submit" value="Search" class="bigbutton"> |
20 | </form> | 29 | </form> |
21 | {loop="$plugins_header.fields_toolbar"} | 30 | {loop="$plugins_header.fields_toolbar"} |
@@ -44,7 +53,7 @@ | |||
44 | <div id="searchcriteria">{$result_count} results for tags <i> | 53 | <div id="searchcriteria">{$result_count} results for tags <i> |
45 | {loop="search_crits"} | 54 | {loop="search_crits"} |
46 | <span class="linktag" title="Remove tag"> | 55 | <span class="linktag" title="Remove tag"> |
47 | <a href="?removetag={$value}">{$value} <span class="remove">x</span></a> | 56 | <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a> |
48 | </span> | 57 | </span> |
49 | {/loop}</i></div> | 58 | {/loop}</i></div> |
50 | {/if} | 59 | {/if} |
diff --git a/tpl/tools.html b/tpl/tools.html index c13f4f16..c13f4f16 100755..100644 --- a/tpl/tools.html +++ b/tpl/tools.html | |||