aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitattributes31
-rw-r--r--.travis.yml1
-rw-r--r--Makefile16
-rw-r--r--application/ApplicationUtils.php2
-rw-r--r--application/HttpUtils.php49
-rw-r--r--application/LinkDB.php118
-rw-r--r--application/LinkFilter.php259
-rw-r--r--application/LinkUtils.php79
-rw-r--r--application/Url.php23
-rw-r--r--application/Utils.php30
-rw-r--r--docker/.htaccess2
-rw-r--r--docker/development/Dockerfile28
-rw-r--r--docker/development/IMAGE.md10
-rw-r--r--docker/development/nginx.conf64
-rw-r--r--docker/development/supervised.conf13
-rw-r--r--docker/production/Dockerfile20
-rw-r--r--docker/production/IMAGE.md5
-rw-r--r--docker/production/nginx.conf56
-rw-r--r--docker/production/stable/Dockerfile20
-rw-r--r--docker/production/stable/IMAGE.md5
-rw-r--r--docker/production/stable/nginx.conf56
-rw-r--r--docker/production/stable/supervised.conf13
-rw-r--r--docker/production/supervised.conf13
-rw-r--r--inc/shaarli.css34
-rw-r--r--index.php350
-rw-r--r--plugins/qrcode/qrcode.css23
-rw-r--r--plugins/qrcode/qrcode.html8
-rw-r--r--plugins/qrcode/qrcode.php22
-rw-r--r--plugins/qrcode/shaarli-qrcode.js10
-rw-r--r--plugins/wallabag/README.md25
-rw-r--r--plugins/wallabag/WallabagInstance.php71
-rw-r--r--plugins/wallabag/config.php.dist3
-rw-r--r--plugins/wallabag/wallabag.html2
-rw-r--r--plugins/wallabag/wallabag.php17
-rw-r--r--shaarli_version.php2
-rw-r--r--tests/HttpUtils/GetHttpUrlTest.php26
-rw-r--r--tests/LinkDBTest.php229
-rw-r--r--tests/LinkFilterTest.php242
-rw-r--r--tests/LinkUtilsTest.php85
-rw-r--r--tests/Url/UrlTest.php29
-rw-r--r--tests/UtilsTest.php66
-rw-r--r--tests/plugins/PlugQrcodeTest.php4
-rw-r--r--tests/plugins/PluginWallabagTest.php4
-rw-r--r--tests/plugins/WallabagInstanceTest.php60
-rw-r--r--tests/utils/ReferenceLinkDB.php5
-rw-r--r--tpl/404.html17
-rw-r--r--tpl/linklist.html19
-rw-r--r--[-rwxr-xr-x]tpl/tools.html0
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
11Dockerfile 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
24composer.json export-ignore
25doc/**/*.json export-ignore
26doc/**/*.md export-ignore
27docker/ export-ignore
28Doxyfile export-ignore
29Makefile export-ignore
30phpunit.xml export-ignore
31tests/ 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
12script: 12script:
13 - make clean 13 - make clean
14 - make check_permissions
14 - make test 15 - make test
diff --git a/Makefile b/Makefile
index a86f9aa8..75c54f28 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ BIN = vendor/bin
16PHP_SOURCE = index.php application tests plugins 16PHP_SOURCE = index.php application tests plugins
17PHP_COMMA_SOURCE = index.php,application,tests,plugins 17PHP_COMMA_SOURCE = index.php,application,tests,plugins
18 18
19all: static_analysis_summary test 19all: 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##
104check_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 */
27function get_http_url($url, $timeout = 30, $maxBytes = 4194304) 27function 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 */
70function 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 */
8class 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 */
10function 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 */
31function 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 */
51function 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 */
70function 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 */
61function 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 */
15function 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 */
74function checkDateFormat($format, $string) 94function 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 @@
1Allow from none
2Deny 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 @@
1FROM debian:jessie
2MAINTAINER Shaarli Community
3
4RUN apt-get update \
5 && apt-get install -y \
6 nginx-light php5-fpm php5-gd supervisor \
7 git nano
8
9ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
10RUN chmod 755 /usr/local/bin/composer
11
12COPY nginx.conf /etc/nginx/nginx.conf
13COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
14RUN echo "<?php phpinfo(); ?>" > /var/www/index.php
15
16WORKDIR /var/www
17RUN rm -rf html \
18 && git clone https://github.com/shaarli/Shaarli.git shaarli \
19 && chown -R www-data:www-data .
20
21WORKDIR /var/www/shaarli
22RUN composer install
23
24VOLUME /var/www/shaarli/data
25
26EXPOSE 80
27
28CMD ["/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 @@
1user www-data www-data;
2daemon off;
3worker_processes 4;
4
5events {
6 worker_connections 768;
7}
8
9http {
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]
2command=/usr/sbin/php5-fpm -F
3priority=5
4autostart=true
5autorestart=true
6
7[program:nginx]
8command=/usr/sbin/nginx
9priority=10
10autostart=true
11autorestart=true
12stdout_events_enabled=true
13stderr_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 @@
1FROM debian:jessie
2MAINTAINER Shaarli Community
3
4RUN apt-get update \
5 && apt-get install -y curl nginx-light php5-fpm php5-gd supervisor
6
7COPY nginx.conf /etc/nginx/nginx.conf
8COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
9
10WORKDIR /var/www
11RUN 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
16VOLUME /var/www/shaarli/data
17
18EXPOSE 80
19
20CMD ["/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 @@
1user www-data www-data;
2daemon off;
3worker_processes 4;
4
5events {
6 worker_connections 768;
7}
8
9http {
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 @@
1FROM debian:jessie
2MAINTAINER Shaarli Community
3
4RUN apt-get update \
5 && apt-get install -y curl nginx-light php5-fpm php5-gd supervisor
6
7COPY nginx.conf /etc/nginx/nginx.conf
8COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
9
10WORKDIR /var/www
11RUN 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
16VOLUME /var/www/shaarli/data
17
18EXPOSE 80
19
20CMD ["/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 @@
1user www-data www-data;
2daemon off;
3worker_processes 4;
4
5events {
6 worker_connections 768;
7}
8
9http {
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]
2command=/usr/sbin/php5-fpm -F
3priority=5
4autostart=true
5autorestart=true
6
7[program:nginx]
8command=/usr/sbin/nginx
9priority=10
10autostart=true
11autorestart=true
12stdout_events_enabled=true
13stderr_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]
2command=/usr/sbin/php5-fpm -F
3priority=5
4autostart=true
5autorestart=true
6
7[program:nginx]
8command=/usr/sbin/nginx
9priority=10
10autostart=true
11autorestart=true
12stdout_events_enabled=true
13stderr_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
741div#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
760div.daily { 741div.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 {
1119ul.errors { 1100ul.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}
diff --git a/index.php b/index.php
index d0876d95..beba9c32 100644
--- a/index.php
+++ b/index.php
@@ -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 */
122define('shaarli_version', '0.6.1'); 122define('shaarli_version', '0.6.2');
123 123
124// http://server.com/x/shaarli --> /shaarli/ 124// http://server.com/x/shaarli --> /shaarli/
125define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); 125define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0)));
@@ -151,6 +151,8 @@ require_once 'application/CachedPage.php';
151require_once 'application/FileUtils.php'; 151require_once 'application/FileUtils.php';
152require_once 'application/HttpUtils.php'; 152require_once 'application/HttpUtils.php';
153require_once 'application/LinkDB.php'; 153require_once 'application/LinkDB.php';
154require_once 'application/LinkFilter.php';
155require_once 'application/LinkUtils.php';
154require_once 'application/TimeZone.php'; 156require_once 'application/TimeZone.php';
155require_once 'application/Url.php'; 157require_once 'application/Url.php';
156require_once 'application/Utils.php'; 158require_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
312function 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.)
582function 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()
1865function buildLinkList($PAGE,$LINKSDB) 1871function 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&amp;s=8&amp;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&amp;s=8&amp;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 */
54function 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
49function removeQrcode() 55function 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
3For each link in your Shaarli, adds a button to save the target page in your [wallabag](https://www.wallabag.org/). 3For 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
6Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there. 7Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there.
7The directory structure should look like: 8The 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
18To enable the plugin, add `'wallabag'` to your list of enabled plugins in `data/options.php` (`PLUGINS` array) 21To enable the plugin, add `'wallabag'` to your list of enabled plugins in `data/options.php` (`PLUGINS` array).
19. This should look like: 22This 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
25Then, set the `WALLABAG_URL` variable in `data/options.php` pointing to your wallabag URL. Example: 28### Configuration
29
30Copy `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 */
6class 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
7require_once 'WallabagInstance.php';
8
7// don't raise unnecessary warnings 9// don't raise unnecessary warnings
8if (is_file(PluginManager::$PLUGINS_PATH . '/wallabag/config.php')) { 10if (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 @@
6require_once 'application/HttpUtils.php'; 6require_once 'application/HttpUtils.php';
7 7
8/** 8/**
9 * Unitary tests for get_http_url() 9 * Unitary tests for get_http_response()
10 */ 10 */
11class GetHttpUrlTest extends PHPUnit_Framework_TestCase 11class 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
3require_once 'application/LinkFilter.php';
4
5/**
6 * Class LinkFilterTest.
7 */
8class 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
3require_once 'application/LinkUtils.php';
4
5/**
6* Class LinkUtilsTest.
7*/
8class 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
3require_once 'plugins/wallabag/WallabagInstance.php';
4
5/**
6 * Class WallabagInstanceTest
7 */
8class 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