aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.editorconfig23
-rw-r--r--.gitattributes2
-rw-r--r--.github/mailmap2
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml11
-rw-r--r--AUTHORS9
-rw-r--r--CHANGELOG.md38
-rw-r--r--Makefile29
-rw-r--r--README.md2
-rw-r--r--application/ApplicationUtils.php39
-rw-r--r--application/Cache.php2
-rw-r--r--application/FeedBuilder.php4
-rw-r--r--application/FileUtils.php26
-rw-r--r--application/History.php20
-rw-r--r--application/Languages.php167
-rw-r--r--application/LinkDB.php20
-rw-r--r--application/LinkFilter.php139
-rw-r--r--application/LinkUtils.php2
-rw-r--r--application/NetscapeBookmarkUtils.php27
-rw-r--r--application/PageBuilder.php22
-rw-r--r--application/PluginManager.php7
-rw-r--r--application/SessionManager.php83
-rw-r--r--application/ThemeUtils.php1
-rw-r--r--application/Updater.php12
-rw-r--r--application/Utils.php47
-rw-r--r--application/config/ConfigJson.php15
-rw-r--r--application/config/ConfigManager.php8
-rw-r--r--application/config/ConfigPhp.php4
-rw-r--r--application/config/exception/MissingFieldConfigException.php2
-rw-r--r--application/config/exception/PluginConfigOrderException.php2
-rw-r--r--application/config/exception/UnauthorizedConfigException.php2
-rw-r--r--application/exceptions/IOException.php2
-rw-r--r--composer.json3
-rw-r--r--composer.lock249
-rw-r--r--data/.htaccess12
-rw-r--r--doc/md/Download-and-Installation.md18
-rw-r--r--doc/md/Server-requirements.md1
-rw-r--r--doc/md/Shaarli-configuration.md24
-rw-r--r--doc/md/Translations.md152
-rw-r--r--doc/md/Unit-tests-Docker.md56
-rw-r--r--doc/md/Upgrade-and-migration.md27
-rw-r--r--doc/md/docker/docker-101.md78
-rw-r--r--doc/md/docker/reverse-proxy-configuration.md116
-rw-r--r--doc/md/docker/shaarli-images.md13
-rw-r--r--doc/md/images/install-shaarli.pngbin0 -> 44376 bytes
-rw-r--r--doc/md/images/poedit-1.jpgbin0 -> 72956 bytes
-rw-r--r--doc/md/index.md11
-rw-r--r--docker/alpine/Dockerfile.latest47
-rw-r--r--docker/alpine/Dockerfile.master47
-rw-r--r--docker/alpine/IMAGE.md10
-rw-r--r--docker/alpine/nginx.conf (renamed from docker/production/stable/nginx.conf)5
-rw-r--r--docker/alpine/php-fpm.conf16
-rwxr-xr-xdocker/alpine/services.d/.s6-svscan/finish2
-rwxr-xr-xdocker/alpine/services.d/nginx/run2
-rwxr-xr-xdocker/alpine/services.d/php-fpm/run2
-rw-r--r--docker/debian/Dockerfile.stable (renamed from docker/production/stable/Dockerfile)0
-rw-r--r--docker/debian/IMAGE.md (renamed from docker/production/stable/IMAGE.md)0
-rw-r--r--docker/debian/nginx.conf (renamed from docker/production/nginx.conf)0
-rw-r--r--docker/debian/supervised.conf (renamed from docker/production/stable/supervised.conf)0
-rw-r--r--docker/production/Dockerfile37
-rw-r--r--docker/production/IMAGE.md5
-rw-r--r--docker/production/supervised.conf13
-rw-r--r--docker/test/alpine36/Dockerfile34
-rw-r--r--docker/test/debian8/Dockerfile35
-rw-r--r--docker/test/debian9/Dockerfile36
-rw-r--r--docker/test/ubuntu16/Dockerfile36
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po1366
-rw-r--r--index.php183
-rw-r--r--mkdocs.yml2
-rw-r--r--plugins/TODO.md28
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.php13
-rw-r--r--plugins/archiveorg/archiveorg.html6
-rw-r--r--plugins/archiveorg/archiveorg.php11
-rw-r--r--plugins/demo_plugin/demo_plugin.php37
-rw-r--r--plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mobin0 -> 652 bytes
-rw-r--r--plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po21
-rw-r--r--plugins/isso/isso.php17
-rw-r--r--plugins/markdown/help.html6
-rw-r--r--plugins/markdown/markdown.php21
-rw-r--r--plugins/piwik/piwik.php15
-rw-r--r--plugins/playvideos/playvideos.php13
-rw-r--r--plugins/pubsubhubbub/pubsubhubbub.php16
-rw-r--r--plugins/qrcode/qrcode.meta2
-rw-r--r--plugins/qrcode/qrcode.php9
-rw-r--r--plugins/wallabag/wallabag.html6
-rw-r--r--plugins/wallabag/wallabag.php20
-rw-r--r--tests/LanguagesTest.php186
-rw-r--r--tests/LinkUtilsTest.php10
-rw-r--r--tests/NetscapeBookmarkUtils/BookmarkImportTest.php81
-rw-r--r--tests/SessionManagerTest.php160
-rw-r--r--tests/UtilsTest.php88
-rw-r--r--tests/api/controllers/GetLinksTest.php83
-rw-r--r--tests/bootstrap.php6
-rw-r--r--tests/languages/bootstrap.php7
-rw-r--r--tests/languages/de/UtilsDeTest.php12
-rw-r--r--tests/languages/en/UtilsEnTest.php12
-rw-r--r--tests/languages/fr/LanguagesFrTest.php175
-rw-r--r--tests/languages/fr/UtilsFrTest.php12
-rw-r--r--tests/utils/languages/fr/LC_MESSAGES/test.mobin0 -> 456 bytes
-rw-r--r--tests/utils/languages/fr/LC_MESSAGES/test.po19
-rw-r--r--tpl/default/changetag.html2
-rw-r--r--tpl/default/configure.html24
-rw-r--r--tpl/default/css/shaarli.css2
-rw-r--r--tpl/default/import.html8
-rw-r--r--tpl/default/includes.html14
-rw-r--r--tpl/default/install.html21
-rw-r--r--tpl/default/js/shaarli.js17
-rw-r--r--tpl/default/linklist.html30
-rw-r--r--tpl/default/linklist.paging.html4
-rw-r--r--tpl/default/page.footer.html21
-rw-r--r--tpl/default/pluginsadmin.html4
-rw-r--r--tpl/default/tag.cloud.html2
112 files changed, 4009 insertions, 640 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..4a6589a2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,23 @@
1# EditorConfig: http://EditorConfig.org
2
3root = true
4
5[*]
6charset = utf-8
7end_of_line = lf
8insert_final_newline = true
9trim_trailing_whitespace = true
10indent_style = space
11indent_size = 4
12
13[*.{htaccess,html,xml}]
14indent_size = 2
15
16[*.php]
17max_line_length = 100
18
19[Dockerfile]
20max_line_length = 80
21
22[Makefile]
23indent_style = tab
diff --git a/.gitattributes b/.gitattributes
index dd0e573c..b191e227 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -22,8 +22,10 @@ Dockerfile text
22*.ttf binary 22*.ttf binary
23*.min.css binary 23*.min.css binary
24*.min.js binary 24*.min.js binary
25*.mo binary
25 26
26# Exclude from Git archives 27# Exclude from Git archives
28.editorconfig export-ignore
27.gitattributes export-ignore 29.gitattributes export-ignore
28.github export-ignore 30.github export-ignore
29.gitignore export-ignore 31.gitignore export-ignore
diff --git a/.github/mailmap b/.github/mailmap
index 41d91e47..bbdb7908 100644
--- a/.github/mailmap
+++ b/.github/mailmap
@@ -11,3 +11,5 @@ Timo Van Neerden <fire@lehollandaisvolant.net> lehollandaisvolant <levoltigeurho
11VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com> 11VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com>
12VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net> 12VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net>
13VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org> 13VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org>
14Willi Eggeling <thewilli@gmail.com> <mail@wje-online.de>
15Willi Eggeling <thewilli@gmail.com> <thewilli@users.noreply.github.com>
diff --git a/.gitignore b/.gitignore
index d546f248..3f6939a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ vendor/
18# Release archives 18# Release archives
19*.tar.gz 19*.tar.gz
20*.zip 20*.zip
21inc/languages/*/LC_MESSAGES/shaarli.mo
21 22
22# Development and test resources 23# Development and test resources
23coverage 24coverage
diff --git a/.travis.yml b/.travis.yml
index 26535ad3..322e4337 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,12 +1,6 @@
1sudo: false 1sudo: false
2dist: precise 2dist: trusty
3language: php 3language: php
4addons:
5 apt:
6 packages:
7 - locales
8 - language-pack-de
9 - language-pack-fr
10cache: 4cache:
11 directories: 5 directories:
12 - $HOME/.composer/cache 6 - $HOME/.composer/cache
@@ -18,6 +12,9 @@ php:
18install: 12install:
19 - composer self-update 13 - composer self-update
20 - composer install --prefer-dist 14 - composer install --prefer-dist
15 - locale -a
16before_script:
17 - PATH=${PATH//:\.\/node_modules\/\.bin/}
21script: 18script:
22 - make clean 19 - make clean
23 - make check_permissions 20 - make check_permissions
diff --git a/AUTHORS b/AUTHORS
index 2181ec9d..105561c1 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,11 +1,13 @@
1 518 ArthurHoaro <arthur@hoa.ro> 1 537 ArthurHoaro <arthur@hoa.ro>
2 231 VirtualTam <virtualtam@flibidi.net> 2 252 VirtualTam <virtualtam@flibidi.net>
3 147 nodiscc <nodiscc@gmail.com> 3 148 nodiscc <nodiscc@gmail.com>
4 56 Sébastien Sauvage <sebsauvage@sebsauvage.net> 4 56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
5 15 Florian Eula <eula.florian@gmail.com> 5 15 Florian Eula <eula.florian@gmail.com>
6 13 Emilien Klein <emilien@klein.st> 6 13 Emilien Klein <emilien@klein.st>
7 12 Nicolas Danelon <hi@nicolasmd.com.ar> 7 12 Nicolas Danelon <hi@nicolasmd.com.ar>
8 9 Willi Eggeling <thewilli@gmail.com>
8 8 Christophe HENRY <christophe.henry@sbgodin.fr> 9 8 Christophe HENRY <christophe.henry@sbgodin.fr>
10 6 B. van Berkum <dev@dotmpe.com>
9 5 Lucas Cimon <lucas.cimon@gmail.com> 11 5 Lucas Cimon <lucas.cimon@gmail.com>
10 4 Alexandre Alapetite <alexandre@alapetite.fr> 12 4 Alexandre Alapetite <alexandre@alapetite.fr>
11 4 David Sferruzza <david.sferruzza@gmail.com> 13 4 David Sferruzza <david.sferruzza@gmail.com>
@@ -37,6 +39,7 @@
37 1 Kevin Canévet <kevin@streamroot.io> 39 1 Kevin Canévet <kevin@streamroot.io>
38 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org> 40 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
39 1 Lionel Martin <renarddesmers@gmail.com> 41 1 Lionel Martin <renarddesmers@gmail.com>
42 1 Mark Gerarts <mark.gerarts@gmail.com>
40 1 Marsup <marsup@gmail.com> 43 1 Marsup <marsup@gmail.com>
41 1 Sbgodin <Sbgodin@users.noreply.github.com> 44 1 Sbgodin <Sbgodin@users.noreply.github.com>
42 1 TsT <tst2005@gmail.com> 45 1 TsT <tst2005@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60262d56..33feac20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file.
4The format is based on [Keep a Changelog](http://keepachangelog.com/) 4The format is based on [Keep a Changelog](http://keepachangelog.com/)
5and this project adheres to [Semantic Versioning](http://semver.org/). 5and this project adheres to [Semantic Versioning](http://semver.org/).
6 6
7## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07
8
9**Major security issue fixed. Please update.**
10
11### Added
12- Tag search now supports wildcards `*`
13- New setting `privacy.force_login` which can be used with `privacy.hide_public_links` to redirect anonymous users to the login page.
14- New setting `general.default_note_title` used to override default `Note:` title prefix for notes.
15- Add a version hash for asset loading to prevent browser's cache issue
16
17### Changed
18- The "Remember me" checkbox is unchecked by default
19- The default value of the "Remember me" checkbox can be configured under `data/config.json.php`
20
21### Removed
22- Remove obsolete PHP magic quote support
23
24### Fixed
25- Generates a permalink URL if the URL is set to blank
26- Replace links to the old GitHub wiki with ReadTheDocs URIs
27- Use single quotes in the note bookmarklet
28- Daily page if there is no link
29- Bulk link deletion with a single link
30- HTTPS detection behind a reverse proxy
31- Travis tests environment and localization
32- Improve template paths robustness (trailing slash)
33- Robustness: safer gzinflate/zlib usage
34- Description links parsing with parenthesis (without Markdown)
35- Templates:
36 - Sort the tag cloud alphabetically
37 - Firefox social title
38 - Improved visited link color
39 - Fix jumpy textarea with long content in post edit
40
41### Security
42
43- 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)).
44
7## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23 45## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23
8 46
9The documentation has been migrated to ReadTheDocs: 47The documentation has been migrated to ReadTheDocs:
diff --git a/Makefile b/Makefile
index 40badb1d..c2d55946 100644
--- a/Makefile
+++ b/Makefile
@@ -19,6 +19,16 @@ PHP_COMMA_SOURCE = index.php,application,tests,plugins
19all: static_analysis_summary check_permissions test 19all: static_analysis_summary check_permissions test
20 20
21## 21##
22# Docker test adapter
23#
24# Shaarli sources and vendored libraries are copied from a shared volume
25# to a user-owned directory to enable running tests as a non-root user.
26##
27docker_%:
28 rsync -az /shaarli/ ~/shaarli/
29 cd ~/shaarli && make $*
30
31##
22# Concise status of the project 32# Concise status of the project
23# These targets are non-blocking: || exit 0 33# These targets are non-blocking: || exit 0
24## 34##
@@ -105,7 +115,7 @@ check_permissions:
105 @echo "----------------------" 115 @echo "----------------------"
106 @echo "Check file permissions" 116 @echo "Check file permissions"
107 @echo "----------------------" 117 @echo "----------------------"
108 @for file in `git ls-files`; do \ 118 @for file in `git ls-files | grep -v docker`; do \
109 if [ -x $$file ]; then \ 119 if [ -x $$file ]; then \
110 errors=true; \ 120 errors=true; \
111 echo "$${file} is executable"; \ 121 echo "$${file} is executable"; \
@@ -120,12 +130,12 @@ check_permissions:
120# See phpunit.xml for configuration 130# See phpunit.xml for configuration
121# https://phpunit.de/manual/current/en/appendixes.configuration.html 131# https://phpunit.de/manual/current/en/appendixes.configuration.html
122## 132##
123test: 133test: translate
124 @echo "-------" 134 @echo "-------"
125 @echo "PHPUNIT" 135 @echo "PHPUNIT"
126 @echo "-------" 136 @echo "-------"
127 @mkdir -p sandbox coverage 137 @mkdir -p sandbox coverage
128 @$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests 138 @$(BIN)/phpunit --coverage-php coverage/main.cov --bootstrap tests/bootstrap.php --testsuite unit-tests
129 139
130locale_test_%: 140locale_test_%:
131 @UT_LOCALE=$*.utf8 \ 141 @UT_LOCALE=$*.utf8 \
@@ -158,15 +168,15 @@ composer_dependencies: clean
158 composer install --no-dev --prefer-dist 168 composer install --no-dev --prefer-dist
159 find vendor/ -name ".git" -type d -exec rm -rf {} + 169 find vendor/ -name ".git" -type d -exec rm -rf {} +
160 170
161### generate a release tarball and include 3rd-party dependencies 171### generate a release tarball and include 3rd-party dependencies and translations
162release_tar: composer_dependencies htmldoc 172release_tar: composer_dependencies htmldoc translate
163 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD 173 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
164 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/ 174 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
165 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/ 175 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
166 gzip $(ARCHIVE_VERSION).tar 176 gzip $(ARCHIVE_VERSION).tar
167 177
168### generate a release zip and include 3rd-party dependencies 178### generate a release zip and include 3rd-party dependencies and translations
169release_zip: composer_dependencies htmldoc 179release_zip: composer_dependencies htmldoc translate
170 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD 180 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
171 mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor} 181 mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
172 rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/ 182 rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
@@ -203,3 +213,8 @@ htmldoc:
203 mkdocs build' 213 mkdocs build'
204 find doc/html/ -type f -exec chmod a-x '{}' \; 214 find doc/html/ -type f -exec chmod a-x '{}' \;
205 rm -r venv 215 rm -r venv
216
217
218### Generate Shaarli's translation compiled file (.mo)
219translate:
220 @find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \; \ No newline at end of file
diff --git a/README.md b/README.md
index 100ff46b..c1050027 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._
9[![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) 9[![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4)
10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) 10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
11&bull; 11&bull;
12[![](https://img.shields.io/badge/latest-v0.9.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) 12[![](https://img.shields.io/badge/latest-v0.9.2-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2)
13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) 13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
14&bull; 14&bull;
15[![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli) 15[![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli)
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php
index 85dcbeeb..911873a0 100644
--- a/application/ApplicationUtils.php
+++ b/application/ApplicationUtils.php
@@ -149,12 +149,13 @@ class ApplicationUtils
149 public static function checkPHPVersion($minVersion, $curVersion) 149 public static function checkPHPVersion($minVersion, $curVersion)
150 { 150 {
151 if (version_compare($curVersion, $minVersion) < 0) { 151 if (version_compare($curVersion, $minVersion) < 0) {
152 throw new Exception( 152 $msg = t(
153 'Your PHP version is obsolete!' 153 'Your PHP version is obsolete!'
154 .' Shaarli requires at least PHP '.$minVersion.', and thus cannot run.' 154 . ' Shaarli requires at least PHP %s, and thus cannot run.'
155 .' Your PHP version has known security vulnerabilities and should be' 155 . ' Your PHP version has known security vulnerabilities and should be'
156 .' updated as soon as possible.' 156 . ' updated as soon as possible.'
157 ); 157 );
158 throw new Exception(sprintf($msg, $minVersion));
158 } 159 }
159 } 160 }
160 161
@@ -168,17 +169,18 @@ class ApplicationUtils
168 public static function checkResourcePermissions($conf) 169 public static function checkResourcePermissions($conf)
169 { 170 {
170 $errors = array(); 171 $errors = array();
172 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
171 173
172 // Check script and template directories are readable 174 // Check script and template directories are readable
173 foreach (array( 175 foreach (array(
174 'application', 176 'application',
175 'inc', 177 'inc',
176 'plugins', 178 'plugins',
177 $conf->get('resource.raintpl_tpl'), 179 $rainTplDir,
178 $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme'), 180 $rainTplDir.'/'.$conf->get('resource.theme'),
179 ) as $path) { 181 ) as $path) {
180 if (! is_readable(realpath($path))) { 182 if (! is_readable(realpath($path))) {
181 $errors[] = '"'.$path.'" directory is not readable'; 183 $errors[] = '"'.$path.'" '. t('directory is not readable');
182 } 184 }
183 } 185 }
184 186
@@ -190,10 +192,10 @@ class ApplicationUtils
190 $conf->get('resource.raintpl_tmp'), 192 $conf->get('resource.raintpl_tmp'),
191 ) as $path) { 193 ) as $path) {
192 if (! is_readable(realpath($path))) { 194 if (! is_readable(realpath($path))) {
193 $errors[] = '"'.$path.'" directory is not readable'; 195 $errors[] = '"'.$path.'" '. t('directory is not readable');
194 } 196 }
195 if (! is_writable(realpath($path))) { 197 if (! is_writable(realpath($path))) {
196 $errors[] = '"'.$path.'" directory is not writable'; 198 $errors[] = '"'.$path.'" '. t('directory is not writable');
197 } 199 }
198 } 200 }
199 201
@@ -211,13 +213,28 @@ class ApplicationUtils
211 } 213 }
212 214
213 if (! is_readable(realpath($path))) { 215 if (! is_readable(realpath($path))) {
214 $errors[] = '"'.$path.'" file is not readable'; 216 $errors[] = '"'.$path.'" '. t('file is not readable');
215 } 217 }
216 if (! is_writable(realpath($path))) { 218 if (! is_writable(realpath($path))) {
217 $errors[] = '"'.$path.'" file is not writable'; 219 $errors[] = '"'.$path.'" '. t('file is not writable');
218 } 220 }
219 } 221 }
220 222
221 return $errors; 223 return $errors;
222 } 224 }
225
226 /**
227 * Returns a salted hash representing the current Shaarli version.
228 *
229 * Useful for assets browser cache.
230 *
231 * @param string $currentVersion of Shaarli
232 * @param string $salt User personal salt, also used for the authentication
233 *
234 * @return string version hash
235 */
236 public static function getVersionHash($currentVersion, $salt)
237 {
238 return hash_hmac('sha256', $currentVersion, $salt);
239 }
223} 240}
diff --git a/application/Cache.php b/application/Cache.php
index 5d050165..e5d43e61 100644
--- a/application/Cache.php
+++ b/application/Cache.php
@@ -13,7 +13,7 @@
13function purgeCachedPages($pageCacheDir) 13function purgeCachedPages($pageCacheDir)
14{ 14{
15 if (! is_dir($pageCacheDir)) { 15 if (! is_dir($pageCacheDir)) {
16 $error = 'Cannot purge '.$pageCacheDir.': no directory'; 16 $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
17 error_log($error); 17 error_log($error);
18 return $error; 18 return $error;
19 } 19 }
diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php
index 7377bcec..3cfaafb4 100644
--- a/application/FeedBuilder.php
+++ b/application/FeedBuilder.php
@@ -148,9 +148,9 @@ class FeedBuilder
148 $link['url'] = $pageaddr . $link['url']; 148 $link['url'] = $pageaddr . $link['url'];
149 } 149 }
150 if ($this->usePermalinks === true) { 150 if ($this->usePermalinks === true) {
151 $permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>'; 151 $permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
152 } else { 152 } else {
153 $permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>'; 153 $permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
154 } 154 }
155 $link['description'] = format_description($link['description'], '', $pageaddr); 155 $link['description'] = format_description($link['description'], '', $pageaddr);
156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink; 156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
diff --git a/application/FileUtils.php b/application/FileUtils.php
index a167f642..918cb83b 100644
--- a/application/FileUtils.php
+++ b/application/FileUtils.php
@@ -50,7 +50,8 @@ class FileUtils
50 50
51 /** 51 /**
52 * Read data from a file containing Shaarli database format content. 52 * Read data from a file containing Shaarli database format content.
53 * If the file isn't readable or doesn't exists, default data will be returned. 53 *
54 * If the file isn't readable or doesn't exist, default data will be returned.
54 * 55 *
55 * @param string $file File path. 56 * @param string $file File path.
56 * @param mixed $default The default value to return if the file isn't readable. 57 * @param mixed $default The default value to return if the file isn't readable.
@@ -61,16 +62,21 @@ class FileUtils
61 { 62 {
62 // Note that gzinflate is faster than gzuncompress. 63 // Note that gzinflate is faster than gzuncompress.
63 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 64 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
64 if (is_readable($file)) { 65 if (! is_readable($file)) {
65 return unserialize( 66 return $default;
66 gzinflate( 67 }
67 base64_decode( 68
68 substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) 69 $data = file_get_contents($file);
69 ) 70 if ($data == '') {
70 ) 71 return $default;
71 );
72 } 72 }
73 73
74 return $default; 74 return unserialize(
75 gzinflate(
76 base64_decode(
77 substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
78 )
79 )
80 );
75 } 81 }
76} 82}
diff --git a/application/History.php b/application/History.php
index 116b9264..35ec016a 100644
--- a/application/History.php
+++ b/application/History.php
@@ -16,6 +16,7 @@
16 * - UPDATED: link updated 16 * - UPDATED: link updated
17 * - DELETED: link deleted 17 * - DELETED: link deleted
18 * - SETTINGS: the settings have been updated through the UI. 18 * - SETTINGS: the settings have been updated through the UI.
19 * - IMPORT: bulk links import
19 * 20 *
20 * Note: new events are put at the beginning of the file and history array. 21 * Note: new events are put at the beginning of the file and history array.
21 */ 22 */
@@ -42,6 +43,11 @@ class History
42 const SETTINGS = 'SETTINGS'; 43 const SETTINGS = 'SETTINGS';
43 44
44 /** 45 /**
46 * @var string Action key: a bulk import has been processed.
47 */
48 const IMPORT = 'IMPORT';
49
50 /**
45 * @var string History file path. 51 * @var string History file path.
46 */ 52 */
47 protected $historyFilePath; 53 protected $historyFilePath;
@@ -122,6 +128,16 @@ class History
122 } 128 }
123 129
124 /** 130 /**
131 * Add Event: bulk import.
132 *
133 * Note: we don't store links add/update one by one since it can have a huge impact on performances.
134 */
135 public function importLinks()
136 {
137 $this->addEvent(self::IMPORT);
138 }
139
140 /**
125 * Save a new event and write it in the history file. 141 * Save a new event and write it in the history file.
126 * 142 *
127 * @param string $status Event key, should be defined as constant. 143 * @param string $status Event key, should be defined as constant.
@@ -155,7 +171,7 @@ class History
155 } 171 }
156 172
157 if (! is_writable($this->historyFilePath)) { 173 if (! is_writable($this->historyFilePath)) {
158 throw new Exception('History file isn\'t readable or writable'); 174 throw new Exception(t('History file isn\'t readable or writable'));
159 } 175 }
160 } 176 }
161 177
@@ -166,7 +182,7 @@ class History
166 { 182 {
167 $this->history = FileUtils::readFlatDB($this->historyFilePath, []); 183 $this->history = FileUtils::readFlatDB($this->historyFilePath, []);
168 if ($this->history === false) { 184 if ($this->history === false) {
169 throw new Exception('Could not parse history file'); 185 throw new Exception(t('Could not parse history file'));
170 } 186 }
171 } 187 }
172 188
diff --git a/application/Languages.php b/application/Languages.php
index c8b0a25a..357c7524 100644
--- a/application/Languages.php
+++ b/application/Languages.php
@@ -1,21 +1,164 @@
1<?php 1<?php
2 2
3namespace Shaarli;
4
5use Gettext\GettextTranslator;
6use Gettext\Merge;
7use Gettext\Translations;
8use Gettext\Translator;
9use Gettext\TranslatorInterface;
10use Shaarli\Config\ConfigManager;
11
3/** 12/**
4 * Wrapper function for translation which match the API 13 * Class Languages
5 * of gettext()/_() and ngettext(). 14 *
15 * Load Shaarli translations using 'gettext/gettext'.
16 * This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
17 * with a fixed language, or dynamically using autoLocale().
6 * 18 *
7 * Not doing translation for now. 19 * Translation files PO/MO files follow gettext standard and must be placed under:
20 * <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
8 * 21 *
9 * @param string $text Text to translate. 22 * Pros/cons:
10 * @param string $nText The plural message ID. 23 * - gettext extension is faster
11 * @param int $nb The number of items for plural forms. 24 * - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
12 * 25 *
13 * @return String Text translated. 26 * Settings:
27 * - translation.mode:
28 * - auto: use default setting (PHP implementation)
29 * - php: use PHP implementation
30 * - gettext: use gettext wrapper
31 * - translation.language:
32 * - auto: use autoLocale() and the language change according to user HTTP headers
33 * - fixed language: e.g. 'fr'
34 * - translation.extensions:
35 * - domain => translation_path: allow plugins and themes to extend the defaut extension
36 * The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
37 *
38 * @package Shaarli
14 */ 39 */
15function t($text, $nText = '', $nb = 0) { 40class Languages
16 if (empty($nText)) { 41{
17 return $text; 42 /**
43 * Core translations domain
44 */
45 const DEFAULT_DOMAIN = 'shaarli';
46
47 /**
48 * @var TranslatorInterface
49 */
50 protected $translator;
51
52 /**
53 * @var string
54 */
55 protected $language;
56
57 /**
58 * @var ConfigManager
59 */
60 protected $conf;
61
62 /**
63 * Languages constructor.
64 *
65 * @param string $language lang determined by autoLocale(), can be overridden.
66 * @param ConfigManager $conf instance.
67 */
68 public function __construct($language, $conf)
69 {
70 $this->conf = $conf;
71 $confLanguage = $this->conf->get('translation.language', 'auto');
72 if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
73 $this->language = substr($language, 0, 5);
74 } else {
75 $this->language = $confLanguage;
76 }
77
78 if (! extension_loaded('gettext')
79 || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
80 ) {
81 $this->initPhpTranslator();
82 } else {
83 $this->initGettextTranslator();
84 }
85
86 // Register default functions (e.g. '__()') to use our Translator
87 $this->translator->register();
88 }
89
90 /**
91 * Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
92 */
93 protected function initGettextTranslator ()
94 {
95 $this->translator = new GettextTranslator();
96 $this->translator->setLanguage($this->language);
97 $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
98
99 foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
100 if ($domain !== self::DEFAULT_DOMAIN) {
101 $this->translator->loadDomain($domain, $translationPath, false);
102 }
103 }
104 }
105
106 /**
107 * Initialize the translator using a PHP implementation of gettext.
108 *
109 * Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
110 */
111 protected function initPhpTranslator()
112 {
113 $this->translator = new Translator();
114 $translations = new Translations();
115 // Core translations
116 try {
117 /** @var Translations $translations */
118 $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
119 $translations->setDomain('shaarli');
120 $this->translator->loadTranslations($translations);
121 } catch (\InvalidArgumentException $e) {}
122
123
124 // Extension translations (plugins, themes, etc.).
125 foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
126 if ($domain === self::DEFAULT_DOMAIN) {
127 continue;
128 }
129
130 try {
131 /** @var Translations $extension */
132 $extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
133 $extension->setDomain($domain);
134 $this->translator->loadTranslations($extension);
135 } catch (\InvalidArgumentException $e) {}
136 }
137 }
138
139 /**
140 * Checks if a language string is valid.
141 *
142 * @param string $language e.g. 'fr' or 'en_US'
143 *
144 * @return bool true if valid, false otherwise
145 */
146 protected function isValidLanguage($language)
147 {
148 return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
149 }
150
151 /**
152 * Get the list of available languages for Shaarli.
153 *
154 * @return array List of available languages, with their label.
155 */
156 public static function getAvailableLanguages()
157 {
158 return [
159 'auto' => t('Automatic'),
160 'en' => t('English'),
161 'fr' => t('French'),
162 ];
18 } 163 }
19 $actualForm = $nb > 1 ? $nText : $text;
20 return sprintf($actualForm, $nb);
21} 164}
diff --git a/application/LinkDB.php b/application/LinkDB.php
index eace625e..c1661d52 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -133,16 +133,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess
133 { 133 {
134 // TODO: use exceptions instead of "die" 134 // TODO: use exceptions instead of "die"
135 if (!$this->loggedIn) { 135 if (!$this->loggedIn) {
136 die('You are not authorized to add a link.'); 136 die(t('You are not authorized to add a link.'));
137 } 137 }
138 if (!isset($value['id']) || empty($value['url'])) { 138 if (!isset($value['id']) || empty($value['url'])) {
139 die('Internal Error: A link should always have an id and URL.'); 139 die(t('Internal Error: A link should always have an id and URL.'));
140 } 140 }
141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { 141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
142 die('You must specify an integer as a key.'); 142 die(t('You must specify an integer as a key.'));
143 } 143 }
144 if ($offset !== null && $offset !== $value['id']) { 144 if ($offset !== null && $offset !== $value['id']) {
145 die('Array offset and link ID must be equal.'); 145 die(t('Array offset and link ID must be equal.'));
146 } 146 }
147 147
148 // If the link exists, we reuse the real offset, otherwise new entry 148 // If the link exists, we reuse the real offset, otherwise new entry
@@ -248,13 +248,13 @@ class LinkDB implements Iterator, Countable, ArrayAccess
248 $this->links = array(); 248 $this->links = array();
249 $link = array( 249 $link = array(
250 'id' => 1, 250 'id' => 1,
251 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', 251 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
252 'url'=>'https://shaarli.readthedocs.io', 252 'url'=>'https://shaarli.readthedocs.io',
253 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. 253 'description'=>t('Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
254 254
255To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. 255To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
256 256
257You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', 257You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'),
258 'private'=>0, 258 'private'=>0,
259 'created'=> new DateTime(), 259 'created'=> new DateTime(),
260 'tags'=>'opensource software' 260 'tags'=>'opensource software'
@@ -264,9 +264,9 @@ You use the community supported version of the original Shaarli project, by Seba
264 264
265 $link = array( 265 $link = array(
266 'id' => 0, 266 'id' => 0,
267 'title'=>'My secret stuff... - Pastebin.com', 267 'title'=> t('My secret stuff... - Pastebin.com'),
268 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', 268 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
269 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', 269 'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
270 'private'=>1, 270 'private'=>1,
271 'created'=> new DateTime('1 minute ago'), 271 'created'=> new DateTime('1 minute ago'),
272 'tags'=>'secretstuff', 272 'tags'=>'secretstuff',
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
index 95519528..12376e27 100644
--- a/application/LinkFilter.php
+++ b/application/LinkFilter.php
@@ -250,6 +250,51 @@ class LinkFilter
250 } 250 }
251 251
252 /** 252 /**
253 * generate a regex fragment out of a tag
254 * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
255 * @return string generated regex fragment
256 */
257 private static function tag2regex($tag)
258 {
259 $len = strlen($tag);
260 if(!$len || $tag === "-" || $tag === "*"){
261 // nothing to search, return empty regex
262 return '';
263 }
264 if($tag[0] === "-") {
265 // query is negated
266 $i = 1; // use offset to start after '-' character
267 $regex = '(?!'; // create negative lookahead
268 } else {
269 $i = 0; // start at first character
270 $regex = '(?='; // use positive lookahead
271 }
272 $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
273 // iterate over string, separating it into placeholder and content
274 for(; $i < $len; $i++){
275 if($tag[$i] === '*'){
276 // placeholder found
277 $regex .= '[^ ]*?';
278 } else {
279 // regular characters
280 $offset = strpos($tag, '*', $i);
281 if($offset === false){
282 // no placeholder found, set offset to end of string
283 $offset = $len;
284 }
285 // subtract one, as we want to get before the placeholder or end of string
286 $offset -= 1;
287 // we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
288 $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
289 // move $i on
290 $i = $offset;
291 }
292 }
293 $regex .= '(?:$| ))'; // after the tag may only be a space or the end
294 return $regex;
295 }
296
297 /**
253 * Returns the list of links associated with a given list of tags 298 * Returns the list of links associated with a given list of tags
254 * 299 *
255 * You can specify one or more tags, separated by space or a comma, e.g. 300 * You can specify one or more tags, separated by space or a comma, e.g.
@@ -263,20 +308,32 @@ class LinkFilter
263 */ 308 */
264 public function filterTags($tags, $casesensitive = false, $visibility = 'all') 309 public function filterTags($tags, $casesensitive = false, $visibility = 'all')
265 { 310 {
266 // Implode if array for clean up. 311 // get single tags (we may get passed an array, even though the docs say different)
267 $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags; 312 $inputTags = $tags;
268 if (empty($tags)) { 313 if(!is_array($tags)) {
314 // we got an input string, split tags
315 $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
316 }
317
318 if(!count($inputTags)){
319 // no input tags
269 return $this->noFilter($visibility); 320 return $this->noFilter($visibility);
270 } 321 }
271 322
272 $searchtags = self::tagsStrToArray($tags, $casesensitive); 323 // build regex from all tags
273 $filtered = array(); 324 $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
274 if (empty($searchtags)) { 325 if(!$casesensitive) {
275 return $filtered; 326 // make regex case insensitive
327 $re .= 'i';
276 } 328 }
277 329
330 // create resulting array
331 $filtered = array();
332
333 // iterate over each link
278 foreach ($this->links as $key => $link) { 334 foreach ($this->links as $key => $link) {
279 // ignore non private links when 'privatonly' is on. 335 // check level of visibility
336 // ignore non private links when 'privateonly' is on.
280 if ($visibility !== 'all') { 337 if ($visibility !== 'all') {
281 if (! $link['private'] && $visibility === 'private') { 338 if (! $link['private'] && $visibility === 'private') {
282 continue; 339 continue;
@@ -284,25 +341,27 @@ class LinkFilter
284 continue; 341 continue;
285 } 342 }
286 } 343 }
287 344 $search = $link['tags']; // build search string, start with tags of current link
288 $linktags = self::tagsStrToArray($link['tags'], $casesensitive); 345 if(strlen(trim($link['description'])) && strpos($link['description'], '#') !== false){
289 346 // description given and at least one possible tag found
290 $found = true; 347 $descTags = array();
291 for ($i = 0 ; $i < count($searchtags) && $found; $i++) { 348 // find all tags in the form of #tag in the description
292 // Exclusive search, quit if tag found. 349 preg_match_all(
293 // Or, tag not found in the link, quit. 350 '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
294 if (($searchtags[$i][0] == '-' 351 $link['description'],
295 && $this->searchTagAndHashTag(substr($searchtags[$i], 1), $linktags, $link['description'])) 352 $descTags
296 || ($searchtags[$i][0] != '-') 353 );
297 && ! $this->searchTagAndHashTag($searchtags[$i], $linktags, $link['description']) 354 if(count($descTags[1])){
298 ) { 355 // there were some tags in the description, add them to the search string
299 $found = false; 356 $search .= ' ' . implode(' ', $descTags[1]);
300 } 357 }
358 };
359 // match regular expression with search string
360 if(!preg_match($re, $search)){
361 // this entry does _not_ match our regex
362 continue;
301 } 363 }
302 364 $filtered[$key] = $link;
303 if ($found) {
304 $filtered[$key] = $link;
305 }
306 } 365 }
307 return $filtered; 366 return $filtered;
308 } 367 }
@@ -364,28 +423,6 @@ class LinkFilter
364 } 423 }
365 424
366 /** 425 /**
367 * Check if a tag is found in the taglist, or as an hashtag in the link description.
368 *
369 * @param string $tag Tag to search.
370 * @param array $taglist List of tags for the current link.
371 * @param string $description Link description.
372 *
373 * @return bool True if found, false otherwise.
374 */
375 protected function searchTagAndHashTag($tag, $taglist, $description)
376 {
377 if (in_array($tag, $taglist)) {
378 return true;
379 }
380
381 if (preg_match('/(^| )#'. $tag .'([^'. self::$HASHTAG_CHARS .']|$)/mui', $description) > 0) {
382 return true;
383 }
384
385 return false;
386 }
387
388 /**
389 * Convert a list of tags (str) to an array. Also 426 * Convert a list of tags (str) to an array. Also
390 * - handle case sensitivity. 427 * - handle case sensitivity.
391 * - accepts spaces commas as separator. 428 * - accepts spaces commas as separator.
@@ -407,5 +444,11 @@ class LinkFilter
407 444
408class LinkNotFoundException extends Exception 445class LinkNotFoundException extends Exception
409{ 446{
410 protected $message = 'The link you are trying to reach does not exist or has been deleted.'; 447 /**
448 * LinkNotFoundException constructor.
449 */
450 public function __construct()
451 {
452 $this->message = t('The link you are trying to reach does not exist or has been deleted.');
453 }
411} 454}
diff --git a/application/LinkUtils.php b/application/LinkUtils.php
index 976474de..267e62cd 100644
--- a/application/LinkUtils.php
+++ b/application/LinkUtils.php
@@ -109,7 +109,7 @@ function count_private($links)
109 */ 109 */
110function text2clickable($text, $redirector = '') 110function text2clickable($text, $redirector = '')
111{ 111{
112 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si'; 112 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
113 113
114 if (empty($redirector)) { 114 if (empty($redirector)) {
115 return preg_replace($regex, '<a href="$1">$1</a>', $text); 115 return preg_replace($regex, '<a href="$1">$1</a>', $text);
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
index 2a10ff22..dd7057f8 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/NetscapeBookmarkUtils.php
@@ -32,11 +32,10 @@ class NetscapeBookmarkUtils
32 { 32 {
33 // see tpl/export.html for possible values 33 // see tpl/export.html for possible values
34 if (! in_array($selection, array('all', 'public', 'private'))) { 34 if (! in_array($selection, array('all', 'public', 'private'))) {
35 throw new Exception('Invalid export selection: "'.$selection.'"'); 35 throw new Exception(t('Invalid export selection:') .' "'.$selection.'"');
36 } 36 }
37 37
38 $bookmarkLinks = array(); 38 $bookmarkLinks = array();
39
40 foreach ($linkDb as $link) { 39 foreach ($linkDb as $link) {
41 if ($link['private'] != 0 && $selection == 'public') { 40 if ($link['private'] != 0 && $selection == 'public') {
42 continue; 41 continue;
@@ -66,6 +65,7 @@ class NetscapeBookmarkUtils
66 * @param int $importCount how many links were imported 65 * @param int $importCount how many links were imported
67 * @param int $overwriteCount how many links were overwritten 66 * @param int $overwriteCount how many links were overwritten
68 * @param int $skipCount how many links were skipped 67 * @param int $skipCount how many links were skipped
68 * @param int $duration how many seconds did the import take
69 * 69 *
70 * @return string Summary of the bookmark import status 70 * @return string Summary of the bookmark import status
71 */ 71 */
@@ -74,16 +74,18 @@ class NetscapeBookmarkUtils
74 $filesize, 74 $filesize,
75 $importCount=0, 75 $importCount=0,
76 $overwriteCount=0, 76 $overwriteCount=0,
77 $skipCount=0 77 $skipCount=0,
78 $duration=0
78 ) 79 )
79 { 80 {
80 $status = 'File '.$filename.' ('.$filesize.' bytes) '; 81 $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
81 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { 82 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
82 $status .= 'has an unknown file format. Nothing was imported.'; 83 $status .= t('has an unknown file format. Nothing was imported.');
83 } else { 84 } else {
84 $status .= 'was successfully processed: '.$importCount.' links imported, '; 85 $status .= vsprintf(
85 $status .= $overwriteCount.' links overwritten, '; 86 t('was successfully processed in %d seconds: %d links imported, %d links overwritten, %d links skipped.'),
86 $status .= $skipCount.' links skipped.'; 87 [$duration, $importCount, $overwriteCount, $skipCount]
88 );
87 } 89 }
88 return $status; 90 return $status;
89 } 91 }
@@ -101,6 +103,7 @@ class NetscapeBookmarkUtils
101 */ 103 */
102 public static function import($post, $files, $linkDb, $conf, $history) 104 public static function import($post, $files, $linkDb, $conf, $history)
103 { 105 {
106 $start = time();
104 $filename = $files['filetoupload']['name']; 107 $filename = $files['filetoupload']['name'];
105 $filesize = $files['filetoupload']['size']; 108 $filesize = $files['filetoupload']['size'];
106 $data = file_get_contents($files['filetoupload']['tmp_name']); 109 $data = file_get_contents($files['filetoupload']['tmp_name']);
@@ -184,7 +187,6 @@ class NetscapeBookmarkUtils
184 $linkDb[$existingLink['id']] = $newLink; 187 $linkDb[$existingLink['id']] = $newLink;
185 $importCount++; 188 $importCount++;
186 $overwriteCount++; 189 $overwriteCount++;
187 $history->updateLink($newLink);
188 continue; 190 continue;
189 } 191 }
190 192
@@ -196,16 +198,19 @@ class NetscapeBookmarkUtils
196 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); 198 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
197 $linkDb[$newLink['id']] = $newLink; 199 $linkDb[$newLink['id']] = $newLink;
198 $importCount++; 200 $importCount++;
199 $history->addLink($newLink);
200 } 201 }
201 202
202 $linkDb->save($conf->get('resource.page_cache')); 203 $linkDb->save($conf->get('resource.page_cache'));
204 $history->importLinks();
205
206 $duration = time() - $start;
203 return self::importStatus( 207 return self::importStatus(
204 $filename, 208 $filename,
205 $filesize, 209 $filesize,
206 $importCount, 210 $importCount,
207 $overwriteCount, 211 $overwriteCount,
208 $skipCount 212 $skipCount,
213 $duration
209 ); 214 );
210 } 215 }
211} 216}
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index 7a42400d..468f144b 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -32,12 +32,14 @@ class PageBuilder
32 * 32 *
33 * @param ConfigManager $conf Configuration Manager instance (reference). 33 * @param ConfigManager $conf Configuration Manager instance (reference).
34 * @param LinkDB $linkDB instance. 34 * @param LinkDB $linkDB instance.
35 * @param string $token Session token
35 */ 36 */
36 public function __construct(&$conf, $linkDB = null) 37 public function __construct(&$conf, $linkDB = null, $token = null)
37 { 38 {
38 $this->tpl = false; 39 $this->tpl = false;
39 $this->conf = $conf; 40 $this->conf = $conf;
40 $this->linkDB = $linkDB; 41 $this->linkDB = $linkDB;
42 $this->token = $token;
41 } 43 }
42 44
43 /** 45 /**
@@ -49,7 +51,7 @@ class PageBuilder
49 51
50 try { 52 try {
51 $version = ApplicationUtils::checkUpdate( 53 $version = ApplicationUtils::checkUpdate(
52 shaarli_version, 54 SHAARLI_VERSION,
53 $this->conf->get('resource.update_check'), 55 $this->conf->get('resource.update_check'),
54 $this->conf->get('updates.check_updates_interval'), 56 $this->conf->get('updates.check_updates_interval'),
55 $this->conf->get('updates.check_updates'), 57 $this->conf->get('updates.check_updates'),
@@ -75,7 +77,11 @@ class PageBuilder
75 } 77 }
76 $this->tpl->assign('searchcrits', $searchcrits); 78 $this->tpl->assign('searchcrits', $searchcrits);
77 $this->tpl->assign('source', index_url($_SERVER)); 79 $this->tpl->assign('source', index_url($_SERVER));
78 $this->tpl->assign('version', shaarli_version); 80 $this->tpl->assign('version', SHAARLI_VERSION);
81 $this->tpl->assign(
82 'version_hash',
83 ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt'))
84 );
79 $this->tpl->assign('scripturl', index_url($_SERVER)); 85 $this->tpl->assign('scripturl', index_url($_SERVER));
80 $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? 86 $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links?
81 $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly'])); 87 $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly']));
@@ -88,7 +94,8 @@ class PageBuilder
88 $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true)); 94 $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true));
89 $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss'); 95 $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss');
90 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); 96 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
91 $this->tpl->assign('token', getToken($this->conf)); 97 $this->tpl->assign('token', $this->token);
98
92 if ($this->linkDB !== null) { 99 if ($this->linkDB !== null) {
93 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); 100 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
94 } 101 }
@@ -154,9 +161,12 @@ class PageBuilder
154 * 161 *
155 * @param string $message A messate to display what is not found 162 * @param string $message A messate to display what is not found
156 */ 163 */
157 public function render404($message = 'The page you are trying to reach does not exist or has been deleted.') 164 public function render404($message = '')
158 { 165 {
159 header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); 166 if (empty($message)) {
167 $message = t('The page you are trying to reach does not exist or has been deleted.');
168 }
169 header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found'));
160 $this->tpl->assign('error_message', $message); 170 $this->tpl->assign('error_message', $message);
161 $this->renderPage('404'); 171 $this->renderPage('404');
162 } 172 }
diff --git a/application/PluginManager.php b/application/PluginManager.php
index 59ece4fa..cf603845 100644
--- a/application/PluginManager.php
+++ b/application/PluginManager.php
@@ -188,6 +188,9 @@ class PluginManager
188 $metaData[$plugin] = parse_ini_file($metaFile); 188 $metaData[$plugin] = parse_ini_file($metaFile);
189 $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins); 189 $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
190 190
191 if (isset($metaData[$plugin]['description'])) {
192 $metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
193 }
191 // Read parameters and format them into an array. 194 // Read parameters and format them into an array.
192 if (isset($metaData[$plugin]['parameters'])) { 195 if (isset($metaData[$plugin]['parameters'])) {
193 $params = explode(';', $metaData[$plugin]['parameters']); 196 $params = explode(';', $metaData[$plugin]['parameters']);
@@ -203,7 +206,7 @@ class PluginManager
203 $metaData[$plugin]['parameters'][$param]['value'] = ''; 206 $metaData[$plugin]['parameters'][$param]['value'] = '';
204 // Optional parameter description in parameter.PARAM_NAME= 207 // Optional parameter description in parameter.PARAM_NAME=
205 if (isset($metaData[$plugin]['parameter.'. $param])) { 208 if (isset($metaData[$plugin]['parameter.'. $param])) {
206 $metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param]; 209 $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]);
207 } 210 }
208 } 211 }
209 } 212 }
@@ -237,6 +240,6 @@ class PluginFileNotFoundException extends Exception
237 */ 240 */
238 public function __construct($pluginName) 241 public function __construct($pluginName)
239 { 242 {
240 $this->message = 'Plugin "'. $pluginName .'" files not found.'; 243 $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
241 } 244 }
242} 245}
diff --git a/application/SessionManager.php b/application/SessionManager.php
new file mode 100644
index 00000000..3aa4ddfc
--- /dev/null
+++ b/application/SessionManager.php
@@ -0,0 +1,83 @@
1<?php
2namespace Shaarli;
3
4/**
5 * Manages the server-side session
6 */
7class SessionManager
8{
9 protected $session = [];
10
11 /**
12 * Constructor
13 *
14 * @param array $session The $_SESSION array (reference)
15 * @param ConfigManager $conf ConfigManager instance (reference)
16 */
17 public function __construct(& $session, & $conf)
18 {
19 $this->session = &$session;
20 $this->conf = &$conf;
21 }
22
23 /**
24 * Generates a session token
25 *
26 * @return string token
27 */
28 public function generateToken()
29 {
30 $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
31 $this->session['tokens'][$token] = 1;
32 return $token;
33 }
34
35 /**
36 * Checks the validity of a session token, and destroys it afterwards
37 *
38 * @param string $token The token to check
39 *
40 * @return bool true if the token is valid, else false
41 */
42 public function checkToken($token)
43 {
44 if (! isset($this->session['tokens'][$token])) {
45 // the token is wrong, or has already been used
46 return false;
47 }
48
49 // destroy the token to prevent future use
50 unset($this->session['tokens'][$token]);
51 return true;
52 }
53
54 /**
55 * Validate session ID to prevent Full Path Disclosure.
56 *
57 * See #298.
58 * The session ID's format depends on the hash algorithm set in PHP settings
59 *
60 * @param string $sessionId Session ID
61 *
62 * @return true if valid, false otherwise.
63 *
64 * @see http://php.net/manual/en/function.hash-algos.php
65 * @see http://php.net/manual/en/session.configuration.php
66 */
67 public static function checkId($sessionId)
68 {
69 if (empty($sessionId)) {
70 return false;
71 }
72
73 if (!$sessionId) {
74 return false;
75 }
76
77 if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
78 return false;
79 }
80
81 return true;
82 }
83}
diff --git a/application/ThemeUtils.php b/application/ThemeUtils.php
index 2718ed13..16f2f6a2 100644
--- a/application/ThemeUtils.php
+++ b/application/ThemeUtils.php
@@ -22,6 +22,7 @@ class ThemeUtils
22 */ 22 */
23 public static function getThemes($tplDir) 23 public static function getThemes($tplDir)
24 { 24 {
25 $tplDir = rtrim($tplDir, '/');
25 $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR); 26 $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR);
26 $themes = []; 27 $themes = [];
27 foreach ($allTheme as $value) { 28 foreach ($allTheme as $value) {
diff --git a/application/Updater.php b/application/Updater.php
index 0702158a..bc859536 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -73,7 +73,7 @@ class Updater
73 } 73 }
74 74
75 if ($this->methods === null) { 75 if ($this->methods === null) {
76 throw new UpdaterException('Couldn\'t retrieve Updater class methods.'); 76 throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
77 } 77 }
78 78
79 foreach ($this->methods as $method) { 79 foreach ($this->methods as $method) {
@@ -398,7 +398,7 @@ class Updater
398 */ 398 */
399 public function updateMethodCheckUpdateRemoteBranch() 399 public function updateMethodCheckUpdateRemoteBranch()
400 { 400 {
401 if (shaarli_version === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') { 401 if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
402 return true; 402 return true;
403 } 403 }
404 404
@@ -413,7 +413,7 @@ class Updater
413 $latestMajor = $matches[1]; 413 $latestMajor = $matches[1];
414 414
415 // Get current major version digit 415 // Get current major version digit
416 preg_match('/(\d+)\.\d+$/', shaarli_version, $matches); 416 preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
417 $currentMajor = $matches[1]; 417 $currentMajor = $matches[1];
418 418
419 if ($currentMajor === $latestMajor) { 419 if ($currentMajor === $latestMajor) {
@@ -490,7 +490,7 @@ class UpdaterException extends Exception
490 } 490 }
491 491
492 if (! empty($this->method)) { 492 if (! empty($this->method)) {
493 $out .= 'An error occurred while running the update '. $this->method . PHP_EOL; 493 $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
494 } 494 }
495 495
496 if (! empty($this->previous)) { 496 if (! empty($this->previous)) {
@@ -530,11 +530,11 @@ function read_updates_file($updatesFilepath)
530function write_updates_file($updatesFilepath, $updates) 530function write_updates_file($updatesFilepath, $updates)
531{ 531{
532 if (empty($updatesFilepath)) { 532 if (empty($updatesFilepath)) {
533 throw new Exception('Updates file path is not set, can\'t write updates.'); 533 throw new Exception(t('Updates file path is not set, can\'t write updates.'));
534 } 534 }
535 535
536 $res = file_put_contents($updatesFilepath, implode(';', $updates)); 536 $res = file_put_contents($updatesFilepath, implode(';', $updates));
537 if ($res === false) { 537 if ($res === false) {
538 throw new Exception('Unable to write updates in '. $updatesFilepath . '.'); 538 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
539 } 539 }
540} 540}
diff --git a/application/Utils.php b/application/Utils.php
index 4a2f5561..97b12fcf 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -182,36 +182,6 @@ function generateLocation($referer, $host, $loopTerms = array())
182} 182}
183 183
184/** 184/**
185 * Validate session ID to prevent Full Path Disclosure.
186 *
187 * See #298.
188 * The session ID's format depends on the hash algorithm set in PHP settings
189 *
190 * @param string $sessionId Session ID
191 *
192 * @return true if valid, false otherwise.
193 *
194 * @see http://php.net/manual/en/function.hash-algos.php
195 * @see http://php.net/manual/en/session.configuration.php
196 */
197function is_session_id_valid($sessionId)
198{
199 if (empty($sessionId)) {
200 return false;
201 }
202
203 if (!$sessionId) {
204 return false;
205 }
206
207 if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
208 return false;
209 }
210
211 return true;
212}
213
214/**
215 * Sniff browser language to set the locale automatically. 185 * Sniff browser language to set the locale automatically.
216 * Note that is may not work on your server if the corresponding locale is not installed. 186 * Note that is may not work on your server if the corresponding locale is not installed.
217 * 187 *
@@ -452,7 +422,7 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true)
452 */ 422 */
453function alphabetical_sort(&$data, $reverse = false, $byKeys = false) 423function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
454{ 424{
455 $callback = function($a, $b) use ($reverse) { 425 $callback = function ($a, $b) use ($reverse) {
456 // Collator is part of PHP intl. 426 // Collator is part of PHP intl.
457 if (class_exists('Collator')) { 427 if (class_exists('Collator')) {
458 $collator = new Collator(setlocale(LC_COLLATE, 0)); 428 $collator = new Collator(setlocale(LC_COLLATE, 0));
@@ -470,3 +440,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
470 usort($data, $callback); 440 usort($data, $callback);
471 } 441 }
472} 442}
443
444/**
445 * Wrapper function for translation which match the API
446 * of gettext()/_() and ngettext().
447 *
448 * @param string $text Text to translate.
449 * @param string $nText The plural message ID.
450 * @param int $nb The number of items for plural forms.
451 * @param string $domain The domain where the translation is stored (default: shaarli).
452 *
453 * @return string Text translated.
454 */
455function t($text, $nText = '', $nb = 1, $domain = 'shaarli') {
456 return dn__($domain, $text, $nText, $nb);
457}
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php
index 9ef2ef56..8c8d5610 100644
--- a/application/config/ConfigJson.php
+++ b/application/config/ConfigJson.php
@@ -22,10 +22,15 @@ class ConfigJson implements ConfigIO
22 $data = json_decode($data, true); 22 $data = json_decode($data, true);
23 if ($data === null) { 23 if ($data === null) {
24 $errorCode = json_last_error(); 24 $errorCode = json_last_error();
25 $error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #'; 25 $error = sprintf(
26 $error .= $errorCode. '<br>➜ <code>' . json_last_error_msg() .'</code>'; 26 'An error occurred while parsing JSON configuration file (%s): error code #%d',
27 $filepath,
28 $errorCode
29 );
30 $error .= '<br>➜ <code>' . json_last_error_msg() .'</code>';
27 if ($errorCode === JSON_ERROR_SYNTAX) { 31 if ($errorCode === JSON_ERROR_SYNTAX) {
28 $error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; 32 $error .= '<br>';
33 $error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
29 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.'; 34 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
30 } 35 }
31 throw new \Exception($error); 36 throw new \Exception($error);
@@ -44,8 +49,8 @@ class ConfigJson implements ConfigIO
44 if (!file_put_contents($filepath, $data)) { 49 if (!file_put_contents($filepath, $data)) {
45 throw new \IOException( 50 throw new \IOException(
46 $filepath, 51 $filepath,
47 'Shaarli could not create the config file. 52 t('Shaarli could not create the config file. '.
48 Please make sure Shaarli has the right to write in the folder is it installed in.' 53 'Please make sure Shaarli has the right to write in the folder is it installed in.')
49 ); 54 );
50 } 55 }
51 } 56 }
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index fdd5b3d7..9e4c9f63 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -132,7 +132,7 @@ class ConfigManager
132 public function set($setting, $value, $write = false, $isLoggedIn = false) 132 public function set($setting, $value, $write = false, $isLoggedIn = false)
133 { 133 {
134 if (empty($setting) || ! is_string($setting)) { 134 if (empty($setting) || ! is_string($setting)) {
135 throw new \Exception('Invalid setting key parameter. String expected, got: '. gettype($setting)); 135 throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
136 } 136 }
137 137
138 // During the ConfigIO transition, map legacy settings to the new ones. 138 // During the ConfigIO transition, map legacy settings to the new ones.
@@ -317,6 +317,7 @@ class ConfigManager
317 $this->setEmpty('general.header_link', '?'); 317 $this->setEmpty('general.header_link', '?');
318 $this->setEmpty('general.links_per_page', 20); 318 $this->setEmpty('general.links_per_page', 20);
319 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); 319 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
320 $this->setEmpty('general.default_note_title', 'Note: ');
320 321
321 $this->setEmpty('updates.check_updates', false); 322 $this->setEmpty('updates.check_updates', false);
322 $this->setEmpty('updates.check_updates_branch', 'stable'); 323 $this->setEmpty('updates.check_updates_branch', 'stable');
@@ -327,6 +328,7 @@ class ConfigManager
327 328
328 $this->setEmpty('privacy.default_private_links', false); 329 $this->setEmpty('privacy.default_private_links', false);
329 $this->setEmpty('privacy.hide_public_links', false); 330 $this->setEmpty('privacy.hide_public_links', false);
331 $this->setEmpty('privacy.force_login', false);
330 $this->setEmpty('privacy.hide_timestamps', false); 332 $this->setEmpty('privacy.hide_timestamps', false);
331 // default state of the 'remember me' checkbox of the login form 333 // default state of the 'remember me' checkbox of the login form
332 $this->setEmpty('privacy.remember_user_default', true); 334 $this->setEmpty('privacy.remember_user_default', true);
@@ -337,6 +339,10 @@ class ConfigManager
337 $this->setEmpty('redirector.url', ''); 339 $this->setEmpty('redirector.url', '');
338 $this->setEmpty('redirector.encode_url', true); 340 $this->setEmpty('redirector.encode_url', true);
339 341
342 $this->setEmpty('translation.language', 'auto');
343 $this->setEmpty('translation.mode', 'php');
344 $this->setEmpty('translation.extensions', []);
345
340 $this->setEmpty('plugins', array()); 346 $this->setEmpty('plugins', array());
341 } 347 }
342 348
diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php
index 2633824d..2f66e8e0 100644
--- a/application/config/ConfigPhp.php
+++ b/application/config/ConfigPhp.php
@@ -118,8 +118,8 @@ class ConfigPhp implements ConfigIO
118 ) { 118 ) {
119 throw new \IOException( 119 throw new \IOException(
120 $filepath, 120 $filepath,
121 'Shaarli could not create the config file. 121 t('Shaarli could not create the config file. '.
122 Please make sure Shaarli has the right to write in the folder is it installed in.' 122 'Please make sure Shaarli has the right to write in the folder is it installed in.')
123 ); 123 );
124 } 124 }
125 } 125 }
diff --git a/application/config/exception/MissingFieldConfigException.php b/application/config/exception/MissingFieldConfigException.php
index 6346c6a9..9e0a9359 100644
--- a/application/config/exception/MissingFieldConfigException.php
+++ b/application/config/exception/MissingFieldConfigException.php
@@ -18,6 +18,6 @@ class MissingFieldConfigException extends \Exception
18 public function __construct($field) 18 public function __construct($field)
19 { 19 {
20 $this->field = $field; 20 $this->field = $field;
21 $this->message = 'Configuration value is required for '. $this->field; 21 $this->message = sprintf(t('Configuration value is required for %s'), $this->field);
22 } 22 }
23} 23}
diff --git a/application/config/exception/PluginConfigOrderException.php b/application/config/exception/PluginConfigOrderException.php
index f9d68750..f82ec26e 100644
--- a/application/config/exception/PluginConfigOrderException.php
+++ b/application/config/exception/PluginConfigOrderException.php
@@ -12,6 +12,6 @@ class PluginConfigOrderException extends \Exception
12 */ 12 */
13 public function __construct() 13 public function __construct()
14 { 14 {
15 $this->message = 'An error occurred while trying to save plugins loading order.'; 15 $this->message = t('An error occurred while trying to save plugins loading order.');
16 } 16 }
17} 17}
diff --git a/application/config/exception/UnauthorizedConfigException.php b/application/config/exception/UnauthorizedConfigException.php
index 79672c1b..72311fae 100644
--- a/application/config/exception/UnauthorizedConfigException.php
+++ b/application/config/exception/UnauthorizedConfigException.php
@@ -13,6 +13,6 @@ class UnauthorizedConfigException extends \Exception
13 */ 13 */
14 public function __construct() 14 public function __construct()
15 { 15 {
16 $this->message = 'You are not authorized to alter config.'; 16 $this->message = t('You are not authorized to alter config.');
17 } 17 }
18} 18}
diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php
index b563b23d..18e46b77 100644
--- a/application/exceptions/IOException.php
+++ b/application/exceptions/IOException.php
@@ -16,7 +16,7 @@ class IOException extends Exception
16 public function __construct($path, $message = '') 16 public function __construct($path, $message = '')
17 { 17 {
18 $this->path = $path; 18 $this->path = $path;
19 $this->message = empty($message) ? 'Error accessing' : $message; 19 $this->message = empty($message) ? t('Error accessing') : $message;
20 $this->message .= ' "' . $this->path .'"'; 20 $this->message .= ' "' . $this->path .'"';
21 } 21 }
22} 22}
diff --git a/composer.json b/composer.json
index afb8aca4..f331d6ca 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,8 @@
19 "shaarli/netscape-bookmark-parser": "^2.0", 19 "shaarli/netscape-bookmark-parser": "^2.0",
20 "erusev/parsedown": "1.6", 20 "erusev/parsedown": "1.6",
21 "slim/slim": "^3.0", 21 "slim/slim": "^3.0",
22 "pubsubhubbub/publisher": "dev-master" 22 "pubsubhubbub/publisher": "dev-master",
23 "gettext/gettext": "^4.4"
23 }, 24 },
24 "require-dev": { 25 "require-dev": {
25 "phpmd/phpmd" : "@stable", 26 "phpmd/phpmd" : "@stable",
diff --git a/composer.lock b/composer.lock
index 435d6a88..39909b8f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 "This file is @generated automatically" 5 "This file is @generated automatically"
6 ], 6 ],
7 "content-hash": "68beedbfa104c788029b079800cfd6e8", 7 "content-hash": "13b7e1e474fe9264b098ba86face0feb",
8 "packages": [ 8 "packages": [
9 { 9 {
10 "name": "container-interop/container-interop", 10 "name": "container-interop/container-interop",
@@ -77,6 +77,129 @@
77 "time": "2015-10-04T16:44:32+00:00" 77 "time": "2015-10-04T16:44:32+00:00"
78 }, 78 },
79 { 79 {
80 "name": "gettext/gettext",
81 "version": "v4.4.3",
82 "source": {
83 "type": "git",
84 "url": "https://github.com/oscarotero/Gettext.git",
85 "reference": "4f57f004635cc6311a20815ebfdc0757cb337113"
86 },
87 "dist": {
88 "type": "zip",
89 "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/4f57f004635cc6311a20815ebfdc0757cb337113",
90 "reference": "4f57f004635cc6311a20815ebfdc0757cb337113",
91 "shasum": ""
92 },
93 "require": {
94 "gettext/languages": "^2.3",
95 "php": ">=5.4.0"
96 },
97 "require-dev": {
98 "illuminate/view": "*",
99 "phpunit/phpunit": "^4.8|^5.7",
100 "squizlabs/php_codesniffer": "^3.0",
101 "symfony/yaml": "~2",
102 "twig/extensions": "*",
103 "twig/twig": "^1.31|^2.0"
104 },
105 "suggest": {
106 "illuminate/view": "Is necessary if you want to use the Blade extractor",
107 "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator",
108 "twig/extensions": "Is necessary if you want to use the Twig extractor",
109 "twig/twig": "Is necessary if you want to use the Twig extractor"
110 },
111 "type": "library",
112 "autoload": {
113 "psr-4": {
114 "Gettext\\": "src"
115 }
116 },
117 "notification-url": "https://packagist.org/downloads/",
118 "license": [
119 "MIT"
120 ],
121 "authors": [
122 {
123 "name": "Oscar Otero",
124 "email": "oom@oscarotero.com",
125 "homepage": "http://oscarotero.com",
126 "role": "Developer"
127 }
128 ],
129 "description": "PHP gettext manager",
130 "homepage": "https://github.com/oscarotero/Gettext",
131 "keywords": [
132 "JS",
133 "gettext",
134 "i18n",
135 "mo",
136 "po",
137 "translation"
138 ],
139 "time": "2017-08-09T16:59:46+00:00"
140 },
141 {
142 "name": "gettext/languages",
143 "version": "2.3.0",
144 "source": {
145 "type": "git",
146 "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
147 "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7"
148 },
149 "dist": {
150 "type": "zip",
151 "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7",
152 "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7",
153 "shasum": ""
154 },
155 "require": {
156 "php": ">=5.3"
157 },
158 "require-dev": {
159 "phpunit/phpunit": "^4"
160 },
161 "bin": [
162 "bin/export-plural-rules",
163 "bin/export-plural-rules.php"
164 ],
165 "type": "library",
166 "autoload": {
167 "psr-4": {
168 "Gettext\\Languages\\": "src/"
169 }
170 },
171 "notification-url": "https://packagist.org/downloads/",
172 "license": [
173 "MIT"
174 ],
175 "authors": [
176 {
177 "name": "Michele Locati",
178 "email": "mlocati@gmail.com",
179 "role": "Developer"
180 }
181 ],
182 "description": "gettext languages with plural rules",
183 "homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules",
184 "keywords": [
185 "cldr",
186 "i18n",
187 "internationalization",
188 "l10n",
189 "language",
190 "languages",
191 "localization",
192 "php",
193 "plural",
194 "plural rules",
195 "plurals",
196 "translate",
197 "translations",
198 "unicode"
199 ],
200 "time": "2017-03-23T17:02:28+00:00"
201 },
202 {
80 "name": "katzgrau/klogger", 203 "name": "katzgrau/klogger",
81 "version": "1.2.1", 204 "version": "1.2.1",
82 "source": { 205 "source": {
@@ -371,12 +494,12 @@
371 "source": { 494 "source": {
372 "type": "git", 495 "type": "git",
373 "url": "https://github.com/pubsubhubbub/php-publisher.git", 496 "url": "https://github.com/pubsubhubbub/php-publisher.git",
374 "reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7" 497 "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f"
375 }, 498 },
376 "dist": { 499 "dist": {
377 "type": "zip", 500 "type": "zip",
378 "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/a5d6a0e1cc9d49101c3904480e5b06cbb8addba7", 501 "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f",
379 "reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7", 502 "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f",
380 "shasum": "" 503 "shasum": ""
381 }, 504 },
382 "require": { 505 "require": {
@@ -406,7 +529,7 @@
406 "publishers", 529 "publishers",
407 "pubsubhubbub" 530 "pubsubhubbub"
408 ], 531 ],
409 "time": "2016-11-15T06:24:01+00:00" 532 "time": "2017-10-08T10:59:41+00:00"
410 }, 533 },
411 { 534 {
412 "name": "shaarli/netscape-bookmark-parser", 535 "name": "shaarli/netscape-bookmark-parser",
@@ -632,16 +755,16 @@
632 }, 755 },
633 { 756 {
634 "name": "phpdocumentor/reflection-common", 757 "name": "phpdocumentor/reflection-common",
635 "version": "1.0", 758 "version": "1.0.1",
636 "source": { 759 "source": {
637 "type": "git", 760 "type": "git",
638 "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 761 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
639 "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" 762 "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
640 }, 763 },
641 "dist": { 764 "dist": {
642 "type": "zip", 765 "type": "zip",
643 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 766 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
644 "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 767 "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
645 "shasum": "" 768 "shasum": ""
646 }, 769 },
647 "require": { 770 "require": {
@@ -682,20 +805,20 @@
682 "reflection", 805 "reflection",
683 "static analysis" 806 "static analysis"
684 ], 807 ],
685 "time": "2015-12-27T11:43:31+00:00" 808 "time": "2017-09-11T18:02:19+00:00"
686 }, 809 },
687 { 810 {
688 "name": "phpdocumentor/reflection-docblock", 811 "name": "phpdocumentor/reflection-docblock",
689 "version": "3.2.1", 812 "version": "3.2.2",
690 "source": { 813 "source": {
691 "type": "git", 814 "type": "git",
692 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 815 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
693 "reference": "183824db76118b9dddffc7e522b91fa175f75119" 816 "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157"
694 }, 817 },
695 "dist": { 818 "dist": {
696 "type": "zip", 819 "type": "zip",
697 "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119", 820 "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157",
698 "reference": "183824db76118b9dddffc7e522b91fa175f75119", 821 "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157",
699 "shasum": "" 822 "shasum": ""
700 }, 823 },
701 "require": { 824 "require": {
@@ -727,7 +850,7 @@
727 } 850 }
728 ], 851 ],
729 "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 852 "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
730 "time": "2017-08-04T20:55:59+00:00" 853 "time": "2017-08-08T06:39:58+00:00"
731 }, 854 },
732 { 855 {
733 "name": "phpdocumentor/type-resolver", 856 "name": "phpdocumentor/type-resolver",
@@ -844,22 +967,22 @@
844 }, 967 },
845 { 968 {
846 "name": "phpspec/prophecy", 969 "name": "phpspec/prophecy",
847 "version": "v1.7.0", 970 "version": "v1.7.2",
848 "source": { 971 "source": {
849 "type": "git", 972 "type": "git",
850 "url": "https://github.com/phpspec/prophecy.git", 973 "url": "https://github.com/phpspec/prophecy.git",
851 "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" 974 "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
852 }, 975 },
853 "dist": { 976 "dist": {
854 "type": "zip", 977 "type": "zip",
855 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", 978 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
856 "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", 979 "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
857 "shasum": "" 980 "shasum": ""
858 }, 981 },
859 "require": { 982 "require": {
860 "doctrine/instantiator": "^1.0.2", 983 "doctrine/instantiator": "^1.0.2",
861 "php": "^5.3|^7.0", 984 "php": "^5.3|^7.0",
862 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", 985 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
863 "sebastian/comparator": "^1.1|^2.0", 986 "sebastian/comparator": "^1.1|^2.0",
864 "sebastian/recursion-context": "^1.0|^2.0|^3.0" 987 "sebastian/recursion-context": "^1.0|^2.0|^3.0"
865 }, 988 },
@@ -870,7 +993,7 @@
870 "type": "library", 993 "type": "library",
871 "extra": { 994 "extra": {
872 "branch-alias": { 995 "branch-alias": {
873 "dev-master": "1.6.x-dev" 996 "dev-master": "1.7.x-dev"
874 } 997 }
875 }, 998 },
876 "autoload": { 999 "autoload": {
@@ -903,7 +1026,7 @@
903 "spy", 1026 "spy",
904 "stub" 1027 "stub"
905 ], 1028 ],
906 "time": "2017-03-02T20:05:34+00:00" 1029 "time": "2017-09-04T11:05:03+00:00"
907 }, 1030 },
908 { 1031 {
909 "name": "phpunit/php-code-coverage", 1032 "name": "phpunit/php-code-coverage",
@@ -1875,20 +1998,20 @@
1875 }, 1998 },
1876 { 1999 {
1877 "name": "symfony/config", 2000 "name": "symfony/config",
1878 "version": "v3.3.6", 2001 "version": "v3.3.10",
1879 "source": { 2002 "source": {
1880 "type": "git", 2003 "type": "git",
1881 "url": "https://github.com/symfony/config.git", 2004 "url": "https://github.com/symfony/config.git",
1882 "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297" 2005 "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
1883 }, 2006 },
1884 "dist": { 2007 "dist": {
1885 "type": "zip", 2008 "type": "zip",
1886 "url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297", 2009 "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
1887 "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297", 2010 "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
1888 "shasum": "" 2011 "shasum": ""
1889 }, 2012 },
1890 "require": { 2013 "require": {
1891 "php": ">=5.5.9", 2014 "php": "^5.5.9|>=7.0.8",
1892 "symfony/filesystem": "~2.8|~3.0" 2015 "symfony/filesystem": "~2.8|~3.0"
1893 }, 2016 },
1894 "conflict": { 2017 "conflict": {
@@ -1933,20 +2056,20 @@
1933 ], 2056 ],
1934 "description": "Symfony Config Component", 2057 "description": "Symfony Config Component",
1935 "homepage": "https://symfony.com", 2058 "homepage": "https://symfony.com",
1936 "time": "2017-07-19T07:37:29+00:00" 2059 "time": "2017-10-04T18:56:58+00:00"
1937 }, 2060 },
1938 { 2061 {
1939 "name": "symfony/console", 2062 "name": "symfony/console",
1940 "version": "v2.8.26", 2063 "version": "v2.8.28",
1941 "source": { 2064 "source": {
1942 "type": "git", 2065 "type": "git",
1943 "url": "https://github.com/symfony/console.git", 2066 "url": "https://github.com/symfony/console.git",
1944 "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd" 2067 "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853"
1945 }, 2068 },
1946 "dist": { 2069 "dist": {
1947 "type": "zip", 2070 "type": "zip",
1948 "url": "https://api.github.com/repos/symfony/console/zipball/32a3c6b3398de5db8ed381f4ef92970c59c2fcdd", 2071 "url": "https://api.github.com/repos/symfony/console/zipball/f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
1949 "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd", 2072 "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
1950 "shasum": "" 2073 "shasum": ""
1951 }, 2074 },
1952 "require": { 2075 "require": {
@@ -1994,7 +2117,7 @@
1994 ], 2117 ],
1995 "description": "Symfony Console Component", 2118 "description": "Symfony Console Component",
1996 "homepage": "https://symfony.com", 2119 "homepage": "https://symfony.com",
1997 "time": "2017-07-29T21:26:04+00:00" 2120 "time": "2017-10-01T21:00:16+00:00"
1998 }, 2121 },
1999 { 2122 {
2000 "name": "symfony/debug", 2123 "name": "symfony/debug",
@@ -2055,20 +2178,20 @@
2055 }, 2178 },
2056 { 2179 {
2057 "name": "symfony/dependency-injection", 2180 "name": "symfony/dependency-injection",
2058 "version": "v3.3.6", 2181 "version": "v3.3.10",
2059 "source": { 2182 "source": {
2060 "type": "git", 2183 "type": "git",
2061 "url": "https://github.com/symfony/dependency-injection.git", 2184 "url": "https://github.com/symfony/dependency-injection.git",
2062 "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0" 2185 "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
2063 }, 2186 },
2064 "dist": { 2187 "dist": {
2065 "type": "zip", 2188 "type": "zip",
2066 "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8d70987f991481e809c63681ffe8ce3f3fde68a0", 2189 "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
2067 "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0", 2190 "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
2068 "shasum": "" 2191 "shasum": ""
2069 }, 2192 },
2070 "require": { 2193 "require": {
2071 "php": ">=5.5.9", 2194 "php": "^5.5.9|>=7.0.8",
2072 "psr/container": "^1.0" 2195 "psr/container": "^1.0"
2073 }, 2196 },
2074 "conflict": { 2197 "conflict": {
@@ -2121,24 +2244,24 @@
2121 ], 2244 ],
2122 "description": "Symfony DependencyInjection Component", 2245 "description": "Symfony DependencyInjection Component",
2123 "homepage": "https://symfony.com", 2246 "homepage": "https://symfony.com",
2124 "time": "2017-07-28T15:27:31+00:00" 2247 "time": "2017-10-04T17:15:30+00:00"
2125 }, 2248 },
2126 { 2249 {
2127 "name": "symfony/filesystem", 2250 "name": "symfony/filesystem",
2128 "version": "v3.3.6", 2251 "version": "v3.3.10",
2129 "source": { 2252 "source": {
2130 "type": "git", 2253 "type": "git",
2131 "url": "https://github.com/symfony/filesystem.git", 2254 "url": "https://github.com/symfony/filesystem.git",
2132 "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" 2255 "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
2133 }, 2256 },
2134 "dist": { 2257 "dist": {
2135 "type": "zip", 2258 "type": "zip",
2136 "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", 2259 "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
2137 "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", 2260 "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
2138 "shasum": "" 2261 "shasum": ""
2139 }, 2262 },
2140 "require": { 2263 "require": {
2141 "php": ">=5.5.9" 2264 "php": "^5.5.9|>=7.0.8"
2142 }, 2265 },
2143 "type": "library", 2266 "type": "library",
2144 "extra": { 2267 "extra": {
@@ -2170,24 +2293,24 @@
2170 ], 2293 ],
2171 "description": "Symfony Filesystem Component", 2294 "description": "Symfony Filesystem Component",
2172 "homepage": "https://symfony.com", 2295 "homepage": "https://symfony.com",
2173 "time": "2017-07-11T07:17:58+00:00" 2296 "time": "2017-10-03T13:33:10+00:00"
2174 }, 2297 },
2175 { 2298 {
2176 "name": "symfony/finder", 2299 "name": "symfony/finder",
2177 "version": "v3.3.6", 2300 "version": "v3.3.10",
2178 "source": { 2301 "source": {
2179 "type": "git", 2302 "type": "git",
2180 "url": "https://github.com/symfony/finder.git", 2303 "url": "https://github.com/symfony/finder.git",
2181 "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" 2304 "reference": "773e19a491d97926f236942484cb541560ce862d"
2182 }, 2305 },
2183 "dist": { 2306 "dist": {
2184 "type": "zip", 2307 "type": "zip",
2185 "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", 2308 "url": "https://api.github.com/repos/symfony/finder/zipball/773e19a491d97926f236942484cb541560ce862d",
2186 "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", 2309 "reference": "773e19a491d97926f236942484cb541560ce862d",
2187 "shasum": "" 2310 "shasum": ""
2188 }, 2311 },
2189 "require": { 2312 "require": {
2190 "php": ">=5.5.9" 2313 "php": "^5.5.9|>=7.0.8"
2191 }, 2314 },
2192 "type": "library", 2315 "type": "library",
2193 "extra": { 2316 "extra": {
@@ -2219,20 +2342,20 @@
2219 ], 2342 ],
2220 "description": "Symfony Finder Component", 2343 "description": "Symfony Finder Component",
2221 "homepage": "https://symfony.com", 2344 "homepage": "https://symfony.com",
2222 "time": "2017-06-01T21:01:25+00:00" 2345 "time": "2017-10-02T06:42:24+00:00"
2223 }, 2346 },
2224 { 2347 {
2225 "name": "symfony/polyfill-mbstring", 2348 "name": "symfony/polyfill-mbstring",
2226 "version": "v1.4.0", 2349 "version": "v1.6.0",
2227 "source": { 2350 "source": {
2228 "type": "git", 2351 "type": "git",
2229 "url": "https://github.com/symfony/polyfill-mbstring.git", 2352 "url": "https://github.com/symfony/polyfill-mbstring.git",
2230 "reference": "f29dca382a6485c3cbe6379f0c61230167681937" 2353 "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
2231 }, 2354 },
2232 "dist": { 2355 "dist": {
2233 "type": "zip", 2356 "type": "zip",
2234 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", 2357 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
2235 "reference": "f29dca382a6485c3cbe6379f0c61230167681937", 2358 "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
2236 "shasum": "" 2359 "shasum": ""
2237 }, 2360 },
2238 "require": { 2361 "require": {
@@ -2244,7 +2367,7 @@
2244 "type": "library", 2367 "type": "library",
2245 "extra": { 2368 "extra": {
2246 "branch-alias": { 2369 "branch-alias": {
2247 "dev-master": "1.4-dev" 2370 "dev-master": "1.6-dev"
2248 } 2371 }
2249 }, 2372 },
2250 "autoload": { 2373 "autoload": {
@@ -2278,24 +2401,24 @@
2278 "portable", 2401 "portable",
2279 "shim" 2402 "shim"
2280 ], 2403 ],
2281 "time": "2017-06-09T14:24:12+00:00" 2404 "time": "2017-10-11T12:05:26+00:00"
2282 }, 2405 },
2283 { 2406 {
2284 "name": "symfony/yaml", 2407 "name": "symfony/yaml",
2285 "version": "v3.3.6", 2408 "version": "v3.3.10",
2286 "source": { 2409 "source": {
2287 "type": "git", 2410 "type": "git",
2288 "url": "https://github.com/symfony/yaml.git", 2411 "url": "https://github.com/symfony/yaml.git",
2289 "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed" 2412 "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
2290 }, 2413 },
2291 "dist": { 2414 "dist": {
2292 "type": "zip", 2415 "type": "zip",
2293 "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed", 2416 "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
2294 "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed", 2417 "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
2295 "shasum": "" 2418 "shasum": ""
2296 }, 2419 },
2297 "require": { 2420 "require": {
2298 "php": ">=5.5.9" 2421 "php": "^5.5.9|>=7.0.8"
2299 }, 2422 },
2300 "require-dev": { 2423 "require-dev": {
2301 "symfony/console": "~2.8|~3.0" 2424 "symfony/console": "~2.8|~3.0"
@@ -2333,7 +2456,7 @@
2333 ], 2456 ],
2334 "description": "Symfony Yaml Component", 2457 "description": "Symfony Yaml Component",
2335 "homepage": "https://symfony.com", 2458 "homepage": "https://symfony.com",
2336 "time": "2017-07-23T12:43:26+00:00" 2459 "time": "2017-10-05T14:43:42+00:00"
2337 }, 2460 },
2338 { 2461 {
2339 "name": "theseer/fdomdocument", 2462 "name": "theseer/fdomdocument",
diff --git a/data/.htaccess b/data/.htaccess
index f601c1ee..1d49da37 100644
--- a/data/.htaccess
+++ b/data/.htaccess
@@ -1,10 +1,16 @@
1<IfModule version_module> 1<IfModule version_module>
2 <IfVersion >= 2.4> 2 <IfVersion >= 2.4>
3 Require all denied 3 Require all denied
4 <Files "user.css">
5 Require all granted
6 </Files>
4 </IfVersion> 7 </IfVersion>
5 <IfVersion < 2.4> 8 <IfVersion < 2.4>
6 Allow from none 9 Allow from none
7 Deny from all 10 Deny from all
11 <Files "user.css">
12 Allow from all
13 </Files>
8 </IfVersion> 14 </IfVersion>
9</IfModule> 15</IfModule>
10 16
diff --git a/doc/md/Download-and-Installation.md b/doc/md/Download-and-Installation.md
index e5e929ef..be848c97 100644
--- a/doc/md/Download-and-Installation.md
+++ b/doc/md/Download-and-Installation.md
@@ -4,11 +4,18 @@ Document Root (or directly at the document root).
4Also, please make sure your server meets the [requirements](Server-requirements) 4Also, please make sure your server meets the [requirements](Server-requirements)
5and is properly [configured](Server-configuration). 5and is properly [configured](Server-configuration).
6 6
7Several releases are available: 7Multiple releases branches are available:
8
9- latest (last release)
10- stable (previous major release)
11- master (development)
12
13Using one of the following methods:
8 14
9- by downloading full release archives including all dependencies 15- by downloading full release archives including all dependencies
10- by downloading Github archives 16- by downloading Github archives
11- by cloning the Git repository 17- by cloning the Git repository
18- using Docker: [see the documentation](docker/shaarli-images)
12 19
13--- 20---
14 21
@@ -28,14 +35,16 @@ $ unzip shaarli-v0.9.1-full.zip
28$ mv Shaarli /path/to/shaarli/ 35$ mv Shaarli /path/to/shaarli/
29``` 36```
30 37
31In 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).| 38In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page.
39Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).
32 40
33### Using git 41### Using git
34 42
35``` 43```
36$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/ 44$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
37$ git clone -b v0.9 https://github.com/shaarli/Shaarli.git . 45$ git clone -b latest https://github.com/shaarli/Shaarli.git .
38$ composer install --no-dev --prefer-dist 46$ composer install --no-dev --prefer-dist
47$ make translate
39``` 48```
40 49
41## Stable version 50## Stable version
@@ -83,13 +92,14 @@ $ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
83# install/update third-party dependencies 92# install/update third-party dependencies
84$ cd /path/to/shaarli 93$ cd /path/to/shaarli
85$ composer install --no-dev --prefer-dist 94$ composer install --no-dev --prefer-dist
95$ make translate
86``` 96```
87 97
88## Finish Installation 98## Finish Installation
89 99
90Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser. 100Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.
91 101
92![install screenshot](http://i.imgur.com/wuMpDSN.png) 102![install screenshot](images/install-shaarli.png)
93 103
94Setup your Shaarli installation, and it's ready to use! 104Setup your Shaarli installation, and it's ready to use!
95 105
diff --git a/doc/md/Server-requirements.md b/doc/md/Server-requirements.md
index 707af762..400b85a9 100644
--- a/doc/md/Server-requirements.md
+++ b/doc/md/Server-requirements.md
@@ -39,3 +39,4 @@ Extension | Required? | Usage
39[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing 39[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing
40[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`) 40[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
41[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way 41[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
42[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md
index d90e95eb..920c7e27 100644
--- a/doc/md/Shaarli-configuration.md
+++ b/doc/md/Shaarli-configuration.md
@@ -55,6 +55,7 @@ _These settings should not be edited_
55- **links_per_page**: Number of shaares displayed per page. 55- **links_per_page**: Number of shaares displayed per page.
56- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php). 56- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).
57- **enabled_plugins**: List of enabled plugins. 57- **enabled_plugins**: List of enabled plugins.
58- **default_note_title**: Default title of a new note.
58 59
59### Security 60### Security
60 61
@@ -80,6 +81,20 @@ _These settings should not be edited_
80- **page_cache**: Shaarli's internal cache directory. 81- **page_cache**: Shaarli's internal cache directory.
81- **ban_file**: Banned IP file path. 82- **ban_file**: Banned IP file path.
82 83
84### Translation
85
86- **language**: translation language (also see [Translations](Translations))
87 - **auto** (default): The translation language is chosen from the browser locale.
88 It means that the language can be different for 2 different visitors depending on their locale.
89 - **en**: Use the English translation.
90 - **fr**: Use the French translation.
91- **mode**:
92 - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
93 - **gettext**: Use PHP builtin gettext extension
94 (faster, but requires `php-gettext` to be installed and to reload the web server on update)
95- **extension**: Translation extensions for custom themes or plugins.
96Must be an associative array: `translation domain => translation path`.
97
83### Updates 98### Updates
84 99
85- **check_updates**: Enable or disable update check to the git repository. 100- **check_updates**: Enable or disable update check to the git repository.
@@ -90,6 +105,7 @@ _These settings should not be edited_
90 105
91- **default_private_links**: Check the private checkbox by default for every new link. 106- **default_private_links**: Check the private checkbox by default for every new link.
92- **hide_public_links**: All links are hidden while logged out. 107- **hide_public_links**: All links are hidden while logged out.
108- **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page.
93- **hide_timestamps**: Timestamps are hidden. 109- **hide_timestamps**: Timestamps are hidden.
94- **remember_user_default**: Default state of the login page's *remember me* checkbox 110- **remember_user_default**: Default state of the login page's *remember me* checkbox
95 - `true`: checked by default, `false`: unchecked by default 111 - `true`: checked by default, `false`: unchecked by default
@@ -194,6 +210,7 @@ _These settings should not be edited_
194 "privacy": { 210 "privacy": {
195 "default_private_links": true, 211 "default_private_links": true,
196 "hide_public_links": false, 212 "hide_public_links": false,
213 "force_login": false,
197 "hide_timestamps": false, 214 "hide_timestamps": false,
198 "remember_user_default": true 215 "remember_user_default": true
199 }, 216 },
@@ -208,6 +225,13 @@ _These settings should not be edited_
208 "plugins": { 225 "plugins": {
209 "WALLABAG_URL": "http://demo.wallabag.org", 226 "WALLABAG_URL": "http://demo.wallabag.org",
210 "WALLABAG_VERSION": "1" 227 "WALLABAG_VERSION": "1"
228 },
229 "translation": {
230 "language": "fr",
231 "mode": "php",
232 "extensions": {
233 "demo": "plugins/demo_plugin/languages/"
234 }
211 } 235 }
212} ?> 236} ?>
213``` 237```
diff --git a/doc/md/Translations.md b/doc/md/Translations.md
new file mode 100644
index 00000000..54a36655
--- /dev/null
+++ b/doc/md/Translations.md
@@ -0,0 +1,152 @@
1## Translations
2
3Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations
4since `>= v0.9.2`.
5
6Note that only the `default` theme supports translations.
7
8### Contributing
9
10We encourage the community to contribute to Shaarli's translation either by improving existing
11translations or submitting a new language.
12
13Contributing to the translation does not require development skill.
14
15Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`)
16is not stored on the repository, and is generated during the release process.
17
18### How to
19
20First, install [Poedit](https://poedit.net/) tool.
21
22Poedit will extract strings to translate from the PHP source code.
23
24**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
25every translatable string.
26
27You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended)
28or visit every template page in your browser to generate cache files, while logged in.
29
30Here is a list :
31
32```
33http://<replace_domain>/
34http://<replace_domain>/?nonope
35http://<replace_domain>/?do=addlink
36http://<replace_domain>/?do=changepasswd
37http://<replace_domain>/?do=changetag
38http://<replace_domain>/?do=configure
39http://<replace_domain>/?do=tools
40http://<replace_domain>/?do=daily
41http://<replace_domain>/?post
42http://<replace_domain>/?do=export
43http://<replace_domain>/?do=import
44http://<replace_domain>/?do=login
45http://<replace_domain>/?do=picwall
46http://<replace_domain>/?do=pluginadmin
47http://<replace_domain>/?do=tagcloud
48http://<replace_domain>/?do=taglist
49```
50
51#### Improve existing translation
52
53In Poedit, click on "Edit a Translation", and from Shaarli's directory open
54`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
55
56The existing list of translatable strings should have been loaded, then click on the "Update" button.
57
58You can start editing the translation.
59
60![poedit-screenshot](images/poedit-1.jpg)
61
62Save when you're done, then you can submit a pull request containing the updated `shaarli.po`.
63
64#### Add a new language
65
66Open Poedit and select "Create New Translation", then from Shaarli's directory open
67`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
68
69Then select the language you want to create.
70
71Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.
72`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
73format in lowercase (e.g. `de` for German).
74
75Then click on the "Update" button, and you can start to translate every available string.
76
77Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
78
79### Extend Shaarli's translation
80
81If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
82but you won't be able to able to override Shaarli's translation.
83
84However, you can add your own translation domain which extends the main translation list.
85
86> Note that you can find a live example of translation extension in the `demo_plugin`.
87
88First, create your translation files tree directory:
89
90```
91<your_module>/languages/<ISO 3166-1 alpha-2 language code>/LC_MESSAGES/
92```
93
94Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
95`my_theme.po`.
96
97Users have to register your extension in their configuration with the parameter
98`translation.extensions.<domain>: <translation files path>`.
99
100Example:
101
102```php
103if (! $conf->exists('translation.extensions.my_theme')) {
104 $conf->set('translation.extensions.my_theme', '<your_module>/languages/');
105 $conf->write(true);
106}
107```
108
109> Note that the page needs to be reloaded after the registration.
110
111It is then recommended to create a custom translation function which will call the `t()` function with your domain.
112For example :
113
114```php
115function my_theme_t($text, $nText = '', $nb = 1)
116{
117 return t($text, $nText, $nb, 'my_theme'); // the last parameter is your translation domain.
118}
119```
120
121All strings which can be translated should be processed through your function:
122
123```php
124my_theme_t('Comment');
125my_theme_t('Comment', 'Comments', 2);
126```
127
128Or in templates:
129
130```php
131{'Comment'|my_theme_t}
132{function="my_theme_t('Comment', 'Comments', 2)"}
133```
134
135> Note than in template, you need to visit your page at least once to generate a cache file.
136
137When you're done, open Poedit and load translation strings from sources:
138
139 1. `File > New`
140 2. Choose your language
141 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
142 4. Go to `Catalog > Properties...`
143 5. Fill the `Translation Properties` tab
144 6. Add your source path in the `Sources Paths` tab
145 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
146
147```
148my_theme_t
149my_theme_t:1,2
150```
151
152Click on the "Update" button and you're free to start your translations!
diff --git a/doc/md/Unit-tests-Docker.md b/doc/md/Unit-tests-Docker.md
new file mode 100644
index 00000000..c2de7cc7
--- /dev/null
+++ b/doc/md/Unit-tests-Docker.md
@@ -0,0 +1,56 @@
1## Running tests inside Docker containers
2
3Read first:
4
5- [Docker 101](docker/docker-101.md)
6- [Docker resources](docker/resources.md)
7- [Unit tests](Unit-tests.md)
8
9### Docker test images
10
11Test Dockerfiles are located under `docker/tests/<distribution>/Dockerfile`,
12and can be used to build Docker images to run Shaarli test suites under common
13Linux environments.
14
15Dockerfiles are provided for the following environments:
16
17- `alpine36` - [Alpine 3.6](https://www.alpinelinux.org/downloads/)
18- `debian8` - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldstable)
19- `debian9` - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (stable)
20- `ubuntu16` - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (LTS)
21
22What's behind the curtains:
23
24- each image provides:
25 - a base Linux OS
26 - Shaarli PHP dependencies (OS packages)
27 - test PHP dependencies (OS packages)
28 - Composer
29- the local workspace is mapped to the container's `/shaarli/` directory,
30- the files are rsync'd to so tests are run using a standard Linux user account
31 (running tests as `root` would bypass permission checks and may hide issues)
32- the tests are run inside the container.
33
34### Building test images
35
36```bash
37# build the Debian 9 Docker image
38$ cd /path/to/shaarli
39$ cd docker/test/debian9
40$ docker build -t shaarli-test:debian9 .
41```
42
43### Running tests
44
45```bash
46$ cd /path/to/shaarli
47
48# install/update 3rd-party test dependencies
49$ composer install --prefer-dist
50
51# run tests using the freshly built image
52$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test
53
54# run the full test campaign
55$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests
56```
diff --git a/doc/md/Upgrade-and-migration.md b/doc/md/Upgrade-and-migration.md
index b3a08764..1dc07339 100644
--- a/doc/md/Upgrade-and-migration.md
+++ b/doc/md/Upgrade-and-migration.md
@@ -14,7 +14,7 @@ Shaarli stores all user data under the `data` directory:
14- `data/ipbans.php` - banned IP addresses 14- `data/ipbans.php` - banned IP addresses
15- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run 15- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run
16 16
17See [Shaarli configuration](Shaarli configuration) for more information about Shaarli resources. 17See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources.
18 18
19It is recommended to backup this repository _before_ starting updating/upgrading Shaarli: 19It is recommended to backup this repository _before_ starting updating/upgrading Shaarli:
20 20
@@ -27,7 +27,7 @@ As all user data is kept under `data`, this is the only directory you need to wo
27 27
28- backup the `data` directory 28- backup the `data` directory
29- install or update Shaarli: 29- install or update Shaarli:
30 - fresh installation - see [Download and installation](Download and installation) 30 - fresh installation - see [Download and installation](Download-and-installation)
31 - update - see the following sections 31 - update - see the following sections
32- check or restore the `data` directory 32- check or restore the `data` directory
33 33
@@ -35,10 +35,13 @@ As all user data is kept under `data`, this is the only directory you need to wo
35 35
36All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. 36All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.
37 37
38We 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. 38We 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.
39 39
40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! 40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
41 41
42If you use translations in gettext mode - meaning you manually changed the default mode -,
43reload your web server.
44
42After 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). 45After 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).
43 46
44## Upgrading with Git 47## Upgrading with Git
@@ -72,6 +75,14 @@ Updating dependencies
72 Downloading: 100% 75 Downloading: 100%
73``` 76```
74 77
78Shaarli >= `v0.9.2` supports translations:
79
80```bash
81$ make translate
82```
83
84If you use translations in gettext mode, reload your web server.
85
75### Migrating and upgrading from Sebsauvage's repository 86### Migrating and upgrading from Sebsauvage's repository
76 87
77If 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. 88If 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
151 Downloading: 100% 162 Downloading: 100%
152``` 163```
153 164
165Shaarli >= `v0.9.2` supports translations:
166
167```bash
168$ make translate
169```
170
171If you use translations in gettext mode, reload your web server.
172
154Optionally, you can delete information related to the legacy version: 173Optionally, you can delete information related to the legacy version:
155 174
156```bash 175```bash
@@ -173,7 +192,7 @@ Total 3317 (delta 2050), reused 3301 (delta 2034)to
173 192
174#### Step 3: configuration 193#### Step 3: configuration
175 194
176After 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). 195After 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).
177 196
178## Troubleshooting 197## Troubleshooting
179 198
diff --git a/doc/md/docker/docker-101.md b/doc/md/docker/docker-101.md
index b02dd149..a9c00b85 100644
--- a/doc/md/docker/docker-101.md
+++ b/doc/md/docker/docker-101.md
@@ -60,3 +60,81 @@ wheezy: Pulling from debian
60Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe 60Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe
61Status: Downloaded newer image for debian:wheezy 61Status: Downloaded newer image for debian:wheezy
62``` 62```
63
64Docker re-uses layers already downloaded. In other words if you have images based on Alpine or some Ubuntu version for example, those can share disk space.
65
66### Start a container
67A container is an instance created from an image, that can be run and that keeps running until its main process exits. Or until the user stops the container.
68
69The simplest way to start a container from image is ``docker run``. It also pulls the image for you if it is not locally available. For more advanced use, refer to ``docker create``.
70
71Stopped containers are not destroyed, unless you specify ``--rm``. To view all created, running and stopped containers, enter:
72```bash
73$ docker ps -a
74```
75
76Some containers may be designed or configured to be restarted, others are not. Also remember both network ports and volumes of a container are created on start, and not editable later.
77
78### Access a running container
79A running container is accessible using ``docker exec``, or ``docker copy``. You can use ``exec`` to start a root shell in the Shaarli container:
80```bash
81$ docker exec -ti <container-name-or-id> bash
82```
83Note the names and ID's of containers are listed in ``docker ps``. You can even type only one or two letters of the ID, given they are unique.
84
85Access can also be through one or more network ports, or disk volumes. Both are specified on and fixed on ``docker create`` or ``run``.
86
87You can view the console output of the main container process too:
88```bash
89$ docker logs -f <container-name-or-id>
90```
91
92### Docker disk use
93Trying out different images can fill some gigabytes of disk quickly. Besides images, the docker volumes usually take up most disk space.
94
95If you care only about trying out docker and not about what is running or saved, the following commands should help you out quickly if you run low on disk space:
96
97```bash
98$ docker rmi -f $(docker images -aq) # remove or mark all images for disposal
99$ docker volume rm $(docker volume ls -q) # remove all volumes
100```
101
102### Systemd config
103Systemd is the process manager of choice on Debian-based distributions. Once you have a ``docker`` service installed, you can use the following steps to set up Shaarli to run on system start.
104
105```bash
106systemctl enable /etc/systemd/system/docker.shaarli.service
107systemctl start docker.shaarli
108systemctl status docker.*
109journalctl -f # inspect system log if needed
110```
111
112You will need sudo or a root terminal to perform some or all of the steps above. Here are the contents for the service file:
113```
114[Unit]
115Description=Shaarli Bookmark Manager Container
116After=docker.service
117Requires=docker.service
118
119
120[Service]
121Restart=always
122
123# Put any environment you want in an included file, like $host- or $domainname in this example
124EnvironmentFile=/etc/sysconfig/box-environment
125
126# It's just an example..
127ExecStart=/usr/bin/docker run \
128 -p 28010:80 \
129 --name ${hostname}-shaarli \
130 --hostname shaarli.${domainname} \
131 -v /srv/docker-volumes-local/shaarli-data:/var/www/shaarli/data:rw \
132 -v /etc/localtime:/etc/localtime:ro \
133 shaarli/shaarli:latest
134
135ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli
136
137
138[Install]
139WantedBy=multi-user.target
140```
diff --git a/doc/md/docker/reverse-proxy-configuration.md b/doc/md/docker/reverse-proxy-configuration.md
index 91ffecff..6066140e 100644
--- a/doc/md/docker/reverse-proxy-configuration.md
+++ b/doc/md/docker/reverse-proxy-configuration.md
@@ -1,6 +1,120 @@
1## Foreword
2
3This guide assumes that:
4
5- Shaarli runs in a Docker container
6- The host's `10080` port is mapped to the container's `80` port
7- Shaarli's Fully Qualified Domain Name (FQDN) is `shaarli.domain.tld`
8- HTTP traffic is redirected to HTTPS
9
10## Apache
11
12- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/)
13 - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
14 - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
15
16The following HTTP headers are set by using the `ProxyPass` directive:
17
18- `X-Forwarded-For`
19- `X-Forwarded-Host`
20- `X-Forwarded-Server`
21
22```apache
23<VirtualHost *:80>
24 ServerName shaarli.domain.tld
25 Redirect permanent / https://shaarli.domain.tld
26</VirtualHost>
27
28<VirtualHost *:443>
29 ServerName shaarli.domain.tld
30
31 SSLEngine on
32 SSLCertificateFile /path/to/cert
33 SSLCertificateKeyFile /path/to/certkey
34
35 LogLevel warn
36 ErrorLog /var/log/apache2/shaarli-error.log
37 CustomLog /var/log/apache2/shaarli-access.log combined
38
39 RequestHeader set X-Forwarded-Proto "https"
40
41 ProxyPass / http://127.0.0.1:10080/
42 ProxyPassReverse / http://127.0.0.1:10080/
43</VirtualHost>
44```
1 45
2TODO, see https://github.com/shaarli/Shaarli/issues/888
3 46
4## HAProxy 47## HAProxy
5 48
49- [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/)
50
51```conf
52global
53 [...]
54
55defaults
56 [...]
57
58frontend http-in
59 bind :80
60 redirect scheme https code 301 if !{ ssl_fc }
61
62 bind :443 ssl crt /path/to/cert.pem
63
64 default_backend shaarli
65
66
67backend shaarli
68 mode http
69 option http-server-close
70 option forwardfor
71 reqadd X-Forwarded-Proto: https
72
73 server shaarli1 127.0.0.1:10080
74```
75
76
6## Nginx 77## Nginx
78
79- [Nginx documentation](https://nginx.org/en/docs/)
80
81```nginx
82http {
83 [...]
84
85 index index.html index.php;
86
87 root /home/john/web;
88 access_log /var/log/nginx/access.log;
89 error_log /var/log/nginx/error.log;
90
91 server {
92 listen 80;
93 server_name shaarli.domain.tld;
94 return 301 https://shaarli.domain.tld$request_uri;
95 }
96
97 server {
98 listen 443 ssl http2;
99 server_name shaarli.domain.tld;
100
101 ssl_certificate /path/to/cert
102 ssl_certificate_key /path/to/certkey
103
104 location / {
105 proxy_set_header X-Real-IP $remote_addr;
106 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
107 proxy_set_header X-Forwarded-Proto $scheme;
108 proxy_set_header X-Forwarded-Host $host;
109
110 proxy_pass http://localhost:10080/;
111 proxy_set_header Host $host;
112 proxy_connect_timeout 30s;
113 proxy_read_timeout 120s;
114
115 access_log /var/log/nginx/shaarli.access.log;
116 error_log /var/log/nginx/shaarli.error.log;
117 }
118 }
119}
120```
diff --git a/doc/md/docker/shaarli-images.md b/doc/md/docker/shaarli-images.md
index 6d108d21..1d19510a 100644
--- a/doc/md/docker/shaarli-images.md
+++ b/doc/md/docker/shaarli-images.md
@@ -5,14 +5,23 @@ The images can be found in the [`shaarli/shaarli`](https://hub.docker.com/r/shaa
5repository. 5repository.
6 6
7### Available image tags 7### Available image tags
8- `latest`: master branch (tarball release) 8- `latest`: latest branch (tarball release)
9- `master`: master branch (tarball release)
9- `stable`: stable branch (tarball release) 10- `stable`: stable branch (tarball release)
10 11
11All images rely on: 12The `latest` and `master` images rely on:
13
14- [Alpine Linux](https://www.alpinelinux.org/)
15- [PHP7-FPM](http://php-fpm.org/)
16- [Nginx](http://nginx.org/)
17
18The `stable` image relies on:
19
12- [Debian 8 Jessie](https://hub.docker.com/_/debian/) 20- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
13- [PHP5-FPM](http://php-fpm.org/) 21- [PHP5-FPM](http://php-fpm.org/)
14- [Nginx](http://nginx.org/) 22- [Nginx](http://nginx.org/)
15 23
24
16### Download from DockerHub 25### Download from DockerHub
17```bash 26```bash
18$ docker pull shaarli/shaarli 27$ docker pull shaarli/shaarli
diff --git a/doc/md/images/install-shaarli.png b/doc/md/images/install-shaarli.png
new file mode 100644
index 00000000..7ae33816
--- /dev/null
+++ b/doc/md/images/install-shaarli.png
Binary files differ
diff --git a/doc/md/images/poedit-1.jpg b/doc/md/images/poedit-1.jpg
new file mode 100644
index 00000000..673ae6d6
--- /dev/null
+++ b/doc/md/images/poedit-1.jpg
Binary files differ
diff --git a/doc/md/index.md b/doc/md/index.md
index 24ada6c7..2b7d0f00 100644
--- a/doc/md/index.md
+++ b/doc/md/index.md
@@ -22,6 +22,17 @@ It runs the latest development version of Shaarli and is updated/reset daily.
22 22
23Login: `demo`; Password: `demo` 23Login: `demo`; Password: `demo`
24 24
25Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage):
26```
27MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
28docker run -ti --rm \
29 -p 8000:80 \
30 -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
31 shaarli/shaarli
32```
33
34A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101).
35To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](Upgrade-and-migration) documentation.
25 36
26## Features 37## Features
27 38
diff --git a/docker/alpine/Dockerfile.latest b/docker/alpine/Dockerfile.latest
new file mode 100644
index 00000000..dd4a173c
--- /dev/null
+++ b/docker/alpine/Dockerfile.latest
@@ -0,0 +1,47 @@
1FROM alpine:3.6
2MAINTAINER Shaarli Community
3
4RUN apk --update --no-cache add \
5 ca-certificates \
6 curl \
7 nginx \
8 php7 \
9 php7-ctype \
10 php7-curl \
11 php7-fpm \
12 php7-gd \
13 php7-iconv \
14 php7-intl \
15 php7-json \
16 php7-mbstring \
17 php7-openssl \
18 php7-phar \
19 php7-session \
20 php7-xml \
21 php7-zlib \
22 s6
23
24COPY nginx.conf /etc/nginx/nginx.conf
25COPY php-fpm.conf /etc/php7/php-fpm.conf
26COPY services.d /etc/services.d
27
28RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
29 && rm -rf /etc/php7/php-fpm.d/www.conf \
30 && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
31 && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
32
33
34WORKDIR /var/www
35RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \
36 && mv Shaarli-latest shaarli \
37 && cd shaarli \
38 && composer --prefer-dist --no-dev install \
39 && rm -rf ~/.composer \
40 && chown -R nginx:nginx .
41
42VOLUME /var/www/shaarli/data
43
44EXPOSE 80
45
46ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
47CMD []
diff --git a/docker/alpine/Dockerfile.master b/docker/alpine/Dockerfile.master
new file mode 100644
index 00000000..58f7c6e7
--- /dev/null
+++ b/docker/alpine/Dockerfile.master
@@ -0,0 +1,47 @@
1FROM alpine:3.6
2MAINTAINER Shaarli Community
3
4RUN apk --update --no-cache add \
5 ca-certificates \
6 curl \
7 nginx \
8 php7 \
9 php7-ctype \
10 php7-curl \
11 php7-fpm \
12 php7-gd \
13 php7-iconv \
14 php7-intl \
15 php7-json \
16 php7-mbstring \
17 php7-openssl \
18 php7-phar \
19 php7-session \
20 php7-xml \
21 php7-zlib \
22 s6
23
24COPY nginx.conf /etc/nginx/nginx.conf
25COPY php-fpm.conf /etc/php7/php-fpm.conf
26COPY services.d /etc/services.d
27
28RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
29 && rm -rf /etc/php7/php-fpm.d/www.conf \
30 && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
31 && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
32
33
34WORKDIR /var/www
35RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
36 && mv Shaarli-master shaarli \
37 && cd shaarli \
38 && composer --prefer-dist --no-dev install \
39 && rm -rf ~/.composer \
40 && chown -R nginx:nginx .
41
42VOLUME /var/www/shaarli/data
43
44EXPOSE 80
45
46ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
47CMD []
diff --git a/docker/alpine/IMAGE.md b/docker/alpine/IMAGE.md
new file mode 100644
index 00000000..a8952257
--- /dev/null
+++ b/docker/alpine/IMAGE.md
@@ -0,0 +1,10 @@
1## Alpine images
2- [Alpine Linux](https://www.alpinelinux.org/)
3- [PHP-FPM](http://php-fpm.org/)
4- [Nginx](http://nginx.org/)
5
6### `shaarli/shaarli:latest`
7- [Shaarli](https://github.com/shaarli/Shaarli), `latest` branch
8
9### `shaarli/shaarli:master`
10- [Shaarli](https://github.com/shaarli/Shaarli), `master` branch
diff --git a/docker/production/stable/nginx.conf b/docker/alpine/nginx.conf
index e8754d9b..07fba33f 100644
--- a/docker/production/stable/nginx.conf
+++ b/docker/alpine/nginx.conf
@@ -1,6 +1,7 @@
1user www-data www-data; 1user nginx nginx;
2daemon off; 2daemon off;
3worker_processes 4; 3worker_processes 4;
4pid /var/run/nginx.pid;
4 5
5events { 6events {
6 worker_connections 768; 7 worker_connections 768;
@@ -59,7 +60,7 @@ http {
59 fastcgi_split_path_info ^(.+\.php)(/.+)$; 60 fastcgi_split_path_info ^(.+\.php)(/.+)$;
60 61
61 # filter and proxy PHP requests to PHP-FPM 62 # filter and proxy PHP requests to PHP-FPM
62 fastcgi_pass unix:/var/run/php5-fpm.sock; 63 fastcgi_pass unix:/var/run/php-fpm.sock;
63 fastcgi_index index.php; 64 fastcgi_index index.php;
64 include fastcgi.conf; 65 include fastcgi.conf;
65 } 66 }
diff --git a/docker/alpine/php-fpm.conf b/docker/alpine/php-fpm.conf
new file mode 100644
index 00000000..0843c164
--- /dev/null
+++ b/docker/alpine/php-fpm.conf
@@ -0,0 +1,16 @@
1[global]
2daemonize = no
3
4[www]
5user = nginx
6group = nginx
7listen.owner = nginx
8listen.group = nginx
9catch_workers_output = yes
10listen = /var/run/php-fpm.sock
11pm = dynamic
12pm.max_children = 20
13pm.start_servers = 1
14pm.min_spare_servers = 1
15pm.max_spare_servers = 3
16pm.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
index 00000000..1dadeeaf
--- /dev/null
+++ b/docker/alpine/services.d/.s6-svscan/finish
@@ -0,0 +1,2 @@
1#!/bin/sh
2/bin/true
diff --git a/docker/alpine/services.d/nginx/run b/docker/alpine/services.d/nginx/run
new file mode 100755
index 00000000..21e7b0d6
--- /dev/null
+++ b/docker/alpine/services.d/nginx/run
@@ -0,0 +1,2 @@
1#!/bin/execlineb -P
2nginx
diff --git a/docker/alpine/services.d/php-fpm/run b/docker/alpine/services.d/php-fpm/run
new file mode 100755
index 00000000..21dd0107
--- /dev/null
+++ b/docker/alpine/services.d/php-fpm/run
@@ -0,0 +1,2 @@
1#!/bin/execlineb -P
2php-fpm7 -F
diff --git a/docker/production/stable/Dockerfile b/docker/debian/Dockerfile.stable
index fc9588b0..fc9588b0 100644
--- a/docker/production/stable/Dockerfile
+++ b/docker/debian/Dockerfile.stable
diff --git a/docker/production/stable/IMAGE.md b/docker/debian/IMAGE.md
index d85b1d7a..d85b1d7a 100644
--- a/docker/production/stable/IMAGE.md
+++ b/docker/debian/IMAGE.md
diff --git a/docker/production/nginx.conf b/docker/debian/nginx.conf
index e8754d9b..e8754d9b 100644
--- a/docker/production/nginx.conf
+++ b/docker/debian/nginx.conf
diff --git a/docker/production/stable/supervised.conf b/docker/debian/supervised.conf
index 5acd9795..5acd9795 100644
--- a/docker/production/stable/supervised.conf
+++ b/docker/debian/supervised.conf
diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile
deleted file mode 100644
index d0509115..00000000
--- a/docker/production/Dockerfile
+++ /dev/null
@@ -1,37 +0,0 @@
1FROM debian:jessie
2MAINTAINER Shaarli Community
3
4ENV TERM dumb
5RUN apt-get update \
6 && apt-get install --no-install-recommends -y \
7 ca-certificates \
8 curl \
9 nginx-light \
10 php5-curl \
11 php5-fpm \
12 php5-gd \
13 php5-intl \
14 supervisor \
15 && apt-get clean
16
17RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini
18RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini
19COPY nginx.conf /etc/nginx/nginx.conf
20COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
21
22ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
23RUN chmod 755 /usr/local/bin/composer
24
25WORKDIR /var/www
26RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
27 && mv Shaarli-master shaarli \
28 && cd shaarli \
29 && composer --prefer-dist --no-dev install
30RUN rm -rf html \
31 && chown -R www-data:www-data .
32
33VOLUME /var/www/shaarli/data
34
35EXPOSE 80
36
37CMD ["/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
index 6f827b35..00000000
--- a/docker/production/IMAGE.md
+++ /dev/null
@@ -1,5 +0,0 @@
1## shaarli:latest
2- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
3- [PHP5-FPM](http://php-fpm.org/)
4- [Nginx](http://nginx.org/)
5- [Shaarli](https://github.com/shaarli/Shaarli)
diff --git a/docker/production/supervised.conf b/docker/production/supervised.conf
deleted file mode 100644
index 5acd9795..00000000
--- a/docker/production/supervised.conf
+++ /dev/null
@@ -1,13 +0,0 @@
1[program:php5-fpm]
2command=/usr/sbin/php5-fpm -F
3priority=5
4autostart=true
5autorestart=true
6
7[program:nginx]
8command=/usr/sbin/nginx
9priority=10
10autostart=true
11autorestart=true
12stdout_events_enabled=true
13stderr_events_enabled=true
diff --git a/docker/test/alpine36/Dockerfile b/docker/test/alpine36/Dockerfile
new file mode 100644
index 00000000..fa84f6e2
--- /dev/null
+++ b/docker/test/alpine36/Dockerfile
@@ -0,0 +1,34 @@
1FROM alpine:3.6
2MAINTAINER Shaarli Community
3
4RUN apk --update --no-cache add \
5 ca-certificates \
6 curl \
7 make \
8 php7 \
9 php7-ctype \
10 php7-curl \
11 php7-dom \
12 php7-gd \
13 php7-iconv \
14 php7-intl \
15 php7-json \
16 php7-mbstring \
17 php7-openssl \
18 php7-phar \
19 php7-session \
20 php7-simplexml \
21 php7-tokenizer \
22 php7-xdebug \
23 php7-xml \
24 php7-zlib \
25 rsync
26
27RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
28
29RUN mkdir /shaarli
30WORKDIR /shaarli
31VOLUME /shaarli
32
33ENTRYPOINT ["make"]
34CMD []
diff --git a/docker/test/debian8/Dockerfile b/docker/test/debian8/Dockerfile
new file mode 100644
index 00000000..eaa34e9b
--- /dev/null
+++ b/docker/test/debian8/Dockerfile
@@ -0,0 +1,35 @@
1FROM debian:jessie
2MAINTAINER Shaarli Community
3
4ENV TERM dumb
5ENV DEBIAN_FRONTEND noninteractive
6ENV LANG en_US.UTF-8
7ENV LANGUAGE en_US:en
8
9RUN apt-get update \
10 && apt-get install --no-install-recommends -y \
11 ca-certificates \
12 curl \
13 locales \
14 make \
15 php5 \
16 php5-curl \
17 php5-gd \
18 php5-intl \
19 php5-xdebug \
20 rsync \
21 && apt-get clean
22
23RUN locale-gen en_US.UTF-8 \
24 && locale-gen de_DE.UTF-8 \
25 && locale-gen fr_FR.UTF-8
26
27ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
28RUN chmod 755 /usr/local/bin/composer
29
30RUN mkdir /shaarli
31WORKDIR /shaarli
32VOLUME /shaarli
33
34ENTRYPOINT ["make"]
35CMD []
diff --git a/docker/test/debian9/Dockerfile b/docker/test/debian9/Dockerfile
new file mode 100644
index 00000000..3ab4b93d
--- /dev/null
+++ b/docker/test/debian9/Dockerfile
@@ -0,0 +1,36 @@
1FROM debian:stretch
2MAINTAINER Shaarli Community
3
4ENV TERM dumb
5ENV DEBIAN_FRONTEND noninteractive
6ENV LANG en_US.UTF-8
7ENV LANGUAGE en_US:en
8
9RUN apt-get update \
10 && apt-get install --no-install-recommends -y \
11 ca-certificates \
12 curl \
13 locales \
14 make \
15 php7.0 \
16 php7.0-curl \
17 php7.0-gd \
18 php7.0-intl \
19 php7.0-xml \
20 php-xdebug \
21 rsync \
22 && apt-get clean
23
24RUN locale-gen en_US.UTF-8 \
25 && locale-gen de_DE.UTF-8 \
26 && locale-gen fr_FR.UTF-8
27
28ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
29RUN chmod 755 /usr/local/bin/composer
30
31RUN mkdir /shaarli
32WORKDIR /shaarli
33VOLUME /shaarli
34
35ENTRYPOINT ["make"]
36CMD []
diff --git a/docker/test/ubuntu16/Dockerfile b/docker/test/ubuntu16/Dockerfile
new file mode 100644
index 00000000..e53ed9e3
--- /dev/null
+++ b/docker/test/ubuntu16/Dockerfile
@@ -0,0 +1,36 @@
1FROM ubuntu:16.04
2MAINTAINER Shaarli Community
3
4ENV TERM dumb
5ENV DEBIAN_FRONTEND noninteractive
6ENV LANG en_US.UTF-8
7ENV LANGUAGE en_US:en
8
9RUN apt-get update \
10 && apt-get install --no-install-recommends -y \
11 ca-certificates \
12 curl \
13 language-pack-de \
14 language-pack-en \
15 language-pack-fr \
16 locales \
17 make \
18 php7.0 \
19 php7.0-curl \
20 php7.0-gd \
21 php7.0-intl \
22 php7.0-xml \
23 php-xdebug \
24 rsync \
25 && apt-get clean
26
27ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
28RUN chmod 755 /usr/local/bin/composer
29
30RUN useradd -m dev \
31 && mkdir /shaarli
32USER dev
33WORKDIR /shaarli
34
35ENTRYPOINT ["make"]
36CMD []
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
new file mode 100644
index 00000000..6b2de950
--- /dev/null
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -0,0 +1,1366 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2017-10-22 13:13+0200\n"
5"PO-Revision-Date: 2017-10-22 13:14+0200\n"
6"Last-Translator: \n"
7"Language-Team: Shaarli\n"
8"Language: fr_FR\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.0.4\n"
13"X-Poedit-Basepath: ../../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-SourceCharset: UTF-8\n"
16"X-Poedit-KeywordsList: t:1,2;t\n"
17"X-Poedit-SearchPath-0: .\n"
18
19#: application/ApplicationUtils.php:153
20#, php-format
21msgid ""
22"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
23"cannot run. Your PHP version has known security vulnerabilities and should "
24"be updated as soon as possible."
25msgstr ""
26"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
27"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
28"connues et devrait être mise à jour au plus tôt."
29
30#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195
31msgid "directory is not readable"
32msgstr "le répertoire n'est pas accessible en lecture"
33
34#: application/ApplicationUtils.php:198
35msgid "directory is not writable"
36msgstr "le répertoire n'est pas accessible en écriture"
37
38#: application/ApplicationUtils.php:216
39msgid "file is not readable"
40msgstr "le fichier n'est pas accessible en lecture"
41
42#: application/ApplicationUtils.php:219
43msgid "file is not writable"
44msgstr "le fichier n'est pas accessible en écriture"
45
46#: application/Cache.php:16
47#, php-format
48msgid "Cannot purge %s: no directory"
49msgstr "Impossible de purger %s: le répertoire n'existe pas"
50
51#: application/FeedBuilder.php:151
52msgid "Direct link"
53msgstr "Liens directs"
54
55#: application/FeedBuilder.php:153
56#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
57#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178
58msgid "Permalink"
59msgstr "Permalien"
60
61#: application/History.php:174
62msgid "History file isn't readable or writable"
63msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
64
65#: application/History.php:185
66msgid "Could not parse history file"
67msgstr "Format incorrect pour le fichier d'historique"
68
69#: application/Languages.php:159
70msgid "Automatic"
71msgstr "Automatique"
72
73#: application/Languages.php:160
74msgid "English"
75msgstr "Anglais"
76
77#: application/Languages.php:161
78msgid "French"
79msgstr "Français"
80
81#: application/LinkDB.php:136
82msgid "You are not authorized to add a link."
83msgstr "Vous n'êtes pas autorisé à ajouter un lien."
84
85#: application/LinkDB.php:139
86msgid "Internal Error: A link should always have an id and URL."
87msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
88
89#: application/LinkDB.php:142
90msgid "You must specify an integer as a key."
91msgstr "Vous devez utiliser un entier comme clé."
92
93#: application/LinkDB.php:145
94msgid "Array offset and link ID must be equal."
95msgstr "La clé du tableau et l'ID du lien doivent être égaux."
96
97#: application/LinkDB.php:251
98#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
99#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
100#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
101#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
102msgid ""
103"The personal, minimalist, super-fast, database free, bookmarking service"
104msgstr ""
105"Le gestionnaire de marque-page personnel, minimaliste, et sans base de "
106"données"
107
108#: application/LinkDB.php:253
109msgid ""
110"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
111"me, you must first login.\n"
112"\n"
113"To learn how to use Shaarli, consult the link \"Documentation\" at the "
114"bottom of this page.\n"
115"\n"
116"You use the community supported version of the original Shaarli project, by "
117"Sebastien Sauvage."
118msgstr ""
119"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me "
120"modifier ou me supprimer, vous devez d'abord vous connecter.\n"
121"\n"
122"Pour apprendre comment utiliser Shaarli, consultez le lien « Documentation » "
123"en bas de page.\n"
124"\n"
125"Vous utilisez la version supportée par la communauté du projet original "
126"Shaarli, de Sébastien Sauvage."
127
128#: application/LinkDB.php:267
129msgid "My secret stuff... - Pastebin.com"
130msgstr "Mes trucs secrets... - Pastebin.com"
131
132#: application/LinkDB.php:269
133msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
134msgstr ""
135"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
136"supprimer aussi."
137
138#: application/LinkFilter.php:452
139msgid "The link you are trying to reach does not exist or has been deleted."
140msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
141
142#: application/NetscapeBookmarkUtils.php:35
143msgid "Invalid export selection:"
144msgstr "Sélection d'export invalide :"
145
146#: application/NetscapeBookmarkUtils.php:81
147#, php-format
148msgid "File %s (%d bytes) "
149msgstr "Le fichier %s (%d octets) "
150
151#: application/NetscapeBookmarkUtils.php:83
152msgid "has an unknown file format. Nothing was imported."
153msgstr "a un format inconnu. Rien n'a été importé."
154
155#: application/NetscapeBookmarkUtils.php:86
156#, php-format
157msgid ""
158"was successfully processed in %d seconds: %d links imported, %d links "
159"overwritten, %d links skipped."
160msgstr ""
161"a été importé avec succès en %d secondes : %d liens importés, %d liens "
162"écrasés, %d liens ignorés."
163
164#: application/PageBuilder.php:165
165msgid "The page you are trying to reach does not exist or has been deleted."
166msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée."
167
168#: application/PageBuilder.php:167
169msgid "404 Not Found"
170msgstr "404 Introuvable"
171
172#: application/PluginManager.php:243
173#, php-format
174msgid "Plugin \"%s\" files not found."
175msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
176
177#: application/Updater.php:76
178msgid "Couldn't retrieve Updater class methods."
179msgstr "Impossible de récupérer les méthodes de la classe Updater."
180
181#: application/Updater.php:485
182msgid "An error occurred while running the update "
183msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
184
185#: application/Updater.php:525
186msgid "Updates file path is not set, can't write updates."
187msgstr ""
188"Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
189"d'écrire les mises à jour."
190
191#: application/Updater.php:530
192msgid "Unable to write updates in "
193msgstr "Impossible d'écrire les mises à jour dans "
194
195#: application/Utils.php:406 tests/UtilsTest.php:398
196msgid "Setting not set"
197msgstr "Paramètre non défini"
198
199#: application/Utils.php:413 tests/UtilsTest.php:396 tests/UtilsTest.php:397
200msgid "Unlimited"
201msgstr "Illimité"
202
203#: application/Utils.php:416 tests/UtilsTest.php:393 tests/UtilsTest.php:394
204#: tests/UtilsTest.php:408
205msgid "B"
206msgstr "o"
207
208#: application/Utils.php:416 tests/UtilsTest.php:387 tests/UtilsTest.php:388
209#: tests/UtilsTest.php:395
210msgid "kiB"
211msgstr "ko"
212
213#: application/Utils.php:416 tests/UtilsTest.php:389 tests/UtilsTest.php:390
214#: tests/UtilsTest.php:406 tests/UtilsTest.php:407
215msgid "MiB"
216msgstr "Mo"
217
218#: application/Utils.php:416 tests/UtilsTest.php:391 tests/UtilsTest.php:392
219msgid "GiB"
220msgstr "Go"
221
222#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121
223msgid ""
224"Shaarli could not create the config file. Please make sure Shaarli has the "
225"right to write in the folder is it installed in."
226msgstr ""
227"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
228"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
229
230#: application/config/ConfigManager.php:135
231msgid "Invalid setting key parameter. String expected, got: "
232msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
233
234#: application/config/exception/MissingFieldConfigException.php:21
235#, php-format
236msgid "Configuration value is required for %s"
237msgstr "Le paramètre %s est obligatoire"
238
239#: application/config/exception/PluginConfigOrderException.php:15
240msgid "An error occurred while trying to save plugins loading order."
241msgstr ""
242"Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions."
243
244#: application/config/exception/UnauthorizedConfigException.php:16
245msgid "You are not authorized to alter config."
246msgstr "Vous n'êtes pas autorisé à modifier la configuration."
247
248#: application/exceptions/IOException.php:19
249msgid "Error accessing"
250msgstr "Une erreur s'est produite en accédant à"
251
252#: index.php:133
253msgid "Shared links on "
254msgstr "Liens partagés sur "
255
256#: index.php:155
257msgid "Insufficient permissions:"
258msgstr "Permissions insuffisantes :"
259
260#: index.php:382
261msgid "I said: NO. You are banned for the moment. Go away."
262msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
263
264#: index.php:447
265msgid "Wrong login/password."
266msgstr "Nom d'utilisateur ou mot de passe incorrects."
267
268#: index.php:1107
269msgid "You are not supposed to change a password on an Open Shaarli."
270msgstr ""
271"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
272
273#: index.php:1112 index.php:1153 index.php:1229 index.php:1259 index.php:1359
274msgid "Wrong token."
275msgstr "Jeton invalide."
276
277#: index.php:1117
278msgid "The old password is not correct."
279msgstr "L'ancien mot de passe est incorrect."
280
281#: index.php:1137
282msgid "Your password has been changed"
283msgstr "Votre mot de passe a été modifié"
284
285#: index.php:1190
286msgid "Configuration was saved."
287msgstr "La configuration a été sauvegardé."
288
289#: index.php:1241
290#, php-format
291msgid "The tag was removed from %d link."
292msgid_plural "The tag was removed from %d links."
293msgstr[0] "Le tag a été supprimé de %d lien."
294msgstr[1] "Le tag a été supprimé de %d liens."
295
296#: index.php:1242
297#, php-format
298msgid "The tag was renamed in %d link."
299msgid_plural "The tag was renamed in %d links."
300msgstr[0] "Le tag a été renommé dans %d lien."
301msgstr[1] "Le tag a été renommé dans %d liens."
302
303#: index.php:1458
304msgid "Note: "
305msgstr "Note : "
306
307#: index.php:1567
308#, php-format
309msgid ""
310"The file you are trying to upload is probably bigger than what this "
311"webserver can accept (%s). Please upload in smaller chunks."
312msgstr ""
313"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
314"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
315"légères."
316
317#: index.php:1983
318#, php-format
319msgid ""
320"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
321"variable \"session.save_path\" is set correctly in your PHP config, and that "
322"you have write access to it.<br>It currently points to %s.<br>On some "
323"browsers, accessing your server via a hostname like 'localhost' or any "
324"custom hostname without a dot causes cookie storage to fail. We recommend "
325"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
326msgstr ""
327"<pre>Les sesssions ne semble pas fonctionner sur ce serveur.<br>Assurez vous "
328"que la variable « session.save_path » est correctement définie dans votre "
329"fichier de configuration PHP, et que vous y avez les droits d'écriture."
330"<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains navigateurs, "
331"accéder à votre serveur depuis un nom d'hôte comme « localhost » ou autre "
332"nom personnalisé sans point '.' entraine l'échec de la sauvegarde des "
333"cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse "
334"IP ou un <em>Fully Qualified Domain Name</em>.<br>"
335
336#: index.php:1993
337msgid "Click to try again."
338msgstr "Cliquer ici pour réessayer."
339
340#: plugins/addlink_toolbar/addlink_toolbar.php:29
341msgid "URI"
342msgstr "URI"
343
344#: plugins/addlink_toolbar/addlink_toolbar.php:33
345#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
346msgid "Add link"
347msgstr "Shaare"
348
349#: plugins/addlink_toolbar/addlink_toolbar.php:50
350msgid "Adds the addlink input on the linklist page."
351msgstr "Ajout le formulaire d'ajout de liens sur la page principale."
352
353#: plugins/archiveorg/archiveorg.php:23
354msgid "View on archive.org"
355msgstr "Voir sur archive.org"
356
357#: plugins/archiveorg/archiveorg.php:36
358msgid "For each link, add an Archive.org icon."
359msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
360
361#: plugins/demo_plugin/demo_plugin.php:469
362msgid ""
363"A demo plugin covering all use cases for template designers and plugin "
364"developers."
365msgstr ""
366"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
367"designers et les développeurs."
368
369#: plugins/isso/isso.php:20
370msgid ""
371"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
372"administration page."
373msgstr ""
374"Erreur de l'extension Isso : Merci de définir le paramètre « ISSO_SERVER » "
375"dans la page d'administration des extensions."
376
377#: plugins/isso/isso.php:63
378msgid "Let visitor comment your shaares on permalinks with Isso."
379msgstr ""
380"Permet aux visiteurs de commenter vos shaares sur les permaliens avec Isso."
381
382#: plugins/isso/isso.php:64
383msgid "Isso server URL (without 'http://')"
384msgstr "URL du serveur Isso (sans 'http://')"
385
386#: plugins/markdown/markdown.php:159
387msgid "Description will be rendered with"
388msgstr "La description sera générée avec"
389
390#: plugins/markdown/markdown.php:160
391msgid "Markdown syntax documentation"
392msgstr "Documentation sur la syntaxe Markdown"
393
394#: plugins/markdown/markdown.php:161
395msgid "Markdown syntax"
396msgstr "la syntaxe Markdown"
397
398#: plugins/markdown/markdown.php:340
399msgid ""
400"Render shaare description with Markdown syntax.<br><strong>Warning</"
401"strong>:\n"
402"If your shaared descriptions contained HTML tags before enabling the "
403"markdown plugin,\n"
404"enabling it might break your page.\n"
405"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
406"markdown#html-rendering\">README</a>."
407msgstr ""
408"Utilise la syntaxe Markdown pour la description des liens."
409"<br><strong>Attention</strong> :\n"
410"Si vous aviez des descriptions contenant du HTML avant d'activer cette "
411"extension,\n"
412"l'activer pourrait déformer vos pages.\n"
413"Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
414"markdown#html-rendering\">README</a>."
415
416#: plugins/piwik/piwik.php:21
417msgid ""
418"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
419"administration page."
420msgstr ""
421"Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et "
422"PIWIK_SITEID dans la page d'administration des extensions."
423
424#: plugins/piwik/piwik.php:70
425msgid "A plugin that adds Piwik tracking code to Shaarli pages."
426msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli."
427
428#: plugins/piwik/piwik.php:71
429msgid "Piwik URL"
430msgstr "URL de Piwik"
431
432#: plugins/piwik/piwik.php:72
433msgid "Piwik site ID"
434msgstr "Site ID de Piwik"
435
436#: plugins/playvideos/playvideos.php:22
437msgid "Video player"
438msgstr "Lecteur vidéo"
439
440#: plugins/playvideos/playvideos.php:25
441msgid "Play Videos"
442msgstr "Jouer les vidéos"
443
444#: plugins/playvideos/playvideos.php:56
445msgid "Add a button in the toolbar allowing to watch all videos."
446msgstr ""
447"Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos."
448
449#: plugins/playvideos/youtube_playlist.js:214
450msgid "plugins/playvideos/jquery-1.11.2.min.js"
451msgstr ""
452
453#: plugins/pubsubhubbub/pubsubhubbub.php:69
454#, php-format
455msgid "Could not publish to PubSubHubbub: %s"
456msgstr "Impossible de publier vers PubSubHubbub : %s"
457
458#: plugins/pubsubhubbub/pubsubhubbub.php:95
459#, php-format
460msgid "Could not post to %s"
461msgstr "Impossible de publier vers %s"
462
463#: plugins/pubsubhubbub/pubsubhubbub.php:99
464#, php-format
465msgid "Bad response from the hub %s"
466msgstr "Mauvaise réponse du hub %s"
467
468#: plugins/pubsubhubbub/pubsubhubbub.php:110
469msgid "Enable PubSubHubbub feed publishing."
470msgstr "Active la publication de flux vers PubSubHubbub."
471
472#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68
473msgid "For each link, add a QRCode icon."
474msgstr "Pour chaque liens, ajouter une icône de QRCode."
475
476#: plugins/wallabag/wallabag.php:21
477msgid ""
478"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
479"plugin administration page."
480msgstr ""
481"Erreur de l'extension Wallabag : Merci de définir le paramètre « "
482"WALLABAG_URL » dans la page d'administration des extensions."
483
484#: plugins/wallabag/wallabag.php:47
485msgid "Save to wallabag"
486msgstr "Sauvegarder dans Wallabag"
487
488#: plugins/wallabag/wallabag.php:69
489msgid "Wallabag API URL"
490msgstr "URL de l'API Wallabag"
491
492#: plugins/wallabag/wallabag.php:70
493msgid "Wallabag API version (1 or 2)"
494msgstr "Version de l'API Wallabag (1 ou 2)"
495
496#: tests/LanguagesTest.php:188 tests/LanguagesTest.php:201
497#: tests/languages/fr/LanguagesFrTest.php:160
498#: tests/languages/fr/LanguagesFrTest.php:173
499#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
500#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81
501msgid "Search"
502msgid_plural "Search"
503msgstr[0] "Rechercher"
504msgstr[1] "Rechercher"
505
506#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
507msgid "Sorry, nothing to see here."
508msgstr "Désolé, il y a rien à voir ici."
509
510#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
511msgid "Shaare a new link"
512msgstr "Partager un nouveau lien"
513
514#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
515msgid "URL or leave empty to post a note"
516msgstr "URL ou laisser vide pour créer une note"
517
518#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
519#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
520msgid "Change password"
521msgstr "Modification du mot de passe"
522
523#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
524msgid "Current password"
525msgstr "Mot de passe actuel"
526
527#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
528msgid "New password"
529msgstr "Nouveau mot de passe"
530
531#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
532msgid "Change"
533msgstr "Changer"
534
535#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
536#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
537msgid "Manage tags"
538msgstr "Gérer les tags"
539
540#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
541#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
542msgid "Tag"
543msgstr "Tag"
544
545#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
546msgid "New name"
547msgstr "Nouveau nom"
548
549#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
550msgid "Case sensitive"
551msgstr "Sensible à la casse"
552
553#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
554msgid "Rename"
555msgstr "Renommer"
556
557#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
558#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
559#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172
560msgid "Delete"
561msgstr "Supprimer"
562
563#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
564msgid "You can also edit tags in the"
565msgstr "Vous pouvez aussi modifier les tags dans la"
566
567#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
568msgid "tag list"
569msgstr "liste des tags"
570
571#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
572msgid "Configure"
573msgstr "Configurer"
574
575#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
576msgid "title"
577msgstr "titre"
578
579#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
580msgid "Home link"
581msgstr "Lien vers l'accueil"
582
583#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
584msgid "Default value"
585msgstr "Valeur par défaut"
586
587#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
588msgid "Theme"
589msgstr "Thème"
590
591#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
592#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
593msgid "Language"
594msgstr "Langue"
595
596#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116
597#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
598msgid "Timezone"
599msgstr "Fuseau horaire"
600
601#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
602#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
603msgid "Continent"
604msgstr "Continent"
605
606#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
607#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
608msgid "City"
609msgstr "Ville"
610
611#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:163
612msgid "Redirector"
613msgstr "Redirecteur"
614
615#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
616msgid "e. g."
617msgstr "ex :"
618
619#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
620msgid "will mask the HTTP_REFERER"
621msgstr "masque le HTTP_REFERER"
622
623#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
624msgid "Disable session cookie hijacking protection"
625msgstr "Désactiver la protection contre le détournement de cookies"
626
627#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
628msgid "Check this if you get disconnected or if your IP address changes often"
629msgstr ""
630"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP "
631"change souvent"
632
633#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:198
634msgid "Private links by default"
635msgstr "Liens privés par défaut"
636
637#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
638msgid "All new links are private by default"
639msgstr "Tous les nouveaux liens sont privés par défaut"
640
641#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:214
642msgid "RSS direct links"
643msgstr "Liens directs dans le flux RSS"
644
645#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
646msgid "Check this to use direct URL instead of permalink in feeds"
647msgstr ""
648"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
649"dans le flux RSS"
650
651#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:230
652msgid "Hide public links"
653msgstr "Cacher les liens publics"
654
655#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231
656msgid "Do not show any links if the user is not logged in"
657msgstr "N'afficher aucun lien sans être connecté"
658
659#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:246
660#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
661msgid "Check updates"
662msgstr "Vérifier les mises à jour"
663
664#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
665#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
666msgid "Notify me when a new release is ready"
667msgstr "Me notifier lorsqu'une nouvelle version est disponible"
668
669#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:262
670#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
671msgid "Enable REST API"
672msgstr "Activer l'API REST"
673
674#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
675#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
676msgid "Allow third party software to use Shaarli such as mobile application"
677msgstr ""
678"Permets aux applications tierces d'utiliser Shaarli, par exemple les "
679"applications mobiles"
680
681#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:278
682msgid "API secret"
683msgstr "Clé d'API secrète"
684
685#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:289
686#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
687#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
688#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:192
689msgid "Save"
690msgstr "Enregistrer"
691
692#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
693msgid "The Daily Shaarli"
694msgstr "Le Quotidien Shaarli"
695
696#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
697msgid "1 RSS entry per day"
698msgstr "1 entrée RSS par jour"
699
700#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
701msgid "Previous day"
702msgstr "Jour précédent"
703
704#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
705msgid "All links of one day in a single page."
706msgstr "Tous les liens d'un jour sur une page."
707
708#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
709msgid "Next day"
710msgstr "Jour suivant"
711
712#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
713#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
714msgid "Edit"
715msgstr "Modifier"
716
717#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
718#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
719#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
720msgid "Shaare"
721msgstr "Shaare"
722
723#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
724msgid "Created:"
725msgstr "Création :"
726
727#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
728msgid "URL"
729msgstr "URL"
730
731#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
732msgid "Title"
733msgstr "Titre"
734
735#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
736#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
737#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
738#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
739#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
740msgid "Description"
741msgstr "Description"
742
743#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
744msgid "Tags"
745msgstr "Tags"
746
747#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
748#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
749#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
750msgid "Private"
751msgstr "Privé"
752
753#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
754msgid "Apply Changes"
755msgstr "Appliquer les changements"
756
757#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
758msgid "Export Database"
759msgstr "Exporter les données"
760
761#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
762msgid "Selection"
763msgstr "Choisir"
764
765#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
766msgid "All"
767msgstr "Tous"
768
769#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
770msgid "Public"
771msgstr "Publics"
772
773#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
774msgid "Prepend note permalinks with this Shaarli instance's URL"
775msgstr "Préfixer les liens de notes avec l'URL de l'instance de Shaarli"
776
777#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
778msgid "Useful to import bookmarks in a web browser"
779msgstr "Utile pour importer les marques-pages dans un navigateur"
780
781#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
782msgid "Export"
783msgstr "Exporter"
784
785#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
786msgid "Import Database"
787msgstr "Importer des données"
788
789#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
790msgid "Maximum size allowed:"
791msgstr "Taille maximum autorisée :"
792
793#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
794msgid "Visibility"
795msgstr "Visibilité"
796
797#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
798msgid "Use values from the imported file, default to public"
799msgstr ""
800"Utiliser les valeurs présentes dans le fichier d'import, public par défaut"
801
802#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
803msgid "Import all bookmarks as private"
804msgstr "Importer tous les liens comme privés"
805
806#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
807msgid "Import all bookmarks as public"
808msgstr "Importer tous les liens comme publics"
809
810#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
811msgid "Overwrite existing bookmarks"
812msgstr "Remplacer les liens existants"
813
814#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
815msgid "Duplicates based on URL"
816msgstr "Les doublons s'appuient sur les URL"
817
818#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
819msgid "Add default tags"
820msgstr "Ajouter des tags par défaut"
821
822#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
823msgid "Import"
824msgstr "Importer"
825
826#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
827msgid "Install Shaarli"
828msgstr "Installation de Shaarli"
829
830#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
831msgid "It looks like it's the first time you run Shaarli. Please configure it."
832msgstr ""
833"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de "
834"le configurer."
835
836#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
837#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
838#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
839#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
840msgid "Username"
841msgstr "Nom d'utilisateur"
842
843#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
844#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
845#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
846#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148
847msgid "Password"
848msgstr "Mot de passe"
849
850#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
851msgid "Shaarli title"
852msgstr "Titre du Shaarli"
853
854#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
855msgid "My links"
856msgstr "Mes liens"
857
858#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
859msgid "Install"
860msgstr "Installer"
861
862#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
863#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
864msgid "shaare"
865msgid_plural "shaares"
866msgstr[0] "shaare"
867msgstr[1] "shaares"
868
869#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
870#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
871msgid "private link"
872msgid_plural "private links"
873msgstr[0] "lien privé"
874msgstr[1] "liens privés"
875
876#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
877#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
878#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117
879msgid "Search text"
880msgstr "Recherche texte"
881
882#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
883#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
884#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124
885#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
886#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
887#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
888#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
889msgid "Filter by tag"
890msgstr "Filtrer par tag"
891
892#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
893msgid "Nothing found."
894msgstr "Aucun résultat."
895
896#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119
897#, php-format
898msgid "%s result"
899msgid_plural "%s results"
900msgstr[0] "%s résultat"
901msgstr[1] "%s résultats"
902
903#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
904msgid "for"
905msgstr "pour"
906
907#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
908msgid "tagged"
909msgstr "taggé"
910
911#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
912msgid "Remove tag"
913msgstr "Retirer le tag"
914
915#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
916msgid "with status"
917msgstr "avec le statut"
918
919#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
920msgid "without any tag"
921msgstr "sans tag"
922
923#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174
924#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
925#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
926msgid "Fold"
927msgstr "Replier"
928
929#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
930msgid "Edited: "
931msgstr "Modifié : "
932
933#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
934msgid "permalink"
935msgstr "permalien"
936
937#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
938msgid "Add tag"
939msgstr "Ajouter un tag"
940
941#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
942#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7
943msgid "Filters"
944msgstr "Filtres"
945
946#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
947#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12
948msgid "Filter private links"
949msgstr "Filtrer par liens privés"
950
951#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
952#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
953msgid "Filter untagged links"
954msgstr "Filtrer par liens privés"
955
956#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
957#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
958#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:22
959#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:74
960#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
961#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
962msgid "Fold all"
963msgstr "Replier tout"
964
965#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
966#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:67
967msgid "Links per page"
968msgstr "Liens par page"
969
970#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
971msgid ""
972"You have been banned after too many failed login attempts. Try again later."
973msgstr ""
974"Vous avez été banni après trop d'échec d'authentification. Merci de "
975"réessayer plus tard."
976
977#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
978#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
979#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
980#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95
981#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71
982#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95
983msgid "Login"
984msgstr "Connexion"
985
986#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
987#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
988#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
989msgid "Remember me"
990msgstr "Rester connecté"
991
992#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
993#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
994#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
995#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
996msgid "by the Shaarli community"
997msgstr "par la communauté Shaarli"
998
999#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1000#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
1001msgid "Documentation"
1002msgstr "Documentation"
1003
1004#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1005#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
1006msgid "Expand"
1007msgstr "Déplier"
1008
1009#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
1010#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
1011msgid "Expand all"
1012msgstr "Déplier tout"
1013
1014#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1015#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
1016msgid "Are you sure you want to delete this link?"
1017msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
1018
1019#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
1020#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
1021msgid "Tools"
1022msgstr "Outils"
1023
1024#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1025#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36
1026#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1027msgid "Tag cloud"
1028msgstr "Nuage de tags"
1029
1030#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
1031#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39
1032msgid "Picture wall"
1033msgstr "Mur d'images"
1034
1035#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1036#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42
1037msgid "Daily"
1038msgstr "Quotidien"
1039
1040#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
1041#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1042#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61
1043#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86
1044msgid "RSS Feed"
1045msgstr "Flux RSS"
1046
1047#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1048#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1049#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66
1050#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102
1051msgid "Logout"
1052msgstr "Déconnexion"
1053
1054#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1055#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
1056msgid "is available"
1057msgstr "est disponible"
1058
1059#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
1060#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176
1061msgid "Error"
1062msgstr "Erreur"
1063
1064#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1065msgid "Picture Wall"
1066msgstr "Mur d'images"
1067
1068#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1069msgid "pics"
1070msgstr "images"
1071
1072#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1073msgid "You need to enable Javascript to change plugin loading order."
1074msgstr ""
1075"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
1076
1077#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
1078#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
1079msgid "Plugin administration"
1080msgstr "Administration des extensions"
1081
1082#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1083msgid "Enabled Plugins"
1084msgstr "Extensions activées"
1085
1086#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
1087#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1088msgid "No plugin enabled."
1089msgstr "Aucune extension activée."
1090
1091#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
1092#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
1093msgid "Disable"
1094msgstr "Désactiver"
1095
1096#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1097#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1098#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98
1099#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1100msgid "Name"
1101msgstr "Nom"
1102
1103#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1104#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1105msgid "Order"
1106msgstr "Ordre"
1107
1108#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1109msgid "Disabled Plugins"
1110msgstr "Extensions désactivées"
1111
1112#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
1113msgid "No plugin disabled."
1114msgstr "Aucune extension désactivée."
1115
1116#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
1117#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
1118msgid "Enable"
1119msgstr "Activer"
1120
1121#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1122msgid "More plugins available"
1123msgstr "Plus d'extensions disponibles"
1124
1125#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
1126msgid "in the documentation"
1127msgstr "dans la documentation"
1128
1129#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
1130msgid "Plugin configuration"
1131msgstr "Configuration des extensions"
1132
1133#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1134#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1135msgid "tags"
1136msgstr "tags"
1137
1138#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1139#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1140msgid "List all links with those tags"
1141msgstr "Lister tous les liens avec ces tags"
1142
1143#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1144msgid "Tag list"
1145msgstr "List des tags"
1146
1147#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1148#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1149msgid "Sort by:"
1150msgstr "Trier par :"
1151
1152#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1153#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
1154msgid "Cloud"
1155msgstr "Nuage"
1156
1157#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6
1158#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
1159msgid "Most used"
1160msgstr "Plus utilisés"
1161
1162#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
1163#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
1164msgid "Alphabetical"
1165msgstr "Alphabétique"
1166
1167#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1168msgid "Settings"
1169msgstr "Paramètres"
1170
1171#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1172msgid "Change Shaarli settings: title, timezone, etc."
1173msgstr "Changer les paramètres de Shaarli : titre, fuseau horaire, etc."
1174
1175#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
1176msgid "Configure your Shaarli"
1177msgstr "Conguration de Shaarli"
1178
1179#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
1180msgid "Enable, disable and configure plugins"
1181msgstr "Activer, désactiver et configurer les extensions"
1182
1183#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1184msgid "Change your password"
1185msgstr "Modification du mot de passe"
1186
1187#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
1188msgid "Rename or delete a tag in all links"
1189msgstr "Rename or delete a tag in all links"
1190
1191#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1192msgid ""
1193"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1194"delicious...)"
1195msgstr ""
1196"Importer des marques pages au format Netscape HTML (comme exportés depuis "
1197"Firefox, Chrome, Opera, delicious...)"
1198
1199#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1200msgid "Import links"
1201msgstr "Importer des liens"
1202
1203#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1204msgid ""
1205"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1206"Opera, delicious...)"
1207msgstr ""
1208"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
1209"Firefox, Chrome, Opera, delicious...)"
1210
1211#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1212msgid "Export database"
1213msgstr "Exporter les données"
1214
1215#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
1216msgid ""
1217"Drag one of these button to your bookmarks toolbar or right-click it and "
1218"\"Bookmark This Link\""
1219msgstr ""
1220"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit "
1221"dessus et « Ajouter aux favoris »"
1222
1223#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
1224msgid "then click on the bookmarklet in any page you want to share."
1225msgstr ""
1226"puis cliquer sur le marque page depuis un site que vous souhaitez partager."
1227
1228#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1229#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100
1230msgid ""
1231"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1232"Link"
1233msgstr ""
1234"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1235"Ajouter aux favoris »"
1236
1237#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
1238msgid "then click ✚Shaare link button in any page you want to share"
1239msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
1240
1241#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1242#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
1243msgid "The selected text is too long, it will be truncated."
1244msgstr "Le texte sélectionné est trop long, il sera tronqué."
1245
1246#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
1247msgid "Shaare link"
1248msgstr "Shaare"
1249
1250#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
1251msgid ""
1252"Then click ✚Add Note button anytime to start composing a private Note (text "
1253"post) to your Shaarli"
1254msgstr ""
1255"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
1256
1257#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
1258msgid "Add Note"
1259msgstr "Ajouter une Note"
1260
1261#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
1262msgid ""
1263"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1264"functionality."
1265msgstr ""
1266"Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette "
1267"fonctionalité."
1268
1269#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1270msgid "Add to"
1271msgstr "Ajouter à"
1272
1273#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
1274msgid "3rd party"
1275msgstr "Applications tierces"
1276
1277#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
1278#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
1279msgid "Plugin"
1280msgstr "Extension"
1281
1282#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
1283#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
1284msgid "plugin"
1285msgstr "extension"
1286
1287#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
1288msgid ""
1289"Drag this link to your bookmarks toolbar, or right-click it and choose "
1290"Bookmark This Link"
1291msgstr ""
1292"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1293"Ajouter aux favoris »"
1294
1295#~ msgid ""
1296#~ "An error occurred while parsing JSON configuration file (%s): error code #"
1297#~ "%d"
1298#~ msgstr ""
1299#~ "Une erreur s'est produite lors de la lecture du fichier de configuration "
1300#~ "JSON (%s) : code d'erreur #%d"
1301
1302#~ msgid ""
1303#~ "Please check your JSON syntax (without PHP comment tags) using a JSON "
1304#~ "lint tool such as "
1305#~ msgstr ""
1306#~ "Merci de vérifier la syntaxe JSON (sans les balises de commentaires PHP) "
1307#~ "en utilisant un validateur de JSON tel que "
1308
1309#~ msgid ""
1310#~ "Error: missing Composer dependencies\n"
1311#~ "\n"
1312#~ "If you installed Shaarli through Git or using the development branch,\n"
1313#~ "please refer to the installation documentation to install PHP "
1314#~ "dependencies using Composer:\n"
1315#~ msgstr ""
1316#~ "Erreur : les dépendances Composer sont manquantes\n"
1317#~ "\n"
1318#~ "Si vous avez installé Shaarli avec Git ou depuis la branche de "
1319#~ "développement\n"
1320#~ "merci de consulter la documentation d'installation pour installer les "
1321#~ "dépendances Composer :\n"
1322#~ "\n"
1323
1324#~ msgid "Sessions do not seem to work correctly on your server."
1325#~ msgstr "Les sessions ne semblent "
1326
1327#~ msgid "Tag was renamed in "
1328#~ msgstr "Le tag a été renommé dans "
1329
1330#, fuzzy
1331#~| msgid "My links"
1332#~ msgid " links"
1333#~ msgstr "Mes liens"
1334
1335#, fuzzy
1336#~| msgid ""
1337#~| "Error: missing Composer configuration\n"
1338#~| "\n"
1339#~ msgid "Error: missing Composer configuration"
1340#~ msgstr ""
1341#~ "Erreur : la configuration Composer est manquante\n"
1342#~ "\n"
1343
1344#, fuzzy
1345#~| msgid ""
1346#~| "Shaarli could not create the config file. Please make sure Shaarli has "
1347#~| "the right to write in the folder is it installed in."
1348#~ msgid ""
1349#~ "Shaarli could not create the config file. \n"
1350#~ " Please make sure Shaarli has the right to write in the "
1351#~ "folder is it installed in."
1352#~ msgstr ""
1353#~ "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier "
1354#~ "que Shaarli a les droits d'écriture dans le dossier dans lequel il est "
1355#~ "installé."
1356
1357#, fuzzy
1358#~| msgid "Plugin"
1359#~ msgid "Plugin \""
1360#~ msgstr "Extension"
1361
1362#~ msgid "Your PHP version is obsolete!"
1363#~ msgstr "Votre version de PHP est obsolète !"
1364
1365#~ msgid " Shaarli requires at least PHP "
1366#~ msgstr "Shaarli nécessite au moins PHP"
diff --git a/index.php b/index.php
index 218d317d..e1516d37 100644
--- a/index.php
+++ b/index.php
@@ -64,7 +64,6 @@ require_once 'application/FeedBuilder.php';
64require_once 'application/FileUtils.php'; 64require_once 'application/FileUtils.php';
65require_once 'application/History.php'; 65require_once 'application/History.php';
66require_once 'application/HttpUtils.php'; 66require_once 'application/HttpUtils.php';
67require_once 'application/Languages.php';
68require_once 'application/LinkDB.php'; 67require_once 'application/LinkDB.php';
69require_once 'application/LinkFilter.php'; 68require_once 'application/LinkFilter.php';
70require_once 'application/LinkUtils.php'; 69require_once 'application/LinkUtils.php';
@@ -76,8 +75,10 @@ require_once 'application/Utils.php';
76require_once 'application/PluginManager.php'; 75require_once 'application/PluginManager.php';
77require_once 'application/Router.php'; 76require_once 'application/Router.php';
78require_once 'application/Updater.php'; 77require_once 'application/Updater.php';
78use \Shaarli\Languages;
79use \Shaarli\ThemeUtils; 79use \Shaarli\ThemeUtils;
80use \Shaarli\Config\ConfigManager; 80use \Shaarli\Config\ConfigManager;
81use \Shaarli\SessionManager;
81 82
82// Ensure the PHP version is supported 83// Ensure the PHP version is supported
83try { 84try {
@@ -88,7 +89,7 @@ try {
88 exit; 89 exit;
89} 90}
90 91
91define('shaarli_version', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); 92define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
92 93
93// Force cookie path (but do not change lifetime) 94// Force cookie path (but do not change lifetime)
94$cookie = session_get_cookie_params(); 95$cookie = session_get_cookie_params();
@@ -115,14 +116,23 @@ if (session_id() == '') {
115} 116}
116 117
117// Regenerate session ID if invalid or not defined in cookie. 118// Regenerate session ID if invalid or not defined in cookie.
118if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) { 119if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
119 session_regenerate_id(true); 120 session_regenerate_id(true);
120 $_COOKIE['shaarli'] = session_id(); 121 $_COOKIE['shaarli'] = session_id();
121} 122}
122 123
123$conf = new ConfigManager(); 124$conf = new ConfigManager();
125$sessionManager = new SessionManager($_SESSION, $conf);
126
127// Sniff browser language and set date format accordingly.
128if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
129 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
130}
131
132new Languages(setlocale(LC_MESSAGES, 0), $conf);
133
124$conf->setEmpty('general.timezone', date_default_timezone_get()); 134$conf->setEmpty('general.timezone', date_default_timezone_get());
125$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); 135$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
126RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory 136RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
127RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 137RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
128 138
@@ -144,7 +154,7 @@ if (! is_file($conf->getConfigFileExt())) {
144 $errors = ApplicationUtils::checkResourcePermissions($conf); 154 $errors = ApplicationUtils::checkResourcePermissions($conf);
145 155
146 if ($errors != array()) { 156 if ($errors != array()) {
147 $message = '<p>Insufficient permissions:</p><ul>'; 157 $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
148 158
149 foreach ($errors as $error) { 159 foreach ($errors as $error) {
150 $message .= '<li>'.$error.'</li>'; 160 $message .= '<li>'.$error.'</li>';
@@ -157,17 +167,12 @@ if (! is_file($conf->getConfigFileExt())) {
157 } 167 }
158 168
159 // Display the installation form if no existing config is found 169 // Display the installation form if no existing config is found
160 install($conf); 170 install($conf, $sessionManager);
161} 171}
162 172
163// a token depending of deployment salt, user password, and the current ip 173// a token depending of deployment salt, user password, and the current ip
164define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); 174define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
165 175
166// Sniff browser language and set date format accordingly.
167if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
168 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
169}
170
171/** 176/**
172 * Checking session state (i.e. is the user still logged in) 177 * Checking session state (i.e. is the user still logged in)
173 * 178 *
@@ -376,9 +381,9 @@ function ban_canLogin($conf)
376// Process login form: Check if login/password is correct. 381// Process login form: Check if login/password is correct.
377if (isset($_POST['login'])) 382if (isset($_POST['login']))
378{ 383{
379 if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.'); 384 if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.'));
380 if (isset($_POST['password']) 385 if (isset($_POST['password'])
381 && tokenOk($_POST['token']) 386 && $sessionManager->checkToken($_POST['token'])
382 && (check_auth($_POST['login'], $_POST['password'], $conf)) 387 && (check_auth($_POST['login'], $_POST['password'], $conf))
383 ) { // Login/password is OK. 388 ) { // Login/password is OK.
384 ban_loginOk($conf); 389 ban_loginOk($conf);
@@ -440,7 +445,8 @@ if (isset($_POST['login']))
440 } 445 }
441 } 446 }
442 } 447 }
443 echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen. 448 // Redirect to login screen.
449 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
444 exit; 450 exit;
445 } 451 }
446} 452}
@@ -451,32 +457,6 @@ if (isset($_POST['login']))
451if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. 457if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session.
452 458
453/** 459/**
454 * Returns a token.
455 *
456 * @param ConfigManager $conf Configuration Manager instance.
457 *
458 * @return string token.
459 */
460function getToken($conf)
461{
462 $rnd = sha1(uniqid('', true) .'_'. mt_rand() . $conf->get('credentials.salt')); // We generate a random string.
463 $_SESSION['tokens'][$rnd]=1; // Store it on the server side.
464 return $rnd;
465}
466
467// Tells if a token is OK. Using this function will destroy the token.
468// true=token is OK.
469function tokenOk($token)
470{
471 if (isset($_SESSION['tokens'][$token]))
472 {
473 unset($_SESSION['tokens'][$token]); // Token is used: destroy it.
474 return true; // Token is OK.
475 }
476 return false; // Wrong token, or already used.
477}
478
479/**
480 * Daily RSS feed: 1 RSS entry per day giving all the links on that day. 460 * Daily RSS feed: 1 RSS entry per day giving all the links on that day.
481 * Gives the last 7 days (which have links). 461 * Gives the last 7 days (which have links).
482 * This RSS feed cannot be filtered. 462 * This RSS feed cannot be filtered.
@@ -683,12 +663,13 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
683/** 663/**
684 * Render HTML page (according to URL parameters and user rights) 664 * Render HTML page (according to URL parameters and user rights)
685 * 665 *
686 * @param ConfigManager $conf Configuration Manager instance. 666 * @param ConfigManager $conf Configuration Manager instance.
687 * @param PluginManager $pluginManager Plugin Manager instance, 667 * @param PluginManager $pluginManager Plugin Manager instance,
688 * @param LinkDB $LINKSDB 668 * @param LinkDB $LINKSDB
689 * @param History $history instance 669 * @param History $history instance
670 * @param SessionManager $sessionManager SessionManager instance
690 */ 671 */
691function renderPage($conf, $pluginManager, $LINKSDB, $history) 672function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
692{ 673{
693 $updater = new Updater( 674 $updater = new Updater(
694 read_updates_file($conf->get('resource.updates')), 675 read_updates_file($conf->get('resource.updates')),
@@ -709,7 +690,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
709 die($e->getMessage()); 690 die($e->getMessage());
710 } 691 }
711 692
712 $PAGE = new PageBuilder($conf, $LINKSDB); 693 $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
713 $PAGE->assign('linkcount', count($LINKSDB)); 694 $PAGE->assign('linkcount', count($LINKSDB));
714 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 695 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
715 $PAGE->assign('plugin_errors', $pluginManager->getErrors()); 696 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
@@ -718,6 +699,23 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
718 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; 699 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
719 $targetPage = Router::findPage($query, $_GET, isLoggedIn()); 700 $targetPage = Router::findPage($query, $_GET, isLoggedIn());
720 701
702 if (
703 // if the user isn't logged in
704 !isLoggedIn() &&
705 // and Shaarli doesn't have public content...
706 $conf->get('privacy.hide_public_links') &&
707 // and is configured to enforce the login
708 $conf->get('privacy.force_login') &&
709 // and the current page isn't already the login page
710 $targetPage !== Router::$PAGE_LOGIN &&
711 // and the user is not requesting a feed (which would lead to a different content-type as expected)
712 $targetPage !== Router::$PAGE_FEED_ATOM &&
713 $targetPage !== Router::$PAGE_FEED_RSS
714 ) {
715 // force current page to be the login page
716 $targetPage = Router::$PAGE_LOGIN;
717 }
718
721 // Call plugin hooks for header, footer and includes, specifying which page will be rendered. 719 // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
722 // Then assign generated data to RainTPL. 720 // Then assign generated data to RainTPL.
723 $common_hooks = array( 721 $common_hooks = array(
@@ -823,7 +821,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
823 } 821 }
824 822
825 $data = array( 823 $data = array(
826 'search_tags' => implode(' ', $filteringTags), 824 'search_tags' => implode(' ', escape($filteringTags)),
827 'tags' => $tagList, 825 'tags' => $tagList,
828 ); 826 );
829 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); 827 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
@@ -853,7 +851,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
853 } 851 }
854 852
855 $data = [ 853 $data = [
856 'search_tags' => implode(' ', $filteringTags), 854 'search_tags' => implode(' ', escape($filteringTags)),
857 'tags' => $tags, 855 'tags' => $tags,
858 ]; 856 ];
859 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); 857 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
@@ -1083,16 +1081,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1083 if ($targetPage == Router::$PAGE_CHANGEPASSWORD) 1081 if ($targetPage == Router::$PAGE_CHANGEPASSWORD)
1084 { 1082 {
1085 if ($conf->get('security.open_shaarli')) { 1083 if ($conf->get('security.open_shaarli')) {
1086 die('You are not supposed to change a password on an Open Shaarli.'); 1084 die(t('You are not supposed to change a password on an Open Shaarli.'));
1087 } 1085 }
1088 1086
1089 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) 1087 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
1090 { 1088 {
1091 if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! 1089 if (!$sessionManager->checkToken($_POST['token'])) die(t('Wrong token.')); // Go away!
1092 1090
1093 // Make sure old password is correct. 1091 // Make sure old password is correct.
1094 $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')); 1092 $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt'));
1095 if ($oldhash!= $conf->get('credentials.hash')) { echo '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; } 1093 if ($oldhash!= $conf->get('credentials.hash')) {
1094 echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
1095 exit;
1096 }
1096 // Save new password 1097 // Save new password
1097 // Salt renders rainbow-tables attacks useless. 1098 // Salt renders rainbow-tables attacks useless.
1098 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); 1099 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
@@ -1110,7 +1111,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1110 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>'; 1111 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
1111 exit; 1112 exit;
1112 } 1113 }
1113 echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>'; 1114 echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
1114 exit; 1115 exit;
1115 } 1116 }
1116 else // show the change password form. 1117 else // show the change password form.
@@ -1125,8 +1126,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1125 { 1126 {
1126 if (!empty($_POST['title']) ) 1127 if (!empty($_POST['title']) )
1127 { 1128 {
1128 if (!tokenOk($_POST['token'])) { 1129 if (!$sessionManager->checkToken($_POST['token'])) {
1129 die('Wrong token.'); // Go away! 1130 die(t('Wrong token.')); // Go away!
1130 } 1131 }
1131 $tz = 'UTC'; 1132 $tz = 'UTC';
1132 if (!empty($_POST['continent']) && !empty($_POST['city']) 1133 if (!empty($_POST['continent']) && !empty($_POST['city'])
@@ -1146,6 +1147,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1146 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); 1147 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1147 $conf->set('api.enabled', !empty($_POST['enableApi'])); 1148 $conf->set('api.enabled', !empty($_POST['enableApi']));
1148 $conf->set('api.secret', escape($_POST['apiSecret'])); 1149 $conf->set('api.secret', escape($_POST['apiSecret']));
1150 $conf->set('translation.language', escape($_POST['language']));
1151
1149 try { 1152 try {
1150 $conf->write(isLoggedIn()); 1153 $conf->write(isLoggedIn());
1151 $history->updateSettings(); 1154 $history->updateSettings();
@@ -1161,7 +1164,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1161 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>'; 1164 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
1162 exit; 1165 exit;
1163 } 1166 }
1164 echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>'; 1167 echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
1165 exit; 1168 exit;
1166 } 1169 }
1167 else // Show the configuration form. 1170 else // Show the configuration form.
@@ -1183,6 +1186,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1183 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false)); 1186 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
1184 $PAGE->assign('api_enabled', $conf->get('api.enabled', true)); 1187 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
1185 $PAGE->assign('api_secret', $conf->get('api.secret')); 1188 $PAGE->assign('api_secret', $conf->get('api.secret'));
1189 $PAGE->assign('languages', Languages::getAvailableLanguages());
1190 $PAGE->assign('language', $conf->get('translation.language'));
1186 $PAGE->renderPage('configure'); 1191 $PAGE->renderPage('configure');
1187 exit; 1192 exit;
1188 } 1193 }
@@ -1197,8 +1202,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1197 exit; 1202 exit;
1198 } 1203 }
1199 1204
1200 if (!tokenOk($_POST['token'])) { 1205 if (!$sessionManager->checkToken($_POST['token'])) {
1201 die('Wrong token.'); 1206 die(t('Wrong token.'));
1202 } 1207 }
1203 1208
1204 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag'])); 1209 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
@@ -1208,9 +1213,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1208 } 1213 }
1209 $delete = empty($_POST['totag']); 1214 $delete = empty($_POST['totag']);
1210 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag'])); 1215 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
1216 $count = count($alteredLinks);
1211 $alert = $delete 1217 $alert = $delete
1212 ? sprintf(t('The tag was removed from %d links.'), count($alteredLinks)) 1218 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
1213 : sprintf(t('The tag was renamed in %d links.'), count($alteredLinks)); 1219 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
1214 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>'; 1220 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
1215 exit; 1221 exit;
1216 } 1222 }
@@ -1226,8 +1232,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1226 if (isset($_POST['save_edit'])) 1232 if (isset($_POST['save_edit']))
1227 { 1233 {
1228 // Go away! 1234 // Go away!
1229 if (! tokenOk($_POST['token'])) { 1235 if (! $sessionManager->checkToken($_POST['token'])) {
1230 die('Wrong token.'); 1236 die(t('Wrong token.'));
1231 } 1237 }
1232 1238
1233 // lf_id should only be present if the link exists. 1239 // lf_id should only be present if the link exists.
@@ -1326,8 +1332,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1326 // -------- User clicked the "Delete" button when editing a link: Delete link from database. 1332 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1327 if ($targetPage == Router::$PAGE_DELETELINK) 1333 if ($targetPage == Router::$PAGE_DELETELINK)
1328 { 1334 {
1329 if (! tokenOk($_GET['token'])) { 1335 if (! $sessionManager->checkToken($_GET['token'])) {
1330 die('Wrong token.'); 1336 die(t('Wrong token.'));
1331 } 1337 }
1332 1338
1333 $ids = trim($_GET['lf_linkdate']); 1339 $ids = trim($_GET['lf_linkdate']);
@@ -1426,7 +1432,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1426 1432
1427 if ($url == '') { 1433 if ($url == '') {
1428 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); 1434 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
1429 $title = 'Note: '; 1435 $title = $conf->get('general.default_note_title', t('Note: '));
1430 } 1436 }
1431 $url = escape($url); 1437 $url = escape($url);
1432 $title = escape($title); 1438 $title = escape($title);
@@ -1533,14 +1539,17 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1533 // Import bookmarks from an uploaded file 1539 // Import bookmarks from an uploaded file
1534 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) { 1540 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1535 // The file is too big or some form field may be missing. 1541 // The file is too big or some form field may be missing.
1536 echo '<script>alert("The file you are trying to upload is probably' 1542 $msg = sprintf(
1537 .' bigger than what this webserver can accept (' 1543 t(
1538 .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').' 1544 'The file you are trying to upload is probably bigger than what this webserver can accept'
1539 .' Please upload in smaller chunks.");document.location=\'?do=' 1545 .' (%s). Please upload in smaller chunks.'
1540 .Router::$PAGE_IMPORT .'\';</script>'; 1546 ),
1547 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
1548 );
1549 echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
1541 exit; 1550 exit;
1542 } 1551 }
1543 if (! tokenOk($_POST['token'])) { 1552 if (! $sessionManager->checkToken($_POST['token'])) {
1544 die('Wrong token.'); 1553 die('Wrong token.');
1545 } 1554 }
1546 $status = NetscapeBookmarkUtils::import( 1555 $status = NetscapeBookmarkUtils::import(
@@ -1607,7 +1616,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1607 // Get a fresh token 1616 // Get a fresh token
1608 if ($targetPage == Router::$GET_TOKEN) { 1617 if ($targetPage == Router::$GET_TOKEN) {
1609 header('Content-Type:text/plain'); 1618 header('Content-Type:text/plain');
1610 echo getToken($conf); 1619 echo $sessionManager->generateToken($conf);
1611 exit; 1620 exit;
1612 } 1621 }
1613 1622
@@ -1933,10 +1942,10 @@ function lazyThumbnail($conf, $url,$href=false)
1933 * Installation 1942 * Installation
1934 * This function should NEVER be called if the file data/config.php exists. 1943 * This function should NEVER be called if the file data/config.php exists.
1935 * 1944 *
1936 * @param ConfigManager $conf Configuration Manager instance. 1945 * @param ConfigManager $conf Configuration Manager instance.
1946 * @param SessionManager $sessionManager SessionManager instance
1937 */ 1947 */
1938function install($conf) 1948function install($conf, $sessionManager) {
1939{
1940 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work. 1949 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
1941 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705); 1950 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
1942 1951
@@ -1945,12 +1954,20 @@ function install($conf)
1945 // (Because on some hosts, session.save_path may not be set correctly, 1954 // (Because on some hosts, session.save_path may not be set correctly,
1946 // or we may not have write access to it.) 1955 // or we may not have write access to it.)
1947 if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) 1956 if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working'))
1948 { // Step 2: Check if data in session is correct. 1957 {
1949 echo '<pre>Sessions do not seem to work correctly on your server.<br>'; 1958 // Step 2: Check if data in session is correct.
1950 echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>'; 1959 $msg = t(
1951 echo 'It currently points to '.session_save_path().'<br>'; 1960 '<pre>Sessions do not seem to work correctly on your server.<br>'.
1952 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>'; 1961 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
1953 echo '<br><a href="?">Click to try again.</a></pre>'; 1962 'and that you have write access to it.<br>'.
1963 'It currently points to %s.<br>'.
1964 'On some browsers, accessing your server via a hostname like \'localhost\' '.
1965 'or any custom hostname without a dot causes cookie storage to fail. '.
1966 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
1967 );
1968 $msg = sprintf($msg, session_save_path());
1969 echo $msg;
1970 echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
1954 die; 1971 die;
1955 } 1972 }
1956 if (!isset($_SESSION['session_tested'])) 1973 if (!isset($_SESSION['session_tested']))
@@ -1983,6 +2000,7 @@ function install($conf)
1983 } else { 2000 } else {
1984 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER))); 2001 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
1985 } 2002 }
2003 $conf->set('translation.language', escape($_POST['language']));
1986 $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); 2004 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1987 $conf->set('api.enabled', !empty($_POST['enableApi'])); 2005 $conf->set('api.enabled', !empty($_POST['enableApi']));
1988 $conf->set( 2006 $conf->set(
@@ -2010,10 +2028,11 @@ function install($conf)
2010 exit; 2028 exit;
2011 } 2029 }
2012 2030
2013 $PAGE = new PageBuilder($conf); 2031 $PAGE = new PageBuilder($conf, null, $sessionManager->generateToken());
2014 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); 2032 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
2015 $PAGE->assign('continents', $continents); 2033 $PAGE->assign('continents', $continents);
2016 $PAGE->assign('cities', $cities); 2034 $PAGE->assign('cities', $cities);
2035 $PAGE->assign('languages', Languages::getAvailableLanguages());
2017 $PAGE->renderPage('install'); 2036 $PAGE->renderPage('install');
2018 exit; 2037 exit;
2019} 2038}
@@ -2286,7 +2305,7 @@ $response = $app->run(true);
2286if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { 2305if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
2287 // We use UTF-8 for proper international characters handling. 2306 // We use UTF-8 for proper international characters handling.
2288 header('Content-Type: text/html; charset=utf-8'); 2307 header('Content-Type: text/html; charset=utf-8');
2289 renderPage($conf, $pluginManager, $linkDb, $history); 2308 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager);
2290} else { 2309} else {
2291 $app->respond($response); 2310 $app->respond($response);
2292} 2311}
diff --git a/mkdocs.yml b/mkdocs.yml
index 648d8f67..8617ea45 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -43,8 +43,10 @@ pages:
43 - Versioning and Branches: Versioning-and-Branches.md 43 - Versioning and Branches: Versioning-and-Branches.md
44 - Security: Security.md 44 - Security: Security.md
45 - Static analysis: Static-analysis.md 45 - Static analysis: Static-analysis.md
46 - Translations: Translations.md
46 - Theming: Theming.md 47 - Theming: Theming.md
47 - Unit tests: Unit-tests.md 48 - Unit tests: Unit-tests.md
49 - Unit tests inside Docker: Unit-tests-Docker.md
48- About: 50- About:
49 - FAQ: FAQ.md 51 - FAQ: FAQ.md
50 - Community & Related software: Community-&-Related-software.md 52 - Community & Related software: Community-&-Related-software.md
diff --git a/plugins/TODO.md b/plugins/TODO.md
deleted file mode 100644
index e3313d67..00000000
--- a/plugins/TODO.md
+++ /dev/null
@@ -1,28 +0,0 @@
1https://github.com/shaarli/Shaarli/issues/181 - Add Disqus or Isso comments box on a permalink page
2
3 * http://posativ.org/isso/
4 * install debian package https://packages.debian.org/sid/isso
5 * configure server http://posativ.org/isso/docs/configuration/server/
6 * configure client http://posativ.org/isso/docs/configuration/client/
7 * 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)
8
9
10Problem: by default, Isso thread ID is guessed from the current url (only one thread per page).
11if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config,
12with data-isso-id being the permalink of an item.
13
14`<section data-isso-id="aH7klxW" id="isso-thread"></section>`
15`data-isso-id: Set a custom thread id, defaults to current URI.`
16
17Problem: feature is currently broken https://github.com/posativ/isso/issues/27
18
19Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread
20when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment
21count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done
22by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server.
23
24Showing all full isso threads in the linklist would destroy layout
25
26-----------------------------------------------------------
27
28http://www.git-attitude.fr/2014/11/04/git-rerere/ for the merge
diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php
index ddf50aaf..8c05a231 100644
--- a/plugins/addlink_toolbar/addlink_toolbar.php
+++ b/plugins/addlink_toolbar/addlink_toolbar.php
@@ -26,11 +26,11 @@ function hook_addlink_toolbar_render_header($data)
26 array( 26 array(
27 'type' => 'text', 27 'type' => 'text',
28 'name' => 'post', 28 'name' => 'post',
29 'placeholder' => 'URI', 29 'placeholder' => t('URI'),
30 ), 30 ),
31 array( 31 array(
32 'type' => 'submit', 32 'type' => 'submit',
33 'value' => 'Add link', 33 'value' => t('Add link'),
34 'class' => 'bigbutton', 34 'class' => 'bigbutton',
35 ), 35 ),
36 ), 36 ),
@@ -40,3 +40,12 @@ function hook_addlink_toolbar_render_header($data)
40 40
41 return $data; 41 return $data;
42} 42}
43
44/**
45 * This function is never called, but contains translation calls for GNU gettext extraction.
46 */
47function addlink_toolbar_dummy_translation()
48{
49 // meta
50 t('Adds the addlink input on the linklist page.');
51}
diff --git a/plugins/archiveorg/archiveorg.html b/plugins/archiveorg/archiveorg.html
index 0781fe35..ad501f47 100644
--- a/plugins/archiveorg/archiveorg.html
+++ b/plugins/archiveorg/archiveorg.html
@@ -1 +1,5 @@
1<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> 1<span>
2 <a href="https://web.archive.org/web/%s">
3 <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
4 </a>
5</span>
diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php
index 03d13d0e..cda35751 100644
--- a/plugins/archiveorg/archiveorg.php
+++ b/plugins/archiveorg/archiveorg.php
@@ -20,9 +20,18 @@ function hook_archiveorg_render_linklist($data)
20 if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) { 20 if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
21 continue; 21 continue;
22 } 22 }
23 $archive = sprintf($archive_html, $value['url']); 23 $archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
24 $value['link_plugin'][] = $archive; 24 $value['link_plugin'][] = $archive;
25 } 25 }
26 26
27 return $data; 27 return $data;
28} 28}
29
30/**
31 * This function is never called, but contains translation calls for GNU gettext extraction.
32 */
33function archiveorg_dummy_translation()
34{
35 // meta
36 t('For each link, add an Archive.org icon.');
37}
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php
index 8fdbf663..b80a2b6d 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -14,6 +14,26 @@
14 * and check user status with _LOGGEDIN_. 14 * and check user status with _LOGGEDIN_.
15 */ 15 */
16 16
17use Shaarli\Config\ConfigManager;
18
19/**
20 * In the footer hook, there is a working example of a translation extension for Shaarli.
21 *
22 * The extension must be attached to a new translation domain (i.e. NOT 'shaarli').
23 * Use case: any custom theme or non official plugin can use the translation system.
24 *
25 * See the documentation for more information.
26 */
27const EXT_TRANSLATION_DOMAIN = 'demo';
28
29/*
30 * This is not necessary, but it's easier if you don't want Poedit to mix up your translations.
31 */
32function demo_plugin_t($text, $nText = '', $nb = 1)
33{
34 return t($text, $nText, $nb, EXT_TRANSLATION_DOMAIN);
35}
36
17/** 37/**
18 * Initialization function. 38 * Initialization function.
19 * It will be called when the plugin is loaded. 39 * It will be called when the plugin is loaded.
@@ -27,6 +47,12 @@ function demo_plugin_init($conf)
27{ 47{
28 $conf->get('toto', 'nope'); 48 $conf->get('toto', 'nope');
29 49
50 if (! $conf->exists('translation.extensions.demo')) {
51 // Custom translation with the domain 'demo'
52 $conf->set('translation.extensions.demo', 'plugins/demo_plugin/languages/');
53 $conf->write(true);
54 }
55
30 $errors[] = 'This a demo init error.'; 56 $errors[] = 'This a demo init error.';
31 return $errors; 57 return $errors;
32} 58}
@@ -160,7 +186,7 @@ function hook_demo_plugin_render_includes($data)
160function hook_demo_plugin_render_footer($data) 186function hook_demo_plugin_render_footer($data)
161{ 187{
162 // footer text 188 // footer text
163 $data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.'; 189 $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
164 190
165 // Free elements at the end of the page. 191 // Free elements at the end of the page.
166 $data['endofpage'][] = '<marquee id="demo_marquee">' . 192 $data['endofpage'][] = '<marquee id="demo_marquee">' .
@@ -433,3 +459,12 @@ function hook_demo_plugin_render_feed($data)
433 } 459 }
434 return $data; 460 return $data;
435} 461}
462
463/**
464 * This function is never called, but contains translation calls for GNU gettext extraction.
465 */
466function demo_dummy_translation()
467{
468 // meta
469 t('A demo plugin covering all use cases for template designers and plugin developers.');
470}
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
index 00000000..0f80f6ed
--- /dev/null
+++ b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo
Binary files 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
index 00000000..921379c0
--- /dev/null
+++ b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po
@@ -0,0 +1,21 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Demo plugin\n"
4"POT-Creation-Date: 2017-08-19 10:45+0200\n"
5"PO-Revision-Date: 2017-08-19 11:28+0200\n"
6"Last-Translator: \n"
7"Language-Team: demo\n"
8"Language: fr\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.0.2\n"
13"X-Poedit-Basepath: ../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-KeywordsList: ;demo_plugin_t:1,2;demo_plugin_t\n"
16"X-Poedit-SourceCharset: UTF-8\n"
17"X-Poedit-SearchPath-0: .\n"
18
19#: demo_plugin.php:173
20msgid "Shaarli is now enhanced by the awesome demo_plugin."
21msgstr "Shaarli est maintenant amélioré avec le fantastique demo_plugin."
diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php
index ce16645f..5bc1cce2 100644
--- a/plugins/isso/isso.php
+++ b/plugins/isso/isso.php
@@ -4,10 +4,11 @@
4 * Plugin Isso. 4 * Plugin Isso.
5 */ 5 */
6 6
7use Shaarli\Config\ConfigManager;
8
7/** 9/**
8 * Display an error everywhere if the plugin is enabled without configuration. 10 * Display an error everywhere if the plugin is enabled without configuration.
9 * 11 *
10 * @param $data array List of links
11 * @param $conf ConfigManager instance 12 * @param $conf ConfigManager instance
12 * 13 *
13 * @return mixed - linklist data with Isso plugin. 14 * @return mixed - linklist data with Isso plugin.
@@ -16,8 +17,8 @@ function isso_init($conf)
16{ 17{
17 $issoUrl = $conf->get('plugins.ISSO_SERVER'); 18 $issoUrl = $conf->get('plugins.ISSO_SERVER');
18 if (empty($issoUrl)) { 19 if (empty($issoUrl)) {
19 $error = 'Isso plugin error: '. 20 $error = t('Isso plugin error: '.
20 'Please define the "ISSO_SERVER" setting in the plugin administration page.'; 21 'Please define the "ISSO_SERVER" setting in the plugin administration page.');
21 return array($error); 22 return array($error);
22 } 23 }
23} 24}
@@ -52,3 +53,13 @@ function hook_isso_render_linklist($data, $conf)
52 53
53 return $data; 54 return $data;
54} 55}
56
57/**
58 * This function is never called, but contains translation calls for GNU gettext extraction.
59 */
60function isso_dummy_translation()
61{
62 // meta
63 t('Let visitor comment your shaares on permalinks with Isso.');
64 t('Isso server URL (without \'http://\')');
65}
diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html
index 9c4e5ae0..ded3d347 100644
--- a/plugins/markdown/help.html
+++ b/plugins/markdown/help.html
@@ -1,5 +1,5 @@
1<div class="md_help"> 1<div class="md_help">
2 Description will be rendered with 2 %s
3 <a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation"> 3 <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
4 Markdown syntax</a>. 4 %s</a>.
5</div> 5</div>
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
index 772c56e8..1531549d 100644
--- a/plugins/markdown/markdown.php
+++ b/plugins/markdown/markdown.php
@@ -154,8 +154,13 @@ function hook_markdown_render_includes($data)
154function hook_markdown_render_editlink($data) 154function hook_markdown_render_editlink($data)
155{ 155{
156 // Load help HTML into a string 156 // Load help HTML into a string
157 $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html'); 157 $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
158 158 $translations = [
159 t('Description will be rendered with'),
160 t('Markdown syntax documentation'),
161 t('Markdown syntax'),
162 ];
163 $data['edit_link_plugin'][] = vsprintf($txt, $translations);
159 // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion. 164 // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
160 if (! in_array(NO_MD_TAG, $data['tags'])) { 165 if (! in_array(NO_MD_TAG, $data['tags'])) {
161 $data['tags'][NO_MD_TAG] = 0; 166 $data['tags'][NO_MD_TAG] = 0;
@@ -325,3 +330,15 @@ function process_markdown($description, $escape = true, $allowedProtocols = [])
325 330
326 return $processedDescription; 331 return $processedDescription;
327} 332}
333
334/**
335 * This function is never called, but contains translation calls for GNU gettext extraction.
336 */
337function markdown_dummy_translation()
338{
339 // meta
340 t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
341If your shaared descriptions contained HTML tags before enabling the markdown plugin,
342enabling it might break your page.
343See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
344}
diff --git a/plugins/piwik/piwik.php b/plugins/piwik/piwik.php
index 4a2b48a1..ca00c2be 100644
--- a/plugins/piwik/piwik.php
+++ b/plugins/piwik/piwik.php
@@ -18,8 +18,8 @@ function piwik_init($conf)
18 $piwikUrl = $conf->get('plugins.PIWIK_URL'); 18 $piwikUrl = $conf->get('plugins.PIWIK_URL');
19 $piwikSiteid = $conf->get('plugins.PIWIK_SITEID'); 19 $piwikSiteid = $conf->get('plugins.PIWIK_SITEID');
20 if (empty($piwikUrl) || empty($piwikSiteid)) { 20 if (empty($piwikUrl) || empty($piwikSiteid)) {
21 $error = 'Piwik plugin error: ' . 21 $error = t('Piwik plugin error: ' .
22 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'; 22 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.');
23 return array($error); 23 return array($error);
24 } 24 }
25} 25}
@@ -60,3 +60,14 @@ function hook_piwik_render_footer($data, $conf)
60 60
61 return $data; 61 return $data;
62} 62}
63
64/**
65 * This function is never called, but contains translation calls for GNU gettext extraction.
66 */
67function piwik_dummy_translation()
68{
69 // meta
70 t('A plugin that adds Piwik tracking code to Shaarli pages.');
71 t('Piwik URL');
72 t('Piwik site ID');
73}
diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php
index 64484504..c6d6b0cc 100644
--- a/plugins/playvideos/playvideos.php
+++ b/plugins/playvideos/playvideos.php
@@ -19,10 +19,10 @@ function hook_playvideos_render_header($data)
19 $playvideo = array( 19 $playvideo = array(
20 'attr' => array( 20 'attr' => array(
21 'href' => '#', 21 'href' => '#',
22 'title' => 'Video player', 22 'title' => t('Video player'),
23 'id' => 'playvideos', 23 'id' => 'playvideos',
24 ), 24 ),
25 'html' => 'â–º Play Videos' 25 'html' => 'â–º '. t('Play Videos')
26 ); 26 );
27 $data['buttons_toolbar'][] = $playvideo; 27 $data['buttons_toolbar'][] = $playvideo;
28 } 28 }
@@ -46,3 +46,12 @@ function hook_playvideos_render_footer($data)
46 46
47 return $data; 47 return $data;
48} 48}
49
50/**
51 * This function is never called, but contains translation calls for GNU gettext extraction.
52 */
53function playvideos_dummy_translation()
54{
55 // meta
56 t('Add a button in the toolbar allowing to watch all videos.');
57}
diff --git a/plugins/pubsubhubbub/pubsubhubbub.php b/plugins/pubsubhubbub/pubsubhubbub.php
index 03b6757b..184b588b 100644
--- a/plugins/pubsubhubbub/pubsubhubbub.php
+++ b/plugins/pubsubhubbub/pubsubhubbub.php
@@ -10,6 +10,7 @@
10 */ 10 */
11 11
12use pubsubhubbub\publisher\Publisher; 12use pubsubhubbub\publisher\Publisher;
13use Shaarli\Config\ConfigManager;
13 14
14/** 15/**
15 * Plugin init function - set the hub to the default appspot one. 16 * Plugin init function - set the hub to the default appspot one.
@@ -65,7 +66,7 @@ function hook_pubsubhubbub_save_link($data, $conf)
65 $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL')); 66 $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL'));
66 $p->publish_update($feeds, $httpPost); 67 $p->publish_update($feeds, $httpPost);
67 } catch (Exception $e) { 68 } catch (Exception $e) {
68 error_log('Could not publish to PubSubHubbub: ' . $e->getMessage()); 69 error_log(sprintf(t('Could not publish to PubSubHubbub: %s'), $e->getMessage()));
69 } 70 }
70 71
71 return $data; 72 return $data;
@@ -91,11 +92,20 @@ function nocurl_http_post($url, $postString) {
91 $context = stream_context_create($params); 92 $context = stream_context_create($params);
92 $fp = @fopen($url, 'rb', false, $context); 93 $fp = @fopen($url, 'rb', false, $context);
93 if (!$fp) { 94 if (!$fp) {
94 throw new Exception('Could not post to '. $url); 95 throw new Exception(sprintf(t('Could not post to %s'), $url));
95 } 96 }
96 $response = @stream_get_contents($fp); 97 $response = @stream_get_contents($fp);
97 if ($response === false) { 98 if ($response === false) {
98 throw new Exception('Bad response from the hub '. $url); 99 throw new Exception(sprintf(t('Bad response from the hub %s'), $url));
99 } 100 }
100 return $response; 101 return $response;
101} 102}
103
104/**
105 * This function is never called, but contains translation calls for GNU gettext extraction.
106 */
107function pubsubhubbub_dummy_translation()
108{
109 // meta
110 t('Enable PubSubHubbub feed publishing.');
111}
diff --git a/plugins/qrcode/qrcode.meta b/plugins/qrcode/qrcode.meta
index cbf371ea..1812cd21 100644
--- a/plugins/qrcode/qrcode.meta
+++ b/plugins/qrcode/qrcode.meta
@@ -1 +1 @@
description="For each link, add a QRCode icon ." description="For each link, add a QRCode icon."
diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php
index 8bc610d1..0f96a106 100644
--- a/plugins/qrcode/qrcode.php
+++ b/plugins/qrcode/qrcode.php
@@ -59,3 +59,12 @@ function hook_qrcode_render_includes($data)
59 59
60 return $data; 60 return $data;
61} 61}
62
63/**
64 * This function is never called, but contains translation calls for GNU gettext extraction.
65 */
66function qrcode_dummy_translation()
67{
68 // meta
69 t('For each link, add a QRCode icon.');
70}
diff --git a/plugins/wallabag/wallabag.html b/plugins/wallabag/wallabag.html
index e861536d..4c57691d 100644
--- a/plugins/wallabag/wallabag.html
+++ b/plugins/wallabag/wallabag.html
@@ -1 +1,5 @@
1<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> 1<span>
2 <a href="%s%s" target="_blank">
3 <img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="%s" alt="wallabag" />
4 </a>
5</span>
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php
index 641e4cc2..9dfd079e 100644
--- a/plugins/wallabag/wallabag.php
+++ b/plugins/wallabag/wallabag.php
@@ -5,6 +5,7 @@
5 */ 5 */
6 6
7require_once 'WallabagInstance.php'; 7require_once 'WallabagInstance.php';
8use Shaarli\Config\ConfigManager;
8 9
9/** 10/**
10 * Init function, return an error if the server is not set. 11 * Init function, return an error if the server is not set.
@@ -17,8 +18,8 @@ function wallabag_init($conf)
17{ 18{
18 $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); 19 $wallabagUrl = $conf->get('plugins.WALLABAG_URL');
19 if (empty($wallabagUrl)) { 20 if (empty($wallabagUrl)) {
20 $error = 'Wallabag plugin error: '. 21 $error = t('Wallabag plugin error: '.
21 'Please define the "WALLABAG_URL" setting in the plugin administration page.'; 22 'Please define the "WALLABAG_URL" setting in the plugin administration page.');
22 return array($error); 23 return array($error);
23 } 24 }
24} 25}
@@ -43,12 +44,14 @@ function hook_wallabag_render_linklist($data, $conf)
43 44
44 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); 45 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
45 46
47 $linkTitle = t('Save to wallabag');
46 foreach ($data['links'] as &$value) { 48 foreach ($data['links'] as &$value) {
47 $wallabag = sprintf( 49 $wallabag = sprintf(
48 $wallabagHtml, 50 $wallabagHtml,
49 $wallabagInstance->getWallabagUrl(), 51 $wallabagInstance->getWallabagUrl(),
50 urlencode($value['url']), 52 urlencode($value['url']),
51 PluginManager::$PLUGINS_PATH 53 PluginManager::$PLUGINS_PATH,
54 $linkTitle
52 ); 55 );
53 $value['link_plugin'][] = $wallabag; 56 $value['link_plugin'][] = $wallabag;
54 } 57 }
@@ -56,3 +59,14 @@ function hook_wallabag_render_linklist($data, $conf)
56 return $data; 59 return $data;
57} 60}
58 61
62/**
63 * This function is never called, but contains translation calls for GNU gettext extraction.
64 */
65function wallabag_dummy_translation()
66{
67 // meta
68 t('For each link, add a QRCode icon.');
69 t('Wallabag API URL');
70 t('Wallabag API version (1 or 2)');
71}
72
diff --git a/tests/LanguagesTest.php b/tests/LanguagesTest.php
index 79c136c8..864ce630 100644
--- a/tests/LanguagesTest.php
+++ b/tests/LanguagesTest.php
@@ -1,41 +1,203 @@
1<?php 1<?php
2 2
3require_once 'application/Languages.php'; 3namespace Shaarli;
4
5use Shaarli\Config\ConfigManager;
4 6
5/** 7/**
6 * Class LanguagesTest. 8 * Class LanguagesTest.
7 */ 9 */
8class LanguagesTest extends PHPUnit_Framework_TestCase 10class LanguagesTest extends \PHPUnit_Framework_TestCase
9{ 11{
10 /** 12 /**
13 * @var string Config file path (without extension).
14 */
15 protected static $configFile = 'tests/utils/config/configJson';
16
17 /**
18 * @var ConfigManager
19 */
20 protected $conf;
21
22 /**
23 *
24 */
25 public function setUp()
26 {
27 $this->conf = new ConfigManager(self::$configFile);
28 }
29
30 /**
31 * Test t() with a simple non identified value.
32 */
33 public function testTranslateSingleNotIDGettext()
34 {
35 $this->conf->set('translation.mode', 'gettext');
36 new Languages('en', $this->conf);
37 $text = 'abcdé 564 fgK';
38 $this->assertEquals($text, t($text));
39 }
40
41 /**
42 * Test t() with a simple identified value in gettext mode.
43 */
44 public function testTranslateSingleIDGettext()
45 {
46 $this->conf->set('translation.mode', 'gettext');
47 new Languages('en', $this->conf);
48 $text = 'permalink';
49 $this->assertEquals($text, t($text));
50 }
51
52 /**
53 * Test t() with a non identified plural form in gettext mode.
54 */
55 public function testTranslatePluralNotIDGettext()
56 {
57 $this->conf->set('translation.mode', 'gettext');
58 new Languages('en', $this->conf);
59 $text = 'sandwich';
60 $nText = 'sandwiches';
61 $this->assertEquals('sandwiches', t($text, $nText, 0));
62 $this->assertEquals('sandwich', t($text, $nText, 1));
63 $this->assertEquals('sandwiches', t($text, $nText, 2));
64 }
65
66 /**
67 * Test t() with an identified plural form in gettext mode.
68 */
69 public function testTranslatePluralIDGettext()
70 {
71 $this->conf->set('translation.mode', 'gettext');
72 new Languages('en', $this->conf);
73 $text = 'shaare';
74 $nText = 'shaares';
75 // In english, zero is followed by plural form
76 $this->assertEquals('shaares', t($text, $nText, 0));
77 $this->assertEquals('shaare', t($text, $nText, 1));
78 $this->assertEquals('shaares', t($text, $nText, 2));
79 }
80
81 /**
11 * Test t() with a simple non identified value. 82 * Test t() with a simple non identified value.
12 */ 83 */
13 public function testTranslateSingleNotID() 84 public function testTranslateSingleNotIDPhp()
14 { 85 {
86 $this->conf->set('translation.mode', 'php');
87 new Languages('en', $this->conf);
15 $text = 'abcdé 564 fgK'; 88 $text = 'abcdé 564 fgK';
16 $this->assertEquals($text, t($text)); 89 $this->assertEquals($text, t($text));
17 } 90 }
18 91
19 /** 92 /**
20 * Test t() with a non identified plural form. 93 * Test t() with a simple identified value in PHP mode.
21 */ 94 */
22 public function testTranslatePluralNotID() 95 public function testTranslateSingleIDPhp()
23 { 96 {
24 $text = '%s sandwich'; 97 $this->conf->set('translation.mode', 'php');
25 $nText = '%s sandwiches'; 98 new Languages('en', $this->conf);
26 $this->assertEquals('0 sandwich', t($text, $nText)); 99 $text = 'permalink';
27 $this->assertEquals('1 sandwich', t($text, $nText, 1)); 100 $this->assertEquals($text, t($text));
28 $this->assertEquals('2 sandwiches', t($text, $nText, 2));
29 } 101 }
30 102
31 /** 103 /**
32 * Test t() with a non identified invalid plural form. 104 * Test t() with a non identified plural form in PHP mode.
33 */ 105 */
34 public function testTranslatePluralNotIDInvalid() 106 public function testTranslatePluralNotIDPhp()
35 { 107 {
108 $this->conf->set('translation.mode', 'php');
109 new Languages('en', $this->conf);
36 $text = 'sandwich'; 110 $text = 'sandwich';
37 $nText = 'sandwiches'; 111 $nText = 'sandwiches';
112 $this->assertEquals('sandwiches', t($text, $nText, 0));
38 $this->assertEquals('sandwich', t($text, $nText, 1)); 113 $this->assertEquals('sandwich', t($text, $nText, 1));
39 $this->assertEquals('sandwiches', t($text, $nText, 2)); 114 $this->assertEquals('sandwiches', t($text, $nText, 2));
40 } 115 }
116
117 /**
118 * Test t() with an identified plural form in PHP mode.
119 */
120 public function testTranslatePluralIDPhp()
121 {
122 $this->conf->set('translation.mode', 'php');
123 new Languages('en', $this->conf);
124 $text = 'shaare';
125 $nText = 'shaares';
126 // In english, zero is followed by plural form
127 $this->assertEquals('shaares', t($text, $nText, 0));
128 $this->assertEquals('shaare', t($text, $nText, 1));
129 $this->assertEquals('shaares', t($text, $nText, 2));
130 }
131
132 /**
133 * Test t() with an invalid language set in the configuration in gettext mode.
134 */
135 public function testTranslateWithInvalidConfLanguageGettext()
136 {
137 $this->conf->set('translation.mode', 'gettext');
138 $this->conf->set('translation.language', 'nope');
139 new Languages('fr', $this->conf);
140 $text = 'grumble';
141 $this->assertEquals($text, t($text));
142 }
143
144 /**
145 * Test t() with an invalid language set in the configuration in PHP mode.
146 */
147 public function testTranslateWithInvalidConfLanguagePhp()
148 {
149 $this->conf->set('translation.mode', 'php');
150 $this->conf->set('translation.language', 'nope');
151 new Languages('fr', $this->conf);
152 $text = 'grumble';
153 $this->assertEquals($text, t($text));
154 }
155
156 /**
157 * Test t() with an invalid language set with auto language in gettext mode.
158 */
159 public function testTranslateWithInvalidAutoLanguageGettext()
160 {
161 $this->conf->set('translation.mode', 'gettext');
162 new Languages('nope', $this->conf);
163 $text = 'grumble';
164 $this->assertEquals($text, t($text));
165 }
166
167 /**
168 * Test t() with an invalid language set with auto language in PHP mode.
169 */
170 public function testTranslateWithInvalidAutoLanguagePhp()
171 {
172 $this->conf->set('translation.mode', 'php');
173 new Languages('nope', $this->conf);
174 $text = 'grumble';
175 $this->assertEquals($text, t($text));
176 }
177
178 /**
179 * Test t() with an extension language file in gettext mode
180 */
181 public function testTranslationExtensionGettext()
182 {
183 $this->conf->set('translation.mode', 'gettext');
184 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
185 new Languages('en', $this->conf);
186 $txt = 'car'; // ignore me poedit
187 $this->assertEquals('car', t($txt, $txt, 1, 'test'));
188 $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
189 }
190
191 /**
192 * Test t() with an extension language file in PHP mode
193 */
194 public function testTranslationExtensionPhp()
195 {
196 $this->conf->set('translation.mode', 'php');
197 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
198 new Languages('en', $this->conf);
199 $txt = 'car'; // ignore me poedit
200 $this->assertEquals('car', t($txt, $txt, 1, 'test'));
201 $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
202 }
41} 203}
diff --git a/tests/LinkUtilsTest.php b/tests/LinkUtilsTest.php
index 7c0d4b0b..c77922ec 100644
--- a/tests/LinkUtilsTest.php
+++ b/tests/LinkUtilsTest.php
@@ -103,6 +103,16 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
103 $expectedText = 'stuff <a href="http://hello.there/is=someone#here">http://hello.there/is=someone#here</a> otherstuff'; 103 $expectedText = 'stuff <a href="http://hello.there/is=someone#here">http://hello.there/is=someone#here</a> otherstuff';
104 $processedText = text2clickable($text, ''); 104 $processedText = text2clickable($text, '');
105 $this->assertEquals($expectedText, $processedText); 105 $this->assertEquals($expectedText, $processedText);
106
107 $text = 'stuff http://hello.there/is=someone#here(please) otherstuff';
108 $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)">http://hello.there/is=someone#here(please)</a> otherstuff';
109 $processedText = text2clickable($text, '');
110 $this->assertEquals($expectedText, $processedText);
111
112 $text = 'stuff http://hello.there/is=someone#here(please)&no otherstuff';
113 $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)&no">http://hello.there/is=someone#here(please)&no</a> otherstuff';
114 $processedText = text2clickable($text, '');
115 $this->assertEquals($expectedText, $processedText);
106 } 116 }
107 117
108 /** 118 /**
diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
index 5fc1d1e8..4961aa2c 100644
--- a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
+++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
@@ -132,8 +132,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
132 public function testImportInternetExplorerEncoding() 132 public function testImportInternetExplorerEncoding()
133 { 133 {
134 $files = file2array('internet_explorer_encoding.htm'); 134 $files = file2array('internet_explorer_encoding.htm');
135 $this->assertEquals( 135 $this->assertStringMatchesFormat(
136 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:' 136 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
137 .' 1 links imported, 0 links overwritten, 0 links skipped.', 137 .' 1 links imported, 0 links overwritten, 0 links skipped.',
138 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 138 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
139 ); 139 );
@@ -161,8 +161,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
161 public function testImportNested() 161 public function testImportNested()
162 { 162 {
163 $files = file2array('netscape_nested.htm'); 163 $files = file2array('netscape_nested.htm');
164 $this->assertEquals( 164 $this->assertStringMatchesFormat(
165 'File netscape_nested.htm (1337 bytes) was successfully processed:' 165 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
166 .' 8 links imported, 0 links overwritten, 0 links skipped.', 166 .' 8 links imported, 0 links overwritten, 0 links skipped.',
167 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 167 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
168 ); 168 );
@@ -283,8 +283,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
283 public function testImportDefaultPrivacyNoPost() 283 public function testImportDefaultPrivacyNoPost()
284 { 284 {
285 $files = file2array('netscape_basic.htm'); 285 $files = file2array('netscape_basic.htm');
286 $this->assertEquals( 286 $this->assertStringMatchesFormat(
287 'File netscape_basic.htm (482 bytes) was successfully processed:' 287 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
288 .' 2 links imported, 0 links overwritten, 0 links skipped.', 288 .' 2 links imported, 0 links overwritten, 0 links skipped.',
289 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 289 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
290 ); 290 );
@@ -328,8 +328,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
328 { 328 {
329 $post = array('privacy' => 'default'); 329 $post = array('privacy' => 'default');
330 $files = file2array('netscape_basic.htm'); 330 $files = file2array('netscape_basic.htm');
331 $this->assertEquals( 331 $this->assertStringMatchesFormat(
332 'File netscape_basic.htm (482 bytes) was successfully processed:' 332 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
333 .' 2 links imported, 0 links overwritten, 0 links skipped.', 333 .' 2 links imported, 0 links overwritten, 0 links skipped.',
334 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 334 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
335 ); 335 );
@@ -372,8 +372,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
372 { 372 {
373 $post = array('privacy' => 'public'); 373 $post = array('privacy' => 'public');
374 $files = file2array('netscape_basic.htm'); 374 $files = file2array('netscape_basic.htm');
375 $this->assertEquals( 375 $this->assertStringMatchesFormat(
376 'File netscape_basic.htm (482 bytes) was successfully processed:' 376 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
377 .' 2 links imported, 0 links overwritten, 0 links skipped.', 377 .' 2 links imported, 0 links overwritten, 0 links skipped.',
378 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 378 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
379 ); 379 );
@@ -396,8 +396,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
396 { 396 {
397 $post = array('privacy' => 'private'); 397 $post = array('privacy' => 'private');
398 $files = file2array('netscape_basic.htm'); 398 $files = file2array('netscape_basic.htm');
399 $this->assertEquals( 399 $this->assertStringMatchesFormat(
400 'File netscape_basic.htm (482 bytes) was successfully processed:' 400 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
401 .' 2 links imported, 0 links overwritten, 0 links skipped.', 401 .' 2 links imported, 0 links overwritten, 0 links skipped.',
402 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 402 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
403 ); 403 );
@@ -422,8 +422,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
422 422
423 // import links as private 423 // import links as private
424 $post = array('privacy' => 'private'); 424 $post = array('privacy' => 'private');
425 $this->assertEquals( 425 $this->assertStringMatchesFormat(
426 'File netscape_basic.htm (482 bytes) was successfully processed:' 426 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
427 .' 2 links imported, 0 links overwritten, 0 links skipped.', 427 .' 2 links imported, 0 links overwritten, 0 links skipped.',
428 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 428 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
429 ); 429 );
@@ -442,8 +442,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
442 'privacy' => 'public', 442 'privacy' => 'public',
443 'overwrite' => 'true' 443 'overwrite' => 'true'
444 ); 444 );
445 $this->assertEquals( 445 $this->assertStringMatchesFormat(
446 'File netscape_basic.htm (482 bytes) was successfully processed:' 446 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
447 .' 2 links imported, 2 links overwritten, 0 links skipped.', 447 .' 2 links imported, 2 links overwritten, 0 links skipped.',
448 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 448 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
449 ); 449 );
@@ -468,8 +468,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
468 468
469 // import links as public 469 // import links as public
470 $post = array('privacy' => 'public'); 470 $post = array('privacy' => 'public');
471 $this->assertEquals( 471 $this->assertStringMatchesFormat(
472 'File netscape_basic.htm (482 bytes) was successfully processed:' 472 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
473 .' 2 links imported, 0 links overwritten, 0 links skipped.', 473 .' 2 links imported, 0 links overwritten, 0 links skipped.',
474 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 474 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
475 ); 475 );
@@ -489,8 +489,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
489 'privacy' => 'private', 489 'privacy' => 'private',
490 'overwrite' => 'true' 490 'overwrite' => 'true'
491 ); 491 );
492 $this->assertEquals( 492 $this->assertStringMatchesFormat(
493 'File netscape_basic.htm (482 bytes) was successfully processed:' 493 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
494 .' 2 links imported, 2 links overwritten, 0 links skipped.', 494 .' 2 links imported, 2 links overwritten, 0 links skipped.',
495 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 495 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
496 ); 496 );
@@ -513,8 +513,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
513 { 513 {
514 $post = array('privacy' => 'public'); 514 $post = array('privacy' => 'public');
515 $files = file2array('netscape_basic.htm'); 515 $files = file2array('netscape_basic.htm');
516 $this->assertEquals( 516 $this->assertStringMatchesFormat(
517 'File netscape_basic.htm (482 bytes) was successfully processed:' 517 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
518 .' 2 links imported, 0 links overwritten, 0 links skipped.', 518 .' 2 links imported, 0 links overwritten, 0 links skipped.',
519 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 519 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
520 ); 520 );
@@ -523,8 +523,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
523 523
524 // re-import as private, DO NOT enable overwriting 524 // re-import as private, DO NOT enable overwriting
525 $post = array('privacy' => 'private'); 525 $post = array('privacy' => 'private');
526 $this->assertEquals( 526 $this->assertStringMatchesFormat(
527 'File netscape_basic.htm (482 bytes) was successfully processed:' 527 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
528 .' 0 links imported, 0 links overwritten, 2 links skipped.', 528 .' 0 links imported, 0 links overwritten, 2 links skipped.',
529 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 529 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
530 ); 530 );
@@ -542,8 +542,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
542 'default_tags' => 'tag1,tag2 tag3' 542 'default_tags' => 'tag1,tag2 tag3'
543 ); 543 );
544 $files = file2array('netscape_basic.htm'); 544 $files = file2array('netscape_basic.htm');
545 $this->assertEquals( 545 $this->assertStringMatchesFormat(
546 'File netscape_basic.htm (482 bytes) was successfully processed:' 546 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
547 .' 2 links imported, 0 links overwritten, 0 links skipped.', 547 .' 2 links imported, 0 links overwritten, 0 links skipped.',
548 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 548 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
549 ); 549 );
@@ -569,8 +569,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
569 'default_tags' => 'tag1&,tag2 "tag3"' 569 'default_tags' => 'tag1&,tag2 "tag3"'
570 ); 570 );
571 $files = file2array('netscape_basic.htm'); 571 $files = file2array('netscape_basic.htm');
572 $this->assertEquals( 572 $this->assertStringMatchesFormat(
573 'File netscape_basic.htm (482 bytes) was successfully processed:' 573 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
574 .' 2 links imported, 0 links overwritten, 0 links skipped.', 574 .' 2 links imported, 0 links overwritten, 0 links skipped.',
575 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 575 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
576 ); 576 );
@@ -594,8 +594,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
594 public function testImportSameDate() 594 public function testImportSameDate()
595 { 595 {
596 $files = file2array('same_date.htm'); 596 $files = file2array('same_date.htm');
597 $this->assertEquals( 597 $this->assertStringMatchesFormat(
598 'File same_date.htm (453 bytes) was successfully processed:' 598 'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
599 .' 3 links imported, 0 links overwritten, 0 links skipped.', 599 .' 3 links imported, 0 links overwritten, 0 links skipped.',
600 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history) 600 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history)
601 ); 601 );
@@ -622,24 +622,19 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
622 'overwrite' => 'true', 622 'overwrite' => 'true',
623 ]; 623 ];
624 $files = file2array('netscape_basic.htm'); 624 $files = file2array('netscape_basic.htm');
625 $nbLinks = 2;
626 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); 625 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
627 $history = $this->history->getHistory(); 626 $history = $this->history->getHistory();
628 $this->assertEquals($nbLinks, count($history)); 627 $this->assertEquals(1, count($history));
629 foreach ($history as $value) { 628 $this->assertEquals(History::IMPORT, $history[0]['event']);
630 $this->assertEquals(History::CREATED, $value['event']); 629 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
631 $this->assertTrue(new DateTime('-5 seconds') < $value['datetime']);
632 $this->assertTrue(is_int($value['id']));
633 }
634 630
635 // re-import as private, enable overwriting 631 // re-import as private, enable overwriting
636 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); 632 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
637 $history = $this->history->getHistory(); 633 $history = $this->history->getHistory();
638 $this->assertEquals($nbLinks * 2, count($history)); 634 $this->assertEquals(2, count($history));
639 for ($i = 0 ; $i < $nbLinks ; $i++) { 635 $this->assertEquals(History::IMPORT, $history[0]['event']);
640 $this->assertEquals(History::UPDATED, $history[$i]['event']); 636 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
641 $this->assertTrue(new DateTime('-5 seconds') < $history[$i]['datetime']); 637 $this->assertEquals(History::IMPORT, $history[1]['event']);
642 $this->assertTrue(is_int($history[$i]['id'])); 638 $this->assertTrue(new DateTime('-5 seconds') < $history[1]['datetime']);
643 }
644 } 639 }
645} 640}
diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php
new file mode 100644
index 00000000..a92c3ccc
--- /dev/null
+++ b/tests/SessionManagerTest.php
@@ -0,0 +1,160 @@
1<?php
2// Initialize reference data _before_ PHPUnit starts a session
3require_once 'tests/utils/ReferenceSessionIdHashes.php';
4ReferenceSessionIdHashes::genAllHashes();
5
6use \Shaarli\SessionManager;
7use \PHPUnit\Framework\TestCase;
8
9
10/**
11 * Fake ConfigManager
12 */
13class FakeConfigManager
14{
15 public static function get($key)
16 {
17 return $key;
18 }
19}
20
21
22/**
23 * Test coverage for SessionManager
24 */
25class SessionManagerTest extends TestCase
26{
27 // Session ID hashes
28 protected static $sidHashes = null;
29
30 /**
31 * Assign reference data
32 */
33 public static function setUpBeforeClass()
34 {
35 self::$sidHashes = ReferenceSessionIdHashes::getHashes();
36 }
37
38 /**
39 * Generate a session token
40 */
41 public function testGenerateToken()
42 {
43 $session = [];
44 $conf = new FakeConfigManager();
45 $sessionManager = new SessionManager($session, $conf);
46
47 $token = $sessionManager->generateToken();
48
49 $this->assertEquals(1, $session['tokens'][$token]);
50 $this->assertEquals(40, strlen($token));
51 }
52
53 /**
54 * Check a session token
55 */
56 public function testCheckToken()
57 {
58 $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
59 $session = [
60 'tokens' => [
61 $token => 1,
62 ],
63 ];
64 $conf = new FakeConfigManager();
65 $sessionManager = new SessionManager($session, $conf);
66
67
68 // check and destroy the token
69 $this->assertTrue($sessionManager->checkToken($token));
70 $this->assertFalse(isset($session['tokens'][$token]));
71
72 // ensure the token has been destroyed
73 $this->assertFalse($sessionManager->checkToken($token));
74 }
75
76 /**
77 * Generate and check a session token
78 */
79 public function testGenerateAndCheckToken()
80 {
81 $session = [];
82 $conf = new FakeConfigManager();
83 $sessionManager = new SessionManager($session, $conf);
84
85 $token = $sessionManager->generateToken();
86
87 // ensure a token has been generated
88 $this->assertEquals(1, $session['tokens'][$token]);
89 $this->assertEquals(40, strlen($token));
90
91 // check and destroy the token
92 $this->assertTrue($sessionManager->checkToken($token));
93 $this->assertFalse(isset($session['tokens'][$token]));
94
95 // ensure the token has been destroyed
96 $this->assertFalse($sessionManager->checkToken($token));
97 }
98
99 /**
100 * Check an invalid session token
101 */
102 public function testCheckInvalidToken()
103 {
104 $session = [];
105 $conf = new FakeConfigManager();
106 $sessionManager = new SessionManager($session, $conf);
107
108 $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
109 }
110
111 /**
112 * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
113 *
114 * This tests extensively covers all hash algorithms / bit representations
115 */
116 public function testIsAnyHashSessionIdValid()
117 {
118 foreach (self::$sidHashes as $algo => $bpcs) {
119 foreach ($bpcs as $bpc => $hash) {
120 $this->assertTrue(SessionManager::checkId($hash));
121 }
122 }
123 }
124
125 /**
126 * Test checkId with a valid ID - SHA-1 hashes
127 */
128 public function testIsSha1SessionIdValid()
129 {
130 $this->assertTrue(SessionManager::checkId(sha1('shaarli')));
131 }
132
133 /**
134 * Test checkId with a valid ID - SHA-256 hashes
135 */
136 public function testIsSha256SessionIdValid()
137 {
138 $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
139 }
140
141 /**
142 * Test checkId with a valid ID - SHA-512 hashes
143 */
144 public function testIsSha512SessionIdValid()
145 {
146 $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
147 }
148
149 /**
150 * Test checkId with invalid IDs.
151 */
152 public function testIsSessionIdInvalid()
153 {
154 $this->assertFalse(SessionManager::checkId(''));
155 $this->assertFalse(SessionManager::checkId([]));
156 $this->assertFalse(
157 SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
158 );
159 }
160}
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index 3d1aa653..6cd37a7a 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -5,10 +5,6 @@
5 5
6require_once 'application/Utils.php'; 6require_once 'application/Utils.php';
7require_once 'application/Languages.php'; 7require_once 'application/Languages.php';
8require_once 'tests/utils/ReferenceSessionIdHashes.php';
9
10// Initialize reference data before PHPUnit starts a session
11ReferenceSessionIdHashes::genAllHashes();
12 8
13 9
14/** 10/**
@@ -16,9 +12,6 @@ ReferenceSessionIdHashes::genAllHashes();
16 */ 12 */
17class UtilsTest extends PHPUnit_Framework_TestCase 13class UtilsTest extends PHPUnit_Framework_TestCase
18{ 14{
19 // Session ID hashes
20 protected static $sidHashes = null;
21
22 // Log file 15 // Log file
23 protected static $testLogFile = 'tests.log'; 16 protected static $testLogFile = 'tests.log';
24 17
@@ -30,13 +23,11 @@ class UtilsTest extends PHPUnit_Framework_TestCase
30 */ 23 */
31 protected static $defaultTimeZone; 24 protected static $defaultTimeZone;
32 25
33
34 /** 26 /**
35 * Assign reference data 27 * Assign reference data
36 */ 28 */
37 public static function setUpBeforeClass() 29 public static function setUpBeforeClass()
38 { 30 {
39 self::$sidHashes = ReferenceSessionIdHashes::getHashes();
40 self::$defaultTimeZone = date_default_timezone_get(); 31 self::$defaultTimeZone = date_default_timezone_get();
41 // Timezone without DST for test consistency 32 // Timezone without DST for test consistency
42 date_default_timezone_set('Africa/Nairobi'); 33 date_default_timezone_set('Africa/Nairobi');
@@ -221,57 +212,8 @@ class UtilsTest extends PHPUnit_Framework_TestCase
221 $this->assertEquals('?', generateLocation($ref, 'localhost')); 212 $this->assertEquals('?', generateLocation($ref, 'localhost'));
222 } 213 }
223 214
224 /**
225 * Test is_session_id_valid with a valid ID - TEST ALL THE HASHES!
226 *
227 * This tests extensively covers all hash algorithms / bit representations
228 */
229 public function testIsAnyHashSessionIdValid()
230 {
231 foreach (self::$sidHashes as $algo => $bpcs) {
232 foreach ($bpcs as $bpc => $hash) {
233 $this->assertTrue(is_session_id_valid($hash));
234 }
235 }
236 }
237 215
238 /** 216 /**
239 * Test is_session_id_valid with a valid ID - SHA-1 hashes
240 */
241 public function testIsSha1SessionIdValid()
242 {
243 $this->assertTrue(is_session_id_valid(sha1('shaarli')));
244 }
245
246 /**
247 * Test is_session_id_valid with a valid ID - SHA-256 hashes
248 */
249 public function testIsSha256SessionIdValid()
250 {
251 $this->assertTrue(is_session_id_valid(hash('sha256', 'shaarli')));
252 }
253
254 /**
255 * Test is_session_id_valid with a valid ID - SHA-512 hashes
256 */
257 public function testIsSha512SessionIdValid()
258 {
259 $this->assertTrue(is_session_id_valid(hash('sha512', 'shaarli')));
260 }
261
262 /**
263 * Test is_session_id_valid with invalid IDs.
264 */
265 public function testIsSessionIdInvalid()
266 {
267 $this->assertFalse(is_session_id_valid(''));
268 $this->assertFalse(is_session_id_valid(array()));
269 $this->assertFalse(
270 is_session_id_valid('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
271 );
272 }
273
274 /**
275 * Test generateSecretApi. 217 * Test generateSecretApi.
276 */ 218 */
277 public function testGenerateSecretApi() 219 public function testGenerateSecretApi()
@@ -384,18 +326,18 @@ class UtilsTest extends PHPUnit_Framework_TestCase
384 */ 326 */
385 public function testHumanBytes() 327 public function testHumanBytes()
386 { 328 {
387 $this->assertEquals('2kiB', human_bytes(2 * 1024)); 329 $this->assertEquals('2'. t('kiB'), human_bytes(2 * 1024));
388 $this->assertEquals('2kiB', human_bytes(strval(2 * 1024))); 330 $this->assertEquals('2'. t('kiB'), human_bytes(strval(2 * 1024)));
389 $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2)))); 331 $this->assertEquals('2'. t('MiB'), human_bytes(2 * (pow(1024, 2))));
390 $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2))))); 332 $this->assertEquals('2'. t('MiB'), human_bytes(strval(2 * (pow(1024, 2)))));
391 $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3)))); 333 $this->assertEquals('2'. t('GiB'), human_bytes(2 * (pow(1024, 3))));
392 $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3))))); 334 $this->assertEquals('2'. t('GiB'), human_bytes(strval(2 * (pow(1024, 3)))));
393 $this->assertEquals('374B', human_bytes(374)); 335 $this->assertEquals('374'. t('B'), human_bytes(374));
394 $this->assertEquals('374B', human_bytes('374')); 336 $this->assertEquals('374'. t('B'), human_bytes('374'));
395 $this->assertEquals('232kiB', human_bytes(237481)); 337 $this->assertEquals('232'. t('kiB'), human_bytes(237481));
396 $this->assertEquals('Unlimited', human_bytes('0')); 338 $this->assertEquals(t('Unlimited'), human_bytes('0'));
397 $this->assertEquals('Unlimited', human_bytes(0)); 339 $this->assertEquals(t('Unlimited'), human_bytes(0));
398 $this->assertEquals('Setting not set', human_bytes('')); 340 $this->assertEquals(t('Setting not set'), human_bytes(''));
399 } 341 }
400 342
401 /** 343 /**
@@ -403,9 +345,9 @@ class UtilsTest extends PHPUnit_Framework_TestCase
403 */ 345 */
404 public function testGetMaxUploadSize() 346 public function testGetMaxUploadSize()
405 { 347 {
406 $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k')); 348 $this->assertEquals('1'. t('MiB'), get_max_upload_size(2097152, '1024k'));
407 $this->assertEquals('1MiB', get_max_upload_size('1m', '2m')); 349 $this->assertEquals('1'. t('MiB'), get_max_upload_size('1m', '2m'));
408 $this->assertEquals('100B', get_max_upload_size(100, 100)); 350 $this->assertEquals('100'. t('B'), get_max_upload_size(100, 100));
409 } 351 }
410 352
411 /** 353 /**
diff --git a/tests/api/controllers/GetLinksTest.php b/tests/api/controllers/GetLinksTest.php
index 4cb70224..d22ed3bf 100644
--- a/tests/api/controllers/GetLinksTest.php
+++ b/tests/api/controllers/GetLinksTest.php
@@ -367,6 +367,89 @@ class GetLinksTest extends \PHPUnit_Framework_TestCase
367 $this->assertEquals(1, count($data)); 367 $this->assertEquals(1, count($data));
368 $this->assertEquals(41, $data[0]['id']); 368 $this->assertEquals(41, $data[0]['id']);
369 $this->assertEquals(self::NB_FIELDS_LINK, count($data[0])); 369 $this->assertEquals(self::NB_FIELDS_LINK, count($data[0]));
370
371 // wildcard: placeholder at the start
372 $env = Environment::mock([
373 'REQUEST_METHOD' => 'GET',
374 'QUERY_STRING' => 'searchtags=*Tuff',
375 ]);
376 $request = Request::createFromEnvironment($env);
377 $response = $this->controller->getLinks($request, new Response());
378 $this->assertEquals(200, $response->getStatusCode());
379 $data = json_decode((string) $response->getBody(), true);
380 $this->assertEquals(2, count($data));
381 $this->assertEquals(41, $data[0]['id']);
382
383 // wildcard: placeholder at the end
384 $env = Environment::mock([
385 'REQUEST_METHOD' => 'GET',
386 'QUERY_STRING' => 'searchtags=c*',
387 ]);
388 $request = Request::createFromEnvironment($env);
389 $response = $this->controller->getLinks($request, new Response());
390 $this->assertEquals(200, $response->getStatusCode());
391 $data = json_decode((string) $response->getBody(), true);
392 $this->assertEquals(4, count($data));
393 $this->assertEquals(6, $data[0]['id']);
394
395 // wildcard: placeholder at the middle
396 $env = Environment::mock([
397 'REQUEST_METHOD' => 'GET',
398 'QUERY_STRING' => 'searchtags=w*b',
399 ]);
400 $request = Request::createFromEnvironment($env);
401 $response = $this->controller->getLinks($request, new Response());
402 $this->assertEquals(200, $response->getStatusCode());
403 $data = json_decode((string) $response->getBody(), true);
404 $this->assertEquals(4, count($data));
405 $this->assertEquals(6, $data[0]['id']);
406
407 // wildcard: match all
408 $env = Environment::mock([
409 'REQUEST_METHOD' => 'GET',
410 'QUERY_STRING' => 'searchtags=*',
411 ]);
412 $request = Request::createFromEnvironment($env);
413 $response = $this->controller->getLinks($request, new Response());
414 $this->assertEquals(200, $response->getStatusCode());
415 $data = json_decode((string) $response->getBody(), true);
416 $this->assertEquals(9, count($data));
417 $this->assertEquals(41, $data[0]['id']);
418
419 // wildcard: optional ('*' does not need to expand)
420 $env = Environment::mock([
421 'REQUEST_METHOD' => 'GET',
422 'QUERY_STRING' => 'searchtags=*stuff*',
423 ]);
424 $request = Request::createFromEnvironment($env);
425 $response = $this->controller->getLinks($request, new Response());
426 $this->assertEquals(200, $response->getStatusCode());
427 $data = json_decode((string) $response->getBody(), true);
428 $this->assertEquals(2, count($data));
429 $this->assertEquals(41, $data[0]['id']);
430
431 // wildcard: exclusions
432 $env = Environment::mock([
433 'REQUEST_METHOD' => 'GET',
434 'QUERY_STRING' => 'searchtags=*a*+-*e*',
435 ]);
436 $request = Request::createFromEnvironment($env);
437 $response = $this->controller->getLinks($request, new Response());
438 $this->assertEquals(200, $response->getStatusCode());
439 $data = json_decode((string) $response->getBody(), true);
440 $this->assertEquals(1, count($data));
441 $this->assertEquals(41, $data[0]['id']); // finds '#hashtag' in descr.
442
443 // wildcard: exclude all
444 $env = Environment::mock([
445 'REQUEST_METHOD' => 'GET',
446 'QUERY_STRING' => 'searchtags=-*',
447 ]);
448 $request = Request::createFromEnvironment($env);
449 $response = $this->controller->getLinks($request, new Response());
450 $this->assertEquals(200, $response->getStatusCode());
451 $data = json_decode((string) $response->getBody(), true);
452 $this->assertEquals(0, count($data));
370 } 453 }
371 454
372 /** 455 /**
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 00000000..d36d73cd
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,6 @@
1<?php
2
3require_once 'vendor/autoload.php';
4
5$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
6new \Shaarli\Languages('en', $conf);
diff --git a/tests/languages/bootstrap.php b/tests/languages/bootstrap.php
index 95609210..da6ac2e4 100644
--- a/tests/languages/bootstrap.php
+++ b/tests/languages/bootstrap.php
@@ -1,7 +1,6 @@
1<?php 1<?php
2if (! empty('UT_LOCALE')) { 2require_once 'tests/bootstrap.php';
3
4if (! empty(getenv('UT_LOCALE'))) {
3 setlocale(LC_ALL, getenv('UT_LOCALE')); 5 setlocale(LC_ALL, getenv('UT_LOCALE'));
4} 6}
5
6require_once 'vendor/autoload.php';
7
diff --git a/tests/languages/de/UtilsDeTest.php b/tests/languages/de/UtilsDeTest.php
index 6c9c9adc..4569c923 100644
--- a/tests/languages/de/UtilsDeTest.php
+++ b/tests/languages/de/UtilsDeTest.php
@@ -81,12 +81,12 @@ class UtilsDeTest extends UtilsTest
81 } 81 }
82 82
83 /** 83 /**
84 * Test autoLocale with multiples value, the second one is valid 84 * Test autoLocale with multiples value, the second one is available
85 */ 85 */
86 public function testAutoLocaleMultipleSecondValid() 86 public function testAutoLocaleMultipleSecondAvailable()
87 { 87 {
88 $current = setlocale(LC_ALL, 0); 88 $current = setlocale(LC_ALL, 0);
89 $header = 'pt_BR,fr-fr'; 89 $header = 'mag_IN,fr-fr';
90 autoLocale($header); 90 autoLocale($header);
91 $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0)); 91 $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0));
92 92
@@ -106,12 +106,12 @@ class UtilsDeTest extends UtilsTest
106 } 106 }
107 107
108 /** 108 /**
109 * Test autoLocale with an invalid value: defaults to en_US. 109 * Test autoLocale with an unavailable value: defaults to en_US.
110 */ 110 */
111 public function testAutoLocaleInvalid() 111 public function testAutoLocaleUnavailable()
112 { 112 {
113 $current = setlocale(LC_ALL, 0); 113 $current = setlocale(LC_ALL, 0);
114 autoLocale('pt_BR'); 114 autoLocale('mag_IN');
115 $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); 115 $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
116 116
117 setlocale(LC_ALL, $current); 117 setlocale(LC_ALL, $current);
diff --git a/tests/languages/en/UtilsEnTest.php b/tests/languages/en/UtilsEnTest.php
index d8680b2b..a74063ae 100644
--- a/tests/languages/en/UtilsEnTest.php
+++ b/tests/languages/en/UtilsEnTest.php
@@ -81,12 +81,12 @@ class UtilsEnTest extends UtilsTest
81 } 81 }
82 82
83 /** 83 /**
84 * Test autoLocale with multiples value, the second one is valid 84 * Test autoLocale with multiples value, the second one is available
85 */ 85 */
86 public function testAutoLocaleMultipleSecondValid() 86 public function testAutoLocaleMultipleSecondAvailable()
87 { 87 {
88 $current = setlocale(LC_ALL, 0); 88 $current = setlocale(LC_ALL, 0);
89 $header = 'pt_BR,fr-fr'; 89 $header = 'mag_IN,fr-fr';
90 autoLocale($header); 90 autoLocale($header);
91 $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0)); 91 $this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0));
92 92
@@ -106,12 +106,12 @@ class UtilsEnTest extends UtilsTest
106 } 106 }
107 107
108 /** 108 /**
109 * Test autoLocale with an invalid value: defaults to en_US. 109 * Test autoLocale with an unavailable value: defaults to en_US.
110 */ 110 */
111 public function testAutoLocaleInvalid() 111 public function testAutoLocaleUnavailable()
112 { 112 {
113 $current = setlocale(LC_ALL, 0); 113 $current = setlocale(LC_ALL, 0);
114 autoLocale('pt_BR'); 114 autoLocale('mag_IN');
115 $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); 115 $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
116 116
117 setlocale(LC_ALL, $current); 117 setlocale(LC_ALL, $current);
diff --git a/tests/languages/fr/LanguagesFrTest.php b/tests/languages/fr/LanguagesFrTest.php
new file mode 100644
index 00000000..79d05172
--- /dev/null
+++ b/tests/languages/fr/LanguagesFrTest.php
@@ -0,0 +1,175 @@
1<?php
2
3
4namespace Shaarli;
5
6
7use Shaarli\Config\ConfigManager;
8
9/**
10 * Class LanguagesFrTest
11 *
12 * Test the translation system in PHP and gettext mode with French language.
13 *
14 * @package Shaarli
15 */
16class LanguagesFrTest extends \PHPUnit_Framework_TestCase
17{
18 /**
19 * @var string Config file path (without extension).
20 */
21 protected static $configFile = 'tests/utils/config/configJson';
22
23 /**
24 * @var ConfigManager
25 */
26 protected $conf;
27
28 /**
29 * Init: force French
30 */
31 public function setUp()
32 {
33 $this->conf = new ConfigManager(self::$configFile);
34 $this->conf->set('translation.language', 'fr');
35 }
36
37 /**
38 * Reset the locale since gettext seems to mess with it, making it too long
39 */
40 public static function tearDownAfterClass()
41 {
42 if (! empty(getenv('UT_LOCALE'))) {
43 setlocale(LC_ALL, getenv('UT_LOCALE'));
44 }
45 }
46
47 /**
48 * Test t() with a simple non identified value.
49 */
50 public function testTranslateSingleNotIDGettext()
51 {
52 $this->conf->set('translation.mode', 'gettext');
53 new Languages('en', $this->conf);
54 $text = 'abcdé 564 fgK';
55 $this->assertEquals($text, t($text));
56 }
57
58 /**
59 * Test t() with a simple identified value in gettext mode.
60 */
61 public function testTranslateSingleIDGettext()
62 {
63 $this->conf->set('translation.mode', 'gettext');
64 new Languages('en', $this->conf);
65 $text = 'permalink';
66 $this->assertEquals('permalien', t($text));
67 }
68
69 /**
70 * Test t() with a non identified plural form in gettext mode.
71 */
72 public function testTranslatePluralNotIDGettext()
73 {
74 $this->conf->set('translation.mode', 'gettext');
75 new Languages('en', $this->conf);
76 $text = 'sandwich';
77 $nText = 'sandwiches';
78 // Not ID, so English fallback, and in english, plural 0
79 $this->assertEquals('sandwiches', t($text, $nText, 0));
80 $this->assertEquals('sandwich', t($text, $nText, 1));
81 $this->assertEquals('sandwiches', t($text, $nText, 2));
82 }
83
84 /**
85 * Test t() with an identified plural form in gettext mode.
86 */
87 public function testTranslatePluralIDGettext()
88 {
89 $this->conf->set('translation.mode', 'gettext');
90 new Languages('en', $this->conf);
91 $text = 'shaare';
92 $nText = 'shaares';
93 $this->assertEquals('shaare', t($text, $nText, 0));
94 $this->assertEquals('shaare', t($text, $nText, 1));
95 $this->assertEquals('shaares', t($text, $nText, 2));
96 }
97
98 /**
99 * Test t() with a simple non identified value.
100 */
101 public function testTranslateSingleNotIDPhp()
102 {
103 $this->conf->set('translation.mode', 'php');
104 new Languages('en', $this->conf);
105 $text = 'abcdé 564 fgK';
106 $this->assertEquals($text, t($text));
107 }
108
109 /**
110 * Test t() with a simple identified value in PHP mode.
111 */
112 public function testTranslateSingleIDPhp()
113 {
114 $this->conf->set('translation.mode', 'php');
115 new Languages('en', $this->conf);
116 $text = 'permalink';
117 $this->assertEquals('permalien', t($text));
118 }
119
120 /**
121 * Test t() with a non identified plural form in PHP mode.
122 */
123 public function testTranslatePluralNotIDPhp()
124 {
125 $this->conf->set('translation.mode', 'php');
126 new Languages('en', $this->conf);
127 $text = 'sandwich';
128 $nText = 'sandwiches';
129 // Not ID, so English fallback, and in english, plural 0
130 $this->assertEquals('sandwiches', t($text, $nText, 0));
131 $this->assertEquals('sandwich', t($text, $nText, 1));
132 $this->assertEquals('sandwiches', t($text, $nText, 2));
133 }
134
135 /**
136 * Test t() with an identified plural form in PHP mode.
137 */
138 public function testTranslatePluralIDPhp()
139 {
140 $this->conf->set('translation.mode', 'php');
141 new Languages('en', $this->conf);
142 $text = 'shaare';
143 $nText = 'shaares';
144 // In english, zero is followed by plural form
145 $this->assertEquals('shaare', t($text, $nText, 0));
146 $this->assertEquals('shaare', t($text, $nText, 1));
147 $this->assertEquals('shaares', t($text, $nText, 2));
148 }
149
150 /**
151 * Test t() with an extension language file in gettext mode
152 */
153 public function testTranslationExtensionGettext()
154 {
155 $this->conf->set('translation.mode', 'gettext');
156 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
157 new Languages('en', $this->conf);
158 $txt = 'car'; // ignore me poedit
159 $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
160 $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
161 }
162
163 /**
164 * Test t() with an extension language file in PHP mode
165 */
166 public function testTranslationExtensionPhp()
167 {
168 $this->conf->set('translation.mode', 'php');
169 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
170 new Languages('en', $this->conf);
171 $txt = 'car'; // ignore me poedit
172 $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
173 $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
174 }
175}
diff --git a/tests/languages/fr/UtilsFrTest.php b/tests/languages/fr/UtilsFrTest.php
index 0d50a878..3dbb126f 100644
--- a/tests/languages/fr/UtilsFrTest.php
+++ b/tests/languages/fr/UtilsFrTest.php
@@ -81,12 +81,12 @@ class UtilsFrTest extends UtilsTest
81 } 81 }
82 82
83 /** 83 /**
84 * Test autoLocale with multiples value, the second one is valid 84 * Test autoLocale with multiples value, the second one is available
85 */ 85 */
86 public function testAutoLocaleMultipleSecondValid() 86 public function testAutoLocaleMultipleSecondAvailable()
87 { 87 {
88 $current = setlocale(LC_ALL, 0); 88 $current = setlocale(LC_ALL, 0);
89 $header = 'pt_BR,de-de'; 89 $header = 'mag_IN,de-de';
90 autoLocale($header); 90 autoLocale($header);
91 $this->assertEquals('de_DE.utf8', setlocale(LC_ALL, 0)); 91 $this->assertEquals('de_DE.utf8', setlocale(LC_ALL, 0));
92 92
@@ -106,12 +106,12 @@ class UtilsFrTest extends UtilsTest
106 } 106 }
107 107
108 /** 108 /**
109 * Test autoLocale with an invalid value: defaults to en_US. 109 * Test autoLocale with an unavailable value: defaults to en_US.
110 */ 110 */
111 public function testAutoLocaleInvalid() 111 public function testAutoLocaleUnavailable()
112 { 112 {
113 $current = setlocale(LC_ALL, 0); 113 $current = setlocale(LC_ALL, 0);
114 autoLocale('pt_BR'); 114 autoLocale('mag_IN');
115 $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0)); 115 $this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
116 116
117 setlocale(LC_ALL, $current); 117 setlocale(LC_ALL, $current);
diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.mo b/tests/utils/languages/fr/LC_MESSAGES/test.mo
new file mode 100644
index 00000000..416c7831
--- /dev/null
+++ b/tests/utils/languages/fr/LC_MESSAGES/test.mo
Binary files 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
index 00000000..89a4fd9b
--- /dev/null
+++ b/tests/utils/languages/fr/LC_MESSAGES/test.po
@@ -0,0 +1,19 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Extension test\n"
4"POT-Creation-Date: 2017-05-20 13:54+0200\n"
5"PO-Revision-Date: 2017-05-20 14:16+0200\n"
6"Last-Translator: \n"
7"Language-Team: Shaarli\n"
8"Language: fr_FR\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"Plural-Forms: nplurals=2; plural=(n > 1);\n"
13"X-Generator: Poedit 2.0.1\n"
14
15msgid "car"
16msgstr "voiture"
17
18msgid "Search"
19msgstr "Fouille"
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index 49dd20d9..6606c4fa 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -32,7 +32,7 @@
32 </div> 32 </div>
33 </form> 33 </form>
34 34
35 <p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p> 35 <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p>
36 </div> 36 </div>
37</div> 37</div>
38{include="page.footer"} 38{include="page.footer"}
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index 76a1b9fd..cc3b299b 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -70,6 +70,30 @@
70 </div> 70 </div>
71 </div> 71 </div>
72 <div class="pure-g"> 72 <div class="pure-g">
73 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
74 <div class="form-label">
75 <label for="language">
76 <span class="label-name">{'Language'|t}</span>
77 </label>
78 </div>
79 </div>
80 <div class="pure-u-lg-{$ratioInput} pure-u-1">
81 <div class="form-input">
82 <select name="language" id="language" class="align">
83 {loop="$languages"}
84 <option value="{$key}"
85 {if="$key===$language"}
86 selected="selected"
87 {/if}
88 >
89 {$value}
90 </option>
91 {/loop}
92 </select>
93 </div>
94 </div>
95 </div>
96 <div class="pure-g">
73 <div class="pure-u-lg-{$ratioLabel} pure-u-1 "> 97 <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
74 <div class="form-label"> 98 <div class="form-label">
75 <label> 99 <label>
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css
index e1868c59..ba589723 100644
--- a/tpl/default/css/shaarli.css
+++ b/tpl/default/css/shaarli.css
@@ -539,7 +539,7 @@ body, .pure-g [class*="pure-u"] {
539} 539}
540 540
541.linklist-item-title a:visited .linklist-link { 541.linklist-item-title a:visited .linklist-link {
542 color: #555555; 542 color: #2a4c41;
543} 543}
544 544
545.linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{ 545.linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{
diff --git a/tpl/default/import.html b/tpl/default/import.html
index 1f040685..000a50ac 100644
--- a/tpl/default/import.html
+++ b/tpl/default/import.html
@@ -18,7 +18,7 @@
18 <div class="center" id="import-field"> 18 <div class="center" id="import-field">
19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
20 <input type="file" name="filetoupload"> 20 <input type="file" name="filetoupload">
21 <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p> 21 <p><br>{'Maximum size allowed:'|t} <strong>{$maxfilesizeHuman}</strong></p>
22 </div> 22 </div>
23 23
24 <div class="pure-g"> 24 <div class="pure-g">
@@ -31,15 +31,15 @@
31 <div class="radio-buttons"> 31 <div class="radio-buttons">
32 <div> 32 <div>
33 <input type="radio" name="privacy" value="default" checked="checked"> 33 <input type="radio" name="privacy" value="default" checked="checked">
34 Use values from the imported file, default to public 34 {'Use values from the imported file, default to public'|t}
35 </div> 35 </div>
36 <div> 36 <div>
37 <input type="radio" name="privacy" value="private"> 37 <input type="radio" name="privacy" value="private">
38 Import all bookmarks as private 38 {'Import all bookmarks as private'|t}
39 </div> 39 </div>
40 <div> 40 <div>
41 <input type="radio" name="privacy" value="public"> 41 <input type="radio" name="privacy" value="public">
42 Import all bookmarks as public 42 {'Import all bookmarks as public'|t}
43 </div> 43 </div>
44 </div> 44 </div>
45 </div> 45 </div>
diff --git a/tpl/default/includes.html b/tpl/default/includes.html
index 0350ef66..80c08333 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -5,16 +5,16 @@
5<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" /> 5<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
6<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" /> 6<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
7<link href="img/favicon.png" rel="shortcut icon" type="image/png" /> 7<link href="img/favicon.png" rel="shortcut icon" type="image/png" />
8<link type="text/css" rel="stylesheet" href="css/pure.min.css" /> 8<link type="text/css" rel="stylesheet" href="css/pure.min.css?v={$version_hash}" />
9<link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css"> 9<link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css?v={$version_hash}">
10<link type="text/css" rel="stylesheet" href="css/pure-extras.css"> 10<link type="text/css" rel="stylesheet" href="css/pure-extras.css?v={$version_hash}">
11<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css" /> 11<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css?v={$version_hash}" />
12<link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" /> 12<link type="text/css" rel="stylesheet" href="inc/awesomplete.css?v={$version_hash}#" />
13<link type="text/css" rel="stylesheet" href="css/shaarli.css" /> 13<link type="text/css" rel="stylesheet" href="css/shaarli.css?v={$version_hash}" />
14{if="is_file('data/user.css')"} 14{if="is_file('data/user.css')"}
15 <link type="text/css" rel="stylesheet" href="data/user.css#" /> 15 <link type="text/css" rel="stylesheet" href="data/user.css#" />
16{/if} 16{/if}
17{loop="$plugins_includes.css_files"} 17{loop="$plugins_includes.css_files"}
18 <link type="text/css" rel="stylesheet" href="{$value}#"/> 18 <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
19{/loop} 19{/loop}
20<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/> \ No newline at end of file 20<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/> \ No newline at end of file
diff --git a/tpl/default/install.html b/tpl/default/install.html
index 164d453b..6199b33d 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -68,6 +68,27 @@
68 <div class="pure-g"> 68 <div class="pure-g">
69 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 69 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
70 <div class="form-label"> 70 <div class="form-label">
71 <label for="language">
72 <span class="label-name">{'Language'|t}</span>
73 </label>
74 </div>
75 </div>
76 <div class="pure-u-lg-{$ratioInput} pure-u-1">
77 <div class="form-input">
78 <select name="language" id="language" class="align">
79 {loop="$languages"}
80 <option value="{$key}">
81 {$value}
82 </option>
83 {/loop}
84 </select>
85 </div>
86 </div>
87 </div>
88
89 <div class="pure-g">
90 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
91 <div class="form-label">
71 <label> 92 <label>
72 <span class="label-name">{'Timezone'|t}</span><br> 93 <span class="label-name">{'Timezone'|t}</span><br>
73 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span> 94 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js
index 1c66ebbd..09b07eed 100644
--- a/tpl/default/js/shaarli.js
+++ b/tpl/default/js/shaarli.js
@@ -138,6 +138,9 @@ window.onload = function () {
138 }); 138 });
139 foldAllButton.firstElementChild.classList.toggle('fa-chevron-down'); 139 foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
140 foldAllButton.firstElementChild.classList.toggle('fa-chevron-up'); 140 foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
141 foldAllButton.title = state === 'down'
142 ? document.getElementById('translation-fold-all').innerHTML
143 : document.getElementById('translation-expand-all').innerHTML
141 }); 144 });
142 }); 145 });
143 } 146 }
@@ -146,7 +149,7 @@ window.onload = function () {
146 { 149 {
147 // Switch fold/expand - up = fold 150 // Switch fold/expand - up = fold
148 if (button.classList.contains('fa-chevron-up')) { 151 if (button.classList.contains('fa-chevron-up')) {
149 button.title = 'Expand'; 152 button.title = document.getElementById('translation-expand').innerHTML;
150 if (description != null) { 153 if (description != null) {
151 description.style.display = 'none'; 154 description.style.display = 'none';
152 } 155 }
@@ -155,7 +158,7 @@ window.onload = function () {
155 } 158 }
156 } 159 }
157 else { 160 else {
158 button.title = 'Fold'; 161 button.title = document.getElementById('translation-fold').innerHTML;
159 if (description != null) { 162 if (description != null) {
160 description.style.display = 'block'; 163 description.style.display = 'block';
161 } 164 }
@@ -173,7 +176,7 @@ window.onload = function () {
173 var deleteLinks = document.querySelectorAll('.confirm-delete'); 176 var deleteLinks = document.querySelectorAll('.confirm-delete');
174 [].forEach.call(deleteLinks, function(deleteLink) { 177 [].forEach.call(deleteLinks, function(deleteLink) {
175 deleteLink.addEventListener('click', function(event) { 178 deleteLink.addEventListener('click', function(event) {
176 if(! confirm('Are you sure you want to delete this link ?')) { 179 if(! confirm(document.getElementById('translation-delete-link').innerHTML)) {
177 event.preventDefault(); 180 event.preventDefault();
178 } 181 }
179 }); 182 });
@@ -275,8 +278,14 @@ window.onload = function () {
275 }; 278 };
276 function init () { 279 function init () {
277 function resize () { 280 function resize () {
281 /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
282 var scrollTop = window.pageYOffset ||
283 (document.documentElement || document.body.parentNode || document.body).scrollTop;
284
278 description.style.height = 'auto'; 285 description.style.height = 'auto';
279 description.style.height = description.scrollHeight+10+'px'; 286 description.style.height = description.scrollHeight+10+'px';
287
288 window.scrollTo(0, scrollTop);
280 } 289 }
281 /* 0-timeout to get the already changed text */ 290 /* 0-timeout to get the already changed text */
282 function delayedResize () { 291 function delayedResize () {
@@ -612,7 +621,7 @@ function activateFirefoxSocial(node) {
612 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable. 621 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
613 var data = { 622 var data = {
614 name: title, 623 name: title,
615 description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.", 624 description: document.getElementById('translation-delete-link').innerHTML,
616 author: "Shaarli", 625 author: "Shaarli",
617 version: "1.0.0", 626 version: "1.0.0",
618 627
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index 685821e3..5dab8e9a 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -86,7 +86,7 @@
86 <div class="pure-g pure-alert pure-alert-success search-result"> 86 <div class="pure-g pure-alert pure-alert-success search-result">
87 <div class="pure-u-2-24"></div> 87 <div class="pure-u-2-24"></div>
88 <div class="pure-u-20-24"> 88 <div class="pure-u-20-24">
89 {function="t('%s result', '%s results', $result_count)"} 89 {function="sprintf(t('%s result', '%s results', $result_count), $result_count)"}
90 {if="!empty($search_term)"} 90 {if="!empty($search_term)"}
91 {'for'|t} <em><strong>{$search_term}</strong></em> 91 {'for'|t} <em><strong>{$search_term}</strong></em>
92 {/if} 92 {/if}
@@ -117,6 +117,16 @@
117 <div class="pure-g"> 117 <div class="pure-g">
118 <div class="pure-u-lg-2-24 pure-u-1-24"></div> 118 <div class="pure-u-lg-2-24 pure-u-1-24"></div>
119 <div class="pure-u-lg-20-24 pure-u-22-24"> 119 <div class="pure-u-lg-20-24 pure-u-22-24">
120 {ignore}Set translation here, for performances{/ignore}
121 {$strPrivate=t('Private')}
122 {$strEdit=t('Edit')}
123 {$strDelete=t('Delete')}
124 {$strFold=t('Fold')}
125 {$strEdited=t('Edited: ')}
126 {$strPermalink=t('Permalink')}
127 {$strPermalinkLc=t('permalink')}
128 {$strAddTag=t('Add tag')}
129 {ignore}End of translations{/ignore}
120 {loop="links"} 130 {loop="links"}
121 <div class="anchor" id="{$value.shorturl}"></div> 131 <div class="anchor" id="{$value.shorturl}"></div>
122 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> 132 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
@@ -125,12 +135,12 @@
125 {if="isLoggedIn()"} 135 {if="isLoggedIn()"}
126 <div class="linklist-item-editbuttons"> 136 <div class="linklist-item-editbuttons">
127 {if="$value.private"} 137 {if="$value.private"}
128 <span class="label label-private">{'Private'|t}</span> 138 <span class="label label-private">{$strPrivate}</span>
129 {/if} 139 {/if}
130 <input type="checkbox" class="delete-checkbox" value="{$value.id}"> 140 <input type="checkbox" class="delete-checkbox" value="{$value.id}">
131 <!-- FIXME! JS translation --> 141 <!-- FIXME! JS translation -->
132 <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a> 142 <a href="?edit_link={$value.id}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link"></i></a>
133 <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a> 143 <a href="#" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
134 </div> 144 </div>
135 {/if} 145 {/if}
136 146
@@ -164,7 +174,7 @@
164 <i class="fa fa-tags"></i> 174 <i class="fa fa-tags"></i>
165 {$tag_counter=count($value.taglist)} 175 {$tag_counter=count($value.taglist)}
166 {loop="value.taglist"} 176 {loop="value.taglist"}
167 <span class="label label-tag" title="Add tag"> 177 <span class="label label-tag" title="{$strAddTag}">
168 <a href="?addtag={$value|urlencode}">{$value}</a> 178 <a href="?addtag={$value|urlencode}">{$value}</a>
169 </span> 179 </span>
170 {if="$tag_counter - 1 != $counter"}&middot;{/if} 180 {if="$tag_counter - 1 != $counter"}&middot;{/if}
@@ -174,9 +184,9 @@
174 184
175 <div class="pure-g"> 185 <div class="pure-g">
176 <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1"> 186 <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1">
177 <a href="?{$value.shorturl}" title="{'Permalink'|t}"> 187 <a href="?{$value.shorturl}" title="{$strPermalink}">
178 {if="!$hide_timestamps || isLoggedIn()"} 188 {if="!$hide_timestamps || isLoggedIn()"}
179 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} 189 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
180 <span class="linkdate" title="{$updated}"> 190 <span class="linkdate" title="{$updated}">
181 <i class="fa fa-clock-o"></i> 191 <i class="fa fa-clock-o"></i>
182 {$value.created|format_date} 192 {$value.created|format_date}
@@ -184,7 +194,7 @@
184 &middot; 194 &middot;
185 </span> 195 </span>
186 {/if} 196 {/if}
187 {'permalink'|t} 197 {$strPermalinkLc}
188 </a> 198 </a>
189 199
190 <div class="pure-u-0 pure-u-lg-visible"> 200 <div class="pure-u-0 pure-u-lg-visible">
@@ -205,7 +215,7 @@
205 </a> 215 </a>
206 {if="isLoggedIn()"} 216 {if="isLoggedIn()"}
207 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" 217 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
208 title="{'Delete'|t}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete"> 218 title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
209 <i class="fa fa-trash"></i> 219 <i class="fa fa-trash"></i>
210 </a> 220 </a>
211 {/if} 221 {/if}
@@ -221,7 +231,7 @@
221 {if="isLoggedIn()"} 231 {if="isLoggedIn()"}
222 &middot; 232 &middot;
223 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" 233 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
224 title="{'Delete'|t}" class="delete-link confirm-delete"> 234 title="{$strDelete}" class="delete-link confirm-delete">
225 <i class="fa fa-trash"></i> 235 <i class="fa fa-trash"></i>
226 </a> 236 </a>
227 {/if} 237 {/if}
diff --git a/tpl/default/linklist.paging.html b/tpl/default/linklist.paging.html
index 41e9fa34..347b3d13 100644
--- a/tpl/default/linklist.paging.html
+++ b/tpl/default/linklist.paging.html
@@ -13,7 +13,7 @@
13 <a href="?untaggedonly" title="{'Filter untagged links'|t}" 13 <a href="?untaggedonly" title="{'Filter untagged links'|t}"
14 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if} 14 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
15 ><i class="fa fa-tag"></i></a> 15 ><i class="fa fa-tag"></i></a>
16 <a href="#" class="filter-off fold-all pure-u-lg-0" title="Fold all"> 16 <a href="#" class="filter-off fold-all pure-u-lg-0" title="{'Fold all'|t}">
17 <i class="fa fa-chevron-up"></i> 17 <i class="fa fa-chevron-up"></i>
18 </a> 18 </a>
19 {loop="$action_plugin"} 19 {loop="$action_plugin"}
@@ -53,7 +53,7 @@
53 <form method="GET" class="pure-u-0 pure-u-lg-visible"> 53 <form method="GET" class="pure-u-0 pure-u-lg-visible">
54 <input type="text" name="linksperpage" placeholder="133"> 54 <input type="text" name="linksperpage" placeholder="133">
55 </form> 55 </form>
56 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="Fold all"> 56 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="{'Fold all'|t}">
57 <i class="fa fa-chevron-up"></i> 57 <i class="fa fa-chevron-up"></i>
58 </a> 58 </a>
59 </div> 59 </div>
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index 94f771a2..659e8c7f 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -8,8 +8,8 @@
8 {$version} 8 {$version}
9 {/if} 9 {/if}
10 &middot; 10 &middot;
11 The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community &middot; 11 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot;
12 <a href="doc/html/index.html" rel="nofollow">Documentation</a> 12 <a href="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
13 {loop="$plugins_footer.text"} 13 {loop="$plugins_footer.text"}
14 {$value} 14 {$value}
15 {/loop} 15 {/loop}
@@ -27,6 +27,17 @@
27 <script src="{$value}#"></script> 27 <script src="{$value}#"></script>
28{/loop} 28{/loop}
29 29
30<script src="js/shaarli.js"></script> 30<div id="js-translations" class="hidden">
31<script src="inc/awesomplete.js#"></script> 31 <span id="translation-fold">{'Fold'|t}</span>
32<script src="inc/awesomplete-multiple-tags.js#"></script> 32 <span id="translation-fold-all">{'Fold all'|t}</span>
33 <span id="translation-expand">{'Expand'|t}</span>
34 <span id="translation-expand-all">{'Expand all'|t}</span>
35 <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span>
36 <span id="translation-shaarli-desc">
37 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t}
38 </span>
39</div>
40
41<script src="js/shaarli.js?v={$version_hash}"></script>
42<script src="inc/awesomplete.js?v={$version_hash}#"></script>
43<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script>
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index 5cc1802f..717cb517 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -116,8 +116,8 @@
116 </section> 116 </section>
117 117
118 <div class="center more"> 118 <div class="center more">
119 More plugins available 119 {"More plugins available"|t}
120 <a href="doc/Community-&-Related-software.html#third-party-plugins">in the documentation</a>. 120 <a href="doc/Community-&-Related-software.html#third-party-plugins">{"in the documentation"|t}</a>.
121 </div> 121 </div>
122 <div class="center"> 122 <div class="center">
123 <input type="submit" value="{'Save'|t}" name="save"> 123 <input type="submit" value="{'Save'|t}" name="save">
diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html
index 96b357a3..68335c70 100644
--- a/tpl/default/tag.cloud.html
+++ b/tpl/default/tag.cloud.html
@@ -26,7 +26,7 @@
26 <input type="hidden" name="do" value="tagcloud"> 26 <input type="hidden" name="do" value="tagcloud">
27 <input type="text" name="searchtags" placeholder="{'Filter by tag'|t}" 27 <input type="text" name="searchtags" placeholder="{'Filter by tag'|t}"
28 {if="!empty($search_tags)"} 28 {if="!empty($search_tags)"}
29 value="{$search_tags}" 29 value="{$search_tags}"
30 {/if} 30 {/if}
31 autocomplete="off" data-multiple data-autofirst data-minChars="1" 31 autocomplete="off" data-multiple data-autofirst data-minChars="1"
32 data-list="{loop="$tags"}{$key}, {/loop}" 32 data-list="{loop="$tags"}{$key}, {/loop}"