]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge pull request #977 from ArthurHoaro/feature/dl-filter
authorArthurHoaro <arthur@hoa.ro>
Tue, 23 Jan 2018 17:41:38 +0000 (18:41 +0100)
committerGitHub <noreply@github.com>
Tue, 23 Jan 2018 17:41:38 +0000 (18:41 +0100)
Extract the title/charset during page download, and check content type

120 files changed:
.editorconfig [new file with mode: 0644]
.gitattributes
.github/mailmap
.gitignore
.travis.yml
AUTHORS
CHANGELOG.md
Makefile
README.md
application/ApplicationUtils.php
application/Cache.php
application/FeedBuilder.php
application/FileUtils.php
application/History.php
application/HttpUtils.php
application/Languages.php
application/LinkDB.php
application/LinkFilter.php
application/LinkUtils.php
application/NetscapeBookmarkUtils.php
application/PageBuilder.php
application/PluginManager.php
application/SessionManager.php [new file with mode: 0644]
application/ThemeUtils.php
application/Updater.php
application/Utils.php
application/config/ConfigJson.php
application/config/ConfigManager.php
application/config/ConfigPhp.php
application/config/exception/MissingFieldConfigException.php
application/config/exception/PluginConfigOrderException.php
application/config/exception/UnauthorizedConfigException.php
application/exceptions/IOException.php
composer.json
composer.lock
data/.htaccess
doc/md/Backup,-restore,-import-and-export.md
doc/md/Bookmarklet.md
doc/md/Browsing-and-searching.md
doc/md/Community-&-Related-software.md
doc/md/Download-and-Installation.md
doc/md/Features.md [deleted file]
doc/md/Firefox-share.md
doc/md/Server-requirements.md
doc/md/Shaarli-configuration.md
doc/md/Translations.md [new file with mode: 0644]
doc/md/Unit-tests.md
doc/md/Upgrade-and-migration.md
doc/md/docker/docker-101.md
doc/md/docker/reverse-proxy-configuration.md
doc/md/docker/shaarli-images.md
doc/md/images/install-shaarli.png [new file with mode: 0644]
doc/md/images/poedit-1.jpg [new file with mode: 0644]
doc/md/index.md
docker/alpine/Dockerfile.armhf.latest [new file with mode: 0644]
docker/alpine/Dockerfile.armhf.master [new file with mode: 0644]
docker/alpine/Dockerfile.latest [new file with mode: 0644]
docker/alpine/Dockerfile.master [new file with mode: 0644]
docker/alpine/IMAGE.md [new file with mode: 0644]
docker/alpine/nginx.conf [moved from docker/production/stable/nginx.conf with 94% similarity]
docker/alpine/php-fpm.conf [new file with mode: 0644]
docker/alpine/services.d/.s6-svscan/finish [new file with mode: 0755]
docker/alpine/services.d/nginx/run [new file with mode: 0755]
docker/alpine/services.d/php-fpm/run [new file with mode: 0755]
docker/debian/Dockerfile.stable [moved from docker/production/stable/Dockerfile with 100% similarity]
docker/debian/IMAGE.md [moved from docker/production/stable/IMAGE.md with 100% similarity]
docker/debian/nginx.conf [moved from docker/production/nginx.conf with 100% similarity]
docker/debian/supervised.conf [moved from docker/production/stable/supervised.conf with 100% similarity]
docker/production/Dockerfile [deleted file]
docker/production/IMAGE.md [deleted file]
docker/production/supervised.conf [deleted file]
inc/languages/fr/LC_MESSAGES/shaarli.po [new file with mode: 0644]
index.php
mkdocs.yml
plugins/TODO.md [deleted file]
plugins/addlink_toolbar/addlink_toolbar.php
plugins/archiveorg/archiveorg.html
plugins/archiveorg/archiveorg.php
plugins/demo_plugin/demo_plugin.php
plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo [new file with mode: 0644]
plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po [new file with mode: 0644]
plugins/isso/isso.php
plugins/markdown/help.html
plugins/markdown/markdown.php
plugins/piwik/piwik.php
plugins/playvideos/playvideos.php
plugins/pubsubhubbub/pubsubhubbub.php
plugins/qrcode/qrcode.meta
plugins/qrcode/qrcode.php
plugins/wallabag/wallabag.html
plugins/wallabag/wallabag.php
tests/HttpUtils/ServerUrlTest.php
tests/LanguagesTest.php
tests/LinkFilterTest.php
tests/LinkUtilsTest.php
tests/NetscapeBookmarkUtils/BookmarkImportTest.php
tests/SessionManagerTest.php [new file with mode: 0644]
tests/UtilsTest.php
tests/bootstrap.php [new file with mode: 0644]
tests/languages/bootstrap.php
tests/languages/fr/LanguagesFrTest.php [new file with mode: 0644]
tests/utils/FakeConfigManager.php [new file with mode: 0644]
tests/utils/ReferenceLinkDB.php
tests/utils/languages/fr/LC_MESSAGES/test.mo [new file with mode: 0644]
tests/utils/languages/fr/LC_MESSAGES/test.po [new file with mode: 0644]
tpl/default/changetag.html
tpl/default/configure.html
tpl/default/css/shaarli.css
tpl/default/img/apple-touch-icon.png [new file with mode: 0644]
tpl/default/import.html
tpl/default/includes.html
tpl/default/install.html
tpl/default/js/shaarli.js
tpl/default/linklist.html
tpl/default/linklist.paging.html
tpl/default/page.footer.html
tpl/default/page.header.html
tpl/default/pluginsadmin.html
tpl/default/tag.cloud.html
tpl/default/tag.list.html

diff --git a/.editorconfig b/.editorconfig
new file mode 100644 (file)
index 0000000..4a6589a
--- /dev/null
@@ -0,0 +1,23 @@
+# EditorConfig: http://EditorConfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.{htaccess,html,xml}]
+indent_size = 2
+
+[*.php]
+max_line_length = 100
+
+[Dockerfile]
+max_line_length = 80
+
+[Makefile]
+indent_style = tab
index dd0e573cffb0d6a6aa35bd3a701ea87522963387..b191e2277e675afa959ee0cafb3a239ad2a715b3 100644 (file)
@@ -22,8 +22,10 @@ Dockerfile      text
 *.ttf           binary
 *.min.css       binary
 *.min.js        binary
+*.mo            binary
 
 # Exclude from Git archives
+.editorconfig   export-ignore
 .gitattributes  export-ignore
 .github         export-ignore
 .gitignore      export-ignore
index 41d91e4758eda026135d1a8d330955558b6b6dd3..7633afcf23829481372b4810045db893ec4feaf5 100644 (file)
@@ -1,6 +1,8 @@
 ArthurHoaro <arthur@hoa.ro>
 Florian Eula <eula.florian@gmail.com> feula
 Florian Eula <eula.florian@gmail.com> <mr.pikzen@gmail.com>
+Immánuel Fodor <immanuelfactor+github@gmail.com>
+kalvn <kalvnthereal@gmail.com> <kalvn@users.noreply.github.com>
 Nicolas Danelon <hi@nicolasmd.com.ar> nicolasm
 Nicolas Danelon <hi@nicolasmd.com.ar> <nda@3818.com.ar>
 Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@gmail.com>
@@ -11,3 +13,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 d546f248af2b64bf8ed443f58585636e1e288871..3f6939a41503144d044b55dce89a646738b40de4 100644 (file)
@@ -18,6 +18,7 @@ vendor/
 # Release archives
 *.tar.gz
 *.zip
+inc/languages/*/LC_MESSAGES/shaarli.mo
 
 # Development and test resources
 coverage
index b6b9bddf60d0595dae6069fd33cf8ad166c5cee5..322e4337fbbb2f8a2ba35de3ccba63e93ab09749 100644 (file)
@@ -13,6 +13,8 @@ install:
   - composer self-update
   - composer install --prefer-dist
   - locale -a
+before_script:
+  - PATH=${PATH//:\.\/node_modules\/\.bin/}
 script:
   - make clean
   - make check_permissions
diff --git a/AUTHORS b/AUTHORS
index 2181ec9d9869282b33dc6a3d3dcdddb8993c850d..6c4d9529b977d17f84a4e495effa6ad13698bc74 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,16 +1,19 @@
-   518 ArthurHoaro <arthur@hoa.ro>
-   231 VirtualTam <virtualtam@flibidi.net>
-   147 nodiscc <nodiscc@gmail.com>
+   574 ArthurHoaro <arthur@hoa.ro>
+   277 VirtualTam <virtualtam@flibidi.net>
+   169 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>
+     4 kalvn <kalvnthereal@gmail.com>
+     3 Immánuel Fodor <immanuelfactor+github@gmail.com>
      3 Teromene <teromene@teromene.fr>
-     3 kalvn <kalvnthereal@gmail.com>
      2 Chris Kuethe <chris.kuethe@gmail.com>
      2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
      2 Mathieu Chabanon <git@matchab.fr>
@@ -25,6 +28,7 @@
      1 BoboTiG <bobotig@gmail.com>
      1 Bronco <bronco@warriordudimanche.net>
      1 D Low <daniellowtw@gmail.com>
+     1 Daniel Jakots <vigdis@chown.me>
      1 Dimtion <zizou.xena@gmail.com>
      1 Fanch <fanch-github@qth.fr>
      1 Felix Bartels <felix@host-consultants.de>
      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 Neros <contact@neros.fr>
      1 Sbgodin <Sbgodin@users.noreply.github.com>
      1 TsT <tst2005@gmail.com>
      1 dimtion <zizou.xena@gmail.com>
+     1 durcheinandr <jochen@durcheinandr.de>
index 60262d564d3983084a96f823a241e64f49d80a8c..47a902f0fa20c1c7d59cc30f2804f74810e1d39e 100644 (file)
@@ -4,6 +4,80 @@ 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.4](https://github.com/shaarli/Shaarli/releases/tag/v0.9.4) - UNPUBLISHED
+### Added
+- Enable translations: Shaarli is now also available in French. Other language translations are welcome!
+- Add EditorConfig configuration
+- Add favicons for mobile devices
+- Add Alpine Linux arm32v7 Dockerfiles (master, latest)
+
+### Changed
+- Do not write bookmark edition history during file imports (performance)
+- Migrate Docker images (master, latest) to Alpine Linux
+- Improve unitary tests and code coverage
+- Improve thumbnail display
+- Improve theme ergonomics
+- Improve messages if there is no plugin or parameter available in the admin page
+- Increase buffer size for cURL download
+- Force HTTPS if the original port is 443 behind a reverse proxy (workaround)
+
+### Removed
+- Remove redirector setting from Configure page
+
+### Fixed
+- Fix broken links in the documentation
+- Enable access to `data/user.css` (Apache 2.2 & 2.4)
+- Don't URL encode description links if parameter `redirector.encode_url` is set to false
+- Fix an issue preventing the Save button to appear for plugin parameters
+
+
+## [v0.9.3](https://github.com/shaarli/Shaarli/releases/tag/v0.9.3) - 2018-01-04
+**XSS vulnerability fixed. Please update.**
+
+## Security
+- Fix an XSS (cross-site-scripting) vulnerability in `index.php` -
+  [CVE-2018-5249](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-5249)
+
+
+## [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
+
+- Fixed reflected XSS vulnerability introduced in v0.9.1, discovered by @chb9 ([CVE-2017-15215](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15215)).
+
+
 ## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23
 
 The documentation has been migrated to ReadTheDocs:
@@ -77,7 +151,7 @@ Theming:
     - Introduce a new theme
     - Allow selecting themes/templates from the configuration page
     - New/Edit link form can be submitted using CTRL+Enter in the textarea
-    - Shaarli version is displayed in the footer when logged in 
+    - Shaarli version is displayed in the footer when logged in
 - Add plugin placeholders to Atom/RSS feed templates
 - Add OpenSearch to feed templates
 - Add `campaign_` to the URL cleanup pattern list
@@ -107,7 +181,7 @@ Theming:
 - Improved date time display depending on the locale
 - Partial namespace support for Shaarli classes
 - Shaarli version is now only present in `shaarli_version.php`
-- Human readable maximum file size upload 
+- Human readable maximum file size upload
 
 
 ### Removed
@@ -133,6 +207,13 @@ Theming:
 - Markdown plugin: escape HTML entities by default
 
 
+## [v0.8.5](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5) - 2018-01-04
+**XSS vulnerability fixed. Please update.**
+
+## Security
+- Fix an XSS (cross-site-scripting) vulnerability in `index.php` -
+  [CVE-2018-5249](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-5249)
+
 ## [v0.8.4](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) - 2017-03-04
 ### Security
 - Markdown plugin: escape HTML entities by default
@@ -148,7 +229,7 @@ Theming:
 
 ## [v0.8.1](https://github.com/shaarli/Shaarli/releases/tag/v0.8.1) - 2016-12-12
 
-> Note: this version will create an automatic backup of your database if anything goes wrong. 
+> Note: this version will create an automatic backup of your database if anything goes wrong.
 
 ### Added
 - Add CHANGELOG.md to track the whole project's history
@@ -165,7 +246,7 @@ Theming:
 - Link ID complete refactoring:
     - Links now have a numeric ID instead of dates
     - Short URLs are now created once and can't change over time (previous URL are kept)
-- Templates: 
+- Templates:
     - Changed placeholder behaviour for: `buttons_toolbar`, `fields_toolbar` and `action_plugin`
     - Cleanup `{loop}` declarations in templates
     - Tools: hide Firefox Social button when not in HTTPS
@@ -183,7 +264,7 @@ Theming:
 - Plugins:
     - Tools: only display parameter description when it exists
     - archive.org: do not propose archival of private notes
-    - Markdown: 
+    - Markdown:
         - render links properly in code blocks
         - bug regarding the `nomarkdown` tag
     - W3C compliance
@@ -322,7 +403,7 @@ Please use our release archives, or follow the
 ### Fixed
 - Fix a bug where renaming a tag was causing a 404
 - Fix a bug allowing to search blank terms
-- Fix a bug preventing to remove a tag with special chars when searching 
+- Fix a bug preventing to remove a tag with special chars when searching
 
 
 ## [v0.6.2](https://github.com/shaarli/Shaarli/releases/tag/v0.6.2) - 2015-12-23
@@ -628,7 +709,7 @@ Initial release on GitHub.
 - When you click the key to see only private links, it turns yellow
 
 ### Changed
-- The "Daily" page now automatically skips empty days. 
+- The "Daily" page now automatically skips empty days.
 
 ### Fixed
 - Corrected the tag encoding (there was a bug when selecting a second tag which contains accented characters)
@@ -926,7 +1007,7 @@ Initial release on GitHub.
 - Nicer timezone selection patch by killruana
 
 ### Fixed
-- New lines now appear correctly in the RSS feed descriptions. 
+- New lines now appear correctly in the RSS feed descriptions.
 
 
 ## [v0.0.17beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
@@ -980,7 +1061,7 @@ Initial release on GitHub.
 ## [v0.0.14beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
 ### Added
 - You no longer need to disable `magic_quotes` on your host.
-  Shaarli will cope with this option beeing activated. 
+  Shaarli will cope with this option beeing activated.
 
 
 ## [v0.0.13beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
index a3696ec987886657dfd2ecca1345575c4928c38c..d659d908e45aad561638d8f7617b30b3ed51c35a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,6 @@
 # The personal, minimalist, super-fast, database free, bookmarking service.
 # Makefile for PHP code analysis & testing, documentation and release generation
 
-# Prerequisites:
-# - install Composer, either:
-#   - from your distro's package manager;
-#   - from the official website (https://getcomposer.org/download/);
-# - install/update test dependencies:
-#   $ composer install  # 1st setup
-#   $ composer update
-# - install Xdebug for PHPUnit code coverage reports:
-#   - see http://xdebug.org/docs/install
-#   - enable in php.ini
-
 BIN = vendor/bin
 PHP_SOURCE = index.php application tests plugins
 PHP_COMMA_SOURCE = index.php,application,tests,plugins
@@ -115,7 +104,7 @@ check_permissions:
        @echo "----------------------"
        @echo "Check file permissions"
        @echo "----------------------"
-       @for file in `git ls-files`; do \
+       @for file in `git ls-files | grep -v docker`; do \
                if [ -x $$file ]; then \
                        errors=true; \
                        echo "$${file} is executable"; \
@@ -130,12 +119,12 @@ check_permissions:
 # See phpunit.xml for configuration
 # https://phpunit.de/manual/current/en/appendixes.configuration.html
 ##
-test:
+test: translate
        @echo "-------"
        @echo "PHPUNIT"
        @echo "-------"
        @mkdir -p sandbox coverage
-       @$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests
+       @$(BIN)/phpunit --coverage-php coverage/main.cov --bootstrap tests/bootstrap.php --testsuite unit-tests
 
 locale_test_%:
        @UT_LOCALE=$*.utf8 \
@@ -168,15 +157,15 @@ composer_dependencies: clean
        composer install --no-dev --prefer-dist
        find vendor/ -name ".git" -type d -exec rm -rf {} +
 
-### generate a release tarball and include 3rd-party dependencies
-release_tar: composer_dependencies htmldoc
+### generate a release tarball and include 3rd-party dependencies and translations
+release_tar: composer_dependencies htmldoc translate
        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 htmldoc
+### generate a release zip and include 3rd-party dependencies and translations
+release_zip: composer_dependencies htmldoc translate
        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/
@@ -213,3 +202,8 @@ htmldoc:
        mkdocs build'
        find doc/html/ -type f -exec chmod a-x '{}' \;
        rm -r venv
+
+
+### Generate Shaarli's translation compiled file (.mo)
+translate:
+       @find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \;
\ No newline at end of file
index 100ff46bc94d90200b10b3b96e3b0c1dd0e06453..e7e8ad4c67fa859e59ff23932363e2e1fad27093 100644 (file)
--- a/README.md
+++ b/README.md
@@ -6,10 +6,10 @@ _Do you want to share the links you discover?_
 _Shaarli is a minimalist link sharing service that you can install on your own server._
 _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/badge/stable-v0.8.5-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5)
 [![](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.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1)
+[![](https://img.shields.io/badge/latest-v0.9.3-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.3)
 [![](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..911873a071ca7813f7ef9e3fa75d8ce18b2f8ec8 100644 (file)
@@ -149,12 +149,13 @@ class ApplicationUtils
     public static function checkPHPVersion($minVersion, $curVersion)
     {
         if (version_compare($curVersion, $minVersion) < 0) {
-            throw new Exception(
+            $msg = t(
                 'Your PHP version is obsolete!'
-                .' Shaarli requires at least PHP '.$minVersion.', and thus cannot run.'
-                .' Your PHP version has known security vulnerabilities and should be'
-                .' updated as soon as possible.'
+                 . ' Shaarli requires at least PHP %s, and thus cannot run.'
+                 . ' Your PHP version has known security vulnerabilities and should be'
+                 . ' updated as soon as possible.'
             );
+            throw new Exception(sprintf($msg, $minVersion));
         }
     }
 
@@ -168,17 +169,18 @@ 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';
+                $errors[] = '"'.$path.'" '. t('directory is not readable');
             }
         }
 
@@ -190,10 +192,10 @@ class ApplicationUtils
             $conf->get('resource.raintpl_tmp'),
         ) as $path) {
             if (! is_readable(realpath($path))) {
-                $errors[] = '"'.$path.'" directory is not readable';
+                $errors[] = '"'.$path.'" '. t('directory is not readable');
             }
             if (! is_writable(realpath($path))) {
-                $errors[] = '"'.$path.'" directory is not writable';
+                $errors[] = '"'.$path.'" '. t('directory is not writable');
             }
         }
 
@@ -211,13 +213,28 @@ class ApplicationUtils
             }
 
             if (! is_readable(realpath($path))) {
-                $errors[] = '"'.$path.'" file is not readable';
+                $errors[] = '"'.$path.'" '. t('file is not readable');
             }
             if (! is_writable(realpath($path))) {
-                $errors[] = '"'.$path.'" file is not writable';
+                $errors[] = '"'.$path.'" '. t('file is not writable');
             }
         }
 
         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 5d050165dbf7e8472094f7df8d984808cccb9413..e5d43e611656823eaef4b952219f9a7755198026 100644 (file)
@@ -13,7 +13,7 @@
 function purgeCachedPages($pageCacheDir)
 {
     if (! is_dir($pageCacheDir)) {
-        $error = 'Cannot purge '.$pageCacheDir.': no directory';
+        $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
         error_log($error);
         return $error;
     }
index 7377bcec09c3fa21b92434953661133b386e0829..ebae18b41c27ac011ff0cb12f8ad69fc6728d13e 100644 (file)
@@ -148,11 +148,11 @@ class FeedBuilder
             $link['url'] = $pageaddr . $link['url'];
         }
         if ($this->usePermalinks === true) {
-            $permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>';
+            $permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
         } else {
-            $permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>';
+            $permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
         }
-        $link['description']  = format_description($link['description'], '', $pageaddr);
+        $link['description']  = format_description($link['description'], '', false, $pageaddr);
         $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
 
         $pubDate = $link['created'];
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 116b9264019c2a667e19f2d1e47aee0d5ab0b362..35ec016a9be27c374dec0e96cbcd0d27e20a119a 100644 (file)
@@ -16,6 +16,7 @@
  *   - UPDATED: link updated
  *   - DELETED: link deleted
  *   - SETTINGS: the settings have been updated through the UI.
+ *   - IMPORT: bulk links import
  *
  * Note: new events are put at the beginning of the file and history array.
  */
@@ -41,6 +42,11 @@ class History
      */
     const SETTINGS = 'SETTINGS';
 
+    /**
+     * @var string Action key: a bulk import has been processed.
+     */
+    const IMPORT = 'IMPORT';
+
     /**
      * @var string History file path.
      */
@@ -121,6 +127,16 @@ class History
         $this->addEvent(self::SETTINGS);
     }
 
+    /**
+     * Add Event: bulk import.
+     *
+     * Note: we don't store links add/update one by one since it can have a huge impact on performances.
+     */
+    public function importLinks()
+    {
+        $this->addEvent(self::IMPORT);
+    }
+
     /**
      * Save a new event and write it in the history file.
      *
@@ -155,7 +171,7 @@ class History
         }
 
         if (! is_writable($this->historyFilePath)) {
-            throw new Exception('History file isn\'t readable or writable');
+            throw new Exception(t('History file isn\'t readable or writable'));
         }
     }
 
@@ -166,7 +182,7 @@ class History
     {
         $this->history = FileUtils::readFlatDB($this->historyFilePath, []);
         if ($this->history === false) {
-            throw new Exception('Could not parse history file');
+            throw new Exception(t('Could not parse history file'));
         }
     }
 
index 2edf5ce2df74bcb39fd5a1ce15c401302ff666c1..83a4c5e28699eff2472e4b7c132cef1e8bc53e8b 100644 (file)
@@ -82,7 +82,7 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
     }
 
     // Max download size management
-    curl_setopt($ch, CURLOPT_BUFFERSIZE,        1024);
+    curl_setopt($ch, CURLOPT_BUFFERSIZE,        1024*16);
     curl_setopt($ch, CURLOPT_NOPROGRESS,        false);
     curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
         function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes)
@@ -308,6 +308,13 @@ function server_url($server)
                 $port = $server['HTTP_X_FORWARDED_PORT'];
             }
 
+            // This is a workaround for proxies that don't forward the scheme properly.
+            // Connecting over port 443 has to be in HTTPS.
+            // See https://github.com/shaarli/Shaarli/issues/1022
+            if ($port == '443') {
+                $scheme = 'https';
+            }
+
             if (($scheme == 'http' && $port != '80')
                 || ($scheme == 'https' && $port != '443')
             ) {
index c8b0a25a355c2a60ffbaf33a16cb6d3d06ee1b5a..357c7524ed458a128148522b124dd17876faac87 100644 (file)
 <?php
 
+namespace Shaarli;
+
+use Gettext\GettextTranslator;
+use Gettext\Merge;
+use Gettext\Translations;
+use Gettext\Translator;
+use Gettext\TranslatorInterface;
+use Shaarli\Config\ConfigManager;
+
 /**
- * Wrapper function for translation which match the API
- * of gettext()/_() and ngettext().
+ * Class Languages
+ *
+ * Load Shaarli translations using 'gettext/gettext'.
+ * This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
+ * with a fixed language, or dynamically using autoLocale().
  *
- * Not doing translation for now.
+ * Translation files PO/MO files follow gettext standard and must be placed under:
+ *   <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
  *
- * @param string $text  Text to translate.
- * @param string $nText The plural message ID.
- * @param int    $nb    The number of items for plural forms.
+ * Pros/cons:
+ *   - gettext extension is faster
+ *   - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
  *
- * @return String Text translated.
+ * Settings:
+ *   - translation.mode:
+ *     - auto: use default setting (PHP implementation)
+ *     - php: use PHP implementation
+ *     - gettext: use gettext wrapper
+ *   - translation.language:
+ *     - auto: use autoLocale() and the language change according to user HTTP headers
+ *     - fixed language: e.g. 'fr'
+ *   - translation.extensions:
+ *     - domain => translation_path: allow plugins and themes to extend the defaut extension
+ *       The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
+ *
+ * @package Shaarli
  */
-function t($text, $nText = '', $nb = 0) {
-    if (empty($nText)) {
-        return $text;
+class Languages
+{
+    /**
+     * Core translations domain
+     */
+    const DEFAULT_DOMAIN = 'shaarli';
+
+    /**
+     * @var TranslatorInterface
+     */
+    protected $translator;
+
+    /**
+     * @var string
+     */
+    protected $language;
+
+    /**
+     * @var ConfigManager
+     */
+    protected $conf;
+
+    /**
+     * Languages constructor.
+     *
+     * @param string        $language lang determined by autoLocale(), can be overridden.
+     * @param ConfigManager $conf     instance.
+     */
+    public function __construct($language, $conf)
+    {
+        $this->conf = $conf;
+        $confLanguage = $this->conf->get('translation.language', 'auto');
+        if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
+            $this->language = substr($language, 0, 5);
+        } else {
+            $this->language = $confLanguage;
+        }
+
+        if (! extension_loaded('gettext')
+            || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
+        ) {
+            $this->initPhpTranslator();
+        } else {
+            $this->initGettextTranslator();
+        }
+
+        // Register default functions (e.g. '__()') to use our Translator
+        $this->translator->register();
+    }
+
+    /**
+     * Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
+     */
+    protected function initGettextTranslator ()
+    {
+        $this->translator = new GettextTranslator();
+        $this->translator->setLanguage($this->language);
+        $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
+
+        foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
+            if ($domain !== self::DEFAULT_DOMAIN) {
+                $this->translator->loadDomain($domain, $translationPath, false);
+            }
+        }
+    }
+
+    /**
+     * Initialize the translator using a PHP implementation of gettext.
+     *
+     * Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
+     */
+    protected function initPhpTranslator()
+    {
+        $this->translator = new Translator();
+        $translations = new Translations();
+        // Core translations
+        try {
+            /** @var Translations $translations */
+            $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
+            $translations->setDomain('shaarli');
+            $this->translator->loadTranslations($translations);
+        } catch (\InvalidArgumentException $e) {}
+
+
+        // Extension translations (plugins, themes, etc.).
+        foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
+            if ($domain === self::DEFAULT_DOMAIN) {
+                continue;
+            }
+
+            try {
+                /** @var Translations $extension */
+                $extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
+                $extension->setDomain($domain);
+                $this->translator->loadTranslations($extension);
+            } catch (\InvalidArgumentException $e) {}
+        }
+    }
+
+    /**
+     * Checks if a language string is valid.
+     *
+     * @param string $language e.g. 'fr' or 'en_US'
+     *
+     * @return bool true if valid, false otherwise
+     */
+    protected function isValidLanguage($language)
+    {
+        return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
+    }
+
+    /**
+     * Get the list of available languages for Shaarli.
+     *
+     * @return array List of available languages, with their label.
+     */
+    public static function getAvailableLanguages()
+    {
+        return [
+            'auto' => t('Automatic'),
+            'en' => t('English'),
+            'fr' => t('French'),
+        ];
     }
-    $actualForm = $nb > 1 ? $nText : $text;
-    return sprintf($actualForm, $nb);
 }
index 22c1f0ab5321b3ccb274931637ba642357ddecb6..c1661d52c972970e34f26b156f60b3012dd3cb97 100644 (file)
@@ -133,16 +133,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess
     {
         // TODO: use exceptions instead of "die"
         if (!$this->loggedIn) {
-            die('You are not authorized to add a link.');
+            die(t('You are not authorized to add a link.'));
         }
         if (!isset($value['id']) || empty($value['url'])) {
-            die('Internal Error: A link should always have an id and URL.');
+            die(t('Internal Error: A link should always have an id and URL.'));
         }
         if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
-            die('You must specify an integer as a key.');
+            die(t('You must specify an integer as a key.'));
         }
         if ($offset !== null && $offset !== $value['id']) {
-            die('Array offset and link ID must be equal.');
+            die(t('Array offset and link ID must be equal.'));
         }
 
         // If the link exists, we reuse the real offset, otherwise new entry
@@ -248,13 +248,13 @@ class LinkDB implements Iterator, Countable, ArrayAccess
         $this->links = array();
         $link = array(
             'id' => 1,
-            'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
+            'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
             'url'=>'https://shaarli.readthedocs.io',
-            'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
+            'description'=>t('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.
+To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
 
-You use the community supported version of the original Shaarli project, by Sebastien Sauvage.',
+You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'),
             'private'=>0,
             'created'=> new DateTime(),
             'tags'=>'opensource software'
@@ -264,9 +264,9 @@ You use the community supported version of the original Shaarli project, by Seba
 
         $link = array(
             'id' => 0,
-            'title'=>'My secret stuff... - Pastebin.com',
+            'title'=> t('My secret stuff... - Pastebin.com'),
             'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
-            'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
+            'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
             'private'=>1,
             'created'=> new DateTime('1 minute ago'),
             'tags'=>'secretstuff',
@@ -289,13 +289,15 @@ You use the community supported version of the original Shaarli project, by Seba
             return;
         }
 
+        $this->urls = [];
+        $this->ids = [];
         $this->links = FileUtils::readFlatDB($this->datastore, []);
 
         $toremove = array();
         foreach ($this->links as $key => &$link) {
             if (! $this->loggedIn && $link['private'] != 0) {
                 // Transition for not upgraded databases.
-                $toremove[] = $key;
+                unset($this->links[$key]);
                 continue;
             }
 
@@ -329,14 +331,10 @@ You use the community supported version of the original Shaarli project, by Seba
                 }
                 $link['shorturl'] = smallHash($link['linkdate']);
             }
-        }
 
-        // If user is not logged in, filter private links.
-        foreach ($toremove as $offset) {
-            unset($this->links[$offset]);
+            $this->urls[$link['url']] = $key;
+            $this->ids[$link['id']] = $key;
         }
-
-        $this->reorder();
     }
 
     /**
@@ -346,6 +344,7 @@ You use the community supported version of the original Shaarli project, by Seba
      */
     private function write()
     {
+        $this->reorder();
         FileUtils::writeFlatDB($this->datastore, $this->links);
     }
 
@@ -528,8 +527,8 @@ You use the community supported version of the original Shaarli project, by Seba
             return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
         });
 
-        $this->urls = array();
-        $this->ids = array();
+        $this->urls = [];
+        $this->ids = [];
         foreach ($this->links as $key => $link) {
             $this->urls[$link['url']] = $key;
             $this->ids[$link['id']] = $key;
index 99ecd1e238640bbba9fc32c60cdaeaabedba16d8..12376e27dc3aefe540960afbb0c194ec6ad55c16 100644 (file)
@@ -444,5 +444,11 @@ class LinkFilter
 
 class LinkNotFoundException extends Exception
 {
-    protected $message = 'The link you are trying to reach does not exist or has been deleted.';
+    /**
+     * LinkNotFoundException constructor.
+     */
+    public function __construct()
+    {
+        $this->message =  t('The link you are trying to reach does not exist or has been deleted.');
+    }
 }
index c0dd32a66cfa0160e4b37160fdf226d4abb14499..3705f7e919c4a1ea021830d65565639a6694df1f 100644 (file)
@@ -123,14 +123,15 @@ function count_private($links)
  *
  * @param string $text       input string.
  * @param string $redirector if a redirector is set, use it to gerenate links.
+ * @param bool   $urlEncode  Use `urlencode()` on the URL after the redirector or not.
  *
  * @return string returns $text with all links converted to HTML links.
  *
  * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
  */
-function text2clickable($text, $redirector = '')
+function text2clickable($text, $redirector = '', $urlEncode = true)
 {
-    $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);
@@ -138,8 +139,9 @@ function text2clickable($text, $redirector = '')
     // Redirector is set, urlencode the final URL.
     return preg_replace_callback(
         $regex,
-        function ($matches) use ($redirector) {
-            return '<a href="' . $redirector . urlencode($matches[1]) .'">'. $matches[1] .'</a>';
+        function ($matches) use ($redirector, $urlEncode) {
+            $url = $urlEncode ? urlencode($matches[1]) : $matches[1];
+            return '<a href="' . $redirector . $url .'">'. $matches[1] .'</a>';
         },
         $text
     );
@@ -185,12 +187,13 @@ function space2nbsp($text)
  *
  * @param string $description shaare's description.
  * @param string $redirector  if a redirector is set, use it to gerenate links.
+ * @param bool   $urlEncode  Use `urlencode()` on the URL after the redirector or not.
  * @param string $indexUrl    URL to Shaarli's index.
- *
+
  * @return string formatted description.
  */
-function format_description($description, $redirector = '', $indexUrl = '') {
-    return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl)));
+function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '') {
+    return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl)));
 }
 
 /**
index 2a10ff22149f8c89f30458d9e5c907e19847438a..dd7057f80e5bd25e1d5fce7bb984644b7c9e2689 100644 (file)
@@ -32,11 +32,10 @@ class NetscapeBookmarkUtils
     {
         // see tpl/export.html for possible values
         if (! in_array($selection, array('all', 'public', 'private'))) {
-            throw new Exception('Invalid export selection: "'.$selection.'"');
+            throw new Exception(t('Invalid export selection:') .' "'.$selection.'"');
         }
 
         $bookmarkLinks = array();
-
         foreach ($linkDb as $link) {
             if ($link['private'] != 0 && $selection == 'public') {
                 continue;
@@ -66,6 +65,7 @@ class NetscapeBookmarkUtils
      * @param int    $importCount    how many links were imported
      * @param int    $overwriteCount how many links were overwritten
      * @param int    $skipCount      how many links were skipped
+     * @param int    $duration       how many seconds did the import take
      *
      * @return string Summary of the bookmark import status
      */
@@ -74,16 +74,18 @@ class NetscapeBookmarkUtils
         $filesize,
         $importCount=0,
         $overwriteCount=0,
-        $skipCount=0
+        $skipCount=0,
+        $duration=0
     )
     {
-        $status = 'File '.$filename.' ('.$filesize.' bytes) ';
+        $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
         if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
-            $status .= 'has an unknown file format. Nothing was imported.';
+            $status .= t('has an unknown file format. Nothing was imported.');
         } else {
-            $status .= 'was successfully processed: '.$importCount.' links imported, ';
-            $status .= $overwriteCount.' links overwritten, ';
-            $status .= $skipCount.' links skipped.';
+            $status .= vsprintf(
+                t('was successfully processed in %d seconds: %d links imported, %d links overwritten, %d links skipped.'),
+                [$duration, $importCount, $overwriteCount, $skipCount]
+            );
         }
         return $status;
     }
@@ -101,6 +103,7 @@ class NetscapeBookmarkUtils
      */
     public static function import($post, $files, $linkDb, $conf, $history)
     {
+        $start = time();
         $filename = $files['filetoupload']['name'];
         $filesize = $files['filetoupload']['size'];
         $data = file_get_contents($files['filetoupload']['tmp_name']);
@@ -184,7 +187,6 @@ class NetscapeBookmarkUtils
                 $linkDb[$existingLink['id']] = $newLink;
                 $importCount++;
                 $overwriteCount++;
-                $history->updateLink($newLink);
                 continue;
             }
 
@@ -196,16 +198,19 @@ class NetscapeBookmarkUtils
             $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
             $linkDb[$newLink['id']] = $newLink;
             $importCount++;
-            $history->addLink($newLink);
         }
 
         $linkDb->save($conf->get('resource.page_cache'));
+        $history->importLinks();
+
+        $duration = time() - $start;
         return self::importStatus(
             $filename,
             $filesize,
             $importCount,
             $overwriteCount,
-            $skipCount
+            $skipCount,
+            $duration
         );
     }
 }
index 7a42400d88f6baa2444932d179985f5ad89cd14a..468f144b873871136a1aa6c573994d34acf842ef 100644 (file)
@@ -32,12 +32,14 @@ class PageBuilder
      *
      * @param ConfigManager $conf   Configuration Manager instance (reference).
      * @param LinkDB        $linkDB instance.
+     * @param string        $token  Session token
      */
-    public function __construct(&$conf, $linkDB = null)
+    public function __construct(&$conf, $linkDB = null, $token = null)
     {
         $this->tpl = false;
         $this->conf = $conf;
         $this->linkDB = $linkDB;
+        $this->token = $token;
     }
 
     /**
@@ -49,7 +51,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 +77,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']));
@@ -88,7 +94,8 @@ class PageBuilder
         $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true));
         $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));
+        $this->tpl->assign('token', $this->token);
+
         if ($this->linkDB !== null) {
             $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
         }
@@ -154,9 +161,12 @@ class PageBuilder
      *
      * @param string $message A messate to display what is not found
      */
-    public function render404($message = 'The page you are trying to reach does not exist or has been deleted.')
+    public function render404($message = '')
     {
-        header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
+        if (empty($message)) {
+            $message = t('The page you are trying to reach does not exist or has been deleted.');
+        }
+        header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found'));
         $this->tpl->assign('error_message', $message);
         $this->renderPage('404');
     }
index 59ece4fa9c735b8c2d150b939d48794fddeb2279..cf6038453e499e7e4030b68b1fffd13bfadbb79b 100644 (file)
@@ -188,6 +188,9 @@ class PluginManager
             $metaData[$plugin] = parse_ini_file($metaFile);
             $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
 
+            if (isset($metaData[$plugin]['description'])) {
+                $metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
+            }
             // Read parameters and format them into an array.
             if (isset($metaData[$plugin]['parameters'])) {
                 $params = explode(';', $metaData[$plugin]['parameters']);
@@ -203,7 +206,7 @@ class PluginManager
                 $metaData[$plugin]['parameters'][$param]['value'] = '';
                 // Optional parameter description in parameter.PARAM_NAME=
                 if (isset($metaData[$plugin]['parameter.'. $param])) {
-                    $metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param];
+                    $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]);
                 }
             }
         }
@@ -237,6 +240,6 @@ class PluginFileNotFoundException extends Exception
      */
     public function __construct($pluginName)
     {
-        $this->message = 'Plugin "'. $pluginName .'" files not found.';
+        $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
     }
 }
diff --git a/application/SessionManager.php b/application/SessionManager.php
new file mode 100644 (file)
index 0000000..71f0b38
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+namespace Shaarli;
+
+/**
+ * Manages the server-side session
+ */
+class SessionManager
+{
+    protected $session = [];
+
+    /**
+     * Constructor
+     *
+     * @param array         $session The $_SESSION array (reference)
+     * @param ConfigManager $conf    ConfigManager instance
+     */
+    public function __construct(& $session, $conf)
+    {
+        $this->session = &$session;
+        $this->conf = $conf;
+    }
+
+    /**
+     * Generates a session token
+     *
+     * @return string token
+     */
+    public function generateToken()
+    {
+        $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
+        $this->session['tokens'][$token] = 1;
+        return $token;
+    }
+
+    /**
+     * Checks the validity of a session token, and destroys it afterwards
+     *
+     * @param string $token The token to check
+     *
+     * @return bool true if the token is valid, else false
+     */
+    public function checkToken($token)
+    {
+        if (! isset($this->session['tokens'][$token])) {
+            // the token is wrong, or has already been used
+            return false;
+        }
+
+        // destroy the token to prevent future use
+        unset($this->session['tokens'][$token]);
+        return true;
+    }
+
+    /**
+     * Validate session ID to prevent Full Path Disclosure.
+     *
+     * See #298.
+     * The session ID's format depends on the hash algorithm set in PHP settings
+     *
+     * @param string $sessionId Session ID
+     *
+     * @return true if valid, false otherwise.
+     *
+     * @see http://php.net/manual/en/function.hash-algos.php
+     * @see http://php.net/manual/en/session.configuration.php
+     */
+    public static function checkId($sessionId)
+    {
+        if (empty($sessionId)) {
+            return false;
+        }
+
+        if (!$sessionId) {
+            return false;
+        }
+
+        if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
+            return false;
+        }
+
+        return true;
+    }
+}
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..8d2bd577d1a27c3f60588f9949f4ab5a311da433 100644 (file)
@@ -73,7 +73,7 @@ class Updater
         }
 
         if ($this->methods === null) {
-            throw new UpdaterException('Couldn\'t retrieve Updater class methods.');
+            throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
         }
 
         foreach ($this->methods as $method) {
@@ -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) {
@@ -436,6 +436,15 @@ class Updater
         }
         return true;
     }
+
+    /**
+     * Save the datastore -> the link order is now applied when links are saved.
+     */
+    public function updateMethodReorderDatastore()
+    {
+        $this->linkDB->save($this->conf->get('resource.page_cache'));
+        return true;
+    }
 }
 
 /**
@@ -482,7 +491,7 @@ class UpdaterException extends Exception
         }
 
         if (! empty($this->method)) {
-            $out .= 'An error occurred while running the update '. $this->method . PHP_EOL;
+            $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
         }
 
         if (! empty($this->previous)) {
@@ -522,11 +531,11 @@ function read_updates_file($updatesFilepath)
 function write_updates_file($updatesFilepath, $updates)
 {
     if (empty($updatesFilepath)) {
-        throw new Exception('Updates file path is not set, can\'t write updates.');
+        throw new Exception(t('Updates file path is not set, can\'t write updates.'));
     }
 
     $res = file_put_contents($updatesFilepath, implode(';', $updates));
     if ($res === false) {
-        throw new Exception('Unable to write updates in '. $updatesFilepath . '.');
+        throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
     }
 }
index 4a2f5561cfdf5dfb38ed9a0a4c0f46b58b27c8c6..97b12fcf5b5e1d8beb94ab507f3f32479b89e747 100644 (file)
@@ -181,36 +181,6 @@ function generateLocation($referer, $host, $loopTerms = array())
     return $finalReferer;
 }
 
-/**
- * Validate session ID to prevent Full Path Disclosure.
- *
- * See #298.
- * The session ID's format depends on the hash algorithm set in PHP settings
- *
- * @param string $sessionId Session ID
- *
- * @return true if valid, false otherwise.
- *
- * @see http://php.net/manual/en/function.hash-algos.php
- * @see http://php.net/manual/en/session.configuration.php
- */
-function is_session_id_valid($sessionId)
-{
-    if (empty($sessionId)) {
-        return false;
-    }
-
-    if (!$sessionId) {
-        return false;
-    }
-
-    if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
-        return false;
-    }
-
-    return true;
-}
-
 /**
  * Sniff browser language to set the locale automatically.
  * Note that is may not work on your server if the corresponding locale is not installed.
@@ -452,7 +422,7 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true)
  */
 function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
 {
-    $callback = function($a, $b) use ($reverse) {
+    $callback = function ($a, $b) use ($reverse) {
         // Collator is part of PHP intl.
         if (class_exists('Collator')) {
             $collator = new Collator(setlocale(LC_COLLATE, 0));
@@ -470,3 +440,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
         usort($data, $callback);
     }
 }
+
+/**
+ * Wrapper function for translation which match the API
+ * of gettext()/_() and ngettext().
+ *
+ * @param string $text   Text to translate.
+ * @param string $nText  The plural message ID.
+ * @param int    $nb     The number of items for plural forms.
+ * @param string $domain The domain where the translation is stored (default: shaarli).
+ *
+ * @return string Text translated.
+ */
+function t($text, $nText = '', $nb = 1, $domain = 'shaarli') {
+    return dn__($domain, $text, $nText, $nb);
+}
index 9ef2ef562634bb346cf398b290093cbd02e66926..8c8d5610efb145c49c3b319514f7e3ba0dab68ca 100644 (file)
@@ -22,10 +22,15 @@ class ConfigJson implements ConfigIO
         $data = json_decode($data, true);
         if ($data === null) {
             $errorCode = json_last_error();
-            $error  = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #';
-            $error .= $errorCode. '<br>âžœ <code>' . json_last_error_msg() .'</code>';
+            $error  = sprintf(
+                'An error occurred while parsing JSON configuration file (%s): error code #%d',
+                $filepath,
+                $errorCode
+            );
+            $error .= '<br>âžœ <code>' . json_last_error_msg() .'</code>';
             if ($errorCode === JSON_ERROR_SYNTAX) {
-                $error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
+                $error .= '<br>';
+                $error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
                 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
             }
             throw new \Exception($error);
@@ -44,8 +49,8 @@ class ConfigJson implements ConfigIO
         if (!file_put_contents($filepath, $data)) {
             throw new \IOException(
                 $filepath,
-                'Shaarli could not create the config file.
-                Please make sure Shaarli has the right to write in the folder is it installed in.'
+                t('Shaarli could not create the config file. '.
+                  'Please make sure Shaarli has the right to write in the folder is it installed in.')
             );
         }
     }
index 32f6ef6db5418044a12fd92959e19a2d7634a029..9e4c9f6328d113d4de7dc1af370e6f8a26058a7f 100644 (file)
@@ -132,7 +132,7 @@ class ConfigManager
     public function set($setting, $value, $write = false, $isLoggedIn = false)
     {
         if (empty($setting) || ! is_string($setting)) {
-            throw new \Exception('Invalid setting key parameter. String expected, got: '. gettype($setting));
+            throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
         }
 
         // During the ConfigIO transition, map legacy settings to the new ones.
@@ -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');
@@ -338,6 +339,10 @@ class ConfigManager
         $this->setEmpty('redirector.url', '');
         $this->setEmpty('redirector.encode_url', true);
 
+        $this->setEmpty('translation.language', 'auto');
+        $this->setEmpty('translation.mode', 'php');
+        $this->setEmpty('translation.extensions', []);
+
         $this->setEmpty('plugins', array());
     }
 
index 2633824d3197f209d3048f74949d95b96d128148..2f66e8e00ac18a537db96d77e2bf66e175c87c29 100644 (file)
@@ -118,8 +118,8 @@ class ConfigPhp implements ConfigIO
         ) {
             throw new \IOException(
                 $filepath,
-                'Shaarli could not create the config file.
-                Please make sure Shaarli has the right to write in the folder is it installed in.'
+                t('Shaarli could not create the config file. '.
+                  'Please make sure Shaarli has the right to write in the folder is it installed in.')
             );
         }
     }
index 6346c6a9ebbc60e6826753855c32cf5d8e680e02..9e0a93594d21f61ee76a8d85e2ea231f848ac83e 100644 (file)
@@ -18,6 +18,6 @@ class MissingFieldConfigException extends \Exception
     public function __construct($field)
     {
         $this->field = $field;
-        $this->message = 'Configuration value is required for '. $this->field;
+        $this->message = sprintf(t('Configuration value is required for %s'), $this->field);
     }
 }
index f9d68750030a9226f53a9b74058fa358fee2252d..f82ec26ed0d1a3bed7a19d9c5b9493e155f7362d 100644 (file)
@@ -12,6 +12,6 @@ class PluginConfigOrderException extends \Exception
      */
     public function __construct()
     {
-        $this->message = 'An error occurred while trying to save plugins loading order.';
+        $this->message = t('An error occurred while trying to save plugins loading order.');
     }
 }
index 79672c1bf95a3aa8019f09b853fc4d05162128e1..72311faeffc98ee0a76e7fc3bc884e6a69f8ca12 100644 (file)
@@ -13,6 +13,6 @@ class UnauthorizedConfigException extends \Exception
      */
     public function __construct()
     {
-        $this->message = 'You are not authorized to alter config.';
+        $this->message = t('You are not authorized to alter config.');
     }
 }
index b563b23d53671e37da9cce3fc6a25d6a428a1598..18e46b77a2291999eb9b3ee8656de4c4cb17efe6 100644 (file)
@@ -16,7 +16,7 @@ class IOException extends Exception
     public function __construct($path, $message = '')
     {
         $this->path = $path;
-        $this->message = empty($message) ? 'Error accessing' : $message;
+        $this->message = empty($message) ? t('Error accessing') : $message;
         $this->message .= ' "' . $this->path .'"';
     }
 }
index afb8aca4c42209a7f8ddc685a1c7626e416a313c..f331d6caf265174a86c537a303b2b38f95fe2d6b 100644 (file)
@@ -19,7 +19,8 @@
         "shaarli/netscape-bookmark-parser": "^2.0",
         "erusev/parsedown": "1.6",
         "slim/slim": "^3.0",
-        "pubsubhubbub/publisher": "dev-master"
+        "pubsubhubbub/publisher": "dev-master",
+        "gettext/gettext": "^4.4"
     },
     "require-dev": {
         "phpmd/phpmd" : "@stable",
index 435d6a8822f3b8fb2ecb799e4f96de7f255007d5..39909b8fd2347813c39b2db6b60f9e9a1aabedaa 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "68beedbfa104c788029b079800cfd6e8",
+    "content-hash": "13b7e1e474fe9264b098ba86face0feb",
     "packages": [
         {
             "name": "container-interop/container-interop",
             ],
             "time": "2015-10-04T16:44:32+00:00"
         },
+        {
+            "name": "gettext/gettext",
+            "version": "v4.4.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/oscarotero/Gettext.git",
+                "reference": "4f57f004635cc6311a20815ebfdc0757cb337113"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/4f57f004635cc6311a20815ebfdc0757cb337113",
+                "reference": "4f57f004635cc6311a20815ebfdc0757cb337113",
+                "shasum": ""
+            },
+            "require": {
+                "gettext/languages": "^2.3",
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "illuminate/view": "*",
+                "phpunit/phpunit": "^4.8|^5.7",
+                "squizlabs/php_codesniffer": "^3.0",
+                "symfony/yaml": "~2",
+                "twig/extensions": "*",
+                "twig/twig": "^1.31|^2.0"
+            },
+            "suggest": {
+                "illuminate/view": "Is necessary if you want to use the Blade extractor",
+                "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator",
+                "twig/extensions": "Is necessary if you want to use the Twig extractor",
+                "twig/twig": "Is necessary if you want to use the Twig extractor"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Gettext\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Oscar Otero",
+                    "email": "oom@oscarotero.com",
+                    "homepage": "http://oscarotero.com",
+                    "role": "Developer"
+                }
+            ],
+            "description": "PHP gettext manager",
+            "homepage": "https://github.com/oscarotero/Gettext",
+            "keywords": [
+                "JS",
+                "gettext",
+                "i18n",
+                "mo",
+                "po",
+                "translation"
+            ],
+            "time": "2017-08-09T16:59:46+00:00"
+        },
+        {
+            "name": "gettext/languages",
+            "version": "2.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
+                "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7",
+                "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4"
+            },
+            "bin": [
+                "bin/export-plural-rules",
+                "bin/export-plural-rules.php"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Gettext\\Languages\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michele Locati",
+                    "email": "mlocati@gmail.com",
+                    "role": "Developer"
+                }
+            ],
+            "description": "gettext languages with plural rules",
+            "homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules",
+            "keywords": [
+                "cldr",
+                "i18n",
+                "internationalization",
+                "l10n",
+                "language",
+                "languages",
+                "localization",
+                "php",
+                "plural",
+                "plural rules",
+                "plurals",
+                "translate",
+                "translations",
+                "unicode"
+            ],
+            "time": "2017-03-23T17:02:28+00:00"
+        },
         {
             "name": "katzgrau/klogger",
             "version": "1.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/pubsubhubbub/php-publisher.git",
-                "reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7"
+                "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/a5d6a0e1cc9d49101c3904480e5b06cbb8addba7",
-                "reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7",
+                "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f",
+                "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f",
                 "shasum": ""
             },
             "require": {
                 "publishers",
                 "pubsubhubbub"
             ],
-            "time": "2016-11-15T06:24:01+00:00"
+            "time": "2017-10-08T10:59:41+00:00"
         },
         {
             "name": "shaarli/netscape-bookmark-parser",
         },
         {
             "name": "phpdocumentor/reflection-common",
-            "version": "1.0",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
-                "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c"
+                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
-                "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
                 "shasum": ""
             },
             "require": {
                 "reflection",
                 "static analysis"
             ],
-            "time": "2015-12-27T11:43:31+00:00"
+            "time": "2017-09-11T18:02:19+00:00"
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "3.2.1",
+            "version": "3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "183824db76118b9dddffc7e522b91fa175f75119"
+                "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119",
-                "reference": "183824db76118b9dddffc7e522b91fa175f75119",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157",
+                "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2017-08-04T20:55:59+00:00"
+            "time": "2017-08-08T06:39:58+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "v1.7.0",
+            "version": "v1.7.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "93d39f1f7f9326d746203c7c056f300f7f126073"
+                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073",
-                "reference": "93d39f1f7f9326d746203c7c056f300f7f126073",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
+                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.0.2",
                 "php": "^5.3|^7.0",
-                "phpdocumentor/reflection-docblock": "^2.0|^3.0.2",
+                "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
                 "sebastian/comparator": "^1.1|^2.0",
                 "sebastian/recursion-context": "^1.0|^2.0|^3.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.6.x-dev"
+                    "dev-master": "1.7.x-dev"
                 }
             },
             "autoload": {
                 "spy",
                 "stub"
             ],
-            "time": "2017-03-02T20:05:34+00:00"
+            "time": "2017-09-04T11:05:03+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
         },
         {
             "name": "symfony/config",
-            "version": "v3.3.6",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297"
+                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297",
-                "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297",
+                "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
+                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^5.5.9|>=7.0.8",
                 "symfony/filesystem": "~2.8|~3.0"
             },
             "conflict": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-19T07:37:29+00:00"
+            "time": "2017-10-04T18:56:58+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v2.8.26",
+            "version": "v2.8.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd"
+                "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/32a3c6b3398de5db8ed381f4ef92970c59c2fcdd",
-                "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd",
+                "url": "https://api.github.com/repos/symfony/console/zipball/f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
+                "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:26:04+00:00"
+            "time": "2017-10-01T21:00:16+00:00"
         },
         {
             "name": "symfony/debug",
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.6",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0"
+                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8d70987f991481e809c63681ffe8ce3f3fde68a0",
-                "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
+                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^5.5.9|>=7.0.8",
                 "psr/container": "^1.0"
             },
             "conflict": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-28T15:27:31+00:00"
+            "time": "2017-10-04T17:15:30+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.3.6",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "427987eb4eed764c3b6e38d52a0f87989e010676"
+                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676",
-                "reference": "427987eb4eed764c3b6e38d52a0f87989e010676",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
+                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^5.5.9|>=7.0.8"
             },
             "type": "library",
             "extra": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-11T07:17:58+00:00"
+            "time": "2017-10-03T13:33:10+00:00"
         },
         {
             "name": "symfony/finder",
-            "version": "v3.3.6",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
+                "reference": "773e19a491d97926f236942484cb541560ce862d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
-                "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/773e19a491d97926f236942484cb541560ce862d",
+                "reference": "773e19a491d97926f236942484cb541560ce862d",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^5.5.9|>=7.0.8"
             },
             "type": "library",
             "extra": {
             ],
             "description": "Symfony Finder Component",
             "homepage": "https://symfony.com",
-            "time": "2017-06-01T21:01:25+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.4.0",
+            "version": "v1.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "f29dca382a6485c3cbe6379f0c61230167681937"
+                "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937",
-                "reference": "f29dca382a6485c3cbe6379f0c61230167681937",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
+                "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.4-dev"
+                    "dev-master": "1.6-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2017-06-09T14:24:12+00:00"
+            "time": "2017-10-11T12:05:26+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.6",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed"
+                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
-                "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
+                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^5.5.9|>=7.0.8"
             },
             "require-dev": {
                 "symfony/console": "~2.8|~3.0"
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-23T12:43:26+00:00"
+            "time": "2017-10-05T14:43:42+00:00"
         },
         {
             "name": "theseer/fdomdocument",
index f601c1eeee5ff6e42f4eccdd43f76169fdf0ba0a..1d49da37a072dea22d567d4326563a7a07bf4534 100644 (file)
@@ -1,10 +1,16 @@
 <IfModule version_module>
   <IfVersion >= 2.4>
-     Require all denied
+    Require all denied
+    <Files "user.css">
+      Require all granted
+    </Files>
   </IfVersion>
   <IfVersion < 2.4>
-     Allow from none
-     Deny from all
+    Allow from none
+    Deny from all
+    <Files "user.css">
+      Allow from all
+    </Files>
   </IfVersion>
 </IfModule>
 
index 897248576b678c95f45e4021e4c250f6697897a9..bb790074c9973dfe45a9c8b9e0a1d3c29aa628fa 100644 (file)
@@ -45,6 +45,10 @@ Shaarli cannot import data directly from [Scuttle](https://github.com/scronide/s
 However, you can use the third-party [scuttle-to-shaarli](https://github.com/q2apro/scuttle-to-shaarli)
 tool to export the Scuttle database to the Netscape HTML format compatible with the Shaarli importer.
 
+### Refind
+
+You can use the third-party tool [Derefind](https://github.com/ShawnPConroy/Derefind) to convert refind.com bookmark exports to a format that can be imported into Shaarli.
+
 ## Import Shaarli links to Firefox
 
 - Export your Shaarli links as described above.
index e53e3261980fa7b86d082d22b255e1ea295291f6..c899e3cf6a8fc41c340a119d2b361774b1b8531a 100644 (file)
@@ -21,7 +21,7 @@ _This bookmarklet button is compatible with Firefox, Opera, Chrome and Safari. U
 
 Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunatly, there is nothing Shaarli can do about it.
 
-See [#196](https://github.com/shaarli/Shaarli#196).
+See [#196](https://github.com/shaarli/Shaarli/issues/196).
 
 There is an open bug for both Firefox and Chromium:
 
index 35707482a5399677a254335dcf371897af8b38bd..16c69855cd9e4c92d876947f881e1dc02097f3f7 100644 (file)
@@ -14,10 +14,24 @@ Use the `Filter by tags` field to restrict displayed links to entries tagged wit
 
 **Hidden tags:** Tags starting with a dot `.` (example `.secret`) are private. They can only be seen and searched when logged in.
 
-Alternatively you can use the `Tag cloud` to discover all tags and click on any of them to display related links.
+### Tag cloud
 
-To search for links that are not tagged, enter `""` in the tag search field.
+The `Tag cloud` page diplays a "cloud" view of all tags in your Shaarli.
+
+ * The most frequently used tags are displayed with a bigger font size.
+ * When sorting by `Most used` or `Alphabetical`, tags are displayed as a _list_, along with counters and edit/delete buttons for each tag.
+ * Clicking on any tag will display a list of all Shaares matching this tag.
+ * Clicking on the counter next to a tag `example`, will filter the tag cloud to only display tags found in Shaares tagged `example`. Repeat this any number of times to further filter the tag cloud. Click `List all links with those tags` to display Shaares matching your current tag filter.
 
 ## Filtering RSS feeds/Picture wall
 
-RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS feeds).
+RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS-feeds).
+
+## Filter buttons
+
+Filter buttons can be found at the top left of the link list. They allow you to apply different filters to the list:
+
+ * **Private links:** When this toggle button is enabled, only shaares set to `private` will be shown.
+ * **Untagged links:** When the this toggle button is enabled (top left of the link list), only shaares _without any tags_ will be shown in the link list.
+Filter buttons are only available when logged in.
index 8edbeefa0c18466002b6ebdc46dccee98d650779..207153b61eb075621a100f8ffc3b5e44b73008ee 100644 (file)
@@ -1,23 +1,7 @@
 _Unofficial but related work on Shaarli. If you maintain one of these,
 please get in touch with us to help us find a way to adapt your work to our fork._
 
-## Community
-- [Liens en vrac de sebsauvage](http://sebsauvage.net/links/) - the original Shaarli
-- [A large list of Shaarlis](http://porneia.free.fr/pub/links/ou-est-shaarli.html)
-- [A list of working Shaarli aggregators](https://raw.githubusercontent.com/Oros42/find_shaarlis/master/annuaires.json)
-- [A list of some known Shaarlis](https://github.com/Oros42/shaarlis_list)
-- [Adieu Delicious, Diigo et StumbleUpon. Salut Shaarli ! - sebsauvage.net](http://sebsauvage.net/rhaa/index.php?2011/09/16/09/29/58-adieu-delicious-diigo-et-stumbleupon-salut-shaarli-) (fr) _16/09/2011 - the original post about Shaarli_
-- [Original ideas/fixme/TODO page](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:ideas)
-- [Original discussion page](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:discussion) (fr)
-- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
-- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni)
-
-
-### Articles and social media discussions
-- 2016-09-22 - Hacker News - https://news.ycombinator.com/item?id=12552176
-- 2015-08-15 - Reddit - [Question about migrating from WordPress to Shaarli.](https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/)
-- 2015-06-22 - Hacker News - https://news.ycombinator.com/item?id=9755366
-- 2015-05-12 - Reddit - [shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)](https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/)
+## Related software
 
 
 ### REST API clients
@@ -29,28 +13,34 @@ See [REST API](REST-API) for a list of official and community clients.
 - [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter.
 - [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.
 - [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli.
+- [twemoji](https://github.com/NerosTie/twemoji) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli (Twemoji version) 
 - [google analytics](https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin) by [@ericjuden](http://github.com/ericjuden): Adds Google Analytics tracking support
 - [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.
+- [markdown-toolbar](https://github.com/immanuelfodor/shaarli-markdown-toolbar) by [@immanuelfodor](https://github.com/immanuelfodor) - Easily insert markdown syntax into the Description field when editing a link.
 - [related](https://github.com/ilesinge/shaarli-related) by [@ilesinge](https://github.com/ilesinge) - Show related links based on the number of identical tags.
 - [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.
 - [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli
+- [shaarli2mastodon](https://github.com/kalvn/shaarli2mastodon) by [@kalvn](https://github.com/kalvn) - This Shaarli plugin allows you to automatically publish links you post on your Mastodon timeline.
+- [shaarli-descriptor](https://github.com/immanuelfodor/shaarli-descriptor) by [@immanuelfodor](https://github.com/immanuelfodor) - Customize the default height/number of rows of the Description field when editing a link.
 
 
 ### Third-party themes
 See [Theming](Theming) for a list of community-contributed themes, and an installation guide.
 
 
-## Integration with other platforms 
+### Integration with other platforms 
 - [tt-rss-shaarli](https://github.com/jcsaaddupuy/tt-rss-shaarli) - [Tiny-Tiny RSS](http://tt-rss.org/) plugin that adds support for sharing articles with Shaarli
 - [octopress-shaarli](https://github.com/ahmet2mir/octopress-shaarli) - Octopress plugin to retrieve Shaarli links on the sidebar
 - [Scuttle to Shaarli](https://github.com/q2apro/scuttle-to-shaarli) - Import bookmarks from Scuttle
 
 
 ### Mobile Apps
-- [ShaarliOS](https://github.com/mro/ShaarliOS) iOS share extension - see [#308](https://github.com/shaarli/Shaarli/issues/308#issuecomment-184592070) for some promo codes,
+- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension.
 - [Shaarli for Android](http://sebsauvage.net/links/?ZAyDzg) - Android application that adds Shaarli as a sharing provider
 - [Shaarlier for Android](https://github.com/dimtion/Shaarlier) - Android application to simply add links directly into your Shaarli
 
+### Browser addons
+ * [Shaarli Web Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli.
 
 ### Server apps
 - [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content
@@ -61,7 +51,22 @@ See [Theming](Theming) for a list of community-contributed themes, and an instal
 - [Self dead link](https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. [Another version](https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for other shaarli instances (but is more resource consuming).
 - [Bookmark Archiver](https://github.com/pirate/bookmark-archiver) - Save an archived copy of all websites starred using browser bookmarks/Shaarli/Delicious/Instapaper/Unmark.it/Pocket/Pinboard. Outputs browseable html. 
 
-
 ## Alternatives to Shaarli
-See the [bookmarks & link sharing](https://github.com/Kickball/awesome-selfhosted/#bookmarks--link-sharing)
-section on [awesome-selfhosted](https://github.com/Kickball/awesome-selfhosted/).
+See [awesome-selfhosted: bookmarks & link sharing](https://github.com/Kickball/awesome-selfhosted/#bookmarks--link-sharing).
+
+## Community
+- [Liens en vrac de sebsauvage](http://sebsauvage.net/links/) - the original Shaarli
+- [A large list of Shaarlis](http://porneia.free.fr/pub/links/ou-est-shaarli.html)
+- [A list of working Shaarli aggregators](https://raw.githubusercontent.com/Oros42/find_shaarlis/master/annuaires.json)
+- [A list of some known Shaarlis](https://github.com/Oros42/shaarlis_list)
+- [Adieu Delicious, Diigo et StumbleUpon. Salut Shaarli ! - sebsauvage.net](http://sebsauvage.net/rhaa/index.php?2011/09/16/09/29/58-adieu-delicious-diigo-et-stumbleupon-salut-shaarli-) (fr) _16/09/2011 - the original post about Shaarli_
+- [Original ideas/fixme/TODO page](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:ideas)
+- [Original discussion page](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:discussion) (fr)
+- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
+- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni)
+
+### Articles and social media discussions
+- 2016-09-22 - Hacker News - https://news.ycombinator.com/item?id=12552176
+- 2015-08-15 - Reddit - [Question about migrating from WordPress to Shaarli.](https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/)
+- 2015-06-22 - Hacker News - https://news.ycombinator.com/item?id=9755366
+- 2015-05-12 - Reddit - [shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)](https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/)
index e5e929ef36c7d04f2987de12247d8f2b632777cc..0fdbd27de59ab5b184b3c9812cb1479d6ab40e76 100644 (file)
@@ -4,44 +4,57 @@ Document Root (or directly at the document root).
 Also, please make sure your server meets the [requirements](Server-requirements)
 and is properly [configured](Server-configuration).
 
-Several releases are available:
+Multiple releases branches are available:
+
+- latest (last release)
+- stable (previous major release)
+- master (development)
+
+Using one of the following methods:
 
 - by downloading full release archives including all dependencies
 - by downloading Github archives
 - by cloning the Git repository
+- using Docker: [see the documentation](docker/shaarli-images.md)
 
----
+--------------------------------------------------------------------------------
 
 ## Latest release (recommended)
-### Download as an archive
-Get the latest released version from the [releases](https://github.com/shaarli/Shaarli/releases) page.
 
-**Download our *shaarli-full* archive** to include dependencies.
+### Download as an archive
 
-The current latest released version is `v0.9.1`
+In most cases, you should download the latest Shaarli release from the [releases](https://github.com/shaarli/Shaarli/releases) page. **Download our *shaarli-full* archive** to include dependencies.
 
-Or in command lines:
+The current latest released version is `v0.9.3`
 
 ```bash
-$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.1/shaarli-v0.9.1-full.zip
-$ unzip shaarli-v0.9.1-full.zip
+$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.3/shaarli-v0.9.3-full.zip
+$ unzip shaarli-v0.9.3-full.zip
 $ mv Shaarli /path/to/shaarli/
 ```
 
-In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).|
-
 ### Using git
 
+Cloning using `git` or downloading Github branches as zip files requires additional steps:
+
+ * Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
+ * Install [python3-virtualenv](https://pypi.python.org/pypi/virtualenv) to build the local HTML documentation.
+
 ```
 $ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
-$ git clone -b v0.9 https://github.com/shaarli/Shaarli.git .
+$ git clone -b latest https://github.com/shaarli/Shaarli.git .
 $ composer install --no-dev --prefer-dist
+$ make translate
+$ make htmldoc
 ```
 
+--------------------------------------------------------------------------------
+
 ## Stable version
 
 The stable version has been experienced by Shaarli users, and will receive security updates.
 
+
 ### Download as an archive
 
 As a .zip archive:
@@ -60,9 +73,9 @@ $ tar xvf stable.tar.gz
 $ mv Shaarli-stable /path/to/shaarli/
 ```
 
-### Clone with Git 
+### Using git
 
-[Composer](https://getcomposer.org/) is required to build a functional Shaarli installation when pulling from git.
+Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
 
 ```bash
 $ git clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/
@@ -71,25 +84,34 @@ $ cd /path/to/shaarli/
 $ composer install --no-dev --prefer-dist
 ```
 
+
+--------------------------------------------------------------------------------
+
 ## Development version (mainline)
 
 _Use at your own risk!_
 
+Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
+
 To get the latest changes from the `master` branch:
 
 ```bash
-# clone the repository  
+# clone the repository
 $ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
 # install/update third-party dependencies
 $ cd /path/to/shaarli
 $ composer install --no-dev --prefer-dist
+$ make translate
+$ make htmldoc
 ```
 
+-------------------------------------------------------------------------------
+
 ## Finish Installation
 
 Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.
 
-![install screenshot](http://i.imgur.com/wuMpDSN.png)
+![install screenshot](images/install-shaarli.png)
 
 Setup your Shaarli installation, and it's ready to use!
 
diff --git a/doc/md/Features.md b/doc/md/Features.md
deleted file mode 100644 (file)
index eef88d0..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-### Main features
-Shaarli is intended:
-
-- to share, comment and save interesting links and news
-- to bookmark useful/frequent personal links (as private links) and share them between computers
-- as a minimal blog/microblog/writing platform (no character limit)
-- as a read-it-later list (for example items tagged `readlater`)
-- to draft and save articles/ideas
-- to keep code snippets
-- to keep notes and documentation
-- as a shared clipboard between machines
-- as a todo list
-- to store playlists (e.g. with the `music` or `video` tags)
-- to keep extracts/comments from webpages that may disappear
-- to keep track of ongoing discussions (for example items tagged `discussion`)
-- [to feed RSS aggregators](http://shaarli.chassegnouf.net/?9Efeiw) (planets) with specific tags
-- to feed other social networks, blogs... using RSS feeds and external services (dlvr.it, ifttt.com ...)
-
-### Using Shaarli as a blog, notepad, pastebin...
-
-- Go to your Shaarli setup and log in
-- Click the `Add Link` button
-- To share text only, do not enter any URL in the corresponding input field and click `Add Link`
-- Pick a title and enter your article, or note, in the description field; add a few tags; optionally check `Private` then click `Save`
-- Voilà!  Your article is now published (privately if you selected that option) and accessible using its permalink.
index 878884a497e9bc67879576db27ff0216cd2a24ba..9a46b1854c401dc736fba9495ea12b214a79c08d 100644 (file)
@@ -1,3 +1,6 @@
+| Note | Firefox Share is no longer available for Firefox 57 and later versions. |
+|---------|---------|
+
 ### Add Shaarli as a sharing service to Firefox
 
 - Open your Shaarli and `Login`
index 707af7622b428589d716dbae0e8f79fd92538bba..2dc442df27240eae55974dd99befb11746666359 100644 (file)
@@ -35,7 +35,8 @@ Library | Required? | Usage
 Extension | Required? | Usage
 ---|:---:|---
 [`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS
-[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows | multibyte (Unicode) string support
+[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
 [`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing
 [`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
 [`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
+[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
index 374864147065318ea2b703253bea39e2daa29458..920c7e27e151a3cf1a870a46ee0b2e8c1ced9505 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
 
@@ -80,6 +81,20 @@ _These settings should not be edited_
 - **page_cache**: Shaarli's internal cache directory.  
 - **ban_file**: Banned IP file path.
 
+### Translation
+
+- **language**: translation language (also see [Translations](Translations))
+    - **auto** (default): The translation language is chosen from the browser locale. 
+    It means that the language can be different for 2 different visitors depending on their locale.  
+    - **en**: Use the English translation.
+    - **fr**: Use the French translation.
+- **mode**: 
+    - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
+    - **gettext**: Use PHP builtin gettext extension 
+    (faster, but requires `php-gettext` to be installed and to reload the web server on update)
+- **extension**: Translation extensions for custom themes or plugins. 
+Must be an associative array: `translation domain => translation path`.
+
 ### Updates
 
 - **check_updates**: Enable or disable update check to the git repository.  
@@ -210,6 +225,13 @@ _These settings should not be edited_
     "plugins": {
         "WALLABAG_URL": "http://demo.wallabag.org",
         "WALLABAG_VERSION": "1"
+    },
+    "translation": {
+        "language": "fr",
+        "mode": "php",
+        "extensions": {
+            "demo": "plugins/demo_plugin/languages/"
+        }
     }
 } ?>
 ```
diff --git a/doc/md/Translations.md b/doc/md/Translations.md
new file mode 100644 (file)
index 0000000..54a3665
--- /dev/null
@@ -0,0 +1,152 @@
+## Translations
+
+Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations
+since `>= v0.9.2`.
+
+Note that only the `default` theme supports translations.
+
+### Contributing
+
+We encourage the community to contribute to Shaarli's translation either by improving existing 
+translations or submitting a new language. 
+
+Contributing to the translation does not require development skill.
+
+Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`)
+is not stored on the repository, and is generated during the release process.
+
+### How to
+
+First, install [Poedit](https://poedit.net/) tool.
+
+Poedit will extract strings to translate from the PHP source code.
+
+**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract 
+every translatable string. 
+
+You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07)  (recommended)
+or visit every template page in your browser to generate cache files, while logged in.
+
+Here is a list :
+
+```
+http://<replace_domain>/
+http://<replace_domain>/?nonope
+http://<replace_domain>/?do=addlink
+http://<replace_domain>/?do=changepasswd
+http://<replace_domain>/?do=changetag
+http://<replace_domain>/?do=configure
+http://<replace_domain>/?do=tools
+http://<replace_domain>/?do=daily
+http://<replace_domain>/?post
+http://<replace_domain>/?do=export
+http://<replace_domain>/?do=import
+http://<replace_domain>/?do=login
+http://<replace_domain>/?do=picwall
+http://<replace_domain>/?do=pluginadmin
+http://<replace_domain>/?do=tagcloud
+http://<replace_domain>/?do=taglist
+```
+
+#### Improve existing translation
+
+In Poedit, click on "Edit a Translation", and from Shaarli's directory open 
+`inc/languages/<lang>/LC_MESSAGES/shaarli.po`. 
+
+The existing list of translatable strings should have been loaded, then click on the "Update" button.
+
+You can start editing the translation.
+
+![poedit-screenshot](images/poedit-1.jpg)
+
+Save when you're done, then you can submit a pull request containing the updated `shaarli.po`.
+
+#### Add a new language
+
+Open Poedit and select "Create New Translation", then from Shaarli's directory open 
+`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
+
+Then select the language you want to create. 
+
+Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.  
+`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2) 
+format in lowercase (e.g. `de` for German).
+
+Then click on the "Update" button, and you can start to translate every available string.
+
+Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
+
+### Extend Shaarli's translation
+
+If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
+but you won't be able to able to override Shaarli's translation.
+
+However, you can add your own translation domain which extends the main translation list.
+
+> Note that you can find a live example of translation extension in the `demo_plugin`.
+
+First, create your translation files tree directory:
+
+```
+<your_module>/languages/<ISO 3166-1 alpha-2 language code>/LC_MESSAGES/
+```
+
+Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
+`my_theme.po`.
+
+Users have to register your extension in their configuration with the parameter 
+`translation.extensions.<domain>: <translation files path>`.
+
+Example:
+
+```php
+if (! $conf->exists('translation.extensions.my_theme')) {
+    $conf->set('translation.extensions.my_theme', '<your_module>/languages/');
+    $conf->write(true);
+}
+```
+
+> Note that the page needs to be reloaded after the registration.
+
+It is then recommended to create a custom translation function which will call the `t()` function with your domain.
+For example :
+
+```php
+function my_theme_t($text, $nText = '', $nb = 1)
+{
+    return t($text, $nText, $nb, 'my_theme'); // the last parameter is your translation domain.
+}
+```
+
+All strings which can be translated should be processed through your function:
+
+```php
+my_theme_t('Comment');
+my_theme_t('Comment', 'Comments', 2);
+```
+
+Or in templates:
+
+```php
+{'Comment'|my_theme_t}
+{function="my_theme_t('Comment', 'Comments', 2)"}
+```
+
+> Note than in template, you need to visit your page at least once to generate a cache file.
+
+When you're done, open Poedit and load translation strings from sources:
+
+  1. `File > New`
+  2. Choose your language
+  3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
+  4. Go to `Catalog > Properties...` 
+  5. Fill the `Translation Properties` tab
+  6. Add your source path in the `Sources Paths` tab
+  7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
+  
+```
+my_theme_t
+my_theme_t:1,2
+```
+
+Click on the "Update" button and you're free to start your translations!
index d200634f6ea6513ff5094abc7971e0752cd2b543..f6030d5c2f6134848b58ed8987f1458c8dd4df23 100644 (file)
@@ -2,12 +2,12 @@
 
 The framework used is [PHPUnit](https://phpunit.de/); it can be installed with [Composer](https://getcomposer.org/), which is a dependency management tool.
 
-Regarding Composer, you can either use:
+### Install composer
 
-- a system-wide version, e.g. installed through your distro's package manager
-- a local version, downloadable [here](https://getcomposer.org/download/)
+You can either use:
 
-#### Sample usage
+- a system-wide version, e.g. installed through your distro's package manager
+- a local version, downloadable [here](https://getcomposer.org/download/).
 
 ```bash
 # system-wide version
@@ -29,6 +29,8 @@ $ composer update
 
 #### Install and enable Xdebug to generate PHPUnit coverage reports
 
+See http://xdebug.org/docs/install
+
 For Debian-based distros:
 ```bash
 $ aptitude install php5-xdebug
index b3a087644e9b7be11ca1624c5cfaba74e00534a3..1dc073398fa190c8c3aaa71deded5e0ace7c5246 100644 (file)
@@ -14,7 +14,7 @@ Shaarli stores all user data under the `data` directory:
 - `data/ipbans.php` - banned IP addresses
 - `data/updates.txt` - contains all automatic update to the configuration and datastore files already run
 
-See [Shaarli configuration](Shaarli configuration) for more information about Shaarli resources.
+See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources.
 
 It is recommended to backup this repository _before_ starting updating/upgrading Shaarli:
 
@@ -27,7 +27,7 @@ As all user data is kept under `data`, this is the only directory you need to wo
 
 - backup the `data` directory
 - install or update Shaarli:
-    - fresh installation - see [Download and installation](Download and installation)
+    - fresh installation - see [Download and installation](Download-and-installation)
     - update - see the following sections
 - check or restore the `data` directory
 
@@ -35,10 +35,13 @@ As all user data is kept under `data`, this is the only directory you need to wo
 
 All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.
 
-We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download and installation) for `git` complete instructions.
+We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions.
 
 Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
 
+If you use translations in gettext mode - meaning you manually changed the default mode -, 
+reload your web server.
+
 After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details).
 
 ## Upgrading with Git
@@ -72,6 +75,14 @@ Updating dependencies
     Downloading: 100%
 ```
 
+Shaarli >= `v0.9.2` supports translations:
+
+```bash
+$ make translate
+```
+
+If you use translations in gettext mode, reload your web server.
+
 ### Migrating and upgrading from Sebsauvage's repository
 
 If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy.
@@ -151,6 +162,14 @@ Updating dependencies
     Downloading: 100%
 ```
 
+Shaarli >= `v0.9.2` supports translations:
+
+```bash
+$ make translate
+```
+
+If you use translations in gettext mode, reload your web server.
+
 Optionally, you can delete information related to the legacy version:
 
 ```bash
@@ -173,7 +192,7 @@ Total 3317 (delta 2050), reused 3301 (delta 2034)to
 
 #### Step 3: configuration
 
-After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli configuration) for more details).
+After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration) for more details).
 
 ## Troubleshooting
 
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 91ffecff9c0a090afa4eaa358f7face88b26d9ca..6066140eb77be13adba84d5673fe2fcd1d5d6180 100644 (file)
@@ -1,6 +1,120 @@
+## Foreword
+
+This guide assumes that:
+
+- Shaarli runs in a Docker container
+- The host's `10080` port is mapped to the container's `80` port
+- Shaarli's Fully Qualified Domain Name (FQDN) is `shaarli.domain.tld`
+- HTTP traffic is redirected to HTTPS
+
+## Apache
+
+- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/)
+    - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
+    - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
+
+The following HTTP headers are set by using the `ProxyPass` directive:
+
+- `X-Forwarded-For`
+- `X-Forwarded-Host`
+- `X-Forwarded-Server`
+
+```apache
+<VirtualHost *:80>
+    ServerName shaarli.domain.tld
+    Redirect permanent / https://shaarli.domain.tld
+</VirtualHost>
+
+<VirtualHost *:443>
+    ServerName shaarli.domain.tld
+
+    SSLEngine on
+    SSLCertificateFile    /path/to/cert
+    SSLCertificateKeyFile /path/to/certkey
+
+    LogLevel warn
+    ErrorLog  /var/log/apache2/shaarli-error.log
+    CustomLog /var/log/apache2/shaarli-access.log combined
+
+    RequestHeader set X-Forwarded-Proto "https"
+
+    ProxyPass        / http://127.0.0.1:10080/
+    ProxyPassReverse / http://127.0.0.1:10080/
+</VirtualHost>
+```
 
-TODO, see https://github.com/shaarli/Shaarli/issues/888
 
 ## HAProxy
 
+- [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/)
+
+```conf
+global
+    [...]
+
+defaults
+    [...]
+
+frontend http-in
+    bind :80
+       redirect scheme https code 301 if !{ ssl_fc }
+
+       bind :443 ssl crt /path/to/cert.pem
+
+       default_backend shaarli
+
+
+backend shaarli
+    mode http
+    option http-server-close
+    option forwardfor
+    reqadd X-Forwarded-Proto: https
+
+    server shaarli1 127.0.0.1:10080
+```
+
+
 ## Nginx
+
+- [Nginx documentation](https://nginx.org/en/docs/)
+
+```nginx
+http {
+    [...]
+
+    index index.html index.php;
+
+    root        /home/john/web;
+    access_log  /var/log/nginx/access.log;
+    error_log   /var/log/nginx/error.log;
+
+       server {
+               listen       80;
+               server_name  shaarli.domain.tld;
+               return       301 https://shaarli.domain.tld$request_uri;
+       }
+
+       server {
+               listen       443 ssl http2;
+               server_name  shaarli.domain.tld;
+
+        ssl_certificate       /path/to/cert
+        ssl_certificate_key   /path/to/certkey
+
+               location / {
+                       proxy_set_header  X-Real-IP         $remote_addr;
+                       proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+                       proxy_set_header  X-Forwarded-Proto $scheme;
+                       proxy_set_header  X-Forwarded-Host  $host;
+
+                       proxy_pass             http://localhost:10080/;
+                       proxy_set_header Host  $host;
+                       proxy_connect_timeout  30s;
+                       proxy_read_timeout     120s;
+
+                       access_log      /var/log/nginx/shaarli.access.log;
+                       error_log       /var/log/nginx/shaarli.error.log;
+               }
+       }
+}
+```
index 6d108d21249880a64cac62ef82e070a5958f2867..12f7b5d1ffaf8672acd7b0e5c58f45c44f5132ca 100644 (file)
@@ -1,3 +1,6 @@
+A brief guide on getting starting using docker is given in [Docker 101](docker-101.md).
+To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](../Upgrade-and-migration.md).
+
 ## Get and run a Shaarli image
 
 ### DockerHub repository
@@ -5,14 +8,24 @@ The images can be found in the [`shaarli/shaarli`](https://hub.docker.com/r/shaa
 repository.
 
 ### Available image tags
-- `latest`: master branch (tarball release)
+- `latest`: latest branch (tarball release)
+- `master`: master branch (tarball release)
 - `stable`: stable branch (tarball release)
 
-All images rely on:
+The `latest` and `master` images rely on:
+
+- [Alpine Linux](https://www.alpinelinux.org/)
+- [PHP7-FPM](http://php-fpm.org/)
+- [Nginx](http://nginx.org/)
+
+The `stable` image relies on:
+
 - [Debian 8 Jessie](https://hub.docker.com/_/debian/)
 - [PHP5-FPM](http://php-fpm.org/)
 - [Nginx](http://nginx.org/)
 
+Additional [Dockerfiles](https://github.com/shaarli/Shaarli/tree/master/docker) are provided for the `arm32v7` platform, relying on [Linuxserver.io Alpine armhf images](https://hub.docker.com/r/lsiobase/alpine.armhf/). These images must be built using [`docker build`](https://docs.docker.com/engine/reference/commandline/build/) on an `arm32v7` machine or using an emulator such as [qemu](https://resin.io/blog/building-arm-containers-on-any-x86-machine-even-dockerhub/).
+
 ### Download from DockerHub
 ```bash
 $ docker pull shaarli/shaarli
@@ -69,3 +82,14 @@ backstabbing_galileo
 $ docker ps -a
 CONTAINER ID  IMAGE            COMMAND               CREATED         STATUS        PORTS                 NAMES
 ```
+
+### Automatic builds
+
+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
+```
diff --git a/doc/md/images/install-shaarli.png b/doc/md/images/install-shaarli.png
new file mode 100644 (file)
index 0000000..7ae3381
Binary files /dev/null and b/doc/md/images/install-shaarli.png differ
diff --git a/doc/md/images/poedit-1.jpg b/doc/md/images/poedit-1.jpg
new file mode 100644 (file)
index 0000000..673ae6d
Binary files /dev/null and b/doc/md/images/poedit-1.jpg differ
index 24ada6c7cfafd5bcee221db47660ac6087d3a17b..e77b4d3a18c59468663fbfbc0c783fb677222f03 100644 (file)
@@ -22,9 +22,25 @@ It runs the latest development version of Shaarli and is updated/reset daily.
 
 Login: `demo`; Password: `demo`
 
-
 ## Features
 
+Shaarli can be used:
+
+- to share, comment and save interesting links and news.
+- to bookmark useful/frequent personal links (as private links) and share them between computers.
+- as a minimal blog/microblog/writing platform (no character limit).
+- as a read-it-later list (for example items tagged `readlater`).
+- to draft and save articles/posts/ideas.
+- to keep code snippets.
+- to keep notes and documentation.
+- as a shared clipboard/notepad/pastebin between machines.
+- as a todo list.
+- to store playlists (e.g. with the `music` or `video` tags).
+- to keep extracts/comments from webpages that may disappear.
+- to keep track of ongoing discussions (for example items tagged `discussion`).
+- [to feed RSS aggregators](http://shaarli.chassegnouf.net/?9Efeiw) (planets) with specific tags.
+- to feed other social networks, blogs... using RSS feeds and external services (dlvr.it, ifttt.com ...).
+
 ### Interface
 - minimalist design (simple is beautiful)
 - FAST
@@ -78,14 +94,12 @@ Easily extensible by any client using the REST API exposed by Shaarli.
 
 See the [API documentation](http://shaarli.github.io/api-documentation/).
 
-### Other usages
-Though Shaarli is primarily a bookmarking application, it can serve other purposes
-(see [Features](Features)):
-
-- micro-blogging
-- pastebin
-- online notepad
-- snippet archive
+### Using Shaarli as a blog, notepad, pastebin...
+- Go to your Shaarli setup and log in
+- Click the `Add Link` button
+- To share text only, do not enter any URL in the corresponding input field and click `Add Link`
+- Pick a title and enter your article, or note, in the description field; add a few tags; optionally check `Private` then click `Save`
+- Voilà!  Your article is now published (privately if you selected that option) and accessible using its permalink.
 
 ## About
 ### Shaarli community fork
diff --git a/docker/alpine/Dockerfile.armhf.latest b/docker/alpine/Dockerfile.armhf.latest
new file mode 100644 (file)
index 0000000..c923834
--- /dev/null
@@ -0,0 +1,47 @@
+FROM lsiobase/alpine.armhf:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+        ca-certificates \
+        curl \
+        nginx \
+        php7 \
+        php7-ctype \
+        php7-curl \
+        php7-fpm \
+        php7-gd \
+        php7-iconv \
+        php7-intl \
+        php7-json \
+        php7-mbstring \
+        php7-openssl \
+        php7-phar \
+        php7-session \
+        php7-xml \
+        php7-zlib \
+        s6
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY php-fpm.conf /etc/php7/php-fpm.conf
+COPY services.d /etc/services.d
+
+RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
+    && rm -rf /etc/php7/php-fpm.d/www.conf \
+    && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
+    && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
+
+
+WORKDIR /var/www
+RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \
+    && mv Shaarli-latest shaarli \
+    && cd shaarli \
+    && composer --prefer-dist --no-dev install \
+    && rm -rf ~/.composer \
+    && chown -R nginx:nginx .
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
+CMD []
diff --git a/docker/alpine/Dockerfile.armhf.master b/docker/alpine/Dockerfile.armhf.master
new file mode 100644 (file)
index 0000000..7f1bdf8
--- /dev/null
@@ -0,0 +1,47 @@
+FROM lsiobase/alpine.armhf:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+        ca-certificates \
+        curl \
+        nginx \
+        php7 \
+        php7-ctype \
+        php7-curl \
+        php7-fpm \
+        php7-gd \
+        php7-iconv \
+        php7-intl \
+        php7-json \
+        php7-mbstring \
+        php7-openssl \
+        php7-phar \
+        php7-session \
+        php7-xml \
+        php7-zlib \
+        s6
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY php-fpm.conf /etc/php7/php-fpm.conf
+COPY services.d /etc/services.d
+
+RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
+    && rm -rf /etc/php7/php-fpm.d/www.conf \
+    && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
+    && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
+
+
+WORKDIR /var/www
+RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
+    && mv Shaarli-master shaarli \
+    && cd shaarli \
+    && composer --prefer-dist --no-dev install \
+    && rm -rf ~/.composer \
+    && chown -R nginx:nginx .
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
+CMD []
diff --git a/docker/alpine/Dockerfile.latest b/docker/alpine/Dockerfile.latest
new file mode 100644 (file)
index 0000000..dd4a173
--- /dev/null
@@ -0,0 +1,47 @@
+FROM alpine:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+        ca-certificates \
+        curl \
+        nginx \
+        php7 \
+        php7-ctype \
+        php7-curl \
+        php7-fpm \
+        php7-gd \
+        php7-iconv \
+        php7-intl \
+        php7-json \
+        php7-mbstring \
+        php7-openssl \
+        php7-phar \
+        php7-session \
+        php7-xml \
+        php7-zlib \
+        s6
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY php-fpm.conf /etc/php7/php-fpm.conf
+COPY services.d /etc/services.d
+
+RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
+    && rm -rf /etc/php7/php-fpm.d/www.conf \
+    && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
+    && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
+
+
+WORKDIR /var/www
+RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \
+    && mv Shaarli-latest shaarli \
+    && cd shaarli \
+    && composer --prefer-dist --no-dev install \
+    && rm -rf ~/.composer \
+    && chown -R nginx:nginx .
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
+CMD []
diff --git a/docker/alpine/Dockerfile.master b/docker/alpine/Dockerfile.master
new file mode 100644 (file)
index 0000000..58f7c6e
--- /dev/null
@@ -0,0 +1,47 @@
+FROM alpine:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+        ca-certificates \
+        curl \
+        nginx \
+        php7 \
+        php7-ctype \
+        php7-curl \
+        php7-fpm \
+        php7-gd \
+        php7-iconv \
+        php7-intl \
+        php7-json \
+        php7-mbstring \
+        php7-openssl \
+        php7-phar \
+        php7-session \
+        php7-xml \
+        php7-zlib \
+        s6
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY php-fpm.conf /etc/php7/php-fpm.conf
+COPY services.d /etc/services.d
+
+RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
+    && rm -rf /etc/php7/php-fpm.d/www.conf \
+    && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
+    && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
+
+
+WORKDIR /var/www
+RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
+    && mv Shaarli-master shaarli \
+    && cd shaarli \
+    && composer --prefer-dist --no-dev install \
+    && rm -rf ~/.composer \
+    && chown -R nginx:nginx .
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
+CMD []
diff --git a/docker/alpine/IMAGE.md b/docker/alpine/IMAGE.md
new file mode 100644 (file)
index 0000000..a895225
--- /dev/null
@@ -0,0 +1,10 @@
+## Alpine images
+- [Alpine Linux](https://www.alpinelinux.org/)
+- [PHP-FPM](http://php-fpm.org/)
+- [Nginx](http://nginx.org/)
+
+### `shaarli/shaarli:latest`
+- [Shaarli](https://github.com/shaarli/Shaarli), `latest` branch
+
+### `shaarli/shaarli:master`
+- [Shaarli](https://github.com/shaarli/Shaarli), `master` branch
similarity index 94%
rename from docker/production/stable/nginx.conf
rename to docker/alpine/nginx.conf
index e8754d9b478f85cbeb033756d2114759f1809d37..07fba33fec11bcbf90741c086586550a0b86c57c 100644 (file)
@@ -1,6 +1,7 @@
-user www-data www-data;
+user nginx nginx;
 daemon off;
 worker_processes 4;
+pid /var/run/nginx.pid;
 
 events {
     worker_connections  768;
@@ -59,7 +60,7 @@ http {
             fastcgi_split_path_info ^(.+\.php)(/.+)$;
 
             # filter and proxy PHP requests to PHP-FPM
-            fastcgi_pass   unix:/var/run/php5-fpm.sock;
+            fastcgi_pass   unix:/var/run/php-fpm.sock;
             fastcgi_index  index.php;
             include        fastcgi.conf;
         }
diff --git a/docker/alpine/php-fpm.conf b/docker/alpine/php-fpm.conf
new file mode 100644 (file)
index 0000000..0843c16
--- /dev/null
@@ -0,0 +1,16 @@
+[global]
+daemonize = no
+
+[www]
+user = nginx
+group = nginx
+listen.owner = nginx
+listen.group = nginx
+catch_workers_output = yes
+listen = /var/run/php-fpm.sock
+pm = dynamic
+pm.max_children = 20
+pm.start_servers = 1
+pm.min_spare_servers = 1
+pm.max_spare_servers = 3
+pm.max_requests = 2048
diff --git a/docker/alpine/services.d/.s6-svscan/finish b/docker/alpine/services.d/.s6-svscan/finish
new file mode 100755 (executable)
index 0000000..1dadeea
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+/bin/true
diff --git a/docker/alpine/services.d/nginx/run b/docker/alpine/services.d/nginx/run
new file mode 100755 (executable)
index 0000000..21e7b0d
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/execlineb -P
+nginx
diff --git a/docker/alpine/services.d/php-fpm/run b/docker/alpine/services.d/php-fpm/run
new file mode 100755 (executable)
index 0000000..21dd010
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/execlineb -P
+php-fpm7 -F
diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile
deleted file mode 100644 (file)
index d050911..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-FROM debian:jessie
-MAINTAINER Shaarli Community
-
-ENV TERM dumb
-RUN apt-get update \
-    && apt-get install --no-install-recommends -y \
-       ca-certificates \
-       curl \
-       nginx-light \
-       php5-curl \
-       php5-fpm \
-       php5-gd \
-       php5-intl \
-       supervisor \
-    && apt-get clean
-
-RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini
-RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini
-COPY nginx.conf /etc/nginx/nginx.conf
-COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
-
-ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
-RUN chmod 755 /usr/local/bin/composer
-
-WORKDIR /var/www
-RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
-    && mv Shaarli-master shaarli \
-    && cd shaarli \
-    && composer --prefer-dist --no-dev install
-RUN rm -rf html \
-    && chown -R www-data:www-data .
-
-VOLUME /var/www/shaarli/data
-
-EXPOSE 80
-
-CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
diff --git a/docker/production/IMAGE.md b/docker/production/IMAGE.md
deleted file mode 100644 (file)
index 6f827b3..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-## shaarli:latest
-- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
-- [PHP5-FPM](http://php-fpm.org/)
-- [Nginx](http://nginx.org/)
-- [Shaarli](https://github.com/shaarli/Shaarli)
diff --git a/docker/production/supervised.conf b/docker/production/supervised.conf
deleted file mode 100644 (file)
index 5acd979..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-[program:php5-fpm]
-command=/usr/sbin/php5-fpm -F
-priority=5
-autostart=true
-autorestart=true
-
-[program:nginx]
-command=/usr/sbin/nginx
-priority=10
-autostart=true
-autorestart=true
-stdout_events_enabled=true
-stderr_events_enabled=true
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
new file mode 100644 (file)
index 0000000..323c611
--- /dev/null
@@ -0,0 +1,1367 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Shaarli\n"
+"POT-Creation-Date: 2017-11-11 10:59+0100\n"
+"PO-Revision-Date: 2017-11-11 11:00+0100\n"
+"Last-Translator: \n"
+"Language-Team: Shaarli\n"
+"Language: fr_FR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.0.4\n"
+"X-Poedit-Basepath: ../../../..\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-KeywordsList: t:1,2;t\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: application/ApplicationUtils.php:153
+#, php-format
+msgid ""
+"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
+"cannot run. Your PHP version has known security vulnerabilities and should "
+"be updated as soon as possible."
+msgstr ""
+"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
+"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
+"connues et devrait Ãªtre mise Ã  jour au plus tôt."
+
+#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195
+msgid "directory is not readable"
+msgstr "le répertoire n'est pas accessible en lecture"
+
+#: application/ApplicationUtils.php:198
+msgid "directory is not writable"
+msgstr "le répertoire n'est pas accessible en Ã©criture"
+
+#: application/ApplicationUtils.php:216
+msgid "file is not readable"
+msgstr "le fichier n'est pas accessible en lecture"
+
+#: application/ApplicationUtils.php:219
+msgid "file is not writable"
+msgstr "le fichier n'est pas accessible en Ã©criture"
+
+#: application/Cache.php:16
+#, php-format
+msgid "Cannot purge %s: no directory"
+msgstr "Impossible de purger %s: le répertoire n'existe pas"
+
+#: application/FeedBuilder.php:151
+msgid "Direct link"
+msgstr "Liens directs"
+
+#: application/FeedBuilder.php:153
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178
+msgid "Permalink"
+msgstr "Permalien"
+
+#: application/History.php:174
+msgid "History file isn't readable or writable"
+msgstr "Le fichier d'historique n'est pas accessible en lecture ou en Ã©criture"
+
+#: application/History.php:185
+msgid "Could not parse history file"
+msgstr "Format incorrect pour le fichier d'historique"
+
+#: application/Languages.php:159
+msgid "Automatic"
+msgstr "Automatique"
+
+#: application/Languages.php:160
+msgid "English"
+msgstr "Anglais"
+
+#: application/Languages.php:161
+msgid "French"
+msgstr "Français"
+
+#: application/LinkDB.php:136
+msgid "You are not authorized to add a link."
+msgstr "Vous n'êtes pas autorisé Ã  ajouter un lien."
+
+#: application/LinkDB.php:139
+msgid "Internal Error: A link should always have an id and URL."
+msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
+
+#: application/LinkDB.php:142
+msgid "You must specify an integer as a key."
+msgstr "Vous devez utiliser un entier comme clé."
+
+#: application/LinkDB.php:145
+msgid "Array offset and link ID must be equal."
+msgstr "La clé du tableau et l'ID du lien doivent Ãªtre Ã©gaux."
+
+#: application/LinkDB.php:251
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
+msgid ""
+"The personal, minimalist, super-fast, database free, bookmarking service"
+msgstr ""
+"Le gestionnaire de marque-page personnel, minimaliste, et sans base de "
+"données"
+
+#: application/LinkDB.php:253
+msgid ""
+"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
+"me, you must first login.\n"
+"\n"
+"To learn how to use Shaarli, consult the link \"Documentation\" at the "
+"bottom of this page.\n"
+"\n"
+"You use the community supported version of the original Shaarli project, by "
+"Sebastien Sauvage."
+msgstr ""
+"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me "
+"modifier ou me supprimer, vous devez d'abord vous connecter.\n"
+"\n"
+"Pour apprendre comment utiliser Shaarli, consultez le lien Â« Documentation Â» "
+"en bas de page.\n"
+"\n"
+"Vous utilisez la version supportée par la communauté du projet original "
+"Shaarli, de Sébastien Sauvage."
+
+#: application/LinkDB.php:267
+msgid "My secret stuff... - Pastebin.com"
+msgstr "Mes trucs secrets... - Pastebin.com"
+
+#: application/LinkDB.php:269
+msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
+msgstr ""
+"Pssst ! Je suis un lien privé que VOUS Ãªtes le seul Ã  voir. Vous pouvez me "
+"supprimer aussi."
+
+#: application/LinkFilter.php:452
+msgid "The link you are trying to reach does not exist or has been deleted."
+msgstr "Le lien que vous essayez de consulter n'existe pas ou a Ã©té supprimé."
+
+#: application/NetscapeBookmarkUtils.php:35
+msgid "Invalid export selection:"
+msgstr "Sélection d'export invalide :"
+
+#: application/NetscapeBookmarkUtils.php:81
+#, php-format
+msgid "File %s (%d bytes) "
+msgstr "Le fichier %s (%d octets) "
+
+#: application/NetscapeBookmarkUtils.php:83
+msgid "has an unknown file format. Nothing was imported."
+msgstr "a un format inconnu. Rien n'a Ã©té importé."
+
+#: application/NetscapeBookmarkUtils.php:86
+#, php-format
+msgid ""
+"was successfully processed in %d seconds: %d links imported, %d links "
+"overwritten, %d links skipped."
+msgstr ""
+"a Ã©té importé avec succès en %d secondes : %d liens importés, %d liens "
+"écrasés, %d liens ignorés."
+
+#: application/PageBuilder.php:167
+msgid "The page you are trying to reach does not exist or has been deleted."
+msgstr "La page que vous essayez de consulter n'existe pas ou a Ã©té supprimée."
+
+#: application/PageBuilder.php:169
+msgid "404 Not Found"
+msgstr "404 Introuvable"
+
+#: application/PluginManager.php:243
+#, php-format
+msgid "Plugin \"%s\" files not found."
+msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
+
+#: application/Updater.php:76
+msgid "Couldn't retrieve Updater class methods."
+msgstr "Impossible de récupérer les méthodes de la classe Updater."
+
+#: application/Updater.php:493
+msgid "An error occurred while running the update "
+msgstr "Une erreur s'est produite lors de l'exécution de la mise Ã  jour "
+
+#: application/Updater.php:533
+msgid "Updates file path is not set, can't write updates."
+msgstr ""
+"Le chemin vers le fichier de mise Ã  jour n'est pas défini, impossible "
+"d'écrire les mises Ã  jour."
+
+#: application/Updater.php:538
+msgid "Unable to write updates in "
+msgstr "Impossible d'écrire les mises Ã  jour dans "
+
+#: application/Utils.php:376 tests/UtilsTest.php:340
+msgid "Setting not set"
+msgstr "Paramètre non défini"
+
+#: application/Utils.php:383 tests/UtilsTest.php:338 tests/UtilsTest.php:339
+msgid "Unlimited"
+msgstr "Illimité"
+
+#: application/Utils.php:386 tests/UtilsTest.php:335 tests/UtilsTest.php:336
+#: tests/UtilsTest.php:350
+msgid "B"
+msgstr "o"
+
+#: application/Utils.php:386 tests/UtilsTest.php:329 tests/UtilsTest.php:330
+#: tests/UtilsTest.php:337
+msgid "kiB"
+msgstr "ko"
+
+#: application/Utils.php:386 tests/UtilsTest.php:331 tests/UtilsTest.php:332
+#: tests/UtilsTest.php:348 tests/UtilsTest.php:349
+msgid "MiB"
+msgstr "Mo"
+
+#: application/Utils.php:386 tests/UtilsTest.php:333 tests/UtilsTest.php:334
+msgid "GiB"
+msgstr "Go"
+
+#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121
+msgid ""
+"Shaarli could not create the config file. Please make sure Shaarli has the "
+"right to write in the folder is it installed in."
+msgstr ""
+"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
+"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
+
+#: application/config/ConfigManager.php:135
+msgid "Invalid setting key parameter. String expected, got: "
+msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
+
+#: application/config/exception/MissingFieldConfigException.php:21
+#, php-format
+msgid "Configuration value is required for %s"
+msgstr "Le paramètre %s est obligatoire"
+
+#: application/config/exception/PluginConfigOrderException.php:15
+msgid "An error occurred while trying to save plugins loading order."
+msgstr ""
+"Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions."
+
+#: application/config/exception/UnauthorizedConfigException.php:16
+msgid "You are not authorized to alter config."
+msgstr "Vous n'êtes pas autorisé Ã  modifier la configuration."
+
+#: application/exceptions/IOException.php:19
+msgid "Error accessing"
+msgstr "Une erreur s'est produite en accédant Ã "
+
+#: index.php:135
+msgid "Shared links on "
+msgstr "Liens partagés sur "
+
+#: index.php:157
+msgid "Insufficient permissions:"
+msgstr "Permissions insuffisantes :"
+
+#: index.php:384
+msgid "I said: NO. You are banned for the moment. Go away."
+msgstr "NON. Vous Ãªtes banni pour le moment. Revenez plus tard."
+
+#: index.php:449
+msgid "Wrong login/password."
+msgstr "Nom d'utilisateur ou mot de passe incorrects."
+
+#: index.php:1092
+msgid "You are not supposed to change a password on an Open Shaarli."
+msgstr ""
+"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
+
+#: index.php:1097 index.php:1138 index.php:1214 index.php:1244 index.php:1344
+msgid "Wrong token."
+msgstr "Jeton invalide."
+
+#: index.php:1102
+msgid "The old password is not correct."
+msgstr "L'ancien mot de passe est incorrect."
+
+#: index.php:1122
+msgid "Your password has been changed"
+msgstr "Votre mot de passe a Ã©té modifié"
+
+#: index.php:1175
+msgid "Configuration was saved."
+msgstr "La configuration a Ã©té sauvegardé."
+
+#: index.php:1226
+#, php-format
+msgid "The tag was removed from %d link."
+msgid_plural "The tag was removed from %d links."
+msgstr[0] "Le tag a Ã©té supprimé de %d lien."
+msgstr[1] "Le tag a Ã©té supprimé de %d liens."
+
+#: index.php:1227
+#, php-format
+msgid "The tag was renamed in %d link."
+msgid_plural "The tag was renamed in %d links."
+msgstr[0] "Le tag a Ã©té renommé dans %d lien."
+msgstr[1] "Le tag a Ã©té renommé dans %d liens."
+
+#: index.php:1443
+msgid "Note: "
+msgstr "Note : "
+
+#: index.php:1552
+#, php-format
+msgid ""
+"The file you are trying to upload is probably bigger than what this "
+"webserver can accept (%s). Please upload in smaller chunks."
+msgstr ""
+"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
+"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
+"légères."
+
+#: index.php:1972
+#, php-format
+msgid ""
+"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
+"variable \"session.save_path\" is set correctly in your PHP config, and that "
+"you have write access to it.<br>It currently points to %s.<br>On some "
+"browsers, accessing your server via a hostname like 'localhost' or any "
+"custom hostname without a dot causes cookie storage to fail. We recommend "
+"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
+msgstr ""
+"<pre>Les sesssions ne semble pas fonctionner sur ce serveur.<br>Assurez vous "
+"que la variable Â« session.save_path Â» est correctement définie dans votre "
+"fichier de configuration PHP, et que vous y avez les droits d'écriture."
+"<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains navigateurs, "
+"accéder Ã  votre serveur depuis un nom d'hôte comme Â« localhost Â» ou autre "
+"nom personnalisé sans point '.' entraine l'échec de la sauvegarde des "
+"cookies. Nous vous recommandons d'accéder Ã  votre serveur depuis son adresse "
+"IP ou un <em>Fully Qualified Domain Name</em>.<br>"
+
+#: index.php:1982
+msgid "Click to try again."
+msgstr "Cliquer ici pour réessayer."
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:29
+msgid "URI"
+msgstr "URI"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:33
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Add link"
+msgstr "Shaare"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:50
+msgid "Adds the addlink input on the linklist page."
+msgstr "Ajout le formulaire d'ajout de liens sur la page principale."
+
+#: plugins/archiveorg/archiveorg.php:23
+msgid "View on archive.org"
+msgstr "Voir sur archive.org"
+
+#: plugins/archiveorg/archiveorg.php:36
+msgid "For each link, add an Archive.org icon."
+msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
+
+#: plugins/demo_plugin/demo_plugin.php:469
+msgid ""
+"A demo plugin covering all use cases for template designers and plugin "
+"developers."
+msgstr ""
+"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
+"designers et les développeurs."
+
+#: plugins/isso/isso.php:20
+msgid ""
+"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
+"administration page."
+msgstr ""
+"Erreur de l'extension Isso : Merci de définir le paramètre Â« ISSO_SERVER Â» "
+"dans la page d'administration des extensions."
+
+#: plugins/isso/isso.php:63
+msgid "Let visitor comment your shaares on permalinks with Isso."
+msgstr ""
+"Permet aux visiteurs de commenter vos shaares sur les permaliens avec Isso."
+
+#: plugins/isso/isso.php:64
+msgid "Isso server URL (without 'http://')"
+msgstr "URL du serveur Isso (sans 'http://')"
+
+#: plugins/markdown/markdown.php:159
+msgid "Description will be rendered with"
+msgstr "La description sera générée avec"
+
+#: plugins/markdown/markdown.php:160
+msgid "Markdown syntax documentation"
+msgstr "Documentation sur la syntaxe Markdown"
+
+#: plugins/markdown/markdown.php:161
+msgid "Markdown syntax"
+msgstr "la syntaxe Markdown"
+
+#: plugins/markdown/markdown.php:340
+msgid ""
+"Render shaare description with Markdown syntax.<br><strong>Warning</"
+"strong>:\n"
+"If your shaared descriptions contained HTML tags before enabling the "
+"markdown plugin,\n"
+"enabling it might break your page.\n"
+"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
+"markdown#html-rendering\">README</a>."
+msgstr ""
+"Utilise la syntaxe Markdown pour la description des liens."
+"<br><strong>Attention</strong> :\n"
+"Si vous aviez des descriptions contenant du HTML avant d'activer cette "
+"extension,\n"
+"l'activer pourrait déformer vos pages.\n"
+"Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
+"markdown#html-rendering\">README</a>."
+
+#: plugins/piwik/piwik.php:21
+msgid ""
+"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
+"administration page."
+msgstr ""
+"Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et "
+"PIWIK_SITEID dans la page d'administration des extensions."
+
+#: plugins/piwik/piwik.php:70
+msgid "A plugin that adds Piwik tracking code to Shaarli pages."
+msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli."
+
+#: plugins/piwik/piwik.php:71
+msgid "Piwik URL"
+msgstr "URL de Piwik"
+
+#: plugins/piwik/piwik.php:72
+msgid "Piwik site ID"
+msgstr "Site ID de Piwik"
+
+#: plugins/playvideos/playvideos.php:22
+msgid "Video player"
+msgstr "Lecteur vidéo"
+
+#: plugins/playvideos/playvideos.php:25
+msgid "Play Videos"
+msgstr "Jouer les vidéos"
+
+#: plugins/playvideos/playvideos.php:56
+msgid "Add a button in the toolbar allowing to watch all videos."
+msgstr ""
+"Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos."
+
+#: plugins/playvideos/youtube_playlist.js:214
+msgid "plugins/playvideos/jquery-1.11.2.min.js"
+msgstr ""
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:69
+#, php-format
+msgid "Could not publish to PubSubHubbub: %s"
+msgstr "Impossible de publier vers PubSubHubbub : %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:95
+#, php-format
+msgid "Could not post to %s"
+msgstr "Impossible de publier vers %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:99
+#, php-format
+msgid "Bad response from the hub %s"
+msgstr "Mauvaise réponse du hub %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:110
+msgid "Enable PubSubHubbub feed publishing."
+msgstr "Active la publication de flux vers PubSubHubbub."
+
+#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68
+msgid "For each link, add a QRCode icon."
+msgstr "Pour chaque liens, ajouter une icône de QRCode."
+
+#: plugins/wallabag/wallabag.php:21
+msgid ""
+"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
+"plugin administration page."
+msgstr ""
+"Erreur de l'extension Wallabag : Merci de définir le paramètre Â« "
+"WALLABAG_URL Â» dans la page d'administration des extensions."
+
+#: plugins/wallabag/wallabag.php:47
+msgid "Save to wallabag"
+msgstr "Sauvegarder dans Wallabag"
+
+#: plugins/wallabag/wallabag.php:69
+msgid "Wallabag API URL"
+msgstr "URL de l'API Wallabag"
+
+#: plugins/wallabag/wallabag.php:70
+msgid "Wallabag API version (1 or 2)"
+msgstr "Version de l'API Wallabag (1 ou 2)"
+
+#: tests/LanguagesTest.php:188 tests/LanguagesTest.php:201
+#: tests/languages/fr/LanguagesFrTest.php:160
+#: tests/languages/fr/LanguagesFrTest.php:173
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81
+msgid "Search"
+msgid_plural "Search"
+msgstr[0] "Rechercher"
+msgstr[1] "Rechercher"
+
+#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
+msgid "Sorry, nothing to see here."
+msgstr "Désolé, il y a rien Ã  voir ici."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+msgid "Shaare a new link"
+msgstr "Partager un nouveau lien"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "URL or leave empty to post a note"
+msgstr "URL ou laisser vide pour créer une note"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Change password"
+msgstr "Modification du mot de passe"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Current password"
+msgstr "Mot de passe actuel"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "New password"
+msgstr "Nouveau mot de passe"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Change"
+msgstr "Changer"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Manage tags"
+msgstr "Gérer les tags"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+msgid "Tag"
+msgstr "Tag"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "New name"
+msgstr "Nouveau nom"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+msgid "Case sensitive"
+msgstr "Sensible Ã  la casse"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+msgid "Rename"
+msgstr "Renommer"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172
+msgid "Delete"
+msgstr "Supprimer"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
+msgid "You can also edit tags in the"
+msgstr "Vous pouvez aussi modifier les tags dans la"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
+msgid "tag list"
+msgstr "liste des tags"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "Configure"
+msgstr "Configurer"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "title"
+msgstr "titre"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+msgid "Home link"
+msgstr "Lien vers l'accueil"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "Default value"
+msgstr "Valeur par défaut"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "Theme"
+msgstr "Thème"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+msgid "Language"
+msgstr "Langue"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "Timezone"
+msgstr "Fuseau horaire"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
+msgid "Continent"
+msgstr "Continent"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
+msgid "City"
+msgstr "Ville"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
+msgid "Disable session cookie hijacking protection"
+msgstr "Désactiver la protection contre le détournement de cookies"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166
+msgid "Check this if you get disconnected or if your IP address changes often"
+msgstr ""
+"Cocher cette case si vous Ãªtes souvent déconnecté ou si votre adresse IP "
+"change souvent"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
+msgid "Private links by default"
+msgstr "Liens privés par défaut"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184
+msgid "All new links are private by default"
+msgstr "Tous les nouveaux liens sont privés par défaut"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
+msgid "RSS direct links"
+msgstr "Liens directs dans le flux RSS"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200
+msgid "Check this to use direct URL instead of permalink in feeds"
+msgstr ""
+"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
+"dans le flux RSS"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
+msgid "Hide public links"
+msgstr "Cacher les liens publics"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216
+msgid "Do not show any links if the user is not logged in"
+msgstr "N'afficher aucun lien sans Ãªtre connecté"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
+msgid "Check updates"
+msgstr "Vérifier les mises Ã  jour"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
+msgid "Notify me when a new release is ready"
+msgstr "Me notifier lorsqu'une nouvelle version est disponible"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+msgid "Enable REST API"
+msgstr "Activer l'API REST"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
+msgid "Allow third party software to use Shaarli such as mobile application"
+msgstr ""
+"Permets aux applications tierces d'utiliser Shaarli, par exemple les "
+"applications mobiles"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
+msgid "API secret"
+msgstr "Clé d'API secrète"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
+msgid "Save"
+msgstr "Enregistrer"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "The Daily Shaarli"
+msgstr "Le Quotidien Shaarli"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "1 RSS entry per day"
+msgstr "1 entrée RSS par jour"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
+msgid "Previous day"
+msgstr "Jour précédent"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "All links of one day in a single page."
+msgstr "Tous les liens d'un jour sur une page."
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
+msgid "Next day"
+msgstr "Jour suivant"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
+msgid "Edit"
+msgstr "Modifier"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
+msgid "Shaare"
+msgstr "Shaare"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
+msgid "Created:"
+msgstr "Création :"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "URL"
+msgstr "URL"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+msgid "Title"
+msgstr "Titre"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
+msgid "Description"
+msgstr "Description"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+msgid "Tags"
+msgstr "Tags"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
+msgid "Private"
+msgstr "Privé"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+msgid "Apply Changes"
+msgstr "Appliquer les changements"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Export Database"
+msgstr "Exporter les données"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "Selection"
+msgstr "Choisir"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+msgid "All"
+msgstr "Tous"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "Public"
+msgstr "Publics"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
+msgid "Prepend note permalinks with this Shaarli instance's URL"
+msgstr "Préfixer les liens de notes avec l'URL de l'instance de Shaarli"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+msgid "Useful to import bookmarks in a web browser"
+msgstr "Utile pour importer les marques-pages dans un navigateur"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
+msgid "Export"
+msgstr "Exporter"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Import Database"
+msgstr "Importer des données"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Maximum size allowed:"
+msgstr "Taille maximum autorisée :"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Visibility"
+msgstr "Visibilité"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Use values from the imported file, default to public"
+msgstr ""
+"Utiliser les valeurs présentes dans le fichier d'import, public par défaut"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "Import all bookmarks as private"
+msgstr "Importer tous les liens comme privés"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+msgid "Import all bookmarks as public"
+msgstr "Importer tous les liens comme publics"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
+msgid "Overwrite existing bookmarks"
+msgstr "Remplacer les liens existants"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "Duplicates based on URL"
+msgstr "Les doublons s'appuient sur les URL"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+msgid "Add default tags"
+msgstr "Ajouter des tags par défaut"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "Import"
+msgstr "Importer"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Install Shaarli"
+msgstr "Installation de Shaarli"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
+msgid "It looks like it's the first time you run Shaarli. Please configure it."
+msgstr ""
+"Il semblerait que Ã§a soit la première fois que vous lancez Shaarli. Merci de "
+"le configurer."
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
+msgid "Username"
+msgstr "Nom d'utilisateur"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148
+msgid "Password"
+msgstr "Mot de passe"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+msgid "Shaarli title"
+msgstr "Titre du Shaarli"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
+msgid "My links"
+msgstr "Mes liens"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
+msgid "Install"
+msgstr "Installer"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
+msgid "shaare"
+msgid_plural "shaares"
+msgstr[0] "shaare"
+msgstr[1] "shaares"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
+msgid "private link"
+msgid_plural "private links"
+msgstr[0] "lien privé"
+msgstr[1] "liens privés"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117
+msgid "Search text"
+msgstr "Recherche texte"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
+msgid "Filter by tag"
+msgstr "Filtrer par tag"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
+msgid "Nothing found."
+msgstr "Aucun résultat."
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119
+#, php-format
+msgid "%s result"
+msgid_plural "%s results"
+msgstr[0] "%s résultat"
+msgstr[1] "%s résultats"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+msgid "for"
+msgstr "pour"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
+msgid "tagged"
+msgstr "taggé"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "Remove tag"
+msgstr "Retirer le tag"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
+msgid "with status"
+msgstr "avec le statut"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
+msgid "without any tag"
+msgstr "sans tag"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
+msgid "Fold"
+msgstr "Replier"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
+msgid "Edited: "
+msgstr "Modifié : "
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
+msgid "permalink"
+msgstr "permalien"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
+msgid "Add tag"
+msgstr "Ajouter un tag"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7
+msgid "Filters"
+msgstr "Filtres"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12
+msgid "Filter private links"
+msgstr "Filtrer par liens privés"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
+msgid "Filter untagged links"
+msgstr "Filtrer par liens privés"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:22
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:74
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
+msgid "Fold all"
+msgstr "Replier tout"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:67
+msgid "Links per page"
+msgstr "Liens par page"
+
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid ""
+"You have been banned after too many failed login attempts. Try again later."
+msgstr ""
+"Vous avez Ã©té banni après trop d'échec d'authentification. Merci de "
+"réessayer plus tard."
+
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95
+msgid "Login"
+msgstr "Connexion"
+
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
+msgid "Remember me"
+msgstr "Rester connecté"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "by the Shaarli community"
+msgstr "par la communauté Shaarli"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
+msgid "Documentation"
+msgstr "Documentation"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
+msgid "Expand"
+msgstr "Déplier"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
+msgid "Expand all"
+msgstr "Déplier tout"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
+msgid "Are you sure you want to delete this link?"
+msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
+msgid "Tools"
+msgstr "Outils"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Tag cloud"
+msgstr "Nuage de tags"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39
+msgid "Picture wall"
+msgstr "Mur d'images"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42
+msgid "Daily"
+msgstr "Quotidien"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86
+msgid "RSS Feed"
+msgstr "Flux RSS"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102
+msgid "Logout"
+msgstr "Déconnexion"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
+msgid "is available"
+msgstr "est disponible"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176
+msgid "Error"
+msgstr "Erreur"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Picture Wall"
+msgstr "Mur d'images"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "pics"
+msgstr "images"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "You need to enable Javascript to change plugin loading order."
+msgstr ""
+"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Plugin administration"
+msgstr "Administration des extensions"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Enabled Plugins"
+msgstr "Extensions activées"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
+msgid "No plugin enabled."
+msgstr "Aucune extension activée."
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
+msgid "Disable"
+msgstr "Désactiver"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+msgid "Name"
+msgstr "Nom"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
+msgid "Order"
+msgstr "Ordre"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
+msgid "Disabled Plugins"
+msgstr "Extensions désactivées"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
+msgid "No plugin disabled."
+msgstr "Aucune extension désactivée."
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
+msgid "Enable"
+msgstr "Activer"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "More plugins available"
+msgstr "Plus d'extensions disponibles"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
+msgid "in the documentation"
+msgstr "dans la documentation"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
+msgid "Plugin configuration"
+msgstr "Configuration des extensions"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195
+msgid "No parameter available."
+msgstr "Aucun paramètre disponible."
+
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "tags"
+msgstr "tags"
+
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "List all links with those tags"
+msgstr "Lister tous les liens avec ces tags"
+
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Tag list"
+msgstr "List des tags"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
+msgid "Sort by:"
+msgstr "Trier par :"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
+msgid "Cloud"
+msgstr "Nuage"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
+msgid "Most used"
+msgstr "Plus utilisés"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
+msgid "Alphabetical"
+msgstr "Alphabétique"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Settings"
+msgstr "Paramètres"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Change Shaarli settings: title, timezone, etc."
+msgstr "Changer les paramètres de Shaarli : titre, fuseau horaire, etc."
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "Configure your Shaarli"
+msgstr "Conguration de Shaarli"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
+msgid "Enable, disable and configure plugins"
+msgstr "Activer, désactiver et configurer les extensions"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Change your password"
+msgstr "Modification du mot de passe"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+msgid "Rename or delete a tag in all links"
+msgstr "Rename or delete a tag in all links"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid ""
+"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
+"delicious...)"
+msgstr ""
+"Importer des marques pages au format Netscape HTML (comme exportés depuis "
+"Firefox, Chrome, Opera, delicious...)"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+msgid "Import links"
+msgstr "Importer des liens"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+msgid ""
+"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
+"Opera, delicious...)"
+msgstr ""
+"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
+"Firefox, Chrome, Opera, delicious...)"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+msgid "Export database"
+msgstr "Exporter les données"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
+msgid ""
+"Drag one of these button to your bookmarks toolbar or right-click it and "
+"\"Bookmark This Link\""
+msgstr ""
+"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit "
+"dessus et Â« Ajouter aux favoris Â»"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+msgid "then click on the bookmarklet in any page you want to share."
+msgstr ""
+"puis cliquer sur le marque page depuis un site que vous souhaitez partager."
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100
+msgid ""
+"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
+"Link"
+msgstr ""
+"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et Â« "
+"Ajouter aux favoris Â»"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+msgid "then click âœšShaare link button in any page you want to share"
+msgstr "puis cliquer sur âœšShaare depuis un site que vous souhaitez partager"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
+msgid "The selected text is too long, it will be truncated."
+msgstr "Le texte sélectionné est trop long, il sera tronqué."
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
+msgid "Shaare link"
+msgstr "Shaare"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
+msgid ""
+"Then click âœšAdd Note button anytime to start composing a private Note (text "
+"post) to your Shaarli"
+msgstr ""
+"Puis cliquer sur âœšAdd Note pour commencer Ã  rédiger une Note sur Shaarli"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
+msgid "Add Note"
+msgstr "Ajouter une Note"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
+msgid ""
+"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
+"functionality."
+msgstr ""
+"Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette "
+"fonctionalité."
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "Add to"
+msgstr "Ajouter Ã "
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
+msgid "3rd party"
+msgstr "Applications tierces"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
+msgid "Plugin"
+msgstr "Extension"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
+msgid "plugin"
+msgstr "extension"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
+msgid ""
+"Drag this link to your bookmarks toolbar, or right-click it and choose "
+"Bookmark This Link"
+msgstr ""
+"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et Â« "
+"Ajouter aux favoris Â»"
+
+#~ msgid "Redirector"
+#~ msgstr "Redirecteur"
+
+#~ msgid "e. g."
+#~ msgstr "ex :"
+
+#~ msgid "will mask the HTTP_REFERER"
+#~ msgstr "masque le HTTP_REFERER"
+
+#~ msgid ""
+#~ "An error occurred while parsing JSON configuration file (%s): error code #"
+#~ "%d"
+#~ msgstr ""
+#~ "Une erreur s'est produite lors de la lecture du fichier de configuration "
+#~ "JSON (%s) : code d'erreur #%d"
+
+#~ msgid ""
+#~ "Please check your JSON syntax (without PHP comment tags) using a JSON "
+#~ "lint tool such as "
+#~ msgstr ""
+#~ "Merci de vérifier la syntaxe JSON (sans les balises de commentaires PHP) "
+#~ "en utilisant un validateur de JSON tel que "
+
+#~ msgid ""
+#~ "Error: missing Composer dependencies\n"
+#~ "\n"
+#~ "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"
+#~ msgstr ""
+#~ "Erreur : les dépendances Composer sont manquantes\n"
+#~ "\n"
+#~ "Si vous avez installé Shaarli avec Git ou depuis la branche de "
+#~ "développement\n"
+#~ "merci de consulter la documentation d'installation pour installer les "
+#~ "dépendances Composer :\n"
+#~ "\n"
+
+#~ msgid "Sessions do not seem to work correctly on your server."
+#~ msgstr "Les sessions ne semblent "
+
+#~ msgid "Tag was renamed in "
+#~ msgstr "Le tag a Ã©té renommé dans "
+
+#, fuzzy
+#~| msgid "My links"
+#~ msgid " links"
+#~ msgstr "Mes liens"
+
+#, fuzzy
+#~| msgid ""
+#~| "Error: missing Composer configuration\n"
+#~| "\n"
+#~ msgid "Error: missing Composer configuration"
+#~ msgstr ""
+#~ "Erreur : la configuration Composer est manquante\n"
+#~ "\n"
+
+#, fuzzy
+#~| msgid ""
+#~| "Shaarli could not create the config file. Please make sure Shaarli has "
+#~| "the right to write in the folder is it installed in."
+#~ msgid ""
+#~ "Shaarli could not create the config file. \n"
+#~ "                   Please make sure Shaarli has the right to write in the "
+#~ "folder is it installed in."
+#~ msgstr ""
+#~ "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier "
+#~ "que Shaarli a les droits d'écriture dans le dossier dans lequel il est "
+#~ "installé."
+
+#, fuzzy
+#~| msgid "Plugin"
+#~ msgid "Plugin \""
+#~ msgstr "Extension"
+
+#~ msgid "Your PHP version is obsolete!"
+#~ msgstr "Votre version de PHP est obsolète !"
+
+#~ msgid " Shaarli requires at least PHP "
+#~ msgstr "Shaarli nécessite au moins PHP"
index ac51038d7f1f50e940a2a6ad66aa3806dff742b7..d57789e656749c248fbffe1e60a862b0aad4fbe6 100644 (file)
--- a/index.php
+++ b/index.php
@@ -64,7 +64,6 @@ require_once 'application/FeedBuilder.php';
 require_once 'application/FileUtils.php';
 require_once 'application/History.php';
 require_once 'application/HttpUtils.php';
-require_once 'application/Languages.php';
 require_once 'application/LinkDB.php';
 require_once 'application/LinkFilter.php';
 require_once 'application/LinkUtils.php';
@@ -76,8 +75,10 @@ require_once 'application/Utils.php';
 require_once 'application/PluginManager.php';
 require_once 'application/Router.php';
 require_once 'application/Updater.php';
+use \Shaarli\Languages;
 use \Shaarli\ThemeUtils;
 use \Shaarli\Config\ConfigManager;
+use \Shaarli\SessionManager;
 
 // Ensure the PHP version is supported
 try {
@@ -88,7 +89,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();
@@ -115,14 +116,23 @@ if (session_id() == '') {
 }
 
 // Regenerate session ID if invalid or not defined in cookie.
-if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) {
+if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
     session_regenerate_id(true);
     $_COOKIE['shaarli'] = session_id();
 }
 
 $conf = new ConfigManager();
+$sessionManager = new SessionManager($_SESSION, $conf);
+
+// Sniff browser language and set date format accordingly.
+if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+    autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
+}
+
+new Languages(setlocale(LC_MESSAGES, 0), $conf);
+
 $conf->setEmpty('general.timezone', date_default_timezone_get());
-$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER)));
+$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
 RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
 RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
 
@@ -144,7 +154,7 @@ if (! is_file($conf->getConfigFileExt())) {
     $errors = ApplicationUtils::checkResourcePermissions($conf);
 
     if ($errors != array()) {
-        $message = '<p>Insufficient permissions:</p><ul>';
+        $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
 
         foreach ($errors as $error) {
             $message .= '<li>'.$error.'</li>';
@@ -157,17 +167,12 @@ if (! is_file($conf->getConfigFileExt())) {
     }
 
     // Display the installation form if no existing config is found
-    install($conf);
+    install($conf, $sessionManager);
 }
 
 // a token depending of deployment salt, user password, and the current ip
 define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
 
-// Sniff browser language and set date format accordingly.
-if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
-    autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
-}
-
 /**
  * Checking session state (i.e. is the user still logged in)
  *
@@ -376,9 +381,9 @@ function ban_canLogin($conf)
 // Process login form: Check if login/password is correct.
 if (isset($_POST['login']))
 {
-    if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.');
+    if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.'));
     if (isset($_POST['password'])
-        && tokenOk($_POST['token'])
+        && $sessionManager->checkToken($_POST['token'])
         && (check_auth($_POST['login'], $_POST['password'], $conf))
     ) {   // Login/password is OK.
         ban_loginOk($conf);
@@ -431,7 +436,7 @@ if (isset($_POST['login']))
     else
     {
         ban_loginFailed($conf);
-        $redir = '&username='. $_POST['login'];
+        $redir = '&username='. urlencode($_POST['login']);
         if (isset($_GET['post'])) {
             $redir .= '&post=' . urlencode($_GET['post']);
             foreach (array('description', 'source', 'title', 'tags') as $param) {
@@ -440,7 +445,8 @@ if (isset($_POST['login']))
                 }
             }
         }
-        echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen.
+        // Redirect to login screen.
+        echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
         exit;
     }
 }
@@ -450,32 +456,6 @@ if (isset($_POST['login']))
 // Token should be used in any form which acts on data (create,update,delete,import...).
 if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array();  // Token are attached to the session.
 
-/**
- * Returns a token.
- *
- * @param ConfigManager $conf Configuration Manager instance.
- *
- * @return string token.
- */
-function getToken($conf)
-{
-    $rnd = sha1(uniqid('', true) .'_'. mt_rand() . $conf->get('credentials.salt'));  // We generate a random string.
-    $_SESSION['tokens'][$rnd]=1;  // Store it on the server side.
-    return $rnd;
-}
-
-// Tells if a token is OK. Using this function will destroy the token.
-// true=token is OK.
-function tokenOk($token)
-{
-    if (isset($_SESSION['tokens'][$token]))
-    {
-        unset($_SESSION['tokens'][$token]); // Token is used: destroy it.
-        return true; // Token is OK.
-    }
-    return false; // Wrong token, or already used.
-}
-
 /**
  * Daily RSS feed: 1 RSS entry per day giving all the links on that day.
  * Gives the last 7 days (which have links).
@@ -546,7 +526,11 @@ function showDailyRSS($conf) {
 
         // We pre-format some fields for proper output.
         foreach ($links as &$link) {
-            $link['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
+            $link['formatedDescription'] = format_description(
+                $link['description'],
+                $conf->get('redirector.url'),
+                $conf->get('redirector.encode_url')
+            );
             $link['thumbnail'] = thumbnail($conf, $link['url']);
             $link['timestamp'] = $link['created']->getTimestamp();
             if (startsWith($link['url'], '?')) {
@@ -618,7 +602,11 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
         $taglist = explode(' ',$link['tags']);
         uasort($taglist, 'strcasecmp');
         $linksToDisplay[$key]['taglist']=$taglist;
-        $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
+        $linksToDisplay[$key]['formatedDescription'] = format_description(
+            $link['description'],
+            $conf->get('redirector.url'),
+            $conf->get('redirector.encode_url')
+        );
         $linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']);
         $linksToDisplay[$key]['timestamp'] =  $link['created']->getTimestamp();
     }
@@ -683,12 +671,13 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
 /**
  * Render HTML page (according to URL parameters and user rights)
  *
- * @param ConfigManager $conf          Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance,
- * @param LinkDB        $LINKSDB
- * @param History       $history       instance
+ * @param ConfigManager  $conf           Configuration Manager instance.
+ * @param PluginManager  $pluginManager  Plugin Manager instance,
+ * @param LinkDB         $LINKSDB
+ * @param History        $history        instance
+ * @param SessionManager $sessionManager SessionManager instance
  */
-function renderPage($conf, $pluginManager, $LINKSDB, $history)
+function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
 {
     $updater = new Updater(
         read_updates_file($conf->get('resource.updates')),
@@ -709,7 +698,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
         die($e->getMessage());
     }
 
-    $PAGE = new PageBuilder($conf, $LINKSDB);
+    $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
     $PAGE->assign('linkcount', count($LINKSDB));
     $PAGE->assign('privateLinkcount', count_private($LINKSDB));
     $PAGE->assign('plugin_errors', $pluginManager->getErrors());
@@ -840,7 +829,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()));
@@ -870,7 +859,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()]);
@@ -1100,16 +1089,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
     if ($targetPage == Router::$PAGE_CHANGEPASSWORD)
     {
         if ($conf->get('security.open_shaarli')) {
-            die('You are not supposed to change a password on an Open Shaarli.');
+            die(t('You are not supposed to change a password on an Open Shaarli.'));
         }
 
         if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
         {
-            if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away!
+            if (!$sessionManager->checkToken($_POST['token'])) die(t('Wrong token.')); // Go away!
 
             // Make sure old password is correct.
             $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt'));
-            if ($oldhash!= $conf->get('credentials.hash')) { echo '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; }
+            if ($oldhash!= $conf->get('credentials.hash')) {
+                echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
+                exit;
+            }
             // Save new password
             // Salt renders rainbow-tables attacks useless.
             $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
@@ -1127,7 +1119,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
                 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
                 exit;
             }
-            echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>';
+            echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
             exit;
         }
         else // show the change password form.
@@ -1142,8 +1134,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
     {
         if (!empty($_POST['title']) )
         {
-            if (!tokenOk($_POST['token'])) {
-                die('Wrong token.'); // Go away!
+            if (!$sessionManager->checkToken($_POST['token'])) {
+                die(t('Wrong token.')); // Go away!
             }
             $tz = 'UTC';
             if (!empty($_POST['continent']) && !empty($_POST['city'])
@@ -1163,6 +1155,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
             $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
             $conf->set('api.enabled', !empty($_POST['enableApi']));
             $conf->set('api.secret', escape($_POST['apiSecret']));
+            $conf->set('translation.language', escape($_POST['language']));
+
             try {
                 $conf->write(isLoggedIn());
                 $history->updateSettings();
@@ -1178,7 +1172,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
                 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
                 exit;
             }
-            echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>';
+            echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
             exit;
         }
         else // Show the configuration form.
@@ -1200,6 +1194,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
             $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
             $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
             $PAGE->assign('api_secret', $conf->get('api.secret'));
+            $PAGE->assign('languages', Languages::getAvailableLanguages());
+            $PAGE->assign('language', $conf->get('translation.language'));
             $PAGE->renderPage('configure');
             exit;
         }
@@ -1214,8 +1210,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
             exit;
         }
 
-        if (!tokenOk($_POST['token'])) {
-            die('Wrong token.');
+        if (!$sessionManager->checkToken($_POST['token'])) {
+            die(t('Wrong token.'));
         }
 
         $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
@@ -1225,9 +1221,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
         }
         $delete = empty($_POST['totag']);
         $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
+        $count = count($alteredLinks);
         $alert = $delete
-            ? sprintf(t('The tag was removed from %d links.'), count($alteredLinks))
-            : sprintf(t('The tag was renamed in %d links.'), count($alteredLinks));
+            ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
+            : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
         echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
         exit;
     }
@@ -1243,8 +1240,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
     if (isset($_POST['save_edit']))
     {
         // Go away!
-        if (! tokenOk($_POST['token'])) {
-            die('Wrong token.');
+        if (! $sessionManager->checkToken($_POST['token'])) {
+            die(t('Wrong token.'));
         }
 
         // lf_id should only be present if the link exists.
@@ -1343,8 +1340,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
     // -------- User clicked the "Delete" button when editing a link: Delete link from database.
     if ($targetPage == Router::$PAGE_DELETELINK)
     {
-        if (! tokenOk($_GET['token'])) {
-            die('Wrong token.');
+        if (! $sessionManager->checkToken($_GET['token'])) {
+            die(t('Wrong token.'));
         }
 
         $ids = trim($_GET['lf_linkdate']);
@@ -1437,7 +1434,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
 
             if ($url == '') {
                 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
-                $title = 'Note: ';
+                $title = $conf->get('general.default_note_title', t('Note: '));
             }
             $url = escape($url);
             $title = escape($title);
@@ -1544,14 +1541,17 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
         // Import bookmarks from an uploaded file
         if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
             // The file is too big or some form field may be missing.
-            echo '<script>alert("The file you are trying to upload is probably'
-                .' bigger than what this webserver can accept ('
-                .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
-                .' Please upload in smaller chunks.");document.location=\'?do='
-                .Router::$PAGE_IMPORT .'\';</script>';
+            $msg = sprintf(
+                t(
+                    'The file you are trying to upload is probably bigger than what this webserver can accept'
+                    .' (%s). Please upload in smaller chunks.'
+                ),
+                get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
+            );
+            echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
             exit;
         }
-        if (! tokenOk($_POST['token'])) {
+        if (! $sessionManager->checkToken($_POST['token'])) {
             die('Wrong token.');
         }
         $status = NetscapeBookmarkUtils::import(
@@ -1618,7 +1618,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
     // Get a fresh token
     if ($targetPage == Router::$GET_TOKEN) {
         header('Content-Type:text/plain');
-        echo getToken($conf);
+        echo $sessionManager->generateToken($conf);
         exit;
     }
 
@@ -1690,7 +1690,11 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
     while ($i<$end && $i<count($keys))
     {
         $link = $linksToDisplay[$keys[$i]];
-        $link['description'] = format_description($link['description'], $conf->get('redirector.url'));
+        $link['description'] = format_description(
+            $link['description'],
+            $conf->get('redirector.url'),
+            $conf->get('redirector.encode_url')
+        );
         $classLi =  ($i % 2) != 0 ? '' : 'publicLinkHightLight';
         $link['class'] = $link['private'] == 0 ? $classLi : 'private';
         $link['timestamp'] = $link['created']->getTimestamp();
@@ -1944,10 +1948,10 @@ function lazyThumbnail($conf, $url,$href=false)
  * Installation
  * This function should NEVER be called if the file data/config.php exists.
  *
- * @param ConfigManager $conf Configuration Manager instance.
+ * @param ConfigManager  $conf           Configuration Manager instance.
+ * @param SessionManager $sessionManager SessionManager instance
  */
-function install($conf)
-{
+function install($conf, $sessionManager) {
     // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
     if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
 
@@ -1956,12 +1960,20 @@ function install($conf)
     // (Because on some hosts, session.save_path may not be set correctly,
     // or we may not have write access to it.)
     if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working'))
-    {   // Step 2: Check if data in session is correct.
-        echo '<pre>Sessions do not seem to work correctly on your server.<br>';
-        echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>';
-        echo 'It currently points to '.session_save_path().'<br>';
-        echo 'Check that the hostname used to access Shaarli contains a dot. On some browsers, accessing your server via a hostname like \'localhost\' or any custom hostname without a dot causes cookie storage to fail. We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>';
-        echo '<br><a href="?">Click to try again.</a></pre>';
+    {
+        // Step 2: Check if data in session is correct.
+        $msg = t(
+            '<pre>Sessions do not seem to work correctly on your server.<br>'.
+            'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
+            'and that you have write access to it.<br>'.
+            'It currently points to %s.<br>'.
+            'On some browsers, accessing your server via a hostname like \'localhost\' '.
+            'or any custom hostname without a dot causes cookie storage to fail. '.
+            'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
+        );
+        $msg = sprintf($msg, session_save_path());
+        echo $msg;
+        echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
         die;
     }
     if (!isset($_SESSION['session_tested']))
@@ -1994,6 +2006,7 @@ function install($conf)
         } else {
             $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
         }
+        $conf->set('translation.language', escape($_POST['language']));
         $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
         $conf->set('api.enabled', !empty($_POST['enableApi']));
         $conf->set(
@@ -2021,10 +2034,11 @@ function install($conf)
         exit;
     }
 
-    $PAGE = new PageBuilder($conf);
+    $PAGE = new PageBuilder($conf, null, $sessionManager->generateToken());
     list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
     $PAGE->assign('continents', $continents);
     $PAGE->assign('cities', $cities);
+    $PAGE->assign('languages', Languages::getAvailableLanguages());
     $PAGE->renderPage('install');
     exit;
 }
@@ -2297,7 +2311,7 @@ $response = $app->run(true);
 if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
     // We use UTF-8 for proper international characters handling.
     header('Content-Type: text/html; charset=utf-8');
-    renderPage($conf, $pluginManager, $linkDb, $history);
+    renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager);
 } else {
     $app->respond($response);
 }
index 03a7a34e80e3f448e1e8bbffec77cbd76490dc29..443c3a08d4059e0dc0c10757da49290d6bed3a89 100644 (file)
@@ -22,16 +22,15 @@ pages:
     - Reverse proxy configuration: docker/reverse-proxy-configuration.md
     - Docker resources: docker/resources.md
 - Usage:
-    - Features: Features.md
     - Bookmarklet: Bookmarklet.md
     - Browsing and searching: Browsing-and-searching.md
     - Firefox share: Firefox-share.md
     - RSS feeds: RSS-feeds.md 
     - REST API: REST-API.md
+    - Community & Related software: Community-&-Related-software.md
 - How To:
     - Backup, restore, import and export: Backup,-restore,-import-and-export.md
     - Various hacks: Various-hacks.md
-- Troubleshooting: Troubleshooting.md
 - Development:
     - Development guidelines: Development-guidelines.md
     - Continuous integration tools: Continuous-integration-tools.md
@@ -43,9 +42,9 @@ pages:
     - Versioning and Branches: Versioning-and-Branches.md
     - Security: Security.md
     - Static analysis: Static-analysis.md
+    - Translations: Translations.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
+- FAQ: FAQ.md
+- Troubleshooting: Troubleshooting.md
diff --git a/plugins/TODO.md b/plugins/TODO.md
deleted file mode 100644 (file)
index e3313d6..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-https://github.com/shaarli/Shaarli/issues/181 -  Add Disqus or Isso comments box on a permalink page  
-
- * http://posativ.org/isso/
- * install debian package https://packages.debian.org/sid/isso  
- * configure server http://posativ.org/isso/docs/configuration/server/  
- * configure client http://posativ.org/isso/docs/configuration/client/  
- * http://posativ.org/isso/docs/quickstart/ and add `<script data-isso="//comments.example.tld/" src="//comments.example.tld/js/embed.min.js"></script>` to includes.html template; then add `<section id="isso-thread"></section>` in the linklist template where you want the comments (in the linklist_plugins loop for example)
-
-Problem: by default, Isso thread ID is guessed from the current url (only one thread per page).  
-if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config,
-with data-isso-id being the permalink of an item.
-
-`<section data-isso-id="aH7klxW" id="isso-thread"></section>` 
-`data-isso-id: Set a custom thread id, defaults to current URI.`
-
-Problem: feature is currently broken https://github.com/posativ/isso/issues/27 
-
-Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread
-when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment
-count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done
-by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server.
-
-Showing all full isso threads in the linklist would destroy layout
-
------------------------------------------------------------
-
-http://www.git-attitude.fr/2014/11/04/git-rerere/ for the merge
index ddf50aaf3aba2b2eccd116030d355a7678f8914b..8c05a23176ba960f8efaf61f5194a679bb1f7f36 100644 (file)
@@ -26,11 +26,11 @@ function hook_addlink_toolbar_render_header($data)
                 array(
                     'type' => 'text',
                     'name' => 'post',
-                    'placeholder' => 'URI',
+                    'placeholder' => t('URI'),
                 ),
                 array(
                     'type' => 'submit',
-                    'value' => 'Add link',
+                    'value' => t('Add link'),
                     'class' => 'bigbutton',
                 ),
             ),
@@ -40,3 +40,12 @@ function hook_addlink_toolbar_render_header($data)
 
     return $data;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function addlink_toolbar_dummy_translation()
+{
+    // meta
+    t('Adds the addlink input on the linklist page.');
+}
index 0781fe35d045631971f304533496f1bec82ddcc2..ad501f4799b5770b6bd6f77c550c49317ac55bbe 100644 (file)
@@ -1 +1,5 @@
-<span><a href="https://web.archive.org/web/%s"><img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="View on archive.org" alt="archive.org" /></a></span>
+<span>
+  <a href="https://web.archive.org/web/%s">
+    <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
+  </a>
+</span>
index 03d13d0efb2db403c0d2bef225f66546cee18ae3..cda35751041933c6caffd061c59121da5833c24f 100644 (file)
@@ -20,9 +20,18 @@ function hook_archiveorg_render_linklist($data)
         if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
             continue;
         }
-        $archive = sprintf($archive_html, $value['url']);
+        $archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
         $value['link_plugin'][] = $archive;
     }
 
     return $data;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function archiveorg_dummy_translation()
+{
+    // meta
+    t('For each link, add an Archive.org icon.');
+}
index 8fdbf66383c144226770cc03334701801e7f5da8..b80a2b6d5175bfb57d8dcf9774c4336357807c5b 100644 (file)
  * and check user status with _LOGGEDIN_.
  */
 
+use Shaarli\Config\ConfigManager;
+
+/**
+ * In the footer hook, there is a working example of a translation extension for Shaarli.
+ *
+ * The extension must be attached to a new translation domain (i.e. NOT 'shaarli').
+ * Use case: any custom theme or non official plugin can use the translation system.
+ *
+ * See the documentation for more information.
+ */
+const EXT_TRANSLATION_DOMAIN = 'demo';
+
+/*
+ * This is not necessary, but it's easier if you don't want Poedit to mix up your translations.
+ */
+function demo_plugin_t($text, $nText = '', $nb = 1)
+{
+    return t($text, $nText, $nb, EXT_TRANSLATION_DOMAIN);
+}
+
 /**
  * Initialization function.
  * It will be called when the plugin is loaded.
@@ -27,6 +47,12 @@ function demo_plugin_init($conf)
 {
     $conf->get('toto', 'nope');
 
+    if (! $conf->exists('translation.extensions.demo')) {
+        // Custom translation with the domain 'demo'
+        $conf->set('translation.extensions.demo', 'plugins/demo_plugin/languages/');
+        $conf->write(true);
+    }
+
     $errors[] = 'This a demo init error.';
     return $errors;
 }
@@ -160,7 +186,7 @@ function hook_demo_plugin_render_includes($data)
 function hook_demo_plugin_render_footer($data)
 {
     // footer text
-    $data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.';
+    $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
 
     // Free elements at the end of the page.
     $data['endofpage'][] = '<marquee id="demo_marquee">' .
@@ -433,3 +459,12 @@ function hook_demo_plugin_render_feed($data)
     }
     return $data;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function demo_dummy_translation()
+{
+    // meta
+    t('A demo plugin covering all use cases for template designers and plugin developers.');
+}
diff --git a/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo
new file mode 100644 (file)
index 0000000..0f80f6e
Binary files /dev/null and b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo differ
diff --git a/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po
new file mode 100644 (file)
index 0000000..921379c
--- /dev/null
@@ -0,0 +1,21 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Demo plugin\n"
+"POT-Creation-Date: 2017-08-19 10:45+0200\n"
+"PO-Revision-Date: 2017-08-19 11:28+0200\n"
+"Last-Translator: \n"
+"Language-Team: demo\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.0.2\n"
+"X-Poedit-Basepath: ../../..\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Poedit-KeywordsList: ;demo_plugin_t:1,2;demo_plugin_t\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: demo_plugin.php:173
+msgid "Shaarli is now enhanced by the awesome demo_plugin."
+msgstr "Shaarli est maintenant amélioré avec le fantastique demo_plugin."
index ce16645f9ad72f15121c511e7a2077aa65c3983f..5bc1cce26e79d1be3cc64fa3bda9109a12c2ea3f 100644 (file)
@@ -4,10 +4,11 @@
  * Plugin Isso.
  */
 
+use Shaarli\Config\ConfigManager;
+
 /**
  * Display an error everywhere if the plugin is enabled without configuration.
  *
- * @param $data array         List of links
  * @param $conf ConfigManager instance
  *
  * @return mixed - linklist data with Isso plugin.
@@ -16,8 +17,8 @@ function isso_init($conf)
 {
     $issoUrl = $conf->get('plugins.ISSO_SERVER');
     if (empty($issoUrl)) {
-        $error = 'Isso plugin error: '.
-            'Please define the "ISSO_SERVER" setting in the plugin administration page.';
+        $error = t('Isso plugin error: '.
+            'Please define the "ISSO_SERVER" setting in the plugin administration page.');
         return array($error);
     }
 }
@@ -52,3 +53,13 @@ function hook_isso_render_linklist($data, $conf)
 
     return $data;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function isso_dummy_translation()
+{
+    // meta
+    t('Let visitor comment your shaares on permalinks with Isso.');
+    t('Isso server URL (without \'http://\')');
+}
index 9c4e5ae06c1a42d446cd27f8bcc66e946169f82d..ded3d3472394f14237957eb69602fa421f2aaaaa 100644 (file)
@@ -1,5 +1,5 @@
 <div class="md_help">
-    Description will be rendered with
-    <a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation">
-        Markdown syntax</a>.
+    %s
+    <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
+        %s</a>.
 </div>
index 772c56e8e2295ed2eab29364a84eebec7305bf5c..1531549d8f613e594b6fd36259e3e825932ea43e 100644 (file)
@@ -154,8 +154,13 @@ function hook_markdown_render_includes($data)
 function hook_markdown_render_editlink($data)
 {
     // Load help HTML into a string
-    $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
-
+    $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
+    $translations = [
+        t('Description will be rendered with'),
+        t('Markdown syntax documentation'),
+        t('Markdown syntax'),
+    ];
+    $data['edit_link_plugin'][] = vsprintf($txt, $translations);
     // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
     if (! in_array(NO_MD_TAG, $data['tags'])) {
         $data['tags'][NO_MD_TAG] = 0;
@@ -325,3 +330,15 @@ function process_markdown($description, $escape = true, $allowedProtocols = [])
 
     return $processedDescription;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function markdown_dummy_translation()
+{
+    // meta
+    t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
+If your shaared descriptions contained HTML tags before enabling the markdown plugin,
+enabling it might break your page.
+See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
+}
index 4a2b48a121fcd4249fe2c257beb35fb28150d16e..ca00c2be71f33dd566be43fb2fb2f791d41e26cf 100644 (file)
@@ -18,8 +18,8 @@ function piwik_init($conf)
     $piwikUrl = $conf->get('plugins.PIWIK_URL');
     $piwikSiteid = $conf->get('plugins.PIWIK_SITEID');
     if (empty($piwikUrl) || empty($piwikSiteid)) {
-        $error = 'Piwik plugin error: ' .
-            'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.';
+        $error = t('Piwik plugin error: ' .
+            'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.');
         return array($error);
     }
 }
@@ -60,3 +60,14 @@ function hook_piwik_render_footer($data, $conf)
 
     return $data;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function piwik_dummy_translation()
+{
+    // meta
+    t('A plugin that adds Piwik tracking code to Shaarli pages.');
+    t('Piwik URL');
+    t('Piwik site ID');
+}
index 644845049ff6599407fc43f9c5d99a9c0d4efd81..c6d6b0cc6815e4f5c2547efdc395a67ccde08e3d 100644 (file)
@@ -19,10 +19,10 @@ function hook_playvideos_render_header($data)
         $playvideo = array(
             'attr' => array(
                 'href' => '#',
-                'title' => 'Video player',
+                'title' => t('Video player'),
                 'id' => 'playvideos',
             ),
-            'html' => 'â–º Play Videos'
+            'html' => 'â–º '. t('Play Videos')
         );
         $data['buttons_toolbar'][] = $playvideo;
     }
@@ -46,3 +46,12 @@ function hook_playvideos_render_footer($data)
 
     return $data;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function playvideos_dummy_translation()
+{
+    // meta
+    t('Add a button in the toolbar allowing to watch all videos.');
+}
index 03b6757bd3687f9f46c2402c72beb6f8641392bb..184b588b7f44d62b89df0fc2f0e07094da0e8b41 100644 (file)
@@ -10,6 +10,7 @@
  */
 
 use pubsubhubbub\publisher\Publisher;
+use Shaarli\Config\ConfigManager;
 
 /**
  * Plugin init function - set the hub to the default appspot one.
@@ -65,7 +66,7 @@ function hook_pubsubhubbub_save_link($data, $conf)
         $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL'));
         $p->publish_update($feeds, $httpPost);
     } catch (Exception $e) {
-        error_log('Could not publish to PubSubHubbub: ' . $e->getMessage());
+        error_log(sprintf(t('Could not publish to PubSubHubbub: %s'), $e->getMessage()));
     }
 
     return $data;
@@ -91,11 +92,20 @@ function nocurl_http_post($url, $postString) {
     $context = stream_context_create($params);
     $fp = @fopen($url, 'rb', false, $context);
     if (!$fp) {
-        throw new Exception('Could not post to '. $url);
+        throw new Exception(sprintf(t('Could not post to %s'), $url));
     }
     $response = @stream_get_contents($fp);
     if ($response === false) {
-        throw new Exception('Bad response from the hub '. $url);
+        throw new Exception(sprintf(t('Bad response from the hub %s'), $url));
     }
     return $response;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function pubsubhubbub_dummy_translation()
+{
+    // meta
+    t('Enable PubSubHubbub feed publishing.');
+}
index cbf371ea21c78b0667572aadc45a40817f04204e..1812cd21154709feca5ce069e4b659c24316d1b3 100644 (file)
@@ -1 +1 @@
-description="For each link, add a QRCode icon ."
+description="For each link, add a QRCode icon."
index 8bc610d1fad120ec34694c4d3c1a3c7501f14e64..0f96a106921e856eff1160cc0b881c1b68b2e75d 100644 (file)
@@ -59,3 +59,12 @@ function hook_qrcode_render_includes($data)
 
     return $data;
 }
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function qrcode_dummy_translation()
+{
+    // meta
+    t('For each link, add a QRCode icon.');
+}
index e861536d53866fe4fe7f2a47182f3442043a5b84..4c57691d0387705cb5e0ebacc1334cb175b45e3a 100644 (file)
@@ -1 +1,5 @@
-<span><a href="%s%s" target="_blank"><img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="Save to wallabag" alt="wallabag" /></a></span>
+<span>
+  <a href="%s%s" target="_blank">
+    <img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="%s" alt="wallabag" />
+  </a>
+</span>
index 641e4cc237ed4f20a44fafda7eae1b871bf2e118..9dfd079eb21da9d027dc71a3aacfb9fe5d0608a0 100644 (file)
@@ -5,6 +5,7 @@
  */
 
 require_once 'WallabagInstance.php';
+use Shaarli\Config\ConfigManager;
 
 /**
  * Init function, return an error if the server is not set.
@@ -17,8 +18,8 @@ function wallabag_init($conf)
 {
     $wallabagUrl = $conf->get('plugins.WALLABAG_URL');
     if (empty($wallabagUrl)) {
-        $error = 'Wallabag plugin error: '.
-            'Please define the "WALLABAG_URL" setting in the plugin administration page.';
+        $error = t('Wallabag plugin error: '.
+            'Please define the "WALLABAG_URL" setting in the plugin administration page.');
         return array($error);
     }
 }
@@ -43,12 +44,14 @@ function hook_wallabag_render_linklist($data, $conf)
 
     $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
 
+    $linkTitle = t('Save to wallabag');
     foreach ($data['links'] as &$value) {
         $wallabag = sprintf(
             $wallabagHtml,
             $wallabagInstance->getWallabagUrl(),
             urlencode($value['url']),
-            PluginManager::$PLUGINS_PATH
+            PluginManager::$PLUGINS_PATH,
+            $linkTitle
         );
         $value['link_plugin'][] = $wallabag;
     }
@@ -56,3 +59,14 @@ function hook_wallabag_render_linklist($data, $conf)
     return $data;
 }
 
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function wallabag_dummy_translation()
+{
+    // meta
+    t('For each link, add a QRCode icon.');
+    t('Wallabag API URL');
+    t('Wallabag API version (1 or 2)');
+}
+
index dac02b3e77cab58cdf05a57fbecfa3cabae1247b..324b827acc11e9612c933fa89893dfd318f523a3 100644 (file)
@@ -186,4 +186,36 @@ class ServerUrlTest extends PHPUnit_Framework_TestCase
             )
         );
     }
+
+    /**
+     * Misconfigured server (see #1022): Proxy HTTP but 443
+     */
+    public function testHttpWithPort433()
+    {
+        $this->assertEquals(
+            'https://host.tld',
+            server_url(
+                array(
+                    'HTTPS' => 'Off',
+                    'SERVER_NAME' => 'host.tld',
+                    'SERVER_PORT' => '80',
+                    'HTTP_X_FORWARDED_PROTO' => 'http',
+                    'HTTP_X_FORWARDED_PORT' => '443'
+                )
+            )
+        );
+
+        $this->assertEquals(
+            'https://host.tld',
+            server_url(
+                array(
+                    'HTTPS' => 'Off',
+                    'SERVER_NAME' => 'host.tld',
+                    'SERVER_PORT' => '80',
+                    'HTTP_X_FORWARDED_PROTO' => 'https, http',
+                    'HTTP_X_FORWARDED_PORT' => '443, 80'
+                )
+            )
+        );
+    }
 }
index 79c136c88d5dea79ab89f1ebf24603685fc27245..864ce63060de1b9fb0c98f4599d5e40fe5064170 100644 (file)
 <?php
 
-require_once 'application/Languages.php';
+namespace Shaarli;
+
+use Shaarli\Config\ConfigManager;
 
 /**
  * Class LanguagesTest.
  */
-class LanguagesTest extends PHPUnit_Framework_TestCase
+class LanguagesTest extends \PHPUnit_Framework_TestCase
 {
+    /**
+     * @var string Config file path (without extension).
+     */
+    protected static $configFile = 'tests/utils/config/configJson';
+
+    /**
+     * @var ConfigManager
+     */
+    protected $conf;
+
+    /**
+     *
+     */
+    public function setUp()
+    {
+        $this->conf = new ConfigManager(self::$configFile);
+    }
+
+    /**
+     * Test t() with a simple non identified value.
+     */
+    public function testTranslateSingleNotIDGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('en', $this->conf);
+        $text = 'abcdé 564 fgK';
+        $this->assertEquals($text, t($text));
+    }
+
+    /**
+     * Test t() with a simple identified value in gettext mode.
+     */
+    public function testTranslateSingleIDGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('en', $this->conf);
+        $text = 'permalink';
+        $this->assertEquals($text, t($text));
+    }
+
+    /**
+     * Test t() with a non identified plural form in gettext mode.
+     */
+    public function testTranslatePluralNotIDGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('en', $this->conf);
+        $text = 'sandwich';
+        $nText = 'sandwiches';
+        $this->assertEquals('sandwiches', t($text, $nText, 0));
+        $this->assertEquals('sandwich', t($text, $nText, 1));
+        $this->assertEquals('sandwiches', t($text, $nText, 2));
+    }
+
+    /**
+     * Test t() with an identified plural form in gettext mode.
+     */
+    public function testTranslatePluralIDGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('en', $this->conf);
+        $text = 'shaare';
+        $nText = 'shaares';
+        // In english, zero is followed by plural form
+        $this->assertEquals('shaares', t($text, $nText, 0));
+        $this->assertEquals('shaare', t($text, $nText, 1));
+        $this->assertEquals('shaares', t($text, $nText, 2));
+    }
+
     /**
      * Test t() with a simple non identified value.
      */
-    public function testTranslateSingleNotID()
+    public function testTranslateSingleNotIDPhp()
     {
+        $this->conf->set('translation.mode', 'php');
+        new Languages('en', $this->conf);
         $text = 'abcdé 564 fgK';
         $this->assertEquals($text, t($text));
     }
 
     /**
-     * Test t() with a non identified plural form.
+     * Test t() with a simple identified value in PHP mode.
      */
-    public function testTranslatePluralNotID()
+    public function testTranslateSingleIDPhp()
     {
-        $text = '%s sandwich';
-        $nText = '%s sandwiches';
-        $this->assertEquals('0 sandwich', t($text, $nText));
-        $this->assertEquals('1 sandwich', t($text, $nText, 1));
-        $this->assertEquals('2 sandwiches', t($text, $nText, 2));
+        $this->conf->set('translation.mode', 'php');
+        new Languages('en', $this->conf);
+        $text = 'permalink';
+        $this->assertEquals($text, t($text));
     }
 
     /**
-     * Test t() with a non identified invalid plural form.
+     * Test t() with a non identified plural form in PHP mode.
      */
-    public function testTranslatePluralNotIDInvalid()
+    public function testTranslatePluralNotIDPhp()
     {
+        $this->conf->set('translation.mode', 'php');
+        new Languages('en', $this->conf);
         $text = 'sandwich';
         $nText = 'sandwiches';
+        $this->assertEquals('sandwiches', t($text, $nText, 0));
         $this->assertEquals('sandwich', t($text, $nText, 1));
         $this->assertEquals('sandwiches', t($text, $nText, 2));
     }
+
+    /**
+     * Test t() with an identified plural form in PHP mode.
+     */
+    public function testTranslatePluralIDPhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        new Languages('en', $this->conf);
+        $text = 'shaare';
+        $nText = 'shaares';
+        // In english, zero is followed by plural form
+        $this->assertEquals('shaares', t($text, $nText, 0));
+        $this->assertEquals('shaare', t($text, $nText, 1));
+        $this->assertEquals('shaares', t($text, $nText, 2));
+    }
+
+    /**
+     * Test t() with an invalid language set in the configuration in gettext mode.
+     */
+    public function testTranslateWithInvalidConfLanguageGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        $this->conf->set('translation.language', 'nope');
+        new Languages('fr', $this->conf);
+        $text = 'grumble';
+        $this->assertEquals($text, t($text));
+    }
+
+    /**
+     * Test t() with an invalid language set in the configuration in PHP mode.
+     */
+    public function testTranslateWithInvalidConfLanguagePhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        $this->conf->set('translation.language', 'nope');
+        new Languages('fr', $this->conf);
+        $text = 'grumble';
+        $this->assertEquals($text, t($text));
+    }
+
+    /**
+     * Test t() with an invalid language set with auto language in gettext mode.
+     */
+    public function testTranslateWithInvalidAutoLanguageGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('nope', $this->conf);
+        $text = 'grumble';
+        $this->assertEquals($text, t($text));
+    }
+
+    /**
+     * Test t() with an invalid language set with auto language in PHP mode.
+     */
+    public function testTranslateWithInvalidAutoLanguagePhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        new Languages('nope', $this->conf);
+        $text = 'grumble';
+        $this->assertEquals($text, t($text));
+    }
+
+    /**
+     * Test t() with an extension language file in gettext mode
+     */
+    public function testTranslationExtensionGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
+        new Languages('en', $this->conf);
+        $txt = 'car'; // ignore me poedit
+        $this->assertEquals('car', t($txt, $txt, 1, 'test'));
+        $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
+    }
+
+    /**
+     * Test t() with an extension language file in PHP mode
+     */
+    public function testTranslationExtensionPhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
+        new Languages('en', $this->conf);
+        $txt = 'car'; // ignore me poedit
+        $this->assertEquals('car', t($txt, $txt, 1, 'test'));
+        $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
+    }
 }
index d796d3a301c469652bbb8bcc6295db0e539417cb..9cd6dbd443b2a52b492cd165d12155c06aa14f49 100644 (file)
@@ -7,6 +7,10 @@ require_once 'application/LinkFilter.php';
  */
 class LinkFilterTest extends PHPUnit_Framework_TestCase
 {
+    /**
+     * @var string Test datastore path.
+     */
+    protected static $testDatastore = 'sandbox/datastore.php';
     /**
      * @var LinkFilter instance.
      */
@@ -17,13 +21,20 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
      */
     protected static $refDB;
 
+    /**
+     * @var LinkDB instance
+     */
+    protected static $linkDB;
+
     /**
      * Instanciate linkFilter with ReferenceLinkDB data.
      */
     public static function setUpBeforeClass()
     {
         self::$refDB = new ReferenceLinkDB();
-        self::$linkFilter = new LinkFilter(self::$refDB->getLinks());
+        self::$refDB->write(self::$testDatastore);
+        self::$linkDB = new LinkDB(self::$testDatastore, true, false);
+        self::$linkFilter = new LinkFilter(self::$linkDB);
     }
 
     /**
index ef650f448d382fd2e4ab7ab7959186f831c255c9..7fbd59b0b80489d7b9e3521664e8aeb7d204b19d 100644 (file)
@@ -214,6 +214,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);
     }
 
     /**
@@ -231,6 +241,21 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
         $this->assertEquals($expectedText, $processedText);
     }
 
+    /**
+     * Test text2clickable a redirector set and without URL encode.
+     */
+    public function testText2clickableWithRedirectorDontEncode()
+    {
+        $text = 'stuff http://hello.there/?is=someone&or=something#here otherstuff';
+        $redirector = 'http://redirector.to';
+        $expectedText = 'stuff <a href="'.
+            $redirector .
+            'http://hello.there/?is=someone&or=something#here' .
+            '">http://hello.there/?is=someone&or=something#here</a> otherstuff';
+        $processedText = text2clickable($text, $redirector, false);
+        $this->assertEquals($expectedText, $processedText);
+    }
+
     /**
      * Test testSpace2nbsp.
      */
index 5fc1d1e830da9204ea62c0a17b8e9b4058ebcf46..4961aa2c4f73166d069144776cb574db9a537f67 100644 (file)
@@ -132,8 +132,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
     public function testImportInternetExplorerEncoding()
     {
         $files = file2array('internet_explorer_encoding.htm');
-        $this->assertEquals(
-            'File internet_explorer_encoding.htm (356 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
             .' 1 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -161,8 +161,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
     public function testImportNested()
     {
         $files = file2array('netscape_nested.htm');
-        $this->assertEquals(
-            'File netscape_nested.htm (1337 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
             .' 8 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -283,8 +283,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
     public function testImportDefaultPrivacyNoPost()
     {
         $files = file2array('netscape_basic.htm');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -328,8 +328,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
     {
         $post = array('privacy' => 'default');
         $files = file2array('netscape_basic.htm');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -372,8 +372,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
     {
         $post = array('privacy' => 'public');
         $files = file2array('netscape_basic.htm');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -396,8 +396,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
     {
         $post = array('privacy' => 'private');
         $files = file2array('netscape_basic.htm');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -422,8 +422,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
 
         // import links as private
         $post = array('privacy' => 'private');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -442,8 +442,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
             'privacy' => 'public',
             'overwrite' => 'true'
         );
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 2 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -468,8 +468,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
 
         // import links as public
         $post = array('privacy' => 'public');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -489,8 +489,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
             'privacy' => 'private',
             'overwrite' => 'true'
         );
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 2 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -513,8 +513,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
     {
         $post = array('privacy' => 'public');
         $files = file2array('netscape_basic.htm');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -523,8 +523,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
 
         // re-import as private, DO NOT enable overwriting
         $post = array('privacy' => 'private');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 0 links imported, 0 links overwritten, 2 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -542,8 +542,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
             'default_tags' => 'tag1,tag2 tag3'
         );
         $files = file2array('netscape_basic.htm');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -569,8 +569,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
             'default_tags' => 'tag1&,tag2 "tag3"'
         );
         $files = file2array('netscape_basic.htm');
-        $this->assertEquals(
-            'File netscape_basic.htm (482 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -594,8 +594,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
     public function testImportSameDate()
     {
         $files = file2array('same_date.htm');
-        $this->assertEquals(
-            'File same_date.htm (453 bytes) was successfully processed:'
+        $this->assertStringMatchesFormat(
+            'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
             .' 3 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history)
         );
@@ -622,24 +622,19 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
             'overwrite' => 'true',
         ];
         $files = file2array('netscape_basic.htm');
-        $nbLinks = 2;
         NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
         $history = $this->history->getHistory();
-        $this->assertEquals($nbLinks, count($history));
-        foreach ($history as $value) {
-            $this->assertEquals(History::CREATED, $value['event']);
-            $this->assertTrue(new DateTime('-5 seconds') < $value['datetime']);
-            $this->assertTrue(is_int($value['id']));
-        }
+        $this->assertEquals(1, count($history));
+        $this->assertEquals(History::IMPORT, $history[0]['event']);
+        $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
 
         // re-import as private, enable overwriting
         NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
         $history = $this->history->getHistory();
-        $this->assertEquals($nbLinks * 2, count($history));
-        for ($i = 0 ; $i < $nbLinks ; $i++) {
-            $this->assertEquals(History::UPDATED, $history[$i]['event']);
-            $this->assertTrue(new DateTime('-5 seconds') < $history[$i]['datetime']);
-            $this->assertTrue(is_int($history[$i]['id']));
-        }
+        $this->assertEquals(2, count($history));
+        $this->assertEquals(History::IMPORT, $history[0]['event']);
+        $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
+        $this->assertEquals(History::IMPORT, $history[1]['event']);
+        $this->assertTrue(new DateTime('-5 seconds') < $history[1]['datetime']);
     }
 }
diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php
new file mode 100644 (file)
index 0000000..aa75962
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+require_once 'tests/utils/FakeConfigManager.php';
+
+// Initialize reference data _before_ PHPUnit starts a session
+require_once 'tests/utils/ReferenceSessionIdHashes.php';
+ReferenceSessionIdHashes::genAllHashes();
+
+use \Shaarli\SessionManager;
+use \PHPUnit\Framework\TestCase;
+
+
+/**
+ * Test coverage for SessionManager
+ */
+class SessionManagerTest extends TestCase
+{
+    // Session ID hashes
+    protected static $sidHashes = null;
+
+    // Fake ConfigManager
+    protected static $conf = null;
+
+    /**
+     * Assign reference data
+     */
+    public static function setUpBeforeClass()
+    {
+        self::$sidHashes = ReferenceSessionIdHashes::getHashes();
+        self::$conf = new FakeConfigManager();
+    }
+
+    /**
+     * Generate a session token
+     */
+    public function testGenerateToken()
+    {
+        $session = [];
+        $sessionManager = new SessionManager($session, self::$conf);
+
+        $token = $sessionManager->generateToken();
+
+        $this->assertEquals(1, $session['tokens'][$token]);
+        $this->assertEquals(40, strlen($token));
+    }
+
+    /**
+     * Check a session token
+     */
+    public function testCheckToken()
+    {
+        $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
+        $session = [
+            'tokens' => [
+                $token => 1,
+            ],
+        ];
+        $sessionManager = new SessionManager($session, self::$conf);
+
+        // check and destroy the token
+        $this->assertTrue($sessionManager->checkToken($token));
+        $this->assertFalse(isset($session['tokens'][$token]));
+
+        // ensure the token has been destroyed
+        $this->assertFalse($sessionManager->checkToken($token));
+    }
+
+    /**
+     * Generate and check a session token
+     */
+    public function testGenerateAndCheckToken()
+    {
+        $session = [];
+        $sessionManager = new SessionManager($session, self::$conf);
+
+        $token = $sessionManager->generateToken();
+
+        // ensure a token has been generated
+        $this->assertEquals(1, $session['tokens'][$token]);
+        $this->assertEquals(40, strlen($token));
+
+        // check and destroy the token
+        $this->assertTrue($sessionManager->checkToken($token));
+        $this->assertFalse(isset($session['tokens'][$token]));
+
+        // ensure the token has been destroyed
+        $this->assertFalse($sessionManager->checkToken($token));
+    }
+
+    /**
+     * Check an invalid session token
+     */
+    public function testCheckInvalidToken()
+    {
+        $session = [];
+        $sessionManager = new SessionManager($session, self::$conf);
+
+        $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
+    }
+
+    /**
+     * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
+     *
+     * This tests extensively covers all hash algorithms / bit representations
+     */
+    public function testIsAnyHashSessionIdValid()
+    {
+        foreach (self::$sidHashes as $algo => $bpcs) {
+            foreach ($bpcs as $bpc => $hash) {
+                $this->assertTrue(SessionManager::checkId($hash));
+            }
+        }
+    }
+
+    /**
+     * Test checkId with a valid ID - SHA-1 hashes
+     */
+    public function testIsSha1SessionIdValid()
+    {
+        $this->assertTrue(SessionManager::checkId(sha1('shaarli')));
+    }
+
+    /**
+     * Test checkId with a valid ID - SHA-256 hashes
+     */
+    public function testIsSha256SessionIdValid()
+    {
+        $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
+    }
+
+    /**
+     * Test checkId with a valid ID - SHA-512 hashes
+     */
+    public function testIsSha512SessionIdValid()
+    {
+        $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
+    }
+
+    /**
+     * Test checkId with invalid IDs.
+     */
+    public function testIsSessionIdInvalid()
+    {
+        $this->assertFalse(SessionManager::checkId(''));
+        $this->assertFalse(SessionManager::checkId([]));
+        $this->assertFalse(
+            SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
+        );
+    }
+}
index 3d1aa6538918f8ad48dc5436b91dd03253a3eadd..6cd37a7afa27836249af244559fe23c93748e2bd 100644 (file)
@@ -5,10 +5,6 @@
 
 require_once 'application/Utils.php';
 require_once 'application/Languages.php';
-require_once 'tests/utils/ReferenceSessionIdHashes.php';
-
-// Initialize reference data before PHPUnit starts a session
-ReferenceSessionIdHashes::genAllHashes();
 
 
 /**
@@ -16,9 +12,6 @@ ReferenceSessionIdHashes::genAllHashes();
  */
 class UtilsTest extends PHPUnit_Framework_TestCase
 {
-    // Session ID hashes
-    protected static $sidHashes = null;
-
     // Log file
     protected static $testLogFile = 'tests.log';
 
@@ -30,13 +23,11 @@ class UtilsTest extends PHPUnit_Framework_TestCase
      */
     protected static $defaultTimeZone;
 
-
     /**
      * Assign reference data
      */
     public static function setUpBeforeClass()
     {
-        self::$sidHashes = ReferenceSessionIdHashes::getHashes();
         self::$defaultTimeZone = date_default_timezone_get();
         // Timezone without DST for test consistency
         date_default_timezone_set('Africa/Nairobi');
@@ -221,56 +212,7 @@ class UtilsTest extends PHPUnit_Framework_TestCase
         $this->assertEquals('?', generateLocation($ref, 'localhost'));
     }
 
-    /**
-     * Test is_session_id_valid with a valid ID - TEST ALL THE HASHES!
-     *
-     * This tests extensively covers all hash algorithms / bit representations
-     */
-    public function testIsAnyHashSessionIdValid()
-    {
-        foreach (self::$sidHashes as $algo => $bpcs) {
-            foreach ($bpcs as $bpc => $hash) {
-                $this->assertTrue(is_session_id_valid($hash));
-            }
-        }
-    }
 
-    /**
-     * Test is_session_id_valid with a valid ID - SHA-1 hashes
-     */
-    public function testIsSha1SessionIdValid()
-    {
-        $this->assertTrue(is_session_id_valid(sha1('shaarli')));
-    }
-
-    /**
-     * Test is_session_id_valid with a valid ID - SHA-256 hashes
-     */
-    public function testIsSha256SessionIdValid()
-    {
-        $this->assertTrue(is_session_id_valid(hash('sha256', 'shaarli')));
-    }
-
-    /**
-     * Test is_session_id_valid with a valid ID - SHA-512 hashes
-     */
-    public function testIsSha512SessionIdValid()
-    {
-        $this->assertTrue(is_session_id_valid(hash('sha512', 'shaarli')));
-    }
-
-    /**
-     * Test is_session_id_valid with invalid IDs.
-     */
-    public function testIsSessionIdInvalid()
-    {
-        $this->assertFalse(is_session_id_valid(''));
-        $this->assertFalse(is_session_id_valid(array()));
-        $this->assertFalse(
-            is_session_id_valid('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
-        );
-    }
-    
     /**
      * Test generateSecretApi.
      */
@@ -384,18 +326,18 @@ class UtilsTest extends PHPUnit_Framework_TestCase
      */
     public function testHumanBytes()
     {
-        $this->assertEquals('2kiB', human_bytes(2 * 1024));
-        $this->assertEquals('2kiB', human_bytes(strval(2 * 1024)));
-        $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2))));
-        $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2)))));
-        $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3))));
-        $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3)))));
-        $this->assertEquals('374B', human_bytes(374));
-        $this->assertEquals('374B', human_bytes('374'));
-        $this->assertEquals('232kiB', human_bytes(237481));
-        $this->assertEquals('Unlimited', human_bytes('0'));
-        $this->assertEquals('Unlimited', human_bytes(0));
-        $this->assertEquals('Setting not set', human_bytes(''));
+        $this->assertEquals('2'. t('kiB'), human_bytes(2 * 1024));
+        $this->assertEquals('2'. t('kiB'), human_bytes(strval(2 * 1024)));
+        $this->assertEquals('2'. t('MiB'), human_bytes(2 * (pow(1024, 2))));
+        $this->assertEquals('2'. t('MiB'), human_bytes(strval(2 * (pow(1024, 2)))));
+        $this->assertEquals('2'. t('GiB'), human_bytes(2 * (pow(1024, 3))));
+        $this->assertEquals('2'. t('GiB'), human_bytes(strval(2 * (pow(1024, 3)))));
+        $this->assertEquals('374'. t('B'), human_bytes(374));
+        $this->assertEquals('374'. t('B'), human_bytes('374'));
+        $this->assertEquals('232'. t('kiB'), human_bytes(237481));
+        $this->assertEquals(t('Unlimited'), human_bytes('0'));
+        $this->assertEquals(t('Unlimited'), human_bytes(0));
+        $this->assertEquals(t('Setting not set'), human_bytes(''));
     }
 
     /**
@@ -403,9 +345,9 @@ class UtilsTest extends PHPUnit_Framework_TestCase
      */
     public function testGetMaxUploadSize()
     {
-        $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k'));
-        $this->assertEquals('1MiB', get_max_upload_size('1m', '2m'));
-        $this->assertEquals('100B', get_max_upload_size(100, 100));
+        $this->assertEquals('1'. t('MiB'), get_max_upload_size(2097152, '1024k'));
+        $this->assertEquals('1'. t('MiB'), get_max_upload_size('1m', '2m'));
+        $this->assertEquals('100'. t('B'), get_max_upload_size(100, 100));
     }
 
     /**
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644 (file)
index 0000000..d36d73c
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+
+require_once 'vendor/autoload.php';
+
+$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
+new \Shaarli\Languages('en', $conf);
index 95609210f30ec1f66c17fa97c3196a63a01119a2..da6ac2e4e2ed3c86bec9d8a615cfd6b8ff4496a3 100644 (file)
@@ -1,7 +1,6 @@
 <?php
-if (! empty('UT_LOCALE')) {
+require_once 'tests/bootstrap.php';
+
+if (! empty(getenv('UT_LOCALE'))) {
     setlocale(LC_ALL, getenv('UT_LOCALE'));
 }
-
-require_once 'vendor/autoload.php';
-
diff --git a/tests/languages/fr/LanguagesFrTest.php b/tests/languages/fr/LanguagesFrTest.php
new file mode 100644 (file)
index 0000000..79d0517
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+
+
+namespace Shaarli;
+
+
+use Shaarli\Config\ConfigManager;
+
+/**
+ * Class LanguagesFrTest
+ *
+ * Test the translation system in PHP and gettext mode with French language.
+ *
+ * @package Shaarli
+ */
+class LanguagesFrTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @var string Config file path (without extension).
+     */
+    protected static $configFile = 'tests/utils/config/configJson';
+
+    /**
+     * @var ConfigManager
+     */
+    protected $conf;
+
+    /**
+     * Init: force French
+     */
+    public function setUp()
+    {
+        $this->conf = new ConfigManager(self::$configFile);
+        $this->conf->set('translation.language', 'fr');
+    }
+
+    /**
+     * Reset the locale since gettext seems to mess with it, making it too long
+     */
+    public static function tearDownAfterClass()
+    {
+        if (! empty(getenv('UT_LOCALE'))) {
+            setlocale(LC_ALL, getenv('UT_LOCALE'));
+        }
+    }
+
+    /**
+     * Test t() with a simple non identified value.
+     */
+    public function testTranslateSingleNotIDGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('en', $this->conf);
+        $text = 'abcdé 564 fgK';
+        $this->assertEquals($text, t($text));
+    }
+
+    /**
+     * Test t() with a simple identified value in gettext mode.
+     */
+    public function testTranslateSingleIDGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('en', $this->conf);
+        $text = 'permalink';
+        $this->assertEquals('permalien', t($text));
+    }
+
+    /**
+     * Test t() with a non identified plural form in gettext mode.
+     */
+    public function testTranslatePluralNotIDGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('en', $this->conf);
+        $text = 'sandwich';
+        $nText = 'sandwiches';
+        // Not ID, so English fallback, and in english, plural 0
+        $this->assertEquals('sandwiches', t($text, $nText, 0));
+        $this->assertEquals('sandwich', t($text, $nText, 1));
+        $this->assertEquals('sandwiches', t($text, $nText, 2));
+    }
+
+    /**
+     * Test t() with an identified plural form in gettext mode.
+     */
+    public function testTranslatePluralIDGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        new Languages('en', $this->conf);
+        $text = 'shaare';
+        $nText = 'shaares';
+        $this->assertEquals('shaare', t($text, $nText, 0));
+        $this->assertEquals('shaare', t($text, $nText, 1));
+        $this->assertEquals('shaares', t($text, $nText, 2));
+    }
+
+    /**
+     * Test t() with a simple non identified value.
+     */
+    public function testTranslateSingleNotIDPhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        new Languages('en', $this->conf);
+        $text = 'abcdé 564 fgK';
+        $this->assertEquals($text, t($text));
+    }
+
+    /**
+     * Test t() with a simple identified value in PHP mode.
+     */
+    public function testTranslateSingleIDPhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        new Languages('en', $this->conf);
+        $text = 'permalink';
+        $this->assertEquals('permalien', t($text));
+    }
+
+    /**
+     * Test t() with a non identified plural form in PHP mode.
+     */
+    public function testTranslatePluralNotIDPhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        new Languages('en', $this->conf);
+        $text = 'sandwich';
+        $nText = 'sandwiches';
+        // Not ID, so English fallback, and in english, plural 0
+        $this->assertEquals('sandwiches', t($text, $nText, 0));
+        $this->assertEquals('sandwich', t($text, $nText, 1));
+        $this->assertEquals('sandwiches', t($text, $nText, 2));
+    }
+
+    /**
+     * Test t() with an identified plural form in PHP mode.
+     */
+    public function testTranslatePluralIDPhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        new Languages('en', $this->conf);
+        $text = 'shaare';
+        $nText = 'shaares';
+        // In english, zero is followed by plural form
+        $this->assertEquals('shaare', t($text, $nText, 0));
+        $this->assertEquals('shaare', t($text, $nText, 1));
+        $this->assertEquals('shaares', t($text, $nText, 2));
+    }
+
+    /**
+     * Test t() with an extension language file in gettext mode
+     */
+    public function testTranslationExtensionGettext()
+    {
+        $this->conf->set('translation.mode', 'gettext');
+        $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
+        new Languages('en', $this->conf);
+        $txt = 'car'; // ignore me poedit
+        $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
+        $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
+    }
+
+    /**
+     * Test t() with an extension language file in PHP mode
+     */
+    public function testTranslationExtensionPhp()
+    {
+        $this->conf->set('translation.mode', 'php');
+        $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
+        new Languages('en', $this->conf);
+        $txt = 'car'; // ignore me poedit
+        $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
+        $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
+    }
+}
diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php
new file mode 100644 (file)
index 0000000..f29760c
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * Fake ConfigManager
+ */
+class FakeConfigManager
+{
+    public static function get($key)
+    {
+        return $key;
+    }
+}
index f09eebc13b26f701ecc8944602794bc22adbc219..e887aa78c2251747c46cf37fab4359c782b32b36 100644 (file)
@@ -141,12 +141,34 @@ class ReferenceLinkDB
      */
     public function write($filename)
     {
+        $this->reorder();
         file_put_contents(
             $filename,
             '<?php /* '.base64_encode(gzdeflate(serialize($this->_links))).' */ ?>'
         );
     }
 
+    /**
+     * Reorder links by creation date (newest first).
+     *
+     * Also update the urls and ids mapping arrays.
+     *
+     * @param string $order ASC|DESC
+     */
+    public function reorder($order = 'DESC')
+    {
+        // backward compatibility: ignore reorder if the the `created` field doesn't exist
+        if (! isset(array_values($this->_links)[0]['created'])) {
+            return;
+        }
+
+        $order = $order === 'ASC' ? -1 : 1;
+        // Reorder array by dates.
+        usort($this->_links, function($a, $b) use ($order) {
+            return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
+        });
+    }
+
     /**
      * Returns the number of links in the reference data
      */
@@ -187,6 +209,7 @@ class ReferenceLinkDB
 
     public function getLinks()
     {
+        $this->reorder();
         return $this->_links;
     }
 
diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.mo b/tests/utils/languages/fr/LC_MESSAGES/test.mo
new file mode 100644 (file)
index 0000000..416c783
Binary files /dev/null and b/tests/utils/languages/fr/LC_MESSAGES/test.mo differ
diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.po b/tests/utils/languages/fr/LC_MESSAGES/test.po
new file mode 100644 (file)
index 0000000..89a4fd9
--- /dev/null
@@ -0,0 +1,19 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Extension test\n"
+"POT-Creation-Date: 2017-05-20 13:54+0200\n"
+"PO-Revision-Date: 2017-05-20 14:16+0200\n"
+"Last-Translator: \n"
+"Language-Team: Shaarli\n"
+"Language: fr_FR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Generator: Poedit 2.0.1\n"
+
+msgid "car"
+msgstr "voiture"
+
+msgid "Search"
+msgstr "Fouille"
index 49dd20d951952082202e213a8433db1f619c6da2..6606c4fa6bacb6495f2185ee3b03798520fdde42 100644 (file)
@@ -32,7 +32,7 @@
       </div>
     </form>
 
-    <p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p>
+    <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p>
   </div>
 </div>
 {include="page.footer"}
index 76a1b9fd506f9e6bf17cb0cd520d5a9694da9bac..a63c7ad33a6d8402423aebe90d51b5357c11de17 100644 (file)
           </div>
         </div>
       </div>
+      <div class="pure-g">
+        <div class="pure-u-lg-{$ratioLabel} pure-u-1">
+          <div class="form-label">
+            <label for="language">
+              <span class="label-name">{'Language'|t}</span>
+            </label>
+          </div>
+        </div>
+        <div class="pure-u-lg-{$ratioInput} pure-u-1">
+          <div class="form-input">
+            <select name="language" id="language" class="align">
+              {loop="$languages"}
+                <option value="{$key}"
+                      {if="$key===$language"}
+                      selected="selected"
+                      {/if}
+                >
+                  {$value}
+                </option>
+              {/loop}
+            </select>
+          </div>
+        </div>
+      </div>
       <div class="pure-g">
         <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
           <div class="form-label">
           </div>
         </div>
       </div>
-      <div class="pure-g">
-        <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
-          <div class="form-label">
-            <label for="redirector">
-              <span class="label-name">{'Redirector'|t}</span><br>
-              <span class="label-desc">{'e. g.'|t} <i>http://anonym.to/?</i> {'will mask the HTTP_REFERER'|t}</span>
-            </label>
-          </div>
-        </div>
-        <div class="pure-u-lg-{$ratioInput} pure-u-1 ">
-          <div class="form-input">
-            <input type="text" name="redirector" id="redirector" size="50" value="{$redirector}">
-          </div>
-        </div>
-      </div>
       <div class="clear"></div>
       <div class="pure-g">
         <div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile} ">
index ba589723b3c999e1361f6cab0cc8ebbaf3e830c1..14439402f26237b9694add9ba9e7205dd1e61e81 100644 (file)
@@ -433,7 +433,7 @@ body, .pure-g [class*="pure-u"] {
  * 64em -> lg
  */
 .linklist-filters {
-    margin: 10px 0;
+    margin: 5px 0;
     color: #252525;
     font-size: 0.9em;
 }
@@ -454,7 +454,7 @@ body, .pure-g [class*="pure-u"] {
 }
 
 .linklist-pages {
-    margin: 10px 0;
+    margin: 5px 0;
     color: #252525;
     text-align: center;
 }
@@ -469,7 +469,7 @@ body, .pure-g [class*="pure-u"] {
 }
 
 .linksperpage {
-    margin: 10px 0;
+    margin: 5px 0;
     text-align: right;
     color: #252525;
     font-size: 0.9em;
@@ -506,9 +506,29 @@ body, .pure-g [class*="pure-u"] {
  * CONTENT - LINKLIST ITEMS
  */
 .linklist-item {
-    margin: 0 0 15px 0;
+    margin: 0 0 10px 0;
     background: #f5f5f5;
-    box-shadow: 2px 2px 0.5em #797979;
+    box-shadow: 1px 1px 3px #797979;
+}
+
+.linklist-item-buttons {
+    background: transparent;
+    position: relative;
+    width: 23px;
+    z-index: 99;
+}
+
+.linklist-item-buttons-right {
+    float: right;
+    margin-right: -25px;
+}
+
+.linklist-item-buttons * {
+    display: block;
+    float: left;
+    width:100%;
+    margin: auto;
+    text-align: center;
 }
 
 .linklist-item-title, .linklist-item-title h2  {
@@ -526,7 +546,7 @@ body, .pure-g [class*="pure-u"] {
     line-height: 30px;
 }
 
-.linklist-item-title a {
+.linklist-item-title h2 a {
     font-size: 0.7em;
     color: #252525;
     text-decoration: none;
@@ -538,11 +558,11 @@ body, .pure-g [class*="pure-u"] {
     color: #1b926c;
 }
 
-.linklist-item-title a:visited .linklist-link {
+.linklist-item-title h2 a:visited .linklist-link {
     color: #2a4c41;
 }
 
-.linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{
+.linklist-item-title h2 a:hover, .linklist-item-title .linklist-link:hover{
     color: #252525;
 }
 
@@ -554,8 +574,9 @@ body, .pure-g [class*="pure-u"] {
     color: #F89406;
 }
 
-.linklist-item-title .fold-button {
+.fold-button {
     display: none;
+    color: #252525;
 }
 
 .linklist-item-editbuttons {
@@ -585,24 +606,12 @@ body, .pure-g [class*="pure-u"] {
 
 .linklist-item-description {
     position: relative;
-    padding: 10px;
+    padding: 10px;
     word-wrap: break-word;
     color: #252525;
     line-height: 1.3em;
 }
 
- {
-    position: absolute;
-    left: 3px;
-    top: 0;
-    display: block;
-    content:"";
-    background: #F89406;
-    height: 95%;
-    width: 2px;
-    z-index: 1;
-}
-
 .linklist-item-description a {
     text-decoration: none;
     color: #1b926c;
@@ -618,32 +627,36 @@ body, .pure-g [class*="pure-u"] {
 
 .linklist-item-thumbnail {
     position: relative;
-    margin-top: 10px;
-    padding: 10px;
-    float: left;
+    padding: 0 0 0 5px;
+    margin: 0;
+    float: right;
     z-index: 50;
+    height: 90px;
 }
 
 .linklist-item.private .linklist-item-title::before,
-.linklist-item.private .linklist-item-description::before,
-.linklist-item.private .linklist-item-thumbnail::before {
+.linklist-item.private .linklist-item-description::before {
     position: absolute;
     left: 3px;
     top: 0;
     display: block;
     content:"";
     background: #F89406;
-    height: 95%;
+    height: 96%;
     width: 2px;
     z-index: 1;
 }
 
+.linklist-item.private .linklist-item-description::before {
+    height: 100%;
+}
+
 .linklist-item.private .linklist-item-title::before {
     margin-top: 3px;
 }
 
 .linklist-item-infos {
-    padding: 8px 8px 5px 8px;
+    padding: 4px 8px 4px 8px;
     background: #ddd;
     color: #252525;
 }
@@ -680,6 +693,8 @@ body, .pure-g [class*="pure-u"] {
     overflow: hidden;
     text-overflow: ellipsis;
     font-size: 0.8em;
+    height:23px;
+    line-height:23px;
 }
 
 .linklist-item-infos .mobile-buttons {
@@ -693,6 +708,16 @@ body, .pure-g [class*="pure-u"] {
     height: 16px;
 }
 
+.linklist-item-infos-controls-group {
+    display: inline-block;
+    border-right: 1px solid #5d5d5d;
+    padding-right: 6px;
+}
+
+.ctrl-edit {
+    margin: 0 7px;
+}
+
 /** 64em -> lg **/
 @media screen and (max-width: 64em) {
     .linklist-item-infos-url {
@@ -1284,3 +1309,40 @@ form[name="linkform"].page-form {
     text-decoration: none;
     font-weight: bold;
 }
+
+/**
+ * Markdown
+ */
+.markdown p {
+    margin: 0 !important;
+}
+
+.markdown p + p {
+    margin: 0.5em 0 0 0 !important;
+}
+
+.markdown *:first-child {
+    margin-top: 0 !important;
+}
+
+.markdown *:last-child {
+    margin-bottom: 5px !important;
+}
+
+/**
+ * Pure Button
+ */
+.pure-button-success,
+.pure-button-error,
+.pure-button-warning,
+.pure-button-primary,
+.pure-button-shaarli,
+.pure-button-secondary {
+    color: white !important;
+    border-radius: 4px;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+}
+
+.pure-button-shaarli {
+    background-color: #1B926C;
+}
diff --git a/tpl/default/img/apple-touch-icon.png b/tpl/default/img/apple-touch-icon.png
new file mode 100644 (file)
index 0000000..f29210c
Binary files /dev/null and b/tpl/default/img/apple-touch-icon.png differ
index 1f040685caa5195f0a2e9a41ec199bf720bee178..000a50ac3d069ee9223f345faf4d0a9fc1170dd4 100644 (file)
@@ -18,7 +18,7 @@
       <div class="center" id="import-field">
         <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
         <input type="file" name="filetoupload">
-        <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p>
+        <p><br>{'Maximum size allowed:'|t} <strong>{$maxfilesizeHuman}</strong></p>
       </div>
 
       <div class="pure-g">
           <div class="radio-buttons">
             <div>
               <input type="radio" name="privacy" value="default" checked="checked">
-              Use values from the imported file, default to public
+              {'Use values from the imported file, default to public'|t}
             </div>
             <div>
               <input type="radio" name="privacy" value="private">
-              Import all bookmarks as private
+              {'Import all bookmarks as private'|t}
             </div>
             <div>
               <input type="radio" name="privacy" value="public">
-              Import all bookmarks as public
+              {'Import all bookmarks as public'|t}
             </div>
           </div>
         </div>
index 0350ef6681371e7a24263e29c8e29be8d2b5cef7..b2bfec30918281104cacf6c50cae5441fcc26903 100644 (file)
@@ -5,16 +5,17 @@
 <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 href="img/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
+<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
+<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/>
index 164d453b1d6c162bfa6b5a28b9203dbdfdb10d3e..6199b33d6499bfbf988c65ccc16dc00880c78e51 100644 (file)
       </div>
     </div>
 
+    <div class="pure-g">
+      <div class="pure-u-lg-{$ratioLabel} pure-u-1">
+        <div class="form-label">
+          <label for="language">
+            <span class="label-name">{'Language'|t}</span>
+          </label>
+        </div>
+      </div>
+      <div class="pure-u-lg-{$ratioInput} pure-u-1">
+        <div class="form-input">
+          <select name="language" id="language" class="align">
+            {loop="$languages"}
+              <option value="{$key}">
+                {$value}
+              </option>
+            {/loop}
+          </select>
+        </div>
+      </div>
+    </div>
+
     <div class="pure-g">
       <div class="pure-u-lg-{$ratioLabel} pure-u-1">
         <div class="form-label">
index 1c66ebbdd708f033c5ae26e747e3f313137edee7..cf628e87fc4f15e0b9d1c71a42b5e14994e613f1 100644 (file)
@@ -138,6 +138,9 @@ window.onload = function () {
                 });
                 foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
                 foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
+                foldAllButton.title = state === 'down'
+                    ? document.getElementById('translation-fold-all').innerHTML
+                    : document.getElementById('translation-expand-all').innerHTML
             });
         });
     }
@@ -146,7 +149,7 @@ window.onload = function () {
     {
         // Switch fold/expand - up = fold
         if (button.classList.contains('fa-chevron-up')) {
-            button.title = 'Expand';
+            button.title = document.getElementById('translation-expand').innerHTML;
             if (description != null) {
                 description.style.display = 'none';
             }
@@ -155,7 +158,7 @@ window.onload = function () {
             }
         }
         else {
-            button.title = 'Fold';
+            button.title = document.getElementById('translation-fold').innerHTML;
             if (description != null) {
                 description.style.display = 'block';
             }
@@ -173,7 +176,7 @@ window.onload = function () {
     var deleteLinks = document.querySelectorAll('.confirm-delete');
     [].forEach.call(deleteLinks, function(deleteLink) {
         deleteLink.addEventListener('click', function(event) {
-            if(! confirm('Are you sure you want to delete this link ?')) {
+            if(! confirm(document.getElementById('translation-delete-link').innerHTML)) {
                 event.preventDefault();
             }
         });
@@ -275,8 +278,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 () {
@@ -369,7 +378,7 @@ window.onload = function () {
     var linkCheckboxes = document.querySelectorAll('.delete-checkbox');
     var bar = document.getElementById('actions');
     [].forEach.call(linkCheckboxes, function(checkbox) {
-        checkbox.style.display = 'block';
+        checkbox.style.display = 'inline-block';
         checkbox.addEventListener('click', function(event) {
             var count = 0;
             var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
@@ -612,7 +621,7 @@ function activateFirefoxSocial(node) {
     // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
     var data = {
         name: title,
-        description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
+        description: document.getElementById('translation-delete-link').innerHTML,
         author: "Shaarli",
         version: "1.0.0",
 
index 685821e395bfb184208ad2a96c6fd583220bc4d4..c666e30a80d6cc698e80492c8847f7e52d0f55e9 100644 (file)
@@ -53,9 +53,9 @@
 {/loop}
 
 <div id="linklist">
-  <div class="pure-g">
+  <div id="link-count-block" class="pure-g">
     <div class="pure-u-lg-2-24 pure-u-1-24"></div>
-    <div class="pure-u-lg-20-24 pure-u-22-24">
+    <div id="link-count-content" class="pure-u-lg-20-24 pure-u-22-24">
       <div class="linkcount pure-u-lg-0 center">
         {if="!empty($linkcount)"}
         <span class="strong">{$linkcount}</span> {function="t('shaare', 'shaares', $linkcount)"}
   </div>
 
   {if="count($links)==0"}
-    <div class="pure-g pure-alert pure-alert-error search-result">
+    <div id="search-result-block" class="pure-g pure-alert pure-alert-error search-result">
       <div class="pure-u-2-24"></div>
-      <div class="pure-u-20-24">
+      <div id="search-result-content" class="pure-u-20-24">
         <div id="searchcriteria">{'Nothing found.'|t}</div>
       </div>
     </div>
   {elseif="!empty($search_term) or $search_tags !== '' or !empty($visibility) or $untaggedonly"}
-    <div class="pure-g pure-alert pure-alert-success search-result">
+    <div id="search-result-block" class="pure-g pure-alert pure-alert-success search-result">
       <div class="pure-u-2-24"></div>
-      <div class="pure-u-20-24">
-        {function="t('%s result', '%s results', $result_count)"}
+      <div id="search-result-content" class="pure-u-20-24 search-result-main">
+        {function="sprintf(t('%s result', '%s results', $result_count), $result_count)"}
         {if="!empty($search_term)"}
           {'for'|t} <em><strong>{$search_term}</strong></em>
         {/if}
     </div>
   {/if}
 
-  <div class="pure-g">
+  <div id="linklist-loop-block" class="pure-g">
     <div class="pure-u-lg-2-24 pure-u-1-24"></div>
-    <div class="pure-u-lg-20-24 pure-u-22-24">
+    <div id="linklist-loop-content" class="pure-u-lg-20-24 pure-u-22-24">
+      {ignore}Set translation here, for performances{/ignore}
+      {$strPrivate=t('Private')}
+      {$strEdit=t('Edit')}
+      {$strDelete=t('Delete')}
+      {$strFold=t('Fold')}
+      {$strEdited=t('Edited: ')}
+      {$strPermalink=t('Permalink')}
+      {$strPermalinkLc=t('permalink')}
+      {$strAddTag=t('Add tag')}
+      {ignore}End of translations{/ignore}
       {loop="links"}
         <div class="anchor" id="{$value.shorturl}"></div>
-        <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
 
+        <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
           <div class="linklist-item-title">
+            {$thumb=thumbnail($value.url)}
+            {if="$thumb!=false"}
+              <div class="linklist-item-thumbnail">{$thumb}</div>
+            {/if}
+
             {if="isLoggedIn()"}
               <div class="linklist-item-editbuttons">
                 {if="$value.private"}
-                  <span class="label label-private">{'Private'|t}</span>
+                  <span class="label label-private">{$strPrivate}</span>
                 {/if}
-                <input type="checkbox" class="delete-checkbox" value="{$value.id}">
-                <!-- FIXME! JS translation -->
-                <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a>
-                <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
               </div>
             {/if}
 
             </h2>
           </div>
 
-          {$thumb=thumbnail($value.url)}
-          {if="$thumb!=false"}
-            <div class="linklist-item-thumbnail">{$thumb}</div>
-          {/if}
-
           {if="$value.description"}
             <div class="linklist-item-description">
               {$value.description}
                 <i class="fa fa-tags"></i>
                 {$tag_counter=count($value.taglist)}
                 {loop="value.taglist"}
-                  <span class="label label-tag" title="Add tag">
+                  <span class="label label-tag" title="{$strAddTag}">
                     <a href="?addtag={$value|urlencode}">{$value}</a>
                   </span>
                   {if="$tag_counter - 1 != $counter"}&middot;{/if}
               </div>
             {/if}
 
-            <div class="pure-g">
-              <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1">
-                <a href="?{$value.shorturl}" title="{'Permalink'|t}">
+            <div class="linklist-item-infos-date-url-block pure-g">
+              <div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1">
+                {if="isLoggedIn()"}
+                  <div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible">
+                    <span class="linklist-item-infos-controls-item ctrl-checkbox">
+                      <input type="checkbox" class="delete-checkbox" value="{$value.id}">
+                    </span>
+                    <span class="linklist-item-infos-controls-item ctrl-edit">
+                      <a href="?edit_link={$value.id}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link"></i></a>
+                    </span>
+                    <span class="linklist-item-infos-controls-item ctrl-delete">
+                      <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
+                         title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
+                        <i class="fa fa-trash"></i>
+                      </a>
+                    </span>
+                  </div>
+                {/if}
+                <a href="?{$value.shorturl}" title="{$strPermalink}">
                   {if="!$hide_timestamps || isLoggedIn()"}
-                    {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
+                    {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
                     <span class="linkdate" title="{$updated}">
                       <i class="fa fa-clock-o"></i>
                       {$value.created|format_date}
                       &middot;
                     </span>
                   {/if}
-                  {'permalink'|t}
+                  {$strPermalinkLc}
                 </a>
 
                 <div class="pure-u-0 pure-u-lg-visible">
                 </div>
               </div><div
                 {ignore}do not add space or line break between these div - Firefox issue{/ignore}
-                class="linklist-item-infos-url pure-u-lg-5-8 pure-u-1">
+                class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1">
                 <a href="{$value.real_url}" title="{$value.title}">
                   <i class="fa fa-link"></i> {$value.url}
                 </a>
-                {if="isLoggedIn()"}
-                  <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
-                     title="{'Delete'|t}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
-                    <i class="fa fa-trash"></i>
-                  </a>
-                {/if}
+                <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible">
+                  <a href="#" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
+                </div>
               </div>
               <div class="mobile-buttons pure-u-1 pure-u-lg-0">
                 {if="isset($value.link_plugin)"}
                 {if="isLoggedIn()"}
                   &middot;
                   <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
-                     title="{'Delete'|t}" class="delete-link confirm-delete">
+                     title="{$strDelete}" class="delete-link confirm-delete">
                     <i class="fa fa-trash"></i>
                   </a>
+                  &middot;
+                  <a href="?edit_link={$value.id}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link"></i></a>
                 {/if}
               </div>
             </div>
     {/loop}
   </div>
 
-<div class="pure-g">
+<div id="linklist-paging-bottom-block" class="pure-g">
   <div class="pure-u-lg-2-24 pure-u-1-24"></div>
-  <div class="pure-u-lg-20-24 pure-u-22-24">
+  <div id="linklist-paging-bottom-content" class="pure-u-lg-20-24 pure-u-22-24">
     {include="linklist.paging"}
   </div>
 </div>
index 41e9fa342bc87139fa26d5b130079a40c8bb97bb..347b3d13be87a4e42da796a5a029aac0983c74d7 100644 (file)
@@ -13,7 +13,7 @@
         <a href="?untaggedonly" title="{'Filter untagged links'|t}"
            class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
         ><i class="fa fa-tag"></i></a>
-        <a href="#" class="filter-off fold-all pure-u-lg-0" title="Fold all">
+        <a href="#" class="filter-off fold-all pure-u-lg-0" title="{'Fold all'|t}">
           <i class="fa fa-chevron-up"></i>
         </a>
         {loop="$action_plugin"}
@@ -53,7 +53,7 @@
       <form method="GET" class="pure-u-0 pure-u-lg-visible">
         <input type="text" name="linksperpage" placeholder="133">
       </form>
-      <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="Fold all">
+      <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="{'Fold all'|t}">
         <i class="fa fa-chevron-up"></i>
       </a>
     </div>
index 94f771a250c40583cac13bdae5f8b08a65976eaa..659e8c7fdfcb5b58122694a090bfc81c7c975f0d 100644 (file)
@@ -8,8 +8,8 @@
       {$version}
     {/if}
     &middot;
-    The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community &middot;
-    <a href="doc/html/index.html" rel="nofollow">Documentation</a>
+    {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot;
+    <a href="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
       {loop="$plugins_footer.text"}
           {$value}
       {/loop}
        <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>
+<div id="js-translations" class="hidden">
+  <span id="translation-fold">{'Fold'|t}</span>
+  <span id="translation-fold-all">{'Fold all'|t}</span>
+  <span id="translation-expand">{'Expand'|t}</span>
+  <span id="translation-expand-all">{'Expand all'|t}</span>
+  <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span>
+  <span id="translation-shaarli-desc">
+    {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t}
+  </span>
+</div>
+
+<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 2411703c7e7925aa3106ef7510cf26a1888d6b0c..6f15c1c5adfd5ee4a2cb5e83306fd9c6b470b165 100644 (file)
@@ -1,7 +1,7 @@
 <div class="shaarli-menu pure-g" id="shaarli-menu">
   <div class="pure-u-lg-0 pure-u-1">
     <div class="pure-menu">
-      <a href="{$titleLink}" class="pure-menu-link">
+      <a href="{$titleLink}" class="pure-menu-link shaarli-title" id="shaarli-title-mobile">
         <img src="img/icon.png" width="16" height="16" class="head-logo" alt="logo" />
         {$shaarlititle}
       </a>
     <div class="pure-menu menu-transform pure-menu-horizontal pure-g">
       <ul class="pure-menu-list pure-u-lg-5-6 pure-u-1">
         <li class="pure-menu-item pure-u-0 pure-u-lg-visible">
-          <a href="{$titleLink}" class="pure-menu-link">
+          <a href="{$titleLink}" class="pure-menu-link shaarli-title" id="shaarli-title-desktop">
             <img src="img/icon.png" width="16" height="16" class="head-logo" alt="logo" />
             {$shaarlititle}
           </a>
         </li>
         {if="isLoggedIn() || $openshaarli"}
           <li class="pure-menu-item">
-            <a href="?do=addlink" class="pure-menu-link">
+            <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare">
               <i class="fa fa-plus" ></i> {'Shaare'|t}
             </a>
           </li>
-          <li class="pure-menu-item">
+          <li class="pure-menu-item" id="shaarli-menu-tools">
             <a href="?do=tools" class="pure-menu-link">{'Tools'|t}</a>
           </li>
         {/if}
-        <li class="pure-menu-item">
+        <li class="pure-menu-item" id="shaarli-menu-tags">
           <a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a>
         </li>
-        <li class="pure-menu-item">
+        <li class="pure-menu-item" id="shaarli-menu-picwall">
           <a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a>
         </li>
-        <li class="pure-menu-item">
+        <li class="pure-menu-item" id="shaarli-menu-daily">
           <a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a>
         </li>
         {loop="$plugins_header.buttons_toolbar"}
-          <li class="pure-menu-item">
+          <li class="pure-menu-item shaarli-menu-plugin">
             <a
               {$value.attr.class=isset($value.class) ? $value.attr.class . ' pure-menu-link' : 'pure-menu-link'}
               {loop="$value.attr"}
             </a>
           </li>
         {/loop}
-        <li class="pure-menu-item pure-u-lg-0">
+        <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
             <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
         </li>
         {if="isLoggedIn()"}
-          <li class="pure-menu-item pure-u-lg-0">
+          <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
             <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a>
           </li>
         {else}
-          <li class="pure-menu-item pure-u-lg-0">
+          <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login">
             <a href="?do=login" class="pure-menu-link">{'Login'|t}</a>
           </li>
         {/if}
       </ul>
       <div class="header-buttons pure-u-lg-1-6 pure-u-0 pure-u-lg-visible">
         <ul class="pure-menu-list">
-          <li class="pure-menu-item">
+          <li class="pure-menu-item" id="shaarli-menu-desktop-search">
             <a href="#" class="pure-menu-link subheader-opener"
                data-open-id="search"
                id="search-button" title="{'Search'|t}">
               <i class="fa fa-search"></i>
             </a>
           </li>
-          <li class="pure-menu-item">
+          <li class="pure-menu-item" id="shaarli-menu-desktop-rss">
             <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}">
               <i class="fa fa-rss"></i>
             </a>
           </li>
           {if="!isLoggedIn()"}
-          <li class="pure-menu-item">
-            <a href="?do=login" class="pure-menu-link"
-               data-open-id="header-login-form"
-               id="login-button" title="{'Login'|t}">
-              <i class="fa fa-user"></i>
-            </a>
-          </li>
+            <li class="pure-menu-item" id="shaarli-menu-desktop-login">
+              <a href="?do=login" class="pure-menu-link"
+                 data-open-id="header-login-form"
+                 id="login-button" title="{'Login'|t}">
+                <i class="fa fa-user"></i>
+              </a>
+            </li>
           {else}
-          <li class="pure-menu-item">
-            <a href="?do=logout" class="pure-menu-link" title="{'Logout'|t}">
-              <i class="fa fa-sign-out"></i>
-            </a>
-          </li>
+            <li class="pure-menu-item" id="shaarli-menu-desktop-logout">
+              <a href="?do=logout" class="pure-menu-link" title="{'Logout'|t}">
+                <i class="fa fa-sign-out"></i>
+              </a>
+            </li>
           {/if}
         </ul>
       </div>
 {/if}
 
 {if="!empty($plugin_errors) && isLoggedIn()"}
-  <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable">
+  <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
     <div class="pure-u-2-24"></div>
     <div class="pure-u-20-24">
         {loop="plugin_errors"}
index 5cc1802f77dff26790ced7d1c77ce52c176c7438..b2d7cdc5900fff5967f6d0f6acc9afee1c7c0920 100644 (file)
@@ -27,7 +27,7 @@
 
         <div>
           {if="count($enabledPlugins)==0"}
-          <p>{'No plugin enabled.'|t}</p>
+            <p class="center">{'No plugin enabled.'|t}</p>
           {else}
           <table id="plugin_table">
             <thead>
@@ -77,7 +77,7 @@
 
         <div>
           {if="count($disabledPlugins)==0"}
-          <p>{'No plugin disabled.'|t}</p>
+            <p class="center">{'No plugin disabled.'|t}</p>
           {else}
           <table>
             <thead>
       </section>
 
       <div class="center more">
-        More plugins available
-        <a href="doc/Community-&-Related-software.html#third-party-plugins">in the documentation</a>.
+        {"More plugins available"|t}
+        <a href="doc/Community-&-Related-software.html#third-party-plugins">{"in the documentation"|t}</a>.
       </div>
       <div class="center">
         <input type="submit" value="{'Save'|t}" name="save">
       <section id="plugin_parameters">
         <div>
           {if="count($enabledPlugins)==0"}
-            <p>{'No plugin enabled.'|t}</p>
+            <p class="center">{'No plugin enabled.'|t}</p>
           {else}
+            {$nbParameters=0}
             {loop="$enabledPlugins"}
+              {$nbParameters=$nbParameters+count($value.parameters)}
               {if="count($value.parameters) > 0"}
                 <div class="plugin_parameters">
                   <h3 class="window-subtitle">{function="str_replace('_', ' ', $key)"}</h3>
                 </div>
               {/if}
             {/loop}
+            {if="$nbParameters===0"}
+              <p class="center">{'No parameter available.'|t}</p>
+            {else}
+              <div class="center">
+                <input type="submit" name="parameters_form" value="{'Save'|t}"/>
+              </div>
+            {/if}
           {/if}
-          <div class="center">
-            <input type="submit" name="parameters_form" value="{'Save'|t}"/>
-          </div>
         </div>
       </section>
     </div>
index 96b357a3e1ec282885c4314388a0a67adb4131a5..12701465e3b07704d9ab5a20c66ab24a9256ac5e 100644 (file)
     {$countTags=count($tags)}
     <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
     {if="!empty($search_tags)"}
-    <p class="enter">
-      <a href="?searchtags={$search_tags|urlencode}">{'List all links with those tags'|t}</a>
+    <p class="center">
+      <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
+        {'List all links with those tags'|t}
+      </a>
     </p>
     {/if}
 
@@ -26,7 +28,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 a3e741d394588ebe8730e6ba73e91c89ef06397a..7140c67a8e174647ffbb8581bb9863faf880f880 100644 (file)
@@ -15,7 +15,9 @@
     <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
     {if="!empty($search_tags)"}
       <p class="center">
-        <a href="?searchtags={$search_tags|urlencode}">{'List all links with those tags'|t}</a>
+        <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
+          {'List all links with those tags'|t}
+        </a>
       </p>
     {/if}