aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.editorconfig23
-rw-r--r--.gitattributes2
-rw-r--r--.github/mailmap2
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml2
-rw-r--r--AUTHORS13
-rw-r--r--CHANGELOG.md64
-rw-r--r--Makefile30
-rw-r--r--README.md4
-rw-r--r--application/ApplicationUtils.php19
-rw-r--r--application/Cache.php2
-rw-r--r--application/FeedBuilder.php6
-rw-r--r--application/History.php20
-rw-r--r--application/HttpUtils.php23
-rw-r--r--application/Languages.php167
-rw-r--r--application/LinkDB.php37
-rw-r--r--application/LinkFilter.php8
-rw-r--r--application/LinkUtils.php104
-rw-r--r--application/NetscapeBookmarkUtils.php27
-rw-r--r--application/PageBuilder.php13
-rw-r--r--application/PluginManager.php7
-rw-r--r--application/SessionManager.php83
-rw-r--r--application/Updater.php17
-rw-r--r--application/Utils.php47
-rw-r--r--application/config/ConfigJson.php15
-rw-r--r--application/config/ConfigManager.php6
-rw-r--r--application/config/ConfigPhp.php4
-rw-r--r--application/config/exception/MissingFieldConfigException.php2
-rw-r--r--application/config/exception/PluginConfigOrderException.php2
-rw-r--r--application/config/exception/UnauthorizedConfigException.php2
-rw-r--r--application/exceptions/IOException.php2
-rw-r--r--composer.json3
-rw-r--r--composer.lock249
-rw-r--r--data/.htaccess12
-rw-r--r--doc/md/Backup,-restore,-import-and-export.md4
-rw-r--r--doc/md/Bookmarklet.md2
-rw-r--r--doc/md/Browsing-and-searching.md20
-rw-r--r--doc/md/Community-&-Related-software.md49
-rw-r--r--doc/md/Download-and-Installation.md54
-rw-r--r--doc/md/Features.md25
-rw-r--r--doc/md/Firefox-share.md3
-rw-r--r--doc/md/Server-requirements.md3
-rw-r--r--doc/md/Shaarli-configuration.md21
-rw-r--r--doc/md/Translations.md152
-rw-r--r--doc/md/Unit-tests.md10
-rw-r--r--doc/md/Upgrade-and-migration.md27
-rw-r--r--doc/md/docker/reverse-proxy-configuration.md116
-rw-r--r--doc/md/docker/shaarli-images.md28
-rw-r--r--doc/md/images/install-shaarli.pngbin0 -> 44376 bytes
-rw-r--r--doc/md/images/poedit-1.jpgbin0 -> 72956 bytes
-rw-r--r--doc/md/index.md43
-rw-r--r--docker/alpine/Dockerfile.armhf.latest47
-rw-r--r--docker/alpine/Dockerfile.armhf.master47
-rw-r--r--docker/alpine/Dockerfile.latest47
-rw-r--r--docker/alpine/Dockerfile.master47
-rw-r--r--docker/alpine/IMAGE.md10
-rw-r--r--docker/alpine/nginx.conf (renamed from docker/production/stable/nginx.conf)5
-rw-r--r--docker/alpine/php-fpm.conf16
-rwxr-xr-xdocker/alpine/services.d/.s6-svscan/finish2
-rwxr-xr-xdocker/alpine/services.d/nginx/run2
-rwxr-xr-xdocker/alpine/services.d/php-fpm/run2
-rw-r--r--docker/debian/Dockerfile.stable (renamed from docker/production/stable/Dockerfile)0
-rw-r--r--docker/debian/IMAGE.md (renamed from docker/production/stable/IMAGE.md)0
-rw-r--r--docker/debian/nginx.conf (renamed from docker/production/nginx.conf)0
-rw-r--r--docker/debian/supervised.conf (renamed from docker/production/stable/supervised.conf)0
-rw-r--r--docker/production/Dockerfile37
-rw-r--r--docker/production/IMAGE.md5
-rw-r--r--docker/production/supervised.conf13
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po1367
-rw-r--r--index.php192
-rw-r--r--mkdocs.yml9
-rw-r--r--plugins/TODO.md28
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.php13
-rw-r--r--plugins/archiveorg/archiveorg.html6
-rw-r--r--plugins/archiveorg/archiveorg.php11
-rw-r--r--plugins/demo_plugin/demo_plugin.php37
-rw-r--r--plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mobin0 -> 652 bytes
-rw-r--r--plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po21
-rw-r--r--plugins/isso/isso.php17
-rw-r--r--plugins/markdown/help.html6
-rw-r--r--plugins/markdown/markdown.php21
-rw-r--r--plugins/piwik/piwik.php15
-rw-r--r--plugins/playvideos/playvideos.php13
-rw-r--r--plugins/pubsubhubbub/pubsubhubbub.php16
-rw-r--r--plugins/qrcode/qrcode.meta2
-rw-r--r--plugins/qrcode/qrcode.php9
-rw-r--r--plugins/wallabag/wallabag.html6
-rw-r--r--plugins/wallabag/wallabag.php20
-rw-r--r--shaarli_version.php2
-rw-r--r--tests/HttpUtils/ServerUrlTest.php32
-rw-r--r--tests/LanguagesTest.php186
-rw-r--r--tests/LinkFilterTest.php13
-rw-r--r--tests/LinkUtilsTest.php259
-rw-r--r--tests/NetscapeBookmarkUtils/BookmarkImportTest.php81
-rw-r--r--tests/SessionManagerTest.php149
-rw-r--r--tests/UtilsTest.php88
-rw-r--r--tests/bootstrap.php6
-rw-r--r--tests/languages/bootstrap.php7
-rw-r--r--tests/languages/fr/LanguagesFrTest.php175
-rw-r--r--tests/utils/FakeConfigManager.php12
-rw-r--r--tests/utils/ReferenceLinkDB.php23
-rw-r--r--tests/utils/languages/fr/LC_MESSAGES/test.mobin0 -> 456 bytes
-rw-r--r--tests/utils/languages/fr/LC_MESSAGES/test.po19
-rw-r--r--tpl/default/changetag.html2
-rw-r--r--tpl/default/configure.html39
-rw-r--r--tpl/default/css/shaarli.css120
-rw-r--r--tpl/default/img/apple-touch-icon.pngbin0 -> 18276 bytes
-rw-r--r--tpl/default/import.html8
-rw-r--r--tpl/default/includes.html3
-rw-r--r--tpl/default/install.html21
-rw-r--r--tpl/default/js/shaarli.js13
-rw-r--r--tpl/default/linklist.html93
-rw-r--r--tpl/default/linklist.paging.html4
-rw-r--r--tpl/default/page.footer.html15
-rw-r--r--tpl/default/page.header.html52
-rw-r--r--tpl/default/pluginsadmin.html22
-rw-r--r--tpl/default/tag.cloud.html6
-rw-r--r--tpl/default/tag.list.html4
118 files changed, 4266 insertions, 852 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..4a6589a2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,23 @@
1# EditorConfig: http://EditorConfig.org
2
3root = true
4
5[*]
6charset = utf-8
7end_of_line = lf
8insert_final_newline = true
9trim_trailing_whitespace = true
10indent_style = space
11indent_size = 4
12
13[*.{htaccess,html,xml}]
14indent_size = 2
15
16[*.php]
17max_line_length = 100
18
19[Dockerfile]
20max_line_length = 80
21
22[Makefile]
23indent_style = tab
diff --git a/.gitattributes b/.gitattributes
index dd0e573c..b191e227 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -22,8 +22,10 @@ Dockerfile text
22*.ttf binary 22*.ttf binary
23*.min.css binary 23*.min.css binary
24*.min.js binary 24*.min.js binary
25*.mo binary
25 26
26# Exclude from Git archives 27# Exclude from Git archives
28.editorconfig export-ignore
27.gitattributes export-ignore 29.gitattributes export-ignore
28.github export-ignore 30.github export-ignore
29.gitignore export-ignore 31.gitignore export-ignore
diff --git a/.github/mailmap b/.github/mailmap
index bbdb7908..7633afcf 100644
--- a/.github/mailmap
+++ b/.github/mailmap
@@ -1,6 +1,8 @@
1ArthurHoaro <arthur@hoa.ro> 1ArthurHoaro <arthur@hoa.ro>
2Florian Eula <eula.florian@gmail.com> feula 2Florian Eula <eula.florian@gmail.com> feula
3Florian Eula <eula.florian@gmail.com> <mr.pikzen@gmail.com> 3Florian Eula <eula.florian@gmail.com> <mr.pikzen@gmail.com>
4Immánuel Fodor <immanuelfactor+github@gmail.com>
5kalvn <kalvnthereal@gmail.com> <kalvn@users.noreply.github.com>
4Nicolas Danelon <hi@nicolasmd.com.ar> nicolasm 6Nicolas Danelon <hi@nicolasmd.com.ar> nicolasm
5Nicolas Danelon <hi@nicolasmd.com.ar> <nda@3818.com.ar> 7Nicolas Danelon <hi@nicolasmd.com.ar> <nda@3818.com.ar>
6Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@gmail.com> 8Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@gmail.com>
diff --git a/.gitignore b/.gitignore
index d546f248..3f6939a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ vendor/
18# Release archives 18# Release archives
19*.tar.gz 19*.tar.gz
20*.zip 20*.zip
21inc/languages/*/LC_MESSAGES/shaarli.mo
21 22
22# Development and test resources 23# Development and test resources
23coverage 24coverage
diff --git a/.travis.yml b/.travis.yml
index b6b9bddf..322e4337 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,6 +13,8 @@ install:
13 - composer self-update 13 - composer self-update
14 - composer install --prefer-dist 14 - composer install --prefer-dist
15 - locale -a 15 - locale -a
16before_script:
17 - PATH=${PATH//:\.\/node_modules\/\.bin/}
16script: 18script:
17 - make clean 19 - make clean
18 - make check_permissions 20 - make check_permissions
diff --git a/AUTHORS b/AUTHORS
index 57ff612a..9a6bfb2c 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,6 +1,6 @@
1 542 ArthurHoaro <arthur@hoa.ro> 1 577 ArthurHoaro <arthur@hoa.ro>
2 255 VirtualTam <virtualtam@flibidi.net> 2 283 VirtualTam <virtualtam@flibidi.net>
3 148 nodiscc <nodiscc@gmail.com> 3 179 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>
@@ -11,8 +11,9 @@
11 5 Lucas Cimon <lucas.cimon@gmail.com> 11 5 Lucas Cimon <lucas.cimon@gmail.com>
12 4 Alexandre Alapetite <alexandre@alapetite.fr> 12 4 Alexandre Alapetite <alexandre@alapetite.fr>
13 4 David Sferruzza <david.sferruzza@gmail.com> 13 4 David Sferruzza <david.sferruzza@gmail.com>
14 4 Immánuel Fodor <immanuelfactor+github@gmail.com>
15 4 kalvn <kalvnthereal@gmail.com>
14 3 Teromene <teromene@teromene.fr> 16 3 Teromene <teromene@teromene.fr>
15 3 kalvn <kalvnthereal@gmail.com>
16 2 Chris Kuethe <chris.kuethe@gmail.com> 17 2 Chris Kuethe <chris.kuethe@gmail.com>
17 2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org> 18 2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
18 2 Mathieu Chabanon <git@matchab.fr> 19 2 Mathieu Chabanon <git@matchab.fr>
@@ -27,11 +28,13 @@
27 1 BoboTiG <bobotig@gmail.com> 28 1 BoboTiG <bobotig@gmail.com>
28 1 Bronco <bronco@warriordudimanche.net> 29 1 Bronco <bronco@warriordudimanche.net>
29 1 D Low <daniellowtw@gmail.com> 30 1 D Low <daniellowtw@gmail.com>
31 1 Daniel Jakots <vigdis@chown.me>
30 1 Dimtion <zizou.xena@gmail.com> 32 1 Dimtion <zizou.xena@gmail.com>
31 1 Fanch <fanch-github@qth.fr> 33 1 Fanch <fanch-github@qth.fr>
32 1 Felix Bartels <felix@host-consultants.de> 34 1 Felix Bartels <felix@host-consultants.de>
33 1 Felix Kästner <github.com-fpunktk@fpunktk.de> 35 1 Felix Kästner <github.com-fpunktk@fpunktk.de>
34 1 Florian Voigt <flvoigt@me.com> 36 1 Florian Voigt <flvoigt@me.com>
37 1 Franck Kerbiriou <FranckKe@users.noreply.github.com>
35 1 Gary Marigliano <gmarigliano93@gmail.com> 38 1 Gary Marigliano <gmarigliano93@gmail.com>
36 1 Guillaume Virlet <github@virlet.org> 39 1 Guillaume Virlet <github@virlet.org>
37 1 Jonathan Druart <jonathan.druart@gmail.com> 40 1 Jonathan Druart <jonathan.druart@gmail.com>
@@ -41,6 +44,8 @@
41 1 Lionel Martin <renarddesmers@gmail.com> 44 1 Lionel Martin <renarddesmers@gmail.com>
42 1 Mark Gerarts <mark.gerarts@gmail.com> 45 1 Mark Gerarts <mark.gerarts@gmail.com>
43 1 Marsup <marsup@gmail.com> 46 1 Marsup <marsup@gmail.com>
47 1 Neros <contact@neros.fr>
44 1 Sbgodin <Sbgodin@users.noreply.github.com> 48 1 Sbgodin <Sbgodin@users.noreply.github.com>
45 1 TsT <tst2005@gmail.com> 49 1 TsT <tst2005@gmail.com>
46 1 dimtion <zizou.xena@gmail.com> 50 1 dimtion <zizou.xena@gmail.com>
51 1 durcheinandr <jochen@durcheinandr.de>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a7b120c..29e1fd6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,12 +4,42 @@ All notable changes to this project will be documented in this file.
4The format is based on [Keep a Changelog](http://keepachangelog.com/) 4The format is based on [Keep a Changelog](http://keepachangelog.com/)
5and this project adheres to [Semantic Versioning](http://semver.org/). 5and this project adheres to [Semantic Versioning](http://semver.org/).
6 6
7## [v0.9.3](https://github.com/shaarli/Shaarli/releases/tag/v0.9.3) - 2018-01-04 7## [v0.10.0](https://github.com/shaarli/Shaarli/releases/tag/v0.10.0) - UNPUBLISHED
8
9## [v0.9.4](https://github.com/shaarli/Shaarli/releases/tag/v0.9.4) - 2018-01-30
10### Added
11- Enable translations: Shaarli is now also available in French. Other language translations are welcome!
12- Add EditorConfig configuration
13- Add favicons for mobile devices
14- Add Alpine Linux arm32v7 Dockerfiles (master, latest)
15
16### Changed
17- Do not write bookmark edition history during file imports (performance)
18- Migrate Docker images (master, latest) to Alpine Linux
19- Improve unitary tests and code coverage
20- Improve thumbnail display
21- Improve theme ergonomics
22- Improve messages if there is no plugin or parameter available in the admin page
23- Increase buffer size for cURL download
24- Force HTTPS if the original port is 443 behind a reverse proxy (workaround)
25- Improve page title retrieval performances
26
27### Removed
28- Remove redirector setting from Configure page
29
30### Fixed
31- Fix broken links in the documentation
32- Enable access to `data/user.css` (Apache 2.2 & 2.4)
33- Don't URL encode description links if parameter `redirector.encode_url` is set to false
34- Fix an issue preventing the Save button to appear for plugin parameters
8 35
36
37## [v0.9.3](https://github.com/shaarli/Shaarli/releases/tag/v0.9.3) - 2018-01-04
9**XSS vulnerability fixed. Please update.** 38**XSS vulnerability fixed. Please update.**
10 39
11### Security 40## Security
12- Fix an XSS (cross-site-scripting) vulnerability in `index.php` 41- Fix an XSS (cross-site-scripting) vulnerability in `index.php` -
42 [CVE-2018-5249](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-5249)
13 43
14 44
15## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07 45## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07
@@ -48,7 +78,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
48 78
49### Security 79### Security
50 80
51- Vulnerability introduced in v0.9.1 fixed. 81- 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)).
82
52 83
53## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23 84## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23
54 85
@@ -123,7 +154,7 @@ Theming:
123 - Introduce a new theme 154 - Introduce a new theme
124 - Allow selecting themes/templates from the configuration page 155 - Allow selecting themes/templates from the configuration page
125 - New/Edit link form can be submitted using CTRL+Enter in the textarea 156 - New/Edit link form can be submitted using CTRL+Enter in the textarea
126 - Shaarli version is displayed in the footer when logged in 157 - Shaarli version is displayed in the footer when logged in
127- Add plugin placeholders to Atom/RSS feed templates 158- Add plugin placeholders to Atom/RSS feed templates
128- Add OpenSearch to feed templates 159- Add OpenSearch to feed templates
129- Add `campaign_` to the URL cleanup pattern list 160- Add `campaign_` to the URL cleanup pattern list
@@ -153,7 +184,7 @@ Theming:
153- Improved date time display depending on the locale 184- Improved date time display depending on the locale
154- Partial namespace support for Shaarli classes 185- Partial namespace support for Shaarli classes
155- Shaarli version is now only present in `shaarli_version.php` 186- Shaarli version is now only present in `shaarli_version.php`
156- Human readable maximum file size upload 187- Human readable maximum file size upload
157 188
158 189
159### Removed 190### Removed
@@ -195,6 +226,13 @@ Theming:
195 226
196- Editing a link created before the new ID system would change its permalink. 227- Editing a link created before the new ID system would change its permalink.
197 228
229## [v0.8.5](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5) - 2018-01-04
230**XSS vulnerability fixed. Please update.**
231
232## Security
233- Fix an XSS (cross-site-scripting) vulnerability in `index.php` -
234 [CVE-2018-5249](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-5249)
235
198## [v0.8.4](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) - 2017-03-04 236## [v0.8.4](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) - 2017-03-04
199### Security 237### Security
200- Markdown plugin: escape HTML entities by default 238- Markdown plugin: escape HTML entities by default
@@ -210,7 +248,7 @@ Theming:
210 248
211## [v0.8.1](https://github.com/shaarli/Shaarli/releases/tag/v0.8.1) - 2016-12-12 249## [v0.8.1](https://github.com/shaarli/Shaarli/releases/tag/v0.8.1) - 2016-12-12
212 250
213> Note: this version will create an automatic backup of your database if anything goes wrong. 251> Note: this version will create an automatic backup of your database if anything goes wrong.
214 252
215### Added 253### Added
216- Add CHANGELOG.md to track the whole project's history 254- Add CHANGELOG.md to track the whole project's history
@@ -227,7 +265,7 @@ Theming:
227- Link ID complete refactoring: 265- Link ID complete refactoring:
228 - Links now have a numeric ID instead of dates 266 - Links now have a numeric ID instead of dates
229 - Short URLs are now created once and can't change over time (previous URL are kept) 267 - Short URLs are now created once and can't change over time (previous URL are kept)
230- Templates: 268- Templates:
231 - Changed placeholder behaviour for: `buttons_toolbar`, `fields_toolbar` and `action_plugin` 269 - Changed placeholder behaviour for: `buttons_toolbar`, `fields_toolbar` and `action_plugin`
232 - Cleanup `{loop}` declarations in templates 270 - Cleanup `{loop}` declarations in templates
233 - Tools: hide Firefox Social button when not in HTTPS 271 - Tools: hide Firefox Social button when not in HTTPS
@@ -245,7 +283,7 @@ Theming:
245- Plugins: 283- Plugins:
246 - Tools: only display parameter description when it exists 284 - Tools: only display parameter description when it exists
247 - archive.org: do not propose archival of private notes 285 - archive.org: do not propose archival of private notes
248 - Markdown: 286 - Markdown:
249 - render links properly in code blocks 287 - render links properly in code blocks
250 - bug regarding the `nomarkdown` tag 288 - bug regarding the `nomarkdown` tag
251 - W3C compliance 289 - W3C compliance
@@ -384,7 +422,7 @@ Please use our release archives, or follow the
384### Fixed 422### Fixed
385- Fix a bug where renaming a tag was causing a 404 423- Fix a bug where renaming a tag was causing a 404
386- Fix a bug allowing to search blank terms 424- Fix a bug allowing to search blank terms
387- Fix a bug preventing to remove a tag with special chars when searching 425- Fix a bug preventing to remove a tag with special chars when searching
388 426
389 427
390## [v0.6.2](https://github.com/shaarli/Shaarli/releases/tag/v0.6.2) - 2015-12-23 428## [v0.6.2](https://github.com/shaarli/Shaarli/releases/tag/v0.6.2) - 2015-12-23
@@ -690,7 +728,7 @@ Initial release on GitHub.
690- When you click the key to see only private links, it turns yellow 728- When you click the key to see only private links, it turns yellow
691 729
692### Changed 730### Changed
693- The "Daily" page now automatically skips empty days. 731- The "Daily" page now automatically skips empty days.
694 732
695### Fixed 733### Fixed
696- Corrected the tag encoding (there was a bug when selecting a second tag which contains accented characters) 734- Corrected the tag encoding (there was a bug when selecting a second tag which contains accented characters)
@@ -988,7 +1026,7 @@ Initial release on GitHub.
988- Nicer timezone selection patch by killruana 1026- Nicer timezone selection patch by killruana
989 1027
990### Fixed 1028### Fixed
991- New lines now appear correctly in the RSS feed descriptions. 1029- New lines now appear correctly in the RSS feed descriptions.
992 1030
993 1031
994## [v0.0.17beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history) 1032## [v0.0.17beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
@@ -1042,7 +1080,7 @@ Initial release on GitHub.
1042## [v0.0.14beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history) 1080## [v0.0.14beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
1043### Added 1081### Added
1044- You no longer need to disable `magic_quotes` on your host. 1082- You no longer need to disable `magic_quotes` on your host.
1045 Shaarli will cope with this option beeing activated. 1083 Shaarli will cope with this option beeing activated.
1046 1084
1047 1085
1048## [v0.0.13beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history) 1086## [v0.0.13beta](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
diff --git a/Makefile b/Makefile
index a3696ec9..d659d908 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,6 @@
1# The personal, minimalist, super-fast, database free, bookmarking service. 1# The personal, minimalist, super-fast, database free, bookmarking service.
2# Makefile for PHP code analysis & testing, documentation and release generation 2# Makefile for PHP code analysis & testing, documentation and release generation
3 3
4# Prerequisites:
5# - install Composer, either:
6# - from your distro's package manager;
7# - from the official website (https://getcomposer.org/download/);
8# - install/update test dependencies:
9# $ composer install # 1st setup
10# $ composer update
11# - install Xdebug for PHPUnit code coverage reports:
12# - see http://xdebug.org/docs/install
13# - enable in php.ini
14
15BIN = vendor/bin 4BIN = vendor/bin
16PHP_SOURCE = index.php application tests plugins 5PHP_SOURCE = index.php application tests plugins
17PHP_COMMA_SOURCE = index.php,application,tests,plugins 6PHP_COMMA_SOURCE = index.php,application,tests,plugins
@@ -115,7 +104,7 @@ check_permissions:
115 @echo "----------------------" 104 @echo "----------------------"
116 @echo "Check file permissions" 105 @echo "Check file permissions"
117 @echo "----------------------" 106 @echo "----------------------"
118 @for file in `git ls-files`; do \ 107 @for file in `git ls-files | grep -v docker`; do \
119 if [ -x $$file ]; then \ 108 if [ -x $$file ]; then \
120 errors=true; \ 109 errors=true; \
121 echo "$${file} is executable"; \ 110 echo "$${file} is executable"; \
@@ -130,12 +119,12 @@ check_permissions:
130# See phpunit.xml for configuration 119# See phpunit.xml for configuration
131# https://phpunit.de/manual/current/en/appendixes.configuration.html 120# https://phpunit.de/manual/current/en/appendixes.configuration.html
132## 121##
133test: 122test: translate
134 @echo "-------" 123 @echo "-------"
135 @echo "PHPUNIT" 124 @echo "PHPUNIT"
136 @echo "-------" 125 @echo "-------"
137 @mkdir -p sandbox coverage 126 @mkdir -p sandbox coverage
138 @$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests 127 @$(BIN)/phpunit --coverage-php coverage/main.cov --bootstrap tests/bootstrap.php --testsuite unit-tests
139 128
140locale_test_%: 129locale_test_%:
141 @UT_LOCALE=$*.utf8 \ 130 @UT_LOCALE=$*.utf8 \
@@ -168,15 +157,15 @@ composer_dependencies: clean
168 composer install --no-dev --prefer-dist 157 composer install --no-dev --prefer-dist
169 find vendor/ -name ".git" -type d -exec rm -rf {} + 158 find vendor/ -name ".git" -type d -exec rm -rf {} +
170 159
171### generate a release tarball and include 3rd-party dependencies 160### generate a release tarball and include 3rd-party dependencies and translations
172release_tar: composer_dependencies htmldoc 161release_tar: composer_dependencies htmldoc translate
173 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD 162 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
174 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/ 163 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
175 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/ 164 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
176 gzip $(ARCHIVE_VERSION).tar 165 gzip $(ARCHIVE_VERSION).tar
177 166
178### generate a release zip and include 3rd-party dependencies 167### generate a release zip and include 3rd-party dependencies and translations
179release_zip: composer_dependencies htmldoc 168release_zip: composer_dependencies htmldoc translate
180 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD 169 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
181 mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor} 170 mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
182 rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/ 171 rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
@@ -213,3 +202,8 @@ htmldoc:
213 mkdocs build' 202 mkdocs build'
214 find doc/html/ -type f -exec chmod a-x '{}' \; 203 find doc/html/ -type f -exec chmod a-x '{}' \;
215 rm -r venv 204 rm -r venv
205
206
207### Generate Shaarli's translation compiled file (.mo)
208translate:
209 @find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \; \ No newline at end of file
diff --git a/README.md b/README.md
index 100ff46b..e7e8ad4c 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,10 @@ _Do you want to share the links you discover?_
6_Shaarli is a minimalist link sharing service that you can install on your own server._ 6_Shaarli is a minimalist link sharing service that you can install on your own server._
7_It is designed to be personal (single-user), fast and handy._ 7_It is designed to be personal (single-user), fast and handy._
8 8
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.5-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5)
10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) 10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
11&bull; 11&bull;
12[![](https://img.shields.io/badge/latest-v0.9.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) 12[![](https://img.shields.io/badge/latest-v0.9.3-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.3)
13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) 13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
14&bull; 14&bull;
15[![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli) 15[![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli)
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php
index 5643f4a0..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
@@ -179,7 +180,7 @@ class ApplicationUtils
179 $rainTplDir.'/'.$conf->get('resource.theme'), 180 $rainTplDir.'/'.$conf->get('resource.theme'),
180 ) as $path) { 181 ) as $path) {
181 if (! is_readable(realpath($path))) { 182 if (! is_readable(realpath($path))) {
182 $errors[] = '"'.$path.'" directory is not readable'; 183 $errors[] = '"'.$path.'" '. t('directory is not readable');
183 } 184 }
184 } 185 }
185 186
@@ -191,10 +192,10 @@ class ApplicationUtils
191 $conf->get('resource.raintpl_tmp'), 192 $conf->get('resource.raintpl_tmp'),
192 ) as $path) { 193 ) as $path) {
193 if (! is_readable(realpath($path))) { 194 if (! is_readable(realpath($path))) {
194 $errors[] = '"'.$path.'" directory is not readable'; 195 $errors[] = '"'.$path.'" '. t('directory is not readable');
195 } 196 }
196 if (! is_writable(realpath($path))) { 197 if (! is_writable(realpath($path))) {
197 $errors[] = '"'.$path.'" directory is not writable'; 198 $errors[] = '"'.$path.'" '. t('directory is not writable');
198 } 199 }
199 } 200 }
200 201
@@ -212,10 +213,10 @@ class ApplicationUtils
212 } 213 }
213 214
214 if (! is_readable(realpath($path))) { 215 if (! is_readable(realpath($path))) {
215 $errors[] = '"'.$path.'" file is not readable'; 216 $errors[] = '"'.$path.'" '. t('file is not readable');
216 } 217 }
217 if (! is_writable(realpath($path))) { 218 if (! is_writable(realpath($path))) {
218 $errors[] = '"'.$path.'" file is not writable'; 219 $errors[] = '"'.$path.'" '. t('file is not writable');
219 } 220 }
220 } 221 }
221 222
diff --git a/application/Cache.php b/application/Cache.php
index 5d050165..e5d43e61 100644
--- a/application/Cache.php
+++ b/application/Cache.php
@@ -13,7 +13,7 @@
13function purgeCachedPages($pageCacheDir) 13function purgeCachedPages($pageCacheDir)
14{ 14{
15 if (! is_dir($pageCacheDir)) { 15 if (! is_dir($pageCacheDir)) {
16 $error = 'Cannot purge '.$pageCacheDir.': no directory'; 16 $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
17 error_log($error); 17 error_log($error);
18 return $error; 18 return $error;
19 } 19 }
diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php
index 7377bcec..ebae18b4 100644
--- a/application/FeedBuilder.php
+++ b/application/FeedBuilder.php
@@ -148,11 +148,11 @@ 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'], '', false, $pageaddr);
156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink; 156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
157 157
158 $pubDate = $link['created']; 158 $pubDate = $link['created'];
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/HttpUtils.php b/application/HttpUtils.php
index 00835966..83a4c5e2 100644
--- a/application/HttpUtils.php
+++ b/application/HttpUtils.php
@@ -3,9 +3,11 @@
3 * GET an HTTP URL to retrieve its content 3 * GET an HTTP URL to retrieve its content
4 * Uses the cURL library or a fallback method 4 * Uses the cURL library or a fallback method
5 * 5 *
6 * @param string $url URL to get (http://...) 6 * @param string $url URL to get (http://...)
7 * @param int $timeout network timeout (in seconds) 7 * @param int $timeout network timeout (in seconds)
8 * @param int $maxBytes maximum downloaded bytes (default: 4 MiB) 8 * @param int $maxBytes maximum downloaded bytes (default: 4 MiB)
9 * @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION).
10 * Can be used to add download conditions on the headers (response code, content type, etc.).
9 * 11 *
10 * @return array HTTP response headers, downloaded content 12 * @return array HTTP response headers, downloaded content
11 * 13 *
@@ -29,7 +31,7 @@
29 * @see http://stackoverflow.com/q/9183178 31 * @see http://stackoverflow.com/q/9183178
30 * @see http://stackoverflow.com/q/1462720 32 * @see http://stackoverflow.com/q/1462720
31 */ 33 */
32function get_http_response($url, $timeout = 30, $maxBytes = 4194304) 34function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
33{ 35{
34 $urlObj = new Url($url); 36 $urlObj = new Url($url);
35 $cleanUrl = $urlObj->idnToAscii(); 37 $cleanUrl = $urlObj->idnToAscii();
@@ -75,8 +77,12 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304)
75 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); 77 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
76 curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); 78 curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
77 79
80 if (is_callable($curlWriteFunction)) {
81 curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
82 }
83
78 // Max download size management 84 // Max download size management
79 curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024); 85 curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
80 curl_setopt($ch, CURLOPT_NOPROGRESS, false); 86 curl_setopt($ch, CURLOPT_NOPROGRESS, false);
81 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, 87 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
82 function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) 88 function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes)
@@ -302,6 +308,13 @@ function server_url($server)
302 $port = $server['HTTP_X_FORWARDED_PORT']; 308 $port = $server['HTTP_X_FORWARDED_PORT'];
303 } 309 }
304 310
311 // This is a workaround for proxies that don't forward the scheme properly.
312 // Connecting over port 443 has to be in HTTPS.
313 // See https://github.com/shaarli/Shaarli/issues/1022
314 if ($port == '443') {
315 $scheme = 'https';
316 }
317
305 if (($scheme == 'http' && $port != '80') 318 if (($scheme == 'http' && $port != '80')
306 || ($scheme == 'https' && $port != '443') 319 || ($scheme == 'https' && $port != '443')
307 ) { 320 ) {
diff --git a/application/Languages.php b/application/Languages.php
index c8b0a25a..357c7524 100644
--- a/application/Languages.php
+++ b/application/Languages.php
@@ -1,21 +1,164 @@
1<?php 1<?php
2 2
3namespace Shaarli;
4
5use Gettext\GettextTranslator;
6use Gettext\Merge;
7use Gettext\Translations;
8use Gettext\Translator;
9use Gettext\TranslatorInterface;
10use Shaarli\Config\ConfigManager;
11
3/** 12/**
4 * Wrapper function for translation which match the API 13 * Class Languages
5 * of gettext()/_() and ngettext(). 14 *
15 * Load Shaarli translations using 'gettext/gettext'.
16 * This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
17 * with a fixed language, or dynamically using autoLocale().
6 * 18 *
7 * Not doing translation for now. 19 * Translation files PO/MO files follow gettext standard and must be placed under:
20 * <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
8 * 21 *
9 * @param string $text Text to translate. 22 * Pros/cons:
10 * @param string $nText The plural message ID. 23 * - gettext extension is faster
11 * @param int $nb The number of items for plural forms. 24 * - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
12 * 25 *
13 * @return String Text translated. 26 * Settings:
27 * - translation.mode:
28 * - auto: use default setting (PHP implementation)
29 * - php: use PHP implementation
30 * - gettext: use gettext wrapper
31 * - translation.language:
32 * - auto: use autoLocale() and the language change according to user HTTP headers
33 * - fixed language: e.g. 'fr'
34 * - translation.extensions:
35 * - domain => translation_path: allow plugins and themes to extend the defaut extension
36 * The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
37 *
38 * @package Shaarli
14 */ 39 */
15function t($text, $nText = '', $nb = 0) { 40class Languages
16 if (empty($nText)) { 41{
17 return $text; 42 /**
43 * Core translations domain
44 */
45 const DEFAULT_DOMAIN = 'shaarli';
46
47 /**
48 * @var TranslatorInterface
49 */
50 protected $translator;
51
52 /**
53 * @var string
54 */
55 protected $language;
56
57 /**
58 * @var ConfigManager
59 */
60 protected $conf;
61
62 /**
63 * Languages constructor.
64 *
65 * @param string $language lang determined by autoLocale(), can be overridden.
66 * @param ConfigManager $conf instance.
67 */
68 public function __construct($language, $conf)
69 {
70 $this->conf = $conf;
71 $confLanguage = $this->conf->get('translation.language', 'auto');
72 if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
73 $this->language = substr($language, 0, 5);
74 } else {
75 $this->language = $confLanguage;
76 }
77
78 if (! extension_loaded('gettext')
79 || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
80 ) {
81 $this->initPhpTranslator();
82 } else {
83 $this->initGettextTranslator();
84 }
85
86 // Register default functions (e.g. '__()') to use our Translator
87 $this->translator->register();
88 }
89
90 /**
91 * Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
92 */
93 protected function initGettextTranslator ()
94 {
95 $this->translator = new GettextTranslator();
96 $this->translator->setLanguage($this->language);
97 $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
98
99 foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
100 if ($domain !== self::DEFAULT_DOMAIN) {
101 $this->translator->loadDomain($domain, $translationPath, false);
102 }
103 }
104 }
105
106 /**
107 * Initialize the translator using a PHP implementation of gettext.
108 *
109 * Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
110 */
111 protected function initPhpTranslator()
112 {
113 $this->translator = new Translator();
114 $translations = new Translations();
115 // Core translations
116 try {
117 /** @var Translations $translations */
118 $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
119 $translations->setDomain('shaarli');
120 $this->translator->loadTranslations($translations);
121 } catch (\InvalidArgumentException $e) {}
122
123
124 // Extension translations (plugins, themes, etc.).
125 foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
126 if ($domain === self::DEFAULT_DOMAIN) {
127 continue;
128 }
129
130 try {
131 /** @var Translations $extension */
132 $extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
133 $extension->setDomain($domain);
134 $this->translator->loadTranslations($extension);
135 } catch (\InvalidArgumentException $e) {}
136 }
137 }
138
139 /**
140 * Checks if a language string is valid.
141 *
142 * @param string $language e.g. 'fr' or 'en_US'
143 *
144 * @return bool true if valid, false otherwise
145 */
146 protected function isValidLanguage($language)
147 {
148 return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
149 }
150
151 /**
152 * Get the list of available languages for Shaarli.
153 *
154 * @return array List of available languages, with their label.
155 */
156 public static function getAvailableLanguages()
157 {
158 return [
159 'auto' => t('Automatic'),
160 'en' => t('English'),
161 'fr' => t('French'),
162 ];
18 } 163 }
19 $actualForm = $nb > 1 ? $nText : $text;
20 return sprintf($actualForm, $nb);
21} 164}
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 22c1f0ab..c1661d52 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -133,16 +133,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess
133 { 133 {
134 // TODO: use exceptions instead of "die" 134 // TODO: use exceptions instead of "die"
135 if (!$this->loggedIn) { 135 if (!$this->loggedIn) {
136 die('You are not authorized to add a link.'); 136 die(t('You are not authorized to add a link.'));
137 } 137 }
138 if (!isset($value['id']) || empty($value['url'])) { 138 if (!isset($value['id']) || empty($value['url'])) {
139 die('Internal Error: A link should always have an id and URL.'); 139 die(t('Internal Error: A link should always have an id and URL.'));
140 } 140 }
141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { 141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
142 die('You must specify an integer as a key.'); 142 die(t('You must specify an integer as a key.'));
143 } 143 }
144 if ($offset !== null && $offset !== $value['id']) { 144 if ($offset !== null && $offset !== $value['id']) {
145 die('Array offset and link ID must be equal.'); 145 die(t('Array offset and link ID must be equal.'));
146 } 146 }
147 147
148 // If the link exists, we reuse the real offset, otherwise new entry 148 // If the link exists, we reuse the real offset, otherwise new entry
@@ -248,13 +248,13 @@ class LinkDB implements Iterator, Countable, ArrayAccess
248 $this->links = array(); 248 $this->links = array();
249 $link = array( 249 $link = array(
250 'id' => 1, 250 'id' => 1,
251 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', 251 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
252 'url'=>'https://shaarli.readthedocs.io', 252 'url'=>'https://shaarli.readthedocs.io',
253 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. 253 'description'=>t('Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
254 254
255To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. 255To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
256 256
257You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', 257You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'),
258 'private'=>0, 258 'private'=>0,
259 'created'=> new DateTime(), 259 'created'=> new DateTime(),
260 'tags'=>'opensource software' 260 'tags'=>'opensource software'
@@ -264,9 +264,9 @@ You use the community supported version of the original Shaarli project, by Seba
264 264
265 $link = array( 265 $link = array(
266 'id' => 0, 266 'id' => 0,
267 'title'=>'My secret stuff... - Pastebin.com', 267 'title'=> t('My secret stuff... - Pastebin.com'),
268 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', 268 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
269 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', 269 'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
270 'private'=>1, 270 'private'=>1,
271 'created'=> new DateTime('1 minute ago'), 271 'created'=> new DateTime('1 minute ago'),
272 'tags'=>'secretstuff', 272 'tags'=>'secretstuff',
@@ -289,13 +289,15 @@ You use the community supported version of the original Shaarli project, by Seba
289 return; 289 return;
290 } 290 }
291 291
292 $this->urls = [];
293 $this->ids = [];
292 $this->links = FileUtils::readFlatDB($this->datastore, []); 294 $this->links = FileUtils::readFlatDB($this->datastore, []);
293 295
294 $toremove = array(); 296 $toremove = array();
295 foreach ($this->links as $key => &$link) { 297 foreach ($this->links as $key => &$link) {
296 if (! $this->loggedIn && $link['private'] != 0) { 298 if (! $this->loggedIn && $link['private'] != 0) {
297 // Transition for not upgraded databases. 299 // Transition for not upgraded databases.
298 $toremove[] = $key; 300 unset($this->links[$key]);
299 continue; 301 continue;
300 } 302 }
301 303
@@ -329,14 +331,10 @@ You use the community supported version of the original Shaarli project, by Seba
329 } 331 }
330 $link['shorturl'] = smallHash($link['linkdate']); 332 $link['shorturl'] = smallHash($link['linkdate']);
331 } 333 }
332 }
333 334
334 // If user is not logged in, filter private links. 335 $this->urls[$link['url']] = $key;
335 foreach ($toremove as $offset) { 336 $this->ids[$link['id']] = $key;
336 unset($this->links[$offset]);
337 } 337 }
338
339 $this->reorder();
340 } 338 }
341 339
342 /** 340 /**
@@ -346,6 +344,7 @@ You use the community supported version of the original Shaarli project, by Seba
346 */ 344 */
347 private function write() 345 private function write()
348 { 346 {
347 $this->reorder();
349 FileUtils::writeFlatDB($this->datastore, $this->links); 348 FileUtils::writeFlatDB($this->datastore, $this->links);
350 } 349 }
351 350
@@ -528,8 +527,8 @@ You use the community supported version of the original Shaarli project, by Seba
528 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; 527 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
529 }); 528 });
530 529
531 $this->urls = array(); 530 $this->urls = [];
532 $this->ids = array(); 531 $this->ids = [];
533 foreach ($this->links as $key => $link) { 532 foreach ($this->links as $key => $link) {
534 $this->urls[$link['url']] = $key; 533 $this->urls[$link['url']] = $key;
535 $this->ids[$link['id']] = $key; 534 $this->ids[$link['id']] = $key;
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
index 99ecd1e2..12376e27 100644
--- a/application/LinkFilter.php
+++ b/application/LinkFilter.php
@@ -444,5 +444,11 @@ class LinkFilter
444 444
445class LinkNotFoundException extends Exception 445class LinkNotFoundException extends Exception
446{ 446{
447 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 }
448} 454}
diff --git a/application/LinkUtils.php b/application/LinkUtils.php
index 267e62cd..3705f7e9 100644
--- a/application/LinkUtils.php
+++ b/application/LinkUtils.php
@@ -1,60 +1,81 @@
1<?php 1<?php
2 2
3/** 3/**
4 * Extract title from an HTML document. 4 * Get cURL callback function for CURLOPT_WRITEFUNCTION
5 * 5 *
6 * @param string $html HTML content where to look for a title. 6 * @param string $charset to extract from the downloaded page (reference)
7 * @param string $title to extract from the downloaded page (reference)
8 * @param string $curlGetInfo Optionnaly overrides curl_getinfo function
7 * 9 *
8 * @return bool|string Extracted title if found, false otherwise. 10 * @return Closure
9 */ 11 */
10function html_extract_title($html) 12function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo')
11{ 13{
12 if (preg_match('!<title.*?>(.*?)</title>!is', $html, $matches)) { 14 /**
13 return trim(str_replace("\n", '', $matches[1])); 15 * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
14 } 16 *
15 return false; 17 * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
18 * Then we extract the title and the charset and stop the download when it's done.
19 *
20 * @param resource $ch cURL resource
21 * @param string $data chunk of data being downloaded
22 *
23 * @return int|bool length of $data or false if we need to stop the download
24 */
25 return function(&$ch, $data) use ($curlGetInfo, &$charset, &$title) {
26 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
27 if (!empty($responseCode) && $responseCode != 200) {
28 return false;
29 }
30 $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
31 if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
32 return false;
33 }
34 if (empty($charset)) {
35 $charset = header_extract_charset($contentType);
36 }
37 if (empty($charset)) {
38 $charset = html_extract_charset($data);
39 }
40 if (empty($title)) {
41 $title = html_extract_title($data);
42 }
43 // We got everything we want, stop the download.
44 if (!empty($responseCode) && !empty($contentType) && !empty($charset) && !empty($title)) {
45 return false;
46 }
47
48 return strlen($data);
49 };
16} 50}
17 51
18/** 52/**
19 * Determine charset from downloaded page. 53 * Extract title from an HTML document.
20 * Priority:
21 * 1. HTTP headers (Content type).
22 * 2. HTML content page (tag <meta charset>).
23 * 3. Use a default charset (default: UTF-8).
24 * 54 *
25 * @param array $headers HTTP headers array. 55 * @param string $html HTML content where to look for a title.
26 * @param string $htmlContent HTML content where to look for charset.
27 * @param string $defaultCharset Default charset to apply if other methods failed.
28 * 56 *
29 * @return string Determined charset. 57 * @return bool|string Extracted title if found, false otherwise.
30 */ 58 */
31function get_charset($headers, $htmlContent, $defaultCharset = 'utf-8') 59function html_extract_title($html)
32{ 60{
33 if ($charset = headers_extract_charset($headers)) { 61 if (preg_match('!<title.*?>(.*?)</title>!is', $html, $matches)) {
34 return $charset; 62 return trim(str_replace("\n", '', $matches[1]));
35 }
36
37 if ($charset = html_extract_charset($htmlContent)) {
38 return $charset;
39 } 63 }
40 64 return false;
41 return $defaultCharset;
42} 65}
43 66
44/** 67/**
45 * Extract charset from HTTP headers if it's defined. 68 * Extract charset from HTTP header if it's defined.
46 * 69 *
47 * @param array $headers HTTP headers array. 70 * @param string $header HTTP header Content-Type line.
48 * 71 *
49 * @return bool|string Charset string if found (lowercase), false otherwise. 72 * @return bool|string Charset string if found (lowercase), false otherwise.
50 */ 73 */
51function headers_extract_charset($headers) 74function header_extract_charset($header)
52{ 75{
53 if (! empty($headers['Content-Type']) && strpos($headers['Content-Type'], 'charset=') !== false) { 76 preg_match('/charset="?([^; ]+)/i', $header, $match);
54 preg_match('/charset="?([^; ]+)/i', $headers['Content-Type'], $match); 77 if (! empty($match[1])) {
55 if (! empty($match[1])) { 78 return strtolower(trim($match[1]));
56 return strtolower(trim($match[1]));
57 }
58 } 79 }
59 80
60 return false; 81 return false;
@@ -102,12 +123,13 @@ function count_private($links)
102 * 123 *
103 * @param string $text input string. 124 * @param string $text input string.
104 * @param string $redirector if a redirector is set, use it to gerenate links. 125 * @param string $redirector if a redirector is set, use it to gerenate links.
126 * @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
105 * 127 *
106 * @return string returns $text with all links converted to HTML links. 128 * @return string returns $text with all links converted to HTML links.
107 * 129 *
108 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 130 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
109 */ 131 */
110function text2clickable($text, $redirector = '') 132function text2clickable($text, $redirector = '', $urlEncode = true)
111{ 133{
112 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si'; 134 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
113 135
@@ -117,8 +139,9 @@ function text2clickable($text, $redirector = '')
117 // Redirector is set, urlencode the final URL. 139 // Redirector is set, urlencode the final URL.
118 return preg_replace_callback( 140 return preg_replace_callback(
119 $regex, 141 $regex,
120 function ($matches) use ($redirector) { 142 function ($matches) use ($redirector, $urlEncode) {
121 return '<a href="' . $redirector . urlencode($matches[1]) .'">'. $matches[1] .'</a>'; 143 $url = $urlEncode ? urlencode($matches[1]) : $matches[1];
144 return '<a href="' . $redirector . $url .'">'. $matches[1] .'</a>';
122 }, 145 },
123 $text 146 $text
124 ); 147 );
@@ -164,12 +187,13 @@ function space2nbsp($text)
164 * 187 *
165 * @param string $description shaare's description. 188 * @param string $description shaare's description.
166 * @param string $redirector if a redirector is set, use it to gerenate links. 189 * @param string $redirector if a redirector is set, use it to gerenate links.
190 * @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
167 * @param string $indexUrl URL to Shaarli's index. 191 * @param string $indexUrl URL to Shaarli's index.
168 * 192
169 * @return string formatted description. 193 * @return string formatted description.
170 */ 194 */
171function format_description($description, $redirector = '', $indexUrl = '') { 195function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '') {
172 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl))); 196 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl)));
173} 197}
174 198
175/** 199/**
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 291860ad..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 /**
@@ -92,7 +94,7 @@ class PageBuilder
92 $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true)); 94 $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true));
93 $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');
94 $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));
95 $this->tpl->assign('token', getToken($this->conf)); 97 $this->tpl->assign('token', $this->token);
96 98
97 if ($this->linkDB !== null) { 99 if ($this->linkDB !== null) {
98 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); 100 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
@@ -159,9 +161,12 @@ class PageBuilder
159 * 161 *
160 * @param string $message A messate to display what is not found 162 * @param string $message A messate to display what is not found
161 */ 163 */
162 public function render404($message = 'The page you are trying to reach does not exist or has been deleted.') 164 public function render404($message = '')
163 { 165 {
164 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'));
165 $this->tpl->assign('error_message', $message); 170 $this->tpl->assign('error_message', $message);
166 $this->renderPage('404'); 171 $this->renderPage('404');
167 } 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..71f0b38d
--- /dev/null
+++ b/application/SessionManager.php
@@ -0,0 +1,83 @@
1<?php
2namespace Shaarli;
3
4/**
5 * Manages the server-side session
6 */
7class SessionManager
8{
9 protected $session = [];
10
11 /**
12 * Constructor
13 *
14 * @param array $session The $_SESSION array (reference)
15 * @param ConfigManager $conf ConfigManager instance
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/Updater.php b/application/Updater.php
index 72b2def0..8d2bd577 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) {
@@ -436,6 +436,15 @@ class Updater
436 } 436 }
437 return true; 437 return true;
438 } 438 }
439
440 /**
441 * Save the datastore -> the link order is now applied when links are saved.
442 */
443 public function updateMethodReorderDatastore()
444 {
445 $this->linkDB->save($this->conf->get('resource.page_cache'));
446 return true;
447 }
439} 448}
440 449
441/** 450/**
@@ -482,7 +491,7 @@ class UpdaterException extends Exception
482 } 491 }
483 492
484 if (! empty($this->method)) { 493 if (! empty($this->method)) {
485 $out .= 'An error occurred while running the update '. $this->method . PHP_EOL; 494 $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
486 } 495 }
487 496
488 if (! empty($this->previous)) { 497 if (! empty($this->previous)) {
@@ -522,11 +531,11 @@ function read_updates_file($updatesFilepath)
522function write_updates_file($updatesFilepath, $updates) 531function write_updates_file($updatesFilepath, $updates)
523{ 532{
524 if (empty($updatesFilepath)) { 533 if (empty($updatesFilepath)) {
525 throw new Exception('Updates file path is not set, can\'t write updates.'); 534 throw new Exception(t('Updates file path is not set, can\'t write updates.'));
526 } 535 }
527 536
528 $res = file_put_contents($updatesFilepath, implode(';', $updates)); 537 $res = file_put_contents($updatesFilepath, implode(';', $updates));
529 if ($res === false) { 538 if ($res === false) {
530 throw new Exception('Unable to write updates in '. $updatesFilepath . '.'); 539 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
531 } 540 }
532} 541}
diff --git a/application/Utils.php b/application/Utils.php
index 4a2f5561..97b12fcf 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -182,36 +182,6 @@ function generateLocation($referer, $host, $loopTerms = array())
182} 182}
183 183
184/** 184/**
185 * Validate session ID to prevent Full Path Disclosure.
186 *
187 * See #298.
188 * The session ID's format depends on the hash algorithm set in PHP settings
189 *
190 * @param string $sessionId Session ID
191 *
192 * @return true if valid, false otherwise.
193 *
194 * @see http://php.net/manual/en/function.hash-algos.php
195 * @see http://php.net/manual/en/session.configuration.php
196 */
197function is_session_id_valid($sessionId)
198{
199 if (empty($sessionId)) {
200 return false;
201 }
202
203 if (!$sessionId) {
204 return false;
205 }
206
207 if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
208 return false;
209 }
210
211 return true;
212}
213
214/**
215 * Sniff browser language to set the locale automatically. 185 * Sniff browser language to set the locale automatically.
216 * Note that is may not work on your server if the corresponding locale is not installed. 186 * Note that is may not work on your server if the corresponding locale is not installed.
217 * 187 *
@@ -452,7 +422,7 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true)
452 */ 422 */
453function alphabetical_sort(&$data, $reverse = false, $byKeys = false) 423function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
454{ 424{
455 $callback = function($a, $b) use ($reverse) { 425 $callback = function ($a, $b) use ($reverse) {
456 // Collator is part of PHP intl. 426 // Collator is part of PHP intl.
457 if (class_exists('Collator')) { 427 if (class_exists('Collator')) {
458 $collator = new Collator(setlocale(LC_COLLATE, 0)); 428 $collator = new Collator(setlocale(LC_COLLATE, 0));
@@ -470,3 +440,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
470 usort($data, $callback); 440 usort($data, $callback);
471 } 441 }
472} 442}
443
444/**
445 * Wrapper function for translation which match the API
446 * of gettext()/_() and ngettext().
447 *
448 * @param string $text Text to translate.
449 * @param string $nText The plural message ID.
450 * @param int $nb The number of items for plural forms.
451 * @param string $domain The domain where the translation is stored (default: shaarli).
452 *
453 * @return string Text translated.
454 */
455function t($text, $nText = '', $nb = 1, $domain = 'shaarli') {
456 return dn__($domain, $text, $nText, $nb);
457}
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php
index 9ef2ef56..8c8d5610 100644
--- a/application/config/ConfigJson.php
+++ b/application/config/ConfigJson.php
@@ -22,10 +22,15 @@ class ConfigJson implements ConfigIO
22 $data = json_decode($data, true); 22 $data = json_decode($data, true);
23 if ($data === null) { 23 if ($data === null) {
24 $errorCode = json_last_error(); 24 $errorCode = json_last_error();
25 $error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #'; 25 $error = sprintf(
26 $error .= $errorCode. '<br>➜ <code>' . json_last_error_msg() .'</code>'; 26 'An error occurred while parsing JSON configuration file (%s): error code #%d',
27 $filepath,
28 $errorCode
29 );
30 $error .= '<br>➜ <code>' . json_last_error_msg() .'</code>';
27 if ($errorCode === JSON_ERROR_SYNTAX) { 31 if ($errorCode === JSON_ERROR_SYNTAX) {
28 $error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; 32 $error .= '<br>';
33 $error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
29 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.'; 34 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
30 } 35 }
31 throw new \Exception($error); 36 throw new \Exception($error);
@@ -44,8 +49,8 @@ class ConfigJson implements ConfigIO
44 if (!file_put_contents($filepath, $data)) { 49 if (!file_put_contents($filepath, $data)) {
45 throw new \IOException( 50 throw new \IOException(
46 $filepath, 51 $filepath,
47 'Shaarli could not create the config file. 52 t('Shaarli could not create the config file. '.
48 Please make sure Shaarli has the right to write in the folder is it installed in.' 53 'Please make sure Shaarli has the right to write in the folder is it installed in.')
49 ); 54 );
50 } 55 }
51 } 56 }
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index 7ff2fe67..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.
@@ -339,6 +339,10 @@ class ConfigManager
339 $this->setEmpty('redirector.url', ''); 339 $this->setEmpty('redirector.url', '');
340 $this->setEmpty('redirector.encode_url', true); 340 $this->setEmpty('redirector.encode_url', true);
341 341
342 $this->setEmpty('translation.language', 'auto');
343 $this->setEmpty('translation.mode', 'php');
344 $this->setEmpty('translation.extensions', []);
345
342 $this->setEmpty('plugins', array()); 346 $this->setEmpty('plugins', array());
343 } 347 }
344 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/Backup,-restore,-import-and-export.md b/doc/md/Backup,-restore,-import-and-export.md
index 89724857..bb790074 100644
--- a/doc/md/Backup,-restore,-import-and-export.md
+++ b/doc/md/Backup,-restore,-import-and-export.md
@@ -45,6 +45,10 @@ Shaarli cannot import data directly from [Scuttle](https://github.com/scronide/s
45However, you can use the third-party [scuttle-to-shaarli](https://github.com/q2apro/scuttle-to-shaarli) 45However, you can use the third-party [scuttle-to-shaarli](https://github.com/q2apro/scuttle-to-shaarli)
46tool to export the Scuttle database to the Netscape HTML format compatible with the Shaarli importer. 46tool to export the Scuttle database to the Netscape HTML format compatible with the Shaarli importer.
47 47
48### Refind
49
50You can use the third-party tool [Derefind](https://github.com/ShawnPConroy/Derefind) to convert refind.com bookmark exports to a format that can be imported into Shaarli.
51
48## Import Shaarli links to Firefox 52## Import Shaarli links to Firefox
49 53
50- Export your Shaarli links as described above. 54- Export your Shaarli links as described above.
diff --git a/doc/md/Bookmarklet.md b/doc/md/Bookmarklet.md
index e53e3261..c899e3cf 100644
--- a/doc/md/Bookmarklet.md
+++ b/doc/md/Bookmarklet.md
@@ -21,7 +21,7 @@ _This bookmarklet button is compatible with Firefox, Opera, Chrome and Safari. U
21 21
22Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunatly, there is nothing Shaarli can do about it. 22Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunatly, there is nothing Shaarli can do about it.
23 23
24See [#196](https://github.com/shaarli/Shaarli#196). 24See [#196](https://github.com/shaarli/Shaarli/issues/196).
25 25
26There is an open bug for both Firefox and Chromium: 26There is an open bug for both Firefox and Chromium:
27 27
diff --git a/doc/md/Browsing-and-searching.md b/doc/md/Browsing-and-searching.md
index 35707482..16c69855 100644
--- a/doc/md/Browsing-and-searching.md
+++ b/doc/md/Browsing-and-searching.md
@@ -14,10 +14,24 @@ Use the `Filter by tags` field to restrict displayed links to entries tagged wit
14 14
15**Hidden tags:** Tags starting with a dot `.` (example `.secret`) are private. They can only be seen and searched when logged in. 15**Hidden tags:** Tags starting with a dot `.` (example `.secret`) are private. They can only be seen and searched when logged in.
16 16
17Alternatively you can use the `Tag cloud` to discover all tags and click on any of them to display related links. 17### Tag cloud
18 18
19To search for links that are not tagged, enter `""` in the tag search field. 19The `Tag cloud` page diplays a "cloud" view of all tags in your Shaarli.
20
21 * The most frequently used tags are displayed with a bigger font size.
22 * When sorting by `Most used` or `Alphabetical`, tags are displayed as a _list_, along with counters and edit/delete buttons for each tag.
23 * Clicking on any tag will display a list of all Shaares matching this tag.
24 * Clicking on the counter next to a tag `example`, will filter the tag cloud to only display tags found in Shaares tagged `example`. Repeat this any number of times to further filter the tag cloud. Click `List all links with those tags` to display Shaares matching your current tag filter.
20 25
21## Filtering RSS feeds/Picture wall 26## Filtering RSS feeds/Picture wall
22 27
23RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS feeds). 28RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS-feeds).
29
30## Filter buttons
31
32Filter buttons can be found at the top left of the link list. They allow you to apply different filters to the list:
33
34 * **Private links:** When this toggle button is enabled, only shaares set to `private` will be shown.
35 * **Untagged links:** When the this toggle button is enabled (top left of the link list), only shaares _without any tags_ will be shown in the link list.
36
37Filter buttons are only available when logged in.
diff --git a/doc/md/Community-&-Related-software.md b/doc/md/Community-&-Related-software.md
index 8edbeefa..207153b6 100644
--- a/doc/md/Community-&-Related-software.md
+++ b/doc/md/Community-&-Related-software.md
@@ -1,23 +1,7 @@
1_Unofficial but related work on Shaarli. If you maintain one of these, 1_Unofficial but related work on Shaarli. If you maintain one of these,
2please get in touch with us to help us find a way to adapt your work to our fork._ 2please get in touch with us to help us find a way to adapt your work to our fork._
3 3
4## Community 4## Related software
5- [Liens en vrac de sebsauvage](http://sebsauvage.net/links/) - the original Shaarli
6- [A large list of Shaarlis](http://porneia.free.fr/pub/links/ou-est-shaarli.html)
7- [A list of working Shaarli aggregators](https://raw.githubusercontent.com/Oros42/find_shaarlis/master/annuaires.json)
8- [A list of some known Shaarlis](https://github.com/Oros42/shaarlis_list)
9- [Adieu Delicious, Diigo et StumbleUpon. Salut Shaarli ! - sebsauvage.net](http://sebsauvage.net/rhaa/index.php?2011/09/16/09/29/58-adieu-delicious-diigo-et-stumbleupon-salut-shaarli-) (fr) _16/09/2011 - the original post about Shaarli_
10- [Original ideas/fixme/TODO page](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:ideas)
11- [Original discussion page](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:discussion) (fr)
12- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
13- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni)
14
15
16### Articles and social media discussions
17- 2016-09-22 - Hacker News - https://news.ycombinator.com/item?id=12552176
18- 2015-08-15 - Reddit - [Question about migrating from WordPress to Shaarli.](https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/)
19- 2015-06-22 - Hacker News - https://news.ycombinator.com/item?id=9755366
20- 2015-05-12 - Reddit - [shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)](https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/)
21 5
22 6
23### REST API clients 7### REST API clients
@@ -29,28 +13,34 @@ See [REST API](REST-API) for a list of official and community clients.
29- [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter. 13- [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter.
30- [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli. 14- [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.
31- [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli. 15- [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli.
16- [twemoji](https://github.com/NerosTie/twemoji) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli (Twemoji version)
32- [google analytics](https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin) by [@ericjuden](http://github.com/ericjuden): Adds Google Analytics tracking support 17- [google analytics](https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin) by [@ericjuden](http://github.com/ericjuden): Adds Google Analytics tracking support
33- [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli. 18- [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.
19- [markdown-toolbar](https://github.com/immanuelfodor/shaarli-markdown-toolbar) by [@immanuelfodor](https://github.com/immanuelfodor) - Easily insert markdown syntax into the Description field when editing a link.
34- [related](https://github.com/ilesinge/shaarli-related) by [@ilesinge](https://github.com/ilesinge) - Show related links based on the number of identical tags. 20- [related](https://github.com/ilesinge/shaarli-related) by [@ilesinge](https://github.com/ilesinge) - Show related links based on the number of identical tags.
35- [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks. 21- [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.
36- [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli 22- [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli
23- [shaarli2mastodon](https://github.com/kalvn/shaarli2mastodon) by [@kalvn](https://github.com/kalvn) - This Shaarli plugin allows you to automatically publish links you post on your Mastodon timeline.
24- [shaarli-descriptor](https://github.com/immanuelfodor/shaarli-descriptor) by [@immanuelfodor](https://github.com/immanuelfodor) - Customize the default height/number of rows of the Description field when editing a link.
37 25
38 26
39### Third-party themes 27### Third-party themes
40See [Theming](Theming) for a list of community-contributed themes, and an installation guide. 28See [Theming](Theming) for a list of community-contributed themes, and an installation guide.
41 29
42 30
43## Integration with other platforms 31### Integration with other platforms
44- [tt-rss-shaarli](https://github.com/jcsaaddupuy/tt-rss-shaarli) - [Tiny-Tiny RSS](http://tt-rss.org/) plugin that adds support for sharing articles with Shaarli 32- [tt-rss-shaarli](https://github.com/jcsaaddupuy/tt-rss-shaarli) - [Tiny-Tiny RSS](http://tt-rss.org/) plugin that adds support for sharing articles with Shaarli
45- [octopress-shaarli](https://github.com/ahmet2mir/octopress-shaarli) - Octopress plugin to retrieve Shaarli links on the sidebar 33- [octopress-shaarli](https://github.com/ahmet2mir/octopress-shaarli) - Octopress plugin to retrieve Shaarli links on the sidebar
46- [Scuttle to Shaarli](https://github.com/q2apro/scuttle-to-shaarli) - Import bookmarks from Scuttle 34- [Scuttle to Shaarli](https://github.com/q2apro/scuttle-to-shaarli) - Import bookmarks from Scuttle
47 35
48 36
49### Mobile Apps 37### Mobile Apps
50- [ShaarliOS](https://github.com/mro/ShaarliOS) iOS share extension - see [#308](https://github.com/shaarli/Shaarli/issues/308#issuecomment-184592070) for some promo codes, 38- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension.
51- [Shaarli for Android](http://sebsauvage.net/links/?ZAyDzg) - Android application that adds Shaarli as a sharing provider 39- [Shaarli for Android](http://sebsauvage.net/links/?ZAyDzg) - Android application that adds Shaarli as a sharing provider
52- [Shaarlier for Android](https://github.com/dimtion/Shaarlier) - Android application to simply add links directly into your Shaarli 40- [Shaarlier for Android](https://github.com/dimtion/Shaarlier) - Android application to simply add links directly into your Shaarli
53 41
42### Browser addons
43 * [Shaarli Web Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli.
54 44
55### Server apps 45### Server apps
56- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content 46- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content
@@ -61,7 +51,22 @@ See [Theming](Theming) for a list of community-contributed themes, and an instal
61- [Self dead link](https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. [Another version](https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for other shaarli instances (but is more resource consuming). 51- [Self dead link](https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. [Another version](https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for other shaarli instances (but is more resource consuming).
62- [Bookmark Archiver](https://github.com/pirate/bookmark-archiver) - Save an archived copy of all websites starred using browser bookmarks/Shaarli/Delicious/Instapaper/Unmark.it/Pocket/Pinboard. Outputs browseable html. 52- [Bookmark Archiver](https://github.com/pirate/bookmark-archiver) - Save an archived copy of all websites starred using browser bookmarks/Shaarli/Delicious/Instapaper/Unmark.it/Pocket/Pinboard. Outputs browseable html.
63 53
64
65## Alternatives to Shaarli 54## Alternatives to Shaarli
66See the [bookmarks & link sharing](https://github.com/Kickball/awesome-selfhosted/#bookmarks--link-sharing) 55See [awesome-selfhosted: bookmarks & link sharing](https://github.com/Kickball/awesome-selfhosted/#bookmarks--link-sharing).
67section on [awesome-selfhosted](https://github.com/Kickball/awesome-selfhosted/). 56
57## Community
58- [Liens en vrac de sebsauvage](http://sebsauvage.net/links/) - the original Shaarli
59- [A large list of Shaarlis](http://porneia.free.fr/pub/links/ou-est-shaarli.html)
60- [A list of working Shaarli aggregators](https://raw.githubusercontent.com/Oros42/find_shaarlis/master/annuaires.json)
61- [A list of some known Shaarlis](https://github.com/Oros42/shaarlis_list)
62- [Adieu Delicious, Diigo et StumbleUpon. Salut Shaarli ! - sebsauvage.net](http://sebsauvage.net/rhaa/index.php?2011/09/16/09/29/58-adieu-delicious-diigo-et-stumbleupon-salut-shaarli-) (fr) _16/09/2011 - the original post about Shaarli_
63- [Original ideas/fixme/TODO page](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:ideas)
64- [Original discussion page](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:discussion) (fr)
65- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
66- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni)
67
68### Articles and social media discussions
69- 2016-09-22 - Hacker News - https://news.ycombinator.com/item?id=12552176
70- 2015-08-15 - Reddit - [Question about migrating from WordPress to Shaarli.](https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/)
71- 2015-06-22 - Hacker News - https://news.ycombinator.com/item?id=9755366
72- 2015-05-12 - Reddit - [shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)](https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/)
diff --git a/doc/md/Download-and-Installation.md b/doc/md/Download-and-Installation.md
index e5e929ef..0fdbd27d 100644
--- a/doc/md/Download-and-Installation.md
+++ b/doc/md/Download-and-Installation.md
@@ -4,44 +4,57 @@ Document Root (or directly at the document root).
4Also, please make sure your server meets the [requirements](Server-requirements) 4Also, please make sure your server meets the [requirements](Server-requirements)
5and is properly [configured](Server-configuration). 5and is properly [configured](Server-configuration).
6 6
7Several releases are available: 7Multiple releases branches are available:
8
9- latest (last release)
10- stable (previous major release)
11- master (development)
12
13Using one of the following methods:
8 14
9- by downloading full release archives including all dependencies 15- by downloading full release archives including all dependencies
10- by downloading Github archives 16- by downloading Github archives
11- by cloning the Git repository 17- by cloning the Git repository
18- using Docker: [see the documentation](docker/shaarli-images.md)
12 19
13--- 20--------------------------------------------------------------------------------
14 21
15## Latest release (recommended) 22## Latest release (recommended)
16### Download as an archive
17Get the latest released version from the [releases](https://github.com/shaarli/Shaarli/releases) page.
18 23
19**Download our *shaarli-full* archive** to include dependencies. 24### Download as an archive
20 25
21The current latest released version is `v0.9.1` 26In most cases, you should download the latest Shaarli release from the [releases](https://github.com/shaarli/Shaarli/releases) page. **Download our *shaarli-full* archive** to include dependencies.
22 27
23Or in command lines: 28The current latest released version is `v0.9.3`
24 29
25```bash 30```bash
26$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.1/shaarli-v0.9.1-full.zip 31$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.3/shaarli-v0.9.3-full.zip
27$ unzip shaarli-v0.9.1-full.zip 32$ unzip shaarli-v0.9.3-full.zip
28$ mv Shaarli /path/to/shaarli/ 33$ mv Shaarli /path/to/shaarli/
29``` 34```
30 35
31In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).|
32
33### Using git 36### Using git
34 37
38Cloning using `git` or downloading Github branches as zip files requires additional steps:
39
40 * Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
41 * Install [python3-virtualenv](https://pypi.python.org/pypi/virtualenv) to build the local HTML documentation.
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
48$ make htmldoc
39``` 49```
40 50
51--------------------------------------------------------------------------------
52
41## Stable version 53## Stable version
42 54
43The stable version has been experienced by Shaarli users, and will receive security updates. 55The stable version has been experienced by Shaarli users, and will receive security updates.
44 56
57
45### Download as an archive 58### Download as an archive
46 59
47As a .zip archive: 60As a .zip archive:
@@ -60,9 +73,9 @@ $ tar xvf stable.tar.gz
60$ mv Shaarli-stable /path/to/shaarli/ 73$ mv Shaarli-stable /path/to/shaarli/
61``` 74```
62 75
63### Clone with Git 76### Using git
64 77
65[Composer](https://getcomposer.org/) is required to build a functional Shaarli installation when pulling from git. 78Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
66 79
67```bash 80```bash
68$ git clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/ 81$ git clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/
@@ -71,25 +84,34 @@ $ cd /path/to/shaarli/
71$ composer install --no-dev --prefer-dist 84$ composer install --no-dev --prefer-dist
72``` 85```
73 86
87
88--------------------------------------------------------------------------------
89
74## Development version (mainline) 90## Development version (mainline)
75 91
76_Use at your own risk!_ 92_Use at your own risk!_
77 93
94Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
95
78To get the latest changes from the `master` branch: 96To get the latest changes from the `master` branch:
79 97
80```bash 98```bash
81# clone the repository 99# clone the repository
82$ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/ 100$ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
83# install/update third-party dependencies 101# install/update third-party dependencies
84$ cd /path/to/shaarli 102$ cd /path/to/shaarli
85$ composer install --no-dev --prefer-dist 103$ composer install --no-dev --prefer-dist
104$ make translate
105$ make htmldoc
86``` 106```
87 107
108-------------------------------------------------------------------------------
109
88## Finish Installation 110## Finish Installation
89 111
90Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser. 112Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.
91 113
92![install screenshot](http://i.imgur.com/wuMpDSN.png) 114![install screenshot](images/install-shaarli.png)
93 115
94Setup your Shaarli installation, and it's ready to use! 116Setup your Shaarli installation, and it's ready to use!
95 117
diff --git a/doc/md/Features.md b/doc/md/Features.md
deleted file mode 100644
index eef88d03..00000000
--- a/doc/md/Features.md
+++ /dev/null
@@ -1,25 +0,0 @@
1### Main features
2Shaarli is intended:
3
4- to share, comment and save interesting links and news
5- to bookmark useful/frequent personal links (as private links) and share them between computers
6- as a minimal blog/microblog/writing platform (no character limit)
7- as a read-it-later list (for example items tagged `readlater`)
8- to draft and save articles/ideas
9- to keep code snippets
10- to keep notes and documentation
11- as a shared clipboard between machines
12- as a todo list
13- to store playlists (e.g. with the `music` or `video` tags)
14- to keep extracts/comments from webpages that may disappear
15- to keep track of ongoing discussions (for example items tagged `discussion`)
16- [to feed RSS aggregators](http://shaarli.chassegnouf.net/?9Efeiw) (planets) with specific tags
17- to feed other social networks, blogs... using RSS feeds and external services (dlvr.it, ifttt.com ...)
18
19### Using Shaarli as a blog, notepad, pastebin...
20
21- Go to your Shaarli setup and log in
22- Click the `Add Link` button
23- To share text only, do not enter any URL in the corresponding input field and click `Add Link`
24- Pick a title and enter your article, or note, in the description field; add a few tags; optionally check `Private` then click `Save`
25- Voilà! Your article is now published (privately if you selected that option) and accessible using its permalink.
diff --git a/doc/md/Firefox-share.md b/doc/md/Firefox-share.md
index 878884a4..9a46b185 100644
--- a/doc/md/Firefox-share.md
+++ b/doc/md/Firefox-share.md
@@ -1,3 +1,6 @@
1| Note | Firefox Share is no longer available for Firefox 57 and later versions. |
2|---------|---------|
3
1### Add Shaarli as a sharing service to Firefox 4### Add Shaarli as a sharing service to Firefox
2 5
3- Open your Shaarli and `Login` 6- Open your Shaarli and `Login`
diff --git a/doc/md/Server-requirements.md b/doc/md/Server-requirements.md
index 707af762..2dc442df 100644
--- a/doc/md/Server-requirements.md
+++ b/doc/md/Server-requirements.md
@@ -35,7 +35,8 @@ Library | Required? | Usage
35Extension | Required? | Usage 35Extension | Required? | Usage
36---|:---:|--- 36---|:---:|---
37[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS 37[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS
38[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows | multibyte (Unicode) string support 38[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
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 99b25ba7..920c7e27 100644
--- a/doc/md/Shaarli-configuration.md
+++ b/doc/md/Shaarli-configuration.md
@@ -81,6 +81,20 @@ _These settings should not be edited_
81- **page_cache**: Shaarli's internal cache directory. 81- **page_cache**: Shaarli's internal cache directory.
82- **ban_file**: Banned IP file path. 82- **ban_file**: Banned IP file path.
83 83
84### Translation
85
86- **language**: translation language (also see [Translations](Translations))
87 - **auto** (default): The translation language is chosen from the browser locale.
88 It means that the language can be different for 2 different visitors depending on their locale.
89 - **en**: Use the English translation.
90 - **fr**: Use the French translation.
91- **mode**:
92 - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
93 - **gettext**: Use PHP builtin gettext extension
94 (faster, but requires `php-gettext` to be installed and to reload the web server on update)
95- **extension**: Translation extensions for custom themes or plugins.
96Must be an associative array: `translation domain => translation path`.
97
84### Updates 98### Updates
85 99
86- **check_updates**: Enable or disable update check to the git repository. 100- **check_updates**: Enable or disable update check to the git repository.
@@ -211,6 +225,13 @@ _These settings should not be edited_
211 "plugins": { 225 "plugins": {
212 "WALLABAG_URL": "http://demo.wallabag.org", 226 "WALLABAG_URL": "http://demo.wallabag.org",
213 "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 }
214 } 235 }
215} ?> 236} ?>
216``` 237```
diff --git a/doc/md/Translations.md b/doc/md/Translations.md
new file mode 100644
index 00000000..54a36655
--- /dev/null
+++ b/doc/md/Translations.md
@@ -0,0 +1,152 @@
1## Translations
2
3Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations
4since `>= v0.9.2`.
5
6Note that only the `default` theme supports translations.
7
8### Contributing
9
10We encourage the community to contribute to Shaarli's translation either by improving existing
11translations or submitting a new language.
12
13Contributing to the translation does not require development skill.
14
15Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`)
16is not stored on the repository, and is generated during the release process.
17
18### How to
19
20First, install [Poedit](https://poedit.net/) tool.
21
22Poedit will extract strings to translate from the PHP source code.
23
24**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
25every translatable string.
26
27You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended)
28or visit every template page in your browser to generate cache files, while logged in.
29
30Here is a list :
31
32```
33http://<replace_domain>/
34http://<replace_domain>/?nonope
35http://<replace_domain>/?do=addlink
36http://<replace_domain>/?do=changepasswd
37http://<replace_domain>/?do=changetag
38http://<replace_domain>/?do=configure
39http://<replace_domain>/?do=tools
40http://<replace_domain>/?do=daily
41http://<replace_domain>/?post
42http://<replace_domain>/?do=export
43http://<replace_domain>/?do=import
44http://<replace_domain>/?do=login
45http://<replace_domain>/?do=picwall
46http://<replace_domain>/?do=pluginadmin
47http://<replace_domain>/?do=tagcloud
48http://<replace_domain>/?do=taglist
49```
50
51#### Improve existing translation
52
53In Poedit, click on "Edit a Translation", and from Shaarli's directory open
54`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
55
56The existing list of translatable strings should have been loaded, then click on the "Update" button.
57
58You can start editing the translation.
59
60![poedit-screenshot](images/poedit-1.jpg)
61
62Save when you're done, then you can submit a pull request containing the updated `shaarli.po`.
63
64#### Add a new language
65
66Open Poedit and select "Create New Translation", then from Shaarli's directory open
67`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
68
69Then select the language you want to create.
70
71Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.
72`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
73format in lowercase (e.g. `de` for German).
74
75Then click on the "Update" button, and you can start to translate every available string.
76
77Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
78
79### Extend Shaarli's translation
80
81If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
82but you won't be able to able to override Shaarli's translation.
83
84However, you can add your own translation domain which extends the main translation list.
85
86> Note that you can find a live example of translation extension in the `demo_plugin`.
87
88First, create your translation files tree directory:
89
90```
91<your_module>/languages/<ISO 3166-1 alpha-2 language code>/LC_MESSAGES/
92```
93
94Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
95`my_theme.po`.
96
97Users have to register your extension in their configuration with the parameter
98`translation.extensions.<domain>: <translation files path>`.
99
100Example:
101
102```php
103if (! $conf->exists('translation.extensions.my_theme')) {
104 $conf->set('translation.extensions.my_theme', '<your_module>/languages/');
105 $conf->write(true);
106}
107```
108
109> Note that the page needs to be reloaded after the registration.
110
111It is then recommended to create a custom translation function which will call the `t()` function with your domain.
112For example :
113
114```php
115function my_theme_t($text, $nText = '', $nb = 1)
116{
117 return t($text, $nText, $nb, 'my_theme'); // the last parameter is your translation domain.
118}
119```
120
121All strings which can be translated should be processed through your function:
122
123```php
124my_theme_t('Comment');
125my_theme_t('Comment', 'Comments', 2);
126```
127
128Or in templates:
129
130```php
131{'Comment'|my_theme_t}
132{function="my_theme_t('Comment', 'Comments', 2)"}
133```
134
135> Note than in template, you need to visit your page at least once to generate a cache file.
136
137When you're done, open Poedit and load translation strings from sources:
138
139 1. `File > New`
140 2. Choose your language
141 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
142 4. Go to `Catalog > Properties...`
143 5. Fill the `Translation Properties` tab
144 6. Add your source path in the `Sources Paths` tab
145 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
146
147```
148my_theme_t
149my_theme_t:1,2
150```
151
152Click on the "Update" button and you're free to start your translations!
diff --git a/doc/md/Unit-tests.md b/doc/md/Unit-tests.md
index d200634f..f6030d5c 100644
--- a/doc/md/Unit-tests.md
+++ b/doc/md/Unit-tests.md
@@ -2,12 +2,12 @@
2 2
3The framework used is [PHPUnit](https://phpunit.de/); it can be installed with [Composer](https://getcomposer.org/), which is a dependency management tool. 3The framework used is [PHPUnit](https://phpunit.de/); it can be installed with [Composer](https://getcomposer.org/), which is a dependency management tool.
4 4
5Regarding Composer, you can either use: 5### Install composer
6 6
7- a system-wide version, e.g. installed through your distro's package manager 7You can either use:
8- a local version, downloadable [here](https://getcomposer.org/download/)
9 8
10#### Sample usage 9- a system-wide version, e.g. installed through your distro's package manager
10- a local version, downloadable [here](https://getcomposer.org/download/).
11 11
12```bash 12```bash
13# system-wide version 13# system-wide version
@@ -29,6 +29,8 @@ $ composer update
29 29
30#### Install and enable Xdebug to generate PHPUnit coverage reports 30#### Install and enable Xdebug to generate PHPUnit coverage reports
31 31
32See http://xdebug.org/docs/install
33
32For Debian-based distros: 34For Debian-based distros:
33```bash 35```bash
34$ aptitude install php5-xdebug 36$ aptitude install php5-xdebug
diff --git a/doc/md/Upgrade-and-migration.md b/doc/md/Upgrade-and-migration.md
index b3a08764..1dc07339 100644
--- a/doc/md/Upgrade-and-migration.md
+++ b/doc/md/Upgrade-and-migration.md
@@ -14,7 +14,7 @@ Shaarli stores all user data under the `data` directory:
14- `data/ipbans.php` - banned IP addresses 14- `data/ipbans.php` - banned IP addresses
15- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run 15- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run
16 16
17See [Shaarli configuration](Shaarli configuration) for more information about Shaarli resources. 17See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources.
18 18
19It is recommended to backup this repository _before_ starting updating/upgrading Shaarli: 19It is recommended to backup this repository _before_ starting updating/upgrading Shaarli:
20 20
@@ -27,7 +27,7 @@ As all user data is kept under `data`, this is the only directory you need to wo
27 27
28- backup the `data` directory 28- backup the `data` directory
29- install or update Shaarli: 29- install or update Shaarli:
30 - fresh installation - see [Download and installation](Download and installation) 30 - fresh installation - see [Download and installation](Download-and-installation)
31 - update - see the following sections 31 - update - see the following sections
32- check or restore the `data` directory 32- check or restore the `data` directory
33 33
@@ -35,10 +35,13 @@ As all user data is kept under `data`, this is the only directory you need to wo
35 35
36All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. 36All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.
37 37
38We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download and installation) for `git` complete instructions. 38We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions.
39 39
40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! 40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
41 41
42If you use translations in gettext mode - meaning you manually changed the default mode -,
43reload your web server.
44
42After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details). 45After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details).
43 46
44## Upgrading with Git 47## Upgrading with Git
@@ -72,6 +75,14 @@ Updating dependencies
72 Downloading: 100% 75 Downloading: 100%
73``` 76```
74 77
78Shaarli >= `v0.9.2` supports translations:
79
80```bash
81$ make translate
82```
83
84If you use translations in gettext mode, reload your web server.
85
75### Migrating and upgrading from Sebsauvage's repository 86### Migrating and upgrading from Sebsauvage's repository
76 87
77If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy. 88If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy.
@@ -151,6 +162,14 @@ Updating dependencies
151 Downloading: 100% 162 Downloading: 100%
152``` 163```
153 164
165Shaarli >= `v0.9.2` supports translations:
166
167```bash
168$ make translate
169```
170
171If you use translations in gettext mode, reload your web server.
172
154Optionally, you can delete information related to the legacy version: 173Optionally, you can delete information related to the legacy version:
155 174
156```bash 175```bash
@@ -173,7 +192,7 @@ Total 3317 (delta 2050), reused 3301 (delta 2034)to
173 192
174#### Step 3: configuration 193#### Step 3: configuration
175 194
176After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli configuration) for more details). 195After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration) for more details).
177 196
178## Troubleshooting 197## Troubleshooting
179 198
diff --git a/doc/md/docker/reverse-proxy-configuration.md b/doc/md/docker/reverse-proxy-configuration.md
index 91ffecff..6066140e 100644
--- a/doc/md/docker/reverse-proxy-configuration.md
+++ b/doc/md/docker/reverse-proxy-configuration.md
@@ -1,6 +1,120 @@
1## Foreword
2
3This guide assumes that:
4
5- Shaarli runs in a Docker container
6- The host's `10080` port is mapped to the container's `80` port
7- Shaarli's Fully Qualified Domain Name (FQDN) is `shaarli.domain.tld`
8- HTTP traffic is redirected to HTTPS
9
10## Apache
11
12- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/)
13 - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
14 - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
15
16The following HTTP headers are set by using the `ProxyPass` directive:
17
18- `X-Forwarded-For`
19- `X-Forwarded-Host`
20- `X-Forwarded-Server`
21
22```apache
23<VirtualHost *:80>
24 ServerName shaarli.domain.tld
25 Redirect permanent / https://shaarli.domain.tld
26</VirtualHost>
27
28<VirtualHost *:443>
29 ServerName shaarli.domain.tld
30
31 SSLEngine on
32 SSLCertificateFile /path/to/cert
33 SSLCertificateKeyFile /path/to/certkey
34
35 LogLevel warn
36 ErrorLog /var/log/apache2/shaarli-error.log
37 CustomLog /var/log/apache2/shaarli-access.log combined
38
39 RequestHeader set X-Forwarded-Proto "https"
40
41 ProxyPass / http://127.0.0.1:10080/
42 ProxyPassReverse / http://127.0.0.1:10080/
43</VirtualHost>
44```
1 45
2TODO, see https://github.com/shaarli/Shaarli/issues/888
3 46
4## HAProxy 47## HAProxy
5 48
49- [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/)
50
51```conf
52global
53 [...]
54
55defaults
56 [...]
57
58frontend http-in
59 bind :80
60 redirect scheme https code 301 if !{ ssl_fc }
61
62 bind :443 ssl crt /path/to/cert.pem
63
64 default_backend shaarli
65
66
67backend shaarli
68 mode http
69 option http-server-close
70 option forwardfor
71 reqadd X-Forwarded-Proto: https
72
73 server shaarli1 127.0.0.1:10080
74```
75
76
6## Nginx 77## Nginx
78
79- [Nginx documentation](https://nginx.org/en/docs/)
80
81```nginx
82http {
83 [...]
84
85 index index.html index.php;
86
87 root /home/john/web;
88 access_log /var/log/nginx/access.log;
89 error_log /var/log/nginx/error.log;
90
91 server {
92 listen 80;
93 server_name shaarli.domain.tld;
94 return 301 https://shaarli.domain.tld$request_uri;
95 }
96
97 server {
98 listen 443 ssl http2;
99 server_name shaarli.domain.tld;
100
101 ssl_certificate /path/to/cert
102 ssl_certificate_key /path/to/certkey
103
104 location / {
105 proxy_set_header X-Real-IP $remote_addr;
106 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
107 proxy_set_header X-Forwarded-Proto $scheme;
108 proxy_set_header X-Forwarded-Host $host;
109
110 proxy_pass http://localhost:10080/;
111 proxy_set_header Host $host;
112 proxy_connect_timeout 30s;
113 proxy_read_timeout 120s;
114
115 access_log /var/log/nginx/shaarli.access.log;
116 error_log /var/log/nginx/shaarli.error.log;
117 }
118 }
119}
120```
diff --git a/doc/md/docker/shaarli-images.md b/doc/md/docker/shaarli-images.md
index 6d108d21..12f7b5d1 100644
--- a/doc/md/docker/shaarli-images.md
+++ b/doc/md/docker/shaarli-images.md
@@ -1,3 +1,6 @@
1A brief guide on getting starting using docker is given in [Docker 101](docker-101.md).
2To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](../Upgrade-and-migration.md).
3
1## Get and run a Shaarli image 4## Get and run a Shaarli image
2 5
3### DockerHub repository 6### DockerHub repository
@@ -5,14 +8,24 @@ The images can be found in the [`shaarli/shaarli`](https://hub.docker.com/r/shaa
5repository. 8repository.
6 9
7### Available image tags 10### Available image tags
8- `latest`: master branch (tarball release) 11- `latest`: latest branch (tarball release)
12- `master`: master branch (tarball release)
9- `stable`: stable branch (tarball release) 13- `stable`: stable branch (tarball release)
10 14
11All images rely on: 15The `latest` and `master` images rely on:
16
17- [Alpine Linux](https://www.alpinelinux.org/)
18- [PHP7-FPM](http://php-fpm.org/)
19- [Nginx](http://nginx.org/)
20
21The `stable` image relies on:
22
12- [Debian 8 Jessie](https://hub.docker.com/_/debian/) 23- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
13- [PHP5-FPM](http://php-fpm.org/) 24- [PHP5-FPM](http://php-fpm.org/)
14- [Nginx](http://nginx.org/) 25- [Nginx](http://nginx.org/)
15 26
27Additional [Dockerfiles](https://github.com/shaarli/Shaarli/tree/master/docker) are provided for the `arm32v7` platform, relying on [Linuxserver.io Alpine armhf images](https://hub.docker.com/r/lsiobase/alpine.armhf/). These images must be built using [`docker build`](https://docs.docker.com/engine/reference/commandline/build/) on an `arm32v7` machine or using an emulator such as [qemu](https://resin.io/blog/building-arm-containers-on-any-x86-machine-even-dockerhub/).
28
16### Download from DockerHub 29### Download from DockerHub
17```bash 30```bash
18$ docker pull shaarli/shaarli 31$ docker pull shaarli/shaarli
@@ -69,3 +82,14 @@ backstabbing_galileo
69$ docker ps -a 82$ docker ps -a
70CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 83CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
71``` 84```
85
86### Automatic builds
87
88Docker 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):
89```
90MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
91docker run -ti --rm \
92 -p 8000:80 \
93 -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
94 shaarli/shaarli
95```
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 2b7d0f00..e77b4d3a 100644
--- a/doc/md/index.md
+++ b/doc/md/index.md
@@ -22,20 +22,25 @@ It runs the latest development version of Shaarli and is updated/reset daily.
22 22
23Login: `demo`; Password: `demo` 23Login: `demo`; Password: `demo`
24 24
25Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage):
26```
27MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
28docker run -ti --rm \
29 -p 8000:80 \
30 -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
31 shaarli/shaarli
32```
33
34A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101).
35To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](Upgrade-and-migration) documentation.
36
37## Features 25## Features
38 26
27Shaarli can be used:
28
29- to share, comment and save interesting links and news.
30- to bookmark useful/frequent personal links (as private links) and share them between computers.
31- as a minimal blog/microblog/writing platform (no character limit).
32- as a read-it-later list (for example items tagged `readlater`).
33- to draft and save articles/posts/ideas.
34- to keep code snippets.
35- to keep notes and documentation.
36- as a shared clipboard/notepad/pastebin between machines.
37- as a todo list.
38- to store playlists (e.g. with the `music` or `video` tags).
39- to keep extracts/comments from webpages that may disappear.
40- to keep track of ongoing discussions (for example items tagged `discussion`).
41- [to feed RSS aggregators](http://shaarli.chassegnouf.net/?9Efeiw) (planets) with specific tags.
42- to feed other social networks, blogs... using RSS feeds and external services (dlvr.it, ifttt.com ...).
43
39### Interface 44### Interface
40- minimalist design (simple is beautiful) 45- minimalist design (simple is beautiful)
41- FAST 46- FAST
@@ -89,14 +94,12 @@ Easily extensible by any client using the REST API exposed by Shaarli.
89 94
90See the [API documentation](http://shaarli.github.io/api-documentation/). 95See the [API documentation](http://shaarli.github.io/api-documentation/).
91 96
92### Other usages 97### Using Shaarli as a blog, notepad, pastebin...
93Though Shaarli is primarily a bookmarking application, it can serve other purposes 98- Go to your Shaarli setup and log in
94(see [Features](Features)): 99- Click the `Add Link` button
95 100- To share text only, do not enter any URL in the corresponding input field and click `Add Link`
96- micro-blogging 101- Pick a title and enter your article, or note, in the description field; add a few tags; optionally check `Private` then click `Save`
97- pastebin 102- Voilà! Your article is now published (privately if you selected that option) and accessible using its permalink.
98- online notepad
99- snippet archive
100 103
101## About 104## About
102### Shaarli community fork 105### Shaarli community fork
diff --git a/docker/alpine/Dockerfile.armhf.latest b/docker/alpine/Dockerfile.armhf.latest
new file mode 100644
index 00000000..c923834a
--- /dev/null
+++ b/docker/alpine/Dockerfile.armhf.latest
@@ -0,0 +1,47 @@
1FROM lsiobase/alpine.armhf:3.6
2MAINTAINER Shaarli Community
3
4RUN apk --update --no-cache add \
5 ca-certificates \
6 curl \
7 nginx \
8 php7 \
9 php7-ctype \
10 php7-curl \
11 php7-fpm \
12 php7-gd \
13 php7-iconv \
14 php7-intl \
15 php7-json \
16 php7-mbstring \
17 php7-openssl \
18 php7-phar \
19 php7-session \
20 php7-xml \
21 php7-zlib \
22 s6
23
24COPY nginx.conf /etc/nginx/nginx.conf
25COPY php-fpm.conf /etc/php7/php-fpm.conf
26COPY services.d /etc/services.d
27
28RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
29 && rm -rf /etc/php7/php-fpm.d/www.conf \
30 && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
31 && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
32
33
34WORKDIR /var/www
35RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \
36 && mv Shaarli-latest shaarli \
37 && cd shaarli \
38 && composer --prefer-dist --no-dev install \
39 && rm -rf ~/.composer \
40 && chown -R nginx:nginx .
41
42VOLUME /var/www/shaarli/data
43
44EXPOSE 80
45
46ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
47CMD []
diff --git a/docker/alpine/Dockerfile.armhf.master b/docker/alpine/Dockerfile.armhf.master
new file mode 100644
index 00000000..7f1bdf85
--- /dev/null
+++ b/docker/alpine/Dockerfile.armhf.master
@@ -0,0 +1,47 @@
1FROM lsiobase/alpine.armhf:3.6
2MAINTAINER Shaarli Community
3
4RUN apk --update --no-cache add \
5 ca-certificates \
6 curl \
7 nginx \
8 php7 \
9 php7-ctype \
10 php7-curl \
11 php7-fpm \
12 php7-gd \
13 php7-iconv \
14 php7-intl \
15 php7-json \
16 php7-mbstring \
17 php7-openssl \
18 php7-phar \
19 php7-session \
20 php7-xml \
21 php7-zlib \
22 s6
23
24COPY nginx.conf /etc/nginx/nginx.conf
25COPY php-fpm.conf /etc/php7/php-fpm.conf
26COPY services.d /etc/services.d
27
28RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
29 && rm -rf /etc/php7/php-fpm.d/www.conf \
30 && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
31 && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
32
33
34WORKDIR /var/www
35RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
36 && mv Shaarli-master shaarli \
37 && cd shaarli \
38 && composer --prefer-dist --no-dev install \
39 && rm -rf ~/.composer \
40 && chown -R nginx:nginx .
41
42VOLUME /var/www/shaarli/data
43
44EXPOSE 80
45
46ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
47CMD []
diff --git a/docker/alpine/Dockerfile.latest b/docker/alpine/Dockerfile.latest
new file mode 100644
index 00000000..dd4a173c
--- /dev/null
+++ b/docker/alpine/Dockerfile.latest
@@ -0,0 +1,47 @@
1FROM alpine:3.6
2MAINTAINER Shaarli Community
3
4RUN apk --update --no-cache add \
5 ca-certificates \
6 curl \
7 nginx \
8 php7 \
9 php7-ctype \
10 php7-curl \
11 php7-fpm \
12 php7-gd \
13 php7-iconv \
14 php7-intl \
15 php7-json \
16 php7-mbstring \
17 php7-openssl \
18 php7-phar \
19 php7-session \
20 php7-xml \
21 php7-zlib \
22 s6
23
24COPY nginx.conf /etc/nginx/nginx.conf
25COPY php-fpm.conf /etc/php7/php-fpm.conf
26COPY services.d /etc/services.d
27
28RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
29 && rm -rf /etc/php7/php-fpm.d/www.conf \
30 && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
31 && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
32
33
34WORKDIR /var/www
35RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \
36 && mv Shaarli-latest shaarli \
37 && cd shaarli \
38 && composer --prefer-dist --no-dev install \
39 && rm -rf ~/.composer \
40 && chown -R nginx:nginx .
41
42VOLUME /var/www/shaarli/data
43
44EXPOSE 80
45
46ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
47CMD []
diff --git a/docker/alpine/Dockerfile.master b/docker/alpine/Dockerfile.master
new file mode 100644
index 00000000..58f7c6e7
--- /dev/null
+++ b/docker/alpine/Dockerfile.master
@@ -0,0 +1,47 @@
1FROM alpine:3.6
2MAINTAINER Shaarli Community
3
4RUN apk --update --no-cache add \
5 ca-certificates \
6 curl \
7 nginx \
8 php7 \
9 php7-ctype \
10 php7-curl \
11 php7-fpm \
12 php7-gd \
13 php7-iconv \
14 php7-intl \
15 php7-json \
16 php7-mbstring \
17 php7-openssl \
18 php7-phar \
19 php7-session \
20 php7-xml \
21 php7-zlib \
22 s6
23
24COPY nginx.conf /etc/nginx/nginx.conf
25COPY php-fpm.conf /etc/php7/php-fpm.conf
26COPY services.d /etc/services.d
27
28RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
29 && rm -rf /etc/php7/php-fpm.d/www.conf \
30 && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
31 && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
32
33
34WORKDIR /var/www
35RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
36 && mv Shaarli-master shaarli \
37 && cd shaarli \
38 && composer --prefer-dist --no-dev install \
39 && rm -rf ~/.composer \
40 && chown -R nginx:nginx .
41
42VOLUME /var/www/shaarli/data
43
44EXPOSE 80
45
46ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
47CMD []
diff --git a/docker/alpine/IMAGE.md b/docker/alpine/IMAGE.md
new file mode 100644
index 00000000..a8952257
--- /dev/null
+++ b/docker/alpine/IMAGE.md
@@ -0,0 +1,10 @@
1## Alpine images
2- [Alpine Linux](https://www.alpinelinux.org/)
3- [PHP-FPM](http://php-fpm.org/)
4- [Nginx](http://nginx.org/)
5
6### `shaarli/shaarli:latest`
7- [Shaarli](https://github.com/shaarli/Shaarli), `latest` branch
8
9### `shaarli/shaarli:master`
10- [Shaarli](https://github.com/shaarli/Shaarli), `master` branch
diff --git a/docker/production/stable/nginx.conf b/docker/alpine/nginx.conf
index e8754d9b..07fba33f 100644
--- a/docker/production/stable/nginx.conf
+++ b/docker/alpine/nginx.conf
@@ -1,6 +1,7 @@
1user www-data www-data; 1user nginx nginx;
2daemon off; 2daemon off;
3worker_processes 4; 3worker_processes 4;
4pid /var/run/nginx.pid;
4 5
5events { 6events {
6 worker_connections 768; 7 worker_connections 768;
@@ -59,7 +60,7 @@ http {
59 fastcgi_split_path_info ^(.+\.php)(/.+)$; 60 fastcgi_split_path_info ^(.+\.php)(/.+)$;
60 61
61 # filter and proxy PHP requests to PHP-FPM 62 # filter and proxy PHP requests to PHP-FPM
62 fastcgi_pass unix:/var/run/php5-fpm.sock; 63 fastcgi_pass unix:/var/run/php-fpm.sock;
63 fastcgi_index index.php; 64 fastcgi_index index.php;
64 include fastcgi.conf; 65 include fastcgi.conf;
65 } 66 }
diff --git a/docker/alpine/php-fpm.conf b/docker/alpine/php-fpm.conf
new file mode 100644
index 00000000..0843c164
--- /dev/null
+++ b/docker/alpine/php-fpm.conf
@@ -0,0 +1,16 @@
1[global]
2daemonize = no
3
4[www]
5user = nginx
6group = nginx
7listen.owner = nginx
8listen.group = nginx
9catch_workers_output = yes
10listen = /var/run/php-fpm.sock
11pm = dynamic
12pm.max_children = 20
13pm.start_servers = 1
14pm.min_spare_servers = 1
15pm.max_spare_servers = 3
16pm.max_requests = 2048
diff --git a/docker/alpine/services.d/.s6-svscan/finish b/docker/alpine/services.d/.s6-svscan/finish
new file mode 100755
index 00000000..1dadeeaf
--- /dev/null
+++ b/docker/alpine/services.d/.s6-svscan/finish
@@ -0,0 +1,2 @@
1#!/bin/sh
2/bin/true
diff --git a/docker/alpine/services.d/nginx/run b/docker/alpine/services.d/nginx/run
new file mode 100755
index 00000000..21e7b0d6
--- /dev/null
+++ b/docker/alpine/services.d/nginx/run
@@ -0,0 +1,2 @@
1#!/bin/execlineb -P
2nginx
diff --git a/docker/alpine/services.d/php-fpm/run b/docker/alpine/services.d/php-fpm/run
new file mode 100755
index 00000000..21dd0107
--- /dev/null
+++ b/docker/alpine/services.d/php-fpm/run
@@ -0,0 +1,2 @@
1#!/bin/execlineb -P
2php-fpm7 -F
diff --git a/docker/production/stable/Dockerfile b/docker/debian/Dockerfile.stable
index fc9588b0..fc9588b0 100644
--- a/docker/production/stable/Dockerfile
+++ b/docker/debian/Dockerfile.stable
diff --git a/docker/production/stable/IMAGE.md b/docker/debian/IMAGE.md
index d85b1d7a..d85b1d7a 100644
--- a/docker/production/stable/IMAGE.md
+++ b/docker/debian/IMAGE.md
diff --git a/docker/production/nginx.conf b/docker/debian/nginx.conf
index e8754d9b..e8754d9b 100644
--- a/docker/production/nginx.conf
+++ b/docker/debian/nginx.conf
diff --git a/docker/production/stable/supervised.conf b/docker/debian/supervised.conf
index 5acd9795..5acd9795 100644
--- a/docker/production/stable/supervised.conf
+++ b/docker/debian/supervised.conf
diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile
deleted file mode 100644
index d0509115..00000000
--- a/docker/production/Dockerfile
+++ /dev/null
@@ -1,37 +0,0 @@
1FROM debian:jessie
2MAINTAINER Shaarli Community
3
4ENV TERM dumb
5RUN apt-get update \
6 && apt-get install --no-install-recommends -y \
7 ca-certificates \
8 curl \
9 nginx-light \
10 php5-curl \
11 php5-fpm \
12 php5-gd \
13 php5-intl \
14 supervisor \
15 && apt-get clean
16
17RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini
18RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini
19COPY nginx.conf /etc/nginx/nginx.conf
20COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
21
22ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
23RUN chmod 755 /usr/local/bin/composer
24
25WORKDIR /var/www
26RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
27 && mv Shaarli-master shaarli \
28 && cd shaarli \
29 && composer --prefer-dist --no-dev install
30RUN rm -rf html \
31 && chown -R www-data:www-data .
32
33VOLUME /var/www/shaarli/data
34
35EXPOSE 80
36
37CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
diff --git a/docker/production/IMAGE.md b/docker/production/IMAGE.md
deleted file mode 100644
index 6f827b35..00000000
--- a/docker/production/IMAGE.md
+++ /dev/null
@@ -1,5 +0,0 @@
1## shaarli:latest
2- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
3- [PHP5-FPM](http://php-fpm.org/)
4- [Nginx](http://nginx.org/)
5- [Shaarli](https://github.com/shaarli/Shaarli)
diff --git a/docker/production/supervised.conf b/docker/production/supervised.conf
deleted file mode 100644
index 5acd9795..00000000
--- a/docker/production/supervised.conf
+++ /dev/null
@@ -1,13 +0,0 @@
1[program:php5-fpm]
2command=/usr/sbin/php5-fpm -F
3priority=5
4autostart=true
5autorestart=true
6
7[program:nginx]
8command=/usr/sbin/nginx
9priority=10
10autostart=true
11autorestart=true
12stdout_events_enabled=true
13stderr_events_enabled=true
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
new file mode 100644
index 00000000..323c6111
--- /dev/null
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -0,0 +1,1367 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2017-11-11 10:59+0100\n"
5"PO-Revision-Date: 2017-11-11 11:00+0100\n"
6"Last-Translator: \n"
7"Language-Team: Shaarli\n"
8"Language: fr_FR\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.0.4\n"
13"X-Poedit-Basepath: ../../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-SourceCharset: UTF-8\n"
16"X-Poedit-KeywordsList: t:1,2;t\n"
17"X-Poedit-SearchPath-0: .\n"
18
19#: application/ApplicationUtils.php:153
20#, php-format
21msgid ""
22"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
23"cannot run. Your PHP version has known security vulnerabilities and should "
24"be updated as soon as possible."
25msgstr ""
26"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
27"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
28"connues et devrait être mise à jour au plus tôt."
29
30#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195
31msgid "directory is not readable"
32msgstr "le répertoire n'est pas accessible en lecture"
33
34#: application/ApplicationUtils.php:198
35msgid "directory is not writable"
36msgstr "le répertoire n'est pas accessible en écriture"
37
38#: application/ApplicationUtils.php:216
39msgid "file is not readable"
40msgstr "le fichier n'est pas accessible en lecture"
41
42#: application/ApplicationUtils.php:219
43msgid "file is not writable"
44msgstr "le fichier n'est pas accessible en écriture"
45
46#: application/Cache.php:16
47#, php-format
48msgid "Cannot purge %s: no directory"
49msgstr "Impossible de purger %s: le répertoire n'existe pas"
50
51#: application/FeedBuilder.php:151
52msgid "Direct link"
53msgstr "Liens directs"
54
55#: application/FeedBuilder.php:153
56#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
57#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178
58msgid "Permalink"
59msgstr "Permalien"
60
61#: application/History.php:174
62msgid "History file isn't readable or writable"
63msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
64
65#: application/History.php:185
66msgid "Could not parse history file"
67msgstr "Format incorrect pour le fichier d'historique"
68
69#: application/Languages.php:159
70msgid "Automatic"
71msgstr "Automatique"
72
73#: application/Languages.php:160
74msgid "English"
75msgstr "Anglais"
76
77#: application/Languages.php:161
78msgid "French"
79msgstr "Français"
80
81#: application/LinkDB.php:136
82msgid "You are not authorized to add a link."
83msgstr "Vous n'êtes pas autorisé à ajouter un lien."
84
85#: application/LinkDB.php:139
86msgid "Internal Error: A link should always have an id and URL."
87msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
88
89#: application/LinkDB.php:142
90msgid "You must specify an integer as a key."
91msgstr "Vous devez utiliser un entier comme clé."
92
93#: application/LinkDB.php:145
94msgid "Array offset and link ID must be equal."
95msgstr "La clé du tableau et l'ID du lien doivent être égaux."
96
97#: application/LinkDB.php:251
98#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
99#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
100#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
101#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
102msgid ""
103"The personal, minimalist, super-fast, database free, bookmarking service"
104msgstr ""
105"Le gestionnaire de marque-page personnel, minimaliste, et sans base de "
106"données"
107
108#: application/LinkDB.php:253
109msgid ""
110"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
111"me, you must first login.\n"
112"\n"
113"To learn how to use Shaarli, consult the link \"Documentation\" at the "
114"bottom of this page.\n"
115"\n"
116"You use the community supported version of the original Shaarli project, by "
117"Sebastien Sauvage."
118msgstr ""
119"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me "
120"modifier ou me supprimer, vous devez d'abord vous connecter.\n"
121"\n"
122"Pour apprendre comment utiliser Shaarli, consultez le lien « Documentation » "
123"en bas de page.\n"
124"\n"
125"Vous utilisez la version supportée par la communauté du projet original "
126"Shaarli, de Sébastien Sauvage."
127
128#: application/LinkDB.php:267
129msgid "My secret stuff... - Pastebin.com"
130msgstr "Mes trucs secrets... - Pastebin.com"
131
132#: application/LinkDB.php:269
133msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
134msgstr ""
135"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
136"supprimer aussi."
137
138#: application/LinkFilter.php:452
139msgid "The link you are trying to reach does not exist or has been deleted."
140msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
141
142#: application/NetscapeBookmarkUtils.php:35
143msgid "Invalid export selection:"
144msgstr "Sélection d'export invalide :"
145
146#: application/NetscapeBookmarkUtils.php:81
147#, php-format
148msgid "File %s (%d bytes) "
149msgstr "Le fichier %s (%d octets) "
150
151#: application/NetscapeBookmarkUtils.php:83
152msgid "has an unknown file format. Nothing was imported."
153msgstr "a un format inconnu. Rien n'a été importé."
154
155#: application/NetscapeBookmarkUtils.php:86
156#, php-format
157msgid ""
158"was successfully processed in %d seconds: %d links imported, %d links "
159"overwritten, %d links skipped."
160msgstr ""
161"a été importé avec succès en %d secondes : %d liens importés, %d liens "
162"écrasés, %d liens ignorés."
163
164#: application/PageBuilder.php:167
165msgid "The page you are trying to reach does not exist or has been deleted."
166msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée."
167
168#: application/PageBuilder.php:169
169msgid "404 Not Found"
170msgstr "404 Introuvable"
171
172#: application/PluginManager.php:243
173#, php-format
174msgid "Plugin \"%s\" files not found."
175msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
176
177#: application/Updater.php:76
178msgid "Couldn't retrieve Updater class methods."
179msgstr "Impossible de récupérer les méthodes de la classe Updater."
180
181#: application/Updater.php:493
182msgid "An error occurred while running the update "
183msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
184
185#: application/Updater.php:533
186msgid "Updates file path is not set, can't write updates."
187msgstr ""
188"Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
189"d'écrire les mises à jour."
190
191#: application/Updater.php:538
192msgid "Unable to write updates in "
193msgstr "Impossible d'écrire les mises à jour dans "
194
195#: application/Utils.php:376 tests/UtilsTest.php:340
196msgid "Setting not set"
197msgstr "Paramètre non défini"
198
199#: application/Utils.php:383 tests/UtilsTest.php:338 tests/UtilsTest.php:339
200msgid "Unlimited"
201msgstr "Illimité"
202
203#: application/Utils.php:386 tests/UtilsTest.php:335 tests/UtilsTest.php:336
204#: tests/UtilsTest.php:350
205msgid "B"
206msgstr "o"
207
208#: application/Utils.php:386 tests/UtilsTest.php:329 tests/UtilsTest.php:330
209#: tests/UtilsTest.php:337
210msgid "kiB"
211msgstr "ko"
212
213#: application/Utils.php:386 tests/UtilsTest.php:331 tests/UtilsTest.php:332
214#: tests/UtilsTest.php:348 tests/UtilsTest.php:349
215msgid "MiB"
216msgstr "Mo"
217
218#: application/Utils.php:386 tests/UtilsTest.php:333 tests/UtilsTest.php:334
219msgid "GiB"
220msgstr "Go"
221
222#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121
223msgid ""
224"Shaarli could not create the config file. Please make sure Shaarli has the "
225"right to write in the folder is it installed in."
226msgstr ""
227"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
228"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
229
230#: application/config/ConfigManager.php:135
231msgid "Invalid setting key parameter. String expected, got: "
232msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
233
234#: application/config/exception/MissingFieldConfigException.php:21
235#, php-format
236msgid "Configuration value is required for %s"
237msgstr "Le paramètre %s est obligatoire"
238
239#: application/config/exception/PluginConfigOrderException.php:15
240msgid "An error occurred while trying to save plugins loading order."
241msgstr ""
242"Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions."
243
244#: application/config/exception/UnauthorizedConfigException.php:16
245msgid "You are not authorized to alter config."
246msgstr "Vous n'êtes pas autorisé à modifier la configuration."
247
248#: application/exceptions/IOException.php:19
249msgid "Error accessing"
250msgstr "Une erreur s'est produite en accédant à"
251
252#: index.php:135
253msgid "Shared links on "
254msgstr "Liens partagés sur "
255
256#: index.php:157
257msgid "Insufficient permissions:"
258msgstr "Permissions insuffisantes :"
259
260#: index.php:384
261msgid "I said: NO. You are banned for the moment. Go away."
262msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
263
264#: index.php:449
265msgid "Wrong login/password."
266msgstr "Nom d'utilisateur ou mot de passe incorrects."
267
268#: index.php:1092
269msgid "You are not supposed to change a password on an Open Shaarli."
270msgstr ""
271"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
272
273#: index.php:1097 index.php:1138 index.php:1214 index.php:1244 index.php:1344
274msgid "Wrong token."
275msgstr "Jeton invalide."
276
277#: index.php:1102
278msgid "The old password is not correct."
279msgstr "L'ancien mot de passe est incorrect."
280
281#: index.php:1122
282msgid "Your password has been changed"
283msgstr "Votre mot de passe a été modifié"
284
285#: index.php:1175
286msgid "Configuration was saved."
287msgstr "La configuration a été sauvegardé."
288
289#: index.php:1226
290#, php-format
291msgid "The tag was removed from %d link."
292msgid_plural "The tag was removed from %d links."
293msgstr[0] "Le tag a été supprimé de %d lien."
294msgstr[1] "Le tag a été supprimé de %d liens."
295
296#: index.php:1227
297#, php-format
298msgid "The tag was renamed in %d link."
299msgid_plural "The tag was renamed in %d links."
300msgstr[0] "Le tag a été renommé dans %d lien."
301msgstr[1] "Le tag a été renommé dans %d liens."
302
303#: index.php:1443
304msgid "Note: "
305msgstr "Note : "
306
307#: index.php:1552
308#, php-format
309msgid ""
310"The file you are trying to upload is probably bigger than what this "
311"webserver can accept (%s). Please upload in smaller chunks."
312msgstr ""
313"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
314"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
315"légères."
316
317#: index.php:1972
318#, php-format
319msgid ""
320"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
321"variable \"session.save_path\" is set correctly in your PHP config, and that "
322"you have write access to it.<br>It currently points to %s.<br>On some "
323"browsers, accessing your server via a hostname like 'localhost' or any "
324"custom hostname without a dot causes cookie storage to fail. We recommend "
325"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
326msgstr ""
327"<pre>Les sesssions ne semble pas fonctionner sur ce serveur.<br>Assurez vous "
328"que la variable « session.save_path » est correctement définie dans votre "
329"fichier de configuration PHP, et que vous y avez les droits d'écriture."
330"<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains navigateurs, "
331"accéder à votre serveur depuis un nom d'hôte comme « localhost » ou autre "
332"nom personnalisé sans point '.' entraine l'échec de la sauvegarde des "
333"cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse "
334"IP ou un <em>Fully Qualified Domain Name</em>.<br>"
335
336#: index.php:1982
337msgid "Click to try again."
338msgstr "Cliquer ici pour réessayer."
339
340#: plugins/addlink_toolbar/addlink_toolbar.php:29
341msgid "URI"
342msgstr "URI"
343
344#: plugins/addlink_toolbar/addlink_toolbar.php:33
345#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
346msgid "Add link"
347msgstr "Shaare"
348
349#: plugins/addlink_toolbar/addlink_toolbar.php:50
350msgid "Adds the addlink input on the linklist page."
351msgstr "Ajout le formulaire d'ajout de liens sur la page principale."
352
353#: plugins/archiveorg/archiveorg.php:23
354msgid "View on archive.org"
355msgstr "Voir sur archive.org"
356
357#: plugins/archiveorg/archiveorg.php:36
358msgid "For each link, add an Archive.org icon."
359msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
360
361#: plugins/demo_plugin/demo_plugin.php:469
362msgid ""
363"A demo plugin covering all use cases for template designers and plugin "
364"developers."
365msgstr ""
366"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
367"designers et les développeurs."
368
369#: plugins/isso/isso.php:20
370msgid ""
371"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
372"administration page."
373msgstr ""
374"Erreur de l'extension Isso : Merci de définir le paramètre « ISSO_SERVER » "
375"dans la page d'administration des extensions."
376
377#: plugins/isso/isso.php:63
378msgid "Let visitor comment your shaares on permalinks with Isso."
379msgstr ""
380"Permet aux visiteurs de commenter vos shaares sur les permaliens avec Isso."
381
382#: plugins/isso/isso.php:64
383msgid "Isso server URL (without 'http://')"
384msgstr "URL du serveur Isso (sans 'http://')"
385
386#: plugins/markdown/markdown.php:159
387msgid "Description will be rendered with"
388msgstr "La description sera générée avec"
389
390#: plugins/markdown/markdown.php:160
391msgid "Markdown syntax documentation"
392msgstr "Documentation sur la syntaxe Markdown"
393
394#: plugins/markdown/markdown.php:161
395msgid "Markdown syntax"
396msgstr "la syntaxe Markdown"
397
398#: plugins/markdown/markdown.php:340
399msgid ""
400"Render shaare description with Markdown syntax.<br><strong>Warning</"
401"strong>:\n"
402"If your shaared descriptions contained HTML tags before enabling the "
403"markdown plugin,\n"
404"enabling it might break your page.\n"
405"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
406"markdown#html-rendering\">README</a>."
407msgstr ""
408"Utilise la syntaxe Markdown pour la description des liens."
409"<br><strong>Attention</strong> :\n"
410"Si vous aviez des descriptions contenant du HTML avant d'activer cette "
411"extension,\n"
412"l'activer pourrait déformer vos pages.\n"
413"Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
414"markdown#html-rendering\">README</a>."
415
416#: plugins/piwik/piwik.php:21
417msgid ""
418"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
419"administration page."
420msgstr ""
421"Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et "
422"PIWIK_SITEID dans la page d'administration des extensions."
423
424#: plugins/piwik/piwik.php:70
425msgid "A plugin that adds Piwik tracking code to Shaarli pages."
426msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli."
427
428#: plugins/piwik/piwik.php:71
429msgid "Piwik URL"
430msgstr "URL de Piwik"
431
432#: plugins/piwik/piwik.php:72
433msgid "Piwik site ID"
434msgstr "Site ID de Piwik"
435
436#: plugins/playvideos/playvideos.php:22
437msgid "Video player"
438msgstr "Lecteur vidéo"
439
440#: plugins/playvideos/playvideos.php:25
441msgid "Play Videos"
442msgstr "Jouer les vidéos"
443
444#: plugins/playvideos/playvideos.php:56
445msgid "Add a button in the toolbar allowing to watch all videos."
446msgstr ""
447"Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos."
448
449#: plugins/playvideos/youtube_playlist.js:214
450msgid "plugins/playvideos/jquery-1.11.2.min.js"
451msgstr ""
452
453#: plugins/pubsubhubbub/pubsubhubbub.php:69
454#, php-format
455msgid "Could not publish to PubSubHubbub: %s"
456msgstr "Impossible de publier vers PubSubHubbub : %s"
457
458#: plugins/pubsubhubbub/pubsubhubbub.php:95
459#, php-format
460msgid "Could not post to %s"
461msgstr "Impossible de publier vers %s"
462
463#: plugins/pubsubhubbub/pubsubhubbub.php:99
464#, php-format
465msgid "Bad response from the hub %s"
466msgstr "Mauvaise réponse du hub %s"
467
468#: plugins/pubsubhubbub/pubsubhubbub.php:110
469msgid "Enable PubSubHubbub feed publishing."
470msgstr "Active la publication de flux vers PubSubHubbub."
471
472#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68
473msgid "For each link, add a QRCode icon."
474msgstr "Pour chaque liens, ajouter une icône de QRCode."
475
476#: plugins/wallabag/wallabag.php:21
477msgid ""
478"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
479"plugin administration page."
480msgstr ""
481"Erreur de l'extension Wallabag : Merci de définir le paramètre « "
482"WALLABAG_URL » dans la page d'administration des extensions."
483
484#: plugins/wallabag/wallabag.php:47
485msgid "Save to wallabag"
486msgstr "Sauvegarder dans Wallabag"
487
488#: plugins/wallabag/wallabag.php:69
489msgid "Wallabag API URL"
490msgstr "URL de l'API Wallabag"
491
492#: plugins/wallabag/wallabag.php:70
493msgid "Wallabag API version (1 or 2)"
494msgstr "Version de l'API Wallabag (1 ou 2)"
495
496#: tests/LanguagesTest.php:188 tests/LanguagesTest.php:201
497#: tests/languages/fr/LanguagesFrTest.php:160
498#: tests/languages/fr/LanguagesFrTest.php:173
499#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
500#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81
501msgid "Search"
502msgid_plural "Search"
503msgstr[0] "Rechercher"
504msgstr[1] "Rechercher"
505
506#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
507msgid "Sorry, nothing to see here."
508msgstr "Désolé, il y a rien à voir ici."
509
510#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
511msgid "Shaare a new link"
512msgstr "Partager un nouveau lien"
513
514#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
515msgid "URL or leave empty to post a note"
516msgstr "URL ou laisser vide pour créer une note"
517
518#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
519#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
520msgid "Change password"
521msgstr "Modification du mot de passe"
522
523#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
524msgid "Current password"
525msgstr "Mot de passe actuel"
526
527#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
528msgid "New password"
529msgstr "Nouveau mot de passe"
530
531#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
532msgid "Change"
533msgstr "Changer"
534
535#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
536#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
537msgid "Manage tags"
538msgstr "Gérer les tags"
539
540#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
541#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
542msgid "Tag"
543msgstr "Tag"
544
545#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
546msgid "New name"
547msgstr "Nouveau nom"
548
549#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
550msgid "Case sensitive"
551msgstr "Sensible à la casse"
552
553#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
554msgid "Rename"
555msgstr "Renommer"
556
557#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
558#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
559#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172
560msgid "Delete"
561msgstr "Supprimer"
562
563#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
564msgid "You can also edit tags in the"
565msgstr "Vous pouvez aussi modifier les tags dans la"
566
567#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
568msgid "tag list"
569msgstr "liste des tags"
570
571#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
572msgid "Configure"
573msgstr "Configurer"
574
575#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
576msgid "title"
577msgstr "titre"
578
579#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
580msgid "Home link"
581msgstr "Lien vers l'accueil"
582
583#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
584msgid "Default value"
585msgstr "Valeur par défaut"
586
587#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
588msgid "Theme"
589msgstr "Thème"
590
591#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
592#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
593msgid "Language"
594msgstr "Langue"
595
596#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116
597#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
598msgid "Timezone"
599msgstr "Fuseau horaire"
600
601#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
602#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
603msgid "Continent"
604msgstr "Continent"
605
606#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
607#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
608msgid "City"
609msgstr "Ville"
610
611#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
612msgid "Disable session cookie hijacking protection"
613msgstr "Désactiver la protection contre le détournement de cookies"
614
615#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166
616msgid "Check this if you get disconnected or if your IP address changes often"
617msgstr ""
618"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP "
619"change souvent"
620
621#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
622msgid "Private links by default"
623msgstr "Liens privés par défaut"
624
625#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184
626msgid "All new links are private by default"
627msgstr "Tous les nouveaux liens sont privés par défaut"
628
629#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
630msgid "RSS direct links"
631msgstr "Liens directs dans le flux RSS"
632
633#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200
634msgid "Check this to use direct URL instead of permalink in feeds"
635msgstr ""
636"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
637"dans le flux RSS"
638
639#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
640msgid "Hide public links"
641msgstr "Cacher les liens publics"
642
643#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216
644msgid "Do not show any links if the user is not logged in"
645msgstr "N'afficher aucun lien sans être connecté"
646
647#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231
648#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
649msgid "Check updates"
650msgstr "Vérifier les mises à jour"
651
652#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232
653#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
654msgid "Notify me when a new release is ready"
655msgstr "Me notifier lorsqu'une nouvelle version est disponible"
656
657#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
658#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
659msgid "Enable REST API"
660msgstr "Activer l'API REST"
661
662#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248
663#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
664msgid "Allow third party software to use Shaarli such as mobile application"
665msgstr ""
666"Permets aux applications tierces d'utiliser Shaarli, par exemple les "
667"applications mobiles"
668
669#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
670msgid "API secret"
671msgstr "Clé d'API secrète"
672
673#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
674#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
675#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
676#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
677msgid "Save"
678msgstr "Enregistrer"
679
680#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
681msgid "The Daily Shaarli"
682msgstr "Le Quotidien Shaarli"
683
684#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
685msgid "1 RSS entry per day"
686msgstr "1 entrée RSS par jour"
687
688#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
689msgid "Previous day"
690msgstr "Jour précédent"
691
692#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
693msgid "All links of one day in a single page."
694msgstr "Tous les liens d'un jour sur une page."
695
696#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
697msgid "Next day"
698msgstr "Jour suivant"
699
700#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
701#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
702msgid "Edit"
703msgstr "Modifier"
704
705#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
706#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
707#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
708msgid "Shaare"
709msgstr "Shaare"
710
711#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
712msgid "Created:"
713msgstr "Création :"
714
715#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
716msgid "URL"
717msgstr "URL"
718
719#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
720msgid "Title"
721msgstr "Titre"
722
723#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
724#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
725#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
726#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
727#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
728msgid "Description"
729msgstr "Description"
730
731#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
732msgid "Tags"
733msgstr "Tags"
734
735#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
736#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
737#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
738msgid "Private"
739msgstr "Privé"
740
741#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
742msgid "Apply Changes"
743msgstr "Appliquer les changements"
744
745#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
746msgid "Export Database"
747msgstr "Exporter les données"
748
749#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
750msgid "Selection"
751msgstr "Choisir"
752
753#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
754msgid "All"
755msgstr "Tous"
756
757#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
758msgid "Public"
759msgstr "Publics"
760
761#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
762msgid "Prepend note permalinks with this Shaarli instance's URL"
763msgstr "Préfixer les liens de notes avec l'URL de l'instance de Shaarli"
764
765#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
766msgid "Useful to import bookmarks in a web browser"
767msgstr "Utile pour importer les marques-pages dans un navigateur"
768
769#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
770msgid "Export"
771msgstr "Exporter"
772
773#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
774msgid "Import Database"
775msgstr "Importer des données"
776
777#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
778msgid "Maximum size allowed:"
779msgstr "Taille maximum autorisée :"
780
781#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
782msgid "Visibility"
783msgstr "Visibilité"
784
785#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
786msgid "Use values from the imported file, default to public"
787msgstr ""
788"Utiliser les valeurs présentes dans le fichier d'import, public par défaut"
789
790#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
791msgid "Import all bookmarks as private"
792msgstr "Importer tous les liens comme privés"
793
794#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
795msgid "Import all bookmarks as public"
796msgstr "Importer tous les liens comme publics"
797
798#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
799msgid "Overwrite existing bookmarks"
800msgstr "Remplacer les liens existants"
801
802#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
803msgid "Duplicates based on URL"
804msgstr "Les doublons s'appuient sur les URL"
805
806#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
807msgid "Add default tags"
808msgstr "Ajouter des tags par défaut"
809
810#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
811msgid "Import"
812msgstr "Importer"
813
814#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
815msgid "Install Shaarli"
816msgstr "Installation de Shaarli"
817
818#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
819msgid "It looks like it's the first time you run Shaarli. Please configure it."
820msgstr ""
821"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de "
822"le configurer."
823
824#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
825#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
826#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
827#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
828msgid "Username"
829msgstr "Nom d'utilisateur"
830
831#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
832#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
833#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
834#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148
835msgid "Password"
836msgstr "Mot de passe"
837
838#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
839msgid "Shaarli title"
840msgstr "Titre du Shaarli"
841
842#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
843msgid "My links"
844msgstr "Mes liens"
845
846#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
847msgid "Install"
848msgstr "Installer"
849
850#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
851#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
852msgid "shaare"
853msgid_plural "shaares"
854msgstr[0] "shaare"
855msgstr[1] "shaares"
856
857#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
858#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
859msgid "private link"
860msgid_plural "private links"
861msgstr[0] "lien privé"
862msgstr[1] "liens privés"
863
864#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
865#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
866#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117
867msgid "Search text"
868msgstr "Recherche texte"
869
870#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
871#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
872#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124
873#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
874#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
875#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
876#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
877msgid "Filter by tag"
878msgstr "Filtrer par tag"
879
880#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
881msgid "Nothing found."
882msgstr "Aucun résultat."
883
884#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119
885#, php-format
886msgid "%s result"
887msgid_plural "%s results"
888msgstr[0] "%s résultat"
889msgstr[1] "%s résultats"
890
891#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
892msgid "for"
893msgstr "pour"
894
895#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
896msgid "tagged"
897msgstr "taggé"
898
899#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
900msgid "Remove tag"
901msgstr "Retirer le tag"
902
903#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
904msgid "with status"
905msgstr "avec le statut"
906
907#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
908msgid "without any tag"
909msgstr "sans tag"
910
911#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174
912#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
913#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
914msgid "Fold"
915msgstr "Replier"
916
917#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
918msgid "Edited: "
919msgstr "Modifié : "
920
921#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
922msgid "permalink"
923msgstr "permalien"
924
925#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
926msgid "Add tag"
927msgstr "Ajouter un tag"
928
929#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
930#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7
931msgid "Filters"
932msgstr "Filtres"
933
934#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
935#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12
936msgid "Filter private links"
937msgstr "Filtrer par liens privés"
938
939#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
940#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
941msgid "Filter untagged links"
942msgstr "Filtrer par liens privés"
943
944#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
945#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
946#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:22
947#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:74
948#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
949#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
950msgid "Fold all"
951msgstr "Replier tout"
952
953#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
954#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:67
955msgid "Links per page"
956msgstr "Liens par page"
957
958#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
959msgid ""
960"You have been banned after too many failed login attempts. Try again later."
961msgstr ""
962"Vous avez été banni après trop d'échec d'authentification. Merci de "
963"réessayer plus tard."
964
965#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
966#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
967#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
968#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95
969#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71
970#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95
971msgid "Login"
972msgstr "Connexion"
973
974#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
975#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
976#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
977msgid "Remember me"
978msgstr "Rester connecté"
979
980#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
981#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
982#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
983#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
984msgid "by the Shaarli community"
985msgstr "par la communauté Shaarli"
986
987#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
988#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
989msgid "Documentation"
990msgstr "Documentation"
991
992#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
993#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
994msgid "Expand"
995msgstr "Déplier"
996
997#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
998#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
999msgid "Expand all"
1000msgstr "Déplier tout"
1001
1002#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1003#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
1004msgid "Are you sure you want to delete this link?"
1005msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
1006
1007#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
1008#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
1009msgid "Tools"
1010msgstr "Outils"
1011
1012#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1013#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36
1014#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1015msgid "Tag cloud"
1016msgstr "Nuage de tags"
1017
1018#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
1019#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39
1020msgid "Picture wall"
1021msgstr "Mur d'images"
1022
1023#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1024#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42
1025msgid "Daily"
1026msgstr "Quotidien"
1027
1028#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
1029#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1030#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61
1031#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86
1032msgid "RSS Feed"
1033msgstr "Flux RSS"
1034
1035#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1036#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1037#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66
1038#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102
1039msgid "Logout"
1040msgstr "Déconnexion"
1041
1042#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1043#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
1044msgid "is available"
1045msgstr "est disponible"
1046
1047#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
1048#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176
1049msgid "Error"
1050msgstr "Erreur"
1051
1052#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1053msgid "Picture Wall"
1054msgstr "Mur d'images"
1055
1056#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1057msgid "pics"
1058msgstr "images"
1059
1060#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1061msgid "You need to enable Javascript to change plugin loading order."
1062msgstr ""
1063"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
1064
1065#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
1066#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
1067msgid "Plugin administration"
1068msgstr "Administration des extensions"
1069
1070#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1071msgid "Enabled Plugins"
1072msgstr "Extensions activées"
1073
1074#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
1075#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1076msgid "No plugin enabled."
1077msgstr "Aucune extension activée."
1078
1079#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
1080#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
1081msgid "Disable"
1082msgstr "Désactiver"
1083
1084#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1085#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1086#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98
1087#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1088msgid "Name"
1089msgstr "Nom"
1090
1091#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1092#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1093msgid "Order"
1094msgstr "Ordre"
1095
1096#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1097msgid "Disabled Plugins"
1098msgstr "Extensions désactivées"
1099
1100#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
1101msgid "No plugin disabled."
1102msgstr "Aucune extension désactivée."
1103
1104#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
1105#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
1106msgid "Enable"
1107msgstr "Activer"
1108
1109#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1110msgid "More plugins available"
1111msgstr "Plus d'extensions disponibles"
1112
1113#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
1114msgid "in the documentation"
1115msgstr "dans la documentation"
1116
1117#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
1118msgid "Plugin configuration"
1119msgstr "Configuration des extensions"
1120
1121#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195
1122msgid "No parameter available."
1123msgstr "Aucun paramètre disponible."
1124
1125#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1126#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1127msgid "tags"
1128msgstr "tags"
1129
1130#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1131#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1132msgid "List all links with those tags"
1133msgstr "Lister tous les liens avec ces tags"
1134
1135#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1136msgid "Tag list"
1137msgstr "List des tags"
1138
1139#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1140#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1141msgid "Sort by:"
1142msgstr "Trier par :"
1143
1144#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1145#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
1146msgid "Cloud"
1147msgstr "Nuage"
1148
1149#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6
1150#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
1151msgid "Most used"
1152msgstr "Plus utilisés"
1153
1154#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
1155#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
1156msgid "Alphabetical"
1157msgstr "Alphabétique"
1158
1159#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1160msgid "Settings"
1161msgstr "Paramètres"
1162
1163#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1164msgid "Change Shaarli settings: title, timezone, etc."
1165msgstr "Changer les paramètres de Shaarli : titre, fuseau horaire, etc."
1166
1167#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
1168msgid "Configure your Shaarli"
1169msgstr "Conguration de Shaarli"
1170
1171#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
1172msgid "Enable, disable and configure plugins"
1173msgstr "Activer, désactiver et configurer les extensions"
1174
1175#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1176msgid "Change your password"
1177msgstr "Modification du mot de passe"
1178
1179#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
1180msgid "Rename or delete a tag in all links"
1181msgstr "Rename or delete a tag in all links"
1182
1183#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1184msgid ""
1185"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1186"delicious...)"
1187msgstr ""
1188"Importer des marques pages au format Netscape HTML (comme exportés depuis "
1189"Firefox, Chrome, Opera, delicious...)"
1190
1191#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1192msgid "Import links"
1193msgstr "Importer des liens"
1194
1195#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1196msgid ""
1197"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1198"Opera, delicious...)"
1199msgstr ""
1200"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
1201"Firefox, Chrome, Opera, delicious...)"
1202
1203#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1204msgid "Export database"
1205msgstr "Exporter les données"
1206
1207#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
1208msgid ""
1209"Drag one of these button to your bookmarks toolbar or right-click it and "
1210"\"Bookmark This Link\""
1211msgstr ""
1212"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit "
1213"dessus et « Ajouter aux favoris »"
1214
1215#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
1216msgid "then click on the bookmarklet in any page you want to share."
1217msgstr ""
1218"puis cliquer sur le marque page depuis un site que vous souhaitez partager."
1219
1220#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1221#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100
1222msgid ""
1223"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1224"Link"
1225msgstr ""
1226"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1227"Ajouter aux favoris »"
1228
1229#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
1230msgid "then click ✚Shaare link button in any page you want to share"
1231msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
1232
1233#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1234#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
1235msgid "The selected text is too long, it will be truncated."
1236msgstr "Le texte sélectionné est trop long, il sera tronqué."
1237
1238#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
1239msgid "Shaare link"
1240msgstr "Shaare"
1241
1242#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
1243msgid ""
1244"Then click ✚Add Note button anytime to start composing a private Note (text "
1245"post) to your Shaarli"
1246msgstr ""
1247"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
1248
1249#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
1250msgid "Add Note"
1251msgstr "Ajouter une Note"
1252
1253#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
1254msgid ""
1255"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1256"functionality."
1257msgstr ""
1258"Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette "
1259"fonctionalité."
1260
1261#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1262msgid "Add to"
1263msgstr "Ajouter à"
1264
1265#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
1266msgid "3rd party"
1267msgstr "Applications tierces"
1268
1269#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
1270#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
1271msgid "Plugin"
1272msgstr "Extension"
1273
1274#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
1275#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
1276msgid "plugin"
1277msgstr "extension"
1278
1279#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
1280msgid ""
1281"Drag this link to your bookmarks toolbar, or right-click it and choose "
1282"Bookmark This Link"
1283msgstr ""
1284"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1285"Ajouter aux favoris »"
1286
1287#~ msgid "Redirector"
1288#~ msgstr "Redirecteur"
1289
1290#~ msgid "e. g."
1291#~ msgstr "ex :"
1292
1293#~ msgid "will mask the HTTP_REFERER"
1294#~ msgstr "masque le HTTP_REFERER"
1295
1296#~ msgid ""
1297#~ "An error occurred while parsing JSON configuration file (%s): error code #"
1298#~ "%d"
1299#~ msgstr ""
1300#~ "Une erreur s'est produite lors de la lecture du fichier de configuration "
1301#~ "JSON (%s) : code d'erreur #%d"
1302
1303#~ msgid ""
1304#~ "Please check your JSON syntax (without PHP comment tags) using a JSON "
1305#~ "lint tool such as "
1306#~ msgstr ""
1307#~ "Merci de vérifier la syntaxe JSON (sans les balises de commentaires PHP) "
1308#~ "en utilisant un validateur de JSON tel que "
1309
1310#~ msgid ""
1311#~ "Error: missing Composer dependencies\n"
1312#~ "\n"
1313#~ "If you installed Shaarli through Git or using the development branch,\n"
1314#~ "please refer to the installation documentation to install PHP "
1315#~ "dependencies using Composer:\n"
1316#~ msgstr ""
1317#~ "Erreur : les dépendances Composer sont manquantes\n"
1318#~ "\n"
1319#~ "Si vous avez installé Shaarli avec Git ou depuis la branche de "
1320#~ "développement\n"
1321#~ "merci de consulter la documentation d'installation pour installer les "
1322#~ "dépendances Composer :\n"
1323#~ "\n"
1324
1325#~ msgid "Sessions do not seem to work correctly on your server."
1326#~ msgstr "Les sessions ne semblent "
1327
1328#~ msgid "Tag was renamed in "
1329#~ msgstr "Le tag a été renommé dans "
1330
1331#, fuzzy
1332#~| msgid "My links"
1333#~ msgid " links"
1334#~ msgstr "Mes liens"
1335
1336#, fuzzy
1337#~| msgid ""
1338#~| "Error: missing Composer configuration\n"
1339#~| "\n"
1340#~ msgid "Error: missing Composer configuration"
1341#~ msgstr ""
1342#~ "Erreur : la configuration Composer est manquante\n"
1343#~ "\n"
1344
1345#, fuzzy
1346#~| msgid ""
1347#~| "Shaarli could not create the config file. Please make sure Shaarli has "
1348#~| "the right to write in the folder is it installed in."
1349#~ msgid ""
1350#~ "Shaarli could not create the config file. \n"
1351#~ " Please make sure Shaarli has the right to write in the "
1352#~ "folder is it installed in."
1353#~ msgstr ""
1354#~ "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier "
1355#~ "que Shaarli a les droits d'écriture dans le dossier dans lequel il est "
1356#~ "installé."
1357
1358#, fuzzy
1359#~| msgid "Plugin"
1360#~ msgid "Plugin \""
1361#~ msgstr "Extension"
1362
1363#~ msgid "Your PHP version is obsolete!"
1364#~ msgstr "Votre version de PHP est obsolète !"
1365
1366#~ msgid " Shaarli requires at least PHP "
1367#~ msgstr "Shaarli nécessite au moins PHP"
diff --git a/index.php b/index.php
index c26f50d1..d57789e6 100644
--- a/index.php
+++ b/index.php
@@ -64,7 +64,6 @@ require_once 'application/FeedBuilder.php';
64require_once 'application/FileUtils.php'; 64require_once 'application/FileUtils.php';
65require_once 'application/History.php'; 65require_once 'application/History.php';
66require_once 'application/HttpUtils.php'; 66require_once 'application/HttpUtils.php';
67require_once 'application/Languages.php';
68require_once 'application/LinkDB.php'; 67require_once 'application/LinkDB.php';
69require_once 'application/LinkFilter.php'; 68require_once 'application/LinkFilter.php';
70require_once 'application/LinkUtils.php'; 69require_once 'application/LinkUtils.php';
@@ -76,8 +75,10 @@ require_once 'application/Utils.php';
76require_once 'application/PluginManager.php'; 75require_once 'application/PluginManager.php';
77require_once 'application/Router.php'; 76require_once 'application/Router.php';
78require_once 'application/Updater.php'; 77require_once 'application/Updater.php';
78use \Shaarli\Languages;
79use \Shaarli\ThemeUtils; 79use \Shaarli\ThemeUtils;
80use \Shaarli\Config\ConfigManager; 80use \Shaarli\Config\ConfigManager;
81use \Shaarli\SessionManager;
81 82
82// Ensure the PHP version is supported 83// Ensure the PHP version is supported
83try { 84try {
@@ -115,14 +116,23 @@ if (session_id() == '') {
115} 116}
116 117
117// Regenerate session ID if invalid or not defined in cookie. 118// Regenerate session ID if invalid or not defined in cookie.
118if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) { 119if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
119 session_regenerate_id(true); 120 session_regenerate_id(true);
120 $_COOKIE['shaarli'] = session_id(); 121 $_COOKIE['shaarli'] = session_id();
121} 122}
122 123
123$conf = new ConfigManager(); 124$conf = new ConfigManager();
125$sessionManager = new SessionManager($_SESSION, $conf);
126
127// Sniff browser language and set date format accordingly.
128if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
129 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
130}
131
132new Languages(setlocale(LC_MESSAGES, 0), $conf);
133
124$conf->setEmpty('general.timezone', date_default_timezone_get()); 134$conf->setEmpty('general.timezone', date_default_timezone_get());
125$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); 135$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
126RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory 136RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
127RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 137RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
128 138
@@ -144,7 +154,7 @@ if (! is_file($conf->getConfigFileExt())) {
144 $errors = ApplicationUtils::checkResourcePermissions($conf); 154 $errors = ApplicationUtils::checkResourcePermissions($conf);
145 155
146 if ($errors != array()) { 156 if ($errors != array()) {
147 $message = '<p>Insufficient permissions:</p><ul>'; 157 $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
148 158
149 foreach ($errors as $error) { 159 foreach ($errors as $error) {
150 $message .= '<li>'.$error.'</li>'; 160 $message .= '<li>'.$error.'</li>';
@@ -157,17 +167,12 @@ if (! is_file($conf->getConfigFileExt())) {
157 } 167 }
158 168
159 // Display the installation form if no existing config is found 169 // Display the installation form if no existing config is found
160 install($conf); 170 install($conf, $sessionManager);
161} 171}
162 172
163// a token depending of deployment salt, user password, and the current ip 173// a token depending of deployment salt, user password, and the current ip
164define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); 174define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
165 175
166// Sniff browser language and set date format accordingly.
167if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
168 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
169}
170
171/** 176/**
172 * Checking session state (i.e. is the user still logged in) 177 * Checking session state (i.e. is the user still logged in)
173 * 178 *
@@ -376,9 +381,9 @@ function ban_canLogin($conf)
376// Process login form: Check if login/password is correct. 381// Process login form: Check if login/password is correct.
377if (isset($_POST['login'])) 382if (isset($_POST['login']))
378{ 383{
379 if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.'); 384 if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.'));
380 if (isset($_POST['password']) 385 if (isset($_POST['password'])
381 && tokenOk($_POST['token']) 386 && $sessionManager->checkToken($_POST['token'])
382 && (check_auth($_POST['login'], $_POST['password'], $conf)) 387 && (check_auth($_POST['login'], $_POST['password'], $conf))
383 ) { // Login/password is OK. 388 ) { // Login/password is OK.
384 ban_loginOk($conf); 389 ban_loginOk($conf);
@@ -440,7 +445,8 @@ if (isset($_POST['login']))
440 } 445 }
441 } 446 }
442 } 447 }
443 echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen. 448 // Redirect to login screen.
449 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
444 exit; 450 exit;
445 } 451 }
446} 452}
@@ -451,32 +457,6 @@ if (isset($_POST['login']))
451if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. 457if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session.
452 458
453/** 459/**
454 * Returns a token.
455 *
456 * @param ConfigManager $conf Configuration Manager instance.
457 *
458 * @return string token.
459 */
460function getToken($conf)
461{
462 $rnd = sha1(uniqid('', true) .'_'. mt_rand() . $conf->get('credentials.salt')); // We generate a random string.
463 $_SESSION['tokens'][$rnd]=1; // Store it on the server side.
464 return $rnd;
465}
466
467// Tells if a token is OK. Using this function will destroy the token.
468// true=token is OK.
469function tokenOk($token)
470{
471 if (isset($_SESSION['tokens'][$token]))
472 {
473 unset($_SESSION['tokens'][$token]); // Token is used: destroy it.
474 return true; // Token is OK.
475 }
476 return false; // Wrong token, or already used.
477}
478
479/**
480 * Daily RSS feed: 1 RSS entry per day giving all the links on that day. 460 * Daily RSS feed: 1 RSS entry per day giving all the links on that day.
481 * Gives the last 7 days (which have links). 461 * Gives the last 7 days (which have links).
482 * This RSS feed cannot be filtered. 462 * This RSS feed cannot be filtered.
@@ -546,7 +526,11 @@ function showDailyRSS($conf) {
546 526
547 // We pre-format some fields for proper output. 527 // We pre-format some fields for proper output.
548 foreach ($links as &$link) { 528 foreach ($links as &$link) {
549 $link['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url')); 529 $link['formatedDescription'] = format_description(
530 $link['description'],
531 $conf->get('redirector.url'),
532 $conf->get('redirector.encode_url')
533 );
550 $link['thumbnail'] = thumbnail($conf, $link['url']); 534 $link['thumbnail'] = thumbnail($conf, $link['url']);
551 $link['timestamp'] = $link['created']->getTimestamp(); 535 $link['timestamp'] = $link['created']->getTimestamp();
552 if (startsWith($link['url'], '?')) { 536 if (startsWith($link['url'], '?')) {
@@ -618,7 +602,11 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
618 $taglist = explode(' ',$link['tags']); 602 $taglist = explode(' ',$link['tags']);
619 uasort($taglist, 'strcasecmp'); 603 uasort($taglist, 'strcasecmp');
620 $linksToDisplay[$key]['taglist']=$taglist; 604 $linksToDisplay[$key]['taglist']=$taglist;
621 $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url')); 605 $linksToDisplay[$key]['formatedDescription'] = format_description(
606 $link['description'],
607 $conf->get('redirector.url'),
608 $conf->get('redirector.encode_url')
609 );
622 $linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']); 610 $linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']);
623 $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp(); 611 $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
624 } 612 }
@@ -683,12 +671,13 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
683/** 671/**
684 * Render HTML page (according to URL parameters and user rights) 672 * Render HTML page (according to URL parameters and user rights)
685 * 673 *
686 * @param ConfigManager $conf Configuration Manager instance. 674 * @param ConfigManager $conf Configuration Manager instance.
687 * @param PluginManager $pluginManager Plugin Manager instance, 675 * @param PluginManager $pluginManager Plugin Manager instance,
688 * @param LinkDB $LINKSDB 676 * @param LinkDB $LINKSDB
689 * @param History $history instance 677 * @param History $history instance
678 * @param SessionManager $sessionManager SessionManager instance
690 */ 679 */
691function renderPage($conf, $pluginManager, $LINKSDB, $history) 680function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
692{ 681{
693 $updater = new Updater( 682 $updater = new Updater(
694 read_updates_file($conf->get('resource.updates')), 683 read_updates_file($conf->get('resource.updates')),
@@ -709,7 +698,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
709 die($e->getMessage()); 698 die($e->getMessage());
710 } 699 }
711 700
712 $PAGE = new PageBuilder($conf, $LINKSDB); 701 $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
713 $PAGE->assign('linkcount', count($LINKSDB)); 702 $PAGE->assign('linkcount', count($LINKSDB));
714 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 703 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
715 $PAGE->assign('plugin_errors', $pluginManager->getErrors()); 704 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
@@ -1100,16 +1089,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1100 if ($targetPage == Router::$PAGE_CHANGEPASSWORD) 1089 if ($targetPage == Router::$PAGE_CHANGEPASSWORD)
1101 { 1090 {
1102 if ($conf->get('security.open_shaarli')) { 1091 if ($conf->get('security.open_shaarli')) {
1103 die('You are not supposed to change a password on an Open Shaarli.'); 1092 die(t('You are not supposed to change a password on an Open Shaarli.'));
1104 } 1093 }
1105 1094
1106 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) 1095 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
1107 { 1096 {
1108 if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! 1097 if (!$sessionManager->checkToken($_POST['token'])) die(t('Wrong token.')); // Go away!
1109 1098
1110 // Make sure old password is correct. 1099 // Make sure old password is correct.
1111 $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')); 1100 $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt'));
1112 if ($oldhash!= $conf->get('credentials.hash')) { echo '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; } 1101 if ($oldhash!= $conf->get('credentials.hash')) {
1102 echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
1103 exit;
1104 }
1113 // Save new password 1105 // Save new password
1114 // Salt renders rainbow-tables attacks useless. 1106 // Salt renders rainbow-tables attacks useless.
1115 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); 1107 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
@@ -1127,7 +1119,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1127 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>'; 1119 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
1128 exit; 1120 exit;
1129 } 1121 }
1130 echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>'; 1122 echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
1131 exit; 1123 exit;
1132 } 1124 }
1133 else // show the change password form. 1125 else // show the change password form.
@@ -1142,8 +1134,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1142 { 1134 {
1143 if (!empty($_POST['title']) ) 1135 if (!empty($_POST['title']) )
1144 { 1136 {
1145 if (!tokenOk($_POST['token'])) { 1137 if (!$sessionManager->checkToken($_POST['token'])) {
1146 die('Wrong token.'); // Go away! 1138 die(t('Wrong token.')); // Go away!
1147 } 1139 }
1148 $tz = 'UTC'; 1140 $tz = 'UTC';
1149 if (!empty($_POST['continent']) && !empty($_POST['city']) 1141 if (!empty($_POST['continent']) && !empty($_POST['city'])
@@ -1163,6 +1155,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1163 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); 1155 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1164 $conf->set('api.enabled', !empty($_POST['enableApi'])); 1156 $conf->set('api.enabled', !empty($_POST['enableApi']));
1165 $conf->set('api.secret', escape($_POST['apiSecret'])); 1157 $conf->set('api.secret', escape($_POST['apiSecret']));
1158 $conf->set('translation.language', escape($_POST['language']));
1159
1166 try { 1160 try {
1167 $conf->write(isLoggedIn()); 1161 $conf->write(isLoggedIn());
1168 $history->updateSettings(); 1162 $history->updateSettings();
@@ -1178,7 +1172,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1178 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>'; 1172 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
1179 exit; 1173 exit;
1180 } 1174 }
1181 echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>'; 1175 echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
1182 exit; 1176 exit;
1183 } 1177 }
1184 else // Show the configuration form. 1178 else // Show the configuration form.
@@ -1200,6 +1194,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1200 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false)); 1194 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
1201 $PAGE->assign('api_enabled', $conf->get('api.enabled', true)); 1195 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
1202 $PAGE->assign('api_secret', $conf->get('api.secret')); 1196 $PAGE->assign('api_secret', $conf->get('api.secret'));
1197 $PAGE->assign('languages', Languages::getAvailableLanguages());
1198 $PAGE->assign('language', $conf->get('translation.language'));
1203 $PAGE->renderPage('configure'); 1199 $PAGE->renderPage('configure');
1204 exit; 1200 exit;
1205 } 1201 }
@@ -1214,8 +1210,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1214 exit; 1210 exit;
1215 } 1211 }
1216 1212
1217 if (!tokenOk($_POST['token'])) { 1213 if (!$sessionManager->checkToken($_POST['token'])) {
1218 die('Wrong token.'); 1214 die(t('Wrong token.'));
1219 } 1215 }
1220 1216
1221 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag'])); 1217 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
@@ -1225,9 +1221,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1225 } 1221 }
1226 $delete = empty($_POST['totag']); 1222 $delete = empty($_POST['totag']);
1227 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag'])); 1223 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
1224 $count = count($alteredLinks);
1228 $alert = $delete 1225 $alert = $delete
1229 ? sprintf(t('The tag was removed from %d links.'), count($alteredLinks)) 1226 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
1230 : sprintf(t('The tag was renamed in %d links.'), count($alteredLinks)); 1227 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
1231 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>'; 1228 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
1232 exit; 1229 exit;
1233 } 1230 }
@@ -1243,8 +1240,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1243 if (isset($_POST['save_edit'])) 1240 if (isset($_POST['save_edit']))
1244 { 1241 {
1245 // Go away! 1242 // Go away!
1246 if (! tokenOk($_POST['token'])) { 1243 if (! $sessionManager->checkToken($_POST['token'])) {
1247 die('Wrong token.'); 1244 die(t('Wrong token.'));
1248 } 1245 }
1249 1246
1250 // lf_id should only be present if the link exists. 1247 // lf_id should only be present if the link exists.
@@ -1343,8 +1340,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1343 // -------- User clicked the "Delete" button when editing a link: Delete link from database. 1340 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1344 if ($targetPage == Router::$PAGE_DELETELINK) 1341 if ($targetPage == Router::$PAGE_DELETELINK)
1345 { 1342 {
1346 if (! tokenOk($_GET['token'])) { 1343 if (! $sessionManager->checkToken($_GET['token'])) {
1347 die('Wrong token.'); 1344 die(t('Wrong token.'));
1348 } 1345 }
1349 1346
1350 $ids = trim($_GET['lf_linkdate']); 1347 $ids = trim($_GET['lf_linkdate']);
@@ -1428,22 +1425,16 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1428 // If this is an HTTP(S) link, we try go get the page to extract the title (otherwise we will to straight to the edit form.) 1425 // If this is an HTTP(S) link, we try go get the page to extract the title (otherwise we will to straight to the edit form.)
1429 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) { 1426 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
1430 // Short timeout to keep the application responsive 1427 // Short timeout to keep the application responsive
1431 list($headers, $content) = get_http_response($url, 4); 1428 // The callback will fill $charset and $title with data from the downloaded page.
1432 if (strpos($headers[0], '200 OK') !== false) { 1429 get_http_response($url, 25, 4194304, get_curl_download_callback($charset, $title));
1433 // Retrieve charset. 1430 if (! empty($title) && strtolower($charset) != 'utf-8') {
1434 $charset = get_charset($headers, $content); 1431 $title = mb_convert_encoding($title, 'utf-8', $charset);
1435 // Extract title.
1436 $title = html_extract_title($content);
1437 // Re-encode title in utf-8 if necessary.
1438 if (! empty($title) && strtolower($charset) != 'utf-8') {
1439 $title = mb_convert_encoding($title, 'utf-8', $charset);
1440 }
1441 } 1432 }
1442 } 1433 }
1443 1434
1444 if ($url == '') { 1435 if ($url == '') {
1445 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); 1436 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
1446 $title = $conf->get('general.default_note_title', 'Note: '); 1437 $title = $conf->get('general.default_note_title', t('Note: '));
1447 } 1438 }
1448 $url = escape($url); 1439 $url = escape($url);
1449 $title = escape($title); 1440 $title = escape($title);
@@ -1550,14 +1541,17 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1550 // Import bookmarks from an uploaded file 1541 // Import bookmarks from an uploaded file
1551 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) { 1542 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1552 // The file is too big or some form field may be missing. 1543 // The file is too big or some form field may be missing.
1553 echo '<script>alert("The file you are trying to upload is probably' 1544 $msg = sprintf(
1554 .' bigger than what this webserver can accept (' 1545 t(
1555 .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').' 1546 'The file you are trying to upload is probably bigger than what this webserver can accept'
1556 .' Please upload in smaller chunks.");document.location=\'?do=' 1547 .' (%s). Please upload in smaller chunks.'
1557 .Router::$PAGE_IMPORT .'\';</script>'; 1548 ),
1549 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
1550 );
1551 echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
1558 exit; 1552 exit;
1559 } 1553 }
1560 if (! tokenOk($_POST['token'])) { 1554 if (! $sessionManager->checkToken($_POST['token'])) {
1561 die('Wrong token.'); 1555 die('Wrong token.');
1562 } 1556 }
1563 $status = NetscapeBookmarkUtils::import( 1557 $status = NetscapeBookmarkUtils::import(
@@ -1624,7 +1618,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1624 // Get a fresh token 1618 // Get a fresh token
1625 if ($targetPage == Router::$GET_TOKEN) { 1619 if ($targetPage == Router::$GET_TOKEN) {
1626 header('Content-Type:text/plain'); 1620 header('Content-Type:text/plain');
1627 echo getToken($conf); 1621 echo $sessionManager->generateToken($conf);
1628 exit; 1622 exit;
1629 } 1623 }
1630 1624
@@ -1696,7 +1690,11 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1696 while ($i<$end && $i<count($keys)) 1690 while ($i<$end && $i<count($keys))
1697 { 1691 {
1698 $link = $linksToDisplay[$keys[$i]]; 1692 $link = $linksToDisplay[$keys[$i]];
1699 $link['description'] = format_description($link['description'], $conf->get('redirector.url')); 1693 $link['description'] = format_description(
1694 $link['description'],
1695 $conf->get('redirector.url'),
1696 $conf->get('redirector.encode_url')
1697 );
1700 $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight'; 1698 $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight';
1701 $link['class'] = $link['private'] == 0 ? $classLi : 'private'; 1699 $link['class'] = $link['private'] == 0 ? $classLi : 'private';
1702 $link['timestamp'] = $link['created']->getTimestamp(); 1700 $link['timestamp'] = $link['created']->getTimestamp();
@@ -1950,10 +1948,10 @@ function lazyThumbnail($conf, $url,$href=false)
1950 * Installation 1948 * Installation
1951 * This function should NEVER be called if the file data/config.php exists. 1949 * This function should NEVER be called if the file data/config.php exists.
1952 * 1950 *
1953 * @param ConfigManager $conf Configuration Manager instance. 1951 * @param ConfigManager $conf Configuration Manager instance.
1952 * @param SessionManager $sessionManager SessionManager instance
1954 */ 1953 */
1955function install($conf) 1954function install($conf, $sessionManager) {
1956{
1957 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work. 1955 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
1958 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705); 1956 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
1959 1957
@@ -1962,12 +1960,20 @@ function install($conf)
1962 // (Because on some hosts, session.save_path may not be set correctly, 1960 // (Because on some hosts, session.save_path may not be set correctly,
1963 // or we may not have write access to it.) 1961 // or we may not have write access to it.)
1964 if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) 1962 if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working'))
1965 { // Step 2: Check if data in session is correct. 1963 {
1966 echo '<pre>Sessions do not seem to work correctly on your server.<br>'; 1964 // Step 2: Check if data in session is correct.
1967 echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>'; 1965 $msg = t(
1968 echo 'It currently points to '.session_save_path().'<br>'; 1966 '<pre>Sessions do not seem to work correctly on your server.<br>'.
1969 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>'; 1967 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
1970 echo '<br><a href="?">Click to try again.</a></pre>'; 1968 'and that you have write access to it.<br>'.
1969 'It currently points to %s.<br>'.
1970 'On some browsers, accessing your server via a hostname like \'localhost\' '.
1971 'or any custom hostname without a dot causes cookie storage to fail. '.
1972 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
1973 );
1974 $msg = sprintf($msg, session_save_path());
1975 echo $msg;
1976 echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
1971 die; 1977 die;
1972 } 1978 }
1973 if (!isset($_SESSION['session_tested'])) 1979 if (!isset($_SESSION['session_tested']))
@@ -2000,6 +2006,7 @@ function install($conf)
2000 } else { 2006 } else {
2001 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER))); 2007 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
2002 } 2008 }
2009 $conf->set('translation.language', escape($_POST['language']));
2003 $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); 2010 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
2004 $conf->set('api.enabled', !empty($_POST['enableApi'])); 2011 $conf->set('api.enabled', !empty($_POST['enableApi']));
2005 $conf->set( 2012 $conf->set(
@@ -2027,10 +2034,11 @@ function install($conf)
2027 exit; 2034 exit;
2028 } 2035 }
2029 2036
2030 $PAGE = new PageBuilder($conf); 2037 $PAGE = new PageBuilder($conf, null, $sessionManager->generateToken());
2031 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); 2038 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
2032 $PAGE->assign('continents', $continents); 2039 $PAGE->assign('continents', $continents);
2033 $PAGE->assign('cities', $cities); 2040 $PAGE->assign('cities', $cities);
2041 $PAGE->assign('languages', Languages::getAvailableLanguages());
2034 $PAGE->renderPage('install'); 2042 $PAGE->renderPage('install');
2035 exit; 2043 exit;
2036} 2044}
@@ -2303,7 +2311,7 @@ $response = $app->run(true);
2303if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { 2311if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
2304 // We use UTF-8 for proper international characters handling. 2312 // We use UTF-8 for proper international characters handling.
2305 header('Content-Type: text/html; charset=utf-8'); 2313 header('Content-Type: text/html; charset=utf-8');
2306 renderPage($conf, $pluginManager, $linkDb, $history); 2314 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager);
2307} else { 2315} else {
2308 $app->respond($response); 2316 $app->respond($response);
2309} 2317}
diff --git a/mkdocs.yml b/mkdocs.yml
index 03a7a34e..443c3a08 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -22,16 +22,15 @@ pages:
22 - Reverse proxy configuration: docker/reverse-proxy-configuration.md 22 - Reverse proxy configuration: docker/reverse-proxy-configuration.md
23 - Docker resources: docker/resources.md 23 - Docker resources: docker/resources.md
24- Usage: 24- Usage:
25 - Features: Features.md
26 - Bookmarklet: Bookmarklet.md 25 - Bookmarklet: Bookmarklet.md
27 - Browsing and searching: Browsing-and-searching.md 26 - Browsing and searching: Browsing-and-searching.md
28 - Firefox share: Firefox-share.md 27 - Firefox share: Firefox-share.md
29 - RSS feeds: RSS-feeds.md 28 - RSS feeds: RSS-feeds.md
30 - REST API: REST-API.md 29 - REST API: REST-API.md
30 - Community & Related software: Community-&-Related-software.md
31- How To: 31- How To:
32 - Backup, restore, import and export: Backup,-restore,-import-and-export.md 32 - Backup, restore, import and export: Backup,-restore,-import-and-export.md
33 - Various hacks: Various-hacks.md 33 - Various hacks: Various-hacks.md
34- Troubleshooting: Troubleshooting.md
35- Development: 34- Development:
36 - Development guidelines: Development-guidelines.md 35 - Development guidelines: Development-guidelines.md
37 - Continuous integration tools: Continuous-integration-tools.md 36 - Continuous integration tools: Continuous-integration-tools.md
@@ -43,9 +42,9 @@ pages:
43 - Versioning and Branches: Versioning-and-Branches.md 42 - Versioning and Branches: Versioning-and-Branches.md
44 - Security: Security.md 43 - Security: Security.md
45 - Static analysis: Static-analysis.md 44 - Static analysis: Static-analysis.md
45 - Translations: Translations.md
46 - Theming: Theming.md 46 - Theming: Theming.md
47 - Unit tests: Unit-tests.md 47 - Unit tests: Unit-tests.md
48 - Unit tests inside Docker: Unit-tests-Docker.md 48 - Unit tests inside Docker: Unit-tests-Docker.md
49- About: 49- FAQ: FAQ.md
50 - FAQ: FAQ.md 50- Troubleshooting: Troubleshooting.md
51 - Community & Related software: Community-&-Related-software.md
diff --git a/plugins/TODO.md b/plugins/TODO.md
deleted file mode 100644
index e3313d67..00000000
--- a/plugins/TODO.md
+++ /dev/null
@@ -1,28 +0,0 @@
1https://github.com/shaarli/Shaarli/issues/181 - Add Disqus or Isso comments box on a permalink page
2
3 * http://posativ.org/isso/
4 * install debian package https://packages.debian.org/sid/isso
5 * configure server http://posativ.org/isso/docs/configuration/server/
6 * configure client http://posativ.org/isso/docs/configuration/client/
7 * http://posativ.org/isso/docs/quickstart/ and add `<script data-isso="//comments.example.tld/" src="//comments.example.tld/js/embed.min.js"></script>` to includes.html template; then add `<section id="isso-thread"></section>` in the linklist template where you want the comments (in the linklist_plugins loop for example)
8
9
10Problem: by default, Isso thread ID is guessed from the current url (only one thread per page).
11if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config,
12with data-isso-id being the permalink of an item.
13
14`<section data-isso-id="aH7klxW" id="isso-thread"></section>`
15`data-isso-id: Set a custom thread id, defaults to current URI.`
16
17Problem: feature is currently broken https://github.com/posativ/isso/issues/27
18
19Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread
20when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment
21count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done
22by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server.
23
24Showing all full isso threads in the linklist would destroy layout
25
26-----------------------------------------------------------
27
28http://www.git-attitude.fr/2014/11/04/git-rerere/ for the merge
diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php
index ddf50aaf..8c05a231 100644
--- a/plugins/addlink_toolbar/addlink_toolbar.php
+++ b/plugins/addlink_toolbar/addlink_toolbar.php
@@ -26,11 +26,11 @@ function hook_addlink_toolbar_render_header($data)
26 array( 26 array(
27 'type' => 'text', 27 'type' => 'text',
28 'name' => 'post', 28 'name' => 'post',
29 'placeholder' => 'URI', 29 'placeholder' => t('URI'),
30 ), 30 ),
31 array( 31 array(
32 'type' => 'submit', 32 'type' => 'submit',
33 'value' => 'Add link', 33 'value' => t('Add link'),
34 'class' => 'bigbutton', 34 'class' => 'bigbutton',
35 ), 35 ),
36 ), 36 ),
@@ -40,3 +40,12 @@ function hook_addlink_toolbar_render_header($data)
40 40
41 return $data; 41 return $data;
42} 42}
43
44/**
45 * This function is never called, but contains translation calls for GNU gettext extraction.
46 */
47function addlink_toolbar_dummy_translation()
48{
49 // meta
50 t('Adds the addlink input on the linklist page.');
51}
diff --git a/plugins/archiveorg/archiveorg.html b/plugins/archiveorg/archiveorg.html
index 0781fe35..ad501f47 100644
--- a/plugins/archiveorg/archiveorg.html
+++ b/plugins/archiveorg/archiveorg.html
@@ -1 +1,5 @@
1<span><a href="https://web.archive.org/web/%s"><img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="View on archive.org" alt="archive.org" /></a></span> 1<span>
2 <a href="https://web.archive.org/web/%s">
3 <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
4 </a>
5</span>
diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php
index 03d13d0e..cda35751 100644
--- a/plugins/archiveorg/archiveorg.php
+++ b/plugins/archiveorg/archiveorg.php
@@ -20,9 +20,18 @@ function hook_archiveorg_render_linklist($data)
20 if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) { 20 if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
21 continue; 21 continue;
22 } 22 }
23 $archive = sprintf($archive_html, $value['url']); 23 $archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
24 $value['link_plugin'][] = $archive; 24 $value['link_plugin'][] = $archive;
25 } 25 }
26 26
27 return $data; 27 return $data;
28} 28}
29
30/**
31 * This function is never called, but contains translation calls for GNU gettext extraction.
32 */
33function archiveorg_dummy_translation()
34{
35 // meta
36 t('For each link, add an Archive.org icon.');
37}
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php
index 8fdbf663..b80a2b6d 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -14,6 +14,26 @@
14 * and check user status with _LOGGEDIN_. 14 * and check user status with _LOGGEDIN_.
15 */ 15 */
16 16
17use Shaarli\Config\ConfigManager;
18
19/**
20 * In the footer hook, there is a working example of a translation extension for Shaarli.
21 *
22 * The extension must be attached to a new translation domain (i.e. NOT 'shaarli').
23 * Use case: any custom theme or non official plugin can use the translation system.
24 *
25 * See the documentation for more information.
26 */
27const EXT_TRANSLATION_DOMAIN = 'demo';
28
29/*
30 * This is not necessary, but it's easier if you don't want Poedit to mix up your translations.
31 */
32function demo_plugin_t($text, $nText = '', $nb = 1)
33{
34 return t($text, $nText, $nb, EXT_TRANSLATION_DOMAIN);
35}
36
17/** 37/**
18 * Initialization function. 38 * Initialization function.
19 * It will be called when the plugin is loaded. 39 * It will be called when the plugin is loaded.
@@ -27,6 +47,12 @@ function demo_plugin_init($conf)
27{ 47{
28 $conf->get('toto', 'nope'); 48 $conf->get('toto', 'nope');
29 49
50 if (! $conf->exists('translation.extensions.demo')) {
51 // Custom translation with the domain 'demo'
52 $conf->set('translation.extensions.demo', 'plugins/demo_plugin/languages/');
53 $conf->write(true);
54 }
55
30 $errors[] = 'This a demo init error.'; 56 $errors[] = 'This a demo init error.';
31 return $errors; 57 return $errors;
32} 58}
@@ -160,7 +186,7 @@ function hook_demo_plugin_render_includes($data)
160function hook_demo_plugin_render_footer($data) 186function hook_demo_plugin_render_footer($data)
161{ 187{
162 // footer text 188 // footer text
163 $data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.'; 189 $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
164 190
165 // Free elements at the end of the page. 191 // Free elements at the end of the page.
166 $data['endofpage'][] = '<marquee id="demo_marquee">' . 192 $data['endofpage'][] = '<marquee id="demo_marquee">' .
@@ -433,3 +459,12 @@ function hook_demo_plugin_render_feed($data)
433 } 459 }
434 return $data; 460 return $data;
435} 461}
462
463/**
464 * This function is never called, but contains translation calls for GNU gettext extraction.
465 */
466function demo_dummy_translation()
467{
468 // meta
469 t('A demo plugin covering all use cases for template designers and plugin developers.');
470}
diff --git a/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo
new file mode 100644
index 00000000..0f80f6ed
--- /dev/null
+++ b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo
Binary files differ
diff --git a/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po
new file mode 100644
index 00000000..921379c0
--- /dev/null
+++ b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po
@@ -0,0 +1,21 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Demo plugin\n"
4"POT-Creation-Date: 2017-08-19 10:45+0200\n"
5"PO-Revision-Date: 2017-08-19 11:28+0200\n"
6"Last-Translator: \n"
7"Language-Team: demo\n"
8"Language: fr\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.0.2\n"
13"X-Poedit-Basepath: ../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-KeywordsList: ;demo_plugin_t:1,2;demo_plugin_t\n"
16"X-Poedit-SourceCharset: UTF-8\n"
17"X-Poedit-SearchPath-0: .\n"
18
19#: demo_plugin.php:173
20msgid "Shaarli is now enhanced by the awesome demo_plugin."
21msgstr "Shaarli est maintenant amélioré avec le fantastique demo_plugin."
diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php
index ce16645f..5bc1cce2 100644
--- a/plugins/isso/isso.php
+++ b/plugins/isso/isso.php
@@ -4,10 +4,11 @@
4 * Plugin Isso. 4 * Plugin Isso.
5 */ 5 */
6 6
7use Shaarli\Config\ConfigManager;
8
7/** 9/**
8 * Display an error everywhere if the plugin is enabled without configuration. 10 * Display an error everywhere if the plugin is enabled without configuration.
9 * 11 *
10 * @param $data array List of links
11 * @param $conf ConfigManager instance 12 * @param $conf ConfigManager instance
12 * 13 *
13 * @return mixed - linklist data with Isso plugin. 14 * @return mixed - linklist data with Isso plugin.
@@ -16,8 +17,8 @@ function isso_init($conf)
16{ 17{
17 $issoUrl = $conf->get('plugins.ISSO_SERVER'); 18 $issoUrl = $conf->get('plugins.ISSO_SERVER');
18 if (empty($issoUrl)) { 19 if (empty($issoUrl)) {
19 $error = 'Isso plugin error: '. 20 $error = t('Isso plugin error: '.
20 'Please define the "ISSO_SERVER" setting in the plugin administration page.'; 21 'Please define the "ISSO_SERVER" setting in the plugin administration page.');
21 return array($error); 22 return array($error);
22 } 23 }
23} 24}
@@ -52,3 +53,13 @@ function hook_isso_render_linklist($data, $conf)
52 53
53 return $data; 54 return $data;
54} 55}
56
57/**
58 * This function is never called, but contains translation calls for GNU gettext extraction.
59 */
60function isso_dummy_translation()
61{
62 // meta
63 t('Let visitor comment your shaares on permalinks with Isso.');
64 t('Isso server URL (without \'http://\')');
65}
diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html
index 9c4e5ae0..ded3d347 100644
--- a/plugins/markdown/help.html
+++ b/plugins/markdown/help.html
@@ -1,5 +1,5 @@
1<div class="md_help"> 1<div class="md_help">
2 Description will be rendered with 2 %s
3 <a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation"> 3 <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
4 Markdown syntax</a>. 4 %s</a>.
5</div> 5</div>
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
index 772c56e8..1531549d 100644
--- a/plugins/markdown/markdown.php
+++ b/plugins/markdown/markdown.php
@@ -154,8 +154,13 @@ function hook_markdown_render_includes($data)
154function hook_markdown_render_editlink($data) 154function hook_markdown_render_editlink($data)
155{ 155{
156 // Load help HTML into a string 156 // Load help HTML into a string
157 $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html'); 157 $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
158 158 $translations = [
159 t('Description will be rendered with'),
160 t('Markdown syntax documentation'),
161 t('Markdown syntax'),
162 ];
163 $data['edit_link_plugin'][] = vsprintf($txt, $translations);
159 // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion. 164 // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
160 if (! in_array(NO_MD_TAG, $data['tags'])) { 165 if (! in_array(NO_MD_TAG, $data['tags'])) {
161 $data['tags'][NO_MD_TAG] = 0; 166 $data['tags'][NO_MD_TAG] = 0;
@@ -325,3 +330,15 @@ function process_markdown($description, $escape = true, $allowedProtocols = [])
325 330
326 return $processedDescription; 331 return $processedDescription;
327} 332}
333
334/**
335 * This function is never called, but contains translation calls for GNU gettext extraction.
336 */
337function markdown_dummy_translation()
338{
339 // meta
340 t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
341If your shaared descriptions contained HTML tags before enabling the markdown plugin,
342enabling it might break your page.
343See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
344}
diff --git a/plugins/piwik/piwik.php b/plugins/piwik/piwik.php
index 4a2b48a1..ca00c2be 100644
--- a/plugins/piwik/piwik.php
+++ b/plugins/piwik/piwik.php
@@ -18,8 +18,8 @@ function piwik_init($conf)
18 $piwikUrl = $conf->get('plugins.PIWIK_URL'); 18 $piwikUrl = $conf->get('plugins.PIWIK_URL');
19 $piwikSiteid = $conf->get('plugins.PIWIK_SITEID'); 19 $piwikSiteid = $conf->get('plugins.PIWIK_SITEID');
20 if (empty($piwikUrl) || empty($piwikSiteid)) { 20 if (empty($piwikUrl) || empty($piwikSiteid)) {
21 $error = 'Piwik plugin error: ' . 21 $error = t('Piwik plugin error: ' .
22 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'; 22 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.');
23 return array($error); 23 return array($error);
24 } 24 }
25} 25}
@@ -60,3 +60,14 @@ function hook_piwik_render_footer($data, $conf)
60 60
61 return $data; 61 return $data;
62} 62}
63
64/**
65 * This function is never called, but contains translation calls for GNU gettext extraction.
66 */
67function piwik_dummy_translation()
68{
69 // meta
70 t('A plugin that adds Piwik tracking code to Shaarli pages.');
71 t('Piwik URL');
72 t('Piwik site ID');
73}
diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php
index 64484504..c6d6b0cc 100644
--- a/plugins/playvideos/playvideos.php
+++ b/plugins/playvideos/playvideos.php
@@ -19,10 +19,10 @@ function hook_playvideos_render_header($data)
19 $playvideo = array( 19 $playvideo = array(
20 'attr' => array( 20 'attr' => array(
21 'href' => '#', 21 'href' => '#',
22 'title' => 'Video player', 22 'title' => t('Video player'),
23 'id' => 'playvideos', 23 'id' => 'playvideos',
24 ), 24 ),
25 'html' => 'â–º Play Videos' 25 'html' => 'â–º '. t('Play Videos')
26 ); 26 );
27 $data['buttons_toolbar'][] = $playvideo; 27 $data['buttons_toolbar'][] = $playvideo;
28 } 28 }
@@ -46,3 +46,12 @@ function hook_playvideos_render_footer($data)
46 46
47 return $data; 47 return $data;
48} 48}
49
50/**
51 * This function is never called, but contains translation calls for GNU gettext extraction.
52 */
53function playvideos_dummy_translation()
54{
55 // meta
56 t('Add a button in the toolbar allowing to watch all videos.');
57}
diff --git a/plugins/pubsubhubbub/pubsubhubbub.php b/plugins/pubsubhubbub/pubsubhubbub.php
index 03b6757b..184b588b 100644
--- a/plugins/pubsubhubbub/pubsubhubbub.php
+++ b/plugins/pubsubhubbub/pubsubhubbub.php
@@ -10,6 +10,7 @@
10 */ 10 */
11 11
12use pubsubhubbub\publisher\Publisher; 12use pubsubhubbub\publisher\Publisher;
13use Shaarli\Config\ConfigManager;
13 14
14/** 15/**
15 * Plugin init function - set the hub to the default appspot one. 16 * Plugin init function - set the hub to the default appspot one.
@@ -65,7 +66,7 @@ function hook_pubsubhubbub_save_link($data, $conf)
65 $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL')); 66 $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL'));
66 $p->publish_update($feeds, $httpPost); 67 $p->publish_update($feeds, $httpPost);
67 } catch (Exception $e) { 68 } catch (Exception $e) {
68 error_log('Could not publish to PubSubHubbub: ' . $e->getMessage()); 69 error_log(sprintf(t('Could not publish to PubSubHubbub: %s'), $e->getMessage()));
69 } 70 }
70 71
71 return $data; 72 return $data;
@@ -91,11 +92,20 @@ function nocurl_http_post($url, $postString) {
91 $context = stream_context_create($params); 92 $context = stream_context_create($params);
92 $fp = @fopen($url, 'rb', false, $context); 93 $fp = @fopen($url, 'rb', false, $context);
93 if (!$fp) { 94 if (!$fp) {
94 throw new Exception('Could not post to '. $url); 95 throw new Exception(sprintf(t('Could not post to %s'), $url));
95 } 96 }
96 $response = @stream_get_contents($fp); 97 $response = @stream_get_contents($fp);
97 if ($response === false) { 98 if ($response === false) {
98 throw new Exception('Bad response from the hub '. $url); 99 throw new Exception(sprintf(t('Bad response from the hub %s'), $url));
99 } 100 }
100 return $response; 101 return $response;
101} 102}
103
104/**
105 * This function is never called, but contains translation calls for GNU gettext extraction.
106 */
107function pubsubhubbub_dummy_translation()
108{
109 // meta
110 t('Enable PubSubHubbub feed publishing.');
111}
diff --git a/plugins/qrcode/qrcode.meta b/plugins/qrcode/qrcode.meta
index cbf371ea..1812cd21 100644
--- a/plugins/qrcode/qrcode.meta
+++ b/plugins/qrcode/qrcode.meta
@@ -1 +1 @@
description="For each link, add a QRCode icon ." description="For each link, add a QRCode icon."
diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php
index 8bc610d1..0f96a106 100644
--- a/plugins/qrcode/qrcode.php
+++ b/plugins/qrcode/qrcode.php
@@ -59,3 +59,12 @@ function hook_qrcode_render_includes($data)
59 59
60 return $data; 60 return $data;
61} 61}
62
63/**
64 * This function is never called, but contains translation calls for GNU gettext extraction.
65 */
66function qrcode_dummy_translation()
67{
68 // meta
69 t('For each link, add a QRCode icon.');
70}
diff --git a/plugins/wallabag/wallabag.html b/plugins/wallabag/wallabag.html
index e861536d..4c57691d 100644
--- a/plugins/wallabag/wallabag.html
+++ b/plugins/wallabag/wallabag.html
@@ -1 +1,5 @@
1<span><a href="%s%s" target="_blank"><img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="Save to wallabag" alt="wallabag" /></a></span> 1<span>
2 <a href="%s%s" target="_blank">
3 <img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="%s" alt="wallabag" />
4 </a>
5</span>
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php
index 641e4cc2..9dfd079e 100644
--- a/plugins/wallabag/wallabag.php
+++ b/plugins/wallabag/wallabag.php
@@ -5,6 +5,7 @@
5 */ 5 */
6 6
7require_once 'WallabagInstance.php'; 7require_once 'WallabagInstance.php';
8use Shaarli\Config\ConfigManager;
8 9
9/** 10/**
10 * Init function, return an error if the server is not set. 11 * Init function, return an error if the server is not set.
@@ -17,8 +18,8 @@ function wallabag_init($conf)
17{ 18{
18 $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); 19 $wallabagUrl = $conf->get('plugins.WALLABAG_URL');
19 if (empty($wallabagUrl)) { 20 if (empty($wallabagUrl)) {
20 $error = 'Wallabag plugin error: '. 21 $error = t('Wallabag plugin error: '.
21 'Please define the "WALLABAG_URL" setting in the plugin administration page.'; 22 'Please define the "WALLABAG_URL" setting in the plugin administration page.');
22 return array($error); 23 return array($error);
23 } 24 }
24} 25}
@@ -43,12 +44,14 @@ function hook_wallabag_render_linklist($data, $conf)
43 44
44 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); 45 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
45 46
47 $linkTitle = t('Save to wallabag');
46 foreach ($data['links'] as &$value) { 48 foreach ($data['links'] as &$value) {
47 $wallabag = sprintf( 49 $wallabag = sprintf(
48 $wallabagHtml, 50 $wallabagHtml,
49 $wallabagInstance->getWallabagUrl(), 51 $wallabagInstance->getWallabagUrl(),
50 urlencode($value['url']), 52 urlencode($value['url']),
51 PluginManager::$PLUGINS_PATH 53 PluginManager::$PLUGINS_PATH,
54 $linkTitle
52 ); 55 );
53 $value['link_plugin'][] = $wallabag; 56 $value['link_plugin'][] = $wallabag;
54 } 57 }
@@ -56,3 +59,14 @@ function hook_wallabag_render_linklist($data, $conf)
56 return $data; 59 return $data;
57} 60}
58 61
62/**
63 * This function is never called, but contains translation calls for GNU gettext extraction.
64 */
65function wallabag_dummy_translation()
66{
67 // meta
68 t('For each link, add a QRCode icon.');
69 t('Wallabag API URL');
70 t('Wallabag API version (1 or 2)');
71}
72
diff --git a/shaarli_version.php b/shaarli_version.php
index a92b5619..8cd38931 100644
--- a/shaarli_version.php
+++ b/shaarli_version.php
@@ -1 +1 @@
<?php /* 0.9.3 */ ?> <?php /* 0.9.4 */ ?>
diff --git a/tests/HttpUtils/ServerUrlTest.php b/tests/HttpUtils/ServerUrlTest.php
index dac02b3e..324b827a 100644
--- a/tests/HttpUtils/ServerUrlTest.php
+++ b/tests/HttpUtils/ServerUrlTest.php
@@ -186,4 +186,36 @@ class ServerUrlTest extends PHPUnit_Framework_TestCase
186 ) 186 )
187 ); 187 );
188 } 188 }
189
190 /**
191 * Misconfigured server (see #1022): Proxy HTTP but 443
192 */
193 public function testHttpWithPort433()
194 {
195 $this->assertEquals(
196 'https://host.tld',
197 server_url(
198 array(
199 'HTTPS' => 'Off',
200 'SERVER_NAME' => 'host.tld',
201 'SERVER_PORT' => '80',
202 'HTTP_X_FORWARDED_PROTO' => 'http',
203 'HTTP_X_FORWARDED_PORT' => '443'
204 )
205 )
206 );
207
208 $this->assertEquals(
209 'https://host.tld',
210 server_url(
211 array(
212 'HTTPS' => 'Off',
213 'SERVER_NAME' => 'host.tld',
214 'SERVER_PORT' => '80',
215 'HTTP_X_FORWARDED_PROTO' => 'https, http',
216 'HTTP_X_FORWARDED_PORT' => '443, 80'
217 )
218 )
219 );
220 }
189} 221}
diff --git a/tests/LanguagesTest.php b/tests/LanguagesTest.php
index 79c136c8..864ce630 100644
--- a/tests/LanguagesTest.php
+++ b/tests/LanguagesTest.php
@@ -1,41 +1,203 @@
1<?php 1<?php
2 2
3require_once 'application/Languages.php'; 3namespace Shaarli;
4
5use Shaarli\Config\ConfigManager;
4 6
5/** 7/**
6 * Class LanguagesTest. 8 * Class LanguagesTest.
7 */ 9 */
8class LanguagesTest extends PHPUnit_Framework_TestCase 10class LanguagesTest extends \PHPUnit_Framework_TestCase
9{ 11{
10 /** 12 /**
13 * @var string Config file path (without extension).
14 */
15 protected static $configFile = 'tests/utils/config/configJson';
16
17 /**
18 * @var ConfigManager
19 */
20 protected $conf;
21
22 /**
23 *
24 */
25 public function setUp()
26 {
27 $this->conf = new ConfigManager(self::$configFile);
28 }
29
30 /**
31 * Test t() with a simple non identified value.
32 */
33 public function testTranslateSingleNotIDGettext()
34 {
35 $this->conf->set('translation.mode', 'gettext');
36 new Languages('en', $this->conf);
37 $text = 'abcdé 564 fgK';
38 $this->assertEquals($text, t($text));
39 }
40
41 /**
42 * Test t() with a simple identified value in gettext mode.
43 */
44 public function testTranslateSingleIDGettext()
45 {
46 $this->conf->set('translation.mode', 'gettext');
47 new Languages('en', $this->conf);
48 $text = 'permalink';
49 $this->assertEquals($text, t($text));
50 }
51
52 /**
53 * Test t() with a non identified plural form in gettext mode.
54 */
55 public function testTranslatePluralNotIDGettext()
56 {
57 $this->conf->set('translation.mode', 'gettext');
58 new Languages('en', $this->conf);
59 $text = 'sandwich';
60 $nText = 'sandwiches';
61 $this->assertEquals('sandwiches', t($text, $nText, 0));
62 $this->assertEquals('sandwich', t($text, $nText, 1));
63 $this->assertEquals('sandwiches', t($text, $nText, 2));
64 }
65
66 /**
67 * Test t() with an identified plural form in gettext mode.
68 */
69 public function testTranslatePluralIDGettext()
70 {
71 $this->conf->set('translation.mode', 'gettext');
72 new Languages('en', $this->conf);
73 $text = 'shaare';
74 $nText = 'shaares';
75 // In english, zero is followed by plural form
76 $this->assertEquals('shaares', t($text, $nText, 0));
77 $this->assertEquals('shaare', t($text, $nText, 1));
78 $this->assertEquals('shaares', t($text, $nText, 2));
79 }
80
81 /**
11 * Test t() with a simple non identified value. 82 * Test t() with a simple non identified value.
12 */ 83 */
13 public function testTranslateSingleNotID() 84 public function testTranslateSingleNotIDPhp()
14 { 85 {
86 $this->conf->set('translation.mode', 'php');
87 new Languages('en', $this->conf);
15 $text = 'abcdé 564 fgK'; 88 $text = 'abcdé 564 fgK';
16 $this->assertEquals($text, t($text)); 89 $this->assertEquals($text, t($text));
17 } 90 }
18 91
19 /** 92 /**
20 * Test t() with a non identified plural form. 93 * Test t() with a simple identified value in PHP mode.
21 */ 94 */
22 public function testTranslatePluralNotID() 95 public function testTranslateSingleIDPhp()
23 { 96 {
24 $text = '%s sandwich'; 97 $this->conf->set('translation.mode', 'php');
25 $nText = '%s sandwiches'; 98 new Languages('en', $this->conf);
26 $this->assertEquals('0 sandwich', t($text, $nText)); 99 $text = 'permalink';
27 $this->assertEquals('1 sandwich', t($text, $nText, 1)); 100 $this->assertEquals($text, t($text));
28 $this->assertEquals('2 sandwiches', t($text, $nText, 2));
29 } 101 }
30 102
31 /** 103 /**
32 * Test t() with a non identified invalid plural form. 104 * Test t() with a non identified plural form in PHP mode.
33 */ 105 */
34 public function testTranslatePluralNotIDInvalid() 106 public function testTranslatePluralNotIDPhp()
35 { 107 {
108 $this->conf->set('translation.mode', 'php');
109 new Languages('en', $this->conf);
36 $text = 'sandwich'; 110 $text = 'sandwich';
37 $nText = 'sandwiches'; 111 $nText = 'sandwiches';
112 $this->assertEquals('sandwiches', t($text, $nText, 0));
38 $this->assertEquals('sandwich', t($text, $nText, 1)); 113 $this->assertEquals('sandwich', t($text, $nText, 1));
39 $this->assertEquals('sandwiches', t($text, $nText, 2)); 114 $this->assertEquals('sandwiches', t($text, $nText, 2));
40 } 115 }
116
117 /**
118 * Test t() with an identified plural form in PHP mode.
119 */
120 public function testTranslatePluralIDPhp()
121 {
122 $this->conf->set('translation.mode', 'php');
123 new Languages('en', $this->conf);
124 $text = 'shaare';
125 $nText = 'shaares';
126 // In english, zero is followed by plural form
127 $this->assertEquals('shaares', t($text, $nText, 0));
128 $this->assertEquals('shaare', t($text, $nText, 1));
129 $this->assertEquals('shaares', t($text, $nText, 2));
130 }
131
132 /**
133 * Test t() with an invalid language set in the configuration in gettext mode.
134 */
135 public function testTranslateWithInvalidConfLanguageGettext()
136 {
137 $this->conf->set('translation.mode', 'gettext');
138 $this->conf->set('translation.language', 'nope');
139 new Languages('fr', $this->conf);
140 $text = 'grumble';
141 $this->assertEquals($text, t($text));
142 }
143
144 /**
145 * Test t() with an invalid language set in the configuration in PHP mode.
146 */
147 public function testTranslateWithInvalidConfLanguagePhp()
148 {
149 $this->conf->set('translation.mode', 'php');
150 $this->conf->set('translation.language', 'nope');
151 new Languages('fr', $this->conf);
152 $text = 'grumble';
153 $this->assertEquals($text, t($text));
154 }
155
156 /**
157 * Test t() with an invalid language set with auto language in gettext mode.
158 */
159 public function testTranslateWithInvalidAutoLanguageGettext()
160 {
161 $this->conf->set('translation.mode', 'gettext');
162 new Languages('nope', $this->conf);
163 $text = 'grumble';
164 $this->assertEquals($text, t($text));
165 }
166
167 /**
168 * Test t() with an invalid language set with auto language in PHP mode.
169 */
170 public function testTranslateWithInvalidAutoLanguagePhp()
171 {
172 $this->conf->set('translation.mode', 'php');
173 new Languages('nope', $this->conf);
174 $text = 'grumble';
175 $this->assertEquals($text, t($text));
176 }
177
178 /**
179 * Test t() with an extension language file in gettext mode
180 */
181 public function testTranslationExtensionGettext()
182 {
183 $this->conf->set('translation.mode', 'gettext');
184 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
185 new Languages('en', $this->conf);
186 $txt = 'car'; // ignore me poedit
187 $this->assertEquals('car', t($txt, $txt, 1, 'test'));
188 $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
189 }
190
191 /**
192 * Test t() with an extension language file in PHP mode
193 */
194 public function testTranslationExtensionPhp()
195 {
196 $this->conf->set('translation.mode', 'php');
197 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
198 new Languages('en', $this->conf);
199 $txt = 'car'; // ignore me poedit
200 $this->assertEquals('car', t($txt, $txt, 1, 'test'));
201 $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
202 }
41} 203}
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php
index d796d3a3..9cd6dbd4 100644
--- a/tests/LinkFilterTest.php
+++ b/tests/LinkFilterTest.php
@@ -8,6 +8,10 @@ require_once 'application/LinkFilter.php';
8class LinkFilterTest extends PHPUnit_Framework_TestCase 8class LinkFilterTest extends PHPUnit_Framework_TestCase
9{ 9{
10 /** 10 /**
11 * @var string Test datastore path.
12 */
13 protected static $testDatastore = 'sandbox/datastore.php';
14 /**
11 * @var LinkFilter instance. 15 * @var LinkFilter instance.
12 */ 16 */
13 protected static $linkFilter; 17 protected static $linkFilter;
@@ -18,12 +22,19 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
18 protected static $refDB; 22 protected static $refDB;
19 23
20 /** 24 /**
25 * @var LinkDB instance
26 */
27 protected static $linkDB;
28
29 /**
21 * Instanciate linkFilter with ReferenceLinkDB data. 30 * Instanciate linkFilter with ReferenceLinkDB data.
22 */ 31 */
23 public static function setUpBeforeClass() 32 public static function setUpBeforeClass()
24 { 33 {
25 self::$refDB = new ReferenceLinkDB(); 34 self::$refDB = new ReferenceLinkDB();
26 self::$linkFilter = new LinkFilter(self::$refDB->getLinks()); 35 self::$refDB->write(self::$testDatastore);
36 self::$linkDB = new LinkDB(self::$testDatastore, true, false);
37 self::$linkFilter = new LinkFilter(self::$linkDB);
27 } 38 }
28 39
29 /** 40 /**
diff --git a/tests/LinkUtilsTest.php b/tests/LinkUtilsTest.php
index c77922ec..7fbd59b0 100644
--- a/tests/LinkUtilsTest.php
+++ b/tests/LinkUtilsTest.php
@@ -29,27 +29,13 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
29 } 29 }
30 30
31 /** 31 /**
32 * Test get_charset() with all priorities.
33 */
34 public function testGetCharset()
35 {
36 $headers = array('Content-Type' => 'text/html; charset=Headers');
37 $html = '<html><meta>stuff</meta><meta charset="Html"/></html>';
38 $default = 'default';
39 $this->assertEquals('headers', get_charset($headers, $html, $default));
40 $this->assertEquals('html', get_charset(array(), $html, $default));
41 $this->assertEquals($default, get_charset(array(), '', $default));
42 $this->assertEquals('utf-8', get_charset(array(), ''));
43 }
44
45 /**
46 * Test headers_extract_charset() when the charset is found. 32 * Test headers_extract_charset() when the charset is found.
47 */ 33 */
48 public function testHeadersExtractExistentCharset() 34 public function testHeadersExtractExistentCharset()
49 { 35 {
50 $charset = 'x-MacCroatian'; 36 $charset = 'x-MacCroatian';
51 $headers = array('Content-Type' => 'text/html; charset='. $charset); 37 $headers = 'text/html; charset='. $charset;
52 $this->assertEquals(strtolower($charset), headers_extract_charset($headers)); 38 $this->assertEquals(strtolower($charset), header_extract_charset($headers));
53 } 39 }
54 40
55 /** 41 /**
@@ -57,11 +43,11 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
57 */ 43 */
58 public function testHeadersExtractNonExistentCharset() 44 public function testHeadersExtractNonExistentCharset()
59 { 45 {
60 $headers = array(); 46 $headers = '';
61 $this->assertFalse(headers_extract_charset($headers)); 47 $this->assertFalse(header_extract_charset($headers));
62 48
63 $headers = array('Content-Type' => 'text/html'); 49 $headers = 'text/html';
64 $this->assertFalse(headers_extract_charset($headers)); 50 $this->assertFalse(header_extract_charset($headers));
65 } 51 }
66 52
67 /** 53 /**
@@ -86,6 +72,131 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
86 } 72 }
87 73
88 /** 74 /**
75 * Test the download callback with valid value
76 */
77 public function testCurlDownloadCallbackOk()
78 {
79 $callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_ok');
80 $data = [
81 'HTTP/1.1 200 OK',
82 'Server: GitHub.com',
83 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
84 'Content-Type: text/html; charset=utf-8',
85 'Status: 200 OK',
86 'end' => 'th=device-width"><title>Refactoring · GitHub</title><link rel="search" type="application/opensea',
87 '<title>ignored</title>',
88 ];
89 foreach ($data as $key => $line) {
90 $ignore = null;
91 $expected = $key !== 'end' ? strlen($line) : false;
92 $this->assertEquals($expected, $callback($ignore, $line));
93 if ($expected === false) {
94 break;
95 }
96 }
97 $this->assertEquals('utf-8', $charset);
98 $this->assertEquals('Refactoring · GitHub', $title);
99 }
100
101 /**
102 * Test the download callback with valid values and no charset
103 */
104 public function testCurlDownloadCallbackOkNoCharset()
105 {
106 $callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_no_charset');
107 $data = [
108 'HTTP/1.1 200 OK',
109 'end' => 'th=device-width"><title>Refactoring · GitHub</title><link rel="search" type="application/opensea',
110 '<title>ignored</title>',
111 ];
112 foreach ($data as $key => $line) {
113 $ignore = null;
114 $this->assertEquals(strlen($line), $callback($ignore, $line));
115 }
116 $this->assertEmpty($charset);
117 $this->assertEquals('Refactoring · GitHub', $title);
118 }
119
120 /**
121 * Test the download callback with valid values and no charset
122 */
123 public function testCurlDownloadCallbackOkHtmlCharset()
124 {
125 $callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_no_charset');
126 $data = [
127 'HTTP/1.1 200 OK',
128 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
129 'end' => 'th=device-width"><title>Refactoring · GitHub</title><link rel="search" type="application/opensea',
130 '<title>ignored</title>',
131 ];
132 foreach ($data as $key => $line) {
133 $ignore = null;
134 $expected = $key !== 'end' ? strlen($line) : false;
135 $this->assertEquals($expected, $callback($ignore, $line));
136 if ($expected === false) {
137 break;
138 }
139 }
140 $this->assertEquals('utf-8', $charset);
141 $this->assertEquals('Refactoring · GitHub', $title);
142 }
143
144 /**
145 * Test the download callback with valid values and no title
146 */
147 public function testCurlDownloadCallbackOkNoTitle()
148 {
149 $callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_ok');
150 $data = [
151 'HTTP/1.1 200 OK',
152 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea',
153 'ignored',
154 ];
155 foreach ($data as $key => $line) {
156 $ignore = null;
157 $this->assertEquals(strlen($line), $callback($ignore, $line));
158 }
159 $this->assertEquals('utf-8', $charset);
160 $this->assertEmpty($title);
161 }
162
163 /**
164 * Test the download callback with an invalid content type.
165 */
166 public function testCurlDownloadCallbackInvalidContentType()
167 {
168 $callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_ct_ko');
169 $ignore = null;
170 $this->assertFalse($callback($ignore, ''));
171 $this->assertEmpty($charset);
172 $this->assertEmpty($title);
173 }
174
175 /**
176 * Test the download callback with an invalid response code.
177 */
178 public function testCurlDownloadCallbackInvalidResponseCode()
179 {
180 $callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_rc_ko');
181 $ignore = null;
182 $this->assertFalse($callback($ignore, ''));
183 $this->assertEmpty($charset);
184 $this->assertEmpty($title);
185 }
186
187 /**
188 * Test the download callback with an invalid content type and response code.
189 */
190 public function testCurlDownloadCallbackInvalidContentTypeAndResponseCode()
191 {
192 $callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_rs_ct_ko');
193 $ignore = null;
194 $this->assertFalse($callback($ignore, ''));
195 $this->assertEmpty($charset);
196 $this->assertEmpty($title);
197 }
198
199 /**
89 * Test count_private. 200 * Test count_private.
90 */ 201 */
91 public function testCountPrivateLinks() 202 public function testCountPrivateLinks()
@@ -131,6 +242,21 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
131 } 242 }
132 243
133 /** 244 /**
245 * Test text2clickable a redirector set and without URL encode.
246 */
247 public function testText2clickableWithRedirectorDontEncode()
248 {
249 $text = 'stuff http://hello.there/?is=someone&or=something#here otherstuff';
250 $redirector = 'http://redirector.to';
251 $expectedText = 'stuff <a href="'.
252 $redirector .
253 'http://hello.there/?is=someone&or=something#here' .
254 '">http://hello.there/?is=someone&or=something#here</a> otherstuff';
255 $processedText = text2clickable($text, $redirector, false);
256 $this->assertEquals($expectedText, $processedText);
257 }
258
259 /**
134 * Test testSpace2nbsp. 260 * Test testSpace2nbsp.
135 */ 261 */
136 public function testSpace2nbsp() 262 public function testSpace2nbsp()
@@ -192,3 +318,96 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
192 return str_replace('$1', $hashtag, $hashtagLink); 318 return str_replace('$1', $hashtag, $hashtagLink);
193 } 319 }
194} 320}
321
322// old style mock: PHPUnit doesn't allow function mock
323
324/**
325 * Returns code 200 or html content type.
326 *
327 * @param resource $ch cURL resource
328 * @param int $type cURL info type
329 *
330 * @return int|string 200 or 'text/html'
331 */
332function ut_curl_getinfo_ok($ch, $type)
333{
334 switch ($type) {
335 case CURLINFO_RESPONSE_CODE:
336 return 200;
337 case CURLINFO_CONTENT_TYPE:
338 return 'text/html; charset=utf-8';
339 }
340}
341
342/**
343 * Returns code 200 or html content type without charset.
344 *
345 * @param resource $ch cURL resource
346 * @param int $type cURL info type
347 *
348 * @return int|string 200 or 'text/html'
349 */
350function ut_curl_getinfo_no_charset($ch, $type)
351{
352 switch ($type) {
353 case CURLINFO_RESPONSE_CODE:
354 return 200;
355 case CURLINFO_CONTENT_TYPE:
356 return 'text/html';
357 }
358}
359
360/**
361 * Invalid response code.
362 *
363 * @param resource $ch cURL resource
364 * @param int $type cURL info type
365 *
366 * @return int|string 404 or 'text/html'
367 */
368function ut_curl_getinfo_rc_ko($ch, $type)
369{
370 switch ($type) {
371 case CURLINFO_RESPONSE_CODE:
372 return 404;
373 case CURLINFO_CONTENT_TYPE:
374 return 'text/html; charset=utf-8';
375 }
376}
377
378/**
379 * Invalid content type.
380 *
381 * @param resource $ch cURL resource
382 * @param int $type cURL info type
383 *
384 * @return int|string 200 or 'text/plain'
385 */
386function ut_curl_getinfo_ct_ko($ch, $type)
387{
388 switch ($type) {
389 case CURLINFO_RESPONSE_CODE:
390 return 200;
391 case CURLINFO_CONTENT_TYPE:
392 return 'text/plain';
393 }
394}
395
396/**
397 * Invalid response code and content type.
398 *
399 * @param resource $ch cURL resource
400 * @param int $type cURL info type
401 *
402 * @return int|string 404 or 'text/plain'
403 */
404function ut_curl_getinfo_rs_ct_ko($ch, $type)
405{
406 switch ($type) {
407 case CURLINFO_RESPONSE_CODE:
408 return 404;
409 case CURLINFO_CONTENT_TYPE:
410 return 'text/plain';
411 }
412}
413
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..aa75962a
--- /dev/null
+++ b/tests/SessionManagerTest.php
@@ -0,0 +1,149 @@
1<?php
2require_once 'tests/utils/FakeConfigManager.php';
3
4// Initialize reference data _before_ PHPUnit starts a session
5require_once 'tests/utils/ReferenceSessionIdHashes.php';
6ReferenceSessionIdHashes::genAllHashes();
7
8use \Shaarli\SessionManager;
9use \PHPUnit\Framework\TestCase;
10
11
12/**
13 * Test coverage for SessionManager
14 */
15class SessionManagerTest extends TestCase
16{
17 // Session ID hashes
18 protected static $sidHashes = null;
19
20 // Fake ConfigManager
21 protected static $conf = null;
22
23 /**
24 * Assign reference data
25 */
26 public static function setUpBeforeClass()
27 {
28 self::$sidHashes = ReferenceSessionIdHashes::getHashes();
29 self::$conf = new FakeConfigManager();
30 }
31
32 /**
33 * Generate a session token
34 */
35 public function testGenerateToken()
36 {
37 $session = [];
38 $sessionManager = new SessionManager($session, self::$conf);
39
40 $token = $sessionManager->generateToken();
41
42 $this->assertEquals(1, $session['tokens'][$token]);
43 $this->assertEquals(40, strlen($token));
44 }
45
46 /**
47 * Check a session token
48 */
49 public function testCheckToken()
50 {
51 $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
52 $session = [
53 'tokens' => [
54 $token => 1,
55 ],
56 ];
57 $sessionManager = new SessionManager($session, self::$conf);
58
59 // check and destroy the token
60 $this->assertTrue($sessionManager->checkToken($token));
61 $this->assertFalse(isset($session['tokens'][$token]));
62
63 // ensure the token has been destroyed
64 $this->assertFalse($sessionManager->checkToken($token));
65 }
66
67 /**
68 * Generate and check a session token
69 */
70 public function testGenerateAndCheckToken()
71 {
72 $session = [];
73 $sessionManager = new SessionManager($session, self::$conf);
74
75 $token = $sessionManager->generateToken();
76
77 // ensure a token has been generated
78 $this->assertEquals(1, $session['tokens'][$token]);
79 $this->assertEquals(40, strlen($token));
80
81 // check and destroy the token
82 $this->assertTrue($sessionManager->checkToken($token));
83 $this->assertFalse(isset($session['tokens'][$token]));
84
85 // ensure the token has been destroyed
86 $this->assertFalse($sessionManager->checkToken($token));
87 }
88
89 /**
90 * Check an invalid session token
91 */
92 public function testCheckInvalidToken()
93 {
94 $session = [];
95 $sessionManager = new SessionManager($session, self::$conf);
96
97 $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
98 }
99
100 /**
101 * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
102 *
103 * This tests extensively covers all hash algorithms / bit representations
104 */
105 public function testIsAnyHashSessionIdValid()
106 {
107 foreach (self::$sidHashes as $algo => $bpcs) {
108 foreach ($bpcs as $bpc => $hash) {
109 $this->assertTrue(SessionManager::checkId($hash));
110 }
111 }
112 }
113
114 /**
115 * Test checkId with a valid ID - SHA-1 hashes
116 */
117 public function testIsSha1SessionIdValid()
118 {
119 $this->assertTrue(SessionManager::checkId(sha1('shaarli')));
120 }
121
122 /**
123 * Test checkId with a valid ID - SHA-256 hashes
124 */
125 public function testIsSha256SessionIdValid()
126 {
127 $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
128 }
129
130 /**
131 * Test checkId with a valid ID - SHA-512 hashes
132 */
133 public function testIsSha512SessionIdValid()
134 {
135 $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
136 }
137
138 /**
139 * Test checkId with invalid IDs.
140 */
141 public function testIsSessionIdInvalid()
142 {
143 $this->assertFalse(SessionManager::checkId(''));
144 $this->assertFalse(SessionManager::checkId([]));
145 $this->assertFalse(
146 SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
147 );
148 }
149}
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index 3d1aa653..6cd37a7a 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -5,10 +5,6 @@
5 5
6require_once 'application/Utils.php'; 6require_once 'application/Utils.php';
7require_once 'application/Languages.php'; 7require_once 'application/Languages.php';
8require_once 'tests/utils/ReferenceSessionIdHashes.php';
9
10// Initialize reference data before PHPUnit starts a session
11ReferenceSessionIdHashes::genAllHashes();
12 8
13 9
14/** 10/**
@@ -16,9 +12,6 @@ ReferenceSessionIdHashes::genAllHashes();
16 */ 12 */
17class UtilsTest extends PHPUnit_Framework_TestCase 13class UtilsTest extends PHPUnit_Framework_TestCase
18{ 14{
19 // Session ID hashes
20 protected static $sidHashes = null;
21
22 // Log file 15 // Log file
23 protected static $testLogFile = 'tests.log'; 16 protected static $testLogFile = 'tests.log';
24 17
@@ -30,13 +23,11 @@ class UtilsTest extends PHPUnit_Framework_TestCase
30 */ 23 */
31 protected static $defaultTimeZone; 24 protected static $defaultTimeZone;
32 25
33
34 /** 26 /**
35 * Assign reference data 27 * Assign reference data
36 */ 28 */
37 public static function setUpBeforeClass() 29 public static function setUpBeforeClass()
38 { 30 {
39 self::$sidHashes = ReferenceSessionIdHashes::getHashes();
40 self::$defaultTimeZone = date_default_timezone_get(); 31 self::$defaultTimeZone = date_default_timezone_get();
41 // Timezone without DST for test consistency 32 // Timezone without DST for test consistency
42 date_default_timezone_set('Africa/Nairobi'); 33 date_default_timezone_set('Africa/Nairobi');
@@ -221,57 +212,8 @@ class UtilsTest extends PHPUnit_Framework_TestCase
221 $this->assertEquals('?', generateLocation($ref, 'localhost')); 212 $this->assertEquals('?', generateLocation($ref, 'localhost'));
222 } 213 }
223 214
224 /**
225 * Test is_session_id_valid with a valid ID - TEST ALL THE HASHES!
226 *
227 * This tests extensively covers all hash algorithms / bit representations
228 */
229 public function testIsAnyHashSessionIdValid()
230 {
231 foreach (self::$sidHashes as $algo => $bpcs) {
232 foreach ($bpcs as $bpc => $hash) {
233 $this->assertTrue(is_session_id_valid($hash));
234 }
235 }
236 }
237 215
238 /** 216 /**
239 * Test is_session_id_valid with a valid ID - SHA-1 hashes
240 */
241 public function testIsSha1SessionIdValid()
242 {
243 $this->assertTrue(is_session_id_valid(sha1('shaarli')));
244 }
245
246 /**
247 * Test is_session_id_valid with a valid ID - SHA-256 hashes
248 */
249 public function testIsSha256SessionIdValid()
250 {
251 $this->assertTrue(is_session_id_valid(hash('sha256', 'shaarli')));
252 }
253
254 /**
255 * Test is_session_id_valid with a valid ID - SHA-512 hashes
256 */
257 public function testIsSha512SessionIdValid()
258 {
259 $this->assertTrue(is_session_id_valid(hash('sha512', 'shaarli')));
260 }
261
262 /**
263 * Test is_session_id_valid with invalid IDs.
264 */
265 public function testIsSessionIdInvalid()
266 {
267 $this->assertFalse(is_session_id_valid(''));
268 $this->assertFalse(is_session_id_valid(array()));
269 $this->assertFalse(
270 is_session_id_valid('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
271 );
272 }
273
274 /**
275 * Test generateSecretApi. 217 * Test generateSecretApi.
276 */ 218 */
277 public function testGenerateSecretApi() 219 public function testGenerateSecretApi()
@@ -384,18 +326,18 @@ class UtilsTest extends PHPUnit_Framework_TestCase
384 */ 326 */
385 public function testHumanBytes() 327 public function testHumanBytes()
386 { 328 {
387 $this->assertEquals('2kiB', human_bytes(2 * 1024)); 329 $this->assertEquals('2'. t('kiB'), human_bytes(2 * 1024));
388 $this->assertEquals('2kiB', human_bytes(strval(2 * 1024))); 330 $this->assertEquals('2'. t('kiB'), human_bytes(strval(2 * 1024)));
389 $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2)))); 331 $this->assertEquals('2'. t('MiB'), human_bytes(2 * (pow(1024, 2))));
390 $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2))))); 332 $this->assertEquals('2'. t('MiB'), human_bytes(strval(2 * (pow(1024, 2)))));
391 $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3)))); 333 $this->assertEquals('2'. t('GiB'), human_bytes(2 * (pow(1024, 3))));
392 $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3))))); 334 $this->assertEquals('2'. t('GiB'), human_bytes(strval(2 * (pow(1024, 3)))));
393 $this->assertEquals('374B', human_bytes(374)); 335 $this->assertEquals('374'. t('B'), human_bytes(374));
394 $this->assertEquals('374B', human_bytes('374')); 336 $this->assertEquals('374'. t('B'), human_bytes('374'));
395 $this->assertEquals('232kiB', human_bytes(237481)); 337 $this->assertEquals('232'. t('kiB'), human_bytes(237481));
396 $this->assertEquals('Unlimited', human_bytes('0')); 338 $this->assertEquals(t('Unlimited'), human_bytes('0'));
397 $this->assertEquals('Unlimited', human_bytes(0)); 339 $this->assertEquals(t('Unlimited'), human_bytes(0));
398 $this->assertEquals('Setting not set', human_bytes('')); 340 $this->assertEquals(t('Setting not set'), human_bytes(''));
399 } 341 }
400 342
401 /** 343 /**
@@ -403,9 +345,9 @@ class UtilsTest extends PHPUnit_Framework_TestCase
403 */ 345 */
404 public function testGetMaxUploadSize() 346 public function testGetMaxUploadSize()
405 { 347 {
406 $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k')); 348 $this->assertEquals('1'. t('MiB'), get_max_upload_size(2097152, '1024k'));
407 $this->assertEquals('1MiB', get_max_upload_size('1m', '2m')); 349 $this->assertEquals('1'. t('MiB'), get_max_upload_size('1m', '2m'));
408 $this->assertEquals('100B', get_max_upload_size(100, 100)); 350 $this->assertEquals('100'. t('B'), get_max_upload_size(100, 100));
409 } 351 }
410 352
411 /** 353 /**
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 00000000..d36d73cd
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,6 @@
1<?php
2
3require_once 'vendor/autoload.php';
4
5$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
6new \Shaarli\Languages('en', $conf);
diff --git a/tests/languages/bootstrap.php b/tests/languages/bootstrap.php
index 95609210..da6ac2e4 100644
--- a/tests/languages/bootstrap.php
+++ b/tests/languages/bootstrap.php
@@ -1,7 +1,6 @@
1<?php 1<?php
2if (! empty('UT_LOCALE')) { 2require_once 'tests/bootstrap.php';
3
4if (! empty(getenv('UT_LOCALE'))) {
3 setlocale(LC_ALL, getenv('UT_LOCALE')); 5 setlocale(LC_ALL, getenv('UT_LOCALE'));
4} 6}
5
6require_once 'vendor/autoload.php';
7
diff --git a/tests/languages/fr/LanguagesFrTest.php b/tests/languages/fr/LanguagesFrTest.php
new file mode 100644
index 00000000..79d05172
--- /dev/null
+++ b/tests/languages/fr/LanguagesFrTest.php
@@ -0,0 +1,175 @@
1<?php
2
3
4namespace Shaarli;
5
6
7use Shaarli\Config\ConfigManager;
8
9/**
10 * Class LanguagesFrTest
11 *
12 * Test the translation system in PHP and gettext mode with French language.
13 *
14 * @package Shaarli
15 */
16class LanguagesFrTest extends \PHPUnit_Framework_TestCase
17{
18 /**
19 * @var string Config file path (without extension).
20 */
21 protected static $configFile = 'tests/utils/config/configJson';
22
23 /**
24 * @var ConfigManager
25 */
26 protected $conf;
27
28 /**
29 * Init: force French
30 */
31 public function setUp()
32 {
33 $this->conf = new ConfigManager(self::$configFile);
34 $this->conf->set('translation.language', 'fr');
35 }
36
37 /**
38 * Reset the locale since gettext seems to mess with it, making it too long
39 */
40 public static function tearDownAfterClass()
41 {
42 if (! empty(getenv('UT_LOCALE'))) {
43 setlocale(LC_ALL, getenv('UT_LOCALE'));
44 }
45 }
46
47 /**
48 * Test t() with a simple non identified value.
49 */
50 public function testTranslateSingleNotIDGettext()
51 {
52 $this->conf->set('translation.mode', 'gettext');
53 new Languages('en', $this->conf);
54 $text = 'abcdé 564 fgK';
55 $this->assertEquals($text, t($text));
56 }
57
58 /**
59 * Test t() with a simple identified value in gettext mode.
60 */
61 public function testTranslateSingleIDGettext()
62 {
63 $this->conf->set('translation.mode', 'gettext');
64 new Languages('en', $this->conf);
65 $text = 'permalink';
66 $this->assertEquals('permalien', t($text));
67 }
68
69 /**
70 * Test t() with a non identified plural form in gettext mode.
71 */
72 public function testTranslatePluralNotIDGettext()
73 {
74 $this->conf->set('translation.mode', 'gettext');
75 new Languages('en', $this->conf);
76 $text = 'sandwich';
77 $nText = 'sandwiches';
78 // Not ID, so English fallback, and in english, plural 0
79 $this->assertEquals('sandwiches', t($text, $nText, 0));
80 $this->assertEquals('sandwich', t($text, $nText, 1));
81 $this->assertEquals('sandwiches', t($text, $nText, 2));
82 }
83
84 /**
85 * Test t() with an identified plural form in gettext mode.
86 */
87 public function testTranslatePluralIDGettext()
88 {
89 $this->conf->set('translation.mode', 'gettext');
90 new Languages('en', $this->conf);
91 $text = 'shaare';
92 $nText = 'shaares';
93 $this->assertEquals('shaare', t($text, $nText, 0));
94 $this->assertEquals('shaare', t($text, $nText, 1));
95 $this->assertEquals('shaares', t($text, $nText, 2));
96 }
97
98 /**
99 * Test t() with a simple non identified value.
100 */
101 public function testTranslateSingleNotIDPhp()
102 {
103 $this->conf->set('translation.mode', 'php');
104 new Languages('en', $this->conf);
105 $text = 'abcdé 564 fgK';
106 $this->assertEquals($text, t($text));
107 }
108
109 /**
110 * Test t() with a simple identified value in PHP mode.
111 */
112 public function testTranslateSingleIDPhp()
113 {
114 $this->conf->set('translation.mode', 'php');
115 new Languages('en', $this->conf);
116 $text = 'permalink';
117 $this->assertEquals('permalien', t($text));
118 }
119
120 /**
121 * Test t() with a non identified plural form in PHP mode.
122 */
123 public function testTranslatePluralNotIDPhp()
124 {
125 $this->conf->set('translation.mode', 'php');
126 new Languages('en', $this->conf);
127 $text = 'sandwich';
128 $nText = 'sandwiches';
129 // Not ID, so English fallback, and in english, plural 0
130 $this->assertEquals('sandwiches', t($text, $nText, 0));
131 $this->assertEquals('sandwich', t($text, $nText, 1));
132 $this->assertEquals('sandwiches', t($text, $nText, 2));
133 }
134
135 /**
136 * Test t() with an identified plural form in PHP mode.
137 */
138 public function testTranslatePluralIDPhp()
139 {
140 $this->conf->set('translation.mode', 'php');
141 new Languages('en', $this->conf);
142 $text = 'shaare';
143 $nText = 'shaares';
144 // In english, zero is followed by plural form
145 $this->assertEquals('shaare', t($text, $nText, 0));
146 $this->assertEquals('shaare', t($text, $nText, 1));
147 $this->assertEquals('shaares', t($text, $nText, 2));
148 }
149
150 /**
151 * Test t() with an extension language file in gettext mode
152 */
153 public function testTranslationExtensionGettext()
154 {
155 $this->conf->set('translation.mode', 'gettext');
156 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
157 new Languages('en', $this->conf);
158 $txt = 'car'; // ignore me poedit
159 $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
160 $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
161 }
162
163 /**
164 * Test t() with an extension language file in PHP mode
165 */
166 public function testTranslationExtensionPhp()
167 {
168 $this->conf->set('translation.mode', 'php');
169 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
170 new Languages('en', $this->conf);
171 $txt = 'car'; // ignore me poedit
172 $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
173 $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
174 }
175}
diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php
new file mode 100644
index 00000000..f29760cb
--- /dev/null
+++ b/tests/utils/FakeConfigManager.php
@@ -0,0 +1,12 @@
1<?php
2
3/**
4 * Fake ConfigManager
5 */
6class FakeConfigManager
7{
8 public static function get($key)
9 {
10 return $key;
11 }
12}
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index f09eebc1..e887aa78 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -141,6 +141,7 @@ class ReferenceLinkDB
141 */ 141 */
142 public function write($filename) 142 public function write($filename)
143 { 143 {
144 $this->reorder();
144 file_put_contents( 145 file_put_contents(
145 $filename, 146 $filename,
146 '<?php /* '.base64_encode(gzdeflate(serialize($this->_links))).' */ ?>' 147 '<?php /* '.base64_encode(gzdeflate(serialize($this->_links))).' */ ?>'
@@ -148,6 +149,27 @@ class ReferenceLinkDB
148 } 149 }
149 150
150 /** 151 /**
152 * Reorder links by creation date (newest first).
153 *
154 * Also update the urls and ids mapping arrays.
155 *
156 * @param string $order ASC|DESC
157 */
158 public function reorder($order = 'DESC')
159 {
160 // backward compatibility: ignore reorder if the the `created` field doesn't exist
161 if (! isset(array_values($this->_links)[0]['created'])) {
162 return;
163 }
164
165 $order = $order === 'ASC' ? -1 : 1;
166 // Reorder array by dates.
167 usort($this->_links, function($a, $b) use ($order) {
168 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
169 });
170 }
171
172 /**
151 * Returns the number of links in the reference data 173 * Returns the number of links in the reference data
152 */ 174 */
153 public function countLinks() 175 public function countLinks()
@@ -187,6 +209,7 @@ class ReferenceLinkDB
187 209
188 public function getLinks() 210 public function getLinks()
189 { 211 {
212 $this->reorder();
190 return $this->_links; 213 return $this->_links;
191 } 214 }
192 215
diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.mo b/tests/utils/languages/fr/LC_MESSAGES/test.mo
new file mode 100644
index 00000000..416c7831
--- /dev/null
+++ b/tests/utils/languages/fr/LC_MESSAGES/test.mo
Binary files differ
diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.po b/tests/utils/languages/fr/LC_MESSAGES/test.po
new file mode 100644
index 00000000..89a4fd9b
--- /dev/null
+++ b/tests/utils/languages/fr/LC_MESSAGES/test.po
@@ -0,0 +1,19 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Extension test\n"
4"POT-Creation-Date: 2017-05-20 13:54+0200\n"
5"PO-Revision-Date: 2017-05-20 14:16+0200\n"
6"Last-Translator: \n"
7"Language-Team: Shaarli\n"
8"Language: fr_FR\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"Plural-Forms: nplurals=2; plural=(n > 1);\n"
13"X-Generator: Poedit 2.0.1\n"
14
15msgid "car"
16msgstr "voiture"
17
18msgid "Search"
19msgstr "Fouille"
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index 49dd20d9..6606c4fa 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -32,7 +32,7 @@
32 </div> 32 </div>
33 </form> 33 </form>
34 34
35 <p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p> 35 <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p>
36 </div> 36 </div>
37</div> 37</div>
38{include="page.footer"} 38{include="page.footer"}
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index 76a1b9fd..a63c7ad3 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>
@@ -105,21 +129,6 @@
105 </div> 129 </div>
106 </div> 130 </div>
107 </div> 131 </div>
108 <div class="pure-g">
109 <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
110 <div class="form-label">
111 <label for="redirector">
112 <span class="label-name">{'Redirector'|t}</span><br>
113 <span class="label-desc">{'e. g.'|t} <i>http://anonym.to/?</i> {'will mask the HTTP_REFERER'|t}</span>
114 </label>
115 </div>
116 </div>
117 <div class="pure-u-lg-{$ratioInput} pure-u-1 ">
118 <div class="form-input">
119 <input type="text" name="redirector" id="redirector" size="50" value="{$redirector}">
120 </div>
121 </div>
122 </div>
123 <div class="clear"></div> 132 <div class="clear"></div>
124 <div class="pure-g"> 133 <div class="pure-g">
125 <div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile} "> 134 <div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile} ">
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css
index ba589723..14439402 100644
--- a/tpl/default/css/shaarli.css
+++ b/tpl/default/css/shaarli.css
@@ -433,7 +433,7 @@ body, .pure-g [class*="pure-u"] {
433 * 64em -> lg 433 * 64em -> lg
434 */ 434 */
435.linklist-filters { 435.linklist-filters {
436 margin: 10px 0; 436 margin: 5px 0;
437 color: #252525; 437 color: #252525;
438 font-size: 0.9em; 438 font-size: 0.9em;
439} 439}
@@ -454,7 +454,7 @@ body, .pure-g [class*="pure-u"] {
454} 454}
455 455
456.linklist-pages { 456.linklist-pages {
457 margin: 10px 0; 457 margin: 5px 0;
458 color: #252525; 458 color: #252525;
459 text-align: center; 459 text-align: center;
460} 460}
@@ -469,7 +469,7 @@ body, .pure-g [class*="pure-u"] {
469} 469}
470 470
471.linksperpage { 471.linksperpage {
472 margin: 10px 0; 472 margin: 5px 0;
473 text-align: right; 473 text-align: right;
474 color: #252525; 474 color: #252525;
475 font-size: 0.9em; 475 font-size: 0.9em;
@@ -506,9 +506,29 @@ body, .pure-g [class*="pure-u"] {
506 * CONTENT - LINKLIST ITEMS 506 * CONTENT - LINKLIST ITEMS
507 */ 507 */
508.linklist-item { 508.linklist-item {
509 margin: 0 0 15px 0; 509 margin: 0 0 10px 0;
510 background: #f5f5f5; 510 background: #f5f5f5;
511 box-shadow: 2px 2px 0.5em #797979; 511 box-shadow: 1px 1px 3px #797979;
512}
513
514.linklist-item-buttons {
515 background: transparent;
516 position: relative;
517 width: 23px;
518 z-index: 99;
519}
520
521.linklist-item-buttons-right {
522 float: right;
523 margin-right: -25px;
524}
525
526.linklist-item-buttons * {
527 display: block;
528 float: left;
529 width:100%;
530 margin: auto;
531 text-align: center;
512} 532}
513 533
514.linklist-item-title, .linklist-item-title h2 { 534.linklist-item-title, .linklist-item-title h2 {
@@ -526,7 +546,7 @@ body, .pure-g [class*="pure-u"] {
526 line-height: 30px; 546 line-height: 30px;
527} 547}
528 548
529.linklist-item-title a { 549.linklist-item-title h2 a {
530 font-size: 0.7em; 550 font-size: 0.7em;
531 color: #252525; 551 color: #252525;
532 text-decoration: none; 552 text-decoration: none;
@@ -538,11 +558,11 @@ body, .pure-g [class*="pure-u"] {
538 color: #1b926c; 558 color: #1b926c;
539} 559}
540 560
541.linklist-item-title a:visited .linklist-link { 561.linklist-item-title h2 a:visited .linklist-link {
542 color: #2a4c41; 562 color: #2a4c41;
543} 563}
544 564
545.linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{ 565.linklist-item-title h2 a:hover, .linklist-item-title .linklist-link:hover{
546 color: #252525; 566 color: #252525;
547} 567}
548 568
@@ -554,8 +574,9 @@ body, .pure-g [class*="pure-u"] {
554 color: #F89406; 574 color: #F89406;
555} 575}
556 576
557.linklist-item-title .fold-button { 577.fold-button {
558 display: none; 578 display: none;
579 color: #252525;
559} 580}
560 581
561.linklist-item-editbuttons { 582.linklist-item-editbuttons {
@@ -585,24 +606,12 @@ body, .pure-g [class*="pure-u"] {
585 606
586.linklist-item-description { 607.linklist-item-description {
587 position: relative; 608 position: relative;
588 padding: 10px; 609 padding: 0 10px;
589 word-wrap: break-word; 610 word-wrap: break-word;
590 color: #252525; 611 color: #252525;
591 line-height: 1.3em; 612 line-height: 1.3em;
592} 613}
593 614
594 {
595 position: absolute;
596 left: 3px;
597 top: 0;
598 display: block;
599 content:"";
600 background: #F89406;
601 height: 95%;
602 width: 2px;
603 z-index: 1;
604}
605
606.linklist-item-description a { 615.linklist-item-description a {
607 text-decoration: none; 616 text-decoration: none;
608 color: #1b926c; 617 color: #1b926c;
@@ -618,32 +627,36 @@ body, .pure-g [class*="pure-u"] {
618 627
619.linklist-item-thumbnail { 628.linklist-item-thumbnail {
620 position: relative; 629 position: relative;
621 margin-top: 10px; 630 padding: 0 0 0 5px;
622 padding: 10px; 631 margin: 0;
623 float: left; 632 float: right;
624 z-index: 50; 633 z-index: 50;
634 height: 90px;
625} 635}
626 636
627.linklist-item.private .linklist-item-title::before, 637.linklist-item.private .linklist-item-title::before,
628.linklist-item.private .linklist-item-description::before, 638.linklist-item.private .linklist-item-description::before {
629.linklist-item.private .linklist-item-thumbnail::before {
630 position: absolute; 639 position: absolute;
631 left: 3px; 640 left: 3px;
632 top: 0; 641 top: 0;
633 display: block; 642 display: block;
634 content:""; 643 content:"";
635 background: #F89406; 644 background: #F89406;
636 height: 95%; 645 height: 96%;
637 width: 2px; 646 width: 2px;
638 z-index: 1; 647 z-index: 1;
639} 648}
640 649
650.linklist-item.private .linklist-item-description::before {
651 height: 100%;
652}
653
641.linklist-item.private .linklist-item-title::before { 654.linklist-item.private .linklist-item-title::before {
642 margin-top: 3px; 655 margin-top: 3px;
643} 656}
644 657
645.linklist-item-infos { 658.linklist-item-infos {
646 padding: 8px 8px 5px 8px; 659 padding: 4px 8px 4px 8px;
647 background: #ddd; 660 background: #ddd;
648 color: #252525; 661 color: #252525;
649} 662}
@@ -680,6 +693,8 @@ body, .pure-g [class*="pure-u"] {
680 overflow: hidden; 693 overflow: hidden;
681 text-overflow: ellipsis; 694 text-overflow: ellipsis;
682 font-size: 0.8em; 695 font-size: 0.8em;
696 height:23px;
697 line-height:23px;
683} 698}
684 699
685.linklist-item-infos .mobile-buttons { 700.linklist-item-infos .mobile-buttons {
@@ -693,6 +708,16 @@ body, .pure-g [class*="pure-u"] {
693 height: 16px; 708 height: 16px;
694} 709}
695 710
711.linklist-item-infos-controls-group {
712 display: inline-block;
713 border-right: 1px solid #5d5d5d;
714 padding-right: 6px;
715}
716
717.ctrl-edit {
718 margin: 0 7px;
719}
720
696/** 64em -> lg **/ 721/** 64em -> lg **/
697@media screen and (max-width: 64em) { 722@media screen and (max-width: 64em) {
698 .linklist-item-infos-url { 723 .linklist-item-infos-url {
@@ -1284,3 +1309,40 @@ form[name="linkform"].page-form {
1284 text-decoration: none; 1309 text-decoration: none;
1285 font-weight: bold; 1310 font-weight: bold;
1286} 1311}
1312
1313/**
1314 * Markdown
1315 */
1316.markdown p {
1317 margin: 0 !important;
1318}
1319
1320.markdown p + p {
1321 margin: 0.5em 0 0 0 !important;
1322}
1323
1324.markdown *:first-child {
1325 margin-top: 0 !important;
1326}
1327
1328.markdown *:last-child {
1329 margin-bottom: 5px !important;
1330}
1331
1332/**
1333 * Pure Button
1334 */
1335.pure-button-success,
1336.pure-button-error,
1337.pure-button-warning,
1338.pure-button-primary,
1339.pure-button-shaarli,
1340.pure-button-secondary {
1341 color: white !important;
1342 border-radius: 4px;
1343 text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
1344}
1345
1346.pure-button-shaarli {
1347 background-color: #1B926C;
1348}
diff --git a/tpl/default/img/apple-touch-icon.png b/tpl/default/img/apple-touch-icon.png
new file mode 100644
index 00000000..f29210ce
--- /dev/null
+++ b/tpl/default/img/apple-touch-icon.png
Binary files differ
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 80c08333..b2bfec30 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -5,6 +5,7 @@
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 href="img/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
8<link type="text/css" rel="stylesheet" href="css/pure.min.css?v={$version_hash}" /> 9<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?v={$version_hash}"> 10<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?v={$version_hash}"> 11<link type="text/css" rel="stylesheet" href="css/pure-extras.css?v={$version_hash}">
@@ -17,4 +18,4 @@
17{loop="$plugins_includes.css_files"} 18{loop="$plugins_includes.css_files"}
18 <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/> 19 <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
19{/loop} 20{/loop}
20<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/> \ No newline at end of file 21<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/>
diff --git a/tpl/default/install.html b/tpl/default/install.html
index 164d453b..6199b33d 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -68,6 +68,27 @@
68 <div class="pure-g"> 68 <div class="pure-g">
69 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 69 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
70 <div class="form-label"> 70 <div class="form-label">
71 <label for="language">
72 <span class="label-name">{'Language'|t}</span>
73 </label>
74 </div>
75 </div>
76 <div class="pure-u-lg-{$ratioInput} pure-u-1">
77 <div class="form-input">
78 <select name="language" id="language" class="align">
79 {loop="$languages"}
80 <option value="{$key}">
81 {$value}
82 </option>
83 {/loop}
84 </select>
85 </div>
86 </div>
87 </div>
88
89 <div class="pure-g">
90 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
91 <div class="form-label">
71 <label> 92 <label>
72 <span class="label-name">{'Timezone'|t}</span><br> 93 <span class="label-name">{'Timezone'|t}</span><br>
73 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span> 94 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js
index 55656f80..cf628e87 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 });
@@ -375,7 +378,7 @@ window.onload = function () {
375 var linkCheckboxes = document.querySelectorAll('.delete-checkbox'); 378 var linkCheckboxes = document.querySelectorAll('.delete-checkbox');
376 var bar = document.getElementById('actions'); 379 var bar = document.getElementById('actions');
377 [].forEach.call(linkCheckboxes, function(checkbox) { 380 [].forEach.call(linkCheckboxes, function(checkbox) {
378 checkbox.style.display = 'block'; 381 checkbox.style.display = 'inline-block';
379 checkbox.addEventListener('click', function(event) { 382 checkbox.addEventListener('click', function(event) {
380 var count = 0; 383 var count = 0;
381 var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked'); 384 var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
@@ -618,7 +621,7 @@ function activateFirefoxSocial(node) {
618 // 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.
619 var data = { 622 var data = {
620 name: title, 623 name: title,
621 description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.", 624 description: document.getElementById('translation-delete-link').innerHTML,
622 author: "Shaarli", 625 author: "Shaarli",
623 version: "1.0.0", 626 version: "1.0.0",
624 627
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index 685821e3..c666e30a 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -53,9 +53,9 @@
53{/loop} 53{/loop}
54 54
55<div id="linklist"> 55<div id="linklist">
56 <div class="pure-g"> 56 <div id="link-count-block" class="pure-g">
57 <div class="pure-u-lg-2-24 pure-u-1-24"></div> 57 <div class="pure-u-lg-2-24 pure-u-1-24"></div>
58 <div class="pure-u-lg-20-24 pure-u-22-24"> 58 <div id="link-count-content" class="pure-u-lg-20-24 pure-u-22-24">
59 <div class="linkcount pure-u-lg-0 center"> 59 <div class="linkcount pure-u-lg-0 center">
60 {if="!empty($linkcount)"} 60 {if="!empty($linkcount)"}
61 <span class="strong">{$linkcount}</span> {function="t('shaare', 'shaares', $linkcount)"} 61 <span class="strong">{$linkcount}</span> {function="t('shaare', 'shaares', $linkcount)"}
@@ -76,17 +76,17 @@
76 </div> 76 </div>
77 77
78 {if="count($links)==0"} 78 {if="count($links)==0"}
79 <div class="pure-g pure-alert pure-alert-error search-result"> 79 <div id="search-result-block" class="pure-g pure-alert pure-alert-error search-result">
80 <div class="pure-u-2-24"></div> 80 <div class="pure-u-2-24"></div>
81 <div class="pure-u-20-24"> 81 <div id="search-result-content" class="pure-u-20-24">
82 <div id="searchcriteria">{'Nothing found.'|t}</div> 82 <div id="searchcriteria">{'Nothing found.'|t}</div>
83 </div> 83 </div>
84 </div> 84 </div>
85 {elseif="!empty($search_term) or $search_tags !== '' or !empty($visibility) or $untaggedonly"} 85 {elseif="!empty($search_term) or $search_tags !== '' or !empty($visibility) or $untaggedonly"}
86 <div class="pure-g pure-alert pure-alert-success search-result"> 86 <div id="search-result-block" 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 id="search-result-content" class="pure-u-20-24 search-result-main">
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}
@@ -114,23 +114,34 @@
114 </div> 114 </div>
115 {/if} 115 {/if}
116 116
117 <div class="pure-g"> 117 <div id="linklist-loop-block" 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 id="linklist-loop-content" 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}">
123 132
133 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
124 <div class="linklist-item-title"> 134 <div class="linklist-item-title">
135 {$thumb=thumbnail($value.url)}
136 {if="$thumb!=false"}
137 <div class="linklist-item-thumbnail">{$thumb}</div>
138 {/if}
139
125 {if="isLoggedIn()"} 140 {if="isLoggedIn()"}
126 <div class="linklist-item-editbuttons"> 141 <div class="linklist-item-editbuttons">
127 {if="$value.private"} 142 {if="$value.private"}
128 <span class="label label-private">{'Private'|t}</span> 143 <span class="label label-private">{$strPrivate}</span>
129 {/if} 144 {/if}
130 <input type="checkbox" class="delete-checkbox" value="{$value.id}">
131 <!-- FIXME! JS translation -->
132 <a href="?edit_link={$value.id}" title="{'Edit'|t}"><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>
134 </div> 145 </div>
135 {/if} 146 {/if}
136 147
@@ -147,11 +158,6 @@
147 </h2> 158 </h2>
148 </div> 159 </div>
149 160
150 {$thumb=thumbnail($value.url)}
151 {if="$thumb!=false"}
152 <div class="linklist-item-thumbnail">{$thumb}</div>
153 {/if}
154
155 {if="$value.description"} 161 {if="$value.description"}
156 <div class="linklist-item-description"> 162 <div class="linklist-item-description">
157 {$value.description} 163 {$value.description}
@@ -164,7 +170,7 @@
164 <i class="fa fa-tags"></i> 170 <i class="fa fa-tags"></i>
165 {$tag_counter=count($value.taglist)} 171 {$tag_counter=count($value.taglist)}
166 {loop="value.taglist"} 172 {loop="value.taglist"}
167 <span class="label label-tag" title="Add tag"> 173 <span class="label label-tag" title="{$strAddTag}">
168 <a href="?addtag={$value|urlencode}">{$value}</a> 174 <a href="?addtag={$value|urlencode}">{$value}</a>
169 </span> 175 </span>
170 {if="$tag_counter - 1 != $counter"}&middot;{/if} 176 {if="$tag_counter - 1 != $counter"}&middot;{/if}
@@ -172,11 +178,27 @@
172 </div> 178 </div>
173 {/if} 179 {/if}
174 180
175 <div class="pure-g"> 181 <div class="linklist-item-infos-date-url-block pure-g">
176 <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1"> 182 <div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1">
177 <a href="?{$value.shorturl}" title="{'Permalink'|t}"> 183 {if="isLoggedIn()"}
184 <div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible">
185 <span class="linklist-item-infos-controls-item ctrl-checkbox">
186 <input type="checkbox" class="delete-checkbox" value="{$value.id}">
187 </span>
188 <span class="linklist-item-infos-controls-item ctrl-edit">
189 <a href="?edit_link={$value.id}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link"></i></a>
190 </span>
191 <span class="linklist-item-infos-controls-item ctrl-delete">
192 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
193 title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
194 <i class="fa fa-trash"></i>
195 </a>
196 </span>
197 </div>
198 {/if}
199 <a href="?{$value.shorturl}" title="{$strPermalink}">
178 {if="!$hide_timestamps || isLoggedIn()"} 200 {if="!$hide_timestamps || isLoggedIn()"}
179 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} 201 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
180 <span class="linkdate" title="{$updated}"> 202 <span class="linkdate" title="{$updated}">
181 <i class="fa fa-clock-o"></i> 203 <i class="fa fa-clock-o"></i>
182 {$value.created|format_date} 204 {$value.created|format_date}
@@ -184,7 +206,7 @@
184 &middot; 206 &middot;
185 </span> 207 </span>
186 {/if} 208 {/if}
187 {'permalink'|t} 209 {$strPermalinkLc}
188 </a> 210 </a>
189 211
190 <div class="pure-u-0 pure-u-lg-visible"> 212 <div class="pure-u-0 pure-u-lg-visible">
@@ -199,16 +221,13 @@
199 </div> 221 </div>
200 </div><div 222 </div><div
201 {ignore}do not add space or line break between these div - Firefox issue{/ignore} 223 {ignore}do not add space or line break between these div - Firefox issue{/ignore}
202 class="linklist-item-infos-url pure-u-lg-5-8 pure-u-1"> 224 class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1">
203 <a href="{$value.real_url}" title="{$value.title}"> 225 <a href="{$value.real_url}" title="{$value.title}">
204 <i class="fa fa-link"></i> {$value.url} 226 <i class="fa fa-link"></i> {$value.url}
205 </a> 227 </a>
206 {if="isLoggedIn()"} 228 <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible">
207 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" 229 <a href="#" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
208 title="{'Delete'|t}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete"> 230 </div>
209 <i class="fa fa-trash"></i>
210 </a>
211 {/if}
212 </div> 231 </div>
213 <div class="mobile-buttons pure-u-1 pure-u-lg-0"> 232 <div class="mobile-buttons pure-u-1 pure-u-lg-0">
214 {if="isset($value.link_plugin)"} 233 {if="isset($value.link_plugin)"}
@@ -221,9 +240,11 @@
221 {if="isLoggedIn()"} 240 {if="isLoggedIn()"}
222 &middot; 241 &middot;
223 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" 242 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
224 title="{'Delete'|t}" class="delete-link confirm-delete"> 243 title="{$strDelete}" class="delete-link confirm-delete">
225 <i class="fa fa-trash"></i> 244 <i class="fa fa-trash"></i>
226 </a> 245 </a>
246 &middot;
247 <a href="?edit_link={$value.id}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link"></i></a>
227 {/if} 248 {/if}
228 </div> 249 </div>
229 </div> 250 </div>
@@ -240,9 +261,9 @@
240 {/loop} 261 {/loop}
241 </div> 262 </div>
242 263
243<div class="pure-g"> 264<div id="linklist-paging-bottom-block" class="pure-g">
244 <div class="pure-u-lg-2-24 pure-u-1-24"></div> 265 <div class="pure-u-lg-2-24 pure-u-1-24"></div>
245 <div class="pure-u-lg-20-24 pure-u-22-24"> 266 <div id="linklist-paging-bottom-content" class="pure-u-lg-20-24 pure-u-22-24">
246 {include="linklist.paging"} 267 {include="linklist.paging"}
247 </div> 268 </div>
248</div> 269</div>
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 54b16e8a..659e8c7f 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -8,8 +8,8 @@
8 {$version} 8 {$version}
9 {/if} 9 {/if}
10 &middot; 10 &middot;
11 The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community &middot; 11 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot;
12 <a href="doc/html/index.html" rel="nofollow">Documentation</a> 12 <a href="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
13 {loop="$plugins_footer.text"} 13 {loop="$plugins_footer.text"}
14 {$value} 14 {$value}
15 {/loop} 15 {/loop}
@@ -27,6 +27,17 @@
27 <script src="{$value}#"></script> 27 <script src="{$value}#"></script>
28{/loop} 28{/loop}
29 29
30<div id="js-translations" class="hidden">
31 <span id="translation-fold">{'Fold'|t}</span>
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
30<script src="js/shaarli.js?v={$version_hash}"></script> 41<script src="js/shaarli.js?v={$version_hash}"></script>
31<script src="inc/awesomplete.js?v={$version_hash}#"></script> 42<script src="inc/awesomplete.js?v={$version_hash}#"></script>
32<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script> 43<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script>
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html
index 2411703c..6f15c1c5 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -1,7 +1,7 @@
1<div class="shaarli-menu pure-g" id="shaarli-menu"> 1<div class="shaarli-menu pure-g" id="shaarli-menu">
2 <div class="pure-u-lg-0 pure-u-1"> 2 <div class="pure-u-lg-0 pure-u-1">
3 <div class="pure-menu"> 3 <div class="pure-menu">
4 <a href="{$titleLink}" class="pure-menu-link"> 4 <a href="{$titleLink}" class="pure-menu-link shaarli-title" id="shaarli-title-mobile">
5 <img src="img/icon.png" width="16" height="16" class="head-logo" alt="logo" /> 5 <img src="img/icon.png" width="16" height="16" class="head-logo" alt="logo" />
6 {$shaarlititle} 6 {$shaarlititle}
7 </a> 7 </a>
@@ -12,32 +12,32 @@
12 <div class="pure-menu menu-transform pure-menu-horizontal pure-g"> 12 <div class="pure-menu menu-transform pure-menu-horizontal pure-g">
13 <ul class="pure-menu-list pure-u-lg-5-6 pure-u-1"> 13 <ul class="pure-menu-list pure-u-lg-5-6 pure-u-1">
14 <li class="pure-menu-item pure-u-0 pure-u-lg-visible"> 14 <li class="pure-menu-item pure-u-0 pure-u-lg-visible">
15 <a href="{$titleLink}" class="pure-menu-link"> 15 <a href="{$titleLink}" class="pure-menu-link shaarli-title" id="shaarli-title-desktop">
16 <img src="img/icon.png" width="16" height="16" class="head-logo" alt="logo" /> 16 <img src="img/icon.png" width="16" height="16" class="head-logo" alt="logo" />
17 {$shaarlititle} 17 {$shaarlititle}
18 </a> 18 </a>
19 </li> 19 </li>
20 {if="isLoggedIn() || $openshaarli"} 20 {if="isLoggedIn() || $openshaarli"}
21 <li class="pure-menu-item"> 21 <li class="pure-menu-item">
22 <a href="?do=addlink" class="pure-menu-link"> 22 <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare">
23 <i class="fa fa-plus" ></i> {'Shaare'|t} 23 <i class="fa fa-plus" ></i> {'Shaare'|t}
24 </a> 24 </a>
25 </li> 25 </li>
26 <li class="pure-menu-item"> 26 <li class="pure-menu-item" id="shaarli-menu-tools">
27 <a href="?do=tools" class="pure-menu-link">{'Tools'|t}</a> 27 <a href="?do=tools" class="pure-menu-link">{'Tools'|t}</a>
28 </li> 28 </li>
29 {/if} 29 {/if}
30 <li class="pure-menu-item"> 30 <li class="pure-menu-item" id="shaarli-menu-tags">
31 <a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a> 31 <a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a>
32 </li> 32 </li>
33 <li class="pure-menu-item"> 33 <li class="pure-menu-item" id="shaarli-menu-picwall">
34 <a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a> 34 <a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a>
35 </li> 35 </li>
36 <li class="pure-menu-item"> 36 <li class="pure-menu-item" id="shaarli-menu-daily">
37 <a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a> 37 <a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a>
38 </li> 38 </li>
39 {loop="$plugins_header.buttons_toolbar"} 39 {loop="$plugins_header.buttons_toolbar"}
40 <li class="pure-menu-item"> 40 <li class="pure-menu-item shaarli-menu-plugin">
41 <a 41 <a
42 {$value.attr.class=isset($value.class) ? $value.attr.class . ' pure-menu-link' : 'pure-menu-link'} 42 {$value.attr.class=isset($value.class) ? $value.attr.class . ' pure-menu-link' : 'pure-menu-link'}
43 {loop="$value.attr"} 43 {loop="$value.attr"}
@@ -47,47 +47,47 @@
47 </a> 47 </a>
48 </li> 48 </li>
49 {/loop} 49 {/loop}
50 <li class="pure-menu-item pure-u-lg-0"> 50 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
51 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a> 51 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
52 </li> 52 </li>
53 {if="isLoggedIn()"} 53 {if="isLoggedIn()"}
54 <li class="pure-menu-item pure-u-lg-0"> 54 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
55 <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a> 55 <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a>
56 </li> 56 </li>
57 {else} 57 {else}
58 <li class="pure-menu-item pure-u-lg-0"> 58 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login">
59 <a href="?do=login" class="pure-menu-link">{'Login'|t}</a> 59 <a href="?do=login" class="pure-menu-link">{'Login'|t}</a>
60 </li> 60 </li>
61 {/if} 61 {/if}
62 </ul> 62 </ul>
63 <div class="header-buttons pure-u-lg-1-6 pure-u-0 pure-u-lg-visible"> 63 <div class="header-buttons pure-u-lg-1-6 pure-u-0 pure-u-lg-visible">
64 <ul class="pure-menu-list"> 64 <ul class="pure-menu-list">
65 <li class="pure-menu-item"> 65 <li class="pure-menu-item" id="shaarli-menu-desktop-search">
66 <a href="#" class="pure-menu-link subheader-opener" 66 <a href="#" class="pure-menu-link subheader-opener"
67 data-open-id="search" 67 data-open-id="search"
68 id="search-button" title="{'Search'|t}"> 68 id="search-button" title="{'Search'|t}">
69 <i class="fa fa-search"></i> 69 <i class="fa fa-search"></i>
70 </a> 70 </a>
71 </li> 71 </li>
72 <li class="pure-menu-item"> 72 <li class="pure-menu-item" id="shaarli-menu-desktop-rss">
73 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}"> 73 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}">
74 <i class="fa fa-rss"></i> 74 <i class="fa fa-rss"></i>
75 </a> 75 </a>
76 </li> 76 </li>
77 {if="!isLoggedIn()"} 77 {if="!isLoggedIn()"}
78 <li class="pure-menu-item"> 78 <li class="pure-menu-item" id="shaarli-menu-desktop-login">
79 <a href="?do=login" class="pure-menu-link" 79 <a href="?do=login" class="pure-menu-link"
80 data-open-id="header-login-form" 80 data-open-id="header-login-form"
81 id="login-button" title="{'Login'|t}"> 81 id="login-button" title="{'Login'|t}">
82 <i class="fa fa-user"></i> 82 <i class="fa fa-user"></i>
83 </a> 83 </a>
84 </li> 84 </li>
85 {else} 85 {else}
86 <li class="pure-menu-item"> 86 <li class="pure-menu-item" id="shaarli-menu-desktop-logout">
87 <a href="?do=logout" class="pure-menu-link" title="{'Logout'|t}"> 87 <a href="?do=logout" class="pure-menu-link" title="{'Logout'|t}">
88 <i class="fa fa-sign-out"></i> 88 <i class="fa fa-sign-out"></i>
89 </a> 89 </a>
90 </li> 90 </li>
91 {/if} 91 {/if}
92 </ul> 92 </ul>
93 </div> 93 </div>
@@ -156,7 +156,7 @@
156{/if} 156{/if}
157 157
158{if="!empty($plugin_errors) && isLoggedIn()"} 158{if="!empty($plugin_errors) && isLoggedIn()"}
159 <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable"> 159 <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
160 <div class="pure-u-2-24"></div> 160 <div class="pure-u-2-24"></div>
161 <div class="pure-u-20-24"> 161 <div class="pure-u-20-24">
162 {loop="plugin_errors"} 162 {loop="plugin_errors"}
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index 5cc1802f..b2d7cdc5 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -27,7 +27,7 @@
27 27
28 <div> 28 <div>
29 {if="count($enabledPlugins)==0"} 29 {if="count($enabledPlugins)==0"}
30 <p>{'No plugin enabled.'|t}</p> 30 <p class="center">{'No plugin enabled.'|t}</p>
31 {else} 31 {else}
32 <table id="plugin_table"> 32 <table id="plugin_table">
33 <thead> 33 <thead>
@@ -77,7 +77,7 @@
77 77
78 <div> 78 <div>
79 {if="count($disabledPlugins)==0"} 79 {if="count($disabledPlugins)==0"}
80 <p>{'No plugin disabled.'|t}</p> 80 <p class="center">{'No plugin disabled.'|t}</p>
81 {else} 81 {else}
82 <table> 82 <table>
83 <thead> 83 <thead>
@@ -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">
@@ -135,9 +135,11 @@
135 <section id="plugin_parameters"> 135 <section id="plugin_parameters">
136 <div> 136 <div>
137 {if="count($enabledPlugins)==0"} 137 {if="count($enabledPlugins)==0"}
138 <p>{'No plugin enabled.'|t}</p> 138 <p class="center">{'No plugin enabled.'|t}</p>
139 {else} 139 {else}
140 {$nbParameters=0}
140 {loop="$enabledPlugins"} 141 {loop="$enabledPlugins"}
142 {$nbParameters=$nbParameters+count($value.parameters)}
141 {if="count($value.parameters) > 0"} 143 {if="count($value.parameters) > 0"}
142 <div class="plugin_parameters"> 144 <div class="plugin_parameters">
143 <h3 class="window-subtitle">{function="str_replace('_', ' ', $key)"}</h3> 145 <h3 class="window-subtitle">{function="str_replace('_', ' ', $key)"}</h3>
@@ -159,10 +161,14 @@
159 </div> 161 </div>
160 {/if} 162 {/if}
161 {/loop} 163 {/loop}
164 {if="$nbParameters===0"}
165 <p class="center">{'No parameter available.'|t}</p>
166 {else}
167 <div class="center">
168 <input type="submit" name="parameters_form" value="{'Save'|t}"/>
169 </div>
170 {/if}
162 {/if} 171 {/if}
163 <div class="center">
164 <input type="submit" name="parameters_form" value="{'Save'|t}"/>
165 </div>
166 </div> 172 </div>
167 </section> 173 </section>
168 </div> 174 </div>
diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html
index 68335c70..12701465 100644
--- a/tpl/default/tag.cloud.html
+++ b/tpl/default/tag.cloud.html
@@ -14,8 +14,10 @@
14 {$countTags=count($tags)} 14 {$countTags=count($tags)}
15 <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2> 15 <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
16 {if="!empty($search_tags)"} 16 {if="!empty($search_tags)"}
17 <p class="enter"> 17 <p class="center">
18 <a href="?searchtags={$search_tags|urlencode}">{'List all links with those tags'|t}</a> 18 <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
19 {'List all links with those tags'|t}
20 </a>
19 </p> 21 </p>
20 {/if} 22 {/if}
21 23
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html
index a3e741d3..7140c67a 100644
--- a/tpl/default/tag.list.html
+++ b/tpl/default/tag.list.html
@@ -15,7 +15,9 @@
15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2> 15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
16 {if="!empty($search_tags)"} 16 {if="!empty($search_tags)"}
17 <p class="center"> 17 <p class="center">
18 <a href="?searchtags={$search_tags|urlencode}">{'List all links with those tags'|t}</a> 18 <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
19 {'List all links with those tags'|t}
20 </a>
19 </p> 21 </p>
20 {/if} 22 {/if}
21 23