diff options
author | ArthurHoaro <arthur@hoa.ro> | 2017-10-07 12:22:54 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2017-10-07 12:22:54 +0200 |
commit | 80b15f5d2db8b90fd9b29f94e6bd8652340df4f0 (patch) | |
tree | b4826cbb03c64b0e5ffb6d0a72f21e0f6b9d9ac8 | |
parent | 1ea88ae7d1b7fb13e18f543e7c2ad99c4ccde19a (diff) | |
parent | a01437f9e1e4fb5a098877b243828bf6f4936562 (diff) | |
download | Shaarli-80b15f5d2db8b90fd9b29f94e6bd8652340df4f0.tar.gz Shaarli-80b15f5d2db8b90fd9b29f94e6bd8652340df4f0.tar.zst Shaarli-80b15f5d2db8b90fd9b29f94e6bd8652340df4f0.zip |
Merge branch 'master' into v0.9
48 files changed, 832 insertions, 229 deletions
diff --git a/.github/mailmap b/.github/mailmap index 41d91e47..bbdb7908 100644 --- a/.github/mailmap +++ b/.github/mailmap | |||
@@ -11,3 +11,5 @@ Timo Van Neerden <fire@lehollandaisvolant.net> lehollandaisvolant <levoltigeurho | |||
11 | VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com> | 11 | VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com> |
12 | VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net> | 12 | VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net> |
13 | VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org> | 13 | VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org> |
14 | Willi Eggeling <thewilli@gmail.com> <mail@wje-online.de> | ||
15 | Willi Eggeling <thewilli@gmail.com> <thewilli@users.noreply.github.com> | ||
diff --git a/.travis.yml b/.travis.yml index 26535ad3..b6b9bddf 100644 --- a/.travis.yml +++ b/.travis.yml | |||
@@ -1,12 +1,6 @@ | |||
1 | sudo: false | 1 | sudo: false |
2 | dist: precise | 2 | dist: trusty |
3 | language: php | 3 | language: php |
4 | addons: | ||
5 | apt: | ||
6 | packages: | ||
7 | - locales | ||
8 | - language-pack-de | ||
9 | - language-pack-fr | ||
10 | cache: | 4 | cache: |
11 | directories: | 5 | directories: |
12 | - $HOME/.composer/cache | 6 | - $HOME/.composer/cache |
@@ -18,6 +12,7 @@ php: | |||
18 | install: | 12 | install: |
19 | - composer self-update | 13 | - composer self-update |
20 | - composer install --prefer-dist | 14 | - composer install --prefer-dist |
15 | - locale -a | ||
21 | script: | 16 | script: |
22 | - make clean | 17 | - make clean |
23 | - make check_permissions | 18 | - make check_permissions |
@@ -1,14 +1,16 @@ | |||
1 | 506 ArthurHoaro <arthur@hoa.ro> | 1 | 537 ArthurHoaro <arthur@hoa.ro> |
2 | 204 VirtualTam <virtualtam@flibidi.net> | 2 | 252 VirtualTam <virtualtam@flibidi.net> |
3 | 147 nodiscc <nodiscc@gmail.com> | 3 | 148 nodiscc <nodiscc@gmail.com> |
4 | 56 Sébastien Sauvage <sebsauvage@sebsauvage.net> | 4 | 56 Sébastien Sauvage <sebsauvage@sebsauvage.net> |
5 | 15 Florian Eula <eula.florian@gmail.com> | 5 | 15 Florian Eula <eula.florian@gmail.com> |
6 | 13 Emilien Klein <emilien@klein.st> | 6 | 13 Emilien Klein <emilien@klein.st> |
7 | 12 Nicolas Danelon <hi@nicolasmd.com.ar> | 7 | 12 Nicolas Danelon <hi@nicolasmd.com.ar> |
8 | 9 Willi Eggeling <thewilli@gmail.com> | ||
8 | 8 Christophe HENRY <christophe.henry@sbgodin.fr> | 9 | 8 Christophe HENRY <christophe.henry@sbgodin.fr> |
10 | 6 B. van Berkum <dev@dotmpe.com> | ||
11 | 5 Lucas Cimon <lucas.cimon@gmail.com> | ||
9 | 4 Alexandre Alapetite <alexandre@alapetite.fr> | 12 | 4 Alexandre Alapetite <alexandre@alapetite.fr> |
10 | 4 David Sferruzza <david.sferruzza@gmail.com> | 13 | 4 David Sferruzza <david.sferruzza@gmail.com> |
11 | 3 Lucas Cimon <lucas.cimon@gmail.com> | ||
12 | 3 Teromene <teromene@teromene.fr> | 14 | 3 Teromene <teromene@teromene.fr> |
13 | 3 kalvn <kalvnthereal@gmail.com> | 15 | 3 kalvn <kalvnthereal@gmail.com> |
14 | 2 Chris Kuethe <chris.kuethe@gmail.com> | 16 | 2 Chris Kuethe <chris.kuethe@gmail.com> |
@@ -37,6 +39,7 @@ | |||
37 | 1 Kevin Canévet <kevin@streamroot.io> | 39 | 1 Kevin Canévet <kevin@streamroot.io> |
38 | 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org> | 40 | 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org> |
39 | 1 Lionel Martin <renarddesmers@gmail.com> | 41 | 1 Lionel Martin <renarddesmers@gmail.com> |
42 | 1 Mark Gerarts <mark.gerarts@gmail.com> | ||
40 | 1 Marsup <marsup@gmail.com> | 43 | 1 Marsup <marsup@gmail.com> |
41 | 1 Sbgodin <Sbgodin@users.noreply.github.com> | 44 | 1 Sbgodin <Sbgodin@users.noreply.github.com> |
42 | 1 TsT <tst2005@gmail.com> | 45 | 1 TsT <tst2005@gmail.com> |
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b018cb4..120c5d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
@@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file. | |||
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) |
5 | and this project adheres to [Semantic Versioning](http://semver.org/). | 5 | and this project adheres to [Semantic Versioning](http://semver.org/). |
6 | 6 | ||
7 | ## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07 | ||
8 | |||
9 | **Major security issue fixed. Please update.** | ||
10 | |||
11 | ### Added | ||
12 | - Tag search now supports wildcards `*` | ||
13 | - New setting `privacy.force_login` which can be used with `privacy.hide_public_links` to redirect anonymous users to the login page. | ||
14 | - New setting `general.default_note_title` used to override default `Note:` title prefix for notes. | ||
15 | - Add a version hash for asset loading to prevent browser's cache issue | ||
16 | |||
17 | ### Changed | ||
18 | - The "Remember me" checkbox is unchecked by default | ||
19 | - The default value of the "Remember me" checkbox can be configured under `data/config.json.php` | ||
20 | |||
21 | ### Removed | ||
22 | - Remove obsolete PHP magic quote support | ||
23 | |||
24 | ### Fixed | ||
25 | - Generates a permalink URL if the URL is set to blank | ||
26 | - Replace links to the old GitHub wiki with ReadTheDocs URIs | ||
27 | - Use single quotes in the note bookmarklet | ||
28 | - Daily page if there is no link | ||
29 | - Bulk link deletion with a single link | ||
30 | - HTTPS detection behind a reverse proxy | ||
31 | - Travis tests environment and localization | ||
32 | - Improve template paths robustness (trailing slash) | ||
33 | - Robustness: safer gzinflate/zlib usage | ||
34 | - Description links parsing with parenthesis (without Markdown) | ||
35 | - Templates: | ||
36 | - Sort the tag cloud alphabetically | ||
37 | - Firefox social title | ||
38 | - Improved visited link color | ||
39 | - Fix jumpy textarea with long content in post edit | ||
40 | |||
41 | ### Security | ||
42 | |||
43 | - Vulnerability introduced in v0.9.1 fixed. | ||
44 | |||
7 | ## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23 | 45 | ## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23 |
8 | 46 | ||
9 | The documentation has been migrated to ReadTheDocs: | 47 | The documentation has been migrated to ReadTheDocs: |
@@ -61,7 +99,7 @@ The documentation has been migrated to ReadTheDocs: | |||
61 | This release introduces the REST API, and requires updating HTTP server | 99 | This release introduces the REST API, and requires updating HTTP server |
62 | configuration to enable URL rewriting, see: | 100 | configuration to enable URL rewriting, see: |
63 | - https://shaarli.github.io/api-documentation/ | 101 | - https://shaarli.github.io/api-documentation/ |
64 | - https://github.com/shaarli/Shaarli/wiki/Server-configuration | 102 | - https://shaarli.readthedocs.io/en/master/Server-configuration/ |
65 | 103 | ||
66 | **WARNING**: Shaarli now requires PHP 5.5+. | 104 | **WARNING**: Shaarli now requires PHP 5.5+. |
67 | 105 | ||
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb82951d..03564fd2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md | |||
@@ -54,7 +54,7 @@ Please report any problem you might find. | |||
54 | * starting from branch ` master`, switch to a new branch (eg. `git checkout -b my-awesome-feature`) | 54 | * starting from branch ` master`, switch to a new branch (eg. `git checkout -b my-awesome-feature`) |
55 | * edit the required files (from the Github web interface or your text editor) | 55 | * edit the required files (from the Github web interface or your text editor) |
56 | * add and commit your changes with a meaningful commit message (eg `Cool new feature, fixes issue #1001`) | 56 | * add and commit your changes with a meaningful commit message (eg `Cool new feature, fixes issue #1001`) |
57 | * run unit tests against your patched version, see [Running unit tests](https://github.com/shaarli/Shaarli/wiki/Running-unit-tests) | 57 | * run unit tests against your patched version, see [Running unit tests](https://shaarli.readthedocs.io/en/master/Unit-tests/#run-unit-tests) |
58 | * Open your fork in the Github web interface and click the "Compare and Pull Request" button, enter required info and submit your Pull Request. | 58 | * Open your fork in the Github web interface and click the "Compare and Pull Request" button, enter required info and submit your Pull Request. |
59 | 59 | ||
60 | All changes you will do on the `my-awesome-feature` in the future will be added to your Pull Request. Don't work directly on the master branch, don't do unrelated work on your `my-awesome-feature` branch. | 60 | All changes you will do on the `my-awesome-feature` in the future will be added to your Pull Request. Don't work directly on the master branch, don't do unrelated work on your `my-awesome-feature` branch. |
@@ -19,6 +19,16 @@ PHP_COMMA_SOURCE = index.php,application,tests,plugins | |||
19 | all: static_analysis_summary check_permissions test | 19 | all: static_analysis_summary check_permissions test |
20 | 20 | ||
21 | ## | 21 | ## |
22 | # Docker test adapter | ||
23 | # | ||
24 | # Shaarli sources and vendored libraries are copied from a shared volume | ||
25 | # to a user-owned directory to enable running tests as a non-root user. | ||
26 | ## | ||
27 | docker_%: | ||
28 | rsync -az /shaarli/ ~/shaarli/ | ||
29 | cd ~/shaarli && make $* | ||
30 | |||
31 | ## | ||
22 | # Concise status of the project | 32 | # Concise status of the project |
23 | # These targets are non-blocking: || exit 0 | 33 | # These targets are non-blocking: || exit 0 |
24 | ## | 34 | ## |
@@ -159,14 +169,14 @@ composer_dependencies: clean | |||
159 | find vendor/ -name ".git" -type d -exec rm -rf {} + | 169 | find vendor/ -name ".git" -type d -exec rm -rf {} + |
160 | 170 | ||
161 | ### generate a release tarball and include 3rd-party dependencies | 171 | ### generate a release tarball and include 3rd-party dependencies |
162 | release_tar: composer_dependencies doc_html | 172 | release_tar: composer_dependencies htmldoc |
163 | git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD | 173 | git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD |
164 | tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/ | 174 | tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/ |
165 | tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/ | 175 | tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/ |
166 | gzip $(ARCHIVE_VERSION).tar | 176 | gzip $(ARCHIVE_VERSION).tar |
167 | 177 | ||
168 | ### generate a release zip and include 3rd-party dependencies | 178 | ### generate a release zip and include 3rd-party dependencies |
169 | release_zip: composer_dependencies doc_html | 179 | release_zip: composer_dependencies htmldoc |
170 | git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD | 180 | git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD |
171 | mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor} | 181 | mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor} |
172 | rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/ | 182 | rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/ |
@@ -195,17 +205,11 @@ doxygen: clean | |||
195 | @rm -rf doxygen | 205 | @rm -rf doxygen |
196 | @( cat Doxyfile ; echo "PROJECT_NUMBER=`git describe`" ) | doxygen - | 206 | @( cat Doxyfile ; echo "PROJECT_NUMBER=`git describe`" ) | doxygen - |
197 | 207 | ||
198 | ### Convert local markdown documentation to HTML | 208 | ### generate HTML documentation from Markdown pages with MkDocs |
199 | # | 209 | htmldoc: |
200 | # For all pages: | ||
201 | # - convert GitHub-flavoured relative links to standard Markdown | ||
202 | # - generate html documentation with mkdocs | ||
203 | htmlpages: | ||
204 | python3 -m venv venv/ | 210 | python3 -m venv venv/ |
205 | bash -c 'source venv/bin/activate; \ | 211 | bash -c 'source venv/bin/activate; \ |
206 | pip install mkdocs; \ | 212 | pip install mkdocs; \ |
207 | mkdocs build' | 213 | mkdocs build' |
208 | find doc/html/ -type f -exec chmod a-x '{}' \; | 214 | find doc/html/ -type f -exec chmod a-x '{}' \; |
209 | rm -r venv | 215 | rm -r venv |
210 | |||
211 | doc_html: authors htmlpages | ||
@@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._ | |||
9 | [![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) | 9 | [![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) |
10 | [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) | 10 | [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) |
11 | • | 11 | • |
12 | [![](https://img.shields.io/badge/latest-v0.9.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.0) | 12 | [![](https://img.shields.io/badge/latest-v0.9.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) |
13 | [![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) | 13 | [![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) |
14 | • | 14 | • |
15 | [![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli) | 15 | [![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli) |
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 85dcbeeb..5643f4a0 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php | |||
@@ -168,14 +168,15 @@ class ApplicationUtils | |||
168 | public static function checkResourcePermissions($conf) | 168 | public static function checkResourcePermissions($conf) |
169 | { | 169 | { |
170 | $errors = array(); | 170 | $errors = array(); |
171 | $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); | ||
171 | 172 | ||
172 | // Check script and template directories are readable | 173 | // Check script and template directories are readable |
173 | foreach (array( | 174 | foreach (array( |
174 | 'application', | 175 | 'application', |
175 | 'inc', | 176 | 'inc', |
176 | 'plugins', | 177 | 'plugins', |
177 | $conf->get('resource.raintpl_tpl'), | 178 | $rainTplDir, |
178 | $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme'), | 179 | $rainTplDir.'/'.$conf->get('resource.theme'), |
179 | ) as $path) { | 180 | ) as $path) { |
180 | if (! is_readable(realpath($path))) { | 181 | if (! is_readable(realpath($path))) { |
181 | $errors[] = '"'.$path.'" directory is not readable'; | 182 | $errors[] = '"'.$path.'" directory is not readable'; |
@@ -220,4 +221,19 @@ class ApplicationUtils | |||
220 | 221 | ||
221 | return $errors; | 222 | return $errors; |
222 | } | 223 | } |
224 | |||
225 | /** | ||
226 | * Returns a salted hash representing the current Shaarli version. | ||
227 | * | ||
228 | * Useful for assets browser cache. | ||
229 | * | ||
230 | * @param string $currentVersion of Shaarli | ||
231 | * @param string $salt User personal salt, also used for the authentication | ||
232 | * | ||
233 | * @return string version hash | ||
234 | */ | ||
235 | public static function getVersionHash($currentVersion, $salt) | ||
236 | { | ||
237 | return hash_hmac('sha256', $currentVersion, $salt); | ||
238 | } | ||
223 | } | 239 | } |
diff --git a/application/FileUtils.php b/application/FileUtils.php index a167f642..918cb83b 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php | |||
@@ -50,7 +50,8 @@ class FileUtils | |||
50 | 50 | ||
51 | /** | 51 | /** |
52 | * Read data from a file containing Shaarli database format content. | 52 | * Read data from a file containing Shaarli database format content. |
53 | * If the file isn't readable or doesn't exists, default data will be returned. | 53 | * |
54 | * If the file isn't readable or doesn't exist, default data will be returned. | ||
54 | * | 55 | * |
55 | * @param string $file File path. | 56 | * @param string $file File path. |
56 | * @param mixed $default The default value to return if the file isn't readable. | 57 | * @param mixed $default The default value to return if the file isn't readable. |
@@ -61,16 +62,21 @@ class FileUtils | |||
61 | { | 62 | { |
62 | // Note that gzinflate is faster than gzuncompress. | 63 | // Note that gzinflate is faster than gzuncompress. |
63 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | 64 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 |
64 | if (is_readable($file)) { | 65 | if (! is_readable($file)) { |
65 | return unserialize( | 66 | return $default; |
66 | gzinflate( | 67 | } |
67 | base64_decode( | 68 | |
68 | substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) | 69 | $data = file_get_contents($file); |
69 | ) | 70 | if ($data == '') { |
70 | ) | 71 | return $default; |
71 | ); | ||
72 | } | 72 | } |
73 | 73 | ||
74 | return $default; | 74 | return unserialize( |
75 | gzinflate( | ||
76 | base64_decode( | ||
77 | substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) | ||
78 | ) | ||
79 | ) | ||
80 | ); | ||
75 | } | 81 | } |
76 | } | 82 | } |
diff --git a/application/HttpUtils.php b/application/HttpUtils.php index 88a1efdb..00835966 100644 --- a/application/HttpUtils.php +++ b/application/HttpUtils.php | |||
@@ -401,3 +401,31 @@ function getIpAddressFromProxy($server, $trustedIps) | |||
401 | 401 | ||
402 | return array_pop($ips); | 402 | return array_pop($ips); |
403 | } | 403 | } |
404 | |||
405 | /** | ||
406 | * Returns true if Shaarli's currently browsed in HTTPS. | ||
407 | * Supports reverse proxies (if the headers are correctly set). | ||
408 | * | ||
409 | * @param array $server $_SERVER. | ||
410 | * | ||
411 | * @return bool true if HTTPS, false otherwise. | ||
412 | */ | ||
413 | function is_https($server) | ||
414 | { | ||
415 | |||
416 | if (isset($server['HTTP_X_FORWARDED_PORT'])) { | ||
417 | // Keep forwarded port | ||
418 | if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) { | ||
419 | $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']); | ||
420 | $port = trim($ports[0]); | ||
421 | } else { | ||
422 | $port = $server['HTTP_X_FORWARDED_PORT']; | ||
423 | } | ||
424 | |||
425 | if ($port == '443') { | ||
426 | return true; | ||
427 | } | ||
428 | } | ||
429 | |||
430 | return ! empty($server['HTTPS']); | ||
431 | } | ||
diff --git a/application/LinkDB.php b/application/LinkDB.php index 9308164a..22c1f0ab 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -249,7 +249,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
249 | $link = array( | 249 | $link = array( |
250 | 'id' => 1, | 250 | 'id' => 1, |
251 | 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', | 251 | 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', |
252 | 'url'=>'https://github.com/shaarli/Shaarli/wiki', | 252 | 'url'=>'https://shaarli.readthedocs.io', |
253 | 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. | 253 | 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. |
254 | 254 | ||
255 | To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. | 255 | To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. |
diff --git a/application/LinkFilter.php b/application/LinkFilter.php index 95519528..99ecd1e2 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php | |||
@@ -250,6 +250,51 @@ class LinkFilter | |||
250 | } | 250 | } |
251 | 251 | ||
252 | /** | 252 | /** |
253 | * generate a regex fragment out of a tag | ||
254 | * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard | ||
255 | * @return string generated regex fragment | ||
256 | */ | ||
257 | private static function tag2regex($tag) | ||
258 | { | ||
259 | $len = strlen($tag); | ||
260 | if(!$len || $tag === "-" || $tag === "*"){ | ||
261 | // nothing to search, return empty regex | ||
262 | return ''; | ||
263 | } | ||
264 | if($tag[0] === "-") { | ||
265 | // query is negated | ||
266 | $i = 1; // use offset to start after '-' character | ||
267 | $regex = '(?!'; // create negative lookahead | ||
268 | } else { | ||
269 | $i = 0; // start at first character | ||
270 | $regex = '(?='; // use positive lookahead | ||
271 | } | ||
272 | $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning | ||
273 | // iterate over string, separating it into placeholder and content | ||
274 | for(; $i < $len; $i++){ | ||
275 | if($tag[$i] === '*'){ | ||
276 | // placeholder found | ||
277 | $regex .= '[^ ]*?'; | ||
278 | } else { | ||
279 | // regular characters | ||
280 | $offset = strpos($tag, '*', $i); | ||
281 | if($offset === false){ | ||
282 | // no placeholder found, set offset to end of string | ||
283 | $offset = $len; | ||
284 | } | ||
285 | // subtract one, as we want to get before the placeholder or end of string | ||
286 | $offset -= 1; | ||
287 | // we got a tag name that we want to search for. escape any regex characters to prevent conflicts. | ||
288 | $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/'); | ||
289 | // move $i on | ||
290 | $i = $offset; | ||
291 | } | ||
292 | } | ||
293 | $regex .= '(?:$| ))'; // after the tag may only be a space or the end | ||
294 | return $regex; | ||
295 | } | ||
296 | |||
297 | /** | ||
253 | * Returns the list of links associated with a given list of tags | 298 | * Returns the list of links associated with a given list of tags |
254 | * | 299 | * |
255 | * You can specify one or more tags, separated by space or a comma, e.g. | 300 | * You can specify one or more tags, separated by space or a comma, e.g. |
@@ -263,20 +308,32 @@ class LinkFilter | |||
263 | */ | 308 | */ |
264 | public function filterTags($tags, $casesensitive = false, $visibility = 'all') | 309 | public function filterTags($tags, $casesensitive = false, $visibility = 'all') |
265 | { | 310 | { |
266 | // Implode if array for clean up. | 311 | // get single tags (we may get passed an array, even though the docs say different) |
267 | $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags; | 312 | $inputTags = $tags; |
268 | if (empty($tags)) { | 313 | if(!is_array($tags)) { |
314 | // we got an input string, split tags | ||
315 | $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); | ||
316 | } | ||
317 | |||
318 | if(!count($inputTags)){ | ||
319 | // no input tags | ||
269 | return $this->noFilter($visibility); | 320 | return $this->noFilter($visibility); |
270 | } | 321 | } |
271 | 322 | ||
272 | $searchtags = self::tagsStrToArray($tags, $casesensitive); | 323 | // build regex from all tags |
273 | $filtered = array(); | 324 | $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; |
274 | if (empty($searchtags)) { | 325 | if(!$casesensitive) { |
275 | return $filtered; | 326 | // make regex case insensitive |
327 | $re .= 'i'; | ||
276 | } | 328 | } |
277 | 329 | ||
330 | // create resulting array | ||
331 | $filtered = array(); | ||
332 | |||
333 | // iterate over each link | ||
278 | foreach ($this->links as $key => $link) { | 334 | foreach ($this->links as $key => $link) { |
279 | // ignore non private links when 'privatonly' is on. | 335 | // check level of visibility |
336 | // ignore non private links when 'privateonly' is on. | ||
280 | if ($visibility !== 'all') { | 337 | if ($visibility !== 'all') { |
281 | if (! $link['private'] && $visibility === 'private') { | 338 | if (! $link['private'] && $visibility === 'private') { |
282 | continue; | 339 | continue; |
@@ -284,25 +341,27 @@ class LinkFilter | |||
284 | continue; | 341 | continue; |
285 | } | 342 | } |
286 | } | 343 | } |
287 | 344 | $search = $link['tags']; // build search string, start with tags of current link | |
288 | $linktags = self::tagsStrToArray($link['tags'], $casesensitive); | 345 | if(strlen(trim($link['description'])) && strpos($link['description'], '#') !== false){ |
289 | 346 | // description given and at least one possible tag found | |
290 | $found = true; | 347 | $descTags = array(); |
291 | for ($i = 0 ; $i < count($searchtags) && $found; $i++) { | 348 | // find all tags in the form of #tag in the description |
292 | // Exclusive search, quit if tag found. | 349 | preg_match_all( |
293 | // Or, tag not found in the link, quit. | 350 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', |
294 | if (($searchtags[$i][0] == '-' | 351 | $link['description'], |
295 | && $this->searchTagAndHashTag(substr($searchtags[$i], 1), $linktags, $link['description'])) | 352 | $descTags |
296 | || ($searchtags[$i][0] != '-') | 353 | ); |
297 | && ! $this->searchTagAndHashTag($searchtags[$i], $linktags, $link['description']) | 354 | if(count($descTags[1])){ |
298 | ) { | 355 | // there were some tags in the description, add them to the search string |
299 | $found = false; | 356 | $search .= ' ' . implode(' ', $descTags[1]); |
300 | } | 357 | } |
358 | }; | ||
359 | // match regular expression with search string | ||
360 | if(!preg_match($re, $search)){ | ||
361 | // this entry does _not_ match our regex | ||
362 | continue; | ||
301 | } | 363 | } |
302 | 364 | $filtered[$key] = $link; | |
303 | if ($found) { | ||
304 | $filtered[$key] = $link; | ||
305 | } | ||
306 | } | 365 | } |
307 | return $filtered; | 366 | return $filtered; |
308 | } | 367 | } |
@@ -364,28 +423,6 @@ class LinkFilter | |||
364 | } | 423 | } |
365 | 424 | ||
366 | /** | 425 | /** |
367 | * Check if a tag is found in the taglist, or as an hashtag in the link description. | ||
368 | * | ||
369 | * @param string $tag Tag to search. | ||
370 | * @param array $taglist List of tags for the current link. | ||
371 | * @param string $description Link description. | ||
372 | * | ||
373 | * @return bool True if found, false otherwise. | ||
374 | */ | ||
375 | protected function searchTagAndHashTag($tag, $taglist, $description) | ||
376 | { | ||
377 | if (in_array($tag, $taglist)) { | ||
378 | return true; | ||
379 | } | ||
380 | |||
381 | if (preg_match('/(^| )#'. $tag .'([^'. self::$HASHTAG_CHARS .']|$)/mui', $description) > 0) { | ||
382 | return true; | ||
383 | } | ||
384 | |||
385 | return false; | ||
386 | } | ||
387 | |||
388 | /** | ||
389 | * Convert a list of tags (str) to an array. Also | 426 | * Convert a list of tags (str) to an array. Also |
390 | * - handle case sensitivity. | 427 | * - handle case sensitivity. |
391 | * - accepts spaces commas as separator. | 428 | * - accepts spaces commas as separator. |
diff --git a/application/LinkUtils.php b/application/LinkUtils.php index 976474de..267e62cd 100644 --- a/application/LinkUtils.php +++ b/application/LinkUtils.php | |||
@@ -109,7 +109,7 @@ function count_private($links) | |||
109 | */ | 109 | */ |
110 | function text2clickable($text, $redirector = '') | 110 | function text2clickable($text, $redirector = '') |
111 | { | 111 | { |
112 | $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si'; | 112 | $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si'; |
113 | 113 | ||
114 | if (empty($redirector)) { | 114 | if (empty($redirector)) { |
115 | return preg_replace($regex, '<a href="$1">$1</a>', $text); | 115 | return preg_replace($regex, '<a href="$1">$1</a>', $text); |
diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 7a42400d..291860ad 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php | |||
@@ -49,7 +49,7 @@ class PageBuilder | |||
49 | 49 | ||
50 | try { | 50 | try { |
51 | $version = ApplicationUtils::checkUpdate( | 51 | $version = ApplicationUtils::checkUpdate( |
52 | shaarli_version, | 52 | SHAARLI_VERSION, |
53 | $this->conf->get('resource.update_check'), | 53 | $this->conf->get('resource.update_check'), |
54 | $this->conf->get('updates.check_updates_interval'), | 54 | $this->conf->get('updates.check_updates_interval'), |
55 | $this->conf->get('updates.check_updates'), | 55 | $this->conf->get('updates.check_updates'), |
@@ -75,7 +75,11 @@ class PageBuilder | |||
75 | } | 75 | } |
76 | $this->tpl->assign('searchcrits', $searchcrits); | 76 | $this->tpl->assign('searchcrits', $searchcrits); |
77 | $this->tpl->assign('source', index_url($_SERVER)); | 77 | $this->tpl->assign('source', index_url($_SERVER)); |
78 | $this->tpl->assign('version', shaarli_version); | 78 | $this->tpl->assign('version', SHAARLI_VERSION); |
79 | $this->tpl->assign( | ||
80 | 'version_hash', | ||
81 | ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt')) | ||
82 | ); | ||
79 | $this->tpl->assign('scripturl', index_url($_SERVER)); | 83 | $this->tpl->assign('scripturl', index_url($_SERVER)); |
80 | $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? | 84 | $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? |
81 | $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly'])); | 85 | $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly'])); |
@@ -89,6 +93,7 @@ class PageBuilder | |||
89 | $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss'); | 93 | $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss'); |
90 | $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); | 94 | $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); |
91 | $this->tpl->assign('token', getToken($this->conf)); | 95 | $this->tpl->assign('token', getToken($this->conf)); |
96 | |||
92 | if ($this->linkDB !== null) { | 97 | if ($this->linkDB !== null) { |
93 | $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); | 98 | $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); |
94 | } | 99 | } |
diff --git a/application/ThemeUtils.php b/application/ThemeUtils.php index 2718ed13..16f2f6a2 100644 --- a/application/ThemeUtils.php +++ b/application/ThemeUtils.php | |||
@@ -22,6 +22,7 @@ class ThemeUtils | |||
22 | */ | 22 | */ |
23 | public static function getThemes($tplDir) | 23 | public static function getThemes($tplDir) |
24 | { | 24 | { |
25 | $tplDir = rtrim($tplDir, '/'); | ||
25 | $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR); | 26 | $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR); |
26 | $themes = []; | 27 | $themes = []; |
27 | foreach ($allTheme as $value) { | 28 | foreach ($allTheme as $value) { |
diff --git a/application/Updater.php b/application/Updater.php index 40a15906..72b2def0 100644 --- a/application/Updater.php +++ b/application/Updater.php | |||
@@ -398,7 +398,7 @@ class Updater | |||
398 | */ | 398 | */ |
399 | public function updateMethodCheckUpdateRemoteBranch() | 399 | public function updateMethodCheckUpdateRemoteBranch() |
400 | { | 400 | { |
401 | if (shaarli_version === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') { | 401 | if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') { |
402 | return true; | 402 | return true; |
403 | } | 403 | } |
404 | 404 | ||
@@ -413,7 +413,7 @@ class Updater | |||
413 | $latestMajor = $matches[1]; | 413 | $latestMajor = $matches[1]; |
414 | 414 | ||
415 | // Get current major version digit | 415 | // Get current major version digit |
416 | preg_match('/(\d+)\.\d+$/', shaarli_version, $matches); | 416 | preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches); |
417 | $currentMajor = $matches[1]; | 417 | $currentMajor = $matches[1]; |
418 | 418 | ||
419 | if ($currentMajor === $latestMajor) { | 419 | if ($currentMajor === $latestMajor) { |
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index 8eab26f1..7ff2fe67 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php | |||
@@ -9,8 +9,8 @@ use Shaarli\Config\Exception\UnauthorizedConfigException; | |||
9 | * | 9 | * |
10 | * Manages all Shaarli's settings. | 10 | * Manages all Shaarli's settings. |
11 | * See the documentation for more information on settings: | 11 | * See the documentation for more information on settings: |
12 | * - doc/Shaarli-configuration.html | 12 | * - doc/md/Shaarli-configuration.md |
13 | * - https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration | 13 | * - https://shaarli.readthedocs.io/en/master/Shaarli-configuration/#configuration |
14 | */ | 14 | */ |
15 | class ConfigManager | 15 | class ConfigManager |
16 | { | 16 | { |
@@ -317,6 +317,7 @@ class ConfigManager | |||
317 | $this->setEmpty('general.header_link', '?'); | 317 | $this->setEmpty('general.header_link', '?'); |
318 | $this->setEmpty('general.links_per_page', 20); | 318 | $this->setEmpty('general.links_per_page', 20); |
319 | $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); | 319 | $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); |
320 | $this->setEmpty('general.default_note_title', 'Note: '); | ||
320 | 321 | ||
321 | $this->setEmpty('updates.check_updates', false); | 322 | $this->setEmpty('updates.check_updates', false); |
322 | $this->setEmpty('updates.check_updates_branch', 'stable'); | 323 | $this->setEmpty('updates.check_updates_branch', 'stable'); |
@@ -327,7 +328,10 @@ class ConfigManager | |||
327 | 328 | ||
328 | $this->setEmpty('privacy.default_private_links', false); | 329 | $this->setEmpty('privacy.default_private_links', false); |
329 | $this->setEmpty('privacy.hide_public_links', false); | 330 | $this->setEmpty('privacy.hide_public_links', false); |
331 | $this->setEmpty('privacy.force_login', false); | ||
330 | $this->setEmpty('privacy.hide_timestamps', false); | 332 | $this->setEmpty('privacy.hide_timestamps', false); |
333 | // default state of the 'remember me' checkbox of the login form | ||
334 | $this->setEmpty('privacy.remember_user_default', true); | ||
331 | 335 | ||
332 | $this->setEmpty('thumbnail.enable_thumbnails', true); | 336 | $this->setEmpty('thumbnail.enable_thumbnails', true); |
333 | $this->setEmpty('thumbnail.enable_localcache', true); | 337 | $this->setEmpty('thumbnail.enable_localcache', true); |
diff --git a/composer.json b/composer.json index 756ea588..afb8aca4 100644 --- a/composer.json +++ b/composer.json | |||
@@ -6,7 +6,7 @@ | |||
6 | "homepage": "https://github.com/shaarli/Shaarli", | 6 | "homepage": "https://github.com/shaarli/Shaarli", |
7 | "support": { | 7 | "support": { |
8 | "issues": "https://github.com/shaarli/Shaarli/issues", | 8 | "issues": "https://github.com/shaarli/Shaarli/issues", |
9 | "wiki": "https://github.com/shaarli/Shaarli/wiki" | 9 | "wiki": "https://shaarli.readthedocs.io" |
10 | }, | 10 | }, |
11 | "keywords": ["bookmark", "link", "share", "web"], | 11 | "keywords": ["bookmark", "link", "share", "web"], |
12 | "config": { | 12 | "config": { |
diff --git a/doc/md/Download-and-Installation.md b/doc/md/Download-and-Installation.md index 135f0633..e5e929ef 100644 --- a/doc/md/Download-and-Installation.md +++ b/doc/md/Download-and-Installation.md | |||
@@ -18,13 +18,13 @@ Get the latest released version from the [releases](https://github.com/shaarli/S | |||
18 | 18 | ||
19 | **Download our *shaarli-full* archive** to include dependencies. | 19 | **Download our *shaarli-full* archive** to include dependencies. |
20 | 20 | ||
21 | The current latest released version is `v0.9.0` | 21 | The current latest released version is `v0.9.1` |
22 | 22 | ||
23 | Or in command lines: | 23 | Or in command lines: |
24 | 24 | ||
25 | ```bash | 25 | ```bash |
26 | $ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.0/shaarli-v0.9.0-full.zip | 26 | $ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.1/shaarli-v0.9.1-full.zip |
27 | $ unzip shaarli-v0.9.0-full.zip | 27 | $ unzip shaarli-v0.9.1-full.zip |
28 | $ mv Shaarli /path/to/shaarli/ | 28 | $ mv Shaarli /path/to/shaarli/ |
29 | ``` | 29 | ``` |
30 | 30 | ||
diff --git a/doc/md/Plugin-System.md b/doc/md/Plugin-System.md index 30f0ae74..cbec04c0 100644 --- a/doc/md/Plugin-System.md +++ b/doc/md/Plugin-System.md | |||
@@ -49,10 +49,10 @@ hook_<plugin_name>_<hook_name>($data, $conf) | |||
49 | 49 | ||
50 | Parameters: | 50 | Parameters: |
51 | 51 | ||
52 | - data: see [$data section](https://github.com/shaarli/Shaarli/wiki/Plugin-System#plugins-data) | 52 | - data: see [$data section](https://shaarli.readthedocs.io/en/master/Plugin-System/#plugins-data) |
53 | - conf: the `ConfigManager` instance. | 53 | - conf: the `ConfigManager` instance. |
54 | 54 | ||
55 | For exemple, if my plugin want to add data to the header, this function is needed: | 55 | For example, if my plugin want to add data to the header, this function is needed: |
56 | 56 | ||
57 | hook_demo_plugin_render_header | 57 | hook_demo_plugin_render_header |
58 | 58 | ||
diff --git a/doc/md/Plugins.md b/doc/md/Plugins.md index 7d40637f..463dae17 100644 --- a/doc/md/Plugins.md +++ b/doc/md/Plugins.md | |||
@@ -72,4 +72,4 @@ Usage of each plugin is documented in it's README file: | |||
72 | 72 | ||
73 | #### Third party plugins | 73 | #### Third party plugins |
74 | 74 | ||
75 | See [Community & related software](https://github.com/shaarli/Shaarli/wiki/Community-%26-Related-software#third-party-plugins) | 75 | See [Community & related software](https://shaarli.readthedocs.io/en/master/Community-&-Related-software/) |
diff --git a/doc/md/Release-Shaarli.md b/doc/md/Release-Shaarli.md index 974a7438..e22eabc9 100644 --- a/doc/md/Release-Shaarli.md +++ b/doc/md/Release-Shaarli.md | |||
@@ -46,6 +46,12 @@ TBA | |||
46 | 46 | ||
47 | 47 | ||
48 | ## Increment the version code, update docs, create and push a signed tag | 48 | ## Increment the version code, update docs, create and push a signed tag |
49 | ### Update the list of Git contributors | ||
50 | ```bash | ||
51 | $ make authors | ||
52 | $ git commit -s -m "Update AUTHORS" | ||
53 | ``` | ||
54 | |||
49 | ### Create and merge a Pull Request | 55 | ### Create and merge a Pull Request |
50 | This one is pretty straightforward ;-) | 56 | This one is pretty straightforward ;-) |
51 | 57 | ||
diff --git a/doc/md/Security.md b/doc/md/Security.md index 36f629af..65db4225 100644 --- a/doc/md/Security.md +++ b/doc/md/Security.md | |||
@@ -1,9 +1,6 @@ | |||
1 | ## Client browser | 1 | ## Client browser |
2 | - Shaarli relies on `HTTP_REFERER` for some functions (like redirects and clicking on tags). If you have disabled or masqueraded `HTTP_REFERER` in your browser, some features of Shaarli may not work | 2 | - Shaarli relies on `HTTP_REFERER` for some functions (like redirects and clicking on tags). If you have disabled or masqueraded `HTTP_REFERER` in your browser, some features of Shaarli may not work |
3 | 3 | ||
4 | ## PHP | ||
5 | - `magic_quotes` is an horrible option of PHP which is often activated on servers. No serious developer should rely on this horror to secure their code against SQL injections. You should disable it (and Shaarli expects this option to be disabled). Nevertheless, I have added code to cope with `magic_quotes` on, so you should not be bothered even on crappy hosts. | ||
6 | |||
7 | ## Server and sessions | 4 | ## Server and sessions |
8 | - Directories are protected using `.htaccess` files | 5 | - Directories are protected using `.htaccess` files |
9 | - Forms are protected against XSRF (Cross-site requests forgery): | 6 | - Forms are protected against XSRF (Cross-site requests forgery): |
diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md index 188a3c09..99b25ba7 100644 --- a/doc/md/Shaarli-configuration.md +++ b/doc/md/Shaarli-configuration.md | |||
@@ -55,6 +55,7 @@ _These settings should not be edited_ | |||
55 | - **links_per_page**: Number of shaares displayed per page. | 55 | - **links_per_page**: Number of shaares displayed per page. |
56 | - **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php). | 56 | - **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php). |
57 | - **enabled_plugins**: List of enabled plugins. | 57 | - **enabled_plugins**: List of enabled plugins. |
58 | - **default_note_title**: Default title of a new note. | ||
58 | 59 | ||
59 | ### Security | 60 | ### Security |
60 | 61 | ||
@@ -90,7 +91,10 @@ _These settings should not be edited_ | |||
90 | 91 | ||
91 | - **default_private_links**: Check the private checkbox by default for every new link. | 92 | - **default_private_links**: Check the private checkbox by default for every new link. |
92 | - **hide_public_links**: All links are hidden while logged out. | 93 | - **hide_public_links**: All links are hidden while logged out. |
94 | - **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page. | ||
93 | - **hide_timestamps**: Timestamps are hidden. | 95 | - **hide_timestamps**: Timestamps are hidden. |
96 | - **remember_user_default**: Default state of the login page's *remember me* checkbox | ||
97 | - `true`: checked by default, `false`: unchecked by default | ||
94 | 98 | ||
95 | ### Feed | 99 | ### Feed |
96 | 100 | ||
@@ -192,7 +196,9 @@ _These settings should not be edited_ | |||
192 | "privacy": { | 196 | "privacy": { |
193 | "default_private_links": true, | 197 | "default_private_links": true, |
194 | "hide_public_links": false, | 198 | "hide_public_links": false, |
195 | "hide_timestamps": false | 199 | "force_login": false, |
200 | "hide_timestamps": false, | ||
201 | "remember_user_default": true | ||
196 | }, | 202 | }, |
197 | "thumbnail": { | 203 | "thumbnail": { |
198 | "enable_thumbnails": true, | 204 | "enable_thumbnails": true, |
diff --git a/doc/md/Unit-tests-Docker.md b/doc/md/Unit-tests-Docker.md new file mode 100644 index 00000000..c2de7cc7 --- /dev/null +++ b/doc/md/Unit-tests-Docker.md | |||
@@ -0,0 +1,56 @@ | |||
1 | ## Running tests inside Docker containers | ||
2 | |||
3 | Read first: | ||
4 | |||
5 | - [Docker 101](docker/docker-101.md) | ||
6 | - [Docker resources](docker/resources.md) | ||
7 | - [Unit tests](Unit-tests.md) | ||
8 | |||
9 | ### Docker test images | ||
10 | |||
11 | Test Dockerfiles are located under `docker/tests/<distribution>/Dockerfile`, | ||
12 | and can be used to build Docker images to run Shaarli test suites under common | ||
13 | Linux environments. | ||
14 | |||
15 | Dockerfiles are provided for the following environments: | ||
16 | |||
17 | - `alpine36` - [Alpine 3.6](https://www.alpinelinux.org/downloads/) | ||
18 | - `debian8` - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldstable) | ||
19 | - `debian9` - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (stable) | ||
20 | - `ubuntu16` - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (LTS) | ||
21 | |||
22 | What's behind the curtains: | ||
23 | |||
24 | - each image provides: | ||
25 | - a base Linux OS | ||
26 | - Shaarli PHP dependencies (OS packages) | ||
27 | - test PHP dependencies (OS packages) | ||
28 | - Composer | ||
29 | - the local workspace is mapped to the container's `/shaarli/` directory, | ||
30 | - the files are rsync'd to so tests are run using a standard Linux user account | ||
31 | (running tests as `root` would bypass permission checks and may hide issues) | ||
32 | - the tests are run inside the container. | ||
33 | |||
34 | ### Building test images | ||
35 | |||
36 | ```bash | ||
37 | # build the Debian 9 Docker image | ||
38 | $ cd /path/to/shaarli | ||
39 | $ cd docker/test/debian9 | ||
40 | $ docker build -t shaarli-test:debian9 . | ||
41 | ``` | ||
42 | |||
43 | ### Running tests | ||
44 | |||
45 | ```bash | ||
46 | $ cd /path/to/shaarli | ||
47 | |||
48 | # install/update 3rd-party test dependencies | ||
49 | $ composer install --prefer-dist | ||
50 | |||
51 | # run tests using the freshly built image | ||
52 | $ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test | ||
53 | |||
54 | # run the full test campaign | ||
55 | $ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests | ||
56 | ``` | ||
diff --git a/doc/md/docker/docker-101.md b/doc/md/docker/docker-101.md index b02dd149..a9c00b85 100644 --- a/doc/md/docker/docker-101.md +++ b/doc/md/docker/docker-101.md | |||
@@ -60,3 +60,81 @@ wheezy: Pulling from debian | |||
60 | Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe | 60 | Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe |
61 | Status: Downloaded newer image for debian:wheezy | 61 | Status: Downloaded newer image for debian:wheezy |
62 | ``` | 62 | ``` |
63 | |||
64 | Docker re-uses layers already downloaded. In other words if you have images based on Alpine or some Ubuntu version for example, those can share disk space. | ||
65 | |||
66 | ### Start a container | ||
67 | A container is an instance created from an image, that can be run and that keeps running until its main process exits. Or until the user stops the container. | ||
68 | |||
69 | The simplest way to start a container from image is ``docker run``. It also pulls the image for you if it is not locally available. For more advanced use, refer to ``docker create``. | ||
70 | |||
71 | Stopped containers are not destroyed, unless you specify ``--rm``. To view all created, running and stopped containers, enter: | ||
72 | ```bash | ||
73 | $ docker ps -a | ||
74 | ``` | ||
75 | |||
76 | Some containers may be designed or configured to be restarted, others are not. Also remember both network ports and volumes of a container are created on start, and not editable later. | ||
77 | |||
78 | ### Access a running container | ||
79 | A running container is accessible using ``docker exec``, or ``docker copy``. You can use ``exec`` to start a root shell in the Shaarli container: | ||
80 | ```bash | ||
81 | $ docker exec -ti <container-name-or-id> bash | ||
82 | ``` | ||
83 | Note the names and ID's of containers are listed in ``docker ps``. You can even type only one or two letters of the ID, given they are unique. | ||
84 | |||
85 | Access can also be through one or more network ports, or disk volumes. Both are specified on and fixed on ``docker create`` or ``run``. | ||
86 | |||
87 | You can view the console output of the main container process too: | ||
88 | ```bash | ||
89 | $ docker logs -f <container-name-or-id> | ||
90 | ``` | ||
91 | |||
92 | ### Docker disk use | ||
93 | Trying out different images can fill some gigabytes of disk quickly. Besides images, the docker volumes usually take up most disk space. | ||
94 | |||
95 | If you care only about trying out docker and not about what is running or saved, the following commands should help you out quickly if you run low on disk space: | ||
96 | |||
97 | ```bash | ||
98 | $ docker rmi -f $(docker images -aq) # remove or mark all images for disposal | ||
99 | $ docker volume rm $(docker volume ls -q) # remove all volumes | ||
100 | ``` | ||
101 | |||
102 | ### Systemd config | ||
103 | Systemd is the process manager of choice on Debian-based distributions. Once you have a ``docker`` service installed, you can use the following steps to set up Shaarli to run on system start. | ||
104 | |||
105 | ```bash | ||
106 | systemctl enable /etc/systemd/system/docker.shaarli.service | ||
107 | systemctl start docker.shaarli | ||
108 | systemctl status docker.* | ||
109 | journalctl -f # inspect system log if needed | ||
110 | ``` | ||
111 | |||
112 | You will need sudo or a root terminal to perform some or all of the steps above. Here are the contents for the service file: | ||
113 | ``` | ||
114 | [Unit] | ||
115 | Description=Shaarli Bookmark Manager Container | ||
116 | After=docker.service | ||
117 | Requires=docker.service | ||
118 | |||
119 | |||
120 | [Service] | ||
121 | Restart=always | ||
122 | |||
123 | # Put any environment you want in an included file, like $host- or $domainname in this example | ||
124 | EnvironmentFile=/etc/sysconfig/box-environment | ||
125 | |||
126 | # It's just an example.. | ||
127 | ExecStart=/usr/bin/docker run \ | ||
128 | -p 28010:80 \ | ||
129 | --name ${hostname}-shaarli \ | ||
130 | --hostname shaarli.${domainname} \ | ||
131 | -v /srv/docker-volumes-local/shaarli-data:/var/www/shaarli/data:rw \ | ||
132 | -v /etc/localtime:/etc/localtime:ro \ | ||
133 | shaarli/shaarli:latest | ||
134 | |||
135 | ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli | ||
136 | |||
137 | |||
138 | [Install] | ||
139 | WantedBy=multi-user.target | ||
140 | ``` | ||
diff --git a/doc/md/index.md b/doc/md/index.md index b10e3cf4..2b7d0f00 100644 --- a/doc/md/index.md +++ b/doc/md/index.md | |||
@@ -22,6 +22,17 @@ It runs the latest development version of Shaarli and is updated/reset daily. | |||
22 | 22 | ||
23 | Login: `demo`; Password: `demo` | 23 | Login: `demo`; Password: `demo` |
24 | 24 | ||
25 | Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage): | ||
26 | ``` | ||
27 | MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P) | ||
28 | docker run -ti --rm \ | ||
29 | -p 8000:80 \ | ||
30 | -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \ | ||
31 | shaarli/shaarli | ||
32 | ``` | ||
33 | |||
34 | A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101). | ||
35 | To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](Upgrade-and-migration) documentation. | ||
25 | 36 | ||
26 | ## Features | 37 | ## Features |
27 | 38 | ||
@@ -37,7 +48,7 @@ Login: `demo`; Password: `demo` | |||
37 | - daily RSS feed | 48 | - daily RSS feed |
38 | - permalinks for easy reference | 49 | - permalinks for easy reference |
39 | - links can be public or private | 50 | - links can be public or private |
40 | - extensible through [plugins](https://github.com/shaarli/Shaarli/wiki/Plugins#plugin-usage) | 51 | - extensible through [plugins](https://shaarli.readthedocs.io/en/master/Plugins/#plugin-usage) |
41 | 52 | ||
42 | ### Tag, view and search your links! | 53 | ### Tag, view and search your links! |
43 | - add a custom title and description to archived links | 54 | - add a custom title and description to archived links |
diff --git a/docker/test/alpine36/Dockerfile b/docker/test/alpine36/Dockerfile new file mode 100644 index 00000000..fa84f6e2 --- /dev/null +++ b/docker/test/alpine36/Dockerfile | |||
@@ -0,0 +1,34 @@ | |||
1 | FROM alpine:3.6 | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | RUN apk --update --no-cache add \ | ||
5 | ca-certificates \ | ||
6 | curl \ | ||
7 | make \ | ||
8 | php7 \ | ||
9 | php7-ctype \ | ||
10 | php7-curl \ | ||
11 | php7-dom \ | ||
12 | php7-gd \ | ||
13 | php7-iconv \ | ||
14 | php7-intl \ | ||
15 | php7-json \ | ||
16 | php7-mbstring \ | ||
17 | php7-openssl \ | ||
18 | php7-phar \ | ||
19 | php7-session \ | ||
20 | php7-simplexml \ | ||
21 | php7-tokenizer \ | ||
22 | php7-xdebug \ | ||
23 | php7-xml \ | ||
24 | php7-zlib \ | ||
25 | rsync | ||
26 | |||
27 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer | ||
28 | |||
29 | RUN mkdir /shaarli | ||
30 | WORKDIR /shaarli | ||
31 | VOLUME /shaarli | ||
32 | |||
33 | ENTRYPOINT ["make"] | ||
34 | CMD [] | ||
diff --git a/docker/test/debian8/Dockerfile b/docker/test/debian8/Dockerfile new file mode 100644 index 00000000..eaa34e9b --- /dev/null +++ b/docker/test/debian8/Dockerfile | |||
@@ -0,0 +1,35 @@ | |||
1 | FROM debian:jessie | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | ENV TERM dumb | ||
5 | ENV DEBIAN_FRONTEND noninteractive | ||
6 | ENV LANG en_US.UTF-8 | ||
7 | ENV LANGUAGE en_US:en | ||
8 | |||
9 | RUN apt-get update \ | ||
10 | && apt-get install --no-install-recommends -y \ | ||
11 | ca-certificates \ | ||
12 | curl \ | ||
13 | locales \ | ||
14 | make \ | ||
15 | php5 \ | ||
16 | php5-curl \ | ||
17 | php5-gd \ | ||
18 | php5-intl \ | ||
19 | php5-xdebug \ | ||
20 | rsync \ | ||
21 | && apt-get clean | ||
22 | |||
23 | RUN locale-gen en_US.UTF-8 \ | ||
24 | && locale-gen de_DE.UTF-8 \ | ||
25 | && locale-gen fr_FR.UTF-8 | ||
26 | |||
27 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer | ||
28 | RUN chmod 755 /usr/local/bin/composer | ||
29 | |||
30 | RUN mkdir /shaarli | ||
31 | WORKDIR /shaarli | ||
32 | VOLUME /shaarli | ||
33 | |||
34 | ENTRYPOINT ["make"] | ||
35 | CMD [] | ||
diff --git a/docker/test/debian9/Dockerfile b/docker/test/debian9/Dockerfile new file mode 100644 index 00000000..3ab4b93d --- /dev/null +++ b/docker/test/debian9/Dockerfile | |||
@@ -0,0 +1,36 @@ | |||
1 | FROM debian:stretch | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | ENV TERM dumb | ||
5 | ENV DEBIAN_FRONTEND noninteractive | ||
6 | ENV LANG en_US.UTF-8 | ||
7 | ENV LANGUAGE en_US:en | ||
8 | |||
9 | RUN apt-get update \ | ||
10 | && apt-get install --no-install-recommends -y \ | ||
11 | ca-certificates \ | ||
12 | curl \ | ||
13 | locales \ | ||
14 | make \ | ||
15 | php7.0 \ | ||
16 | php7.0-curl \ | ||
17 | php7.0-gd \ | ||
18 | php7.0-intl \ | ||
19 | php7.0-xml \ | ||
20 | php-xdebug \ | ||
21 | rsync \ | ||
22 | && apt-get clean | ||
23 | |||
24 | RUN locale-gen en_US.UTF-8 \ | ||
25 | && locale-gen de_DE.UTF-8 \ | ||
26 | && locale-gen fr_FR.UTF-8 | ||
27 | |||
28 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer | ||
29 | RUN chmod 755 /usr/local/bin/composer | ||
30 | |||
31 | RUN mkdir /shaarli | ||
32 | WORKDIR /shaarli | ||
33 | VOLUME /shaarli | ||
34 | |||
35 | ENTRYPOINT ["make"] | ||
36 | CMD [] | ||
diff --git a/docker/test/ubuntu16/Dockerfile b/docker/test/ubuntu16/Dockerfile new file mode 100644 index 00000000..e53ed9e3 --- /dev/null +++ b/docker/test/ubuntu16/Dockerfile | |||
@@ -0,0 +1,36 @@ | |||
1 | FROM ubuntu:16.04 | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | ENV TERM dumb | ||
5 | ENV DEBIAN_FRONTEND noninteractive | ||
6 | ENV LANG en_US.UTF-8 | ||
7 | ENV LANGUAGE en_US:en | ||
8 | |||
9 | RUN apt-get update \ | ||
10 | && apt-get install --no-install-recommends -y \ | ||
11 | ca-certificates \ | ||
12 | curl \ | ||
13 | language-pack-de \ | ||
14 | language-pack-en \ | ||
15 | language-pack-fr \ | ||
16 | locales \ | ||
17 | make \ | ||
18 | php7.0 \ | ||
19 | php7.0-curl \ | ||
20 | php7.0-gd \ | ||
21 | php7.0-intl \ | ||
22 | php7.0-xml \ | ||
23 | php-xdebug \ | ||
24 | rsync \ | ||
25 | && apt-get clean | ||
26 | |||
27 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer | ||
28 | RUN chmod 755 /usr/local/bin/composer | ||
29 | |||
30 | RUN useradd -m dev \ | ||
31 | && mkdir /shaarli | ||
32 | USER dev | ||
33 | WORKDIR /shaarli | ||
34 | |||
35 | ENTRYPOINT ["make"] | ||
36 | CMD [] | ||
@@ -48,8 +48,8 @@ if (! file_exists(__DIR__ . '/vendor/autoload.php')) { | |||
48 | ."If you installed Shaarli through Git or using the development branch,\n" | 48 | ."If you installed Shaarli through Git or using the development branch,\n" |
49 | ."please refer to the installation documentation to install PHP" | 49 | ."please refer to the installation documentation to install PHP" |
50 | ." dependencies using Composer:\n" | 50 | ." dependencies using Composer:\n" |
51 | ."- https://github.com/shaarli/Shaarli/wiki/Server-requirements\n" | 51 | ."- https://shaarli.readthedocs.io/en/master/Server-requirements/\n" |
52 | ."- https://github.com/shaarli/Shaarli/wiki/Download-and-Installation"; | 52 | ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/"; |
53 | exit; | 53 | exit; |
54 | } | 54 | } |
55 | require_once 'inc/rain.tpl.class.php'; | 55 | require_once 'inc/rain.tpl.class.php'; |
@@ -88,7 +88,7 @@ try { | |||
88 | exit; | 88 | exit; |
89 | } | 89 | } |
90 | 90 | ||
91 | define('shaarli_version', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); | 91 | define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); |
92 | 92 | ||
93 | // Force cookie path (but do not change lifetime) | 93 | // Force cookie path (but do not change lifetime) |
94 | $cookie = session_get_cookie_params(); | 94 | $cookie = session_get_cookie_params(); |
@@ -133,15 +133,6 @@ date_default_timezone_set($conf->get('general.timezone', 'UTC')); | |||
133 | 133 | ||
134 | ob_start(); // Output buffering for the page cache. | 134 | ob_start(); // Output buffering for the page cache. |
135 | 135 | ||
136 | // In case stupid admin has left magic_quotes enabled in php.ini: | ||
137 | if (get_magic_quotes_gpc()) | ||
138 | { | ||
139 | function stripslashes_deep($value) { $value = is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value); return $value; } | ||
140 | $_POST = array_map('stripslashes_deep', $_POST); | ||
141 | $_GET = array_map('stripslashes_deep', $_GET); | ||
142 | $_COOKIE = array_map('stripslashes_deep', $_COOKIE); | ||
143 | } | ||
144 | |||
145 | // Prevent caching on client side or proxy: (yes, it's ugly) | 136 | // Prevent caching on client side or proxy: (yes, it's ugly) |
146 | header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); | 137 | header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); |
147 | header("Cache-Control: no-store, no-cache, must-revalidate"); | 138 | header("Cache-Control: no-store, no-cache, must-revalidate"); |
@@ -186,42 +177,42 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { | |||
186 | */ | 177 | */ |
187 | function setup_login_state($conf) | 178 | function setup_login_state($conf) |
188 | { | 179 | { |
189 | if ($conf->get('security.open_shaarli')) { | 180 | if ($conf->get('security.open_shaarli')) { |
190 | return true; | 181 | return true; |
191 | } | 182 | } |
192 | $userIsLoggedIn = false; // By default, we do not consider the user as logged in; | 183 | $userIsLoggedIn = false; // By default, we do not consider the user as logged in; |
193 | $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met. | 184 | $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met. |
194 | if (! $conf->exists('credentials.login')) { | 185 | if (! $conf->exists('credentials.login')) { |
195 | $userIsLoggedIn = false; // Shaarli is not configured yet. | 186 | $userIsLoggedIn = false; // Shaarli is not configured yet. |
196 | $loginFailure = true; | 187 | $loginFailure = true; |
197 | } | 188 | } |
198 | if (isset($_COOKIE['shaarli_staySignedIn']) && | 189 | if (isset($_COOKIE['shaarli_staySignedIn']) && |
199 | $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN && | 190 | $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN && |
200 | !$loginFailure) | 191 | !$loginFailure) |
201 | { | 192 | { |
202 | fillSessionInfo($conf); | 193 | fillSessionInfo($conf); |
203 | $userIsLoggedIn = true; | 194 | $userIsLoggedIn = true; |
204 | } | 195 | } |
205 | // If session does not exist on server side, or IP address has changed, or session has expired, logout. | 196 | // If session does not exist on server side, or IP address has changed, or session has expired, logout. |
206 | if (empty($_SESSION['uid']) | 197 | if (empty($_SESSION['uid']) |
207 | || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs()) | 198 | || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs()) |
208 | || time() >= $_SESSION['expires_on']) | 199 | || time() >= $_SESSION['expires_on']) |
209 | { | 200 | { |
210 | logout(); | 201 | logout(); |
211 | $userIsLoggedIn = false; | 202 | $userIsLoggedIn = false; |
212 | $loginFailure = true; | 203 | $loginFailure = true; |
213 | } | 204 | } |
214 | if (!empty($_SESSION['longlastingsession'])) { | 205 | if (!empty($_SESSION['longlastingsession'])) { |
215 | $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked. | 206 | $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked. |
216 | } | 207 | } |
217 | else { | 208 | else { |
218 | $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date. | 209 | $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date. |
219 | } | 210 | } |
220 | if (!$loginFailure) { | 211 | if (!$loginFailure) { |
221 | $userIsLoggedIn = true; | 212 | $userIsLoggedIn = true; |
222 | } | 213 | } |
223 | 214 | ||
224 | return $userIsLoggedIn; | 215 | return $userIsLoggedIn; |
225 | } | 216 | } |
226 | $userIsLoggedIn = setup_login_state($conf); | 217 | $userIsLoggedIn = setup_login_state($conf); |
227 | 218 | ||
@@ -245,10 +236,10 @@ function allIPs() | |||
245 | */ | 236 | */ |
246 | function fillSessionInfo($conf) | 237 | function fillSessionInfo($conf) |
247 | { | 238 | { |
248 | $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid) | 239 | $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid) |
249 | $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked. | 240 | $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked. |
250 | $_SESSION['username']= $conf->get('credentials.login'); | 241 | $_SESSION['username']= $conf->get('credentials.login'); |
251 | $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. | 242 | $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. |
252 | } | 243 | } |
253 | 244 | ||
254 | /** | 245 | /** |
@@ -265,7 +256,7 @@ function check_auth($login, $password, $conf) | |||
265 | $hash = sha1($password . $login . $conf->get('credentials.salt')); | 256 | $hash = sha1($password . $login . $conf->get('credentials.salt')); |
266 | if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash')) | 257 | if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash')) |
267 | { // Login/password is correct. | 258 | { // Login/password is correct. |
268 | fillSessionInfo($conf); | 259 | fillSessionInfo($conf); |
269 | logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful'); | 260 | logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful'); |
270 | return true; | 261 | return true; |
271 | } | 262 | } |
@@ -394,9 +385,10 @@ if (isset($_POST['login'])) | |||
394 | // If user wants to keep the session cookie even after the browser closes: | 385 | // If user wants to keep the session cookie even after the browser closes: |
395 | if (!empty($_POST['longlastingsession'])) | 386 | if (!empty($_POST['longlastingsession'])) |
396 | { | 387 | { |
397 | setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, time()+31536000, WEB_PATH); | 388 | $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year) |
398 | $_SESSION['longlastingsession']=31536000; // (31536000 seconds = 1 year) | 389 | $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now) |
399 | $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // Set session expiration on server-side. | 390 | setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH); |
391 | $_SESSION['expires_on'] = $expiration; // Set session expiration on server-side. | ||
400 | 392 | ||
401 | $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; | 393 | $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; |
402 | session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side | 394 | session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side |
@@ -591,20 +583,29 @@ function showDailyRSS($conf) { | |||
591 | */ | 583 | */ |
592 | function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) | 584 | function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) |
593 | { | 585 | { |
594 | $day=date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD. | 586 | $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD. |
595 | if (isset($_GET['day'])) $day=$_GET['day']; | 587 | if (isset($_GET['day'])) { |
588 | $day = $_GET['day']; | ||
589 | } | ||
596 | 590 | ||
597 | $days = $LINKSDB->days(); | 591 | $days = $LINKSDB->days(); |
598 | $i = array_search($day,$days); | 592 | $i = array_search($day, $days); |
599 | if ($i===false) { $i=count($days)-1; $day=$days[$i]; } | 593 | if ($i === false && count($days)) { |
600 | $previousday=''; | 594 | // no links for day, but at least one day with links |
601 | $nextday=''; | 595 | $i = count($days) - 1; |
602 | if ($i!==false) | 596 | $day = $days[$i]; |
603 | { | ||
604 | if ($i>=1) $previousday=$days[$i-1]; | ||
605 | if ($i<count($days)-1) $nextday=$days[$i+1]; | ||
606 | } | 597 | } |
598 | $previousday = ''; | ||
599 | $nextday = ''; | ||
607 | 600 | ||
601 | if ($i !== false) { | ||
602 | if ($i >= 1) { | ||
603 | $previousday=$days[$i - 1]; | ||
604 | } | ||
605 | if ($i < count($days) - 1) { | ||
606 | $nextday = $days[$i + 1]; | ||
607 | } | ||
608 | } | ||
608 | try { | 609 | try { |
609 | $linksToDisplay = $LINKSDB->filterDay($day); | 610 | $linksToDisplay = $LINKSDB->filterDay($day); |
610 | } catch (Exception $exc) { | 611 | } catch (Exception $exc) { |
@@ -613,9 +614,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) | |||
613 | } | 614 | } |
614 | 615 | ||
615 | // We pre-format some fields for proper output. | 616 | // We pre-format some fields for proper output. |
616 | foreach($linksToDisplay as $key=>$link) | 617 | foreach($linksToDisplay as $key => $link) { |
617 | { | ||
618 | |||
619 | $taglist = explode(' ',$link['tags']); | 618 | $taglist = explode(' ',$link['tags']); |
620 | uasort($taglist, 'strcasecmp'); | 619 | uasort($taglist, 'strcasecmp'); |
621 | $linksToDisplay[$key]['taglist']=$taglist; | 620 | $linksToDisplay[$key]['taglist']=$taglist; |
@@ -629,21 +628,22 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) | |||
629 | so I manually spread entries with a simple method: I roughly evaluate the | 628 | so I manually spread entries with a simple method: I roughly evaluate the |
630 | height of a div according to title and description length. | 629 | height of a div according to title and description length. |
631 | */ | 630 | */ |
632 | $columns=array(array(),array(),array()); // Entries to display, for each column. | 631 | $columns = array(array(), array(), array()); // Entries to display, for each column. |
633 | $fill=array(0,0,0); // Rough estimate of columns fill. | 632 | $fill = array(0, 0, 0); // Rough estimate of columns fill. |
634 | foreach($linksToDisplay as $key=>$link) | 633 | foreach($linksToDisplay as $key => $link) { |
635 | { | ||
636 | // Roughly estimate length of entry (by counting characters) | 634 | // Roughly estimate length of entry (by counting characters) |
637 | // Title: 30 chars = 1 line. 1 line is 30 pixels height. | 635 | // Title: 30 chars = 1 line. 1 line is 30 pixels height. |
638 | // Description: 836 characters gives roughly 342 pixel height. | 636 | // Description: 836 characters gives roughly 342 pixel height. |
639 | // This is not perfect, but it's usually OK. | 637 | // This is not perfect, but it's usually OK. |
640 | $length=strlen($link['title'])+(342*strlen($link['description']))/836; | 638 | $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836; |
641 | if ($link['thumbnail']) $length +=100; // 1 thumbnails roughly takes 100 pixels height. | 639 | if ($link['thumbnail']) { |
640 | $length += 100; // 1 thumbnails roughly takes 100 pixels height. | ||
641 | } | ||
642 | // Then put in column which is the less filled: | 642 | // Then put in column which is the less filled: |
643 | $smallest=min($fill); // find smallest value in array. | 643 | $smallest = min($fill); // find smallest value in array. |
644 | $index=array_search($smallest,$fill); // find index of this smallest value. | 644 | $index = array_search($smallest, $fill); // find index of this smallest value. |
645 | array_push($columns[$index],$link); // Put entry in this column. | 645 | array_push($columns[$index], $link); // Put entry in this column. |
646 | $fill[$index]+=$length; | 646 | $fill[$index] += $length; |
647 | } | 647 | } |
648 | 648 | ||
649 | $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); | 649 | $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); |
@@ -718,6 +718,23 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
718 | $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; | 718 | $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; |
719 | $targetPage = Router::findPage($query, $_GET, isLoggedIn()); | 719 | $targetPage = Router::findPage($query, $_GET, isLoggedIn()); |
720 | 720 | ||
721 | if ( | ||
722 | // if the user isn't logged in | ||
723 | !isLoggedIn() && | ||
724 | // and Shaarli doesn't have public content... | ||
725 | $conf->get('privacy.hide_public_links') && | ||
726 | // and is configured to enforce the login | ||
727 | $conf->get('privacy.force_login') && | ||
728 | // and the current page isn't already the login page | ||
729 | $targetPage !== Router::$PAGE_LOGIN && | ||
730 | // and the user is not requesting a feed (which would lead to a different content-type as expected) | ||
731 | $targetPage !== Router::$PAGE_FEED_ATOM && | ||
732 | $targetPage !== Router::$PAGE_FEED_RSS | ||
733 | ) { | ||
734 | // force current page to be the login page | ||
735 | $targetPage = Router::$PAGE_LOGIN; | ||
736 | } | ||
737 | |||
721 | // Call plugin hooks for header, footer and includes, specifying which page will be rendered. | 738 | // Call plugin hooks for header, footer and includes, specifying which page will be rendered. |
722 | // Then assign generated data to RainTPL. | 739 | // Then assign generated data to RainTPL. |
723 | $common_hooks = array( | 740 | $common_hooks = array( |
@@ -745,6 +762,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
745 | $PAGE->assign('username', escape($_GET['username'])); | 762 | $PAGE->assign('username', escape($_GET['username'])); |
746 | } | 763 | } |
747 | $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):'')); | 764 | $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):'')); |
765 | // add default state of the 'remember me' checkbox | ||
766 | $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default')); | ||
748 | $PAGE->renderPage('loginform'); | 767 | $PAGE->renderPage('loginform'); |
749 | exit; | 768 | exit; |
750 | } | 769 | } |
@@ -803,7 +822,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
803 | $maxcount = max($maxcount, $value); | 822 | $maxcount = max($maxcount, $value); |
804 | } | 823 | } |
805 | 824 | ||
806 | alphabetical_sort($tags, true, true); | 825 | alphabetical_sort($tags, false, true); |
807 | 826 | ||
808 | $tagList = array(); | 827 | $tagList = array(); |
809 | foreach($tags as $key => $value) { | 828 | foreach($tags as $key => $value) { |
@@ -821,7 +840,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
821 | } | 840 | } |
822 | 841 | ||
823 | $data = array( | 842 | $data = array( |
824 | 'search_tags' => implode(' ', $filteringTags), | 843 | 'search_tags' => implode(' ', escape($filteringTags)), |
825 | 'tags' => $tagList, | 844 | 'tags' => $tagList, |
826 | ); | 845 | ); |
827 | $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); | 846 | $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); |
@@ -851,7 +870,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
851 | } | 870 | } |
852 | 871 | ||
853 | $data = [ | 872 | $data = [ |
854 | 'search_tags' => implode(' ', $filteringTags), | 873 | 'search_tags' => implode(' ', escape($filteringTags)), |
855 | 'tags' => $tags, | 874 | 'tags' => $tags, |
856 | ]; | 875 | ]; |
857 | $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); | 876 | $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); |
@@ -1063,10 +1082,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1063 | // -------- Display the Tools menu if requested (import/export/bookmarklet...) | 1082 | // -------- Display the Tools menu if requested (import/export/bookmarklet...) |
1064 | if ($targetPage == Router::$PAGE_TOOLS) | 1083 | if ($targetPage == Router::$PAGE_TOOLS) |
1065 | { | 1084 | { |
1066 | $data = array( | 1085 | $data = [ |
1067 | 'pageabsaddr' => index_url($_SERVER), | 1086 | 'pageabsaddr' => index_url($_SERVER), |
1068 | 'sslenabled' => !empty($_SERVER['HTTPS']) | 1087 | 'sslenabled' => is_https($_SERVER), |
1069 | ); | 1088 | ]; |
1070 | $pluginManager->executeHooks('render_tools', $data); | 1089 | $pluginManager->executeHooks('render_tools', $data); |
1071 | 1090 | ||
1072 | foreach ($data as $key => $value) { | 1091 | foreach ($data as $key => $value) { |
@@ -1233,7 +1252,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1233 | // Linkdate is kept here to: | 1252 | // Linkdate is kept here to: |
1234 | // - use the same permalink for notes as they're displayed when creating them | 1253 | // - use the same permalink for notes as they're displayed when creating them |
1235 | // - let users hack creation date of their posts | 1254 | // - let users hack creation date of their posts |
1236 | // See: https://github.com/shaarli/Shaarli/wiki/Datastore-hacks#changing-the-timestamp-for-a-link | 1255 | // See: https://shaarli.readthedocs.io/en/master/Various-hacks/#changing-the-timestamp-for-a-shaare |
1237 | $linkdate = escape($_POST['lf_linkdate']); | 1256 | $linkdate = escape($_POST['lf_linkdate']); |
1238 | if (isset($LINKSDB[$id])) { | 1257 | if (isset($LINKSDB[$id])) { |
1239 | // Edit | 1258 | // Edit |
@@ -1256,6 +1275,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1256 | // Remove duplicates. | 1275 | // Remove duplicates. |
1257 | $tags = implode(' ', array_unique(explode(' ', $tags))); | 1276 | $tags = implode(' ', array_unique(explode(' ', $tags))); |
1258 | 1277 | ||
1278 | if (empty(trim($_POST['lf_url']))) { | ||
1279 | $_POST['lf_url'] = '?' . smallHash($linkdate . $id); | ||
1280 | } | ||
1259 | $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols')); | 1281 | $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols')); |
1260 | 1282 | ||
1261 | $link = array( | 1283 | $link = array( |
@@ -1325,10 +1347,17 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1325 | die('Wrong token.'); | 1347 | die('Wrong token.'); |
1326 | } | 1348 | } |
1327 | 1349 | ||
1328 | if (strpos($_GET['lf_linkdate'], ' ') !== false) { | 1350 | $ids = trim($_GET['lf_linkdate']); |
1329 | $ids = array_values(array_filter(preg_split('/\s+/', escape($_GET['lf_linkdate'])))); | 1351 | if (strpos($ids, ' ') !== false) { |
1352 | // multiple, space-separated ids provided | ||
1353 | $ids = array_values(array_filter(preg_split('/\s+/', escape($ids)))); | ||
1330 | } else { | 1354 | } else { |
1331 | $ids = [$_GET['lf_linkdate']]; | 1355 | // only a single id provided |
1356 | $ids = [$ids]; | ||
1357 | } | ||
1358 | // assert at least one id is given | ||
1359 | if(!count($ids)){ | ||
1360 | die('no id provided'); | ||
1332 | } | 1361 | } |
1333 | foreach ($ids as $id) { | 1362 | foreach ($ids as $id) { |
1334 | $id = (int) escape($id); | 1363 | $id = (int) escape($id); |
@@ -1414,7 +1443,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1414 | 1443 | ||
1415 | if ($url == '') { | 1444 | if ($url == '') { |
1416 | $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); | 1445 | $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); |
1417 | $title = 'Note: '; | 1446 | $title = $conf->get('general.default_note_title', 'Note: '); |
1418 | } | 1447 | } |
1419 | $url = escape($url); | 1448 | $url = escape($url); |
1420 | $title = escape($title); | 1449 | $title = escape($title); |
@@ -45,6 +45,7 @@ pages: | |||
45 | - Static analysis: Static-analysis.md | 45 | - Static analysis: Static-analysis.md |
46 | - Theming: Theming.md | 46 | - Theming: Theming.md |
47 | - Unit tests: Unit-tests.md | 47 | - Unit tests: Unit-tests.md |
48 | - Unit tests inside Docker: Unit-tests-Docker.md | ||
48 | - About: | 49 | - About: |
49 | - FAQ: FAQ.md | 50 | - FAQ: FAQ.md |
50 | - Community & Related software: Community-&-Related-software.md | 51 | - Community & Related software: Community-&-Related-software.md |
diff --git a/plugins/playvideos/README.md b/plugins/playvideos/README.md index b1698470..ab4be22a 100644 --- a/plugins/playvideos/README.md +++ b/plugins/playvideos/README.md | |||
@@ -8,7 +8,7 @@ This uses code from https://zaius.github.io/youtube_playlist/ and is currently o | |||
8 | 8 | ||
9 | #### Installation and setup | 9 | #### Installation and setup |
10 | 10 | ||
11 | This is a default Shaarli plugin, you just have to enable it. See https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration/ | 11 | This is a default Shaarli plugin, you just have to enable it. See https://shaarli.readthedocs.io/en/master/Shaarli-configuration/ |
12 | 12 | ||
13 | 13 | ||
14 | #### Troubleshooting | 14 | #### Troubleshooting |
diff --git a/tests/HttpUtils/IsHttpsTest.php b/tests/HttpUtils/IsHttpsTest.php new file mode 100644 index 00000000..097f2bcf --- /dev/null +++ b/tests/HttpUtils/IsHttpsTest.php | |||
@@ -0,0 +1,36 @@ | |||
1 | <?php | ||
2 | |||
3 | |||
4 | /** | ||
5 | * Class IsHttpsTest | ||
6 | * | ||
7 | * Test class for is_https() function. | ||
8 | */ | ||
9 | class IsHttpsTest extends PHPUnit_Framework_TestCase | ||
10 | { | ||
11 | |||
12 | /** | ||
13 | * Test is_https with HTTPS values. | ||
14 | */ | ||
15 | public function testIsHttpsTrue() | ||
16 | { | ||
17 | $this->assertTrue(is_https(['HTTPS' => true])); | ||
18 | $this->assertTrue(is_https(['HTTPS' => '1'])); | ||
19 | $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => 443])); | ||
20 | $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '443'])); | ||
21 | $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '443,123,456,'])); | ||
22 | } | ||
23 | |||
24 | /** | ||
25 | * Test is_https with HTTP values. | ||
26 | */ | ||
27 | public function testIsHttpsFalse() | ||
28 | { | ||
29 | $this->assertFalse(is_https([])); | ||
30 | $this->assertFalse(is_https(['HTTPS' => false])); | ||
31 | $this->assertFalse(is_https(['HTTPS' => '0'])); | ||
32 | $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => 123])); | ||
33 | $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '123'])); | ||
34 | $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => ',123,456,'])); | ||
35 | } | ||
36 | } | ||
diff --git a/tests/LinkUtilsTest.php b/tests/LinkUtilsTest.php index 7c0d4b0b..c77922ec 100644 --- a/tests/LinkUtilsTest.php +++ b/tests/LinkUtilsTest.php | |||
@@ -103,6 +103,16 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase | |||
103 | $expectedText = 'stuff <a href="http://hello.there/is=someone#here">http://hello.there/is=someone#here</a> otherstuff'; | 103 | $expectedText = 'stuff <a href="http://hello.there/is=someone#here">http://hello.there/is=someone#here</a> otherstuff'; |
104 | $processedText = text2clickable($text, ''); | 104 | $processedText = text2clickable($text, ''); |
105 | $this->assertEquals($expectedText, $processedText); | 105 | $this->assertEquals($expectedText, $processedText); |
106 | |||
107 | $text = 'stuff http://hello.there/is=someone#here(please) otherstuff'; | ||
108 | $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)">http://hello.there/is=someone#here(please)</a> otherstuff'; | ||
109 | $processedText = text2clickable($text, ''); | ||
110 | $this->assertEquals($expectedText, $processedText); | ||
111 | |||
112 | $text = 'stuff http://hello.there/is=someone#here(please)&no otherstuff'; | ||
113 | $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)&no">http://hello.there/is=someone#here(please)&no</a> otherstuff'; | ||
114 | $processedText = text2clickable($text, ''); | ||
115 | $this->assertEquals($expectedText, $processedText); | ||
106 | } | 116 | } |
107 | 117 | ||
108 | /** | 118 | /** |
diff --git a/tests/api/controllers/GetLinksTest.php b/tests/api/controllers/GetLinksTest.php index 4cb70224..d22ed3bf 100644 --- a/tests/api/controllers/GetLinksTest.php +++ b/tests/api/controllers/GetLinksTest.php | |||
@@ -367,6 +367,89 @@ class GetLinksTest extends \PHPUnit_Framework_TestCase | |||
367 | $this->assertEquals(1, count($data)); | 367 | $this->assertEquals(1, count($data)); |
368 | $this->assertEquals(41, $data[0]['id']); | 368 | $this->assertEquals(41, $data[0]['id']); |
369 | $this->assertEquals(self::NB_FIELDS_LINK, count($data[0])); | 369 | $this->assertEquals(self::NB_FIELDS_LINK, count($data[0])); |
370 | |||
371 | // wildcard: placeholder at the start | ||
372 | $env = Environment::mock([ | ||
373 | 'REQUEST_METHOD' => 'GET', | ||
374 | 'QUERY_STRING' => 'searchtags=*Tuff', | ||
375 | ]); | ||
376 | $request = Request::createFromEnvironment($env); | ||
377 | $response = $this->controller->getLinks($request, new Response()); | ||
378 | $this->assertEquals(200, $response->getStatusCode()); | ||
379 | $data = json_decode((string) $response->getBody(), true); | ||
380 | $this->assertEquals(2, count($data)); | ||
381 | $this->assertEquals(41, $data[0]['id']); | ||
382 | |||
383 | // wildcard: placeholder at the end | ||
384 | $env = Environment::mock([ | ||
385 | 'REQUEST_METHOD' => 'GET', | ||
386 | 'QUERY_STRING' => 'searchtags=c*', | ||
387 | ]); | ||
388 | $request = Request::createFromEnvironment($env); | ||
389 | $response = $this->controller->getLinks($request, new Response()); | ||
390 | $this->assertEquals(200, $response->getStatusCode()); | ||
391 | $data = json_decode((string) $response->getBody(), true); | ||
392 | $this->assertEquals(4, count($data)); | ||
393 | $this->assertEquals(6, $data[0]['id']); | ||
394 | |||
395 | // wildcard: placeholder at the middle | ||
396 | $env = Environment::mock([ | ||
397 | 'REQUEST_METHOD' => 'GET', | ||
398 | 'QUERY_STRING' => 'searchtags=w*b', | ||
399 | ]); | ||
400 | $request = Request::createFromEnvironment($env); | ||
401 | $response = $this->controller->getLinks($request, new Response()); | ||
402 | $this->assertEquals(200, $response->getStatusCode()); | ||
403 | $data = json_decode((string) $response->getBody(), true); | ||
404 | $this->assertEquals(4, count($data)); | ||
405 | $this->assertEquals(6, $data[0]['id']); | ||
406 | |||
407 | // wildcard: match all | ||
408 | $env = Environment::mock([ | ||
409 | 'REQUEST_METHOD' => 'GET', | ||
410 | 'QUERY_STRING' => 'searchtags=*', | ||
411 | ]); | ||
412 | $request = Request::createFromEnvironment($env); | ||
413 | $response = $this->controller->getLinks($request, new Response()); | ||
414 | $this->assertEquals(200, $response->getStatusCode()); | ||
415 | $data = json_decode((string) $response->getBody(), true); | ||
416 | $this->assertEquals(9, count($data)); | ||
417 | $this->assertEquals(41, $data[0]['id']); | ||
418 | |||
419 | // wildcard: optional ('*' does not need to expand) | ||
420 | $env = Environment::mock([ | ||
421 | 'REQUEST_METHOD' => 'GET', | ||
422 | 'QUERY_STRING' => 'searchtags=*stuff*', | ||
423 | ]); | ||
424 | $request = Request::createFromEnvironment($env); | ||
425 | $response = $this->controller->getLinks($request, new Response()); | ||
426 | $this->assertEquals(200, $response->getStatusCode()); | ||
427 | $data = json_decode((string) $response->getBody(), true); | ||
428 | $this->assertEquals(2, count($data)); | ||
429 | $this->assertEquals(41, $data[0]['id']); | ||
430 | |||
431 | // wildcard: exclusions | ||
432 | $env = Environment::mock([ | ||
433 | 'REQUEST_METHOD' => 'GET', | ||
434 | 'QUERY_STRING' => 'searchtags=*a*+-*e*', | ||
435 | ]); | ||
436 | $request = Request::createFromEnvironment($env); | ||
437 | $response = $this->controller->getLinks($request, new Response()); | ||
438 | $this->assertEquals(200, $response->getStatusCode()); | ||
439 | $data = json_decode((string) $response->getBody(), true); | ||
440 | $this->assertEquals(1, count($data)); | ||
441 | $this->assertEquals(41, $data[0]['id']); // finds '#hashtag' in descr. | ||
442 | |||
443 | // wildcard: exclude all | ||
444 | $env = Environment::mock([ | ||
445 | 'REQUEST_METHOD' => 'GET', | ||
446 | 'QUERY_STRING' => 'searchtags=-*', | ||
447 | ]); | ||
448 | $request = Request::createFromEnvironment($env); | ||
449 | $response = $this->controller->getLinks($request, new Response()); | ||
450 | $this->assertEquals(200, $response->getStatusCode()); | ||
451 | $data = json_decode((string) $response->getBody(), true); | ||
452 | $this->assertEquals(0, count($data)); | ||
370 | } | 453 | } |
371 | 454 | ||
372 | /** | 455 | /** |
diff --git a/tests/languages/de/UtilsDeTest.php b/tests/languages/de/UtilsDeTest.php index 6c9c9adc..4569c923 100644 --- a/tests/languages/de/UtilsDeTest.php +++ b/tests/languages/de/UtilsDeTest.php | |||
@@ -81,12 +81,12 @@ class UtilsDeTest extends UtilsTest | |||
81 | } | 81 | } |
82 | 82 | ||
83 | /** | 83 | /** |
84 | * Test autoLocale with multiples value, the second one is valid | 84 | * Test autoLocale with multiples value, the second one is available |
85 | */ | 85 | */ |
86 | public function testAutoLocaleMultipleSecondValid() | 86 | public function testAutoLocaleMultipleSecondAvailable() |
87 | { | 87 | { |
88 | $current = setlocale(LC_ALL, 0); | 88 | $current = setlocale(LC_ALL, 0); |
89 | $header = 'pt_BR,fr-fr'; | 89 | $header = 'mag_IN,fr-fr'; |
90 | autoLocale($header); | 90 | autoLocale($header); |
91 | $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0)); | 91 | $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0)); |
92 | 92 | ||
@@ -106,12 +106,12 @@ class UtilsDeTest extends UtilsTest | |||
106 | } | 106 | } |
107 | 107 | ||
108 | /** | 108 | /** |
109 | * Test autoLocale with an invalid value: defaults to en_US. | 109 | * Test autoLocale with an unavailable value: defaults to en_US. |
110 | */ | 110 | */ |
111 | public function testAutoLocaleInvalid() | 111 | public function testAutoLocaleUnavailable() |
112 | { | 112 | { |
113 | $current = setlocale(LC_ALL, 0); | 113 | $current = setlocale(LC_ALL, 0); |
114 | autoLocale('pt_BR'); | 114 | autoLocale('mag_IN'); |
115 | $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); | 115 | $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); |
116 | 116 | ||
117 | setlocale(LC_ALL, $current); | 117 | setlocale(LC_ALL, $current); |
diff --git a/tests/languages/en/UtilsEnTest.php b/tests/languages/en/UtilsEnTest.php index d8680b2b..a74063ae 100644 --- a/tests/languages/en/UtilsEnTest.php +++ b/tests/languages/en/UtilsEnTest.php | |||
@@ -81,12 +81,12 @@ class UtilsEnTest extends UtilsTest | |||
81 | } | 81 | } |
82 | 82 | ||
83 | /** | 83 | /** |
84 | * Test autoLocale with multiples value, the second one is valid | 84 | * Test autoLocale with multiples value, the second one is available |
85 | */ | 85 | */ |
86 | public function testAutoLocaleMultipleSecondValid() | 86 | public function testAutoLocaleMultipleSecondAvailable() |
87 | { | 87 | { |
88 | $current = setlocale(LC_ALL, 0); | 88 | $current = setlocale(LC_ALL, 0); |
89 | $header = 'pt_BR,fr-fr'; | 89 | $header = 'mag_IN,fr-fr'; |
90 | autoLocale($header); | 90 | autoLocale($header); |
91 | $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0)); | 91 | $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0)); |
92 | 92 | ||
@@ -106,12 +106,12 @@ class UtilsEnTest extends UtilsTest | |||
106 | } | 106 | } |
107 | 107 | ||
108 | /** | 108 | /** |
109 | * Test autoLocale with an invalid value: defaults to en_US. | 109 | * Test autoLocale with an unavailable value: defaults to en_US. |
110 | */ | 110 | */ |
111 | public function testAutoLocaleInvalid() | 111 | public function testAutoLocaleUnavailable() |
112 | { | 112 | { |
113 | $current = setlocale(LC_ALL, 0); | 113 | $current = setlocale(LC_ALL, 0); |
114 | autoLocale('pt_BR'); | 114 | autoLocale('mag_IN'); |
115 | $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); | 115 | $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); |
116 | 116 | ||
117 | setlocale(LC_ALL, $current); | 117 | setlocale(LC_ALL, $current); |
diff --git a/tests/languages/fr/UtilsFrTest.php b/tests/languages/fr/UtilsFrTest.php index 0d50a878..3dbb126f 100644 --- a/tests/languages/fr/UtilsFrTest.php +++ b/tests/languages/fr/UtilsFrTest.php | |||
@@ -81,12 +81,12 @@ class UtilsFrTest extends UtilsTest | |||
81 | } | 81 | } |
82 | 82 | ||
83 | /** | 83 | /** |
84 | * Test autoLocale with multiples value, the second one is valid | 84 | * Test autoLocale with multiples value, the second one is available |
85 | */ | 85 | */ |
86 | public function testAutoLocaleMultipleSecondValid() | 86 | public function testAutoLocaleMultipleSecondAvailable() |
87 | { | 87 | { |
88 | $current = setlocale(LC_ALL, 0); | 88 | $current = setlocale(LC_ALL, 0); |
89 | $header = 'pt_BR,de-de'; | 89 | $header = 'mag_IN,de-de'; |
90 | autoLocale($header); | 90 | autoLocale($header); |
91 | $this->assertEquals('de_DE.utf8', setlocale(LC_ALL, 0)); | 91 | $this->assertEquals('de_DE.utf8', setlocale(LC_ALL, 0)); |
92 | 92 | ||
@@ -106,12 +106,12 @@ class UtilsFrTest extends UtilsTest | |||
106 | } | 106 | } |
107 | 107 | ||
108 | /** | 108 | /** |
109 | * Test autoLocale with an invalid value: defaults to en_US. | 109 | * Test autoLocale with an unavailable value: defaults to en_US. |
110 | */ | 110 | */ |
111 | public function testAutoLocaleInvalid() | 111 | public function testAutoLocaleUnavailable() |
112 | { | 112 | { |
113 | $current = setlocale(LC_ALL, 0); | 113 | $current = setlocale(LC_ALL, 0); |
114 | autoLocale('pt_BR'); | 114 | autoLocale('mag_IN'); |
115 | $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); | 115 | $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); |
116 | 116 | ||
117 | setlocale(LC_ALL, $current); | 117 | setlocale(LC_ALL, $current); |
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css index e1868c59..ba589723 100644 --- a/tpl/default/css/shaarli.css +++ b/tpl/default/css/shaarli.css | |||
@@ -539,7 +539,7 @@ body, .pure-g [class*="pure-u"] { | |||
539 | } | 539 | } |
540 | 540 | ||
541 | .linklist-item-title a:visited .linklist-link { | 541 | .linklist-item-title a:visited .linklist-link { |
542 | color: #555555; | 542 | color: #2a4c41; |
543 | } | 543 | } |
544 | 544 | ||
545 | .linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{ | 545 | .linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{ |
diff --git a/tpl/default/includes.html b/tpl/default/includes.html index 0350ef66..80c08333 100644 --- a/tpl/default/includes.html +++ b/tpl/default/includes.html | |||
@@ -5,16 +5,16 @@ | |||
5 | <link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" /> | 5 | <link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" /> |
6 | <link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" /> | 6 | <link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" /> |
7 | <link href="img/favicon.png" rel="shortcut icon" type="image/png" /> | 7 | <link href="img/favicon.png" rel="shortcut icon" type="image/png" /> |
8 | <link type="text/css" rel="stylesheet" href="css/pure.min.css" /> | 8 | <link type="text/css" rel="stylesheet" href="css/pure.min.css?v={$version_hash}" /> |
9 | <link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css"> | 9 | <link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css?v={$version_hash}"> |
10 | <link type="text/css" rel="stylesheet" href="css/pure-extras.css"> | 10 | <link type="text/css" rel="stylesheet" href="css/pure-extras.css?v={$version_hash}"> |
11 | <link type="text/css" rel="stylesheet" href="css/font-awesome.min.css" /> | 11 | <link type="text/css" rel="stylesheet" href="css/font-awesome.min.css?v={$version_hash}" /> |
12 | <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" /> | 12 | <link type="text/css" rel="stylesheet" href="inc/awesomplete.css?v={$version_hash}#" /> |
13 | <link type="text/css" rel="stylesheet" href="css/shaarli.css" /> | 13 | <link type="text/css" rel="stylesheet" href="css/shaarli.css?v={$version_hash}" /> |
14 | {if="is_file('data/user.css')"} | 14 | {if="is_file('data/user.css')"} |
15 | <link type="text/css" rel="stylesheet" href="data/user.css#" /> | 15 | <link type="text/css" rel="stylesheet" href="data/user.css#" /> |
16 | {/if} | 16 | {/if} |
17 | {loop="$plugins_includes.css_files"} | 17 | {loop="$plugins_includes.css_files"} |
18 | <link type="text/css" rel="stylesheet" href="{$value}#"/> | 18 | <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/> |
19 | {/loop} | 19 | {/loop} |
20 | <link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/> \ No newline at end of file | 20 | <link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/> \ No newline at end of file |
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js index 4f49affa..55656f80 100644 --- a/tpl/default/js/shaarli.js +++ b/tpl/default/js/shaarli.js | |||
@@ -275,8 +275,14 @@ window.onload = function () { | |||
275 | }; | 275 | }; |
276 | function init () { | 276 | function init () { |
277 | function resize () { | 277 | function resize () { |
278 | /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */ | ||
279 | var scrollTop = window.pageYOffset || | ||
280 | (document.documentElement || document.body.parentNode || document.body).scrollTop; | ||
281 | |||
278 | description.style.height = 'auto'; | 282 | description.style.height = 'auto'; |
279 | description.style.height = description.scrollHeight+10+'px'; | 283 | description.style.height = description.scrollHeight+10+'px'; |
284 | |||
285 | window.scrollTo(0, scrollTop); | ||
280 | } | 286 | } |
281 | /* 0-timeout to get the already changed text */ | 287 | /* 0-timeout to get the already changed text */ |
282 | function delayedResize () { | 288 | function delayedResize () { |
@@ -401,14 +407,14 @@ window.onload = function () { | |||
401 | 407 | ||
402 | var message = 'Are you sure you want to delete '+ links.length +' links?\n'; | 408 | var message = 'Are you sure you want to delete '+ links.length +' links?\n'; |
403 | message += 'This action is IRREVERSIBLE!\n\nTitles:\n'; | 409 | message += 'This action is IRREVERSIBLE!\n\nTitles:\n'; |
404 | var ids = ''; | 410 | var ids = []; |
405 | links.forEach(function(item) { | 411 | links.forEach(function(item) { |
406 | message += ' - '+ item['title'] +'\n'; | 412 | message += ' - '+ item['title'] +'\n'; |
407 | ids += item['id'] +'+'; | 413 | ids.push(item['id']); |
408 | }); | 414 | }); |
409 | 415 | ||
410 | if (window.confirm(message)) { | 416 | if (window.confirm(message)) { |
411 | window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value; | 417 | window.location = '?delete_link&lf_linkdate='+ ids.join('+') +'&token='+ token.value; |
412 | } | 418 | } |
413 | }); | 419 | }); |
414 | } | 420 | } |
@@ -607,10 +613,11 @@ function htmlEntities(str) | |||
607 | function activateFirefoxSocial(node) { | 613 | function activateFirefoxSocial(node) { |
608 | var loc = location.href; | 614 | var loc = location.href; |
609 | var baseURL = loc.substring(0, loc.lastIndexOf("/") + 1); | 615 | var baseURL = loc.substring(0, loc.lastIndexOf("/") + 1); |
616 | var title = document.title; | ||
610 | 617 | ||
611 | // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable. | 618 | // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable. |
612 | var data = { | 619 | var data = { |
613 | name: "{$shaarlititle}", | 620 | name: title, |
614 | description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.", | 621 | description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.", |
615 | author: "Shaarli", | 622 | author: "Shaarli", |
616 | version: "1.0.0", | 623 | version: "1.0.0", |
diff --git a/tpl/default/loginform.html b/tpl/default/loginform.html index eb6d8378..5777a218 100644 --- a/tpl/default/loginform.html +++ b/tpl/default/loginform.html | |||
@@ -30,7 +30,8 @@ | |||
30 | </div> | 30 | </div> |
31 | <div class="remember-me"> | 31 | <div class="remember-me"> |
32 | <input type="checkbox" name="longlastingsession" id="longlastingsessionform" | 32 | <input type="checkbox" name="longlastingsession" id="longlastingsessionform" |
33 | checked="checked" tabindex="22"> | 33 | {if="$remember_user_default"}checked="checked"{/if} |
34 | tabindex="22"> | ||
34 | <label for="longlastingsessionform">{'Remember me'|t}</label> | 35 | <label for="longlastingsessionform">{'Remember me'|t}</label> |
35 | </div> | 36 | </div> |
36 | <div> | 37 | <div> |
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html index 94f771a2..54b16e8a 100644 --- a/tpl/default/page.footer.html +++ b/tpl/default/page.footer.html | |||
@@ -27,6 +27,6 @@ | |||
27 | <script src="{$value}#"></script> | 27 | <script src="{$value}#"></script> |
28 | {/loop} | 28 | {/loop} |
29 | 29 | ||
30 | <script src="js/shaarli.js"></script> | 30 | <script src="js/shaarli.js?v={$version_hash}"></script> |
31 | <script src="inc/awesomplete.js#"></script> | 31 | <script src="inc/awesomplete.js?v={$version_hash}#"></script> |
32 | <script src="inc/awesomplete-multiple-tags.js#"></script> | 32 | <script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script> |
diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html index 96b357a3..68335c70 100644 --- a/tpl/default/tag.cloud.html +++ b/tpl/default/tag.cloud.html | |||
@@ -26,7 +26,7 @@ | |||
26 | <input type="hidden" name="do" value="tagcloud"> | 26 | <input type="hidden" name="do" value="tagcloud"> |
27 | <input type="text" name="searchtags" placeholder="{'Filter by tag'|t}" | 27 | <input type="text" name="searchtags" placeholder="{'Filter by tag'|t}" |
28 | {if="!empty($search_tags)"} | 28 | {if="!empty($search_tags)"} |
29 | value="{$search_tags}" | 29 | value="{$search_tags}" |
30 | {/if} | 30 | {/if} |
31 | autocomplete="off" data-multiple data-autofirst data-minChars="1" | 31 | autocomplete="off" data-multiple data-autofirst data-minChars="1" |
32 | data-list="{loop="$tags"}{$key}, {/loop}" | 32 | data-list="{loop="$tags"}{$key}, {/loop}" |
diff --git a/tpl/default/tools.html b/tpl/default/tools.html index 35173d17..72fd58af 100644 --- a/tpl/default/tools.html +++ b/tpl/default/tools.html | |||
@@ -97,7 +97,7 @@ | |||
97 | var%20desc=document.getSelection().toString(); | 97 | var%20desc=document.getSelection().toString(); |
98 | if(desc.length>4000){ | 98 | if(desc.length>4000){ |
99 | desc=desc.substr(0,4000)+'...'; | 99 | desc=desc.substr(0,4000)+'...'; |
100 | alert("{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}"); | 100 | alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}'); |
101 | } | 101 | } |
102 | window.open( | 102 | window.open( |
103 | '{$pageabsaddr}?private=1&post='+ | 103 | '{$pageabsaddr}?private=1&post='+ |
diff --git a/tpl/vintage/loginform.html b/tpl/vintage/loginform.html index 84176385..1becd44f 100644 --- a/tpl/vintage/loginform.html +++ b/tpl/vintage/loginform.html | |||
@@ -24,7 +24,9 @@ | |||
24 | </label> | 24 | </label> |
25 | <input type="submit" value="Login" class="bigbutton" tabindex="4"> | 25 | <input type="submit" value="Login" class="bigbutton" tabindex="4"> |
26 | <label for="longlastingsession"> | 26 | <label for="longlastingsession"> |
27 | <input type="checkbox" name="longlastingsession" id="longlastingsession" tabindex="3"> | 27 | <input type="checkbox" name="longlastingsession" |
28 | id="longlastingsession" tabindex="3" | ||
29 | {if="$remember_user_default"}checked="checked"{/if}> | ||
28 | Stay signed in (Do not check on public computers)</label> | 30 | Stay signed in (Do not check on public computers)</label> |
29 | <input type="hidden" name="token" value="{$token}"> | 31 | <input type="hidden" name="token" value="{$token}"> |
30 | {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if} | 32 | {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if} |