aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2017-10-07 12:22:54 +0200
committerArthurHoaro <arthur@hoa.ro>2017-10-07 12:22:54 +0200
commit80b15f5d2db8b90fd9b29f94e6bd8652340df4f0 (patch)
treeb4826cbb03c64b0e5ffb6d0a72f21e0f6b9d9ac8
parent1ea88ae7d1b7fb13e18f543e7c2ad99c4ccde19a (diff)
parenta01437f9e1e4fb5a098877b243828bf6f4936562 (diff)
downloadShaarli-80b15f5d2db8b90fd9b29f94e6bd8652340df4f0.tar.gz
Shaarli-80b15f5d2db8b90fd9b29f94e6bd8652340df4f0.tar.zst
Shaarli-80b15f5d2db8b90fd9b29f94e6bd8652340df4f0.zip
Merge branch 'master' into v0.9
-rw-r--r--.github/mailmap2
-rw-r--r--.travis.yml9
-rw-r--r--AUTHORS11
-rw-r--r--CHANGELOG.md40
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--Makefile24
-rw-r--r--README.md2
-rw-r--r--application/ApplicationUtils.php20
-rw-r--r--application/FileUtils.php26
-rw-r--r--application/HttpUtils.php28
-rw-r--r--application/LinkDB.php2
-rw-r--r--application/LinkFilter.php131
-rw-r--r--application/LinkUtils.php2
-rw-r--r--application/PageBuilder.php9
-rw-r--r--application/ThemeUtils.php1
-rw-r--r--application/Updater.php4
-rw-r--r--application/config/ConfigManager.php8
-rw-r--r--composer.json2
-rw-r--r--doc/md/Download-and-Installation.md6
-rw-r--r--doc/md/Plugin-System.md4
-rw-r--r--doc/md/Plugins.md2
-rw-r--r--doc/md/Release-Shaarli.md6
-rw-r--r--doc/md/Security.md3
-rw-r--r--doc/md/Shaarli-configuration.md8
-rw-r--r--doc/md/Unit-tests-Docker.md56
-rw-r--r--doc/md/docker/docker-101.md78
-rw-r--r--doc/md/index.md13
-rw-r--r--docker/test/alpine36/Dockerfile34
-rw-r--r--docker/test/debian8/Dockerfile35
-rw-r--r--docker/test/debian9/Dockerfile36
-rw-r--r--docker/test/ubuntu16/Dockerfile36
-rw-r--r--index.php205
-rw-r--r--mkdocs.yml1
-rw-r--r--plugins/playvideos/README.md2
-rw-r--r--tests/HttpUtils/IsHttpsTest.php36
-rw-r--r--tests/LinkUtilsTest.php10
-rw-r--r--tests/api/controllers/GetLinksTest.php83
-rw-r--r--tests/languages/de/UtilsDeTest.php12
-rw-r--r--tests/languages/en/UtilsEnTest.php12
-rw-r--r--tests/languages/fr/UtilsFrTest.php12
-rw-r--r--tpl/default/css/shaarli.css2
-rw-r--r--tpl/default/includes.html14
-rw-r--r--tpl/default/js/shaarli.js15
-rw-r--r--tpl/default/loginform.html3
-rw-r--r--tpl/default/page.footer.html6
-rw-r--r--tpl/default/tag.cloud.html2
-rw-r--r--tpl/default/tools.html2
-rw-r--r--tpl/vintage/loginform.html4
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
11VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com> 11VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com>
12VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net> 12VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net>
13VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org> 13VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org>
14Willi Eggeling <thewilli@gmail.com> <mail@wje-online.de>
15Willi 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 @@
1sudo: false 1sudo: false
2dist: precise 2dist: trusty
3language: php 3language: php
4addons:
5 apt:
6 packages:
7 - locales
8 - language-pack-de
9 - language-pack-fr
10cache: 4cache:
11 directories: 5 directories:
12 - $HOME/.composer/cache 6 - $HOME/.composer/cache
@@ -18,6 +12,7 @@ php:
18install: 12install:
19 - composer self-update 13 - composer self-update
20 - composer install --prefer-dist 14 - composer install --prefer-dist
15 - locale -a
21script: 16script:
22 - make clean 17 - make clean
23 - make check_permissions 18 - make check_permissions
diff --git a/AUTHORS b/AUTHORS
index 9c0ca3d1..105561c1 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -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.
4The format is based on [Keep a Changelog](http://keepachangelog.com/) 4The format is based on [Keep a Changelog](http://keepachangelog.com/)
5and this project adheres to [Semantic Versioning](http://semver.org/). 5and 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
9The documentation has been migrated to ReadTheDocs: 47The documentation has been migrated to ReadTheDocs:
@@ -61,7 +99,7 @@ The documentation has been migrated to ReadTheDocs:
61This release introduces the REST API, and requires updating HTTP server 99This release introduces the REST API, and requires updating HTTP server
62configuration to enable URL rewriting, see: 100configuration 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
60All 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. 60All 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.
diff --git a/Makefile b/Makefile
index 6483fca7..a3696ec9 100644
--- a/Makefile
+++ b/Makefile
@@ -19,6 +19,16 @@ PHP_COMMA_SOURCE = index.php,application,tests,plugins
19all: static_analysis_summary check_permissions test 19all: 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##
27docker_%:
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
162release_tar: composer_dependencies doc_html 172release_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
169release_zip: composer_dependencies doc_html 179release_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# 209htmldoc:
200# For all pages:
201# - convert GitHub-flavoured relative links to standard Markdown
202# - generate html documentation with mkdocs
203htmlpages:
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
211doc_html: authors htmlpages
diff --git a/README.md b/README.md
index 5ca25720..100ff46b 100644
--- a/README.md
+++ b/README.md
@@ -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&bull; 11&bull;
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&bull; 14&bull;
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 */
413function 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
255To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. 255To 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 */
110function text2clickable($text, $redirector = '') 110function 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 */
15class ConfigManager 15class 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
21The current latest released version is `v0.9.0` 21The current latest released version is `v0.9.1`
22 22
23Or in command lines: 23Or 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
50Parameters: 50Parameters:
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
55For exemple, if my plugin want to add data to the header, this function is needed: 55For 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
75See [Community & related software](https://github.com/shaarli/Shaarli/wiki/Community-%26-Related-software#third-party-plugins) 75See [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
50This one is pretty straightforward ;-) 56This 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
3Read 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
11Test Dockerfiles are located under `docker/tests/<distribution>/Dockerfile`,
12and can be used to build Docker images to run Shaarli test suites under common
13Linux environments.
14
15Dockerfiles 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
22What'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
60Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe 60Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe
61Status: Downloaded newer image for debian:wheezy 61Status: Downloaded newer image for debian:wheezy
62``` 62```
63
64Docker 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
67A 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
69The 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
71Stopped 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
76Some 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
79A 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```
83Note 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
85Access can also be through one or more network ports, or disk volumes. Both are specified on and fixed on ``docker create`` or ``run``.
86
87You 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
93Trying out different images can fill some gigabytes of disk quickly. Besides images, the docker volumes usually take up most disk space.
94
95If 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
103Systemd 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
106systemctl enable /etc/systemd/system/docker.shaarli.service
107systemctl start docker.shaarli
108systemctl status docker.*
109journalctl -f # inspect system log if needed
110```
111
112You 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]
115Description=Shaarli Bookmark Manager Container
116After=docker.service
117Requires=docker.service
118
119
120[Service]
121Restart=always
122
123# Put any environment you want in an included file, like $host- or $domainname in this example
124EnvironmentFile=/etc/sysconfig/box-environment
125
126# It's just an example..
127ExecStart=/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
135ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli
136
137
138[Install]
139WantedBy=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
23Login: `demo`; Password: `demo` 23Login: `demo`; Password: `demo`
24 24
25Docker 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```
27MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
28docker run -ti --rm \
29 -p 8000:80 \
30 -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
31 shaarli/shaarli
32```
33
34A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101).
35To 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 @@
1FROM alpine:3.6
2MAINTAINER Shaarli Community
3
4RUN 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
27RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
28
29RUN mkdir /shaarli
30WORKDIR /shaarli
31VOLUME /shaarli
32
33ENTRYPOINT ["make"]
34CMD []
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 @@
1FROM debian:jessie
2MAINTAINER Shaarli Community
3
4ENV TERM dumb
5ENV DEBIAN_FRONTEND noninteractive
6ENV LANG en_US.UTF-8
7ENV LANGUAGE en_US:en
8
9RUN 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
23RUN locale-gen en_US.UTF-8 \
24 && locale-gen de_DE.UTF-8 \
25 && locale-gen fr_FR.UTF-8
26
27ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
28RUN chmod 755 /usr/local/bin/composer
29
30RUN mkdir /shaarli
31WORKDIR /shaarli
32VOLUME /shaarli
33
34ENTRYPOINT ["make"]
35CMD []
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 @@
1FROM debian:stretch
2MAINTAINER Shaarli Community
3
4ENV TERM dumb
5ENV DEBIAN_FRONTEND noninteractive
6ENV LANG en_US.UTF-8
7ENV LANGUAGE en_US:en
8
9RUN 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
24RUN locale-gen en_US.UTF-8 \
25 && locale-gen de_DE.UTF-8 \
26 && locale-gen fr_FR.UTF-8
27
28ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
29RUN chmod 755 /usr/local/bin/composer
30
31RUN mkdir /shaarli
32WORKDIR /shaarli
33VOLUME /shaarli
34
35ENTRYPOINT ["make"]
36CMD []
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 @@
1FROM ubuntu:16.04
2MAINTAINER Shaarli Community
3
4ENV TERM dumb
5ENV DEBIAN_FRONTEND noninteractive
6ENV LANG en_US.UTF-8
7ENV LANGUAGE en_US:en
8
9RUN 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
27ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
28RUN chmod 755 /usr/local/bin/composer
29
30RUN useradd -m dev \
31 && mkdir /shaarli
32USER dev
33WORKDIR /shaarli
34
35ENTRYPOINT ["make"]
36CMD []
diff --git a/index.php b/index.php
index b4c4347a..4068a828 100644
--- a/index.php
+++ b/index.php
@@ -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}
55require_once 'inc/rain.tpl.class.php'; 55require_once 'inc/rain.tpl.class.php';
@@ -88,7 +88,7 @@ try {
88 exit; 88 exit;
89} 89}
90 90
91define('shaarli_version', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); 91define('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
134ob_start(); // Output buffering for the page cache. 134ob_start(); // Output buffering for the page cache.
135 135
136// In case stupid admin has left magic_quotes enabled in php.ini:
137if (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)
146header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); 137header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
147header("Cache-Control: no-store, no-cache, must-revalidate"); 138header("Cache-Control: no-store, no-cache, must-revalidate");
@@ -186,42 +177,42 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
186 */ 177 */
187function setup_login_state($conf) 178function 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 */
246function fillSessionInfo($conf) 237function 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 */
592function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) 584function 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);
diff --git a/mkdocs.yml b/mkdocs.yml
index 648d8f67..03a7a34e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -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
11This is a default Shaarli plugin, you just have to enable it. See https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration/ 11This 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 */
9class 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)
607function activateFirefoxSocial(node) { 613function 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&amp;post='+ 103 '{$pageabsaddr}?private=1&amp;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}