]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge branch 'master' into v0.9
authorArthurHoaro <arthur@hoa.ro>
Sat, 7 Oct 2017 10:22:54 +0000 (12:22 +0200)
committerArthurHoaro <arthur@hoa.ro>
Sat, 7 Oct 2017 10:22:54 +0000 (12:22 +0200)
48 files changed:
.github/mailmap
.travis.yml
AUTHORS
CHANGELOG.md
CONTRIBUTING.md
Makefile
README.md
application/ApplicationUtils.php
application/FileUtils.php
application/HttpUtils.php
application/LinkDB.php
application/LinkFilter.php
application/LinkUtils.php
application/PageBuilder.php
application/ThemeUtils.php
application/Updater.php
application/config/ConfigManager.php
composer.json
doc/md/Download-and-Installation.md
doc/md/Plugin-System.md
doc/md/Plugins.md
doc/md/Release-Shaarli.md
doc/md/Security.md
doc/md/Shaarli-configuration.md
doc/md/Unit-tests-Docker.md [new file with mode: 0644]
doc/md/docker/docker-101.md
doc/md/index.md
docker/test/alpine36/Dockerfile [new file with mode: 0644]
docker/test/debian8/Dockerfile [new file with mode: 0644]
docker/test/debian9/Dockerfile [new file with mode: 0644]
docker/test/ubuntu16/Dockerfile [new file with mode: 0644]
index.php
mkdocs.yml
plugins/playvideos/README.md
tests/HttpUtils/IsHttpsTest.php [new file with mode: 0644]
tests/LinkUtilsTest.php
tests/api/controllers/GetLinksTest.php
tests/languages/de/UtilsDeTest.php
tests/languages/en/UtilsEnTest.php
tests/languages/fr/UtilsFrTest.php
tpl/default/css/shaarli.css
tpl/default/includes.html
tpl/default/js/shaarli.js
tpl/default/loginform.html
tpl/default/page.footer.html
tpl/default/tag.cloud.html
tpl/default/tools.html
tpl/vintage/loginform.html

index 41d91e4758eda026135d1a8d330955558b6b6dd3..bbdb7908fe9f91cddb04ac090fbd1226a1b7cb95 100644 (file)
@@ -11,3 +11,5 @@ Timo Van Neerden <fire@lehollandaisvolant.net> lehollandaisvolant <levoltigeurho
 VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com>
 VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net>
 VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org>
+Willi Eggeling <thewilli@gmail.com> <mail@wje-online.de>
+Willi Eggeling <thewilli@gmail.com> <thewilli@users.noreply.github.com>
index 26535ad34f66fbdb68adb9446ad386ee057b3707..b6b9bddf60d0595dae6069fd33cf8ad166c5cee5 100644 (file)
@@ -1,12 +1,6 @@
 sudo: false
-dist: precise
+dist: trusty
 language: php
-addons:
-  apt:
-    packages:
-      - locales
-      - language-pack-de
-      - language-pack-fr
 cache:
   directories:
     - $HOME/.composer/cache
@@ -18,6 +12,7 @@ php:
 install:
   - composer self-update
   - composer install --prefer-dist
+  - locale -a
 script:
   - make clean
   - make check_permissions
diff --git a/AUTHORS b/AUTHORS
index 9c0ca3d15e4c26727c4e61318c1914f7a47c4c21..105561c1ef3c303a28063a68dc1bd82a2b95adec 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,14 +1,16 @@
-   506 ArthurHoaro <arthur@hoa.ro>
-   204 VirtualTam <virtualtam@flibidi.net>
-   147 nodiscc <nodiscc@gmail.com>
+   537 ArthurHoaro <arthur@hoa.ro>
+   252 VirtualTam <virtualtam@flibidi.net>
+   148 nodiscc <nodiscc@gmail.com>
     56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
     15 Florian Eula <eula.florian@gmail.com>
     13 Emilien Klein <emilien@klein.st>
     12 Nicolas Danelon <hi@nicolasmd.com.ar>
+     9 Willi Eggeling <thewilli@gmail.com>
      8 Christophe HENRY <christophe.henry@sbgodin.fr>
+     6 B. van Berkum <dev@dotmpe.com>
+     5 Lucas Cimon <lucas.cimon@gmail.com>
      4 Alexandre Alapetite <alexandre@alapetite.fr>
      4 David Sferruzza <david.sferruzza@gmail.com>
-     3 Lucas Cimon <lucas.cimon@gmail.com>
      3 Teromene <teromene@teromene.fr>
      3 kalvn <kalvnthereal@gmail.com>
      2 Chris Kuethe <chris.kuethe@gmail.com>
@@ -37,6 +39,7 @@
      1 Kevin Canévet <kevin@streamroot.io>
      1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
      1 Lionel Martin <renarddesmers@gmail.com>
+     1 Mark Gerarts <mark.gerarts@gmail.com>
      1 Marsup <marsup@gmail.com>
      1 Sbgodin <Sbgodin@users.noreply.github.com>
      1 TsT <tst2005@gmail.com>
index 4b018cb48cfca3795cfbb83d057d5d8a264892da..120c5d2250ed490ae2feb466a450b9b3ccc5499f 100644 (file)
@@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/)
 and this project adheres to [Semantic Versioning](http://semver.org/).
 
+## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07
+
+**Major security issue fixed. Please update.**
+
+### Added
+- Tag search now supports wildcards `*`
+- New setting `privacy.force_login` which can be used with `privacy.hide_public_links` to redirect anonymous users to the login page.
+- New setting `general.default_note_title` used to override default `Note:` title prefix for notes.
+- Add a version hash for asset loading to prevent browser's cache issue
+
+### Changed
+- The "Remember me" checkbox is unchecked by default
+- The default value of the "Remember me" checkbox can be configured under `data/config.json.php`
+
+### Removed
+- Remove obsolete PHP magic quote support
+
+### Fixed
+- Generates a permalink URL if the URL is set to blank
+- Replace links to the old GitHub wiki with ReadTheDocs URIs
+- Use single quotes in the note bookmarklet
+- Daily page if there is no link
+- Bulk link deletion with a single link
+- HTTPS detection behind a reverse proxy
+- Travis tests environment and localization
+- Improve template paths robustness (trailing slash)
+- Robustness: safer gzinflate/zlib usage
+- Description links parsing with parenthesis (without Markdown)
+- Templates:
+    - Sort the tag cloud alphabetically
+    - Firefox social title
+    - Improved visited link color
+    - Fix jumpy textarea with long content in post edit
+
+### Security
+
+- Vulnerability introduced in v0.9.1 fixed.
+
 ## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23
 
 The documentation has been migrated to ReadTheDocs:
@@ -61,7 +99,7 @@ The documentation has been migrated to ReadTheDocs:
 This release introduces the REST API, and requires updating HTTP server
 configuration to enable URL rewriting, see:
 - https://shaarli.github.io/api-documentation/
-- https://github.com/shaarli/Shaarli/wiki/Server-configuration
+- https://shaarli.readthedocs.io/en/master/Server-configuration/
 
 **WARNING**: Shaarli now requires PHP 5.5+.
 
index bb82951d61abc12b9bbbc4d1eaf28d55e597303b..03564fd2289fec6157b1bc6bda19a06be4acd27b 100644 (file)
@@ -54,7 +54,7 @@ Please report any problem you might find.
  * starting from branch ` master`, switch to a new branch (eg. `git checkout -b my-awesome-feature`)
  * edit the required files (from the Github web interface or your text editor)
  * add and commit your changes with a meaningful commit message (eg `Cool new feature, fixes issue #1001`)
- * run unit tests against your patched version, see [Running unit tests](https://github.com/shaarli/Shaarli/wiki/Running-unit-tests)
+ * run unit tests against your patched version, see [Running unit tests](https://shaarli.readthedocs.io/en/master/Unit-tests/#run-unit-tests)
  * Open your fork in the Github web interface and click the "Compare and Pull Request" button, enter required info and submit your Pull Request.
 
 All changes you will do on the `my-awesome-feature`  in the future will be added to your Pull Request. Don't work directly on the master branch, don't do unrelated work on your  `my-awesome-feature` branch.
index 6483fca78815b77994bea6539621c2826d0920e4..a3696ec987886657dfd2ecca1345575c4928c38c 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,16 @@ PHP_COMMA_SOURCE = index.php,application,tests,plugins
 
 all: static_analysis_summary check_permissions test
 
+##
+# Docker test adapter
+#
+# Shaarli sources and vendored libraries are copied from a shared volume
+# to a user-owned directory to enable running tests as a non-root user.
+##
+docker_%:
+       rsync -az /shaarli/ ~/shaarli/
+       cd ~/shaarli && make $*
+
 ##
 # Concise status of the project
 # These targets are non-blocking: || exit 0
@@ -159,14 +169,14 @@ composer_dependencies: clean
        find vendor/ -name ".git" -type d -exec rm -rf {} +
 
 ### generate a release tarball and include 3rd-party dependencies
-release_tar: composer_dependencies doc_html
+release_tar: composer_dependencies htmldoc
        git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
        tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
        tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
        gzip $(ARCHIVE_VERSION).tar
 
 ### generate a release zip and include 3rd-party dependencies
-release_zip: composer_dependencies doc_html
+release_zip: composer_dependencies htmldoc
        git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
        mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
        rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
@@ -195,17 +205,11 @@ doxygen: clean
        @rm -rf doxygen
        @( cat Doxyfile ; echo "PROJECT_NUMBER=`git describe`" ) | doxygen -
 
-### Convert local markdown documentation to HTML
-#
-# For all pages:
-#  - convert GitHub-flavoured relative links to standard Markdown
-#  - generate html documentation with mkdocs
-htmlpages:
+### generate HTML documentation from Markdown pages with MkDocs
+htmldoc:
        python3 -m venv venv/
        bash -c 'source venv/bin/activate; \
        pip install mkdocs; \
        mkdocs build'
        find doc/html/ -type f -exec chmod a-x '{}' \;
        rm -r venv
-
-doc_html: authors htmlpages
index 5ca2572000808ac4ce7604521ac482dfbcbe086b..100ff46bc94d90200b10b3b96e3b0c1dd0e06453 100644 (file)
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._
 [![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4)
 [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
 &bull;
-[![](https://img.shields.io/badge/latest-v0.9.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.0)
+[![](https://img.shields.io/badge/latest-v0.9.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1)
 [![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
 &bull;
 [![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli)
index 85dcbeebdb164858680ff68b9fbc1048340d05f1..5643f4a09706f2bb5b867153ee09c3c4974d77c3 100644 (file)
@@ -168,14 +168,15 @@ class ApplicationUtils
     public static function checkResourcePermissions($conf)
     {
         $errors = array();
+        $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
 
         // Check script and template directories are readable
         foreach (array(
             'application',
             'inc',
             'plugins',
-            $conf->get('resource.raintpl_tpl'),
-            $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme'),
+            $rainTplDir,
+            $rainTplDir.'/'.$conf->get('resource.theme'),
         ) as $path) {
             if (! is_readable(realpath($path))) {
                 $errors[] = '"'.$path.'" directory is not readable';
@@ -220,4 +221,19 @@ class ApplicationUtils
 
         return $errors;
     }
+
+    /**
+     * Returns a salted hash representing the current Shaarli version.
+     *
+     * Useful for assets browser cache.
+     *
+     * @param string $currentVersion of Shaarli
+     * @param string $salt           User personal salt, also used for the authentication
+     *
+     * @return string version hash
+     */
+    public static function getVersionHash($currentVersion, $salt)
+    {
+        return hash_hmac('sha256', $currentVersion, $salt);
+    }
 }
index a167f642acd925ce7955c92fa6c9d559103f2ea9..918cb83b3c66cbc5aee40a0c704f1010aa339c1e 100644 (file)
@@ -50,7 +50,8 @@ class FileUtils
 
     /**
      * Read data from a file containing Shaarli database format content.
-     * If the file isn't readable or doesn't exists, default data will be returned.
+     *
+     * If the file isn't readable or doesn't exist, default data will be returned.
      *
      * @param string $file    File path.
      * @param mixed  $default The default value to return if the file isn't readable.
@@ -61,16 +62,21 @@ class FileUtils
     {
         // Note that gzinflate is faster than gzuncompress.
         // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
-        if (is_readable($file)) {
-            return unserialize(
-                gzinflate(
-                    base64_decode(
-                        substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
-                    )
-                )
-            );
+        if (! is_readable($file)) {
+            return $default;
+        }
+
+        $data = file_get_contents($file);
+        if ($data == '') {
+            return $default;
         }
 
-        return $default;
+        return unserialize(
+            gzinflate(
+                base64_decode(
+                    substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
+                )
+            )
+        );
     }
 }
index 88a1efdb86382646648d9a26a2cbeab022f7ecdf..0083596643f510d4ea131fad9df25de215ff77ac 100644 (file)
@@ -401,3 +401,31 @@ function getIpAddressFromProxy($server, $trustedIps)
 
     return array_pop($ips);
 }
+
+/**
+ * Returns true if Shaarli's currently browsed in HTTPS.
+ * Supports reverse proxies (if the headers are correctly set).
+ *
+ * @param array $server $_SERVER.
+ *
+ * @return bool true if HTTPS, false otherwise.
+ */
+function is_https($server)
+{
+
+    if (isset($server['HTTP_X_FORWARDED_PORT'])) {
+        // Keep forwarded port
+        if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) {
+            $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']);
+            $port = trim($ports[0]);
+        } else {
+            $port = $server['HTTP_X_FORWARDED_PORT'];
+        }
+
+        if ($port == '443') {
+            return true;
+        }
+    }
+
+    return ! empty($server['HTTPS']);
+}
index 9308164af685bd8830b6c1a4a68621fddaa9f548..22c1f0ab5321b3ccb274931637ba642357ddecb6 100644 (file)
@@ -249,7 +249,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
         $link = array(
             'id' => 1,
             'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
-            'url'=>'https://github.com/shaarli/Shaarli/wiki',
+            'url'=>'https://shaarli.readthedocs.io',
             'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
 
 To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page.
index 9551952886c0bee94c122983c5b3ce8c8d6332a2..99ecd1e238640bbba9fc32c60cdaeaabedba16d8 100644 (file)
@@ -249,6 +249,51 @@ class LinkFilter
         return $filtered;
     }
 
+    /**
+     * generate a regex fragment out of a tag
+     * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
+     * @return string generated regex fragment
+     */
+    private static function tag2regex($tag)
+    {
+        $len = strlen($tag);
+        if(!$len || $tag === "-" || $tag === "*"){
+            // nothing to search, return empty regex
+            return '';
+        }
+        if($tag[0] === "-") {
+            // query is negated
+            $i = 1; // use offset to start after '-' character
+            $regex = '(?!'; // create negative lookahead
+        } else {
+            $i = 0; // start at first character
+            $regex = '(?='; // use positive lookahead
+        }
+        $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
+        // iterate over string, separating it into placeholder and content
+        for(; $i < $len; $i++){
+            if($tag[$i] === '*'){
+                // placeholder found
+                $regex .= '[^ ]*?';
+            } else {
+                // regular characters
+                $offset = strpos($tag, '*', $i);
+                if($offset === false){
+                    // no placeholder found, set offset to end of string
+                    $offset = $len;
+                }
+                // subtract one, as we want to get before the placeholder or end of string
+                $offset -= 1;
+                // we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
+                $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
+                // move $i on
+                $i = $offset;
+            }
+        }
+        $regex .= '(?:$| ))'; // after the tag may only be a space or the end
+        return $regex;
+    }
+
     /**
      * Returns the list of links associated with a given list of tags
      *
@@ -263,20 +308,32 @@ class LinkFilter
      */
     public function filterTags($tags, $casesensitive = false, $visibility = 'all')
     {
-        // Implode if array for clean up.
-        $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags;
-        if (empty($tags)) {
+        // get single tags (we may get passed an array, even though the docs say different)
+        $inputTags = $tags;
+        if(!is_array($tags)) {
+            // we got an input string, split tags
+            $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
+        }
+
+        if(!count($inputTags)){
+            // no input tags
             return $this->noFilter($visibility);
         }
 
-        $searchtags = self::tagsStrToArray($tags, $casesensitive);
-        $filtered = array();
-        if (empty($searchtags)) {
-            return $filtered;
+        // build regex from all tags
+        $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
+        if(!$casesensitive) {
+            // make regex case insensitive
+            $re .= 'i';
         }
 
+        // create resulting array
+        $filtered = array();
+
+        // iterate over each link
         foreach ($this->links as $key => $link) {
-            // ignore non private links when 'privatonly' is on.
+            // check level of visibility
+            // ignore non private links when 'privateonly' is on.
             if ($visibility !== 'all') {
                 if (! $link['private'] && $visibility === 'private') {
                     continue;
@@ -284,25 +341,27 @@ class LinkFilter
                     continue;
                 }
             }
-
-            $linktags = self::tagsStrToArray($link['tags'], $casesensitive);
-
-            $found = true;
-            for ($i = 0 ; $i < count($searchtags) && $found; $i++) {
-                // Exclusive search, quit if tag found.
-                // Or, tag not found in the link, quit.
-                if (($searchtags[$i][0] == '-'
-                        && $this->searchTagAndHashTag(substr($searchtags[$i], 1), $linktags, $link['description']))
-                    || ($searchtags[$i][0] != '-')
-                        && ! $this->searchTagAndHashTag($searchtags[$i], $linktags, $link['description'])
-                ) {
-                    $found = false;
+            $search = $link['tags']; // build search string, start with tags of current link
+            if(strlen(trim($link['description'])) && strpos($link['description'], '#') !== false){
+                // description given and at least one possible tag found
+                $descTags = array();
+                // find all tags in the form of #tag in the description
+                preg_match_all(
+                    '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
+                    $link['description'],
+                    $descTags
+                );
+                if(count($descTags[1])){
+                    // there were some tags in the description, add them to the search string
+                    $search .= ' ' . implode(' ', $descTags[1]);
                 }
+            };
+            // match regular expression with search string
+            if(!preg_match($re, $search)){
+                // this entry does _not_ match our regex
+                continue;
             }
-
-            if ($found) {
-                $filtered[$key] = $link;
-            }
+            $filtered[$key] = $link;
         }
         return $filtered;
     }
@@ -363,28 +422,6 @@ class LinkFilter
         return array_reverse($filtered, true);
     }
 
-    /**
-     * Check if a tag is found in the taglist, or as an hashtag in the link description.
-     *
-     * @param string $tag         Tag to search.
-     * @param array  $taglist     List of tags for the current link.
-     * @param string $description Link description.
-     *
-     * @return bool True if found, false otherwise.
-     */
-    protected function searchTagAndHashTag($tag, $taglist, $description)
-    {
-        if (in_array($tag, $taglist)) {
-            return true;
-        }
-
-        if (preg_match('/(^| )#'. $tag .'([^'. self::$HASHTAG_CHARS .']|$)/mui', $description) > 0) {
-            return true;
-        }
-
-        return false;
-    }
-
     /**
      * Convert a list of tags (str) to an array. Also
      * - handle case sensitivity.
index 976474de721ad14636b9b431f0cec06a8920e120..267e62cde41f9193e384cede8665311fe76ccd6e 100644 (file)
@@ -109,7 +109,7 @@ function count_private($links)
  */
 function text2clickable($text, $redirector = '')
 {
-    $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si';
+    $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
 
     if (empty($redirector)) {
         return preg_replace($regex, '<a href="$1">$1</a>', $text);
index 7a42400d88f6baa2444932d179985f5ad89cd14a..291860adeb61912af4f8671c8544849224804713 100644 (file)
@@ -49,7 +49,7 @@ class PageBuilder
 
         try {
             $version = ApplicationUtils::checkUpdate(
-                shaarli_version,
+                SHAARLI_VERSION,
                 $this->conf->get('resource.update_check'),
                 $this->conf->get('updates.check_updates_interval'),
                 $this->conf->get('updates.check_updates'),
@@ -75,7 +75,11 @@ class PageBuilder
         }
         $this->tpl->assign('searchcrits', $searchcrits);
         $this->tpl->assign('source', index_url($_SERVER));
-        $this->tpl->assign('version', shaarli_version);
+        $this->tpl->assign('version', SHAARLI_VERSION);
+        $this->tpl->assign(
+            'version_hash',
+            ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt'))
+        );
         $this->tpl->assign('scripturl', index_url($_SERVER));
         $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links?
         $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly']));
@@ -89,6 +93,7 @@ class PageBuilder
         $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss');
         $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
         $this->tpl->assign('token', getToken($this->conf));
+
         if ($this->linkDB !== null) {
             $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
         }
index 2718ed138cf7215609eb61d39351150fe84c8515..16f2f6a2742c701f79d671bcf4d89359584fc4d9 100644 (file)
@@ -22,6 +22,7 @@ class ThemeUtils
      */
     public static function getThemes($tplDir)
     {
+        $tplDir = rtrim($tplDir, '/');
         $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR);
         $themes = [];
         foreach ($allTheme as $value) {
index 40a15906b6bac9b53fb54faff0232b6514dd76ec..72b2def019dea484d28b6de040097d614dc6e867 100644 (file)
@@ -398,7 +398,7 @@ class Updater
      */
     public function updateMethodCheckUpdateRemoteBranch()
     {
-        if (shaarli_version === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
+        if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
             return true;
         }
 
@@ -413,7 +413,7 @@ class Updater
         $latestMajor = $matches[1];
 
         // Get current major version digit
-        preg_match('/(\d+)\.\d+$/', shaarli_version, $matches);
+        preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
         $currentMajor = $matches[1];
 
         if ($currentMajor === $latestMajor) {
index 8eab26f1264e58f0288d3f61fe5c04434b445ed5..7ff2fe671e94b8b6809044a31d17331c92320237 100644 (file)
@@ -9,8 +9,8 @@ use Shaarli\Config\Exception\UnauthorizedConfigException;
  *
  * Manages all Shaarli's settings.
  * See the documentation for more information on settings:
- *   - doc/Shaarli-configuration.html
- *   - https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration
+ *   - doc/md/Shaarli-configuration.md
+ *   - https://shaarli.readthedocs.io/en/master/Shaarli-configuration/#configuration
  */
 class ConfigManager
 {
@@ -317,6 +317,7 @@ class ConfigManager
         $this->setEmpty('general.header_link', '?');
         $this->setEmpty('general.links_per_page', 20);
         $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
+        $this->setEmpty('general.default_note_title', 'Note: ');
 
         $this->setEmpty('updates.check_updates', false);
         $this->setEmpty('updates.check_updates_branch', 'stable');
@@ -327,7 +328,10 @@ class ConfigManager
 
         $this->setEmpty('privacy.default_private_links', false);
         $this->setEmpty('privacy.hide_public_links', false);
+        $this->setEmpty('privacy.force_login', false);
         $this->setEmpty('privacy.hide_timestamps', false);
+        // default state of the 'remember me' checkbox of the login form
+        $this->setEmpty('privacy.remember_user_default', true);
 
         $this->setEmpty('thumbnail.enable_thumbnails', true);
         $this->setEmpty('thumbnail.enable_localcache', true);
index 756ea58891bf02bba1a8a6157c9e5eaa8a80c2d1..afb8aca4c42209a7f8ddc685a1c7626e416a313c 100644 (file)
@@ -6,7 +6,7 @@
     "homepage": "https://github.com/shaarli/Shaarli",
     "support": {
         "issues": "https://github.com/shaarli/Shaarli/issues",
-        "wiki": "https://github.com/shaarli/Shaarli/wiki"
+        "wiki": "https://shaarli.readthedocs.io"
     },
     "keywords": ["bookmark", "link", "share", "web"],
     "config": {
index 135f06336991ce04b63068f4725cc5eedc2d86a9..e5e929ef36c7d04f2987de12247d8f2b632777cc 100644 (file)
@@ -18,13 +18,13 @@ Get the latest released version from the [releases](https://github.com/shaarli/S
 
 **Download our *shaarli-full* archive** to include dependencies.
 
-The current latest released version is `v0.9.0`
+The current latest released version is `v0.9.1`
 
 Or in command lines:
 
 ```bash
-$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.0/shaarli-v0.9.0-full.zip
-$ unzip shaarli-v0.9.0-full.zip
+$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.1/shaarli-v0.9.1-full.zip
+$ unzip shaarli-v0.9.1-full.zip
 $ mv Shaarli /path/to/shaarli/
 ```
 
index 30f0ae7448a033b5964b4032535674047b289ab9..cbec04c0fa0bf12ad87f2b33824c64f7c2700c0b 100644 (file)
@@ -49,10 +49,10 @@ hook_<plugin_name>_<hook_name>($data, $conf)
 
 Parameters:
 
-- data: see [$data section](https://github.com/shaarli/Shaarli/wiki/Plugin-System#plugins-data)
+- data: see [$data section](https://shaarli.readthedocs.io/en/master/Plugin-System/#plugins-data)
 - conf: the `ConfigManager` instance.
 
-For exemple, if my plugin want to add data to the header, this function is needed:
+For example, if my plugin want to add data to the header, this function is needed:
 
     hook_demo_plugin_render_header
 
index 7d40637fe6ca84637738bc6afdbd8086cec7228b..463dae170e37dc3791f1376e3b0f6668f8c0be5e 100644 (file)
@@ -72,4 +72,4 @@ Usage of each plugin is documented in it's README file:
 
 #### Third party plugins
 
-See [Community & related software](https://github.com/shaarli/Shaarli/wiki/Community-%26-Related-software#third-party-plugins)
+See [Community & related software](https://shaarli.readthedocs.io/en/master/Community-&-Related-software/)
index 974a743861994dc44126f69329a90537898f89ad..e22eabc9f32af872d7771a3cda37391261fc6464 100644 (file)
@@ -46,6 +46,12 @@ TBA
 
 
 ## Increment the version code, update docs, create and push a signed tag
+### Update the list of Git contributors
+```bash
+$ make authors
+$ git commit -s -m "Update AUTHORS"
+```
+
 ### Create and merge a Pull Request
 This one is pretty straightforward ;-)
 
index 36f629af0593c36b8d8bbac0e612989bff14f278..65db4225da89df3c1845feb3ae598155b1c8f25a 100644 (file)
@@ -1,9 +1,6 @@
 ## Client browser
 - 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
 
-## PHP
-- `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.
-
 ## Server and sessions
 - Directories are protected using `.htaccess` files
 - Forms are protected against XSRF (Cross-site requests forgery):
index 188a3c09161ef5c68676a3d29d8b133464ede493..99b25ba785a98976c713ace111e3cbe9e55ff1ed 100644 (file)
@@ -55,6 +55,7 @@ _These settings should not be edited_
 - **links_per_page**: Number of shaares displayed per page.  
 - **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).  
 - **enabled_plugins**: List of enabled plugins.
+- **default_note_title**: Default title of a new note.
 
 ### Security
 
@@ -90,7 +91,10 @@ _These settings should not be edited_
 
 - **default_private_links**: Check the private checkbox by default for every new link.  
 - **hide_public_links**: All links are hidden while logged out.  
+- **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page.
 - **hide_timestamps**: Timestamps are hidden.
+- **remember_user_default**: Default state of the login page's *remember me* checkbox
+    - `true`: checked by default, `false`: unchecked by default
 
 ### Feed
 
@@ -192,7 +196,9 @@ _These settings should not be edited_
     "privacy": {
         "default_private_links": true,
         "hide_public_links": false,
-        "hide_timestamps": false
+        "force_login": false,
+        "hide_timestamps": false,
+        "remember_user_default": true
     },
     "thumbnail": {
         "enable_thumbnails": true,
diff --git a/doc/md/Unit-tests-Docker.md b/doc/md/Unit-tests-Docker.md
new file mode 100644 (file)
index 0000000..c2de7cc
--- /dev/null
@@ -0,0 +1,56 @@
+## Running tests inside Docker containers
+
+Read first:
+
+- [Docker 101](docker/docker-101.md)
+- [Docker resources](docker/resources.md)
+- [Unit tests](Unit-tests.md)
+
+### Docker test images
+
+Test Dockerfiles are located under `docker/tests/<distribution>/Dockerfile`,
+and can be used to build Docker images to run Shaarli test suites under common
+Linux environments.
+
+Dockerfiles are provided for the following environments:
+
+- `alpine36` - [Alpine 3.6](https://www.alpinelinux.org/downloads/)
+- `debian8` - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldstable)
+- `debian9` - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (stable)
+- `ubuntu16` - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (LTS)
+
+What's behind the curtains:
+
+- each image provides:
+    - a base Linux OS
+    - Shaarli PHP dependencies (OS packages)
+    - test PHP dependencies (OS packages)
+    - Composer
+- the local workspace is mapped to the container's `/shaarli/` directory,
+- the files are rsync'd to so tests are run using a standard Linux user account
+  (running tests as `root` would bypass permission checks and may hide issues)
+- the tests are run inside the container.
+
+### Building test images
+
+```bash
+# build the Debian 9 Docker image
+$ cd /path/to/shaarli
+$ cd docker/test/debian9
+$ docker build -t shaarli-test:debian9 .
+```
+
+### Running tests
+
+```bash
+$ cd /path/to/shaarli
+
+# install/update 3rd-party test dependencies
+$ composer install --prefer-dist
+
+# run tests using the freshly built image
+$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test
+
+# run the full test campaign
+$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests
+```
index b02dd149a8be61974cb4469ecb0327217d0c3fb0..a9c00b85cbda8c246855934ec7b74dcc856ae26c 100644 (file)
@@ -60,3 +60,81 @@ wheezy: Pulling from debian
 Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe
 Status: Downloaded newer image for debian:wheezy
 ```
+
+Docker re-uses layers already downloaded. In other words if you have images based on Alpine or some Ubuntu version for example, those can share disk space.
+
+### Start a container
+A container is an instance created from an image, that can be run and that keeps running until its main process exits. Or until the user stops the container. 
+
+The simplest way to start a container from image is ``docker run``. It also pulls the image for you if it is not locally available. For more advanced use, refer to ``docker create``.
+
+Stopped containers are not destroyed, unless you specify ``--rm``. To view all created, running and stopped containers, enter:
+```bash
+$ docker ps -a
+```
+
+Some containers may be designed or configured to be restarted, others are not. Also remember both network ports and volumes of a container are created on start, and not editable later.
+
+### Access a running container
+A running container is accessible using ``docker exec``, or ``docker copy``. You can use ``exec`` to start a root shell in the Shaarli container:
+```bash
+$ docker exec -ti <container-name-or-id> bash
+```
+Note the names and ID's of containers are listed in ``docker ps``. You can even type only one or two letters of the ID, given they are unique.
+
+Access can also be through one or more network ports, or disk volumes. Both are specified on and fixed on ``docker create`` or ``run``.
+
+You can view the console output of the main container process too:
+```bash
+$ docker logs -f <container-name-or-id>
+```
+
+### Docker disk use
+Trying out different images can fill some gigabytes of disk quickly. Besides images, the docker volumes usually take up most disk space.
+
+If you care only about trying out docker and not about what is running or saved, the following commands should help you out quickly if you run low on disk space:
+
+```bash
+$ docker rmi -f $(docker images -aq) # remove or mark all images for disposal
+$ docker volume rm $(docker volume ls -q) # remove all volumes
+```
+
+### Systemd config
+Systemd is the process manager of choice on Debian-based distributions. Once you have a ``docker`` service installed, you can use the following steps to set up Shaarli to run on system start.
+
+```bash
+systemctl enable /etc/systemd/system/docker.shaarli.service
+systemctl start docker.shaarli
+systemctl status docker.*
+journalctl -f # inspect system log if needed
+```
+
+You will need sudo or a root terminal to perform some or all of the steps above. Here are the contents for the service file:
+```
+[Unit]
+Description=Shaarli Bookmark Manager Container
+After=docker.service
+Requires=docker.service
+
+
+[Service]
+Restart=always
+
+# Put any environment you want in an included file, like $host- or $domainname in this example
+EnvironmentFile=/etc/sysconfig/box-environment
+
+# It's just an example..
+ExecStart=/usr/bin/docker run \
+  -p 28010:80 \
+  --name ${hostname}-shaarli \
+  --hostname shaarli.${domainname} \
+  -v /srv/docker-volumes-local/shaarli-data:/var/www/shaarli/data:rw \
+  -v /etc/localtime:/etc/localtime:ro \
+  shaarli/shaarli:latest
+
+ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli
+
+
+[Install]
+WantedBy=multi-user.target
+```
index b10e3cf4caeaa8f1b473c0513e0ed7d669c8c985..2b7d0f0070de616c3de1f48d0250ef62b451b67f 100644 (file)
@@ -22,6 +22,17 @@ It runs the latest development version of Shaarli and is updated/reset daily.
 
 Login: `demo`; Password: `demo`
 
+Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage):
+```
+MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
+docker run -ti --rm \
+         -p 8000:80 \
+         -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
+         shaarli/shaarli
+```
+
+A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101).
+To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](Upgrade-and-migration) documentation.
 
 ## Features
 
@@ -37,7 +48,7 @@ Login: `demo`; Password: `demo`
     - daily RSS feed
 - permalinks for easy reference
 - links can be public or private
-- extensible through [plugins](https://github.com/shaarli/Shaarli/wiki/Plugins#plugin-usage)
+- extensible through [plugins](https://shaarli.readthedocs.io/en/master/Plugins/#plugin-usage)
 
 ### Tag, view and search your links!
 - 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 (file)
index 0000000..fa84f6e
--- /dev/null
@@ -0,0 +1,34 @@
+FROM alpine:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+        ca-certificates \
+        curl \
+        make \
+        php7 \
+        php7-ctype \
+        php7-curl \
+        php7-dom \
+        php7-gd \
+        php7-iconv \
+        php7-intl \
+        php7-json \
+        php7-mbstring \
+        php7-openssl \
+        php7-phar \
+        php7-session \
+        php7-simplexml \
+        php7-tokenizer \
+        php7-xdebug \
+        php7-xml \
+        php7-zlib \
+        rsync
+
+RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
diff --git a/docker/test/debian8/Dockerfile b/docker/test/debian8/Dockerfile
new file mode 100644 (file)
index 0000000..eaa34e9
--- /dev/null
@@ -0,0 +1,35 @@
+FROM debian:jessie
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+    && apt-get install --no-install-recommends -y \
+       ca-certificates \
+       curl \
+       locales \
+       make \
+       php5 \
+       php5-curl \
+       php5-gd \
+       php5-intl \
+       php5-xdebug \
+       rsync \
+    && apt-get clean
+
+RUN locale-gen en_US.UTF-8 \
+    && locale-gen de_DE.UTF-8 \
+    && locale-gen fr_FR.UTF-8
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
diff --git a/docker/test/debian9/Dockerfile b/docker/test/debian9/Dockerfile
new file mode 100644 (file)
index 0000000..3ab4b93
--- /dev/null
@@ -0,0 +1,36 @@
+FROM debian:stretch
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+    && apt-get install --no-install-recommends -y \
+       ca-certificates \
+       curl \
+       locales \
+       make \
+       php7.0 \
+       php7.0-curl \
+       php7.0-gd \
+       php7.0-intl \
+       php7.0-xml \
+       php-xdebug \
+       rsync \
+    && apt-get clean
+
+RUN locale-gen en_US.UTF-8 \
+    && locale-gen de_DE.UTF-8 \
+    && locale-gen fr_FR.UTF-8
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
diff --git a/docker/test/ubuntu16/Dockerfile b/docker/test/ubuntu16/Dockerfile
new file mode 100644 (file)
index 0000000..e53ed9e
--- /dev/null
@@ -0,0 +1,36 @@
+FROM ubuntu:16.04
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+    && apt-get install --no-install-recommends -y \
+       ca-certificates \
+       curl \
+       language-pack-de \
+       language-pack-en \
+       language-pack-fr \
+       locales \
+       make \
+       php7.0 \
+       php7.0-curl \
+       php7.0-gd \
+       php7.0-intl \
+       php7.0-xml \
+       php-xdebug \
+       rsync \
+    && apt-get clean
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN useradd -m dev \
+    && mkdir /shaarli
+USER dev
+WORKDIR /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
index b4c4347a40c46addc904079ae31f36b6b08da707..4068a828f10293ee40a8e245e5052e908cbe8bf3 100644 (file)
--- a/index.php
+++ b/index.php
@@ -48,8 +48,8 @@ if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
         ."If you installed Shaarli through Git or using the development branch,\n"
         ."please refer to the installation documentation to install PHP"
         ." dependencies using Composer:\n"
-        ."- https://github.com/shaarli/Shaarli/wiki/Server-requirements\n"
-        ."- https://github.com/shaarli/Shaarli/wiki/Download-and-Installation";
+        ."- https://shaarli.readthedocs.io/en/master/Server-requirements/\n"
+        ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
     exit;
 }
 require_once 'inc/rain.tpl.class.php';
@@ -88,7 +88,7 @@ try {
     exit;
 }
 
-define('shaarli_version', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
+define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
 
 // Force cookie path (but do not change lifetime)
 $cookie = session_get_cookie_params();
@@ -133,15 +133,6 @@ date_default_timezone_set($conf->get('general.timezone', 'UTC'));
 
 ob_start();  // Output buffering for the page cache.
 
-// In case stupid admin has left magic_quotes enabled in php.ini:
-if (get_magic_quotes_gpc())
-{
-    function stripslashes_deep($value) { $value = is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value); return $value; }
-    $_POST = array_map('stripslashes_deep', $_POST);
-    $_GET = array_map('stripslashes_deep', $_GET);
-    $_COOKIE = array_map('stripslashes_deep', $_COOKIE);
-}
-
 // Prevent caching on client side or proxy: (yes, it's ugly)
 header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
 header("Cache-Control: no-store, no-cache, must-revalidate");
@@ -186,42 +177,42 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  */
 function setup_login_state($conf)
 {
-       if ($conf->get('security.open_shaarli')) {
-           return true;
-       }
-       $userIsLoggedIn = false; // By default, we do not consider the user as logged in;
-       $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
-       if (! $conf->exists('credentials.login')) {
-           $userIsLoggedIn = false;  // Shaarli is not configured yet.
-           $loginFailure = true;
-       }
-       if (isset($_COOKIE['shaarli_staySignedIn']) &&
-           $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN &&
-           !$loginFailure)
-       {
-           fillSessionInfo($conf);
-           $userIsLoggedIn = true;
-       }
-       // If session does not exist on server side, or IP address has changed, or session has expired, logout.
-       if (empty($_SESSION['uid'])
+    if ($conf->get('security.open_shaarli')) {
+        return true;
+    }
+    $userIsLoggedIn = false; // By default, we do not consider the user as logged in;
+    $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
+    if (! $conf->exists('credentials.login')) {
+        $userIsLoggedIn = false;  // Shaarli is not configured yet.
+        $loginFailure = true;
+    }
+    if (isset($_COOKIE['shaarli_staySignedIn']) &&
+        $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN &&
+        !$loginFailure)
+    {
+        fillSessionInfo($conf);
+        $userIsLoggedIn = true;
+    }
+    // If session does not exist on server side, or IP address has changed, or session has expired, logout.
+    if (empty($_SESSION['uid'])
         || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs())
         || time() >= $_SESSION['expires_on'])
-       {
-           logout();
-           $userIsLoggedIn = false;
-           $loginFailure = true;
-       }
-       if (!empty($_SESSION['longlastingsession'])) {
-           $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
-       }
-       else {
-           $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
-       }
-       if (!$loginFailure) {
-           $userIsLoggedIn = true;
-       }
-
-       return $userIsLoggedIn;
+    {
+        logout();
+        $userIsLoggedIn = false;
+        $loginFailure = true;
+    }
+    if (!empty($_SESSION['longlastingsession'])) {
+        $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
+    }
+    else {
+        $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
+    }
+    if (!$loginFailure) {
+        $userIsLoggedIn = true;
+    }
+
+    return $userIsLoggedIn;
 }
 $userIsLoggedIn = setup_login_state($conf);
 
@@ -245,10 +236,10 @@ function allIPs()
  */
 function fillSessionInfo($conf)
 {
-       $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid)
-       $_SESSION['ip']=allIPs();                // We store IP address(es) of the client to make sure session is not hijacked.
-       $_SESSION['username']= $conf->get('credentials.login');
-       $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT;  // Set session expiration.
+    $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid)
+    $_SESSION['ip']=allIPs();                // We store IP address(es) of the client to make sure session is not hijacked.
+    $_SESSION['username']= $conf->get('credentials.login');
+    $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT;  // Set session expiration.
 }
 
 /**
@@ -265,7 +256,7 @@ function check_auth($login, $password, $conf)
     $hash = sha1($password . $login . $conf->get('credentials.salt'));
     if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash'))
     {   // Login/password is correct.
-               fillSessionInfo($conf);
+        fillSessionInfo($conf);
         logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful');
         return true;
     }
@@ -394,9 +385,10 @@ if (isset($_POST['login']))
         // If user wants to keep the session cookie even after the browser closes:
         if (!empty($_POST['longlastingsession']))
         {
-                       setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, time()+31536000, WEB_PATH);
-            $_SESSION['longlastingsession']=31536000;  // (31536000 seconds = 1 year)
-            $_SESSION['expires_on']=time()+$_SESSION['longlastingsession'];  // Set session expiration on server-side.
+            $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year)
+            $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now)
+            setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH);
+            $_SESSION['expires_on'] = $expiration;  // Set session expiration on server-side.
 
             $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
             session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side
@@ -591,20 +583,29 @@ function showDailyRSS($conf) {
  */
 function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
 {
-    $day=date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
-    if (isset($_GET['day'])) $day=$_GET['day'];
+    $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
+    if (isset($_GET['day'])) {
+      $day = $_GET['day'];
+    }
 
     $days = $LINKSDB->days();
-    $i = array_search($day,$days);
-    if ($i===false) { $i=count($days)-1; $day=$days[$i]; }
-    $previousday='';
-    $nextday='';
-    if ($i!==false)
-    {
-        if ($i>=1) $previousday=$days[$i-1];
-        if ($i<count($days)-1) $nextday=$days[$i+1];
+    $i = array_search($day, $days);
+    if ($i === false && count($days)) {
+        // no links for day, but at least one day with links
+        $i = count($days) - 1;
+        $day = $days[$i];
     }
+    $previousday = '';
+    $nextday = '';
 
+    if ($i !== false) {
+        if ($i >= 1) {
+             $previousday=$days[$i - 1];
+        }
+        if ($i < count($days) - 1) {
+          $nextday = $days[$i + 1];
+        }
+    }
     try {
         $linksToDisplay = $LINKSDB->filterDay($day);
     } catch (Exception $exc) {
@@ -613,9 +614,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
     }
 
     // We pre-format some fields for proper output.
-    foreach($linksToDisplay as $key=>$link)
-    {
-
+    foreach($linksToDisplay as $key => $link) {
         $taglist = explode(' ',$link['tags']);
         uasort($taglist, 'strcasecmp');
         $linksToDisplay[$key]['taglist']=$taglist;
@@ -629,21 +628,22 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
        so I manually spread entries with a simple method: I roughly evaluate the
        height of a div according to title and description length.
     */
-    $columns=array(array(),array(),array()); // Entries to display, for each column.
-    $fill=array(0,0,0);  // Rough estimate of columns fill.
-    foreach($linksToDisplay as $key=>$link)
-    {
+    $columns = array(array(), array(), array()); // Entries to display, for each column.
+    $fill = array(0, 0, 0);  // Rough estimate of columns fill.
+    foreach($linksToDisplay as $key => $link) {
         // Roughly estimate length of entry (by counting characters)
         // Title: 30 chars = 1 line. 1 line is 30 pixels height.
         // Description: 836 characters gives roughly 342 pixel height.
         // This is not perfect, but it's usually OK.
-        $length=strlen($link['title'])+(342*strlen($link['description']))/836;
-        if ($link['thumbnail']) $length +=100; // 1 thumbnails roughly takes 100 pixels height.
+        $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836;
+        if ($link['thumbnail']) {
+          $length += 100; // 1 thumbnails roughly takes 100 pixels height.
+        }
         // Then put in column which is the less filled:
-        $smallest=min($fill); // find smallest value in array.
-        $index=array_search($smallest,$fill); // find index of this smallest value.
-        array_push($columns[$index],$link); // Put entry in this column.
-        $fill[$index]+=$length;
+        $smallest = min($fill); // find smallest value in array.
+        $index = array_search($smallest, $fill); // find index of this smallest value.
+        array_push($columns[$index], $link); // Put entry in this column.
+        $fill[$index] += $length;
     }
 
     $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
@@ -718,6 +718,23 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
     $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
     $targetPage = Router::findPage($query, $_GET, isLoggedIn());
 
+    if (
+        // if the user isn't logged in
+        !isLoggedIn() &&
+        // and Shaarli doesn't have public content...
+        $conf->get('privacy.hide_public_links') &&
+        // and is configured to enforce the login
+        $conf->get('privacy.force_login') &&
+        // and the current page isn't already the login page
+        $targetPage !== Router::$PAGE_LOGIN &&
+        // and the user is not requesting a feed (which would lead to a different content-type as expected)
+        $targetPage !== Router::$PAGE_FEED_ATOM &&
+        $targetPage !== Router::$PAGE_FEED_RSS
+    ) {
+        // force current page to be the login page
+        $targetPage = Router::$PAGE_LOGIN;
+    }
+
     // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
     // Then assign generated data to RainTPL.
     $common_hooks = array(
@@ -745,6 +762,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
             $PAGE->assign('username', escape($_GET['username']));
         }
         $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
+        // add default state of the 'remember me' checkbox
+        $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
         $PAGE->renderPage('loginform');
         exit;
     }
@@ -803,7 +822,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
             $maxcount = max($maxcount, $value);
         }
 
-        alphabetical_sort($tags, true, true);
+        alphabetical_sort($tags, false, true);
 
         $tagList = array();
         foreach($tags as $key => $value) {
@@ -821,7 +840,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
         }
 
         $data = array(
-            'search_tags' => implode(' ', $filteringTags),
+            'search_tags' => implode(' ', escape($filteringTags)),
             'tags' => $tagList,
         );
         $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
@@ -851,7 +870,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
         }
 
         $data = [
-            'search_tags' => implode(' ', $filteringTags),
+            'search_tags' => implode(' ', escape($filteringTags)),
             'tags' => $tags,
         ];
         $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
@@ -1063,10 +1082,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
     // -------- Display the Tools menu if requested (import/export/bookmarklet...)
     if ($targetPage == Router::$PAGE_TOOLS)
     {
-        $data = array(
+        $data = [
             'pageabsaddr' => index_url($_SERVER),
-            'sslenabled' => !empty($_SERVER['HTTPS'])
-        );
+            'sslenabled' => is_https($_SERVER),
+        ];
         $pluginManager->executeHooks('render_tools', $data);
 
         foreach ($data as $key => $value) {
@@ -1233,7 +1252,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
         // Linkdate is kept here to:
         //   - use the same permalink for notes as they're displayed when creating them
         //   - let users hack creation date of their posts
-        //     See: https://github.com/shaarli/Shaarli/wiki/Datastore-hacks#changing-the-timestamp-for-a-link
+        //     See: https://shaarli.readthedocs.io/en/master/Various-hacks/#changing-the-timestamp-for-a-shaare
         $linkdate = escape($_POST['lf_linkdate']);
         if (isset($LINKSDB[$id])) {
             // Edit
@@ -1256,6 +1275,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
         // Remove duplicates.
         $tags = implode(' ', array_unique(explode(' ', $tags)));
 
+        if (empty(trim($_POST['lf_url']))) {
+            $_POST['lf_url'] = '?' . smallHash($linkdate . $id);
+        }
         $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols'));
 
         $link = array(
@@ -1325,10 +1347,17 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
             die('Wrong token.');
         }
 
-        if (strpos($_GET['lf_linkdate'], ' ') !== false) {
-            $ids = array_values(array_filter(preg_split('/\s+/', escape($_GET['lf_linkdate']))));
+        $ids = trim($_GET['lf_linkdate']);
+        if (strpos($ids, ' ') !== false) {
+            // multiple, space-separated ids provided
+            $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
         } else {
-            $ids = [$_GET['lf_linkdate']];
+            // only a single id provided
+            $ids = [$ids];
+        }
+        // assert at least one id is given
+        if(!count($ids)){
+            die('no id provided');
         }
         foreach ($ids as $id) {
             $id = (int) escape($id);
@@ -1414,7 +1443,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
 
             if ($url == '') {
                 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
-                $title = 'Note: ';
+                $title = $conf->get('general.default_note_title', 'Note: ');
             }
             $url = escape($url);
             $title = escape($title);
index 648d8f67b473ac75fa430108d04c4c9b2c624180..03a7a34e80e3f448e1e8bbffec77cbd76490dc29 100644 (file)
@@ -45,6 +45,7 @@ pages:
     - Static analysis: Static-analysis.md
     - Theming: Theming.md
     - Unit tests: Unit-tests.md
+    - Unit tests inside Docker: Unit-tests-Docker.md
 - About:
     - FAQ: FAQ.md
     - Community & Related software: Community-&-Related-software.md
index b1698470755d1d7adff2e91528f57518d14afbda..ab4be22a46b5ad2a222fb84e1bae5ca4af40e745 100644 (file)
@@ -8,7 +8,7 @@ This uses code from https://zaius.github.io/youtube_playlist/ and is currently o
 
 #### Installation and setup
 
-This is a default Shaarli plugin, you just have to enable it. See https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration/
+This is a default Shaarli plugin, you just have to enable it. See https://shaarli.readthedocs.io/en/master/Shaarli-configuration/
 
 
 #### Troubleshooting
diff --git a/tests/HttpUtils/IsHttpsTest.php b/tests/HttpUtils/IsHttpsTest.php
new file mode 100644 (file)
index 0000000..097f2bc
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+
+/**
+ * Class IsHttpsTest
+ *
+ * Test class for is_https() function.
+ */
+class IsHttpsTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Test is_https with HTTPS values.
+     */
+    public function testIsHttpsTrue()
+    {
+        $this->assertTrue(is_https(['HTTPS' => true]));
+        $this->assertTrue(is_https(['HTTPS' => '1']));
+        $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => 443]));
+        $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '443']));
+        $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '443,123,456,']));
+    }
+
+    /**
+     * Test is_https with HTTP values.
+     */
+    public function testIsHttpsFalse()
+    {
+        $this->assertFalse(is_https([]));
+        $this->assertFalse(is_https(['HTTPS' => false]));
+        $this->assertFalse(is_https(['HTTPS' => '0']));
+        $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => 123]));
+        $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '123']));
+        $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => ',123,456,']));
+    }
+}
index 7c0d4b0bdc9bf7e0c029231bab2d07e16db26082..c77922ecea0c2a13dcbd8ff59e961925775956cb 100644 (file)
@@ -103,6 +103,16 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
         $expectedText = 'stuff <a href="http://hello.there/is=someone#here">http://hello.there/is=someone#here</a> otherstuff';
         $processedText = text2clickable($text, '');
         $this->assertEquals($expectedText, $processedText);
+
+        $text = 'stuff http://hello.there/is=someone#here(please) otherstuff';
+        $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)">http://hello.there/is=someone#here(please)</a> otherstuff';
+        $processedText = text2clickable($text, '');
+        $this->assertEquals($expectedText, $processedText);
+
+        $text = 'stuff http://hello.there/is=someone#here(please)&no otherstuff';
+        $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)&no">http://hello.there/is=someone#here(please)&no</a> otherstuff';
+        $processedText = text2clickable($text, '');
+        $this->assertEquals($expectedText, $processedText);
     }
 
     /**
index 4cb70224ce734318862810e647eabdd52490d452..d22ed3bfe97fe8d98054f1eff9e9e3597573e5a9 100644 (file)
@@ -367,6 +367,89 @@ class GetLinksTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals(1, count($data));
         $this->assertEquals(41, $data[0]['id']);
         $this->assertEquals(self::NB_FIELDS_LINK, count($data[0]));
+
+        // wildcard: placeholder at the start
+        $env = Environment::mock([
+            'REQUEST_METHOD' => 'GET',
+            'QUERY_STRING' => 'searchtags=*Tuff',
+        ]);
+        $request = Request::createFromEnvironment($env);
+        $response = $this->controller->getLinks($request, new Response());
+        $this->assertEquals(200, $response->getStatusCode());
+        $data = json_decode((string) $response->getBody(), true);
+        $this->assertEquals(2, count($data));
+        $this->assertEquals(41, $data[0]['id']);
+
+        // wildcard: placeholder at the end
+        $env = Environment::mock([
+            'REQUEST_METHOD' => 'GET',
+            'QUERY_STRING' => 'searchtags=c*',
+        ]);
+        $request = Request::createFromEnvironment($env);
+        $response = $this->controller->getLinks($request, new Response());
+        $this->assertEquals(200, $response->getStatusCode());
+        $data = json_decode((string) $response->getBody(), true);
+        $this->assertEquals(4, count($data));
+        $this->assertEquals(6, $data[0]['id']);
+
+        // wildcard: placeholder at the middle
+        $env = Environment::mock([
+            'REQUEST_METHOD' => 'GET',
+            'QUERY_STRING' => 'searchtags=w*b',
+        ]);
+        $request = Request::createFromEnvironment($env);
+        $response = $this->controller->getLinks($request, new Response());
+        $this->assertEquals(200, $response->getStatusCode());
+        $data = json_decode((string) $response->getBody(), true);
+        $this->assertEquals(4, count($data));
+        $this->assertEquals(6, $data[0]['id']);
+
+        // wildcard: match all
+        $env = Environment::mock([
+            'REQUEST_METHOD' => 'GET',
+            'QUERY_STRING' => 'searchtags=*',
+        ]);
+        $request = Request::createFromEnvironment($env);
+        $response = $this->controller->getLinks($request, new Response());
+        $this->assertEquals(200, $response->getStatusCode());
+        $data = json_decode((string) $response->getBody(), true);
+        $this->assertEquals(9, count($data));
+        $this->assertEquals(41, $data[0]['id']);
+
+        // wildcard: optional ('*' does not need to expand)
+        $env = Environment::mock([
+            'REQUEST_METHOD' => 'GET',
+            'QUERY_STRING' => 'searchtags=*stuff*',
+        ]);
+        $request = Request::createFromEnvironment($env);
+        $response = $this->controller->getLinks($request, new Response());
+        $this->assertEquals(200, $response->getStatusCode());
+        $data = json_decode((string) $response->getBody(), true);
+        $this->assertEquals(2, count($data));
+        $this->assertEquals(41, $data[0]['id']);
+
+        // wildcard: exclusions
+        $env = Environment::mock([
+            'REQUEST_METHOD' => 'GET',
+            'QUERY_STRING' => 'searchtags=*a*+-*e*',
+        ]);
+        $request = Request::createFromEnvironment($env);
+        $response = $this->controller->getLinks($request, new Response());
+        $this->assertEquals(200, $response->getStatusCode());
+        $data = json_decode((string) $response->getBody(), true);
+        $this->assertEquals(1, count($data));
+        $this->assertEquals(41, $data[0]['id']); // finds '#hashtag' in descr.
+
+        // wildcard: exclude all
+        $env = Environment::mock([
+            'REQUEST_METHOD' => 'GET',
+            'QUERY_STRING' => 'searchtags=-*',
+        ]);
+        $request = Request::createFromEnvironment($env);
+        $response = $this->controller->getLinks($request, new Response());
+        $this->assertEquals(200, $response->getStatusCode());
+        $data = json_decode((string) $response->getBody(), true);
+        $this->assertEquals(0, count($data));
     }
 
     /**
index 6c9c9adce8cb6b02653bf32b4660f5f69c04edd5..4569c923b3171609d8a755cd32aa4cdf06c755a2 100644 (file)
@@ -81,12 +81,12 @@ class UtilsDeTest extends UtilsTest
     }
 
     /**
-     * Test autoLocale with multiples value, the second one is valid
+     * Test autoLocale with multiples value, the second one is available
      */
-    public function testAutoLocaleMultipleSecondValid()
+    public function testAutoLocaleMultipleSecondAvailable()
     {
         $current = setlocale(LC_ALL, 0);
-        $header = 'pt_BR,fr-fr';
+        $header = 'mag_IN,fr-fr';
         autoLocale($header);
         $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0));
 
@@ -106,12 +106,12 @@ class UtilsDeTest extends UtilsTest
     }
 
     /**
-     * Test autoLocale with an invalid value: defaults to en_US.
+     * Test autoLocale with an unavailable value: defaults to en_US.
      */
-    public function testAutoLocaleInvalid()
+    public function testAutoLocaleUnavailable()
     {
         $current = setlocale(LC_ALL, 0);
-        autoLocale('pt_BR');
+        autoLocale('mag_IN');
         $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
 
         setlocale(LC_ALL, $current);
index d8680b2b429c1ae6ab4b1f0f888e61c5d3faf4a4..a74063ae92de8fdd41c65727f64c8e071b0bfebd 100644 (file)
@@ -81,12 +81,12 @@ class UtilsEnTest extends UtilsTest
     }
 
     /**
-     * Test autoLocale with multiples value, the second one is valid
+     * Test autoLocale with multiples value, the second one is available
      */
-    public function testAutoLocaleMultipleSecondValid()
+    public function testAutoLocaleMultipleSecondAvailable()
     {
         $current = setlocale(LC_ALL, 0);
-        $header = 'pt_BR,fr-fr';
+        $header = 'mag_IN,fr-fr';
         autoLocale($header);
         $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0));
 
@@ -106,12 +106,12 @@ class UtilsEnTest extends UtilsTest
     }
 
     /**
-     * Test autoLocale with an invalid value: defaults to en_US.
+     * Test autoLocale with an unavailable value: defaults to en_US.
      */
-    public function testAutoLocaleInvalid()
+    public function testAutoLocaleUnavailable()
     {
         $current = setlocale(LC_ALL, 0);
-        autoLocale('pt_BR');
+        autoLocale('mag_IN');
         $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
 
         setlocale(LC_ALL, $current);
index 0d50a87829f241591b0868446831c2918dcd4618..3dbb126faef25b2b82b6d7bf4571eff124ec98d1 100644 (file)
@@ -81,12 +81,12 @@ class UtilsFrTest extends UtilsTest
     }
 
     /**
-     * Test autoLocale with multiples value, the second one is valid
+     * Test autoLocale with multiples value, the second one is available
      */
-    public function testAutoLocaleMultipleSecondValid()
+    public function testAutoLocaleMultipleSecondAvailable()
     {
         $current = setlocale(LC_ALL, 0);
-        $header = 'pt_BR,de-de';
+        $header = 'mag_IN,de-de';
         autoLocale($header);
         $this->assertEquals('de_DE.utf8', setlocale(LC_ALL, 0));
 
@@ -106,12 +106,12 @@ class UtilsFrTest extends UtilsTest
     }
 
     /**
-     * Test autoLocale with an invalid value: defaults to en_US.
+     * Test autoLocale with an unavailable value: defaults to en_US.
      */
-    public function testAutoLocaleInvalid()
+    public function testAutoLocaleUnavailable()
     {
         $current = setlocale(LC_ALL, 0);
-        autoLocale('pt_BR');
+        autoLocale('mag_IN');
         $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
 
         setlocale(LC_ALL, $current);
index e1868c59aafaa5da7f37b5b6972ac5986be0d6fb..ba589723b3c999e1361f6cab0cc8ebbaf3e830c1 100644 (file)
@@ -539,7 +539,7 @@ body, .pure-g [class*="pure-u"] {
 }
 
 .linklist-item-title a:visited .linklist-link {
-    color: #555555;
+    color: #2a4c41;
 }
 
 .linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{
index 0350ef6681371e7a24263e29c8e29be8d2b5cef7..80c083331d10a31dced6a4188cfa3f858fbf5397 100644 (file)
@@ -5,16 +5,16 @@
 <link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
 <link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
 <link href="img/favicon.png" rel="shortcut icon" type="image/png" />
-<link type="text/css" rel="stylesheet" href="css/pure.min.css" />
-<link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css">
-<link type="text/css" rel="stylesheet" href="css/pure-extras.css">
-<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css" />
-<link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
-<link type="text/css" rel="stylesheet" href="css/shaarli.css" />
+<link type="text/css" rel="stylesheet" href="css/pure.min.css?v={$version_hash}" />
+<link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css?v={$version_hash}">
+<link type="text/css" rel="stylesheet" href="css/pure-extras.css?v={$version_hash}">
+<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css?v={$version_hash}" />
+<link type="text/css" rel="stylesheet" href="inc/awesomplete.css?v={$version_hash}#" />
+<link type="text/css" rel="stylesheet" href="css/shaarli.css?v={$version_hash}" />
 {if="is_file('data/user.css')"}
   <link type="text/css" rel="stylesheet" href="data/user.css#" />
 {/if}
 {loop="$plugins_includes.css_files"}
-  <link type="text/css" rel="stylesheet" href="{$value}#"/>
+  <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
 {/loop}
 <link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/>
\ No newline at end of file
index 4f49affa3321226024bae670117d7851ba385141..55656f80ea39032dd62b321cf6e707aa3e786868 100644 (file)
@@ -275,8 +275,14 @@ window.onload = function () {
     };
     function init () {
         function resize () {
+            /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
+            var scrollTop  = window.pageYOffset ||
+                (document.documentElement || document.body.parentNode || document.body).scrollTop;
+
             description.style.height = 'auto';
             description.style.height = description.scrollHeight+10+'px';
+
+            window.scrollTo(0, scrollTop);
         }
         /* 0-timeout to get the already changed text */
         function delayedResize () {
@@ -401,14 +407,14 @@ window.onload = function () {
 
             var message = 'Are you sure you want to delete '+ links.length +' links?\n';
             message += 'This action is IRREVERSIBLE!\n\nTitles:\n';
-            var ids = '';
+            var ids = [];
             links.forEach(function(item) {
                 message += '  - '+ item['title'] +'\n';
-                ids += item['id'] +'+';
+                ids.push(item['id']);
             });
 
             if (window.confirm(message)) {
-                window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value;
+                window.location = '?delete_link&lf_linkdate='+ ids.join('+') +'&token='+ token.value;
             }
         });
     }
@@ -607,10 +613,11 @@ function htmlEntities(str)
 function activateFirefoxSocial(node) {
     var loc = location.href;
     var baseURL = loc.substring(0, loc.lastIndexOf("/") + 1);
+    var title = document.title;
 
     // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
     var data = {
-        name: "{$shaarlititle}",
+        name: title,
         description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
         author: "Shaarli",
         version: "1.0.0",
index eb6d83781817690374ad593d4457eaf0fdd60401..5777a2186da5d94521d5d7c43726a4ab79da1de1 100644 (file)
@@ -30,7 +30,8 @@
         </div>
         <div class="remember-me">
           <input type="checkbox" name="longlastingsession" id="longlastingsessionform"
-             checked="checked" tabindex="22">
+             {if="$remember_user_default"}checked="checked"{/if}
+             tabindex="22">
           <label for="longlastingsessionform">{'Remember me'|t}</label>
         </div>
         <div>
index 94f771a250c40583cac13bdae5f8b08a65976eaa..54b16e8a3454dabcdd248dd21ae567ed6cac7814 100644 (file)
@@ -27,6 +27,6 @@
        <script src="{$value}#"></script>
 {/loop}
 
-<script src="js/shaarli.js"></script>
-<script src="inc/awesomplete.js#"></script>
-<script src="inc/awesomplete-multiple-tags.js#"></script>
+<script src="js/shaarli.js?v={$version_hash}"></script>
+<script src="inc/awesomplete.js?v={$version_hash}#"></script>
+<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script>
index 96b357a3e1ec282885c4314388a0a67adb4131a5..68335c709ba90b5be6b79e80e2cabd9c03b05b9f 100644 (file)
@@ -26,7 +26,7 @@
           <input type="hidden" name="do" value="tagcloud">
           <input type="text" name="searchtags" placeholder="{'Filter by tag'|t}"
                  {if="!empty($search_tags)"}
-                 value="{$search_tags}"
+                    value="{$search_tags}"
                  {/if}
           autocomplete="off" data-multiple data-autofirst data-minChars="1"
           data-list="{loop="$tags"}{$key}, {/loop}"
index 35173d179144aab4bac64494cd2e4177e13f61f2..72fd58aff680b59a1773bc25c6624550034c318d 100644 (file)
@@ -97,7 +97,7 @@
             var%20desc=document.getSelection().toString();
             if(desc.length>4000){
               desc=desc.substr(0,4000)+'...';
-              alert("{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}");
+              alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}');
             }
             window.open(
               '{$pageabsaddr}?private=1&amp;post='+
index 84176385765837973d77be29941db622ccad1f6a..1becd44f7db6c305d9bed899c65c13db6aa16305 100644 (file)
@@ -24,7 +24,9 @@
         </label>
         <input type="submit" value="Login" class="bigbutton" tabindex="4">
         <label for="longlastingsession">
-          <input type="checkbox" name="longlastingsession" id="longlastingsession" tabindex="3">
+          <input type="checkbox" name="longlastingsession"
+                 id="longlastingsession" tabindex="3"
+                 {if="$remember_user_default"}checked="checked"{/if}>
           Stay signed in (Do not check on public computers)</label>
         <input type="hidden" name="token" value="{$token}">
         {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}