diff options
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 | |||
3 | root = true | ||
4 | |||
5 | [*] | ||
6 | charset = utf-8 | ||
7 | end_of_line = lf | ||
8 | insert_final_newline = true | ||
9 | trim_trailing_whitespace = true | ||
10 | indent_style = space | ||
11 | indent_size = 4 | ||
12 | |||
13 | [*.{htaccess,html,xml}] | ||
14 | indent_size = 2 | ||
15 | |||
16 | [*.php] | ||
17 | max_line_length = 100 | ||
18 | |||
19 | [Dockerfile] | ||
20 | max_line_length = 80 | ||
21 | |||
22 | [Makefile] | ||
23 | indent_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 | |||
11 | VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com> | 11 | VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com> |
12 | VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net> | 12 | VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net> |
13 | VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org> | 13 | VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org> |
14 | Willi Eggeling <thewilli@gmail.com> <mail@wje-online.de> | ||
15 | Willi Eggeling <thewilli@gmail.com> <thewilli@users.noreply.github.com> | ||
@@ -18,6 +18,7 @@ vendor/ | |||
18 | # Release archives | 18 | # Release archives |
19 | *.tar.gz | 19 | *.tar.gz |
20 | *.zip | 20 | *.zip |
21 | inc/languages/*/LC_MESSAGES/shaarli.mo | ||
21 | 22 | ||
22 | # Development and test resources | 23 | # Development and test resources |
23 | coverage | 24 | coverage |
diff --git a/.travis.yml b/.travis.yml index 26535ad3..322e4337 100644 --- a/.travis.yml +++ b/.travis.yml | |||
@@ -1,12 +1,6 @@ | |||
1 | sudo: false | 1 | sudo: false |
2 | dist: precise | 2 | dist: trusty |
3 | language: php | 3 | language: php |
4 | addons: | ||
5 | apt: | ||
6 | packages: | ||
7 | - locales | ||
8 | - language-pack-de | ||
9 | - language-pack-fr | ||
10 | cache: | 4 | cache: |
11 | directories: | 5 | directories: |
12 | - $HOME/.composer/cache | 6 | - $HOME/.composer/cache |
@@ -18,6 +12,9 @@ php: | |||
18 | install: | 12 | install: |
19 | - composer self-update | 13 | - composer self-update |
20 | - composer install --prefer-dist | 14 | - composer install --prefer-dist |
15 | - locale -a | ||
16 | before_script: | ||
17 | - PATH=${PATH//:\.\/node_modules\/\.bin/} | ||
21 | script: | 18 | script: |
22 | - make clean | 19 | - make clean |
23 | - make check_permissions | 20 | - make check_permissions |
@@ -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. | |||
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) |
5 | and this project adheres to [Semantic Versioning](http://semver.org/). | 5 | and this project adheres to [Semantic Versioning](http://semver.org/). |
6 | 6 | ||
7 | ## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07 | ||
8 | |||
9 | **Major security issue fixed. Please update.** | ||
10 | |||
11 | ### Added | ||
12 | - Tag search now supports wildcards `*` | ||
13 | - New setting `privacy.force_login` which can be used with `privacy.hide_public_links` to redirect anonymous users to the login page. | ||
14 | - New setting `general.default_note_title` used to override default `Note:` title prefix for notes. | ||
15 | - Add a version hash for asset loading to prevent browser's cache issue | ||
16 | |||
17 | ### Changed | ||
18 | - The "Remember me" checkbox is unchecked by default | ||
19 | - The default value of the "Remember me" checkbox can be configured under `data/config.json.php` | ||
20 | |||
21 | ### Removed | ||
22 | - Remove obsolete PHP magic quote support | ||
23 | |||
24 | ### Fixed | ||
25 | - Generates a permalink URL if the URL is set to blank | ||
26 | - Replace links to the old GitHub wiki with ReadTheDocs URIs | ||
27 | - Use single quotes in the note bookmarklet | ||
28 | - Daily page if there is no link | ||
29 | - Bulk link deletion with a single link | ||
30 | - HTTPS detection behind a reverse proxy | ||
31 | - Travis tests environment and localization | ||
32 | - Improve template paths robustness (trailing slash) | ||
33 | - Robustness: safer gzinflate/zlib usage | ||
34 | - Description links parsing with parenthesis (without Markdown) | ||
35 | - Templates: | ||
36 | - Sort the tag cloud alphabetically | ||
37 | - Firefox social title | ||
38 | - Improved visited link color | ||
39 | - Fix jumpy textarea with long content in post edit | ||
40 | |||
41 | ### Security | ||
42 | |||
43 | - 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 | ||
9 | The documentation has been migrated to ReadTheDocs: | 47 | The documentation has been migrated to ReadTheDocs: |
@@ -19,6 +19,16 @@ PHP_COMMA_SOURCE = index.php,application,tests,plugins | |||
19 | all: static_analysis_summary check_permissions test | 19 | all: static_analysis_summary check_permissions test |
20 | 20 | ||
21 | ## | 21 | ## |
22 | # Docker test adapter | ||
23 | # | ||
24 | # Shaarli sources and vendored libraries are copied from a shared volume | ||
25 | # to a user-owned directory to enable running tests as a non-root user. | ||
26 | ## | ||
27 | docker_%: | ||
28 | rsync -az /shaarli/ ~/shaarli/ | ||
29 | cd ~/shaarli && make $* | ||
30 | |||
31 | ## | ||
22 | # Concise status of the project | 32 | # Concise status of the project |
23 | # These targets are non-blocking: || exit 0 | 33 | # These targets are non-blocking: || exit 0 |
24 | ## | 34 | ## |
@@ -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 | ## |
123 | test: | 133 | test: 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 | ||
130 | locale_test_%: | 140 | locale_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 |
162 | release_tar: composer_dependencies htmldoc | 172 | release_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 |
169 | release_zip: composer_dependencies htmldoc | 179 | release_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) | ||
219 | translate: | ||
220 | @find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \; \ No newline at end of file | ||
@@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._ | |||
9 | [![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) | 9 | [![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) |
10 | [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) | 10 | [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) |
11 | • | 11 | • |
12 | [![](https://img.shields.io/badge/latest-v0.9.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 | • | 14 | • |
15 | [![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli) | 15 | [![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli) |
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 85dcbeeb..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 @@ | |||
13 | function purgeCachedPages($pageCacheDir) | 13 | function 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>— '. $permalink; | 156 | $link['description'] .= PHP_EOL .'<br>— '. $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 | ||
3 | namespace Shaarli; | ||
4 | |||
5 | use Gettext\GettextTranslator; | ||
6 | use Gettext\Merge; | ||
7 | use Gettext\Translations; | ||
8 | use Gettext\Translator; | ||
9 | use Gettext\TranslatorInterface; | ||
10 | use 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 | */ |
15 | function t($text, $nText = '', $nb = 0) { | 40 | class 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 | ||
255 | To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. | 255 | To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. |
256 | 256 | ||
257 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', | 257 | You 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 | ||
408 | class LinkNotFoundException extends Exception | 445 | class 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 | */ |
110 | function text2clickable($text, $redirector = '') | 110 | function text2clickable($text, $redirector = '') |
111 | { | 111 | { |
112 | $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si'; | 112 | $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si'; |
113 | 113 | ||
114 | if (empty($redirector)) { | 114 | if (empty($redirector)) { |
115 | return preg_replace($regex, '<a href="$1">$1</a>', $text); | 115 | return preg_replace($regex, '<a href="$1">$1</a>', $text); |
diff --git a/application/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 | ||
2 | namespace Shaarli; | ||
3 | |||
4 | /** | ||
5 | * Manages the server-side session | ||
6 | */ | ||
7 | class 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) | |||
530 | function write_updates_file($updatesFilepath, $updates) | 530 | function 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 | */ | ||
197 | function 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 | */ |
453 | function alphabetical_sort(&$data, $reverse = false, $byKeys = false) | 423 | function 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 | */ | ||
455 | function 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). | |||
4 | Also, please make sure your server meets the [requirements](Server-requirements) | 4 | Also, please make sure your server meets the [requirements](Server-requirements) |
5 | and is properly [configured](Server-configuration). | 5 | and is properly [configured](Server-configuration). |
6 | 6 | ||
7 | Several releases are available: | 7 | Multiple releases branches are available: |
8 | |||
9 | - latest (last release) | ||
10 | - stable (previous major release) | ||
11 | - master (development) | ||
12 | |||
13 | Using 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 | ||
31 | In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).| | 38 | In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. |
39 | Cloning 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 | ||
90 | Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser. | 100 | Once 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 | ||
94 | Setup your Shaarli installation, and it's ready to use! | 104 | Setup 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. | ||
96 | Must 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 | |||
3 | Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations | ||
4 | since `>= v0.9.2`. | ||
5 | |||
6 | Note that only the `default` theme supports translations. | ||
7 | |||
8 | ### Contributing | ||
9 | |||
10 | We encourage the community to contribute to Shaarli's translation either by improving existing | ||
11 | translations or submitting a new language. | ||
12 | |||
13 | Contributing to the translation does not require development skill. | ||
14 | |||
15 | Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`) | ||
16 | is not stored on the repository, and is generated during the release process. | ||
17 | |||
18 | ### How to | ||
19 | |||
20 | First, install [Poedit](https://poedit.net/) tool. | ||
21 | |||
22 | Poedit 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 | ||
25 | every translatable string. | ||
26 | |||
27 | You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended) | ||
28 | or visit every template page in your browser to generate cache files, while logged in. | ||
29 | |||
30 | Here is a list : | ||
31 | |||
32 | ``` | ||
33 | http://<replace_domain>/ | ||
34 | http://<replace_domain>/?nonope | ||
35 | http://<replace_domain>/?do=addlink | ||
36 | http://<replace_domain>/?do=changepasswd | ||
37 | http://<replace_domain>/?do=changetag | ||
38 | http://<replace_domain>/?do=configure | ||
39 | http://<replace_domain>/?do=tools | ||
40 | http://<replace_domain>/?do=daily | ||
41 | http://<replace_domain>/?post | ||
42 | http://<replace_domain>/?do=export | ||
43 | http://<replace_domain>/?do=import | ||
44 | http://<replace_domain>/?do=login | ||
45 | http://<replace_domain>/?do=picwall | ||
46 | http://<replace_domain>/?do=pluginadmin | ||
47 | http://<replace_domain>/?do=tagcloud | ||
48 | http://<replace_domain>/?do=taglist | ||
49 | ``` | ||
50 | |||
51 | #### Improve existing translation | ||
52 | |||
53 | In Poedit, click on "Edit a Translation", and from Shaarli's directory open | ||
54 | `inc/languages/<lang>/LC_MESSAGES/shaarli.po`. | ||
55 | |||
56 | The existing list of translatable strings should have been loaded, then click on the "Update" button. | ||
57 | |||
58 | You can start editing the translation. | ||
59 | |||
60 | ![poedit-screenshot](images/poedit-1.jpg) | ||
61 | |||
62 | Save when you're done, then you can submit a pull request containing the updated `shaarli.po`. | ||
63 | |||
64 | #### Add a new language | ||
65 | |||
66 | Open Poedit and select "Create New Translation", then from Shaarli's directory open | ||
67 | `inc/languages/<lang>/LC_MESSAGES/shaarli.po`. | ||
68 | |||
69 | Then select the language you want to create. | ||
70 | |||
71 | Click 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) | ||
73 | format in lowercase (e.g. `de` for German). | ||
74 | |||
75 | Then click on the "Update" button, and you can start to translate every available string. | ||
76 | |||
77 | Save when you're done, then you can submit a pull request containing the new `shaarli.po`. | ||
78 | |||
79 | ### Extend Shaarli's translation | ||
80 | |||
81 | If you're writing a custom theme, or a non official plugin, you might want to use the translation system, | ||
82 | but you won't be able to able to override Shaarli's translation. | ||
83 | |||
84 | However, 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 | |||
88 | First, create your translation files tree directory: | ||
89 | |||
90 | ``` | ||
91 | <your_module>/languages/<ISO 3166-1 alpha-2 language code>/LC_MESSAGES/ | ||
92 | ``` | ||
93 | |||
94 | Your `.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 | |||
97 | Users have to register your extension in their configuration with the parameter | ||
98 | `translation.extensions.<domain>: <translation files path>`. | ||
99 | |||
100 | Example: | ||
101 | |||
102 | ```php | ||
103 | if (! $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 | |||
111 | It is then recommended to create a custom translation function which will call the `t()` function with your domain. | ||
112 | For example : | ||
113 | |||
114 | ```php | ||
115 | function 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 | |||
121 | All strings which can be translated should be processed through your function: | ||
122 | |||
123 | ```php | ||
124 | my_theme_t('Comment'); | ||
125 | my_theme_t('Comment', 'Comments', 2); | ||
126 | ``` | ||
127 | |||
128 | Or 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 | |||
137 | When 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 | ``` | ||
148 | my_theme_t | ||
149 | my_theme_t:1,2 | ||
150 | ``` | ||
151 | |||
152 | Click 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 | |||
3 | Read first: | ||
4 | |||
5 | - [Docker 101](docker/docker-101.md) | ||
6 | - [Docker resources](docker/resources.md) | ||
7 | - [Unit tests](Unit-tests.md) | ||
8 | |||
9 | ### Docker test images | ||
10 | |||
11 | Test Dockerfiles are located under `docker/tests/<distribution>/Dockerfile`, | ||
12 | and can be used to build Docker images to run Shaarli test suites under common | ||
13 | Linux environments. | ||
14 | |||
15 | Dockerfiles are provided for the following environments: | ||
16 | |||
17 | - `alpine36` - [Alpine 3.6](https://www.alpinelinux.org/downloads/) | ||
18 | - `debian8` - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldstable) | ||
19 | - `debian9` - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (stable) | ||
20 | - `ubuntu16` - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (LTS) | ||
21 | |||
22 | What's behind the curtains: | ||
23 | |||
24 | - each image provides: | ||
25 | - a base Linux OS | ||
26 | - Shaarli PHP dependencies (OS packages) | ||
27 | - test PHP dependencies (OS packages) | ||
28 | - Composer | ||
29 | - the local workspace is mapped to the container's `/shaarli/` directory, | ||
30 | - the files are rsync'd to so tests are run using a standard Linux user account | ||
31 | (running tests as `root` would bypass permission checks and may hide issues) | ||
32 | - the tests are run inside the container. | ||
33 | |||
34 | ### Building test images | ||
35 | |||
36 | ```bash | ||
37 | # build the Debian 9 Docker image | ||
38 | $ cd /path/to/shaarli | ||
39 | $ cd docker/test/debian9 | ||
40 | $ docker build -t shaarli-test:debian9 . | ||
41 | ``` | ||
42 | |||
43 | ### Running tests | ||
44 | |||
45 | ```bash | ||
46 | $ cd /path/to/shaarli | ||
47 | |||
48 | # install/update 3rd-party test dependencies | ||
49 | $ composer install --prefer-dist | ||
50 | |||
51 | # run tests using the freshly built image | ||
52 | $ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test | ||
53 | |||
54 | # run the full test campaign | ||
55 | $ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests | ||
56 | ``` | ||
diff --git a/doc/md/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 | ||
17 | See [Shaarli configuration](Shaarli configuration) for more information about Shaarli resources. | 17 | See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources. |
18 | 18 | ||
19 | It is recommended to backup this repository _before_ starting updating/upgrading Shaarli: | 19 | It 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 | ||
36 | All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. | 36 | All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. |
37 | 37 | ||
38 | We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download and installation) for `git` complete instructions. | 38 | We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions. |
39 | 39 | ||
40 | Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! | 40 | Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! |
41 | 41 | ||
42 | If you use translations in gettext mode - meaning you manually changed the default mode -, | ||
43 | reload your web server. | ||
44 | |||
42 | After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details). | 45 | After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details). |
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 | ||
78 | Shaarli >= `v0.9.2` supports translations: | ||
79 | |||
80 | ```bash | ||
81 | $ make translate | ||
82 | ``` | ||
83 | |||
84 | If 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 | ||
77 | If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy. | 88 | If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy. |
@@ -151,6 +162,14 @@ Updating dependencies | |||
151 | Downloading: 100% | 162 | Downloading: 100% |
152 | ``` | 163 | ``` |
153 | 164 | ||
165 | Shaarli >= `v0.9.2` supports translations: | ||
166 | |||
167 | ```bash | ||
168 | $ make translate | ||
169 | ``` | ||
170 | |||
171 | If you use translations in gettext mode, reload your web server. | ||
172 | |||
154 | Optionally, you can delete information related to the legacy version: | 173 | Optionally, 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 | ||
176 | After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli configuration) for more details). | 195 | After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration) for more details). |
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 | |||
60 | Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe | 60 | Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe |
61 | Status: Downloaded newer image for debian:wheezy | 61 | Status: Downloaded newer image for debian:wheezy |
62 | ``` | 62 | ``` |
63 | |||
64 | Docker re-uses layers already downloaded. In other words if you have images based on Alpine or some Ubuntu version for example, those can share disk space. | ||
65 | |||
66 | ### Start a container | ||
67 | A container is an instance created from an image, that can be run and that keeps running until its main process exits. Or until the user stops the container. | ||
68 | |||
69 | The simplest way to start a container from image is ``docker run``. It also pulls the image for you if it is not locally available. For more advanced use, refer to ``docker create``. | ||
70 | |||
71 | Stopped containers are not destroyed, unless you specify ``--rm``. To view all created, running and stopped containers, enter: | ||
72 | ```bash | ||
73 | $ docker ps -a | ||
74 | ``` | ||
75 | |||
76 | Some containers may be designed or configured to be restarted, others are not. Also remember both network ports and volumes of a container are created on start, and not editable later. | ||
77 | |||
78 | ### Access a running container | ||
79 | A running container is accessible using ``docker exec``, or ``docker copy``. You can use ``exec`` to start a root shell in the Shaarli container: | ||
80 | ```bash | ||
81 | $ docker exec -ti <container-name-or-id> bash | ||
82 | ``` | ||
83 | Note the names and ID's of containers are listed in ``docker ps``. You can even type only one or two letters of the ID, given they are unique. | ||
84 | |||
85 | Access can also be through one or more network ports, or disk volumes. Both are specified on and fixed on ``docker create`` or ``run``. | ||
86 | |||
87 | You can view the console output of the main container process too: | ||
88 | ```bash | ||
89 | $ docker logs -f <container-name-or-id> | ||
90 | ``` | ||
91 | |||
92 | ### Docker disk use | ||
93 | Trying out different images can fill some gigabytes of disk quickly. Besides images, the docker volumes usually take up most disk space. | ||
94 | |||
95 | If you care only about trying out docker and not about what is running or saved, the following commands should help you out quickly if you run low on disk space: | ||
96 | |||
97 | ```bash | ||
98 | $ docker rmi -f $(docker images -aq) # remove or mark all images for disposal | ||
99 | $ docker volume rm $(docker volume ls -q) # remove all volumes | ||
100 | ``` | ||
101 | |||
102 | ### Systemd config | ||
103 | Systemd is the process manager of choice on Debian-based distributions. Once you have a ``docker`` service installed, you can use the following steps to set up Shaarli to run on system start. | ||
104 | |||
105 | ```bash | ||
106 | systemctl enable /etc/systemd/system/docker.shaarli.service | ||
107 | systemctl start docker.shaarli | ||
108 | systemctl status docker.* | ||
109 | journalctl -f # inspect system log if needed | ||
110 | ``` | ||
111 | |||
112 | You will need sudo or a root terminal to perform some or all of the steps above. Here are the contents for the service file: | ||
113 | ``` | ||
114 | [Unit] | ||
115 | Description=Shaarli Bookmark Manager Container | ||
116 | After=docker.service | ||
117 | Requires=docker.service | ||
118 | |||
119 | |||
120 | [Service] | ||
121 | Restart=always | ||
122 | |||
123 | # Put any environment you want in an included file, like $host- or $domainname in this example | ||
124 | EnvironmentFile=/etc/sysconfig/box-environment | ||
125 | |||
126 | # It's just an example.. | ||
127 | ExecStart=/usr/bin/docker run \ | ||
128 | -p 28010:80 \ | ||
129 | --name ${hostname}-shaarli \ | ||
130 | --hostname shaarli.${domainname} \ | ||
131 | -v /srv/docker-volumes-local/shaarli-data:/var/www/shaarli/data:rw \ | ||
132 | -v /etc/localtime:/etc/localtime:ro \ | ||
133 | shaarli/shaarli:latest | ||
134 | |||
135 | ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli | ||
136 | |||
137 | |||
138 | [Install] | ||
139 | WantedBy=multi-user.target | ||
140 | ``` | ||
diff --git a/doc/md/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 | |||
3 | This 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 | |||
16 | The 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 | ||
2 | TODO, 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 | ||
52 | global | ||
53 | [...] | ||
54 | |||
55 | defaults | ||
56 | [...] | ||
57 | |||
58 | frontend 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 | |||
67 | backend 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 | ||
82 | http { | ||
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 | |||
5 | repository. | 5 | repository. |
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 | ||
11 | All images rely on: | 12 | The `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 | |||
18 | The `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 | ||
23 | Login: `demo`; Password: `demo` | 23 | Login: `demo`; Password: `demo` |
24 | 24 | ||
25 | Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage): | ||
26 | ``` | ||
27 | MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P) | ||
28 | docker run -ti --rm \ | ||
29 | -p 8000:80 \ | ||
30 | -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \ | ||
31 | shaarli/shaarli | ||
32 | ``` | ||
33 | |||
34 | A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101). | ||
35 | To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](Upgrade-and-migration) documentation. | ||
25 | 36 | ||
26 | ## Features | 37 | ## Features |
27 | 38 | ||
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 @@ | |||
1 | FROM alpine:3.6 | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | RUN 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 | |||
24 | COPY nginx.conf /etc/nginx/nginx.conf | ||
25 | COPY php-fpm.conf /etc/php7/php-fpm.conf | ||
26 | COPY services.d /etc/services.d | ||
27 | |||
28 | RUN 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 | |||
34 | WORKDIR /var/www | ||
35 | RUN 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 | |||
42 | VOLUME /var/www/shaarli/data | ||
43 | |||
44 | EXPOSE 80 | ||
45 | |||
46 | ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"] | ||
47 | CMD [] | ||
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 @@ | |||
1 | FROM alpine:3.6 | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | RUN 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 | |||
24 | COPY nginx.conf /etc/nginx/nginx.conf | ||
25 | COPY php-fpm.conf /etc/php7/php-fpm.conf | ||
26 | COPY services.d /etc/services.d | ||
27 | |||
28 | RUN 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 | |||
34 | WORKDIR /var/www | ||
35 | RUN 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 | |||
42 | VOLUME /var/www/shaarli/data | ||
43 | |||
44 | EXPOSE 80 | ||
45 | |||
46 | ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"] | ||
47 | CMD [] | ||
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 @@ | |||
1 | user www-data www-data; | 1 | user nginx nginx; |
2 | daemon off; | 2 | daemon off; |
3 | worker_processes 4; | 3 | worker_processes 4; |
4 | pid /var/run/nginx.pid; | ||
4 | 5 | ||
5 | events { | 6 | events { |
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] | ||
2 | daemonize = no | ||
3 | |||
4 | [www] | ||
5 | user = nginx | ||
6 | group = nginx | ||
7 | listen.owner = nginx | ||
8 | listen.group = nginx | ||
9 | catch_workers_output = yes | ||
10 | listen = /var/run/php-fpm.sock | ||
11 | pm = dynamic | ||
12 | pm.max_children = 20 | ||
13 | pm.start_servers = 1 | ||
14 | pm.min_spare_servers = 1 | ||
15 | pm.max_spare_servers = 3 | ||
16 | pm.max_requests = 2048 | ||
diff --git a/docker/alpine/services.d/.s6-svscan/finish b/docker/alpine/services.d/.s6-svscan/finish new file mode 100755 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 | ||
2 | nginx | ||
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 | ||
2 | php-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 @@ | |||
1 | FROM debian:jessie | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | ENV TERM dumb | ||
5 | RUN 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 | |||
17 | RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini | ||
18 | RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini | ||
19 | COPY nginx.conf /etc/nginx/nginx.conf | ||
20 | COPY supervised.conf /etc/supervisor/conf.d/supervised.conf | ||
21 | |||
22 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer | ||
23 | RUN chmod 755 /usr/local/bin/composer | ||
24 | |||
25 | WORKDIR /var/www | ||
26 | RUN 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 | ||
30 | RUN rm -rf html \ | ||
31 | && chown -R www-data:www-data . | ||
32 | |||
33 | VOLUME /var/www/shaarli/data | ||
34 | |||
35 | EXPOSE 80 | ||
36 | |||
37 | CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"] | ||
diff --git a/docker/production/IMAGE.md b/docker/production/IMAGE.md deleted file mode 100644 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] | ||
2 | command=/usr/sbin/php5-fpm -F | ||
3 | priority=5 | ||
4 | autostart=true | ||
5 | autorestart=true | ||
6 | |||
7 | [program:nginx] | ||
8 | command=/usr/sbin/nginx | ||
9 | priority=10 | ||
10 | autostart=true | ||
11 | autorestart=true | ||
12 | stdout_events_enabled=true | ||
13 | stderr_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 @@ | |||
1 | FROM alpine:3.6 | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | RUN apk --update --no-cache add \ | ||
5 | ca-certificates \ | ||
6 | curl \ | ||
7 | make \ | ||
8 | php7 \ | ||
9 | php7-ctype \ | ||
10 | php7-curl \ | ||
11 | php7-dom \ | ||
12 | php7-gd \ | ||
13 | php7-iconv \ | ||
14 | php7-intl \ | ||
15 | php7-json \ | ||
16 | php7-mbstring \ | ||
17 | php7-openssl \ | ||
18 | php7-phar \ | ||
19 | php7-session \ | ||
20 | php7-simplexml \ | ||
21 | php7-tokenizer \ | ||
22 | php7-xdebug \ | ||
23 | php7-xml \ | ||
24 | php7-zlib \ | ||
25 | rsync | ||
26 | |||
27 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer | ||
28 | |||
29 | RUN mkdir /shaarli | ||
30 | WORKDIR /shaarli | ||
31 | VOLUME /shaarli | ||
32 | |||
33 | ENTRYPOINT ["make"] | ||
34 | CMD [] | ||
diff --git a/docker/test/debian8/Dockerfile b/docker/test/debian8/Dockerfile new file mode 100644 index 00000000..eaa34e9b --- /dev/null +++ b/docker/test/debian8/Dockerfile | |||
@@ -0,0 +1,35 @@ | |||
1 | FROM debian:jessie | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | ENV TERM dumb | ||
5 | ENV DEBIAN_FRONTEND noninteractive | ||
6 | ENV LANG en_US.UTF-8 | ||
7 | ENV LANGUAGE en_US:en | ||
8 | |||
9 | RUN apt-get update \ | ||
10 | && apt-get install --no-install-recommends -y \ | ||
11 | ca-certificates \ | ||
12 | curl \ | ||
13 | locales \ | ||
14 | make \ | ||
15 | php5 \ | ||
16 | php5-curl \ | ||
17 | php5-gd \ | ||
18 | php5-intl \ | ||
19 | php5-xdebug \ | ||
20 | rsync \ | ||
21 | && apt-get clean | ||
22 | |||
23 | RUN locale-gen en_US.UTF-8 \ | ||
24 | && locale-gen de_DE.UTF-8 \ | ||
25 | && locale-gen fr_FR.UTF-8 | ||
26 | |||
27 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer | ||
28 | RUN chmod 755 /usr/local/bin/composer | ||
29 | |||
30 | RUN mkdir /shaarli | ||
31 | WORKDIR /shaarli | ||
32 | VOLUME /shaarli | ||
33 | |||
34 | ENTRYPOINT ["make"] | ||
35 | CMD [] | ||
diff --git a/docker/test/debian9/Dockerfile b/docker/test/debian9/Dockerfile new file mode 100644 index 00000000..3ab4b93d --- /dev/null +++ b/docker/test/debian9/Dockerfile | |||
@@ -0,0 +1,36 @@ | |||
1 | FROM debian:stretch | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | ENV TERM dumb | ||
5 | ENV DEBIAN_FRONTEND noninteractive | ||
6 | ENV LANG en_US.UTF-8 | ||
7 | ENV LANGUAGE en_US:en | ||
8 | |||
9 | RUN apt-get update \ | ||
10 | && apt-get install --no-install-recommends -y \ | ||
11 | ca-certificates \ | ||
12 | curl \ | ||
13 | locales \ | ||
14 | make \ | ||
15 | php7.0 \ | ||
16 | php7.0-curl \ | ||
17 | php7.0-gd \ | ||
18 | php7.0-intl \ | ||
19 | php7.0-xml \ | ||
20 | php-xdebug \ | ||
21 | rsync \ | ||
22 | && apt-get clean | ||
23 | |||
24 | RUN locale-gen en_US.UTF-8 \ | ||
25 | && locale-gen de_DE.UTF-8 \ | ||
26 | && locale-gen fr_FR.UTF-8 | ||
27 | |||
28 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer | ||
29 | RUN chmod 755 /usr/local/bin/composer | ||
30 | |||
31 | RUN mkdir /shaarli | ||
32 | WORKDIR /shaarli | ||
33 | VOLUME /shaarli | ||
34 | |||
35 | ENTRYPOINT ["make"] | ||
36 | CMD [] | ||
diff --git a/docker/test/ubuntu16/Dockerfile b/docker/test/ubuntu16/Dockerfile new file mode 100644 index 00000000..e53ed9e3 --- /dev/null +++ b/docker/test/ubuntu16/Dockerfile | |||
@@ -0,0 +1,36 @@ | |||
1 | FROM ubuntu:16.04 | ||
2 | MAINTAINER Shaarli Community | ||
3 | |||
4 | ENV TERM dumb | ||
5 | ENV DEBIAN_FRONTEND noninteractive | ||
6 | ENV LANG en_US.UTF-8 | ||
7 | ENV LANGUAGE en_US:en | ||
8 | |||
9 | RUN apt-get update \ | ||
10 | && apt-get install --no-install-recommends -y \ | ||
11 | ca-certificates \ | ||
12 | curl \ | ||
13 | language-pack-de \ | ||
14 | language-pack-en \ | ||
15 | language-pack-fr \ | ||
16 | locales \ | ||
17 | make \ | ||
18 | php7.0 \ | ||
19 | php7.0-curl \ | ||
20 | php7.0-gd \ | ||
21 | php7.0-intl \ | ||
22 | php7.0-xml \ | ||
23 | php-xdebug \ | ||
24 | rsync \ | ||
25 | && apt-get clean | ||
26 | |||
27 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer | ||
28 | RUN chmod 755 /usr/local/bin/composer | ||
29 | |||
30 | RUN useradd -m dev \ | ||
31 | && mkdir /shaarli | ||
32 | USER dev | ||
33 | WORKDIR /shaarli | ||
34 | |||
35 | ENTRYPOINT ["make"] | ||
36 | CMD [] | ||
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 @@ | |||
1 | msgid "" | ||
2 | msgstr "" | ||
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 | ||
21 | msgid "" | ||
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." | ||
25 | msgstr "" | ||
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 | ||
31 | msgid "directory is not readable" | ||
32 | msgstr "le répertoire n'est pas accessible en lecture" | ||
33 | |||
34 | #: application/ApplicationUtils.php:198 | ||
35 | msgid "directory is not writable" | ||
36 | msgstr "le répertoire n'est pas accessible en écriture" | ||
37 | |||
38 | #: application/ApplicationUtils.php:216 | ||
39 | msgid "file is not readable" | ||
40 | msgstr "le fichier n'est pas accessible en lecture" | ||
41 | |||
42 | #: application/ApplicationUtils.php:219 | ||
43 | msgid "file is not writable" | ||
44 | msgstr "le fichier n'est pas accessible en écriture" | ||
45 | |||
46 | #: application/Cache.php:16 | ||
47 | #, php-format | ||
48 | msgid "Cannot purge %s: no directory" | ||
49 | msgstr "Impossible de purger %s: le répertoire n'existe pas" | ||
50 | |||
51 | #: application/FeedBuilder.php:151 | ||
52 | msgid "Direct link" | ||
53 | msgstr "Liens directs" | ||
54 | |||
55 | #: application/FeedBuilder.php:153 | ||
56 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 | ||
57 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178 | ||
58 | msgid "Permalink" | ||
59 | msgstr "Permalien" | ||
60 | |||
61 | #: application/History.php:174 | ||
62 | msgid "History file isn't readable or writable" | ||
63 | msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" | ||
64 | |||
65 | #: application/History.php:185 | ||
66 | msgid "Could not parse history file" | ||
67 | msgstr "Format incorrect pour le fichier d'historique" | ||
68 | |||
69 | #: application/Languages.php:159 | ||
70 | msgid "Automatic" | ||
71 | msgstr "Automatique" | ||
72 | |||
73 | #: application/Languages.php:160 | ||
74 | msgid "English" | ||
75 | msgstr "Anglais" | ||
76 | |||
77 | #: application/Languages.php:161 | ||
78 | msgid "French" | ||
79 | msgstr "Français" | ||
80 | |||
81 | #: application/LinkDB.php:136 | ||
82 | msgid "You are not authorized to add a link." | ||
83 | msgstr "Vous n'êtes pas autorisé à ajouter un lien." | ||
84 | |||
85 | #: application/LinkDB.php:139 | ||
86 | msgid "Internal Error: A link should always have an id and URL." | ||
87 | msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL." | ||
88 | |||
89 | #: application/LinkDB.php:142 | ||
90 | msgid "You must specify an integer as a key." | ||
91 | msgstr "Vous devez utiliser un entier comme clé." | ||
92 | |||
93 | #: application/LinkDB.php:145 | ||
94 | msgid "Array offset and link ID must be equal." | ||
95 | msgstr "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 | ||
102 | msgid "" | ||
103 | "The personal, minimalist, super-fast, database free, bookmarking service" | ||
104 | msgstr "" | ||
105 | "Le gestionnaire de marque-page personnel, minimaliste, et sans base de " | ||
106 | "données" | ||
107 | |||
108 | #: application/LinkDB.php:253 | ||
109 | msgid "" | ||
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." | ||
118 | msgstr "" | ||
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 | ||
129 | msgid "My secret stuff... - Pastebin.com" | ||
130 | msgstr "Mes trucs secrets... - Pastebin.com" | ||
131 | |||
132 | #: application/LinkDB.php:269 | ||
133 | msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." | ||
134 | msgstr "" | ||
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 | ||
139 | msgid "The link you are trying to reach does not exist or has been deleted." | ||
140 | msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." | ||
141 | |||
142 | #: application/NetscapeBookmarkUtils.php:35 | ||
143 | msgid "Invalid export selection:" | ||
144 | msgstr "Sélection d'export invalide :" | ||
145 | |||
146 | #: application/NetscapeBookmarkUtils.php:81 | ||
147 | #, php-format | ||
148 | msgid "File %s (%d bytes) " | ||
149 | msgstr "Le fichier %s (%d octets) " | ||
150 | |||
151 | #: application/NetscapeBookmarkUtils.php:83 | ||
152 | msgid "has an unknown file format. Nothing was imported." | ||
153 | msgstr "a un format inconnu. Rien n'a été importé." | ||
154 | |||
155 | #: application/NetscapeBookmarkUtils.php:86 | ||
156 | #, php-format | ||
157 | msgid "" | ||
158 | "was successfully processed in %d seconds: %d links imported, %d links " | ||
159 | "overwritten, %d links skipped." | ||
160 | msgstr "" | ||
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 | ||
165 | msgid "The page you are trying to reach does not exist or has been deleted." | ||
166 | msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée." | ||
167 | |||
168 | #: application/PageBuilder.php:167 | ||
169 | msgid "404 Not Found" | ||
170 | msgstr "404 Introuvable" | ||
171 | |||
172 | #: application/PluginManager.php:243 | ||
173 | #, php-format | ||
174 | msgid "Plugin \"%s\" files not found." | ||
175 | msgstr "Les fichiers de l'extension \"%s\" sont introuvables." | ||
176 | |||
177 | #: application/Updater.php:76 | ||
178 | msgid "Couldn't retrieve Updater class methods." | ||
179 | msgstr "Impossible de récupérer les méthodes de la classe Updater." | ||
180 | |||
181 | #: application/Updater.php:485 | ||
182 | msgid "An error occurred while running the update " | ||
183 | msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " | ||
184 | |||
185 | #: application/Updater.php:525 | ||
186 | msgid "Updates file path is not set, can't write updates." | ||
187 | msgstr "" | ||
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 | ||
192 | msgid "Unable to write updates in " | ||
193 | msgstr "Impossible d'écrire les mises à jour dans " | ||
194 | |||
195 | #: application/Utils.php:406 tests/UtilsTest.php:398 | ||
196 | msgid "Setting not set" | ||
197 | msgstr "Paramètre non défini" | ||
198 | |||
199 | #: application/Utils.php:413 tests/UtilsTest.php:396 tests/UtilsTest.php:397 | ||
200 | msgid "Unlimited" | ||
201 | msgstr "Illimité" | ||
202 | |||
203 | #: application/Utils.php:416 tests/UtilsTest.php:393 tests/UtilsTest.php:394 | ||
204 | #: tests/UtilsTest.php:408 | ||
205 | msgid "B" | ||
206 | msgstr "o" | ||
207 | |||
208 | #: application/Utils.php:416 tests/UtilsTest.php:387 tests/UtilsTest.php:388 | ||
209 | #: tests/UtilsTest.php:395 | ||
210 | msgid "kiB" | ||
211 | msgstr "ko" | ||
212 | |||
213 | #: application/Utils.php:416 tests/UtilsTest.php:389 tests/UtilsTest.php:390 | ||
214 | #: tests/UtilsTest.php:406 tests/UtilsTest.php:407 | ||
215 | msgid "MiB" | ||
216 | msgstr "Mo" | ||
217 | |||
218 | #: application/Utils.php:416 tests/UtilsTest.php:391 tests/UtilsTest.php:392 | ||
219 | msgid "GiB" | ||
220 | msgstr "Go" | ||
221 | |||
222 | #: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121 | ||
223 | msgid "" | ||
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." | ||
226 | msgstr "" | ||
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 | ||
231 | msgid "Invalid setting key parameter. String expected, got: " | ||
232 | msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " | ||
233 | |||
234 | #: application/config/exception/MissingFieldConfigException.php:21 | ||
235 | #, php-format | ||
236 | msgid "Configuration value is required for %s" | ||
237 | msgstr "Le paramètre %s est obligatoire" | ||
238 | |||
239 | #: application/config/exception/PluginConfigOrderException.php:15 | ||
240 | msgid "An error occurred while trying to save plugins loading order." | ||
241 | msgstr "" | ||
242 | "Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions." | ||
243 | |||
244 | #: application/config/exception/UnauthorizedConfigException.php:16 | ||
245 | msgid "You are not authorized to alter config." | ||
246 | msgstr "Vous n'êtes pas autorisé à modifier la configuration." | ||
247 | |||
248 | #: application/exceptions/IOException.php:19 | ||
249 | msgid "Error accessing" | ||
250 | msgstr "Une erreur s'est produite en accédant à " | ||
251 | |||
252 | #: index.php:133 | ||
253 | msgid "Shared links on " | ||
254 | msgstr "Liens partagés sur " | ||
255 | |||
256 | #: index.php:155 | ||
257 | msgid "Insufficient permissions:" | ||
258 | msgstr "Permissions insuffisantes :" | ||
259 | |||
260 | #: index.php:382 | ||
261 | msgid "I said: NO. You are banned for the moment. Go away." | ||
262 | msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard." | ||
263 | |||
264 | #: index.php:447 | ||
265 | msgid "Wrong login/password." | ||
266 | msgstr "Nom d'utilisateur ou mot de passe incorrects." | ||
267 | |||
268 | #: index.php:1107 | ||
269 | msgid "You are not supposed to change a password on an Open Shaarli." | ||
270 | msgstr "" | ||
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 | ||
274 | msgid "Wrong token." | ||
275 | msgstr "Jeton invalide." | ||
276 | |||
277 | #: index.php:1117 | ||
278 | msgid "The old password is not correct." | ||
279 | msgstr "L'ancien mot de passe est incorrect." | ||
280 | |||
281 | #: index.php:1137 | ||
282 | msgid "Your password has been changed" | ||
283 | msgstr "Votre mot de passe a été modifié" | ||
284 | |||
285 | #: index.php:1190 | ||
286 | msgid "Configuration was saved." | ||
287 | msgstr "La configuration a été sauvegardé." | ||
288 | |||
289 | #: index.php:1241 | ||
290 | #, php-format | ||
291 | msgid "The tag was removed from %d link." | ||
292 | msgid_plural "The tag was removed from %d links." | ||
293 | msgstr[0] "Le tag a été supprimé de %d lien." | ||
294 | msgstr[1] "Le tag a été supprimé de %d liens." | ||
295 | |||
296 | #: index.php:1242 | ||
297 | #, php-format | ||
298 | msgid "The tag was renamed in %d link." | ||
299 | msgid_plural "The tag was renamed in %d links." | ||
300 | msgstr[0] "Le tag a été renommé dans %d lien." | ||
301 | msgstr[1] "Le tag a été renommé dans %d liens." | ||
302 | |||
303 | #: index.php:1458 | ||
304 | msgid "Note: " | ||
305 | msgstr "Note : " | ||
306 | |||
307 | #: index.php:1567 | ||
308 | #, php-format | ||
309 | msgid "" | ||
310 | "The file you are trying to upload is probably bigger than what this " | ||
311 | "webserver can accept (%s). Please upload in smaller chunks." | ||
312 | msgstr "" | ||
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 | ||
319 | msgid "" | ||
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>" | ||
326 | msgstr "" | ||
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 | ||
337 | msgid "Click to try again." | ||
338 | msgstr "Cliquer ici pour réessayer." | ||
339 | |||
340 | #: plugins/addlink_toolbar/addlink_toolbar.php:29 | ||
341 | msgid "URI" | ||
342 | msgstr "URI" | ||
343 | |||
344 | #: plugins/addlink_toolbar/addlink_toolbar.php:33 | ||
345 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
346 | msgid "Add link" | ||
347 | msgstr "Shaare" | ||
348 | |||
349 | #: plugins/addlink_toolbar/addlink_toolbar.php:50 | ||
350 | msgid "Adds the addlink input on the linklist page." | ||
351 | msgstr "Ajout le formulaire d'ajout de liens sur la page principale." | ||
352 | |||
353 | #: plugins/archiveorg/archiveorg.php:23 | ||
354 | msgid "View on archive.org" | ||
355 | msgstr "Voir sur archive.org" | ||
356 | |||
357 | #: plugins/archiveorg/archiveorg.php:36 | ||
358 | msgid "For each link, add an Archive.org icon." | ||
359 | msgstr "Pour chaque lien, ajoute une icône pour Archive.org." | ||
360 | |||
361 | #: plugins/demo_plugin/demo_plugin.php:469 | ||
362 | msgid "" | ||
363 | "A demo plugin covering all use cases for template designers and plugin " | ||
364 | "developers." | ||
365 | msgstr "" | ||
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 | ||
370 | msgid "" | ||
371 | "Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " | ||
372 | "administration page." | ||
373 | msgstr "" | ||
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 | ||
378 | msgid "Let visitor comment your shaares on permalinks with Isso." | ||
379 | msgstr "" | ||
380 | "Permet aux visiteurs de commenter vos shaares sur les permaliens avec Isso." | ||
381 | |||
382 | #: plugins/isso/isso.php:64 | ||
383 | msgid "Isso server URL (without 'http://')" | ||
384 | msgstr "URL du serveur Isso (sans 'http://')" | ||
385 | |||
386 | #: plugins/markdown/markdown.php:159 | ||
387 | msgid "Description will be rendered with" | ||
388 | msgstr "La description sera générée avec" | ||
389 | |||
390 | #: plugins/markdown/markdown.php:160 | ||
391 | msgid "Markdown syntax documentation" | ||
392 | msgstr "Documentation sur la syntaxe Markdown" | ||
393 | |||
394 | #: plugins/markdown/markdown.php:161 | ||
395 | msgid "Markdown syntax" | ||
396 | msgstr "la syntaxe Markdown" | ||
397 | |||
398 | #: plugins/markdown/markdown.php:340 | ||
399 | msgid "" | ||
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>." | ||
407 | msgstr "" | ||
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 | ||
417 | msgid "" | ||
418 | "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " | ||
419 | "administration page." | ||
420 | msgstr "" | ||
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 | ||
425 | msgid "A plugin that adds Piwik tracking code to Shaarli pages." | ||
426 | msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli." | ||
427 | |||
428 | #: plugins/piwik/piwik.php:71 | ||
429 | msgid "Piwik URL" | ||
430 | msgstr "URL de Piwik" | ||
431 | |||
432 | #: plugins/piwik/piwik.php:72 | ||
433 | msgid "Piwik site ID" | ||
434 | msgstr "Site ID de Piwik" | ||
435 | |||
436 | #: plugins/playvideos/playvideos.php:22 | ||
437 | msgid "Video player" | ||
438 | msgstr "Lecteur vidéo" | ||
439 | |||
440 | #: plugins/playvideos/playvideos.php:25 | ||
441 | msgid "Play Videos" | ||
442 | msgstr "Jouer les vidéos" | ||
443 | |||
444 | #: plugins/playvideos/playvideos.php:56 | ||
445 | msgid "Add a button in the toolbar allowing to watch all videos." | ||
446 | msgstr "" | ||
447 | "Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos." | ||
448 | |||
449 | #: plugins/playvideos/youtube_playlist.js:214 | ||
450 | msgid "plugins/playvideos/jquery-1.11.2.min.js" | ||
451 | msgstr "" | ||
452 | |||
453 | #: plugins/pubsubhubbub/pubsubhubbub.php:69 | ||
454 | #, php-format | ||
455 | msgid "Could not publish to PubSubHubbub: %s" | ||
456 | msgstr "Impossible de publier vers PubSubHubbub : %s" | ||
457 | |||
458 | #: plugins/pubsubhubbub/pubsubhubbub.php:95 | ||
459 | #, php-format | ||
460 | msgid "Could not post to %s" | ||
461 | msgstr "Impossible de publier vers %s" | ||
462 | |||
463 | #: plugins/pubsubhubbub/pubsubhubbub.php:99 | ||
464 | #, php-format | ||
465 | msgid "Bad response from the hub %s" | ||
466 | msgstr "Mauvaise réponse du hub %s" | ||
467 | |||
468 | #: plugins/pubsubhubbub/pubsubhubbub.php:110 | ||
469 | msgid "Enable PubSubHubbub feed publishing." | ||
470 | msgstr "Active la publication de flux vers PubSubHubbub." | ||
471 | |||
472 | #: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68 | ||
473 | msgid "For each link, add a QRCode icon." | ||
474 | msgstr "Pour chaque liens, ajouter une icône de QRCode." | ||
475 | |||
476 | #: plugins/wallabag/wallabag.php:21 | ||
477 | msgid "" | ||
478 | "Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " | ||
479 | "plugin administration page." | ||
480 | msgstr "" | ||
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 | ||
485 | msgid "Save to wallabag" | ||
486 | msgstr "Sauvegarder dans Wallabag" | ||
487 | |||
488 | #: plugins/wallabag/wallabag.php:69 | ||
489 | msgid "Wallabag API URL" | ||
490 | msgstr "URL de l'API Wallabag" | ||
491 | |||
492 | #: plugins/wallabag/wallabag.php:70 | ||
493 | msgid "Wallabag API version (1 or 2)" | ||
494 | msgstr "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 | ||
501 | msgid "Search" | ||
502 | msgid_plural "Search" | ||
503 | msgstr[0] "Rechercher" | ||
504 | msgstr[1] "Rechercher" | ||
505 | |||
506 | #: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 | ||
507 | msgid "Sorry, nothing to see here." | ||
508 | msgstr "Désolé, il y a rien à voir ici." | ||
509 | |||
510 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
511 | msgid "Shaare a new link" | ||
512 | msgstr "Partager un nouveau lien" | ||
513 | |||
514 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
515 | msgid "URL or leave empty to post a note" | ||
516 | msgstr "URL ou laisser vide pour créer une note" | ||
517 | |||
518 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
519 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
520 | msgid "Change password" | ||
521 | msgstr "Modification du mot de passe" | ||
522 | |||
523 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
524 | msgid "Current password" | ||
525 | msgstr "Mot de passe actuel" | ||
526 | |||
527 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
528 | msgid "New password" | ||
529 | msgstr "Nouveau mot de passe" | ||
530 | |||
531 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
532 | msgid "Change" | ||
533 | msgstr "Changer" | ||
534 | |||
535 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
536 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
537 | msgid "Manage tags" | ||
538 | msgstr "Gérer les tags" | ||
539 | |||
540 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
541 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 | ||
542 | msgid "Tag" | ||
543 | msgstr "Tag" | ||
544 | |||
545 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
546 | msgid "New name" | ||
547 | msgstr "Nouveau nom" | ||
548 | |||
549 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 | ||
550 | msgid "Case sensitive" | ||
551 | msgstr "Sensible à la casse" | ||
552 | |||
553 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
554 | msgid "Rename" | ||
555 | msgstr "Renommer" | ||
556 | |||
557 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
558 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 | ||
559 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172 | ||
560 | msgid "Delete" | ||
561 | msgstr "Supprimer" | ||
562 | |||
563 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | ||
564 | msgid "You can also edit tags in the" | ||
565 | msgstr "Vous pouvez aussi modifier les tags dans la" | ||
566 | |||
567 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | ||
568 | msgid "tag list" | ||
569 | msgstr "liste des tags" | ||
570 | |||
571 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
572 | msgid "Configure" | ||
573 | msgstr "Configurer" | ||
574 | |||
575 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
576 | msgid "title" | ||
577 | msgstr "titre" | ||
578 | |||
579 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
580 | msgid "Home link" | ||
581 | msgstr "Lien vers l'accueil" | ||
582 | |||
583 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
584 | msgid "Default value" | ||
585 | msgstr "Valeur par défaut" | ||
586 | |||
587 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
588 | msgid "Theme" | ||
589 | msgstr "Thème" | ||
590 | |||
591 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 | ||
592 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 | ||
593 | msgid "Language" | ||
594 | msgstr "Langue" | ||
595 | |||
596 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116 | ||
597 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
598 | msgid "Timezone" | ||
599 | msgstr "Fuseau horaire" | ||
600 | |||
601 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 | ||
602 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 | ||
603 | msgid "Continent" | ||
604 | msgstr "Continent" | ||
605 | |||
606 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 | ||
607 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 | ||
608 | msgid "City" | ||
609 | msgstr "Ville" | ||
610 | |||
611 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:163 | ||
612 | msgid "Redirector" | ||
613 | msgstr "Redirecteur" | ||
614 | |||
615 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164 | ||
616 | msgid "e. g." | ||
617 | msgstr "ex :" | ||
618 | |||
619 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164 | ||
620 | msgid "will mask the HTTP_REFERER" | ||
621 | msgstr "masque le HTTP_REFERER" | ||
622 | |||
623 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 | ||
624 | msgid "Disable session cookie hijacking protection" | ||
625 | msgstr "Désactiver la protection contre le détournement de cookies" | ||
626 | |||
627 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 | ||
628 | msgid "Check this if you get disconnected or if your IP address changes often" | ||
629 | msgstr "" | ||
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 | ||
634 | msgid "Private links by default" | ||
635 | msgstr "Liens privés par défaut" | ||
636 | |||
637 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 | ||
638 | msgid "All new links are private by default" | ||
639 | msgstr "Tous les nouveaux liens sont privés par défaut" | ||
640 | |||
641 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:214 | ||
642 | msgid "RSS direct links" | ||
643 | msgstr "Liens directs dans le flux RSS" | ||
644 | |||
645 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215 | ||
646 | msgid "Check this to use direct URL instead of permalink in feeds" | ||
647 | msgstr "" | ||
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 | ||
652 | msgid "Hide public links" | ||
653 | msgstr "Cacher les liens publics" | ||
654 | |||
655 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231 | ||
656 | msgid "Do not show any links if the user is not logged in" | ||
657 | msgstr "N'afficher aucun lien sans être connecté" | ||
658 | |||
659 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:246 | ||
660 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 | ||
661 | msgid "Check updates" | ||
662 | msgstr "Vérifier les mises à jour" | ||
663 | |||
664 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247 | ||
665 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 | ||
666 | msgid "Notify me when a new release is ready" | ||
667 | msgstr "Me notifier lorsqu'une nouvelle version est disponible" | ||
668 | |||
669 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:262 | ||
670 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
671 | msgid "Enable REST API" | ||
672 | msgstr "Activer l'API REST" | ||
673 | |||
674 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263 | ||
675 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 | ||
676 | msgid "Allow third party software to use Shaarli such as mobile application" | ||
677 | msgstr "" | ||
678 | "Permets aux applications tierces d'utiliser Shaarli, par exemple les " | ||
679 | "applications mobiles" | ||
680 | |||
681 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:278 | ||
682 | msgid "API secret" | ||
683 | msgstr "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 | ||
689 | msgid "Save" | ||
690 | msgstr "Enregistrer" | ||
691 | |||
692 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
693 | msgid "The Daily Shaarli" | ||
694 | msgstr "Le Quotidien Shaarli" | ||
695 | |||
696 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 | ||
697 | msgid "1 RSS entry per day" | ||
698 | msgstr "1 entrée RSS par jour" | ||
699 | |||
700 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 | ||
701 | msgid "Previous day" | ||
702 | msgstr "Jour précédent" | ||
703 | |||
704 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
705 | msgid "All links of one day in a single page." | ||
706 | msgstr "Tous les liens d'un jour sur une page." | ||
707 | |||
708 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 | ||
709 | msgid "Next day" | ||
710 | msgstr "Jour suivant" | ||
711 | |||
712 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
713 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 | ||
714 | msgid "Edit" | ||
715 | msgstr "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 | ||
720 | msgid "Shaare" | ||
721 | msgstr "Shaare" | ||
722 | |||
723 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 | ||
724 | msgid "Created:" | ||
725 | msgstr "Création :" | ||
726 | |||
727 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
728 | msgid "URL" | ||
729 | msgstr "URL" | ||
730 | |||
731 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
732 | msgid "Title" | ||
733 | msgstr "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 | ||
740 | msgid "Description" | ||
741 | msgstr "Description" | ||
742 | |||
743 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
744 | msgid "Tags" | ||
745 | msgstr "Tags" | ||
746 | |||
747 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 | ||
748 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
749 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 | ||
750 | msgid "Private" | ||
751 | msgstr "Privé" | ||
752 | |||
753 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 | ||
754 | msgid "Apply Changes" | ||
755 | msgstr "Appliquer les changements" | ||
756 | |||
757 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
758 | msgid "Export Database" | ||
759 | msgstr "Exporter les données" | ||
760 | |||
761 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
762 | msgid "Selection" | ||
763 | msgstr "Choisir" | ||
764 | |||
765 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 | ||
766 | msgid "All" | ||
767 | msgstr "Tous" | ||
768 | |||
769 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
770 | msgid "Public" | ||
771 | msgstr "Publics" | ||
772 | |||
773 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 | ||
774 | msgid "Prepend note permalinks with this Shaarli instance's URL" | ||
775 | msgstr "Préfixer les liens de notes avec l'URL de l'instance de Shaarli" | ||
776 | |||
777 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 | ||
778 | msgid "Useful to import bookmarks in a web browser" | ||
779 | msgstr "Utile pour importer les marques-pages dans un navigateur" | ||
780 | |||
781 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 | ||
782 | msgid "Export" | ||
783 | msgstr "Exporter" | ||
784 | |||
785 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
786 | msgid "Import Database" | ||
787 | msgstr "Importer des données" | ||
788 | |||
789 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
790 | msgid "Maximum size allowed:" | ||
791 | msgstr "Taille maximum autorisée :" | ||
792 | |||
793 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
794 | msgid "Visibility" | ||
795 | msgstr "Visibilité" | ||
796 | |||
797 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
798 | msgid "Use values from the imported file, default to public" | ||
799 | msgstr "" | ||
800 | "Utiliser les valeurs présentes dans le fichier d'import, public par défaut" | ||
801 | |||
802 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
803 | msgid "Import all bookmarks as private" | ||
804 | msgstr "Importer tous les liens comme privés" | ||
805 | |||
806 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
807 | msgid "Import all bookmarks as public" | ||
808 | msgstr "Importer tous les liens comme publics" | ||
809 | |||
810 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 | ||
811 | msgid "Overwrite existing bookmarks" | ||
812 | msgstr "Remplacer les liens existants" | ||
813 | |||
814 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
815 | msgid "Duplicates based on URL" | ||
816 | msgstr "Les doublons s'appuient sur les URL" | ||
817 | |||
818 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 | ||
819 | msgid "Add default tags" | ||
820 | msgstr "Ajouter des tags par défaut" | ||
821 | |||
822 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 | ||
823 | msgid "Import" | ||
824 | msgstr "Importer" | ||
825 | |||
826 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | ||
827 | msgid "Install Shaarli" | ||
828 | msgstr "Installation de Shaarli" | ||
829 | |||
830 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 | ||
831 | msgid "It looks like it's the first time you run Shaarli. Please configure it." | ||
832 | msgstr "" | ||
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 | ||
840 | msgid "Username" | ||
841 | msgstr "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 | ||
847 | msgid "Password" | ||
848 | msgstr "Mot de passe" | ||
849 | |||
850 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 | ||
851 | msgid "Shaarli title" | ||
852 | msgstr "Titre du Shaarli" | ||
853 | |||
854 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 | ||
855 | msgid "My links" | ||
856 | msgstr "Mes liens" | ||
857 | |||
858 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 | ||
859 | msgid "Install" | ||
860 | msgstr "Installer" | ||
861 | |||
862 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
863 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 | ||
864 | msgid "shaare" | ||
865 | msgid_plural "shaares" | ||
866 | msgstr[0] "shaare" | ||
867 | msgstr[1] "shaares" | ||
868 | |||
869 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | ||
870 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 | ||
871 | msgid "private link" | ||
872 | msgid_plural "private links" | ||
873 | msgstr[0] "lien privé" | ||
874 | msgstr[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 | ||
879 | msgid "Search text" | ||
880 | msgstr "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 | ||
889 | msgid "Filter by tag" | ||
890 | msgstr "Filtrer par tag" | ||
891 | |||
892 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 | ||
893 | msgid "Nothing found." | ||
894 | msgstr "Aucun résultat." | ||
895 | |||
896 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119 | ||
897 | #, php-format | ||
898 | msgid "%s result" | ||
899 | msgid_plural "%s results" | ||
900 | msgstr[0] "%s résultat" | ||
901 | msgstr[1] "%s résultats" | ||
902 | |||
903 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 | ||
904 | msgid "for" | ||
905 | msgstr "pour" | ||
906 | |||
907 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 | ||
908 | msgid "tagged" | ||
909 | msgstr "taggé" | ||
910 | |||
911 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 | ||
912 | msgid "Remove tag" | ||
913 | msgstr "Retirer le tag" | ||
914 | |||
915 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 | ||
916 | msgid "with status" | ||
917 | msgstr "avec le statut" | ||
918 | |||
919 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 | ||
920 | msgid "without any tag" | ||
921 | msgstr "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 | ||
926 | msgid "Fold" | ||
927 | msgstr "Replier" | ||
928 | |||
929 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 | ||
930 | msgid "Edited: " | ||
931 | msgstr "Modifié : " | ||
932 | |||
933 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180 | ||
934 | msgid "permalink" | ||
935 | msgstr "permalien" | ||
936 | |||
937 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 | ||
938 | msgid "Add tag" | ||
939 | msgstr "Ajouter un tag" | ||
940 | |||
941 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 | ||
942 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7 | ||
943 | msgid "Filters" | ||
944 | msgstr "Filtres" | ||
945 | |||
946 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 | ||
947 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12 | ||
948 | msgid "Filter private links" | ||
949 | msgstr "Filtrer par liens privés" | ||
950 | |||
951 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | ||
952 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 | ||
953 | msgid "Filter untagged links" | ||
954 | msgstr "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 | ||
962 | msgid "Fold all" | ||
963 | msgstr "Replier tout" | ||
964 | |||
965 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 | ||
966 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:67 | ||
967 | msgid "Links per page" | ||
968 | msgstr "Liens par page" | ||
969 | |||
970 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
971 | msgid "" | ||
972 | "You have been banned after too many failed login attempts. Try again later." | ||
973 | msgstr "" | ||
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 | ||
983 | msgid "Login" | ||
984 | msgstr "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 | ||
989 | msgid "Remember me" | ||
990 | msgstr "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 | ||
996 | msgid "by the Shaarli community" | ||
997 | msgstr "par la communauté Shaarli" | ||
998 | |||
999 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
1000 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 | ||
1001 | msgid "Documentation" | ||
1002 | msgstr "Documentation" | ||
1003 | |||
1004 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
1005 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 | ||
1006 | msgid "Expand" | ||
1007 | msgstr "Déplier" | ||
1008 | |||
1009 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 | ||
1010 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 | ||
1011 | msgid "Expand all" | ||
1012 | msgstr "Déplier tout" | ||
1013 | |||
1014 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
1015 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 | ||
1016 | msgid "Are you sure you want to delete this link?" | ||
1017 | msgstr "Ê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 | ||
1021 | msgid "Tools" | ||
1022 | msgstr "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 | ||
1027 | msgid "Tag cloud" | ||
1028 | msgstr "Nuage de tags" | ||
1029 | |||
1030 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | ||
1031 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39 | ||
1032 | msgid "Picture wall" | ||
1033 | msgstr "Mur d'images" | ||
1034 | |||
1035 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
1036 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42 | ||
1037 | msgid "Daily" | ||
1038 | msgstr "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 | ||
1044 | msgid "RSS Feed" | ||
1045 | msgstr "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 | ||
1051 | msgid "Logout" | ||
1052 | msgstr "Déconnexion" | ||
1053 | |||
1054 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
1055 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169 | ||
1056 | msgid "is available" | ||
1057 | msgstr "est disponible" | ||
1058 | |||
1059 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 | ||
1060 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176 | ||
1061 | msgid "Error" | ||
1062 | msgstr "Erreur" | ||
1063 | |||
1064 | #: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1065 | msgid "Picture Wall" | ||
1066 | msgstr "Mur d'images" | ||
1067 | |||
1068 | #: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1069 | msgid "pics" | ||
1070 | msgstr "images" | ||
1071 | |||
1072 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
1073 | msgid "You need to enable Javascript to change plugin loading order." | ||
1074 | msgstr "" | ||
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 | ||
1079 | msgid "Plugin administration" | ||
1080 | msgstr "Administration des extensions" | ||
1081 | |||
1082 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
1083 | msgid "Enabled Plugins" | ||
1084 | msgstr "Extensions activées" | ||
1085 | |||
1086 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
1087 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 | ||
1088 | msgid "No plugin enabled." | ||
1089 | msgstr "Aucune extension activée." | ||
1090 | |||
1091 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
1092 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 | ||
1093 | msgid "Disable" | ||
1094 | msgstr "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 | ||
1100 | msgid "Name" | ||
1101 | msgstr "Nom" | ||
1102 | |||
1103 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
1104 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 | ||
1105 | msgid "Order" | ||
1106 | msgstr "Ordre" | ||
1107 | |||
1108 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 | ||
1109 | msgid "Disabled Plugins" | ||
1110 | msgstr "Extensions désactivées" | ||
1111 | |||
1112 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 | ||
1113 | msgid "No plugin disabled." | ||
1114 | msgstr "Aucune extension désactivée." | ||
1115 | |||
1116 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97 | ||
1117 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 | ||
1118 | msgid "Enable" | ||
1119 | msgstr "Activer" | ||
1120 | |||
1121 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 | ||
1122 | msgid "More plugins available" | ||
1123 | msgstr "Plus d'extensions disponibles" | ||
1124 | |||
1125 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 | ||
1126 | msgid "in the documentation" | ||
1127 | msgstr "dans la documentation" | ||
1128 | |||
1129 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 | ||
1130 | msgid "Plugin configuration" | ||
1131 | msgstr "Configuration des extensions" | ||
1132 | |||
1133 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1134 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1135 | msgid "tags" | ||
1136 | msgstr "tags" | ||
1137 | |||
1138 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
1139 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
1140 | msgid "List all links with those tags" | ||
1141 | msgstr "Lister tous les liens avec ces tags" | ||
1142 | |||
1143 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1144 | msgid "Tag list" | ||
1145 | msgstr "List des tags" | ||
1146 | |||
1147 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 | ||
1148 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 | ||
1149 | msgid "Sort by:" | ||
1150 | msgstr "Trier par :" | ||
1151 | |||
1152 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 | ||
1153 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5 | ||
1154 | msgid "Cloud" | ||
1155 | msgstr "Nuage" | ||
1156 | |||
1157 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6 | ||
1158 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6 | ||
1159 | msgid "Most used" | ||
1160 | msgstr "Plus utilisés" | ||
1161 | |||
1162 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 | ||
1163 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7 | ||
1164 | msgid "Alphabetical" | ||
1165 | msgstr "Alphabétique" | ||
1166 | |||
1167 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
1168 | msgid "Settings" | ||
1169 | msgstr "Paramètres" | ||
1170 | |||
1171 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1172 | msgid "Change Shaarli settings: title, timezone, etc." | ||
1173 | msgstr "Changer les paramètres de Shaarli : titre, fuseau horaire, etc." | ||
1174 | |||
1175 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 | ||
1176 | msgid "Configure your Shaarli" | ||
1177 | msgstr "Conguration de Shaarli" | ||
1178 | |||
1179 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 | ||
1180 | msgid "Enable, disable and configure plugins" | ||
1181 | msgstr "Activer, désactiver et configurer les extensions" | ||
1182 | |||
1183 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
1184 | msgid "Change your password" | ||
1185 | msgstr "Modification du mot de passe" | ||
1186 | |||
1187 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
1188 | msgid "Rename or delete a tag in all links" | ||
1189 | msgstr "Rename or delete a tag in all links" | ||
1190 | |||
1191 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
1192 | msgid "" | ||
1193 | "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " | ||
1194 | "delicious...)" | ||
1195 | msgstr "" | ||
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 | ||
1200 | msgid "Import links" | ||
1201 | msgstr "Importer des liens" | ||
1202 | |||
1203 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | ||
1204 | msgid "" | ||
1205 | "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " | ||
1206 | "Opera, delicious...)" | ||
1207 | msgstr "" | ||
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 | ||
1212 | msgid "Export database" | ||
1213 | msgstr "Exporter les données" | ||
1214 | |||
1215 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 | ||
1216 | msgid "" | ||
1217 | "Drag one of these button to your bookmarks toolbar or right-click it and " | ||
1218 | "\"Bookmark This Link\"" | ||
1219 | msgstr "" | ||
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 | ||
1224 | msgid "then click on the bookmarklet in any page you want to share." | ||
1225 | msgstr "" | ||
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 | ||
1230 | msgid "" | ||
1231 | "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " | ||
1232 | "Link" | ||
1233 | msgstr "" | ||
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 | ||
1238 | msgid "then click ✚Shaare link button in any page you want to share" | ||
1239 | msgstr "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 | ||
1243 | msgid "The selected text is too long, it will be truncated." | ||
1244 | msgstr "Le texte sélectionné est trop long, il sera tronqué." | ||
1245 | |||
1246 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 | ||
1247 | msgid "Shaare link" | ||
1248 | msgstr "Shaare" | ||
1249 | |||
1250 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 | ||
1251 | msgid "" | ||
1252 | "Then click ✚Add Note button anytime to start composing a private Note (text " | ||
1253 | "post) to your Shaarli" | ||
1254 | msgstr "" | ||
1255 | "Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" | ||
1256 | |||
1257 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 | ||
1258 | msgid "Add Note" | ||
1259 | msgstr "Ajouter une Note" | ||
1260 | |||
1261 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 | ||
1262 | msgid "" | ||
1263 | "You need to browse your Shaarli over <strong>HTTPS</strong> to use this " | ||
1264 | "functionality." | ||
1265 | msgstr "" | ||
1266 | "Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette " | ||
1267 | "fonctionalité." | ||
1268 | |||
1269 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 | ||
1270 | msgid "Add to" | ||
1271 | msgstr "Ajouter à " | ||
1272 | |||
1273 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145 | ||
1274 | msgid "3rd party" | ||
1275 | msgstr "Applications tierces" | ||
1276 | |||
1277 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 | ||
1278 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 | ||
1279 | msgid "Plugin" | ||
1280 | msgstr "Extension" | ||
1281 | |||
1282 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 | ||
1283 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 | ||
1284 | msgid "plugin" | ||
1285 | msgstr "extension" | ||
1286 | |||
1287 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 | ||
1288 | msgid "" | ||
1289 | "Drag this link to your bookmarks toolbar, or right-click it and choose " | ||
1290 | "Bookmark This Link" | ||
1291 | msgstr "" | ||
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" | ||
@@ -64,7 +64,6 @@ require_once 'application/FeedBuilder.php'; | |||
64 | require_once 'application/FileUtils.php'; | 64 | require_once 'application/FileUtils.php'; |
65 | require_once 'application/History.php'; | 65 | require_once 'application/History.php'; |
66 | require_once 'application/HttpUtils.php'; | 66 | require_once 'application/HttpUtils.php'; |
67 | require_once 'application/Languages.php'; | ||
68 | require_once 'application/LinkDB.php'; | 67 | require_once 'application/LinkDB.php'; |
69 | require_once 'application/LinkFilter.php'; | 68 | require_once 'application/LinkFilter.php'; |
70 | require_once 'application/LinkUtils.php'; | 69 | require_once 'application/LinkUtils.php'; |
@@ -76,8 +75,10 @@ require_once 'application/Utils.php'; | |||
76 | require_once 'application/PluginManager.php'; | 75 | require_once 'application/PluginManager.php'; |
77 | require_once 'application/Router.php'; | 76 | require_once 'application/Router.php'; |
78 | require_once 'application/Updater.php'; | 77 | require_once 'application/Updater.php'; |
78 | use \Shaarli\Languages; | ||
79 | use \Shaarli\ThemeUtils; | 79 | use \Shaarli\ThemeUtils; |
80 | use \Shaarli\Config\ConfigManager; | 80 | use \Shaarli\Config\ConfigManager; |
81 | use \Shaarli\SessionManager; | ||
81 | 82 | ||
82 | // Ensure the PHP version is supported | 83 | // Ensure the PHP version is supported |
83 | try { | 84 | try { |
@@ -88,7 +89,7 @@ try { | |||
88 | exit; | 89 | exit; |
89 | } | 90 | } |
90 | 91 | ||
91 | define('shaarli_version', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); | 92 | define('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. |
118 | if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) { | 119 | if (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. | ||
128 | if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { | ||
129 | autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']); | ||
130 | } | ||
131 | |||
132 | new 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))); |
126 | RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory | 136 | RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory |
127 | RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory | 137 | RainTPL::$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 |
164 | define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); | 174 | define('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. | ||
167 | if (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. |
377 | if (isset($_POST['login'])) | 382 | if (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'])) | |||
451 | if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. | 457 | if (!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 | */ | ||
460 | function 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. | ||
469 | function 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 | */ |
691 | function renderPage($conf, $pluginManager, $LINKSDB, $history) | 672 | function 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 | */ |
1938 | function install($conf) | 1948 | function 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); | |||
2286 | if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { | 2305 | if ($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 | } |
@@ -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 @@ | |||
1 | https://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 | |||
10 | Problem: by default, Isso thread ID is guessed from the current url (only one thread per page). | ||
11 | if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config, | ||
12 | with 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 | |||
17 | Problem: feature is currently broken https://github.com/posativ/isso/issues/27 | ||
18 | |||
19 | Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread | ||
20 | when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment | ||
21 | count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done | ||
22 | by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server. | ||
23 | |||
24 | Showing all full isso threads in the linklist would destroy layout | ||
25 | |||
26 | ----------------------------------------------------------- | ||
27 | |||
28 | http://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 | */ | ||
47 | function 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 | */ | ||
33 | function 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 | ||
17 | use 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 | */ | ||
27 | const 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 | */ | ||
32 | function 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) | |||
160 | function hook_demo_plugin_render_footer($data) | 186 | function 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 | */ | ||
466 | function 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 @@ | |||
1 | msgid "" | ||
2 | msgstr "" | ||
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 | ||
20 | msgid "Shaarli is now enhanced by the awesome demo_plugin." | ||
21 | msgstr "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 | ||
7 | use 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 | */ | ||
60 | function 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) | |||
154 | function hook_markdown_render_editlink($data) | 154 | function 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 | */ | ||
337 | function markdown_dummy_translation() | ||
338 | { | ||
339 | // meta | ||
340 | t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>: | ||
341 | If your shaared descriptions contained HTML tags before enabling the markdown plugin, | ||
342 | enabling it might break your page. | ||
343 | See 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 | */ | ||
67 | function 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 | */ | ||
53 | function 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 | ||
12 | use pubsubhubbub\publisher\Publisher; | 12 | use pubsubhubbub\publisher\Publisher; |
13 | use 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 | */ | ||
107 | function 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 | */ | ||
66 | function 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 | ||
7 | require_once 'WallabagInstance.php'; | 7 | require_once 'WallabagInstance.php'; |
8 | use 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 | */ | ||
65 | function 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 | ||
3 | require_once 'application/Languages.php'; | 3 | namespace Shaarli; |
4 | |||
5 | use Shaarli\Config\ConfigManager; | ||
4 | 6 | ||
5 | /** | 7 | /** |
6 | * Class LanguagesTest. | 8 | * Class LanguagesTest. |
7 | */ | 9 | */ |
8 | class LanguagesTest extends PHPUnit_Framework_TestCase | 10 | class 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 | ||
3 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | ||
4 | ReferenceSessionIdHashes::genAllHashes(); | ||
5 | |||
6 | use \Shaarli\SessionManager; | ||
7 | use \PHPUnit\Framework\TestCase; | ||
8 | |||
9 | |||
10 | /** | ||
11 | * Fake ConfigManager | ||
12 | */ | ||
13 | class FakeConfigManager | ||
14 | { | ||
15 | public static function get($key) | ||
16 | { | ||
17 | return $key; | ||
18 | } | ||
19 | } | ||
20 | |||
21 | |||
22 | /** | ||
23 | * Test coverage for SessionManager | ||
24 | */ | ||
25 | class 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 | ||
6 | require_once 'application/Utils.php'; | 6 | require_once 'application/Utils.php'; |
7 | require_once 'application/Languages.php'; | 7 | require_once 'application/Languages.php'; |
8 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | ||
9 | |||
10 | // Initialize reference data before PHPUnit starts a session | ||
11 | ReferenceSessionIdHashes::genAllHashes(); | ||
12 | 8 | ||
13 | 9 | ||
14 | /** | 10 | /** |
@@ -16,9 +12,6 @@ ReferenceSessionIdHashes::genAllHashes(); | |||
16 | */ | 12 | */ |
17 | class UtilsTest extends PHPUnit_Framework_TestCase | 13 | class 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 | |||
3 | require_once 'vendor/autoload.php'; | ||
4 | |||
5 | $conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson'); | ||
6 | new \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 |
2 | if (! empty('UT_LOCALE')) { | 2 | require_once 'tests/bootstrap.php'; |
3 | |||
4 | if (! empty(getenv('UT_LOCALE'))) { | ||
3 | setlocale(LC_ALL, getenv('UT_LOCALE')); | 5 | setlocale(LC_ALL, getenv('UT_LOCALE')); |
4 | } | 6 | } |
5 | |||
6 | require_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 | |||
4 | namespace Shaarli; | ||
5 | |||
6 | |||
7 | use 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 | */ | ||
16 | class 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 @@ | |||
1 | msgid "" | ||
2 | msgstr "" | ||
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 | |||
15 | msgid "car" | ||
16 | msgstr "voiture" | ||
17 | |||
18 | msgid "Search" | ||
19 | msgstr "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} · {'City'|t}</span> | 94 | <span class="label-desc">{'Continent'|t} · {'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"}·{/if} | 180 | {if="$tag_counter - 1 != $counter"}·{/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 | · | 194 | · |
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&lf_linkdate={$value.id}&token={$token}" | 217 | <a href="?delete_link&lf_linkdate={$value.id}&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 | · | 232 | · |
223 | <a href="?delete_link&lf_linkdate={$value.id}&token={$token}" | 233 | <a href="?delete_link&lf_linkdate={$value.id}&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 | · | 10 | · |
11 | The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community · | 11 | {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} · |
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}" |