diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-11-12 13:02:36 +0100 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-11-12 13:02:36 +0100 |
commit | 1409f1c89a7ca01456ae2dcd6357d296e2b99f5a (patch) | |
tree | ffa30a9358e82d27be75d8fc5e57f3c8820dc6d3 | |
parent | 054e03f37fa29da8066f1a637919f13c7e7dc5d2 (diff) | |
parent | a6935feb22df8d9634189ee87d257da9f03eedbd (diff) | |
download | Shaarli-27ca44e31b73358f7df4940132b8264596250958.tar.gz Shaarli-27ca44e31b73358f7df4940132b8264596250958.tar.zst Shaarli-27ca44e31b73358f7df4940132b8264596250958.zip |
218 files changed, 8665 insertions, 3309 deletions
diff --git a/.docker/nginx.conf b/.docker/nginx.conf index 07fba33f..30810a87 100644 --- a/.docker/nginx.conf +++ b/.docker/nginx.conf | |||
@@ -17,27 +17,13 @@ http { | |||
17 | index index.html index.php; | 17 | index index.html index.php; |
18 | 18 | ||
19 | server { | 19 | server { |
20 | listen 80; | 20 | listen 80; |
21 | root /var/www/shaarli; | 21 | root /var/www/shaarli; |
22 | 22 | ||
23 | access_log /var/log/nginx/shaarli.access.log; | 23 | access_log /var/log/nginx/shaarli.access.log; |
24 | error_log /var/log/nginx/shaarli.error.log; | 24 | error_log /var/log/nginx/shaarli.error.log; |
25 | 25 | ||
26 | location ~ /\. { | 26 | location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$ { |
27 | # deny access to dotfiles | ||
28 | access_log off; | ||
29 | log_not_found off; | ||
30 | deny all; | ||
31 | } | ||
32 | |||
33 | location ~ ~$ { | ||
34 | # deny access to temp editor files, e.g. "script.php~" | ||
35 | access_log off; | ||
36 | log_not_found off; | ||
37 | deny all; | ||
38 | } | ||
39 | |||
40 | location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { | ||
41 | # cache static assets | 27 | # cache static assets |
42 | expires max; | 28 | expires max; |
43 | add_header Pragma public; | 29 | add_header Pragma public; |
@@ -49,25 +35,25 @@ http { | |||
49 | alias /var/www/shaarli/images/favicon.ico; | 35 | alias /var/www/shaarli/images/favicon.ico; |
50 | } | 36 | } |
51 | 37 | ||
38 | location /doc/html/ { | ||
39 | default_type "text/html"; | ||
40 | try_files $uri $uri/ $uri.html =404; | ||
41 | } | ||
42 | |||
52 | location / { | 43 | location / { |
53 | # Slim - rewrite URLs | 44 | # Slim - rewrite URLs & do NOT serve static files through this location |
54 | try_files $uri /index.php$is_args$args; | 45 | try_files _ /index.php$is_args$args; |
55 | } | 46 | } |
56 | 47 | ||
57 | location ~ (index)\.php$ { | 48 | location ~ index\.php$ { |
58 | # Slim - split URL path into (script_filename, path_info) | 49 | # Slim - split URL path into (script_filename, path_info) |
59 | try_files $uri =404; | 50 | try_files $uri =404; |
60 | fastcgi_split_path_info ^(.+\.php)(/.+)$; | 51 | fastcgi_split_path_info ^(index.php)(/.+)$; |
61 | 52 | ||
62 | # filter and proxy PHP requests to PHP-FPM | 53 | # filter and proxy PHP requests to PHP-FPM |
63 | fastcgi_pass unix:/var/run/php-fpm.sock; | 54 | fastcgi_pass unix:/var/run/php-fpm.sock; |
64 | fastcgi_index index.php; | 55 | fastcgi_index index.php; |
65 | include fastcgi.conf; | 56 | include fastcgi.conf; |
66 | } | 57 | } |
67 | |||
68 | location ~ \.php$ { | ||
69 | # deny access to all other PHP scripts | ||
70 | deny all; | ||
71 | } | ||
72 | } | 58 | } |
73 | } | 59 | } |
diff --git a/.dockerignore b/.dockerignore index 96fd31c5..19fd87a5 100644 --- a/.dockerignore +++ b/.dockerignore | |||
@@ -2,8 +2,16 @@ | |||
2 | .dev | 2 | .dev |
3 | .git | 3 | .git |
4 | .github | 4 | .github |
5 | .gitattributes | ||
6 | .gitignore | ||
7 | .travis.yml | ||
5 | tests | 8 | tests |
6 | 9 | ||
10 | # Docker related resources are not needed inside the container | ||
11 | .dockerignore | ||
12 | Dockerfile | ||
13 | Dockerfile.armhf | ||
14 | |||
7 | # Docker Compose resources | 15 | # Docker Compose resources |
8 | docker-compose.yml | 16 | docker-compose.yml |
9 | 17 | ||
@@ -13,6 +21,9 @@ data/* | |||
13 | pagecache/* | 21 | pagecache/* |
14 | tmp/* | 22 | tmp/* |
15 | 23 | ||
24 | # Shaarli's docs are created during the build | ||
25 | doc/html/ | ||
26 | |||
16 | # Eclipse project files | 27 | # Eclipse project files |
17 | .settings | 28 | .settings |
18 | .buildpath | 29 | .buildpath |
@@ -13,7 +13,7 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] | |||
13 | # Alternative (if the 2 lines above don't work) | 13 | # Alternative (if the 2 lines above don't work) |
14 | # SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0 | 14 | # SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0 |
15 | 15 | ||
16 | # REST API | 16 | # Slim URL Redirection |
17 | # Ionos Hosting needs RewriteBase / | 17 | # Ionos Hosting needs RewriteBase / |
18 | # RewriteBase / | 18 | # RewriteBase / |
19 | RewriteCond %{REQUEST_FILENAME} !-f | 19 | RewriteCond %{REQUEST_FILENAME} !-f |
diff --git a/.travis.yml b/.travis.yml index d7460947..422bf835 100644 --- a/.travis.yml +++ b/.travis.yml | |||
@@ -49,6 +49,10 @@ cache: | |||
49 | directories: | 49 | directories: |
50 | - $HOME/.composer/cache | 50 | - $HOME/.composer/cache |
51 | 51 | ||
52 | before_install: | ||
53 | # Disable xdebug: it significantly speed up tests and linter, and we don't use coverage yet | ||
54 | - phpenv config-rm xdebug.ini || echo 'No xdebug config.' | ||
55 | |||
52 | install: | 56 | install: |
53 | # install/update composer and php dependencies | 57 | # install/update composer and php dependencies |
54 | - composer config --unset platform && composer config platform.php $TRAVIS_PHP_VERSION | 58 | - composer config --unset platform && composer config platform.php $TRAVIS_PHP_VERSION |
@@ -60,4 +64,5 @@ before_script: | |||
60 | script: | 64 | script: |
61 | - make clean | 65 | - make clean |
62 | - make check_permissions | 66 | - make check_permissions |
67 | - make code_sniffer | ||
63 | - make all_tests | 68 | - make all_tests |
@@ -1,4 +1,4 @@ | |||
1 | 991 ArthurHoaro <arthur@hoa.ro> | 1 | 1097 ArthurHoaro <arthur@hoa.ro> |
2 | 402 VirtualTam <virtualtam@flibidi.net> | 2 | 402 VirtualTam <virtualtam@flibidi.net> |
3 | 294 nodiscc <nodiscc@gmail.com> | 3 | 294 nodiscc <nodiscc@gmail.com> |
4 | 56 Sébastien Sauvage <sebsauvage@sebsauvage.net> | 4 | 56 Sébastien Sauvage <sebsauvage@sebsauvage.net> |
@@ -25,6 +25,7 @@ | |||
25 | 2 Alexandre G.-Raymond <alex@ndre.gr> | 25 | 2 Alexandre G.-Raymond <alex@ndre.gr> |
26 | 2 Chris Kuethe <chris.kuethe@gmail.com> | 26 | 2 Chris Kuethe <chris.kuethe@gmail.com> |
27 | 2 Felix Bartels <felix@host-consultants.de> | 27 | 2 Felix Bartels <felix@host-consultants.de> |
28 | 2 Ganesh Kandu <kanduganesh@gmail.com> | ||
28 | 2 Guillaume Virlet <github@virlet.org> | 29 | 2 Guillaume Virlet <github@virlet.org> |
29 | 2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org> | 30 | 2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org> |
30 | 2 Mathieu Chabanon <git@matchab.fr> | 31 | 2 Mathieu Chabanon <git@matchab.fr> |
@@ -39,6 +40,7 @@ | |||
39 | 2 pips <pips@e5150.fr> | 40 | 2 pips <pips@e5150.fr> |
40 | 2 trailjeep <trailjeep@gmail.com> | 41 | 2 trailjeep <trailjeep@gmail.com> |
41 | 2 yude <yudesleepy@gmail.com> | 42 | 2 yude <yudesleepy@gmail.com> |
43 | 2 yudete <yu@yude.moe> | ||
42 | 1 Adrien Oliva <adrien.oliva@yapbreak.fr> | 44 | 1 Adrien Oliva <adrien.oliva@yapbreak.fr> |
43 | 1 Adrien le Maire <adrien@alemaire.be> | 45 | 1 Adrien le Maire <adrien@alemaire.be> |
44 | 1 Alexis J <alexis@effingo.be> | 46 | 1 Alexis J <alexis@effingo.be> |
@@ -65,6 +67,7 @@ | |||
65 | 1 Kevin Masson <kevin.masson@methodinthemadness.eu> | 67 | 1 Kevin Masson <kevin.masson@methodinthemadness.eu> |
66 | 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org> | 68 | 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org> |
67 | 1 Lionel Martin <renarddesmers@gmail.com> | 69 | 1 Lionel Martin <renarddesmers@gmail.com> |
70 | 1 Loïc Carr <zizou.xena@gmail.com> | ||
68 | 1 Mark Gerarts <mark.gerarts@gmail.com> | 71 | 1 Mark Gerarts <mark.gerarts@gmail.com> |
69 | 1 Marsup <marsup@gmail.com> | 72 | 1 Marsup <marsup@gmail.com> |
70 | 1 Paul van den Burg <github@paulvandenburg.nl> | 73 | 1 Paul van den Burg <github@paulvandenburg.nl> |
diff --git a/CHANGELOG.md b/CHANGELOG.md index f1686d67..18404049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
@@ -4,7 +4,55 @@ All notable changes to this project will be documented in this file. | |||
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) |
5 | and this project adheres to [Semantic Versioning](http://semver.org/). | 5 | and this project adheres to [Semantic Versioning](http://semver.org/). |
6 | 6 | ||
7 | ## [v0.12.1]() - UNRELEASED | 7 | ## [v0.12.2]() - UNRELEASED |
8 | |||
9 | ## [v0.12.1](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-11-12 | ||
10 | |||
11 | > nginx ([#1628](https://github.com/shaarli/Shaarli/pull/1628)) and Apache ([#1630](https://github.com/shaarli/Shaarli/pull/1630)) configurations have been reviewed. It is recommended that you | ||
12 | > update yours using [the documentation](https://shaarli.readthedocs.io/en/master/Server-configuration/). | ||
13 | > Users using official Docker image will receive updated configuration automatically. | ||
14 | |||
15 | ### Added | ||
16 | - Bulk creation of bookmarks | ||
17 | - Server administration tool page (and install page requirements) | ||
18 | - Support any tag separator, not just whitespaces | ||
19 | - Share a private bookmark using a URL with a token | ||
20 | - Add a setting to retrieve bookmark metadata asynchronously (enabled by default) | ||
21 | - Highlight fulltext search results | ||
22 | - Weekly and monthly view/RSS feed for daily page | ||
23 | - MarkdownExtra formatter | ||
24 | - Default formatter: add a setting to disable auto-linkification | ||
25 | - Add mutex on datastore I/O operations to prevent data loss | ||
26 | - PHP 8.0 support | ||
27 | - REST API: allow override of creation and update dates | ||
28 | - Add strict types for bookmarks management | ||
29 | |||
30 | ### Changed | ||
31 | - Improve regex and performances to extract HTML metadata (title, description, etc.) | ||
32 | - Support using Shaarli without URL rewriting (prefix URL with `/index.php/`) | ||
33 | - Improve the "Manage tags" tools page | ||
34 | - Use PSR-3 logger for login attempts | ||
35 | - Move utils classes to Shaarli\Helper namespace and folder | ||
36 | - Include php-simplexml in Docker image | ||
37 | - Raise 404 error instead of 500 if permalink access is denied | ||
38 | - Display error details even with dev.debug set to false | ||
39 | - Reviewed nginx configuration | ||
40 | - Reviewed Apache configuration | ||
41 | - Replace vimeo link in demo bookmarks due to IP ban on the demo instance | ||
42 | - Apply PSR-12 on code base, and add CI check using PHPCS | ||
43 | |||
44 | ### Fixed | ||
45 | - Compatiliby issue on login with PHP 7.1 | ||
46 | - Japanese translations update | ||
47 | - Redirect to referrer after bookmark deletion | ||
48 | - Inject ROOT_PATH in plugin instead of regenerating it everywhere | ||
49 | - Wallabag plugin: minor improvements | ||
50 | - REST API postLink: change relative path to absolute path | ||
51 | - Webpack: fix vintage theme images include | ||
52 | - Docker-compose: fix SSL certificate + add parameter for Docker tag | ||
53 | |||
54 | ### Removed | ||
55 | - `config.json.php` new lines in prefix/suffix to prevent issues with Windows PHP | ||
8 | 56 | ||
9 | ## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-10-13 | 57 | ## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-10-13 |
10 | 58 | ||
@@ -44,6 +44,7 @@ RUN apk --update --no-cache add \ | |||
44 | php7-openssl \ | 44 | php7-openssl \ |
45 | php7-session \ | 45 | php7-session \ |
46 | php7-xml \ | 46 | php7-xml \ |
47 | php7-simplexml \ | ||
47 | php7-zlib \ | 48 | php7-zlib \ |
48 | s6 | 49 | s6 |
49 | 50 | ||
@@ -27,10 +27,6 @@ PHPCS := $(BIN)/phpcs | |||
27 | code_sniffer: | 27 | code_sniffer: |
28 | @$(PHPCS) | 28 | @$(PHPCS) |
29 | 29 | ||
30 | ### - errors filtered by coding standard: PEAR, PSR1, PSR2, Zend... | ||
31 | PHPCS_%: | ||
32 | @$(PHPCS) --report-full --report-width=200 --standard=$* | ||
33 | |||
34 | ### - errors by Git author | 30 | ### - errors by Git author |
35 | code_sniffer_blame: | 31 | code_sniffer_blame: |
36 | @$(PHPCS) --report-gitblame | 32 | @$(PHPCS) --report-gitblame |
@@ -175,6 +171,7 @@ translate: | |||
175 | eslint: | 171 | eslint: |
176 | @yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/ | 172 | @yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/ |
177 | @yarn run eslint -c .dev/.eslintrc.js assets/default/js/ | 173 | @yarn run eslint -c .dev/.eslintrc.js assets/default/js/ |
174 | @yarn run eslint -c .dev/.eslintrc.js assets/common/js/ | ||
178 | 175 | ||
179 | ### Run CSSLint check against Shaarli's SCSS files | 176 | ### Run CSSLint check against Shaarli's SCSS files |
180 | sasslint: | 177 | sasslint: |
@@ -6,13 +6,13 @@ _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.10.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4) | 9 | [![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) |
10 | [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) | 10 | [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) |
11 | • | 11 | • |
12 | [![](https://img.shields.io/badge/latest-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) | 12 | [![](https://img.shields.io/badge/latest-v0.12.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) |
13 | [![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) | 13 | [![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) |
14 | • | 14 | • |
15 | [![](https://img.shields.io/badge/master-v0.11.x-blue.svg)](https://github.com/shaarli/Shaarli) | 15 | [![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli) |
16 | [![](https://img.shields.io/travis/shaarli/Shaarli.svg?label=master)](https://travis-ci.org/shaarli/Shaarli) | 16 | [![](https://img.shields.io/travis/shaarli/Shaarli.svg?label=master)](https://travis-ci.org/shaarli/Shaarli) |
17 | 17 | ||
18 | [![Join the chat at https://gitter.im/shaarli/Shaarli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shaarli/Shaarli) | 18 | [![Join the chat at https://gitter.im/shaarli/Shaarli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shaarli/Shaarli) |
diff --git a/application/History.php b/application/History.php index 4fd2f294..d230f39d 100644 --- a/application/History.php +++ b/application/History.php | |||
@@ -1,9 +1,11 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli; | 3 | namespace Shaarli; |
3 | 4 | ||
4 | use DateTime; | 5 | use DateTime; |
5 | use Exception; | 6 | use Exception; |
6 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Helper\FileUtils; | ||
7 | 9 | ||
8 | /** | 10 | /** |
9 | * Class History | 11 | * Class History |
@@ -30,27 +32,27 @@ class History | |||
30 | /** | 32 | /** |
31 | * @var string Action key: a new link has been created. | 33 | * @var string Action key: a new link has been created. |
32 | */ | 34 | */ |
33 | const CREATED = 'CREATED'; | 35 | public const CREATED = 'CREATED'; |
34 | 36 | ||
35 | /** | 37 | /** |
36 | * @var string Action key: a link has been updated. | 38 | * @var string Action key: a link has been updated. |
37 | */ | 39 | */ |
38 | const UPDATED = 'UPDATED'; | 40 | public const UPDATED = 'UPDATED'; |
39 | 41 | ||
40 | /** | 42 | /** |
41 | * @var string Action key: a link has been deleted. | 43 | * @var string Action key: a link has been deleted. |
42 | */ | 44 | */ |
43 | const DELETED = 'DELETED'; | 45 | public const DELETED = 'DELETED'; |
44 | 46 | ||
45 | /** | 47 | /** |
46 | * @var string Action key: settings have been updated. | 48 | * @var string Action key: settings have been updated. |
47 | */ | 49 | */ |
48 | const SETTINGS = 'SETTINGS'; | 50 | public const SETTINGS = 'SETTINGS'; |
49 | 51 | ||
50 | /** | 52 | /** |
51 | * @var string Action key: a bulk import has been processed. | 53 | * @var string Action key: a bulk import has been processed. |
52 | */ | 54 | */ |
53 | const IMPORT = 'IMPORT'; | 55 | public const IMPORT = 'IMPORT'; |
54 | 56 | ||
55 | /** | 57 | /** |
56 | * @var string History file path. | 58 | * @var string History file path. |
diff --git a/application/Languages.php b/application/Languages.php index d83e0765..60e91631 100644 --- a/application/Languages.php +++ b/application/Languages.php | |||
@@ -41,7 +41,7 @@ class Languages | |||
41 | /** | 41 | /** |
42 | * Core translations domain | 42 | * Core translations domain |
43 | */ | 43 | */ |
44 | const DEFAULT_DOMAIN = 'shaarli'; | 44 | public const DEFAULT_DOMAIN = 'shaarli'; |
45 | 45 | ||
46 | /** | 46 | /** |
47 | * @var TranslatorInterface | 47 | * @var TranslatorInterface |
@@ -76,7 +76,8 @@ class Languages | |||
76 | $this->language = $confLanguage; | 76 | $this->language = $confLanguage; |
77 | } | 77 | } |
78 | 78 | ||
79 | if (! extension_loaded('gettext') | 79 | if ( |
80 | ! extension_loaded('gettext') | ||
80 | || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php']) | 81 | || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php']) |
81 | ) { | 82 | ) { |
82 | $this->initPhpTranslator(); | 83 | $this->initPhpTranslator(); |
@@ -98,7 +99,7 @@ class Languages | |||
98 | $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages'); | 99 | $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages'); |
99 | 100 | ||
100 | // Default extension translation from the current theme | 101 | // Default extension translation from the current theme |
101 | $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language'; | 102 | $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $this->conf->get('theme') . '/language'; |
102 | if (is_dir($themeTransFolder)) { | 103 | if (is_dir($themeTransFolder)) { |
103 | $this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false); | 104 | $this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false); |
104 | } | 105 | } |
@@ -121,7 +122,9 @@ class Languages | |||
121 | $translations = new Translations(); | 122 | $translations = new Translations(); |
122 | // Core translations | 123 | // Core translations |
123 | try { | 124 | try { |
124 | $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po'); | 125 | $translations = $translations->addFromPoFile( |
126 | 'inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po' | ||
127 | ); | ||
125 | $translations->setDomain('shaarli'); | 128 | $translations->setDomain('shaarli'); |
126 | $this->translator->loadTranslations($translations); | 129 | $this->translator->loadTranslations($translations); |
127 | } catch (\InvalidArgumentException $e) { | 130 | } catch (\InvalidArgumentException $e) { |
@@ -129,11 +132,11 @@ class Languages | |||
129 | 132 | ||
130 | // Default extension translation from the current theme | 133 | // Default extension translation from the current theme |
131 | $theme = $this->conf->get('theme'); | 134 | $theme = $this->conf->get('theme'); |
132 | $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language'; | 135 | $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $theme . '/language'; |
133 | if (is_dir($themeTransFolder)) { | 136 | if (is_dir($themeTransFolder)) { |
134 | try { | 137 | try { |
135 | $translations = Translations::fromPoFile( | 138 | $translations = Translations::fromPoFile( |
136 | $themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po' | 139 | $themeTransFolder . '/' . $this->language . '/LC_MESSAGES/' . $theme . '.po' |
137 | ); | 140 | ); |
138 | $translations->setDomain($theme); | 141 | $translations->setDomain($theme); |
139 | $this->translator->loadTranslations($translations); | 142 | $this->translator->loadTranslations($translations); |
@@ -149,7 +152,7 @@ class Languages | |||
149 | 152 | ||
150 | try { | 153 | try { |
151 | $extension = Translations::fromPoFile( | 154 | $extension = Translations::fromPoFile( |
152 | $translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po' | 155 | $translationPath . $this->language . '/LC_MESSAGES/' . $domain . '.po' |
153 | ); | 156 | ); |
154 | $extension->setDomain($domain); | 157 | $extension->setDomain($domain); |
155 | $this->translator->loadTranslations($extension); | 158 | $this->translator->loadTranslations($extension); |
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php index 5aec23c8..c4ff8d7a 100644 --- a/application/Thumbnailer.php +++ b/application/Thumbnailer.php | |||
@@ -13,7 +13,7 @@ use WebThumbnailer\WebThumbnailer; | |||
13 | */ | 13 | */ |
14 | class Thumbnailer | 14 | class Thumbnailer |
15 | { | 15 | { |
16 | const COMMON_MEDIA_DOMAINS = [ | 16 | protected const COMMON_MEDIA_DOMAINS = [ |
17 | 'imgur.com', | 17 | 'imgur.com', |
18 | 'flickr.com', | 18 | 'flickr.com', |
19 | 'youtube.com', | 19 | 'youtube.com', |
@@ -31,9 +31,9 @@ class Thumbnailer | |||
31 | 'deviantart.com', | 31 | 'deviantart.com', |
32 | ]; | 32 | ]; |
33 | 33 | ||
34 | const MODE_ALL = 'all'; | 34 | public const MODE_ALL = 'all'; |
35 | const MODE_COMMON = 'common'; | 35 | public const MODE_COMMON = 'common'; |
36 | const MODE_NONE = 'none'; | 36 | public const MODE_NONE = 'none'; |
37 | 37 | ||
38 | /** | 38 | /** |
39 | * @var WebThumbnailer instance. | 39 | * @var WebThumbnailer instance. |
@@ -60,7 +60,7 @@ class Thumbnailer | |||
60 | // TODO: create a proper error handling system able to catch exceptions... | 60 | // TODO: create a proper error handling system able to catch exceptions... |
61 | die(t( | 61 | die(t( |
62 | 'php-gd extension must be loaded to use thumbnails. ' | 62 | 'php-gd extension must be loaded to use thumbnails. ' |
63 | .'Thumbnails are now disabled. Please reload the page.' | 63 | . 'Thumbnails are now disabled. Please reload the page.' |
64 | )); | 64 | )); |
65 | } | 65 | } |
66 | 66 | ||
@@ -81,7 +81,8 @@ class Thumbnailer | |||
81 | */ | 81 | */ |
82 | public function get($url) | 82 | public function get($url) |
83 | { | 83 | { |
84 | if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON | 84 | if ( |
85 | $this->conf->get('thumbnails.mode') === self::MODE_COMMON | ||
85 | && ! $this->isCommonMediaOrImage($url) | 86 | && ! $this->isCommonMediaOrImage($url) |
86 | ) { | 87 | ) { |
87 | return false; | 88 | return false; |
diff --git a/application/TimeZone.php b/application/TimeZone.php index c1869ef8..a420eb96 100644 --- a/application/TimeZone.php +++ b/application/TimeZone.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Generates a list of available timezone continents and cities. | 4 | * Generates a list of available timezone continents and cities. |
4 | * | 5 | * |
@@ -43,7 +44,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '') | |||
43 | // Try to split the provided timezone | 44 | // Try to split the provided timezone |
44 | $spos = strpos($preselectedTimezone, '/'); | 45 | $spos = strpos($preselectedTimezone, '/'); |
45 | $pcontinent = substr($preselectedTimezone, 0, $spos); | 46 | $pcontinent = substr($preselectedTimezone, 0, $spos); |
46 | $pcity = substr($preselectedTimezone, $spos+1); | 47 | $pcity = substr($preselectedTimezone, $spos + 1); |
47 | } | 48 | } |
48 | 49 | ||
49 | $continents = []; | 50 | $continents = []; |
@@ -60,7 +61,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '') | |||
60 | } | 61 | } |
61 | 62 | ||
62 | $continent = substr($tz, 0, $spos); | 63 | $continent = substr($tz, 0, $spos); |
63 | $city = substr($tz, $spos+1); | 64 | $city = substr($tz, $spos + 1); |
64 | $cities[] = ['continent' => $continent, 'city' => $city]; | 65 | $cities[] = ['continent' => $continent, 'city' => $city]; |
65 | $continents[$continent] = true; | 66 | $continents[$continent] = true; |
66 | } | 67 | } |
@@ -85,7 +86,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '') | |||
85 | function isTimeZoneValid($continent, $city) | 86 | function isTimeZoneValid($continent, $city) |
86 | { | 87 | { |
87 | return in_array( | 88 | return in_array( |
88 | $continent.'/'.$city, | 89 | $continent . '/' . $city, |
89 | timezone_identifiers_list() | 90 | timezone_identifiers_list() |
90 | ); | 91 | ); |
91 | } | 92 | } |
diff --git a/application/Utils.php b/application/Utils.php index bcfda65c..952378ab 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -1,24 +1,27 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Shaarli utilities | 4 | * Shaarli utilities |
4 | */ | 5 | */ |
5 | 6 | ||
6 | /** | 7 | /** |
7 | * Logs a message to a text file | 8 | * Format log using provided data. |
8 | * | 9 | * |
9 | * The log format is compatible with fail2ban. | 10 | * @param string $message the message to log |
11 | * @param string|null $clientIp the client's remote IPv4/IPv6 address | ||
10 | * | 12 | * |
11 | * @param string $logFile where to write the logs | 13 | * @return string Formatted message to log |
12 | * @param string $clientIp the client's remote IPv4/IPv6 address | ||
13 | * @param string $message the message to log | ||
14 | */ | 14 | */ |
15 | function logm($logFile, $clientIp, $message) | 15 | function format_log(string $message, string $clientIp = null): string |
16 | { | 16 | { |
17 | file_put_contents( | 17 | $out = $message; |
18 | $logFile, | 18 | |
19 | date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL, | 19 | if (!empty($clientIp)) { |
20 | FILE_APPEND | 20 | // Note: we keep the first dash to avoid breaking fail2ban configs |
21 | ); | 21 | $out = '- ' . $clientIp . ' - ' . $out; |
22 | } | ||
23 | |||
24 | return $out; | ||
22 | } | 25 | } |
23 | 26 | ||
24 | /** | 27 | /** |
@@ -100,7 +103,7 @@ function escape($input) | |||
100 | } | 103 | } |
101 | 104 | ||
102 | if (is_array($input)) { | 105 | if (is_array($input)) { |
103 | $out = array(); | 106 | $out = []; |
104 | foreach ($input as $key => $value) { | 107 | foreach ($input as $key => $value) { |
105 | $out[escape($key)] = escape($value); | 108 | $out[escape($key)] = escape($value); |
106 | } | 109 | } |
@@ -161,7 +164,7 @@ function checkDateFormat($format, $string) | |||
161 | * | 164 | * |
162 | * @return string $referer - final referer. | 165 | * @return string $referer - final referer. |
163 | */ | 166 | */ |
164 | function generateLocation($referer, $host, $loopTerms = array()) | 167 | function generateLocation($referer, $host, $loopTerms = []) |
165 | { | 168 | { |
166 | $finalReferer = './?'; | 169 | $finalReferer = './?'; |
167 | 170 | ||
@@ -194,7 +197,7 @@ function generateLocation($referer, $host, $loopTerms = array()) | |||
194 | function autoLocale($headerLocale) | 197 | function autoLocale($headerLocale) |
195 | { | 198 | { |
196 | // Default if browser does not send HTTP_ACCEPT_LANGUAGE | 199 | // Default if browser does not send HTTP_ACCEPT_LANGUAGE |
197 | $locales = array('en_US', 'en_US.utf8', 'en_US.UTF-8'); | 200 | $locales = ['en_US', 'en_US.utf8', 'en_US.UTF-8']; |
198 | if (! empty($headerLocale)) { | 201 | if (! empty($headerLocale)) { |
199 | if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) { | 202 | if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) { |
200 | $attempts = []; | 203 | $attempts = []; |
@@ -325,6 +328,23 @@ function format_date($date, $time = true, $intl = true) | |||
325 | } | 328 | } |
326 | 329 | ||
327 | /** | 330 | /** |
331 | * Format the date month according to the locale. | ||
332 | * | ||
333 | * @param DateTimeInterface $date to format. | ||
334 | * | ||
335 | * @return bool|string Formatted date, or false if the input is invalid. | ||
336 | */ | ||
337 | function format_month(DateTimeInterface $date) | ||
338 | { | ||
339 | if (! $date instanceof DateTimeInterface) { | ||
340 | return false; | ||
341 | } | ||
342 | |||
343 | return strftime('%B', $date->getTimestamp()); | ||
344 | } | ||
345 | |||
346 | |||
347 | /** | ||
328 | * Check if the input is an integer, no matter its real type. | 348 | * Check if the input is an integer, no matter its real type. |
329 | * | 349 | * |
330 | * PHP is a bit messy regarding this: | 350 | * PHP is a bit messy regarding this: |
@@ -357,13 +377,15 @@ function return_bytes($val) | |||
357 | return $val; | 377 | return $val; |
358 | } | 378 | } |
359 | $val = trim($val); | 379 | $val = trim($val); |
360 | $last = strtolower($val[strlen($val)-1]); | 380 | $last = strtolower($val[strlen($val) - 1]); |
361 | $val = intval(substr($val, 0, -1)); | 381 | $val = intval(substr($val, 0, -1)); |
362 | switch ($last) { | 382 | switch ($last) { |
363 | case 'g': | 383 | case 'g': |
364 | $val *= 1024; | 384 | $val *= 1024; |
385 | // do no break in order 1024^2 for each unit | ||
365 | case 'm': | 386 | case 'm': |
366 | $val *= 1024; | 387 | $val *= 1024; |
388 | // do no break in order 1024^2 for each unit | ||
367 | case 'k': | 389 | case 'k': |
368 | $val *= 1024; | 390 | $val *= 1024; |
369 | } | 391 | } |
@@ -452,14 +474,28 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) | |||
452 | * Wrapper function for translation which match the API | 474 | * Wrapper function for translation which match the API |
453 | * of gettext()/_() and ngettext(). | 475 | * of gettext()/_() and ngettext(). |
454 | * | 476 | * |
455 | * @param string $text Text to translate. | 477 | * @param string $text Text to translate. |
456 | * @param string $nText The plural message ID. | 478 | * @param string $nText The plural message ID. |
457 | * @param int $nb The number of items for plural forms. | 479 | * @param int $nb The number of items for plural forms. |
458 | * @param string $domain The domain where the translation is stored (default: shaarli). | 480 | * @param string $domain The domain where the translation is stored (default: shaarli). |
481 | * @param array $variables Associative array of variables to replace in translated text. | ||
482 | * @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables. | ||
459 | * | 483 | * |
460 | * @return string Text translated. | 484 | * @return string Text translated. |
461 | */ | 485 | */ |
462 | function t($text, $nText = '', $nb = 1, $domain = 'shaarli') | 486 | function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false) |
487 | { | ||
488 | $postFunction = $fixCase ? 'ucfirst' : function ($input) { | ||
489 | return $input; | ||
490 | }; | ||
491 | |||
492 | return $postFunction(dn__($domain, $text, $nText, $nb, $variables)); | ||
493 | } | ||
494 | |||
495 | /** | ||
496 | * Converts an exception into a printable stack trace string. | ||
497 | */ | ||
498 | function exception2text(Throwable $e): string | ||
463 | { | 499 | { |
464 | return dn__($domain, $text, $nText, $nb); | 500 | return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString(); |
465 | } | 501 | } |
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php index f5b53b01..9fb88358 100644 --- a/application/api/ApiMiddleware.php +++ b/application/api/ApiMiddleware.php | |||
@@ -1,6 +1,8 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Api; | 3 | namespace Shaarli\Api; |
3 | 4 | ||
5 | use malkusch\lock\mutex\FlockMutex; | ||
4 | use Shaarli\Api\Exceptions\ApiAuthorizationException; | 6 | use Shaarli\Api\Exceptions\ApiAuthorizationException; |
5 | use Shaarli\Api\Exceptions\ApiException; | 7 | use Shaarli\Api\Exceptions\ApiException; |
6 | use Shaarli\Bookmark\BookmarkFileService; | 8 | use Shaarli\Bookmark\BookmarkFileService; |
@@ -107,7 +109,8 @@ class ApiMiddleware | |||
107 | */ | 109 | */ |
108 | protected function checkToken($request) | 110 | protected function checkToken($request) |
109 | { | 111 | { |
110 | if (!$request->hasHeader('Authorization') | 112 | if ( |
113 | !$request->hasHeader('Authorization') | ||
111 | && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION']) | 114 | && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION']) |
112 | ) { | 115 | ) { |
113 | throw new ApiAuthorizationException('JWT token not provided'); | 116 | throw new ApiAuthorizationException('JWT token not provided'); |
@@ -143,6 +146,7 @@ class ApiMiddleware | |||
143 | $linkDb = new BookmarkFileService( | 146 | $linkDb = new BookmarkFileService( |
144 | $conf, | 147 | $conf, |
145 | $this->container->get('history'), | 148 | $this->container->get('history'), |
149 | new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2), | ||
146 | true | 150 | true |
147 | ); | 151 | ); |
148 | $this->container['db'] = $linkDb; | 152 | $this->container['db'] = $linkDb; |
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index faebb8f5..05a2840a 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Api; | 3 | namespace Shaarli\Api; |
3 | 4 | ||
4 | use Shaarli\Api\Exceptions\ApiAuthorizationException; | 5 | use Shaarli\Api\Exceptions\ApiAuthorizationException; |
@@ -27,7 +28,7 @@ class ApiUtils | |||
27 | throw new ApiAuthorizationException('Malformed JWT token'); | 28 | throw new ApiAuthorizationException('Malformed JWT token'); |
28 | } | 29 | } |
29 | 30 | ||
30 | $genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] .'.'. $parts[1], $secret, true)); | 31 | $genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] . '.' . $parts[1], $secret, true)); |
31 | if ($parts[2] != $genSign) { | 32 | if ($parts[2] != $genSign) { |
32 | throw new ApiAuthorizationException('Invalid JWT signature'); | 33 | throw new ApiAuthorizationException('Invalid JWT signature'); |
33 | } | 34 | } |
@@ -42,7 +43,8 @@ class ApiUtils | |||
42 | throw new ApiAuthorizationException('Invalid JWT payload'); | 43 | throw new ApiAuthorizationException('Invalid JWT payload'); |
43 | } | 44 | } |
44 | 45 | ||
45 | if (empty($payload->iat) | 46 | if ( |
47 | empty($payload->iat) | ||
46 | || $payload->iat > time() | 48 | || $payload->iat > time() |
47 | || time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION | 49 | || time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION |
48 | ) { | 50 | ) { |
@@ -89,12 +91,12 @@ class ApiUtils | |||
89 | * If no URL is provided, it will generate a local note URL. | 91 | * If no URL is provided, it will generate a local note URL. |
90 | * If no title is provided, it will use the URL as title. | 92 | * If no title is provided, it will use the URL as title. |
91 | * | 93 | * |
92 | * @param array $input Request Link. | 94 | * @param array|null $input Request Link. |
93 | * @param bool $defaultPrivate Request Link. | 95 | * @param bool $defaultPrivate Setting defined if a bookmark is private by default. |
94 | * | 96 | * |
95 | * @return Bookmark instance. | 97 | * @return Bookmark instance. |
96 | */ | 98 | */ |
97 | public static function buildLinkFromRequest($input, $defaultPrivate) | 99 | public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark |
98 | { | 100 | { |
99 | $bookmark = new Bookmark(); | 101 | $bookmark = new Bookmark(); |
100 | $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; | 102 | $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; |
@@ -110,6 +112,15 @@ class ApiUtils | |||
110 | $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); | 112 | $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); |
111 | $bookmark->setPrivate($private); | 113 | $bookmark->setPrivate($private); |
112 | 114 | ||
115 | $created = \DateTime::createFromFormat(\DateTime::ATOM, $input['created'] ?? ''); | ||
116 | if ($created instanceof \DateTimeInterface) { | ||
117 | $bookmark->setCreated($created); | ||
118 | } | ||
119 | $updated = \DateTime::createFromFormat(\DateTime::ATOM, $input['updated'] ?? ''); | ||
120 | if ($updated instanceof \DateTimeInterface) { | ||
121 | $bookmark->setUpdated($updated); | ||
122 | } | ||
123 | |||
113 | return $bookmark; | 124 | return $bookmark; |
114 | } | 125 | } |
115 | 126 | ||
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php index c4b3d0c3..88a845eb 100644 --- a/application/api/controllers/ApiController.php +++ b/application/api/controllers/ApiController.php | |||
@@ -4,6 +4,7 @@ namespace Shaarli\Api\Controllers; | |||
4 | 4 | ||
5 | use Shaarli\Bookmark\BookmarkServiceInterface; | 5 | use Shaarli\Bookmark\BookmarkServiceInterface; |
6 | use Shaarli\Config\ConfigManager; | 6 | use Shaarli\Config\ConfigManager; |
7 | use Shaarli\History; | ||
7 | use Slim\Container; | 8 | use Slim\Container; |
8 | 9 | ||
9 | /** | 10 | /** |
@@ -31,7 +32,7 @@ abstract class ApiController | |||
31 | protected $bookmarkService; | 32 | protected $bookmarkService; |
32 | 33 | ||
33 | /** | 34 | /** |
34 | * @var HistoryController | 35 | * @var History |
35 | */ | 36 | */ |
36 | protected $history; | 37 | protected $history; |
37 | 38 | ||
diff --git a/application/api/controllers/HistoryController.php b/application/api/controllers/HistoryController.php index 505647a9..d83a3a25 100644 --- a/application/api/controllers/HistoryController.php +++ b/application/api/controllers/HistoryController.php | |||
@@ -1,6 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | |||
4 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
5 | 4 | ||
6 | use Shaarli\Api\Exceptions\ApiBadParametersException; | 5 | use Shaarli\Api\Exceptions\ApiBadParametersException; |
diff --git a/application/api/controllers/Info.php b/application/api/controllers/Info.php index 12f6b2f0..ae7db93e 100644 --- a/application/api/controllers/Info.php +++ b/application/api/controllers/Info.php | |||
@@ -29,13 +29,13 @@ class Info extends ApiController | |||
29 | $info = [ | 29 | $info = [ |
30 | 'global_counter' => $this->bookmarkService->count(), | 30 | 'global_counter' => $this->bookmarkService->count(), |
31 | 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE), | 31 | 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE), |
32 | 'settings' => array( | 32 | 'settings' => [ |
33 | 'title' => $this->conf->get('general.title', 'Shaarli'), | 33 | 'title' => $this->conf->get('general.title', 'Shaarli'), |
34 | 'header_link' => $this->conf->get('general.header_link', '?'), | 34 | 'header_link' => $this->conf->get('general.header_link', '?'), |
35 | 'timezone' => $this->conf->get('general.timezone', 'UTC'), | 35 | 'timezone' => $this->conf->get('general.timezone', 'UTC'), |
36 | 'enabled_plugins' => $this->conf->get('general.enabled_plugins', []), | 36 | 'enabled_plugins' => $this->conf->get('general.enabled_plugins', []), |
37 | 'default_private_links' => $this->conf->get('privacy.default_private_links', false), | 37 | 'default_private_links' => $this->conf->get('privacy.default_private_links', false), |
38 | ), | 38 | ], |
39 | ]; | 39 | ]; |
40 | 40 | ||
41 | return $response->withJson($info, 200, $this->jsonStyle); | 41 | return $response->withJson($info, 200, $this->jsonStyle); |
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 29247950..c379b962 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php | |||
@@ -96,11 +96,12 @@ class Links extends ApiController | |||
96 | */ | 96 | */ |
97 | public function getLink($request, $response, $args) | 97 | public function getLink($request, $response, $args) |
98 | { | 98 | { |
99 | if (!$this->bookmarkService->exists($args['id'])) { | 99 | $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null; |
100 | if ($id === null || ! $this->bookmarkService->exists($id)) { | ||
100 | throw new ApiLinkNotFoundException(); | 101 | throw new ApiLinkNotFoundException(); |
101 | } | 102 | } |
102 | $index = index_url($this->ci['environment']); | 103 | $index = index_url($this->ci['environment']); |
103 | $out = ApiUtils::formatLink($this->bookmarkService->get($args['id']), $index); | 104 | $out = ApiUtils::formatLink($this->bookmarkService->get($id), $index); |
104 | 105 | ||
105 | return $response->withJson($out, 200, $this->jsonStyle); | 106 | return $response->withJson($out, 200, $this->jsonStyle); |
106 | } | 107 | } |
@@ -115,10 +116,11 @@ class Links extends ApiController | |||
115 | */ | 116 | */ |
116 | public function postLink($request, $response) | 117 | public function postLink($request, $response) |
117 | { | 118 | { |
118 | $data = $request->getParsedBody(); | 119 | $data = (array) ($request->getParsedBody() ?? []); |
119 | $bookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); | 120 | $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); |
120 | // duplicate by URL, return 409 Conflict | 121 | // duplicate by URL, return 409 Conflict |
121 | if (! empty($bookmark->getUrl()) | 122 | if ( |
123 | ! empty($bookmark->getUrl()) | ||
122 | && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) | 124 | && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) |
123 | ) { | 125 | ) { |
124 | return $response->withJson( | 126 | return $response->withJson( |
@@ -130,7 +132,7 @@ class Links extends ApiController | |||
130 | 132 | ||
131 | $this->bookmarkService->add($bookmark); | 133 | $this->bookmarkService->add($bookmark); |
132 | $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment'])); | 134 | $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment'])); |
133 | $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]); | 135 | $redirect = $this->ci->router->pathFor('getLink', ['id' => $bookmark->getId()]); |
134 | return $response->withAddedHeader('Location', $redirect) | 136 | return $response->withAddedHeader('Location', $redirect) |
135 | ->withJson($out, 201, $this->jsonStyle); | 137 | ->withJson($out, 201, $this->jsonStyle); |
136 | } | 138 | } |
@@ -148,18 +150,20 @@ class Links extends ApiController | |||
148 | */ | 150 | */ |
149 | public function putLink($request, $response, $args) | 151 | public function putLink($request, $response, $args) |
150 | { | 152 | { |
151 | if (! $this->bookmarkService->exists($args['id'])) { | 153 | $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null; |
154 | if ($id === null || !$this->bookmarkService->exists($id)) { | ||
152 | throw new ApiLinkNotFoundException(); | 155 | throw new ApiLinkNotFoundException(); |
153 | } | 156 | } |
154 | 157 | ||
155 | $index = index_url($this->ci['environment']); | 158 | $index = index_url($this->ci['environment']); |
156 | $data = $request->getParsedBody(); | 159 | $data = $request->getParsedBody(); |
157 | 160 | ||
158 | $requestBookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); | 161 | $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); |
159 | // duplicate URL on a different link, return 409 Conflict | 162 | // duplicate URL on a different link, return 409 Conflict |
160 | if (! empty($requestBookmark->getUrl()) | 163 | if ( |
164 | ! empty($requestBookmark->getUrl()) | ||
161 | && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) | 165 | && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) |
162 | && $dup->getId() != $args['id'] | 166 | && $dup->getId() != $id |
163 | ) { | 167 | ) { |
164 | return $response->withJson( | 168 | return $response->withJson( |
165 | ApiUtils::formatLink($dup, $index), | 169 | ApiUtils::formatLink($dup, $index), |
@@ -168,7 +172,7 @@ class Links extends ApiController | |||
168 | ); | 172 | ); |
169 | } | 173 | } |
170 | 174 | ||
171 | $responseBookmark = $this->bookmarkService->get($args['id']); | 175 | $responseBookmark = $this->bookmarkService->get($id); |
172 | $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark); | 176 | $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark); |
173 | $this->bookmarkService->set($responseBookmark); | 177 | $this->bookmarkService->set($responseBookmark); |
174 | 178 | ||
@@ -189,10 +193,11 @@ class Links extends ApiController | |||
189 | */ | 193 | */ |
190 | public function deleteLink($request, $response, $args) | 194 | public function deleteLink($request, $response, $args) |
191 | { | 195 | { |
192 | if (! $this->bookmarkService->exists($args['id'])) { | 196 | $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null; |
197 | if ($id === null || !$this->bookmarkService->exists($id)) { | ||
193 | throw new ApiLinkNotFoundException(); | 198 | throw new ApiLinkNotFoundException(); |
194 | } | 199 | } |
195 | $bookmark = $this->bookmarkService->get($args['id']); | 200 | $bookmark = $this->bookmarkService->get($id); |
196 | $this->bookmarkService->remove($bookmark); | 201 | $this->bookmarkService->remove($bookmark); |
197 | 202 | ||
198 | return $response->withStatus(204); | 203 | return $response->withStatus(204); |
diff --git a/application/api/exceptions/ApiAuthorizationException.php b/application/api/exceptions/ApiAuthorizationException.php index 0e3f4776..c77e9eea 100644 --- a/application/api/exceptions/ApiAuthorizationException.php +++ b/application/api/exceptions/ApiAuthorizationException.php | |||
@@ -28,7 +28,7 @@ class ApiAuthorizationException extends ApiException | |||
28 | */ | 28 | */ |
29 | public function setMessage($message) | 29 | public function setMessage($message) |
30 | { | 30 | { |
31 | $original = $this->debug === true ? ': '. $this->getMessage() : ''; | 31 | $original = $this->debug === true ? ': ' . $this->getMessage() : ''; |
32 | $this->message = $message . $original; | 32 | $this->message = $message . $original; |
33 | } | 33 | } |
34 | } | 34 | } |
diff --git a/application/api/exceptions/ApiException.php b/application/api/exceptions/ApiException.php index d6b66323..7deafb96 100644 --- a/application/api/exceptions/ApiException.php +++ b/application/api/exceptions/ApiException.php | |||
@@ -44,7 +44,7 @@ abstract class ApiException extends \Exception | |||
44 | } | 44 | } |
45 | return [ | 45 | return [ |
46 | 'message' => $this->getMessage(), | 46 | 'message' => $this->getMessage(), |
47 | 'stacktrace' => get_class($this) .': '. $this->getTraceAsString() | 47 | 'stacktrace' => get_class($this) . ': ' . $this->getTraceAsString() |
48 | ]; | 48 | ]; |
49 | } | 49 | } |
50 | 50 | ||
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index 1beb8be2..4238ef25 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php | |||
@@ -1,5 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
5 | use DateTime; | 7 | use DateTime; |
@@ -17,7 +19,7 @@ use Shaarli\Bookmark\Exception\InvalidBookmarkException; | |||
17 | class Bookmark | 19 | class Bookmark |
18 | { | 20 | { |
19 | /** @var string Date format used in string (former ID format) */ | 21 | /** @var string Date format used in string (former ID format) */ |
20 | const LINK_DATE_FORMAT = 'Ymd_His'; | 22 | public const LINK_DATE_FORMAT = 'Ymd_His'; |
21 | 23 | ||
22 | /** @var int Bookmark ID */ | 24 | /** @var int Bookmark ID */ |
23 | protected $id; | 25 | protected $id; |
@@ -52,32 +54,37 @@ class Bookmark | |||
52 | /** @var bool True if the bookmark can only be seen while logged in */ | 54 | /** @var bool True if the bookmark can only be seen while logged in */ |
53 | protected $private; | 55 | protected $private; |
54 | 56 | ||
57 | /** @var mixed[] Available to store any additional content for a bookmark. Currently used for search highlight. */ | ||
58 | protected $additionalContent = []; | ||
59 | |||
55 | /** | 60 | /** |
56 | * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. | 61 | * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. |
57 | * | 62 | * |
58 | * @param array $data | 63 | * @param array $data |
64 | * @param string $tagsSeparator Tags separator loaded from the config file. | ||
65 | * This is a context data, and it should *never* be stored in the Bookmark object. | ||
59 | * | 66 | * |
60 | * @return $this | 67 | * @return $this |
61 | */ | 68 | */ |
62 | public function fromArray($data) | 69 | public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark |
63 | { | 70 | { |
64 | $this->id = $data['id']; | 71 | $this->id = $data['id'] ?? null; |
65 | $this->shortUrl = $data['shorturl']; | 72 | $this->shortUrl = $data['shorturl'] ?? null; |
66 | $this->url = $data['url']; | 73 | $this->url = $data['url'] ?? null; |
67 | $this->title = $data['title']; | 74 | $this->title = $data['title'] ?? null; |
68 | $this->description = $data['description']; | 75 | $this->description = $data['description'] ?? null; |
69 | $this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null; | 76 | $this->thumbnail = $data['thumbnail'] ?? null; |
70 | $this->sticky = isset($data['sticky']) ? $data['sticky'] : false; | 77 | $this->sticky = $data['sticky'] ?? false; |
71 | $this->created = $data['created']; | 78 | $this->created = $data['created'] ?? null; |
72 | if (is_array($data['tags'])) { | 79 | if (is_array($data['tags'])) { |
73 | $this->tags = $data['tags']; | 80 | $this->tags = $data['tags']; |
74 | } else { | 81 | } else { |
75 | $this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY); | 82 | $this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator); |
76 | } | 83 | } |
77 | if (! empty($data['updated'])) { | 84 | if (! empty($data['updated'])) { |
78 | $this->updated = $data['updated']; | 85 | $this->updated = $data['updated']; |
79 | } | 86 | } |
80 | $this->private = $data['private'] ? true : false; | 87 | $this->private = ($data['private'] ?? false) ? true : false; |
81 | 88 | ||
82 | return $this; | 89 | return $this; |
83 | } | 90 | } |
@@ -93,24 +100,29 @@ class Bookmark | |||
93 | * - the URL with the permalink | 100 | * - the URL with the permalink |
94 | * - the title with the URL | 101 | * - the title with the URL |
95 | * | 102 | * |
103 | * Also make sure that we do not save search highlights in the datastore. | ||
104 | * | ||
96 | * @throws InvalidBookmarkException | 105 | * @throws InvalidBookmarkException |
97 | */ | 106 | */ |
98 | public function validate() | 107 | public function validate(): void |
99 | { | 108 | { |
100 | if ($this->id === null | 109 | if ( |
110 | $this->id === null | ||
101 | || ! is_int($this->id) | 111 | || ! is_int($this->id) |
102 | || empty($this->shortUrl) | 112 | || empty($this->shortUrl) |
103 | || empty($this->created) | 113 | || empty($this->created) |
104 | || ! $this->created instanceof DateTimeInterface | ||
105 | ) { | 114 | ) { |
106 | throw new InvalidBookmarkException($this); | 115 | throw new InvalidBookmarkException($this); |
107 | } | 116 | } |
108 | if (empty($this->url)) { | 117 | if (empty($this->url)) { |
109 | $this->url = '/shaare/'. $this->shortUrl; | 118 | $this->url = '/shaare/' . $this->shortUrl; |
110 | } | 119 | } |
111 | if (empty($this->title)) { | 120 | if (empty($this->title)) { |
112 | $this->title = $this->url; | 121 | $this->title = $this->url; |
113 | } | 122 | } |
123 | if (array_key_exists('search_highlight', $this->additionalContent)) { | ||
124 | unset($this->additionalContent['search_highlight']); | ||
125 | } | ||
114 | } | 126 | } |
115 | 127 | ||
116 | /** | 128 | /** |
@@ -119,11 +131,11 @@ class Bookmark | |||
119 | * - created: with the current datetime | 131 | * - created: with the current datetime |
120 | * - shortUrl: with a generated small hash from the date and the given ID | 132 | * - shortUrl: with a generated small hash from the date and the given ID |
121 | * | 133 | * |
122 | * @param int $id | 134 | * @param int|null $id |
123 | * | 135 | * |
124 | * @return Bookmark | 136 | * @return Bookmark |
125 | */ | 137 | */ |
126 | public function setId($id) | 138 | public function setId(?int $id): Bookmark |
127 | { | 139 | { |
128 | $this->id = $id; | 140 | $this->id = $id; |
129 | if (empty($this->created)) { | 141 | if (empty($this->created)) { |
@@ -139,9 +151,9 @@ class Bookmark | |||
139 | /** | 151 | /** |
140 | * Get the Id. | 152 | * Get the Id. |
141 | * | 153 | * |
142 | * @return int | 154 | * @return int|null |
143 | */ | 155 | */ |
144 | public function getId() | 156 | public function getId(): ?int |
145 | { | 157 | { |
146 | return $this->id; | 158 | return $this->id; |
147 | } | 159 | } |
@@ -149,9 +161,9 @@ class Bookmark | |||
149 | /** | 161 | /** |
150 | * Get the ShortUrl. | 162 | * Get the ShortUrl. |
151 | * | 163 | * |
152 | * @return string | 164 | * @return string|null |
153 | */ | 165 | */ |
154 | public function getShortUrl() | 166 | public function getShortUrl(): ?string |
155 | { | 167 | { |
156 | return $this->shortUrl; | 168 | return $this->shortUrl; |
157 | } | 169 | } |
@@ -159,9 +171,9 @@ class Bookmark | |||
159 | /** | 171 | /** |
160 | * Get the Url. | 172 | * Get the Url. |
161 | * | 173 | * |
162 | * @return string | 174 | * @return string|null |
163 | */ | 175 | */ |
164 | public function getUrl() | 176 | public function getUrl(): ?string |
165 | { | 177 | { |
166 | return $this->url; | 178 | return $this->url; |
167 | } | 179 | } |
@@ -171,7 +183,7 @@ class Bookmark | |||
171 | * | 183 | * |
172 | * @return string | 184 | * @return string |
173 | */ | 185 | */ |
174 | public function getTitle() | 186 | public function getTitle(): ?string |
175 | { | 187 | { |
176 | return $this->title; | 188 | return $this->title; |
177 | } | 189 | } |
@@ -181,7 +193,7 @@ class Bookmark | |||
181 | * | 193 | * |
182 | * @return string | 194 | * @return string |
183 | */ | 195 | */ |
184 | public function getDescription() | 196 | public function getDescription(): string |
185 | { | 197 | { |
186 | return ! empty($this->description) ? $this->description : ''; | 198 | return ! empty($this->description) ? $this->description : ''; |
187 | } | 199 | } |
@@ -191,7 +203,7 @@ class Bookmark | |||
191 | * | 203 | * |
192 | * @return DateTimeInterface | 204 | * @return DateTimeInterface |
193 | */ | 205 | */ |
194 | public function getCreated() | 206 | public function getCreated(): ?DateTimeInterface |
195 | { | 207 | { |
196 | return $this->created; | 208 | return $this->created; |
197 | } | 209 | } |
@@ -201,7 +213,7 @@ class Bookmark | |||
201 | * | 213 | * |
202 | * @return DateTimeInterface | 214 | * @return DateTimeInterface |
203 | */ | 215 | */ |
204 | public function getUpdated() | 216 | public function getUpdated(): ?DateTimeInterface |
205 | { | 217 | { |
206 | return $this->updated; | 218 | return $this->updated; |
207 | } | 219 | } |
@@ -209,11 +221,11 @@ class Bookmark | |||
209 | /** | 221 | /** |
210 | * Set the ShortUrl. | 222 | * Set the ShortUrl. |
211 | * | 223 | * |
212 | * @param string $shortUrl | 224 | * @param string|null $shortUrl |
213 | * | 225 | * |
214 | * @return Bookmark | 226 | * @return Bookmark |
215 | */ | 227 | */ |
216 | public function setShortUrl($shortUrl) | 228 | public function setShortUrl(?string $shortUrl): Bookmark |
217 | { | 229 | { |
218 | $this->shortUrl = $shortUrl; | 230 | $this->shortUrl = $shortUrl; |
219 | 231 | ||
@@ -223,14 +235,14 @@ class Bookmark | |||
223 | /** | 235 | /** |
224 | * Set the Url. | 236 | * Set the Url. |
225 | * | 237 | * |
226 | * @param string $url | 238 | * @param string|null $url |
227 | * @param array $allowedProtocols | 239 | * @param string[] $allowedProtocols |
228 | * | 240 | * |
229 | * @return Bookmark | 241 | * @return Bookmark |
230 | */ | 242 | */ |
231 | public function setUrl($url, $allowedProtocols = []) | 243 | public function setUrl(?string $url, array $allowedProtocols = []): Bookmark |
232 | { | 244 | { |
233 | $url = trim($url); | 245 | $url = $url !== null ? trim($url) : ''; |
234 | if (! empty($url)) { | 246 | if (! empty($url)) { |
235 | $url = whitelist_protocols($url, $allowedProtocols); | 247 | $url = whitelist_protocols($url, $allowedProtocols); |
236 | } | 248 | } |
@@ -242,13 +254,13 @@ class Bookmark | |||
242 | /** | 254 | /** |
243 | * Set the Title. | 255 | * Set the Title. |
244 | * | 256 | * |
245 | * @param string $title | 257 | * @param string|null $title |
246 | * | 258 | * |
247 | * @return Bookmark | 259 | * @return Bookmark |
248 | */ | 260 | */ |
249 | public function setTitle($title) | 261 | public function setTitle(?string $title): Bookmark |
250 | { | 262 | { |
251 | $this->title = trim($title); | 263 | $this->title = $title !== null ? trim($title) : ''; |
252 | 264 | ||
253 | return $this; | 265 | return $this; |
254 | } | 266 | } |
@@ -256,11 +268,11 @@ class Bookmark | |||
256 | /** | 268 | /** |
257 | * Set the Description. | 269 | * Set the Description. |
258 | * | 270 | * |
259 | * @param string $description | 271 | * @param string|null $description |
260 | * | 272 | * |
261 | * @return Bookmark | 273 | * @return Bookmark |
262 | */ | 274 | */ |
263 | public function setDescription($description) | 275 | public function setDescription(?string $description): Bookmark |
264 | { | 276 | { |
265 | $this->description = $description; | 277 | $this->description = $description; |
266 | 278 | ||
@@ -271,11 +283,11 @@ class Bookmark | |||
271 | * Set the Created. | 283 | * Set the Created. |
272 | * Note: you shouldn't set this manually except for special cases (like bookmark import) | 284 | * Note: you shouldn't set this manually except for special cases (like bookmark import) |
273 | * | 285 | * |
274 | * @param DateTimeInterface $created | 286 | * @param DateTimeInterface|null $created |
275 | * | 287 | * |
276 | * @return Bookmark | 288 | * @return Bookmark |
277 | */ | 289 | */ |
278 | public function setCreated($created) | 290 | public function setCreated(?DateTimeInterface $created): Bookmark |
279 | { | 291 | { |
280 | $this->created = $created; | 292 | $this->created = $created; |
281 | 293 | ||
@@ -285,11 +297,11 @@ class Bookmark | |||
285 | /** | 297 | /** |
286 | * Set the Updated. | 298 | * Set the Updated. |
287 | * | 299 | * |
288 | * @param DateTimeInterface $updated | 300 | * @param DateTimeInterface|null $updated |
289 | * | 301 | * |
290 | * @return Bookmark | 302 | * @return Bookmark |
291 | */ | 303 | */ |
292 | public function setUpdated($updated) | 304 | public function setUpdated(?DateTimeInterface $updated): Bookmark |
293 | { | 305 | { |
294 | $this->updated = $updated; | 306 | $this->updated = $updated; |
295 | 307 | ||
@@ -301,7 +313,7 @@ class Bookmark | |||
301 | * | 313 | * |
302 | * @return bool | 314 | * @return bool |
303 | */ | 315 | */ |
304 | public function isPrivate() | 316 | public function isPrivate(): bool |
305 | { | 317 | { |
306 | return $this->private ? true : false; | 318 | return $this->private ? true : false; |
307 | } | 319 | } |
@@ -309,11 +321,11 @@ class Bookmark | |||
309 | /** | 321 | /** |
310 | * Set the Private. | 322 | * Set the Private. |
311 | * | 323 | * |
312 | * @param bool $private | 324 | * @param bool|null $private |
313 | * | 325 | * |
314 | * @return Bookmark | 326 | * @return Bookmark |
315 | */ | 327 | */ |
316 | public function setPrivate($private) | 328 | public function setPrivate(?bool $private): Bookmark |
317 | { | 329 | { |
318 | $this->private = $private ? true : false; | 330 | $this->private = $private ? true : false; |
319 | 331 | ||
@@ -323,9 +335,9 @@ class Bookmark | |||
323 | /** | 335 | /** |
324 | * Get the Tags. | 336 | * Get the Tags. |
325 | * | 337 | * |
326 | * @return array | 338 | * @return string[] |
327 | */ | 339 | */ |
328 | public function getTags() | 340 | public function getTags(): array |
329 | { | 341 | { |
330 | return is_array($this->tags) ? $this->tags : []; | 342 | return is_array($this->tags) ? $this->tags : []; |
331 | } | 343 | } |
@@ -333,13 +345,18 @@ class Bookmark | |||
333 | /** | 345 | /** |
334 | * Set the Tags. | 346 | * Set the Tags. |
335 | * | 347 | * |
336 | * @param array $tags | 348 | * @param string[]|null $tags |
337 | * | 349 | * |
338 | * @return Bookmark | 350 | * @return Bookmark |
339 | */ | 351 | */ |
340 | public function setTags($tags) | 352 | public function setTags(?array $tags): Bookmark |
341 | { | 353 | { |
342 | $this->setTagsString(implode(' ', $tags)); | 354 | $this->tags = array_map( |
355 | function (string $tag): string { | ||
356 | return $tag[0] === '-' ? substr($tag, 1) : $tag; | ||
357 | }, | ||
358 | tags_filter($tags, ' ') | ||
359 | ); | ||
343 | 360 | ||
344 | return $this; | 361 | return $this; |
345 | } | 362 | } |
@@ -357,11 +374,11 @@ class Bookmark | |||
357 | /** | 374 | /** |
358 | * Set the Thumbnail. | 375 | * Set the Thumbnail. |
359 | * | 376 | * |
360 | * @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found | 377 | * @param string|bool|null $thumbnail Thumbnail's URL - false if no thumbnail could be found |
361 | * | 378 | * |
362 | * @return Bookmark | 379 | * @return Bookmark |
363 | */ | 380 | */ |
364 | public function setThumbnail($thumbnail) | 381 | public function setThumbnail($thumbnail): Bookmark |
365 | { | 382 | { |
366 | $this->thumbnail = $thumbnail; | 383 | $this->thumbnail = $thumbnail; |
367 | 384 | ||
@@ -369,11 +386,29 @@ class Bookmark | |||
369 | } | 386 | } |
370 | 387 | ||
371 | /** | 388 | /** |
389 | * Return true if: | ||
390 | * - the bookmark's thumbnail is not already set to false (= not found) | ||
391 | * - it's not a note | ||
392 | * - it's an HTTP(S) link | ||
393 | * - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore | ||
394 | * | ||
395 | * @return bool True if the bookmark's thumbnail needs to be retrieved. | ||
396 | */ | ||
397 | public function shouldUpdateThumbnail(): bool | ||
398 | { | ||
399 | return $this->thumbnail !== false | ||
400 | && !$this->isNote() | ||
401 | && startsWith(strtolower($this->url), 'http') | ||
402 | && (null === $this->thumbnail || !is_file($this->thumbnail)) | ||
403 | ; | ||
404 | } | ||
405 | |||
406 | /** | ||
372 | * Get the Sticky. | 407 | * Get the Sticky. |
373 | * | 408 | * |
374 | * @return bool | 409 | * @return bool |
375 | */ | 410 | */ |
376 | public function isSticky() | 411 | public function isSticky(): bool |
377 | { | 412 | { |
378 | return $this->sticky ? true : false; | 413 | return $this->sticky ? true : false; |
379 | } | 414 | } |
@@ -381,11 +416,11 @@ class Bookmark | |||
381 | /** | 416 | /** |
382 | * Set the Sticky. | 417 | * Set the Sticky. |
383 | * | 418 | * |
384 | * @param bool $sticky | 419 | * @param bool|null $sticky |
385 | * | 420 | * |
386 | * @return Bookmark | 421 | * @return Bookmark |
387 | */ | 422 | */ |
388 | public function setSticky($sticky) | 423 | public function setSticky(?bool $sticky): Bookmark |
389 | { | 424 | { |
390 | $this->sticky = $sticky ? true : false; | 425 | $this->sticky = $sticky ? true : false; |
391 | 426 | ||
@@ -393,17 +428,19 @@ class Bookmark | |||
393 | } | 428 | } |
394 | 429 | ||
395 | /** | 430 | /** |
396 | * @return string Bookmark's tags as a string, separated by a space | 431 | * @param string $separator Tags separator loaded from the config file. |
432 | * | ||
433 | * @return string Bookmark's tags as a string, separated by a separator | ||
397 | */ | 434 | */ |
398 | public function getTagsString() | 435 | public function getTagsString(string $separator = ' '): string |
399 | { | 436 | { |
400 | return implode(' ', $this->getTags()); | 437 | return tags_array2str($this->getTags(), $separator); |
401 | } | 438 | } |
402 | 439 | ||
403 | /** | 440 | /** |
404 | * @return bool | 441 | * @return bool |
405 | */ | 442 | */ |
406 | public function isNote() | 443 | public function isNote(): bool |
407 | { | 444 | { |
408 | // We check empty value to get a valid result if the link has not been saved yet | 445 | // We check empty value to get a valid result if the link has not been saved yet |
409 | return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?'; | 446 | return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?'; |
@@ -416,33 +453,65 @@ class Bookmark | |||
416 | * - multiple spaces will be removed | 453 | * - multiple spaces will be removed |
417 | * - trailing dash in tags will be removed | 454 | * - trailing dash in tags will be removed |
418 | * | 455 | * |
419 | * @param string $tags | 456 | * @param string|null $tags |
457 | * @param string $separator Tags separator loaded from the config file. | ||
420 | * | 458 | * |
421 | * @return $this | 459 | * @return $this |
422 | */ | 460 | */ |
423 | public function setTagsString($tags) | 461 | public function setTagsString(?string $tags, string $separator = ' '): Bookmark |
424 | { | 462 | { |
425 | // Remove first '-' char in tags. | 463 | $this->setTags(tags_str2array($tags, $separator)); |
426 | $tags = preg_replace('/(^| )\-/', '$1', $tags); | ||
427 | // Explode all tags separted by spaces or commas | ||
428 | $tags = preg_split('/[\s,]+/', $tags); | ||
429 | // Remove eventual empty values | ||
430 | $tags = array_values(array_filter($tags)); | ||
431 | 464 | ||
432 | $this->tags = $tags; | 465 | return $this; |
466 | } | ||
467 | |||
468 | /** | ||
469 | * Get entire additionalContent array. | ||
470 | * | ||
471 | * @return mixed[] | ||
472 | */ | ||
473 | public function getAdditionalContent(): array | ||
474 | { | ||
475 | return $this->additionalContent; | ||
476 | } | ||
477 | |||
478 | /** | ||
479 | * Set a single entry in additionalContent, by key. | ||
480 | * | ||
481 | * @param string $key | ||
482 | * @param mixed|null $value Any type of value can be set. | ||
483 | * | ||
484 | * @return $this | ||
485 | */ | ||
486 | public function addAdditionalContentEntry(string $key, $value): self | ||
487 | { | ||
488 | $this->additionalContent[$key] = $value; | ||
433 | 489 | ||
434 | return $this; | 490 | return $this; |
435 | } | 491 | } |
436 | 492 | ||
437 | /** | 493 | /** |
494 | * Get a single entry in additionalContent, by key. | ||
495 | * | ||
496 | * @param string $key | ||
497 | * @param mixed|null $default | ||
498 | * | ||
499 | * @return mixed|null can be any type or even null. | ||
500 | */ | ||
501 | public function getAdditionalContentEntry(string $key, $default = null) | ||
502 | { | ||
503 | return array_key_exists($key, $this->additionalContent) ? $this->additionalContent[$key] : $default; | ||
504 | } | ||
505 | |||
506 | /** | ||
438 | * Rename a tag in tags list. | 507 | * Rename a tag in tags list. |
439 | * | 508 | * |
440 | * @param string $fromTag | 509 | * @param string $fromTag |
441 | * @param string $toTag | 510 | * @param string $toTag |
442 | */ | 511 | */ |
443 | public function renameTag($fromTag, $toTag) | 512 | public function renameTag(string $fromTag, string $toTag): void |
444 | { | 513 | { |
445 | if (($pos = array_search($fromTag, $this->tags)) !== false) { | 514 | if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) { |
446 | $this->tags[$pos] = trim($toTag); | 515 | $this->tags[$pos] = trim($toTag); |
447 | } | 516 | } |
448 | } | 517 | } |
@@ -452,9 +521,9 @@ class Bookmark | |||
452 | * | 521 | * |
453 | * @param string $tag | 522 | * @param string $tag |
454 | */ | 523 | */ |
455 | public function deleteTag($tag) | 524 | public function deleteTag(string $tag): void |
456 | { | 525 | { |
457 | if (($pos = array_search($tag, $this->tags)) !== false) { | 526 | if (($pos = array_search($tag, $this->tags ?? [])) !== false) { |
458 | unset($this->tags[$pos]); | 527 | unset($this->tags[$pos]); |
459 | $this->tags = array_values($this->tags); | 528 | $this->tags = array_values($this->tags); |
460 | } | 529 | } |
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php index 3bd5eb20..b9328116 100644 --- a/application/bookmark/BookmarkArray.php +++ b/application/bookmark/BookmarkArray.php | |||
@@ -1,5 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
5 | use Shaarli\Bookmark\Exception\InvalidBookmarkException; | 7 | use Shaarli\Bookmark\Exception\InvalidBookmarkException; |
@@ -70,7 +72,8 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
70 | */ | 72 | */ |
71 | public function offsetSet($offset, $value) | 73 | public function offsetSet($offset, $value) |
72 | { | 74 | { |
73 | if (! $value instanceof Bookmark | 75 | if ( |
76 | ! $value instanceof Bookmark | ||
74 | || $value->getId() === null || empty($value->getUrl()) | 77 | || $value->getId() === null || empty($value->getUrl()) |
75 | || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId()) | 78 | || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId()) |
76 | || $offset !== null && $offset !== $value->getId() | 79 | || $offset !== null && $offset !== $value->getId() |
@@ -187,13 +190,13 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
187 | /** | 190 | /** |
188 | * Returns a bookmark offset in bookmarks array from its unique ID. | 191 | * Returns a bookmark offset in bookmarks array from its unique ID. |
189 | * | 192 | * |
190 | * @param int $id Persistent ID of a bookmark. | 193 | * @param int|null $id Persistent ID of a bookmark. |
191 | * | 194 | * |
192 | * @return int Real offset in local array, or null if doesn't exist. | 195 | * @return int Real offset in local array, or null if doesn't exist. |
193 | */ | 196 | */ |
194 | protected function getBookmarkOffset($id) | 197 | protected function getBookmarkOffset(?int $id): ?int |
195 | { | 198 | { |
196 | if (isset($this->ids[$id])) { | 199 | if ($id !== null && isset($this->ids[$id])) { |
197 | return $this->ids[$id]; | 200 | return $this->ids[$id]; |
198 | } | 201 | } |
199 | return null; | 202 | return null; |
@@ -205,7 +208,7 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
205 | * | 208 | * |
206 | * @return int next ID. | 209 | * @return int next ID. |
207 | */ | 210 | */ |
208 | public function getNextId() | 211 | public function getNextId(): int |
209 | { | 212 | { |
210 | if (!empty($this->ids)) { | 213 | if (!empty($this->ids)) { |
211 | return max(array_keys($this->ids)) + 1; | 214 | return max(array_keys($this->ids)) + 1; |
@@ -214,13 +217,14 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
214 | } | 217 | } |
215 | 218 | ||
216 | /** | 219 | /** |
217 | * @param $url | 220 | * @param string $url |
218 | * | 221 | * |
219 | * @return Bookmark|null | 222 | * @return Bookmark|null |
220 | */ | 223 | */ |
221 | public function getByUrl($url) | 224 | public function getByUrl(string $url): ?Bookmark |
222 | { | 225 | { |
223 | if (! empty($url) | 226 | if ( |
227 | ! empty($url) | ||
224 | && isset($this->urls[$url]) | 228 | && isset($this->urls[$url]) |
225 | && isset($this->bookmarks[$this->urls[$url]]) | 229 | && isset($this->bookmarks[$this->urls[$url]]) |
226 | ) { | 230 | ) { |
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index c9ec2609..6666a251 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php | |||
@@ -1,10 +1,12 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
3 | 4 | ||
4 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
5 | 6 | ||
6 | 7 | use DateTime; | |
7 | use Exception; | 8 | use Exception; |
9 | use malkusch\lock\mutex\Mutex; | ||
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 10 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
9 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; | 11 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; |
10 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; | 12 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; |
@@ -47,15 +49,19 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
47 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ | 49 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ |
48 | protected $isLoggedIn; | 50 | protected $isLoggedIn; |
49 | 51 | ||
52 | /** @var Mutex */ | ||
53 | protected $mutex; | ||
54 | |||
50 | /** | 55 | /** |
51 | * @inheritDoc | 56 | * @inheritDoc |
52 | */ | 57 | */ |
53 | public function __construct(ConfigManager $conf, History $history, $isLoggedIn) | 58 | public function __construct(ConfigManager $conf, History $history, Mutex $mutex, bool $isLoggedIn) |
54 | { | 59 | { |
55 | $this->conf = $conf; | 60 | $this->conf = $conf; |
56 | $this->history = $history; | 61 | $this->history = $history; |
62 | $this->mutex = $mutex; | ||
57 | $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); | 63 | $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); |
58 | $this->bookmarksIO = new BookmarkIO($this->conf); | 64 | $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex); |
59 | $this->isLoggedIn = $isLoggedIn; | 65 | $this->isLoggedIn = $isLoggedIn; |
60 | 66 | ||
61 | if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { | 67 | if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { |
@@ -63,7 +69,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
63 | } else { | 69 | } else { |
64 | try { | 70 | try { |
65 | $this->bookmarks = $this->bookmarksIO->read(); | 71 | $this->bookmarks = $this->bookmarksIO->read(); |
66 | } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { | 72 | } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) { |
67 | $this->bookmarks = new BookmarkArray(); | 73 | $this->bookmarks = new BookmarkArray(); |
68 | 74 | ||
69 | if ($this->isLoggedIn) { | 75 | if ($this->isLoggedIn) { |
@@ -79,25 +85,29 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
79 | if (! $this->bookmarks instanceof BookmarkArray) { | 85 | if (! $this->bookmarks instanceof BookmarkArray) { |
80 | $this->migrate(); | 86 | $this->migrate(); |
81 | exit( | 87 | exit( |
82 | 'Your data store has been migrated, please reload the page.'. PHP_EOL . | 88 | 'Your data store has been migrated, please reload the page.' . PHP_EOL . |
83 | 'If this message keeps showing up, please delete data/updates.txt file.' | 89 | 'If this message keeps showing up, please delete data/updates.txt file.' |
84 | ); | 90 | ); |
85 | } | 91 | } |
86 | } | 92 | } |
87 | 93 | ||
88 | $this->bookmarkFilter = new BookmarkFilter($this->bookmarks); | 94 | $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf); |
89 | } | 95 | } |
90 | 96 | ||
91 | /** | 97 | /** |
92 | * @inheritDoc | 98 | * @inheritDoc |
93 | */ | 99 | */ |
94 | public function findByHash($hash) | 100 | public function findByHash(string $hash, string $privateKey = null): Bookmark |
95 | { | 101 | { |
96 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); | 102 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); |
97 | // PHP 7.3 introduced array_key_first() to avoid this hack | 103 | // PHP 7.3 introduced array_key_first() to avoid this hack |
98 | $first = reset($bookmark); | 104 | $first = reset($bookmark); |
99 | if (! $this->isLoggedIn && $first->isPrivate()) { | 105 | if ( |
100 | throw new Exception('Not authorized'); | 106 | !$this->isLoggedIn |
107 | && $first->isPrivate() | ||
108 | && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) | ||
109 | ) { | ||
110 | throw new BookmarkNotFoundException(); | ||
101 | } | 111 | } |
102 | 112 | ||
103 | return $first; | 113 | return $first; |
@@ -106,7 +116,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
106 | /** | 116 | /** |
107 | * @inheritDoc | 117 | * @inheritDoc |
108 | */ | 118 | */ |
109 | public function findByUrl($url) | 119 | public function findByUrl(string $url): ?Bookmark |
110 | { | 120 | { |
111 | return $this->bookmarks->getByUrl($url); | 121 | return $this->bookmarks->getByUrl($url); |
112 | } | 122 | } |
@@ -115,10 +125,10 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
115 | * @inheritDoc | 125 | * @inheritDoc |
116 | */ | 126 | */ |
117 | public function search( | 127 | public function search( |
118 | $request = [], | 128 | array $request = [], |
119 | $visibility = null, | 129 | string $visibility = null, |
120 | $caseSensitive = false, | 130 | bool $caseSensitive = false, |
121 | $untaggedOnly = false, | 131 | bool $untaggedOnly = false, |
122 | bool $ignoreSticky = false | 132 | bool $ignoreSticky = false |
123 | ) { | 133 | ) { |
124 | if ($visibility === null) { | 134 | if ($visibility === null) { |
@@ -126,8 +136,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
126 | } | 136 | } |
127 | 137 | ||
128 | // Filter bookmark database according to parameters. | 138 | // Filter bookmark database according to parameters. |
129 | $searchtags = isset($request['searchtags']) ? $request['searchtags'] : ''; | 139 | $searchTags = isset($request['searchtags']) ? $request['searchtags'] : ''; |
130 | $searchterm = isset($request['searchterm']) ? $request['searchterm'] : ''; | 140 | $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : ''; |
131 | 141 | ||
132 | if ($ignoreSticky) { | 142 | if ($ignoreSticky) { |
133 | $this->bookmarks->reorder('DESC', true); | 143 | $this->bookmarks->reorder('DESC', true); |
@@ -135,7 +145,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
135 | 145 | ||
136 | return $this->bookmarkFilter->filter( | 146 | return $this->bookmarkFilter->filter( |
137 | BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, | 147 | BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, |
138 | [$searchtags, $searchterm], | 148 | [$searchTags, $searchTerm], |
139 | $caseSensitive, | 149 | $caseSensitive, |
140 | $visibility, | 150 | $visibility, |
141 | $untaggedOnly | 151 | $untaggedOnly |
@@ -145,7 +155,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
145 | /** | 155 | /** |
146 | * @inheritDoc | 156 | * @inheritDoc |
147 | */ | 157 | */ |
148 | public function get($id, $visibility = null) | 158 | public function get(int $id, string $visibility = null): Bookmark |
149 | { | 159 | { |
150 | if (! isset($this->bookmarks[$id])) { | 160 | if (! isset($this->bookmarks[$id])) { |
151 | throw new BookmarkNotFoundException(); | 161 | throw new BookmarkNotFoundException(); |
@@ -156,7 +166,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
156 | } | 166 | } |
157 | 167 | ||
158 | $bookmark = $this->bookmarks[$id]; | 168 | $bookmark = $this->bookmarks[$id]; |
159 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | 169 | if ( |
170 | ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | ||
160 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | 171 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') |
161 | ) { | 172 | ) { |
162 | throw new Exception('Unauthorized'); | 173 | throw new Exception('Unauthorized'); |
@@ -168,20 +179,17 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
168 | /** | 179 | /** |
169 | * @inheritDoc | 180 | * @inheritDoc |
170 | */ | 181 | */ |
171 | public function set($bookmark, $save = true) | 182 | public function set(Bookmark $bookmark, bool $save = true): Bookmark |
172 | { | 183 | { |
173 | if (true !== $this->isLoggedIn) { | 184 | if (true !== $this->isLoggedIn) { |
174 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 185 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
175 | } | 186 | } |
176 | if (! $bookmark instanceof Bookmark) { | ||
177 | throw new Exception(t('Provided data is invalid')); | ||
178 | } | ||
179 | if (! isset($this->bookmarks[$bookmark->getId()])) { | 187 | if (! isset($this->bookmarks[$bookmark->getId()])) { |
180 | throw new BookmarkNotFoundException(); | 188 | throw new BookmarkNotFoundException(); |
181 | } | 189 | } |
182 | $bookmark->validate(); | 190 | $bookmark->validate(); |
183 | 191 | ||
184 | $bookmark->setUpdated(new \DateTime()); | 192 | $bookmark->setUpdated(new DateTime()); |
185 | $this->bookmarks[$bookmark->getId()] = $bookmark; | 193 | $this->bookmarks[$bookmark->getId()] = $bookmark; |
186 | if ($save === true) { | 194 | if ($save === true) { |
187 | $this->save(); | 195 | $this->save(); |
@@ -193,15 +201,12 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
193 | /** | 201 | /** |
194 | * @inheritDoc | 202 | * @inheritDoc |
195 | */ | 203 | */ |
196 | public function add($bookmark, $save = true) | 204 | public function add(Bookmark $bookmark, bool $save = true): Bookmark |
197 | { | 205 | { |
198 | if (true !== $this->isLoggedIn) { | 206 | if (true !== $this->isLoggedIn) { |
199 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 207 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
200 | } | 208 | } |
201 | if (! $bookmark instanceof Bookmark) { | 209 | if (!empty($bookmark->getId())) { |
202 | throw new Exception(t('Provided data is invalid')); | ||
203 | } | ||
204 | if (! empty($bookmark->getId())) { | ||
205 | throw new Exception(t('This bookmarks already exists')); | 210 | throw new Exception(t('This bookmarks already exists')); |
206 | } | 211 | } |
207 | $bookmark->setId($this->bookmarks->getNextId()); | 212 | $bookmark->setId($this->bookmarks->getNextId()); |
@@ -218,14 +223,11 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
218 | /** | 223 | /** |
219 | * @inheritDoc | 224 | * @inheritDoc |
220 | */ | 225 | */ |
221 | public function addOrSet($bookmark, $save = true) | 226 | public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark |
222 | { | 227 | { |
223 | if (true !== $this->isLoggedIn) { | 228 | if (true !== $this->isLoggedIn) { |
224 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 229 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
225 | } | 230 | } |
226 | if (! $bookmark instanceof Bookmark) { | ||
227 | throw new Exception('Provided data is invalid'); | ||
228 | } | ||
229 | if ($bookmark->getId() === null) { | 231 | if ($bookmark->getId() === null) { |
230 | return $this->add($bookmark, $save); | 232 | return $this->add($bookmark, $save); |
231 | } | 233 | } |
@@ -235,14 +237,11 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
235 | /** | 237 | /** |
236 | * @inheritDoc | 238 | * @inheritDoc |
237 | */ | 239 | */ |
238 | public function remove($bookmark, $save = true) | 240 | public function remove(Bookmark $bookmark, bool $save = true): void |
239 | { | 241 | { |
240 | if (true !== $this->isLoggedIn) { | 242 | if (true !== $this->isLoggedIn) { |
241 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 243 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
242 | } | 244 | } |
243 | if (! $bookmark instanceof Bookmark) { | ||
244 | throw new Exception(t('Provided data is invalid')); | ||
245 | } | ||
246 | if (! isset($this->bookmarks[$bookmark->getId()])) { | 245 | if (! isset($this->bookmarks[$bookmark->getId()])) { |
247 | throw new BookmarkNotFoundException(); | 246 | throw new BookmarkNotFoundException(); |
248 | } | 247 | } |
@@ -257,7 +256,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
257 | /** | 256 | /** |
258 | * @inheritDoc | 257 | * @inheritDoc |
259 | */ | 258 | */ |
260 | public function exists($id, $visibility = null) | 259 | public function exists(int $id, string $visibility = null): bool |
261 | { | 260 | { |
262 | if (! isset($this->bookmarks[$id])) { | 261 | if (! isset($this->bookmarks[$id])) { |
263 | return false; | 262 | return false; |
@@ -268,7 +267,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
268 | } | 267 | } |
269 | 268 | ||
270 | $bookmark = $this->bookmarks[$id]; | 269 | $bookmark = $this->bookmarks[$id]; |
271 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | 270 | if ( |
271 | ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | ||
272 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | 272 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') |
273 | ) { | 273 | ) { |
274 | return false; | 274 | return false; |
@@ -280,7 +280,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
280 | /** | 280 | /** |
281 | * @inheritDoc | 281 | * @inheritDoc |
282 | */ | 282 | */ |
283 | public function count($visibility = null) | 283 | public function count(string $visibility = null): int |
284 | { | 284 | { |
285 | return count($this->search([], $visibility)); | 285 | return count($this->search([], $visibility)); |
286 | } | 286 | } |
@@ -288,7 +288,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
288 | /** | 288 | /** |
289 | * @inheritDoc | 289 | * @inheritDoc |
290 | */ | 290 | */ |
291 | public function save() | 291 | public function save(): void |
292 | { | 292 | { |
293 | if (true !== $this->isLoggedIn) { | 293 | if (true !== $this->isLoggedIn) { |
294 | // TODO: raise an Exception instead | 294 | // TODO: raise an Exception instead |
@@ -303,14 +303,15 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
303 | /** | 303 | /** |
304 | * @inheritDoc | 304 | * @inheritDoc |
305 | */ | 305 | */ |
306 | public function bookmarksCountPerTag($filteringTags = [], $visibility = null) | 306 | public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array |
307 | { | 307 | { |
308 | $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); | 308 | $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); |
309 | $tags = []; | 309 | $tags = []; |
310 | $caseMapping = []; | 310 | $caseMapping = []; |
311 | foreach ($bookmarks as $bookmark) { | 311 | foreach ($bookmarks as $bookmark) { |
312 | foreach ($bookmark->getTags() as $tag) { | 312 | foreach ($bookmark->getTags() as $tag) { |
313 | if (empty($tag) | 313 | if ( |
314 | empty($tag) | ||
314 | || (! $this->isLoggedIn && startsWith($tag, '.')) | 315 | || (! $this->isLoggedIn && startsWith($tag, '.')) |
315 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG | 316 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG |
316 | || in_array($tag, $filteringTags, true) | 317 | || in_array($tag, $filteringTags, true) |
@@ -339,38 +340,55 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
339 | $keys = array_keys($tags); | 340 | $keys = array_keys($tags); |
340 | $tmpTags = array_combine($keys, $keys); | 341 | $tmpTags = array_combine($keys, $keys); |
341 | array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); | 342 | array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); |
343 | |||
342 | return $tags; | 344 | return $tags; |
343 | } | 345 | } |
344 | 346 | ||
345 | /** | 347 | /** |
346 | * @inheritDoc | 348 | * @inheritDoc |
347 | */ | 349 | */ |
348 | public function days() | 350 | public function findByDate( |
349 | { | 351 | \DateTimeInterface $from, |
350 | $bookmarkDays = []; | 352 | \DateTimeInterface $to, |
351 | foreach ($this->search() as $bookmark) { | 353 | ?\DateTimeInterface &$previous, |
352 | $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; | 354 | ?\DateTimeInterface &$next |
355 | ): array { | ||
356 | $out = []; | ||
357 | $previous = null; | ||
358 | $next = null; | ||
359 | |||
360 | foreach ($this->search([], null, false, false, true) as $bookmark) { | ||
361 | if ($to < $bookmark->getCreated()) { | ||
362 | $next = $bookmark->getCreated(); | ||
363 | } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { | ||
364 | $out[] = $bookmark; | ||
365 | } else { | ||
366 | if ($previous !== null) { | ||
367 | break; | ||
368 | } | ||
369 | $previous = $bookmark->getCreated(); | ||
370 | } | ||
353 | } | 371 | } |
354 | $bookmarkDays = array_keys($bookmarkDays); | ||
355 | sort($bookmarkDays); | ||
356 | 372 | ||
357 | return $bookmarkDays; | 373 | return $out; |
358 | } | 374 | } |
359 | 375 | ||
360 | /** | 376 | /** |
361 | * @inheritDoc | 377 | * @inheritDoc |
362 | */ | 378 | */ |
363 | public function filterDay($request) | 379 | public function getLatest(): ?Bookmark |
364 | { | 380 | { |
365 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; | 381 | foreach ($this->search([], null, false, false, true) as $bookmark) { |
382 | return $bookmark; | ||
383 | } | ||
366 | 384 | ||
367 | return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); | 385 | return null; |
368 | } | 386 | } |
369 | 387 | ||
370 | /** | 388 | /** |
371 | * @inheritDoc | 389 | * @inheritDoc |
372 | */ | 390 | */ |
373 | public function initialize() | 391 | public function initialize(): void |
374 | { | 392 | { |
375 | $initializer = new BookmarkInitializer($this); | 393 | $initializer = new BookmarkInitializer($this); |
376 | $initializer->initialize(); | 394 | $initializer->initialize(); |
@@ -383,7 +401,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
383 | /** | 401 | /** |
384 | * Handles migration to the new database format (BookmarksArray). | 402 | * Handles migration to the new database format (BookmarksArray). |
385 | */ | 403 | */ |
386 | protected function migrate() | 404 | protected function migrate(): void |
387 | { | 405 | { |
388 | $bookmarkDb = new LegacyLinkDB( | 406 | $bookmarkDb = new LegacyLinkDB( |
389 | $this->conf->get('resource.datastore'), | 407 | $this->conf->get('resource.datastore'), |
@@ -391,14 +409,14 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
391 | false | 409 | false |
392 | ); | 410 | ); |
393 | $updater = new LegacyUpdater( | 411 | $updater = new LegacyUpdater( |
394 | UpdaterUtils::read_updates_file($this->conf->get('resource.updates')), | 412 | UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')), |
395 | $bookmarkDb, | 413 | $bookmarkDb, |
396 | $this->conf, | 414 | $this->conf, |
397 | true | 415 | true |
398 | ); | 416 | ); |
399 | $newUpdates = $updater->update(); | 417 | $newUpdates = $updater->update(); |
400 | if (! empty($newUpdates)) { | 418 | if (! empty($newUpdates)) { |
401 | UpdaterUtils::write_updates_file( | 419 | UpdaterUtils::writeUpdatesFile( |
402 | $this->conf->get('resource.updates'), | 420 | $this->conf->get('resource.updates'), |
403 | $updater->getDoneUpdates() | 421 | $updater->getDoneUpdates() |
404 | ); | 422 | ); |
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index 6636bbfe..db83c51c 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php | |||
@@ -1,9 +1,12 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
5 | use Exception; | 7 | use Exception; |
6 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
9 | use Shaarli\Config\ConfigManager; | ||
7 | 10 | ||
8 | /** | 11 | /** |
9 | * Class LinkFilter. | 12 | * Class LinkFilter. |
@@ -56,12 +59,16 @@ class BookmarkFilter | |||
56 | */ | 59 | */ |
57 | private $bookmarks; | 60 | private $bookmarks; |
58 | 61 | ||
62 | /** @var ConfigManager */ | ||
63 | protected $conf; | ||
64 | |||
59 | /** | 65 | /** |
60 | * @param Bookmark[] $bookmarks initialization. | 66 | * @param Bookmark[] $bookmarks initialization. |
61 | */ | 67 | */ |
62 | public function __construct($bookmarks) | 68 | public function __construct($bookmarks, ConfigManager $conf) |
63 | { | 69 | { |
64 | $this->bookmarks = $bookmarks; | 70 | $this->bookmarks = $bookmarks; |
71 | $this->conf = $conf; | ||
65 | } | 72 | } |
66 | 73 | ||
67 | /** | 74 | /** |
@@ -77,8 +84,13 @@ class BookmarkFilter | |||
77 | * | 84 | * |
78 | * @throws BookmarkNotFoundException | 85 | * @throws BookmarkNotFoundException |
79 | */ | 86 | */ |
80 | public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false) | 87 | public function filter( |
81 | { | 88 | string $type, |
89 | $request, | ||
90 | bool $casesensitive = false, | ||
91 | string $visibility = 'all', | ||
92 | bool $untaggedonly = false | ||
93 | ) { | ||
82 | if (!in_array($visibility, ['all', 'public', 'private'])) { | 94 | if (!in_array($visibility, ['all', 'public', 'private'])) { |
83 | $visibility = 'all'; | 95 | $visibility = 'all'; |
84 | } | 96 | } |
@@ -100,10 +112,14 @@ class BookmarkFilter | |||
100 | $filtered = $this->bookmarks; | 112 | $filtered = $this->bookmarks; |
101 | } | 113 | } |
102 | if (!empty($request[0])) { | 114 | if (!empty($request[0])) { |
103 | $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); | 115 | $filtered = (new BookmarkFilter($filtered, $this->conf)) |
116 | ->filterTags($request[0], $casesensitive, $visibility) | ||
117 | ; | ||
104 | } | 118 | } |
105 | if (!empty($request[1])) { | 119 | if (!empty($request[1])) { |
106 | $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility); | 120 | $filtered = (new BookmarkFilter($filtered, $this->conf)) |
121 | ->filterFulltext($request[1], $visibility) | ||
122 | ; | ||
107 | } | 123 | } |
108 | return $filtered; | 124 | return $filtered; |
109 | case self::$FILTER_TEXT: | 125 | case self::$FILTER_TEXT: |
@@ -128,13 +144,13 @@ class BookmarkFilter | |||
128 | * | 144 | * |
129 | * @return Bookmark[] filtered bookmarks. | 145 | * @return Bookmark[] filtered bookmarks. |
130 | */ | 146 | */ |
131 | private function noFilter($visibility = 'all') | 147 | private function noFilter(string $visibility = 'all') |
132 | { | 148 | { |
133 | if ($visibility === 'all') { | 149 | if ($visibility === 'all') { |
134 | return $this->bookmarks; | 150 | return $this->bookmarks; |
135 | } | 151 | } |
136 | 152 | ||
137 | $out = array(); | 153 | $out = []; |
138 | foreach ($this->bookmarks as $key => $value) { | 154 | foreach ($this->bookmarks as $key => $value) { |
139 | if ($value->isPrivate() && $visibility === 'private') { | 155 | if ($value->isPrivate() && $visibility === 'private') { |
140 | $out[$key] = $value; | 156 | $out[$key] = $value; |
@@ -151,11 +167,11 @@ class BookmarkFilter | |||
151 | * | 167 | * |
152 | * @param string $smallHash permalink hash. | 168 | * @param string $smallHash permalink hash. |
153 | * | 169 | * |
154 | * @return array $filtered array containing permalink data. | 170 | * @return Bookmark[] $filtered array containing permalink data. |
155 | * | 171 | * |
156 | * @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link. | 172 | * @throws BookmarkNotFoundException if the smallhash doesn't match any link. |
157 | */ | 173 | */ |
158 | private function filterSmallHash($smallHash) | 174 | private function filterSmallHash(string $smallHash) |
159 | { | 175 | { |
160 | foreach ($this->bookmarks as $key => $l) { | 176 | foreach ($this->bookmarks as $key => $l) { |
161 | if ($smallHash == $l->getShortUrl()) { | 177 | if ($smallHash == $l->getShortUrl()) { |
@@ -186,15 +202,15 @@ class BookmarkFilter | |||
186 | * @param string $searchterms search query. | 202 | * @param string $searchterms search query. |
187 | * @param string $visibility Optional: return only all/private/public bookmarks. | 203 | * @param string $visibility Optional: return only all/private/public bookmarks. |
188 | * | 204 | * |
189 | * @return array search results. | 205 | * @return Bookmark[] search results. |
190 | */ | 206 | */ |
191 | private function filterFulltext($searchterms, $visibility = 'all') | 207 | private function filterFulltext(string $searchterms, string $visibility = 'all') |
192 | { | 208 | { |
193 | if (empty($searchterms)) { | 209 | if (empty($searchterms)) { |
194 | return $this->noFilter($visibility); | 210 | return $this->noFilter($visibility); |
195 | } | 211 | } |
196 | 212 | ||
197 | $filtered = array(); | 213 | $filtered = []; |
198 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); | 214 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); |
199 | $exactRegex = '/"([^"]+)"/'; | 215 | $exactRegex = '/"([^"]+)"/'; |
200 | // Retrieve exact search terms. | 216 | // Retrieve exact search terms. |
@@ -206,8 +222,8 @@ class BookmarkFilter | |||
206 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); | 222 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); |
207 | 223 | ||
208 | // Filter excluding terms and update andSearch. | 224 | // Filter excluding terms and update andSearch. |
209 | $excludeSearch = array(); | 225 | $excludeSearch = []; |
210 | $andSearch = array(); | 226 | $andSearch = []; |
211 | foreach ($explodedSearchAnd as $needle) { | 227 | foreach ($explodedSearchAnd as $needle) { |
212 | if ($needle[0] == '-' && strlen($needle) > 1) { | 228 | if ($needle[0] == '-' && strlen($needle) > 1) { |
213 | $excludeSearch[] = substr($needle, 1); | 229 | $excludeSearch[] = substr($needle, 1); |
@@ -227,33 +243,38 @@ class BookmarkFilter | |||
227 | } | 243 | } |
228 | } | 244 | } |
229 | 245 | ||
230 | // Concatenate link fields to search across fields. | 246 | $lengths = []; |
231 | // Adds a '\' separator for exact search terms. | 247 | $content = $this->buildFullTextSearchableLink($link, $lengths); |
232 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
233 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
234 | $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
235 | $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
236 | 248 | ||
237 | // Be optimistic | 249 | // Be optimistic |
238 | $found = true; | 250 | $found = true; |
251 | $foundPositions = []; | ||
239 | 252 | ||
240 | // First, we look for exact term search | 253 | // First, we look for exact term search |
241 | for ($i = 0; $i < count($exactSearch) && $found; $i++) { | 254 | // Then iterate over keywords, if keyword is not found, |
242 | $found = strpos($content, $exactSearch[$i]) !== false; | ||
243 | } | ||
244 | |||
245 | // Iterate over keywords, if keyword is not found, | ||
246 | // no need to check for the others. We want all or nothing. | 255 | // no need to check for the others. We want all or nothing. |
247 | for ($i = 0; $i < count($andSearch) && $found; $i++) { | 256 | foreach ([$exactSearch, $andSearch] as $search) { |
248 | $found = strpos($content, $andSearch[$i]) !== false; | 257 | for ($i = 0; $i < count($search) && $found !== false; $i++) { |
258 | $found = mb_strpos($content, $search[$i]); | ||
259 | if ($found === false) { | ||
260 | break; | ||
261 | } | ||
262 | |||
263 | $foundPositions[] = ['start' => $found, 'end' => $found + mb_strlen($search[$i])]; | ||
264 | } | ||
249 | } | 265 | } |
250 | 266 | ||
251 | // Exclude terms. | 267 | // Exclude terms. |
252 | for ($i = 0; $i < count($excludeSearch) && $found; $i++) { | 268 | for ($i = 0; $i < count($excludeSearch) && $found !== false; $i++) { |
253 | $found = strpos($content, $excludeSearch[$i]) === false; | 269 | $found = strpos($content, $excludeSearch[$i]) === false; |
254 | } | 270 | } |
255 | 271 | ||
256 | if ($found) { | 272 | if ($found !== false) { |
273 | $link->addAdditionalContentEntry( | ||
274 | 'search_highlight', | ||
275 | $this->postProcessFoundPositions($lengths, $foundPositions) | ||
276 | ); | ||
277 | |||
257 | $filtered[$id] = $link; | 278 | $filtered[$id] = $link; |
258 | } | 279 | } |
259 | } | 280 | } |
@@ -268,8 +289,9 @@ class BookmarkFilter | |||
268 | * | 289 | * |
269 | * @return string generated regex fragment | 290 | * @return string generated regex fragment |
270 | */ | 291 | */ |
271 | private static function tag2regex($tag) | 292 | protected function tag2regex(string $tag): string |
272 | { | 293 | { |
294 | $tagsSeparator = $this->conf->get('general.tags_separator', ' '); | ||
273 | $len = strlen($tag); | 295 | $len = strlen($tag); |
274 | if (!$len || $tag === "-" || $tag === "*") { | 296 | if (!$len || $tag === "-" || $tag === "*") { |
275 | // nothing to search, return empty regex | 297 | // nothing to search, return empty regex |
@@ -283,12 +305,13 @@ class BookmarkFilter | |||
283 | $i = 0; // start at first character | 305 | $i = 0; // start at first character |
284 | $regex = '(?='; // use positive lookahead | 306 | $regex = '(?='; // use positive lookahead |
285 | } | 307 | } |
286 | $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning | 308 | // before tag may only be the separator or the beginning |
309 | $regex .= '.*(?:^|' . $tagsSeparator . ')'; | ||
287 | // iterate over string, separating it into placeholder and content | 310 | // iterate over string, separating it into placeholder and content |
288 | for (; $i < $len; $i++) { | 311 | for (; $i < $len; $i++) { |
289 | if ($tag[$i] === '*') { | 312 | if ($tag[$i] === '*') { |
290 | // placeholder found | 313 | // placeholder found |
291 | $regex .= '[^ ]*?'; | 314 | $regex .= '[^' . $tagsSeparator . ']*?'; |
292 | } else { | 315 | } else { |
293 | // regular characters | 316 | // regular characters |
294 | $offset = strpos($tag, '*', $i); | 317 | $offset = strpos($tag, '*', $i); |
@@ -304,7 +327,8 @@ class BookmarkFilter | |||
304 | $i = $offset; | 327 | $i = $offset; |
305 | } | 328 | } |
306 | } | 329 | } |
307 | $regex .= '(?:$| ))'; // after the tag may only be a space or the end | 330 | // after the tag may only be the separator or the end |
331 | $regex .= '(?:$|' . $tagsSeparator . '))'; | ||
308 | return $regex; | 332 | return $regex; |
309 | } | 333 | } |
310 | 334 | ||
@@ -314,22 +338,23 @@ class BookmarkFilter | |||
314 | * You can specify one or more tags, separated by space or a comma, e.g. | 338 | * You can specify one or more tags, separated by space or a comma, e.g. |
315 | * print_r($mydb->filterTags('linux programming')); | 339 | * print_r($mydb->filterTags('linux programming')); |
316 | * | 340 | * |
317 | * @param string $tags list of tags separated by commas or blank spaces. | 341 | * @param string|array $tags list of tags, separated by commas or blank spaces if passed as string. |
318 | * @param bool $casesensitive ignore case if false. | 342 | * @param bool $casesensitive ignore case if false. |
319 | * @param string $visibility Optional: return only all/private/public bookmarks. | 343 | * @param string $visibility Optional: return only all/private/public bookmarks. |
320 | * | 344 | * |
321 | * @return array filtered bookmarks. | 345 | * @return Bookmark[] filtered bookmarks. |
322 | */ | 346 | */ |
323 | public function filterTags($tags, $casesensitive = false, $visibility = 'all') | 347 | public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all') |
324 | { | 348 | { |
349 | $tagsSeparator = $this->conf->get('general.tags_separator', ' '); | ||
325 | // get single tags (we may get passed an array, even though the docs say different) | 350 | // get single tags (we may get passed an array, even though the docs say different) |
326 | $inputTags = $tags; | 351 | $inputTags = $tags; |
327 | if (!is_array($tags)) { | 352 | if (!is_array($tags)) { |
328 | // we got an input string, split tags | 353 | // we got an input string, split tags |
329 | $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); | 354 | $inputTags = tags_str2array($inputTags, $tagsSeparator); |
330 | } | 355 | } |
331 | 356 | ||
332 | if (!count($inputTags)) { | 357 | if (count($inputTags) === 0) { |
333 | // no input tags | 358 | // no input tags |
334 | return $this->noFilter($visibility); | 359 | return $this->noFilter($visibility); |
335 | } | 360 | } |
@@ -346,7 +371,7 @@ class BookmarkFilter | |||
346 | } | 371 | } |
347 | 372 | ||
348 | // build regex from all tags | 373 | // build regex from all tags |
349 | $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; | 374 | $re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/'; |
350 | if (!$casesensitive) { | 375 | if (!$casesensitive) { |
351 | // make regex case insensitive | 376 | // make regex case insensitive |
352 | $re .= 'i'; | 377 | $re .= 'i'; |
@@ -366,10 +391,11 @@ class BookmarkFilter | |||
366 | continue; | 391 | continue; |
367 | } | 392 | } |
368 | } | 393 | } |
369 | $search = $link->getTagsString(); // build search string, start with tags of current link | 394 | // build search string, start with tags of current link |
395 | $search = $link->getTagsString($tagsSeparator); | ||
370 | if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { | 396 | if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { |
371 | // description given and at least one possible tag found | 397 | // description given and at least one possible tag found |
372 | $descTags = array(); | 398 | $descTags = []; |
373 | // find all tags in the form of #tag in the description | 399 | // find all tags in the form of #tag in the description |
374 | preg_match_all( | 400 | preg_match_all( |
375 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', | 401 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', |
@@ -378,9 +404,9 @@ class BookmarkFilter | |||
378 | ); | 404 | ); |
379 | if (count($descTags[1])) { | 405 | if (count($descTags[1])) { |
380 | // there were some tags in the description, add them to the search string | 406 | // there were some tags in the description, add them to the search string |
381 | $search .= ' ' . implode(' ', $descTags[1]); | 407 | $search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator); |
382 | } | 408 | } |
383 | }; | 409 | } |
384 | // match regular expression with search string | 410 | // match regular expression with search string |
385 | if (!preg_match($re, $search)) { | 411 | if (!preg_match($re, $search)) { |
386 | // this entry does _not_ match our regex | 412 | // this entry does _not_ match our regex |
@@ -396,9 +422,9 @@ class BookmarkFilter | |||
396 | * | 422 | * |
397 | * @param string $visibility return only all/private/public bookmarks. | 423 | * @param string $visibility return only all/private/public bookmarks. |
398 | * | 424 | * |
399 | * @return array filtered bookmarks. | 425 | * @return Bookmark[] filtered bookmarks. |
400 | */ | 426 | */ |
401 | public function filterUntagged($visibility) | 427 | public function filterUntagged(string $visibility) |
402 | { | 428 | { |
403 | $filtered = []; | 429 | $filtered = []; |
404 | foreach ($this->bookmarks as $key => $link) { | 430 | foreach ($this->bookmarks as $key => $link) { |
@@ -410,7 +436,7 @@ class BookmarkFilter | |||
410 | } | 436 | } |
411 | } | 437 | } |
412 | 438 | ||
413 | if (empty(trim($link->getTagsString()))) { | 439 | if (empty($link->getTags())) { |
414 | $filtered[$key] = $link; | 440 | $filtered[$key] = $link; |
415 | } | 441 | } |
416 | } | 442 | } |
@@ -427,11 +453,11 @@ class BookmarkFilter | |||
427 | * @param string $day day to filter. | 453 | * @param string $day day to filter. |
428 | * @param string $visibility return only all/private/public bookmarks. | 454 | * @param string $visibility return only all/private/public bookmarks. |
429 | 455 | ||
430 | * @return array all link matching given day. | 456 | * @return Bookmark[] all link matching given day. |
431 | * | 457 | * |
432 | * @throws Exception if date format is invalid. | 458 | * @throws Exception if date format is invalid. |
433 | */ | 459 | */ |
434 | public function filterDay($day, $visibility) | 460 | public function filterDay(string $day, string $visibility) |
435 | { | 461 | { |
436 | if (!checkDateFormat('Ymd', $day)) { | 462 | if (!checkDateFormat('Ymd', $day)) { |
437 | throw new Exception('Invalid date format'); | 463 | throw new Exception('Invalid date format'); |
@@ -460,9 +486,9 @@ class BookmarkFilter | |||
460 | * @param string $tags string containing a list of tags. | 486 | * @param string $tags string containing a list of tags. |
461 | * @param bool $casesensitive will convert everything to lowercase if false. | 487 | * @param bool $casesensitive will convert everything to lowercase if false. |
462 | * | 488 | * |
463 | * @return array filtered tags string. | 489 | * @return string[] filtered tags string. |
464 | */ | 490 | */ |
465 | public static function tagsStrToArray($tags, $casesensitive) | 491 | public static function tagsStrToArray(string $tags, bool $casesensitive): array |
466 | { | 492 | { |
467 | // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) | 493 | // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) |
468 | $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); | 494 | $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); |
@@ -470,4 +496,75 @@ class BookmarkFilter | |||
470 | 496 | ||
471 | return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); | 497 | return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); |
472 | } | 498 | } |
499 | |||
500 | /** | ||
501 | * This method finalize the content of the foundPositions array, | ||
502 | * by associated all search results to their associated bookmark field, | ||
503 | * making sure that there is no overlapping results, etc. | ||
504 | * | ||
505 | * @param array $fieldLengths Start and end positions of every bookmark fields in the aggregated bookmark content. | ||
506 | * @param array $foundPositions Positions where the search results were found in the aggregated content. | ||
507 | * | ||
508 | * @return array Updated $foundPositions, by bookmark field. | ||
509 | */ | ||
510 | protected function postProcessFoundPositions(array $fieldLengths, array $foundPositions): array | ||
511 | { | ||
512 | // Sort results by starting position ASC. | ||
513 | usort($foundPositions, function (array $entryA, array $entryB): int { | ||
514 | return $entryA['start'] > $entryB['start'] ? 1 : -1; | ||
515 | }); | ||
516 | |||
517 | $out = []; | ||
518 | $currentMax = -1; | ||
519 | foreach ($foundPositions as $foundPosition) { | ||
520 | // we do not allow overlapping highlights | ||
521 | if ($foundPosition['start'] < $currentMax) { | ||
522 | continue; | ||
523 | } | ||
524 | |||
525 | $currentMax = $foundPosition['end']; | ||
526 | foreach ($fieldLengths as $part => $length) { | ||
527 | if ($foundPosition['start'] < $length['start'] || $foundPosition['start'] > $length['end']) { | ||
528 | continue; | ||
529 | } | ||
530 | |||
531 | $out[$part][] = [ | ||
532 | 'start' => $foundPosition['start'] - $length['start'], | ||
533 | 'end' => $foundPosition['end'] - $length['start'], | ||
534 | ]; | ||
535 | break; | ||
536 | } | ||
537 | } | ||
538 | |||
539 | return $out; | ||
540 | } | ||
541 | |||
542 | /** | ||
543 | * Concatenate link fields to search across fields. Adds a '\' separator for exact search terms. | ||
544 | * Also populate $length array with starting and ending positions of every bookmark field | ||
545 | * inside concatenated content. | ||
546 | * | ||
547 | * @param Bookmark $link | ||
548 | * @param array $lengths (by reference) | ||
549 | * | ||
550 | * @return string Lowercase concatenated fields content. | ||
551 | */ | ||
552 | protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string | ||
553 | { | ||
554 | $tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' ')); | ||
555 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') . '\\'; | ||
556 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') . '\\'; | ||
557 | $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') . '\\'; | ||
558 | $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') . '\\'; | ||
559 | |||
560 | $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; | ||
561 | $nextField = $lengths['title']['end'] + 1; | ||
562 | $lengths['description'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getDescription())]; | ||
563 | $nextField = $lengths['description']['end'] + 1; | ||
564 | $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())]; | ||
565 | $nextField = $lengths['url']['end'] + 1; | ||
566 | $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)]; | ||
567 | |||
568 | return $content; | ||
569 | } | ||
473 | } | 570 | } |
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index 6bf7f365..c78dbe41 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php | |||
@@ -1,7 +1,11 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
7 | use malkusch\lock\mutex\Mutex; | ||
8 | use malkusch\lock\mutex\NoMutex; | ||
5 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; | 9 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; |
6 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; | 10 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; |
7 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; | 11 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; |
@@ -27,11 +31,14 @@ class BookmarkIO | |||
27 | */ | 31 | */ |
28 | protected $conf; | 32 | protected $conf; |
29 | 33 | ||
34 | |||
35 | /** @var Mutex */ | ||
36 | protected $mutex; | ||
37 | |||
30 | /** | 38 | /** |
31 | * string Datastore PHP prefix | 39 | * string Datastore PHP prefix |
32 | */ | 40 | */ |
33 | protected static $phpPrefix = '<?php /* '; | 41 | protected static $phpPrefix = '<?php /* '; |
34 | |||
35 | /** | 42 | /** |
36 | * string Datastore PHP suffix | 43 | * string Datastore PHP suffix |
37 | */ | 44 | */ |
@@ -42,16 +49,21 @@ class BookmarkIO | |||
42 | * | 49 | * |
43 | * @param ConfigManager $conf instance | 50 | * @param ConfigManager $conf instance |
44 | */ | 51 | */ |
45 | public function __construct($conf) | 52 | public function __construct(ConfigManager $conf, Mutex $mutex = null) |
46 | { | 53 | { |
54 | if ($mutex === null) { | ||
55 | // This should only happen with legacy classes | ||
56 | $mutex = new NoMutex(); | ||
57 | } | ||
47 | $this->conf = $conf; | 58 | $this->conf = $conf; |
48 | $this->datastore = $conf->get('resource.datastore'); | 59 | $this->datastore = $conf->get('resource.datastore'); |
60 | $this->mutex = $mutex; | ||
49 | } | 61 | } |
50 | 62 | ||
51 | /** | 63 | /** |
52 | * Reads database from disk to memory | 64 | * Reads database from disk to memory |
53 | * | 65 | * |
54 | * @return BookmarkArray instance | 66 | * @return Bookmark[] |
55 | * | 67 | * |
56 | * @throws NotWritableDataStoreException Data couldn't be loaded | 68 | * @throws NotWritableDataStoreException Data couldn't be loaded |
57 | * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark | 69 | * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark |
@@ -67,11 +79,16 @@ class BookmarkIO | |||
67 | throw new NotWritableDataStoreException($this->datastore); | 79 | throw new NotWritableDataStoreException($this->datastore); |
68 | } | 80 | } |
69 | 81 | ||
82 | $content = null; | ||
83 | $this->mutex->synchronized(function () use (&$content) { | ||
84 | $content = file_get_contents($this->datastore); | ||
85 | }); | ||
86 | |||
70 | // Note that gzinflate is faster than gzuncompress. | 87 | // Note that gzinflate is faster than gzuncompress. |
71 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | 88 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 |
72 | $links = unserialize(gzinflate(base64_decode( | 89 | $links = unserialize(gzinflate(base64_decode( |
73 | substr(file_get_contents($this->datastore), | 90 | substr($content, strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) |
74 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); | 91 | ))); |
75 | 92 | ||
76 | if (empty($links)) { | 93 | if (empty($links)) { |
77 | if (filesize($this->datastore) > 100) { | 94 | if (filesize($this->datastore) > 100) { |
@@ -86,7 +103,7 @@ class BookmarkIO | |||
86 | /** | 103 | /** |
87 | * Saves the database from memory to disk | 104 | * Saves the database from memory to disk |
88 | * | 105 | * |
89 | * @param BookmarkArray $links instance. | 106 | * @param Bookmark[] $links |
90 | * | 107 | * |
91 | * @throws NotWritableDataStoreException the datastore is not writable | 108 | * @throws NotWritableDataStoreException the datastore is not writable |
92 | */ | 109 | */ |
@@ -95,14 +112,18 @@ class BookmarkIO | |||
95 | if (is_file($this->datastore) && !is_writeable($this->datastore)) { | 112 | if (is_file($this->datastore) && !is_writeable($this->datastore)) { |
96 | // The datastore exists but is not writeable | 113 | // The datastore exists but is not writeable |
97 | throw new NotWritableDataStoreException($this->datastore); | 114 | throw new NotWritableDataStoreException($this->datastore); |
98 | } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { | 115 | } elseif (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { |
99 | // The datastore does not exist and its parent directory is not writeable | 116 | // The datastore does not exist and its parent directory is not writeable |
100 | throw new NotWritableDataStoreException(dirname($this->datastore)); | 117 | throw new NotWritableDataStoreException(dirname($this->datastore)); |
101 | } | 118 | } |
102 | 119 | ||
103 | file_put_contents( | 120 | $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix; |
104 | $this->datastore, | 121 | |
105 | self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix | 122 | $this->mutex->synchronized(function () use ($data) { |
106 | ); | 123 | file_put_contents( |
124 | $this->datastore, | ||
125 | $data | ||
126 | ); | ||
127 | }); | ||
107 | } | 128 | } |
108 | } | 129 | } |
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 815047e3..8ab5c441 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php | |||
@@ -1,5 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
5 | /** | 7 | /** |
@@ -11,6 +13,9 @@ namespace Shaarli\Bookmark; | |||
11 | * To prevent data corruption, it does not overwrite existing bookmarks, | 13 | * To prevent data corruption, it does not overwrite existing bookmarks, |
12 | * even though there should not be any. | 14 | * even though there should not be any. |
13 | * | 15 | * |
16 | * We disable this because otherwise it creates indentation issues, and heredoc is not supported by PHP gettext. | ||
17 | * @phpcs:disable Generic.Files.LineLength.TooLong | ||
18 | * | ||
14 | * @package Shaarli\Bookmark | 19 | * @package Shaarli\Bookmark |
15 | */ | 20 | */ |
16 | class BookmarkInitializer | 21 | class BookmarkInitializer |
@@ -23,7 +28,7 @@ class BookmarkInitializer | |||
23 | * | 28 | * |
24 | * @param BookmarkServiceInterface $bookmarkService | 29 | * @param BookmarkServiceInterface $bookmarkService |
25 | */ | 30 | */ |
26 | public function __construct($bookmarkService) | 31 | public function __construct(BookmarkServiceInterface $bookmarkService) |
27 | { | 32 | { |
28 | $this->bookmarkService = $bookmarkService; | 33 | $this->bookmarkService = $bookmarkService; |
29 | } | 34 | } |
@@ -31,13 +36,13 @@ class BookmarkInitializer | |||
31 | /** | 36 | /** |
32 | * Initialize the data store with default bookmarks | 37 | * Initialize the data store with default bookmarks |
33 | */ | 38 | */ |
34 | public function initialize() | 39 | public function initialize(): void |
35 | { | 40 | { |
36 | $bookmark = new Bookmark(); | 41 | $bookmark = new Bookmark(); |
37 | $bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)')); | 42 | $bookmark->setTitle('Calm Jazz Music - YouTube ' . t('(private bookmark with thumbnail demo)')); |
38 | $bookmark->setUrl('https://vimeo.com/153493904'); | 43 | $bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c'); |
39 | $bookmark->setDescription(t( | 44 | $bookmark->setDescription(t( |
40 | 'Shaarli will automatically pick up the thumbnail for links to a variety of websites. | 45 | 'Shaarli will automatically pick up the thumbnail for links to a variety of websites. |
41 | 46 | ||
42 | Explore your new Shaarli instance by trying out controls and menus. | 47 | Explore your new Shaarli instance by trying out controls and menus. |
43 | Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli. | 48 | Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli. |
@@ -52,7 +57,7 @@ Now you can edit or delete the default shaares. | |||
52 | $bookmark = new Bookmark(); | 57 | $bookmark = new Bookmark(); |
53 | $bookmark->setTitle(t('Note: Shaare descriptions')); | 58 | $bookmark->setTitle(t('Note: Shaare descriptions')); |
54 | $bookmark->setDescription(t( | 59 | $bookmark->setDescription(t( |
55 | 'Adding a shaare without entering a URL creates a text-only "note" post such as this one. | 60 | 'Adding a shaare without entering a URL creates a text-only "note" post such as this one. |
56 | This note is private, so you are the only one able to see it while logged in. | 61 | This note is private, so you are the only one able to see it while logged in. |
57 | 62 | ||
58 | You can use this to keep notes, post articles, code snippets, and much more. | 63 | You can use this to keep notes, post articles, code snippets, and much more. |
@@ -89,7 +94,7 @@ Markdown also supports tables: | |||
89 | 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service') | 94 | 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service') |
90 | ); | 95 | ); |
91 | $bookmark->setDescription(t( | 96 | $bookmark->setDescription(t( |
92 | 'Welcome to Shaarli! | 97 | 'Welcome to Shaarli! |
93 | 98 | ||
94 | Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately. | 99 | Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately. |
95 | You can add a description to your bookmarks, such as this one, and tag them. | 100 | You can add a description to your bookmarks, such as this one, and tag them. |
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index b9b483eb..08cdbb4e 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php | |||
@@ -1,79 +1,73 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | namespace Shaarli\Bookmark; | 3 | declare(strict_types=1); |
4 | 4 | ||
5 | namespace Shaarli\Bookmark; | ||
5 | 6 | ||
6 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 7 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
7 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; | 8 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; |
8 | use Shaarli\Config\ConfigManager; | ||
9 | use Shaarli\History; | ||
10 | 9 | ||
11 | /** | 10 | /** |
12 | * Class BookmarksService | 11 | * Class BookmarksService |
13 | * | 12 | * |
14 | * This is the entry point to manipulate the bookmark DB. | 13 | * This is the entry point to manipulate the bookmark DB. |
14 | * | ||
15 | * Regarding return types of a list of bookmarks, it can either be an array or an ArrayAccess implementation, | ||
16 | * so until PHP 8.0 is the minimal supported version with union return types it cannot be explicitly added. | ||
15 | */ | 17 | */ |
16 | interface BookmarkServiceInterface | 18 | interface BookmarkServiceInterface |
17 | { | 19 | { |
18 | /** | 20 | /** |
19 | * BookmarksService constructor. | ||
20 | * | ||
21 | * @param ConfigManager $conf instance | ||
22 | * @param History $history instance | ||
23 | * @param bool $isLoggedIn true if the current user is logged in | ||
24 | */ | ||
25 | public function __construct(ConfigManager $conf, History $history, $isLoggedIn); | ||
26 | |||
27 | /** | ||
28 | * Find a bookmark by hash | 21 | * Find a bookmark by hash |
29 | * | 22 | * |
30 | * @param string $hash | 23 | * @param string $hash Bookmark's hash |
24 | * @param string|null $privateKey Optional key used to access private links while logged out | ||
31 | * | 25 | * |
32 | * @return mixed | 26 | * @return Bookmark |
33 | * | 27 | * |
34 | * @throws \Exception | 28 | * @throws \Exception |
35 | */ | 29 | */ |
36 | public function findByHash($hash); | 30 | public function findByHash(string $hash, string $privateKey = null); |
37 | 31 | ||
38 | /** | 32 | /** |
39 | * @param $url | 33 | * @param $url |
40 | * | 34 | * |
41 | * @return Bookmark|null | 35 | * @return Bookmark|null |
42 | */ | 36 | */ |
43 | public function findByUrl($url); | 37 | public function findByUrl(string $url): ?Bookmark; |
44 | 38 | ||
45 | /** | 39 | /** |
46 | * Search bookmarks | 40 | * Search bookmarks |
47 | * | 41 | * |
48 | * @param mixed $request | 42 | * @param array $request |
49 | * @param string $visibility | 43 | * @param ?string $visibility |
50 | * @param bool $caseSensitive | 44 | * @param bool $caseSensitive |
51 | * @param bool $untaggedOnly | 45 | * @param bool $untaggedOnly |
52 | * @param bool $ignoreSticky | 46 | * @param bool $ignoreSticky |
53 | * | 47 | * |
54 | * @return Bookmark[] | 48 | * @return Bookmark[] |
55 | */ | 49 | */ |
56 | public function search( | 50 | public function search( |
57 | $request = [], | 51 | array $request = [], |
58 | $visibility = null, | 52 | string $visibility = null, |
59 | $caseSensitive = false, | 53 | bool $caseSensitive = false, |
60 | $untaggedOnly = false, | 54 | bool $untaggedOnly = false, |
61 | bool $ignoreSticky = false | 55 | bool $ignoreSticky = false |
62 | ); | 56 | ); |
63 | 57 | ||
64 | /** | 58 | /** |
65 | * Get a single bookmark by its ID. | 59 | * Get a single bookmark by its ID. |
66 | * | 60 | * |
67 | * @param int $id Bookmark ID | 61 | * @param int $id Bookmark ID |
68 | * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an | 62 | * @param ?string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an |
69 | * exception | 63 | * exception |
70 | * | 64 | * |
71 | * @return Bookmark | 65 | * @return Bookmark |
72 | * | 66 | * |
73 | * @throws BookmarkNotFoundException | 67 | * @throws BookmarkNotFoundException |
74 | * @throws \Exception | 68 | * @throws \Exception |
75 | */ | 69 | */ |
76 | public function get($id, $visibility = null); | 70 | public function get(int $id, string $visibility = null); |
77 | 71 | ||
78 | /** | 72 | /** |
79 | * Updates an existing bookmark (depending on its ID). | 73 | * Updates an existing bookmark (depending on its ID). |
@@ -86,7 +80,7 @@ interface BookmarkServiceInterface | |||
86 | * @throws BookmarkNotFoundException | 80 | * @throws BookmarkNotFoundException |
87 | * @throws \Exception | 81 | * @throws \Exception |
88 | */ | 82 | */ |
89 | public function set($bookmark, $save = true); | 83 | public function set(Bookmark $bookmark, bool $save = true): Bookmark; |
90 | 84 | ||
91 | /** | 85 | /** |
92 | * Adds a new bookmark (the ID must be empty). | 86 | * Adds a new bookmark (the ID must be empty). |
@@ -98,7 +92,7 @@ interface BookmarkServiceInterface | |||
98 | * | 92 | * |
99 | * @throws \Exception | 93 | * @throws \Exception |
100 | */ | 94 | */ |
101 | public function add($bookmark, $save = true); | 95 | public function add(Bookmark $bookmark, bool $save = true): Bookmark; |
102 | 96 | ||
103 | /** | 97 | /** |
104 | * Adds or updates a bookmark depending on its ID: | 98 | * Adds or updates a bookmark depending on its ID: |
@@ -112,7 +106,7 @@ interface BookmarkServiceInterface | |||
112 | * | 106 | * |
113 | * @throws \Exception | 107 | * @throws \Exception |
114 | */ | 108 | */ |
115 | public function addOrSet($bookmark, $save = true); | 109 | public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark; |
116 | 110 | ||
117 | /** | 111 | /** |
118 | * Deletes a bookmark. | 112 | * Deletes a bookmark. |
@@ -122,65 +116,72 @@ interface BookmarkServiceInterface | |||
122 | * | 116 | * |
123 | * @throws \Exception | 117 | * @throws \Exception |
124 | */ | 118 | */ |
125 | public function remove($bookmark, $save = true); | 119 | public function remove(Bookmark $bookmark, bool $save = true): void; |
126 | 120 | ||
127 | /** | 121 | /** |
128 | * Get a single bookmark by its ID. | 122 | * Get a single bookmark by its ID. |
129 | * | 123 | * |
130 | * @param int $id Bookmark ID | 124 | * @param int $id Bookmark ID |
131 | * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an | 125 | * @param ?string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an |
132 | * exception | 126 | * exception |
133 | * | 127 | * |
134 | * @return bool | 128 | * @return bool |
135 | */ | 129 | */ |
136 | public function exists($id, $visibility = null); | 130 | public function exists(int $id, string $visibility = null): bool; |
137 | 131 | ||
138 | /** | 132 | /** |
139 | * Return the number of available bookmarks for given visibility. | 133 | * Return the number of available bookmarks for given visibility. |
140 | * | 134 | * |
141 | * @param string $visibility public|private|all | 135 | * @param ?string $visibility public|private|all |
142 | * | 136 | * |
143 | * @return int Number of bookmarks | 137 | * @return int Number of bookmarks |
144 | */ | 138 | */ |
145 | public function count($visibility = null); | 139 | public function count(string $visibility = null): int; |
146 | 140 | ||
147 | /** | 141 | /** |
148 | * Write the datastore. | 142 | * Write the datastore. |
149 | * | 143 | * |
150 | * @throws NotWritableDataStoreException | 144 | * @throws NotWritableDataStoreException |
151 | */ | 145 | */ |
152 | public function save(); | 146 | public function save(): void; |
153 | 147 | ||
154 | /** | 148 | /** |
155 | * Returns the list tags appearing in the bookmarks with the given tags | 149 | * Returns the list tags appearing in the bookmarks with the given tags |
156 | * | 150 | * |
157 | * @param array $filteringTags tags selecting the bookmarks to consider | 151 | * @param array|null $filteringTags tags selecting the bookmarks to consider |
158 | * @param string $visibility process only all/private/public bookmarks | 152 | * @param string|null $visibility process only all/private/public bookmarks |
159 | * | 153 | * |
160 | * @return array tag => bookmarksCount | 154 | * @return array tag => bookmarksCount |
161 | */ | 155 | */ |
162 | public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all'); | 156 | public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; |
163 | 157 | ||
164 | /** | 158 | /** |
165 | * Returns the list of days containing articles (oldest first) | 159 | * Return a list of bookmark matching provided period of time. |
160 | * It also update directly previous and next date outside of given period found in the datastore. | ||
161 | * | ||
162 | * @param \DateTimeInterface $from Starting date. | ||
163 | * @param \DateTimeInterface $to Ending date. | ||
164 | * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from. | ||
165 | * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to. | ||
166 | * | 166 | * |
167 | * @return array containing days (in format YYYYMMDD). | 167 | * @return array List of bookmarks matching provided period of time. |
168 | */ | 168 | */ |
169 | public function days(); | 169 | public function findByDate( |
170 | \DateTimeInterface $from, | ||
171 | \DateTimeInterface $to, | ||
172 | ?\DateTimeInterface &$previous, | ||
173 | ?\DateTimeInterface &$next | ||
174 | ): array; | ||
170 | 175 | ||
171 | /** | 176 | /** |
172 | * Returns the list of articles for a given day. | 177 | * Returns the latest bookmark by creation date. |
173 | * | 178 | * |
174 | * @param string $request day to filter. Format: YYYYMMDD. | 179 | * @return Bookmark|null Found Bookmark or null if the datastore is empty. |
175 | * | ||
176 | * @return Bookmark[] list of shaare found. | ||
177 | * | ||
178 | * @throws BookmarkNotFoundException | ||
179 | */ | 180 | */ |
180 | public function filterDay($request); | 181 | public function getLatest(): ?Bookmark; |
181 | 182 | ||
182 | /** | 183 | /** |
183 | * Creates the default database after a fresh install. | 184 | * Creates the default database after a fresh install. |
184 | */ | 185 | */ |
185 | public function initialize(); | 186 | public function initialize(): void; |
186 | } | 187 | } |
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index e7af4d55..d65e97ed 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php | |||
@@ -66,16 +66,19 @@ function html_extract_tag($tag, $html) | |||
66 | { | 66 | { |
67 | $propertiesKey = ['property', 'name', 'itemprop']; | 67 | $propertiesKey = ['property', 'name', 'itemprop']; |
68 | $properties = implode('|', $propertiesKey); | 68 | $properties = implode('|', $propertiesKey); |
69 | // Try to retrieve OpenGraph image. | 69 | // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' |
70 | $ogRegex = '#<meta[^>]+(?:'. $properties .')=["\']?(?:og:)?'. $tag .'["\'\s][^>]*content=["\']?(.*?)["\'/>]#'; | 70 | $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; |
71 | // Try to retrieve OpenGraph tag. | ||
72 | $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; | ||
71 | // If the attributes are not in the order property => content (e.g. Github) | 73 | // If the attributes are not in the order property => content (e.g. Github) |
72 | // New regex to keep this readable... more or less. | 74 | // New regex to keep this readable... more or less. |
73 | $ogRegexReverse = '#<meta[^>]+content=["\']([^"\']+)[^>]+(?:'. $properties .')=["\']?(?:og)?:'. $tag .'["\'\s/>]#'; | 75 | $ogRegexReverse = '#<meta[^>]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; |
74 | 76 | ||
75 | if (preg_match($ogRegex, $html, $matches) > 0 | 77 | if ( |
78 | preg_match($ogRegex, $html, $matches) > 0 | ||
76 | || preg_match($ogRegexReverse, $html, $matches) > 0 | 79 | || preg_match($ogRegexReverse, $html, $matches) > 0 |
77 | ) { | 80 | ) { |
78 | return $matches[1]; | 81 | return $matches[2]; |
79 | } | 82 | } |
80 | 83 | ||
81 | return false; | 84 | return false; |
@@ -114,7 +117,7 @@ function hashtag_autolink($description, $indexUrl = '') | |||
114 | * \p{Mn} - any non marking space (accents, umlauts, etc) | 117 | * \p{Mn} - any non marking space (accents, umlauts, etc) |
115 | */ | 118 | */ |
116 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; | 119 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; |
117 | $replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>'; | 120 | $replacement = '$1<a href="' . $indexUrl . './add-tag/$2" title="Hashtag $2">#$2</a>'; |
118 | return preg_replace($regex, $replacement, $description); | 121 | return preg_replace($regex, $replacement, $description); |
119 | } | 122 | } |
120 | 123 | ||
@@ -136,12 +139,17 @@ function space2nbsp($text) | |||
136 | * | 139 | * |
137 | * @param string $description shaare's description. | 140 | * @param string $description shaare's description. |
138 | * @param string $indexUrl URL to Shaarli's index. | 141 | * @param string $indexUrl URL to Shaarli's index. |
139 | 142 | * @param bool $autolink Turn on/off automatic linkifications of URLs and hashtags | |
143 | * | ||
140 | * @return string formatted description. | 144 | * @return string formatted description. |
141 | */ | 145 | */ |
142 | function format_description($description, $indexUrl = '') | 146 | function format_description($description, $indexUrl = '', $autolink = true) |
143 | { | 147 | { |
144 | return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl))); | 148 | if ($autolink) { |
149 | $description = hashtag_autolink(text2clickable($description), $indexUrl); | ||
150 | } | ||
151 | |||
152 | return nl2br(space2nbsp($description)); | ||
145 | } | 153 | } |
146 | 154 | ||
147 | /** | 155 | /** |
@@ -169,3 +177,49 @@ function is_note($linkUrl) | |||
169 | { | 177 | { |
170 | return isset($linkUrl[0]) && $linkUrl[0] === '?'; | 178 | return isset($linkUrl[0]) && $linkUrl[0] === '?'; |
171 | } | 179 | } |
180 | |||
181 | /** | ||
182 | * Extract an array of tags from a given tag string, with provided separator. | ||
183 | * | ||
184 | * @param string|null $tags String containing a list of tags separated by $separator. | ||
185 | * @param string $separator Shaarli's default: ' ' (whitespace) | ||
186 | * | ||
187 | * @return array List of tags | ||
188 | */ | ||
189 | function tags_str2array(?string $tags, string $separator): array | ||
190 | { | ||
191 | // For whitespaces, we use the special \s regex character | ||
192 | $separator = $separator === ' ' ? '\s' : $separator; | ||
193 | |||
194 | return preg_split('/\s*' . $separator . '+\s*/', trim($tags) ?? '', -1, PREG_SPLIT_NO_EMPTY); | ||
195 | } | ||
196 | |||
197 | /** | ||
198 | * Return a tag string with provided separator from a list of tags. | ||
199 | * Note that given array is clean up by tags_filter(). | ||
200 | * | ||
201 | * @param array|null $tags List of tags | ||
202 | * @param string $separator | ||
203 | * | ||
204 | * @return string | ||
205 | */ | ||
206 | function tags_array2str(?array $tags, string $separator): string | ||
207 | { | ||
208 | return implode($separator, tags_filter($tags, $separator)); | ||
209 | } | ||
210 | |||
211 | /** | ||
212 | * Clean an array of tags: trim + remove empty entries | ||
213 | * | ||
214 | * @param array|null $tags List of tags | ||
215 | * @param string $separator | ||
216 | * | ||
217 | * @return array | ||
218 | */ | ||
219 | function tags_filter(?array $tags, string $separator): array | ||
220 | { | ||
221 | $trimDefault = " \t\n\r\0\x0B"; | ||
222 | return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string { | ||
223 | return trim($entry, $trimDefault . $separator); | ||
224 | }, $tags ?? []))); | ||
225 | } | ||
diff --git a/application/bookmark/exception/BookmarkNotFoundException.php b/application/bookmark/exception/BookmarkNotFoundException.php index 827a3d35..a91d1efa 100644 --- a/application/bookmark/exception/BookmarkNotFoundException.php +++ b/application/bookmark/exception/BookmarkNotFoundException.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Bookmark\Exception; | 3 | namespace Shaarli\Bookmark\Exception; |
3 | 4 | ||
4 | use Exception; | 5 | use Exception; |
diff --git a/application/bookmark/exception/EmptyDataStoreException.php b/application/bookmark/exception/EmptyDataStoreException.php index cd48c1e6..16a98470 100644 --- a/application/bookmark/exception/EmptyDataStoreException.php +++ b/application/bookmark/exception/EmptyDataStoreException.php | |||
@@ -1,7 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | |||
4 | namespace Shaarli\Bookmark\Exception; | 3 | namespace Shaarli\Bookmark\Exception; |
5 | 4 | ||
6 | 5 | class EmptyDataStoreException extends \Exception | |
7 | class EmptyDataStoreException extends \Exception {} | 6 | { |
7 | } | ||
diff --git a/application/bookmark/exception/InvalidBookmarkException.php b/application/bookmark/exception/InvalidBookmarkException.php index 10c84a6d..fe184f8c 100644 --- a/application/bookmark/exception/InvalidBookmarkException.php +++ b/application/bookmark/exception/InvalidBookmarkException.php | |||
@@ -16,14 +16,14 @@ class InvalidBookmarkException extends \Exception | |||
16 | } else { | 16 | } else { |
17 | $created = 'Not a DateTime object'; | 17 | $created = 'Not a DateTime object'; |
18 | } | 18 | } |
19 | $this->message = 'This bookmark is not valid'. PHP_EOL; | 19 | $this->message = 'This bookmark is not valid' . PHP_EOL; |
20 | $this->message .= ' - ID: '. $bookmark->getId() . PHP_EOL; | 20 | $this->message .= ' - ID: ' . $bookmark->getId() . PHP_EOL; |
21 | $this->message .= ' - Title: '. $bookmark->getTitle() . PHP_EOL; | 21 | $this->message .= ' - Title: ' . $bookmark->getTitle() . PHP_EOL; |
22 | $this->message .= ' - Url: '. $bookmark->getUrl() . PHP_EOL; | 22 | $this->message .= ' - Url: ' . $bookmark->getUrl() . PHP_EOL; |
23 | $this->message .= ' - ShortUrl: '. $bookmark->getShortUrl() . PHP_EOL; | 23 | $this->message .= ' - ShortUrl: ' . $bookmark->getShortUrl() . PHP_EOL; |
24 | $this->message .= ' - Created: '. $created . PHP_EOL; | 24 | $this->message .= ' - Created: ' . $created . PHP_EOL; |
25 | } else { | 25 | } else { |
26 | $this->message = 'The provided data is not a bookmark'. PHP_EOL; | 26 | $this->message = 'The provided data is not a bookmark' . PHP_EOL; |
27 | $this->message .= var_export($bookmark, true); | 27 | $this->message .= var_export($bookmark, true); |
28 | } | 28 | } |
29 | } | 29 | } |
diff --git a/application/bookmark/exception/NotWritableDataStoreException.php b/application/bookmark/exception/NotWritableDataStoreException.php index 95f34b50..df91f3bc 100644 --- a/application/bookmark/exception/NotWritableDataStoreException.php +++ b/application/bookmark/exception/NotWritableDataStoreException.php | |||
@@ -1,9 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | |||
4 | namespace Shaarli\Bookmark\Exception; | 3 | namespace Shaarli\Bookmark\Exception; |
5 | 4 | ||
6 | |||
7 | class NotWritableDataStoreException extends \Exception | 5 | class NotWritableDataStoreException extends \Exception |
8 | { | 6 | { |
9 | /** | 7 | /** |
@@ -13,7 +11,7 @@ class NotWritableDataStoreException extends \Exception | |||
13 | */ | 11 | */ |
14 | public function __construct($dataStore) | 12 | public function __construct($dataStore) |
15 | { | 13 | { |
16 | $this->message = 'Couldn\'t load data from the data store file "'. $dataStore .'". '. | 14 | $this->message = 'Couldn\'t load data from the data store file "' . $dataStore . '". ' . |
17 | 'Your data might be corrupted, or your file isn\'t readable.'; | 15 | 'Your data might be corrupted, or your file isn\'t readable.'; |
18 | } | 16 | } |
19 | } | 17 | } |
diff --git a/application/config/ConfigIO.php b/application/config/ConfigIO.php index 3efe5b6f..a623bc8b 100644 --- a/application/config/ConfigIO.php +++ b/application/config/ConfigIO.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Config; | 3 | namespace Shaarli\Config; |
3 | 4 | ||
4 | /** | 5 | /** |
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php index c0c0dab9..23b22269 100644 --- a/application/config/ConfigJson.php +++ b/application/config/ConfigJson.php | |||
@@ -19,7 +19,7 @@ class ConfigJson implements ConfigIO | |||
19 | $data = file_get_contents($filepath); | 19 | $data = file_get_contents($filepath); |
20 | $data = str_replace(self::getPhpHeaders(), '', $data); | 20 | $data = str_replace(self::getPhpHeaders(), '', $data); |
21 | $data = str_replace(self::getPhpSuffix(), '', $data); | 21 | $data = str_replace(self::getPhpSuffix(), '', $data); |
22 | $data = json_decode($data, true); | 22 | $data = json_decode(trim($data), true); |
23 | if ($data === null) { | 23 | if ($data === null) { |
24 | $errorCode = json_last_error(); | 24 | $errorCode = json_last_error(); |
25 | $error = sprintf( | 25 | $error = sprintf( |
@@ -73,7 +73,7 @@ class ConfigJson implements ConfigIO | |||
73 | */ | 73 | */ |
74 | public static function getPhpHeaders() | 74 | public static function getPhpHeaders() |
75 | { | 75 | { |
76 | return '<?php /*'. PHP_EOL; | 76 | return '<?php /*'; |
77 | } | 77 | } |
78 | 78 | ||
79 | /** | 79 | /** |
@@ -85,6 +85,6 @@ class ConfigJson implements ConfigIO | |||
85 | */ | 85 | */ |
86 | public static function getPhpSuffix() | 86 | public static function getPhpSuffix() |
87 | { | 87 | { |
88 | return PHP_EOL . '*/ ?>'; | 88 | return '*/ ?>'; |
89 | } | 89 | } |
90 | } | 90 | } |
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index 4c98be30..717a038f 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Config; | 3 | namespace Shaarli\Config; |
3 | 4 | ||
4 | use Shaarli\Config\Exception\MissingFieldConfigException; | 5 | use Shaarli\Config\Exception\MissingFieldConfigException; |
@@ -20,7 +21,7 @@ class ConfigManager | |||
20 | */ | 21 | */ |
21 | protected static $NOT_FOUND = 'NOT_FOUND'; | 22 | protected static $NOT_FOUND = 'NOT_FOUND'; |
22 | 23 | ||
23 | public static $DEFAULT_PLUGINS = array('qrcode'); | 24 | public static $DEFAULT_PLUGINS = ['qrcode']; |
24 | 25 | ||
25 | /** | 26 | /** |
26 | * @var string Config folder. | 27 | * @var string Config folder. |
@@ -133,7 +134,7 @@ class ConfigManager | |||
133 | public function set($setting, $value, $write = false, $isLoggedIn = false) | 134 | public function set($setting, $value, $write = false, $isLoggedIn = false) |
134 | { | 135 | { |
135 | if (empty($setting) || ! is_string($setting)) { | 136 | if (empty($setting) || ! is_string($setting)) { |
136 | throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting)); | 137 | throw new \Exception(t('Invalid setting key parameter. String expected, got: ') . gettype($setting)); |
137 | } | 138 | } |
138 | 139 | ||
139 | // During the ConfigIO transition, map legacy settings to the new ones. | 140 | // During the ConfigIO transition, map legacy settings to the new ones. |
@@ -160,7 +161,7 @@ class ConfigManager | |||
160 | public function remove($setting, $write = false, $isLoggedIn = false) | 161 | public function remove($setting, $write = false, $isLoggedIn = false) |
161 | { | 162 | { |
162 | if (empty($setting) || ! is_string($setting)) { | 163 | if (empty($setting) || ! is_string($setting)) { |
163 | throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting)); | 164 | throw new \Exception(t('Invalid setting key parameter. String expected, got: ') . gettype($setting)); |
164 | } | 165 | } |
165 | 166 | ||
166 | // During the ConfigIO transition, map legacy settings to the new ones. | 167 | // During the ConfigIO transition, map legacy settings to the new ones. |
@@ -213,7 +214,7 @@ class ConfigManager | |||
213 | public function write($isLoggedIn) | 214 | public function write($isLoggedIn) |
214 | { | 215 | { |
215 | // These fields are required in configuration. | 216 | // These fields are required in configuration. |
216 | $mandatoryFields = array( | 217 | $mandatoryFields = [ |
217 | 'credentials.login', | 218 | 'credentials.login', |
218 | 'credentials.hash', | 219 | 'credentials.hash', |
219 | 'credentials.salt', | 220 | 'credentials.salt', |
@@ -222,7 +223,7 @@ class ConfigManager | |||
222 | 'general.title', | 223 | 'general.title', |
223 | 'general.header_link', | 224 | 'general.header_link', |
224 | 'privacy.default_private_links', | 225 | 'privacy.default_private_links', |
225 | ); | 226 | ]; |
226 | 227 | ||
227 | // Only logged in user can alter config. | 228 | // Only logged in user can alter config. |
228 | if (is_file($this->getConfigFileExt()) && !$isLoggedIn) { | 229 | if (is_file($this->getConfigFileExt()) && !$isLoggedIn) { |
@@ -366,10 +367,12 @@ class ConfigManager | |||
366 | $this->setEmpty('general.links_per_page', 20); | 367 | $this->setEmpty('general.links_per_page', 20); |
367 | $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); | 368 | $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); |
368 | $this->setEmpty('general.default_note_title', 'Note: '); | 369 | $this->setEmpty('general.default_note_title', 'Note: '); |
369 | $this->setEmpty('general.retrieve_description', false); | 370 | $this->setEmpty('general.retrieve_description', true); |
371 | $this->setEmpty('general.enable_async_metadata', true); | ||
372 | $this->setEmpty('general.tags_separator', ' '); | ||
370 | 373 | ||
371 | $this->setEmpty('updates.check_updates', false); | 374 | $this->setEmpty('updates.check_updates', true); |
372 | $this->setEmpty('updates.check_updates_branch', 'stable'); | 375 | $this->setEmpty('updates.check_updates_branch', 'latest'); |
373 | $this->setEmpty('updates.check_updates_interval', 86400); | 376 | $this->setEmpty('updates.check_updates_interval', 86400); |
374 | 377 | ||
375 | $this->setEmpty('feed.rss_permalinks', true); | 378 | $this->setEmpty('feed.rss_permalinks', true); |
@@ -390,7 +393,7 @@ class ConfigManager | |||
390 | $this->setEmpty('translation.mode', 'php'); | 393 | $this->setEmpty('translation.mode', 'php'); |
391 | $this->setEmpty('translation.extensions', []); | 394 | $this->setEmpty('translation.extensions', []); |
392 | 395 | ||
393 | $this->setEmpty('plugins', array()); | 396 | $this->setEmpty('plugins', []); |
394 | 397 | ||
395 | $this->setEmpty('formatter', 'markdown'); | 398 | $this->setEmpty('formatter', 'markdown'); |
396 | } | 399 | } |
diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php index cad34594..53d6a7a3 100644 --- a/application/config/ConfigPhp.php +++ b/application/config/ConfigPhp.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Config; | 3 | namespace Shaarli\Config; |
3 | 4 | ||
4 | /** | 5 | /** |
@@ -12,7 +13,7 @@ class ConfigPhp implements ConfigIO | |||
12 | /** | 13 | /** |
13 | * @var array List of config key without group. | 14 | * @var array List of config key without group. |
14 | */ | 15 | */ |
15 | public static $ROOT_KEYS = array( | 16 | public static $ROOT_KEYS = [ |
16 | 'login', | 17 | 'login', |
17 | 'hash', | 18 | 'hash', |
18 | 'salt', | 19 | 'salt', |
@@ -22,7 +23,7 @@ class ConfigPhp implements ConfigIO | |||
22 | 'redirector', | 23 | 'redirector', |
23 | 'disablesessionprotection', | 24 | 'disablesessionprotection', |
24 | 'privateLinkByDefault', | 25 | 'privateLinkByDefault', |
25 | ); | 26 | ]; |
26 | 27 | ||
27 | /** | 28 | /** |
28 | * Map legacy config keys with the new ones. | 29 | * Map legacy config keys with the new ones. |
@@ -31,7 +32,7 @@ class ConfigPhp implements ConfigIO | |||
31 | * | 32 | * |
32 | * @var array current key => legacy key. | 33 | * @var array current key => legacy key. |
33 | */ | 34 | */ |
34 | public static $LEGACY_KEYS_MAPPING = array( | 35 | public static $LEGACY_KEYS_MAPPING = [ |
35 | 'credentials.login' => 'login', | 36 | 'credentials.login' => 'login', |
36 | 'credentials.hash' => 'hash', | 37 | 'credentials.hash' => 'hash', |
37 | 'credentials.salt' => 'salt', | 38 | 'credentials.salt' => 'salt', |
@@ -68,7 +69,7 @@ class ConfigPhp implements ConfigIO | |||
68 | 'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS', | 69 | 'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS', |
69 | 'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS', | 70 | 'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS', |
70 | 'security.open_shaarli' => 'config.OPEN_SHAARLI', | 71 | 'security.open_shaarli' => 'config.OPEN_SHAARLI', |
71 | ); | 72 | ]; |
72 | 73 | ||
73 | /** | 74 | /** |
74 | * @inheritdoc | 75 | * @inheritdoc |
@@ -76,12 +77,12 @@ class ConfigPhp implements ConfigIO | |||
76 | public function read($filepath) | 77 | public function read($filepath) |
77 | { | 78 | { |
78 | if (! file_exists($filepath) || ! is_readable($filepath)) { | 79 | if (! file_exists($filepath) || ! is_readable($filepath)) { |
79 | return array(); | 80 | return []; |
80 | } | 81 | } |
81 | 82 | ||
82 | include $filepath; | 83 | include $filepath; |
83 | 84 | ||
84 | $out = array(); | 85 | $out = []; |
85 | foreach (self::$ROOT_KEYS as $key) { | 86 | foreach (self::$ROOT_KEYS as $key) { |
86 | $out[$key] = isset($GLOBALS[$key]) ? $GLOBALS[$key] : ''; | 87 | $out[$key] = isset($GLOBALS[$key]) ? $GLOBALS[$key] : ''; |
87 | } | 88 | } |
@@ -95,7 +96,7 @@ class ConfigPhp implements ConfigIO | |||
95 | */ | 96 | */ |
96 | public function write($filepath, $conf) | 97 | public function write($filepath, $conf) |
97 | { | 98 | { |
98 | $configStr = '<?php '. PHP_EOL; | 99 | $configStr = '<?php ' . PHP_EOL; |
99 | foreach (self::$ROOT_KEYS as $key) { | 100 | foreach (self::$ROOT_KEYS as $key) { |
100 | if (isset($conf[$key])) { | 101 | if (isset($conf[$key])) { |
101 | $configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL; | 102 | $configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL; |
@@ -106,8 +107,8 @@ class ConfigPhp implements ConfigIO | |||
106 | foreach ($conf['config'] as $key => $value) { | 107 | foreach ($conf['config'] as $key => $value) { |
107 | $configStr .= '$GLOBALS[\'config\'][\'' | 108 | $configStr .= '$GLOBALS[\'config\'][\'' |
108 | . $key | 109 | . $key |
109 | .'\'] = ' | 110 | . '\'] = ' |
110 | .var_export($conf['config'][$key], true).';' | 111 | . var_export($conf['config'][$key], true) . ';' |
111 | . PHP_EOL; | 112 | . PHP_EOL; |
112 | } | 113 | } |
113 | 114 | ||
@@ -115,18 +116,19 @@ class ConfigPhp implements ConfigIO | |||
115 | foreach ($conf['plugins'] as $key => $value) { | 116 | foreach ($conf['plugins'] as $key => $value) { |
116 | $configStr .= '$GLOBALS[\'plugins\'][\'' | 117 | $configStr .= '$GLOBALS[\'plugins\'][\'' |
117 | . $key | 118 | . $key |
118 | .'\'] = ' | 119 | . '\'] = ' |
119 | .var_export($conf['plugins'][$key], true).';' | 120 | . var_export($conf['plugins'][$key], true) . ';' |
120 | . PHP_EOL; | 121 | . PHP_EOL; |
121 | } | 122 | } |
122 | } | 123 | } |
123 | 124 | ||
124 | if (!file_put_contents($filepath, $configStr) | 125 | if ( |
126 | !file_put_contents($filepath, $configStr) | ||
125 | || strcmp(file_get_contents($filepath), $configStr) != 0 | 127 | || strcmp(file_get_contents($filepath), $configStr) != 0 |
126 | ) { | 128 | ) { |
127 | throw new \Shaarli\Exceptions\IOException( | 129 | throw new \Shaarli\Exceptions\IOException( |
128 | $filepath, | 130 | $filepath, |
129 | t('Shaarli could not create the config file. '. | 131 | t('Shaarli could not create the config file. ' . |
130 | 'Please make sure Shaarli has the right to write in the folder is it installed in.') | 132 | 'Please make sure Shaarli has the right to write in the folder is it installed in.') |
131 | ); | 133 | ); |
132 | } | 134 | } |
diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php index ea8dfbda..6cadef12 100644 --- a/application/config/ConfigPlugin.php +++ b/application/config/ConfigPlugin.php | |||
@@ -39,8 +39,8 @@ function save_plugin_config($formData) | |||
39 | throw new PluginConfigOrderException(); | 39 | throw new PluginConfigOrderException(); |
40 | } | 40 | } |
41 | 41 | ||
42 | $plugins = array(); | 42 | $plugins = []; |
43 | $newEnabledPlugins = array(); | 43 | $newEnabledPlugins = []; |
44 | foreach ($formData as $key => $data) { | 44 | foreach ($formData as $key => $data) { |
45 | if (startsWith($key, 'order')) { | 45 | if (startsWith($key, 'order')) { |
46 | continue; | 46 | continue; |
@@ -62,7 +62,7 @@ function save_plugin_config($formData) | |||
62 | throw new PluginConfigOrderException(); | 62 | throw new PluginConfigOrderException(); |
63 | } | 63 | } |
64 | 64 | ||
65 | $finalPlugins = array(); | 65 | $finalPlugins = []; |
66 | // Make plugins order continuous. | 66 | // Make plugins order continuous. |
67 | foreach ($plugins as $plugin) { | 67 | foreach ($plugins as $plugin) { |
68 | $finalPlugins[] = $plugin; | 68 | $finalPlugins[] = $plugin; |
@@ -81,7 +81,7 @@ function save_plugin_config($formData) | |||
81 | */ | 81 | */ |
82 | function validate_plugin_order($formData) | 82 | function validate_plugin_order($formData) |
83 | { | 83 | { |
84 | $orders = array(); | 84 | $orders = []; |
85 | foreach ($formData as $key => $value) { | 85 | foreach ($formData as $key => $value) { |
86 | // No duplicate order allowed. | 86 | // No duplicate order allowed. |
87 | if (in_array($value, $orders, true)) { | 87 | if (in_array($value, $orders, true)) { |
diff --git a/application/config/exception/MissingFieldConfigException.php b/application/config/exception/MissingFieldConfigException.php index 9e0a9359..a5f4356a 100644 --- a/application/config/exception/MissingFieldConfigException.php +++ b/application/config/exception/MissingFieldConfigException.php | |||
@@ -1,6 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | |||
4 | namespace Shaarli\Config\Exception; | 3 | namespace Shaarli\Config\Exception; |
5 | 4 | ||
6 | /** | 5 | /** |
diff --git a/application/config/exception/UnauthorizedConfigException.php b/application/config/exception/UnauthorizedConfigException.php index 72311fae..b041c6e3 100644 --- a/application/config/exception/UnauthorizedConfigException.php +++ b/application/config/exception/UnauthorizedConfigException.php | |||
@@ -1,6 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | |||
4 | namespace Shaarli\Config\Exception; | 3 | namespace Shaarli\Config\Exception; |
5 | 4 | ||
6 | /** | 5 | /** |
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 55bb51b5..f0234eca 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php | |||
@@ -4,6 +4,8 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Container; | 5 | namespace Shaarli\Container; |
6 | 6 | ||
7 | use malkusch\lock\mutex\FlockMutex; | ||
8 | use Psr\Log\LoggerInterface; | ||
7 | use Shaarli\Bookmark\BookmarkFileService; | 9 | use Shaarli\Bookmark\BookmarkFileService; |
8 | use Shaarli\Bookmark\BookmarkServiceInterface; | 10 | use Shaarli\Bookmark\BookmarkServiceInterface; |
9 | use Shaarli\Config\ConfigManager; | 11 | use Shaarli\Config\ConfigManager; |
@@ -13,6 +15,7 @@ use Shaarli\Front\Controller\Visitor\ErrorController; | |||
13 | use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; | 15 | use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; |
14 | use Shaarli\History; | 16 | use Shaarli\History; |
15 | use Shaarli\Http\HttpAccess; | 17 | use Shaarli\Http\HttpAccess; |
18 | use Shaarli\Http\MetadataRetriever; | ||
16 | use Shaarli\Netscape\NetscapeBookmarkUtils; | 19 | use Shaarli\Netscape\NetscapeBookmarkUtils; |
17 | use Shaarli\Plugin\PluginManager; | 20 | use Shaarli\Plugin\PluginManager; |
18 | use Shaarli\Render\PageBuilder; | 21 | use Shaarli\Render\PageBuilder; |
@@ -47,6 +50,9 @@ class ContainerBuilder | |||
47 | /** @var LoginManager */ | 50 | /** @var LoginManager */ |
48 | protected $login; | 51 | protected $login; |
49 | 52 | ||
53 | /** @var LoggerInterface */ | ||
54 | protected $logger; | ||
55 | |||
50 | /** @var string|null */ | 56 | /** @var string|null */ |
51 | protected $basePath = null; | 57 | protected $basePath = null; |
52 | 58 | ||
@@ -54,12 +60,14 @@ class ContainerBuilder | |||
54 | ConfigManager $conf, | 60 | ConfigManager $conf, |
55 | SessionManager $session, | 61 | SessionManager $session, |
56 | CookieManager $cookieManager, | 62 | CookieManager $cookieManager, |
57 | LoginManager $login | 63 | LoginManager $login, |
64 | LoggerInterface $logger | ||
58 | ) { | 65 | ) { |
59 | $this->conf = $conf; | 66 | $this->conf = $conf; |
60 | $this->session = $session; | 67 | $this->session = $session; |
61 | $this->login = $login; | 68 | $this->login = $login; |
62 | $this->cookieManager = $cookieManager; | 69 | $this->cookieManager = $cookieManager; |
70 | $this->logger = $logger; | ||
63 | } | 71 | } |
64 | 72 | ||
65 | public function build(): ShaarliContainer | 73 | public function build(): ShaarliContainer |
@@ -70,6 +78,7 @@ class ContainerBuilder | |||
70 | $container['sessionManager'] = $this->session; | 78 | $container['sessionManager'] = $this->session; |
71 | $container['cookieManager'] = $this->cookieManager; | 79 | $container['cookieManager'] = $this->cookieManager; |
72 | $container['loginManager'] = $this->login; | 80 | $container['loginManager'] = $this->login; |
81 | $container['logger'] = $this->logger; | ||
73 | $container['basePath'] = $this->basePath; | 82 | $container['basePath'] = $this->basePath; |
74 | 83 | ||
75 | $container['plugins'] = function (ShaarliContainer $container): PluginManager { | 84 | $container['plugins'] = function (ShaarliContainer $container): PluginManager { |
@@ -84,14 +93,20 @@ class ContainerBuilder | |||
84 | return new BookmarkFileService( | 93 | return new BookmarkFileService( |
85 | $container->conf, | 94 | $container->conf, |
86 | $container->history, | 95 | $container->history, |
96 | new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2), | ||
87 | $container->loginManager->isLoggedIn() | 97 | $container->loginManager->isLoggedIn() |
88 | ); | 98 | ); |
89 | }; | 99 | }; |
90 | 100 | ||
101 | $container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever { | ||
102 | return new MetadataRetriever($container->conf, $container->httpAccess); | ||
103 | }; | ||
104 | |||
91 | $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder { | 105 | $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder { |
92 | return new PageBuilder( | 106 | return new PageBuilder( |
93 | $container->conf, | 107 | $container->conf, |
94 | $container->sessionManager->getSession(), | 108 | $container->sessionManager->getSession(), |
109 | $container->logger, | ||
95 | $container->bookmarkService, | 110 | $container->bookmarkService, |
96 | $container->sessionManager->generateToken(), | 111 | $container->sessionManager->generateToken(), |
97 | $container->loginManager->isLoggedIn() | 112 | $container->loginManager->isLoggedIn() |
@@ -143,7 +158,7 @@ class ContainerBuilder | |||
143 | 158 | ||
144 | $container['updater'] = function (ShaarliContainer $container): Updater { | 159 | $container['updater'] = function (ShaarliContainer $container): Updater { |
145 | return new Updater( | 160 | return new Updater( |
146 | UpdaterUtils::read_updates_file($container->conf->get('resource.updates')), | 161 | UpdaterUtils::readUpdatesFile($container->conf->get('resource.updates')), |
147 | $container->bookmarkService, | 162 | $container->bookmarkService, |
148 | $container->conf, | 163 | $container->conf, |
149 | $container->loginManager->isLoggedIn() | 164 | $container->loginManager->isLoggedIn() |
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 66e669aa..3e5bd252 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php | |||
@@ -4,12 +4,14 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Container; | 5 | namespace Shaarli\Container; |
6 | 6 | ||
7 | use Psr\Log\LoggerInterface; | ||
7 | use Shaarli\Bookmark\BookmarkServiceInterface; | 8 | use Shaarli\Bookmark\BookmarkServiceInterface; |
8 | use Shaarli\Config\ConfigManager; | 9 | use Shaarli\Config\ConfigManager; |
9 | use Shaarli\Feed\FeedBuilder; | 10 | use Shaarli\Feed\FeedBuilder; |
10 | use Shaarli\Formatter\FormatterFactory; | 11 | use Shaarli\Formatter\FormatterFactory; |
11 | use Shaarli\History; | 12 | use Shaarli\History; |
12 | use Shaarli\Http\HttpAccess; | 13 | use Shaarli\Http\HttpAccess; |
14 | use Shaarli\Http\MetadataRetriever; | ||
13 | use Shaarli\Netscape\NetscapeBookmarkUtils; | 15 | use Shaarli\Netscape\NetscapeBookmarkUtils; |
14 | use Shaarli\Plugin\PluginManager; | 16 | use Shaarli\Plugin\PluginManager; |
15 | use Shaarli\Render\PageBuilder; | 17 | use Shaarli\Render\PageBuilder; |
@@ -35,6 +37,8 @@ use Slim\Container; | |||
35 | * @property History $history | 37 | * @property History $history |
36 | * @property HttpAccess $httpAccess | 38 | * @property HttpAccess $httpAccess |
37 | * @property LoginManager $loginManager | 39 | * @property LoginManager $loginManager |
40 | * @property LoggerInterface $logger | ||
41 | * @property MetadataRetriever $metadataRetriever | ||
38 | * @property NetscapeBookmarkUtils $netscapeBookmarkUtils | 42 | * @property NetscapeBookmarkUtils $netscapeBookmarkUtils |
39 | * @property callable $notFoundHandler Overrides default Slim exception display | 43 | * @property callable $notFoundHandler Overrides default Slim exception display |
40 | * @property PageBuilder $pageBuilder | 44 | * @property PageBuilder $pageBuilder |
diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php index 2aa25e5c..c1a9ffbe 100644 --- a/application/exceptions/IOException.php +++ b/application/exceptions/IOException.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Exceptions; | 3 | namespace Shaarli\Exceptions; |
3 | 4 | ||
4 | use Exception; | 5 | use Exception; |
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index f6def630..ed62af26 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Feed; | 3 | namespace Shaarli\Feed; |
3 | 4 | ||
4 | use DateTime; | 5 | use DateTime; |
@@ -102,19 +103,19 @@ class FeedBuilder | |||
102 | } | 103 | } |
103 | 104 | ||
104 | // Optionally filter the results: | 105 | // Optionally filter the results: |
105 | $linksToDisplay = $this->linkDB->search($userInput, null, false, false, true); | 106 | $linksToDisplay = $this->linkDB->search($userInput ?? [], null, false, false, true); |
106 | 107 | ||
107 | $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput); | 108 | $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput); |
108 | 109 | ||
109 | // Can't use array_keys() because $link is a LinkDB instance and not a real array. | 110 | // Can't use array_keys() because $link is a LinkDB instance and not a real array. |
110 | $keys = array(); | 111 | $keys = []; |
111 | foreach ($linksToDisplay as $key => $value) { | 112 | foreach ($linksToDisplay as $key => $value) { |
112 | $keys[] = $key; | 113 | $keys[] = $key; |
113 | } | 114 | } |
114 | 115 | ||
115 | $pageaddr = escape(index_url($this->serverInfo)); | 116 | $pageaddr = escape(index_url($this->serverInfo)); |
116 | $this->formatter->addContextData('index_url', $pageaddr); | 117 | $this->formatter->addContextData('index_url', $pageaddr); |
117 | $linkDisplayed = array(); | 118 | $linkDisplayed = []; |
118 | for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { | 119 | for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { |
119 | $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr); | 120 | $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr); |
120 | } | 121 | } |
@@ -176,9 +177,9 @@ class FeedBuilder | |||
176 | $data = $this->formatter->format($link); | 177 | $data = $this->formatter->format($link); |
177 | $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl']; | 178 | $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl']; |
178 | if ($this->usePermalinks === true) { | 179 | if ($this->usePermalinks === true) { |
179 | $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>'; | 180 | $permalink = '<a href="' . $data['url'] . '" title="' . t('Direct link') . '">' . t('Direct link') . '</a>'; |
180 | } else { | 181 | } else { |
181 | $permalink = '<a href="'. $data['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>'; | 182 | $permalink = '<a href="' . $data['guid'] . '" title="' . t('Permalink') . '">' . t('Permalink') . '</a>'; |
182 | } | 183 | } |
183 | $data['description'] .= PHP_EOL . PHP_EOL . '<br>— ' . $permalink; | 184 | $data['description'] .= PHP_EOL . PHP_EOL . '<br>— ' . $permalink; |
184 | 185 | ||
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 9d4a0fa0..7e0afafc 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php | |||
@@ -12,10 +12,13 @@ namespace Shaarli\Formatter; | |||
12 | */ | 12 | */ |
13 | class BookmarkDefaultFormatter extends BookmarkFormatter | 13 | class BookmarkDefaultFormatter extends BookmarkFormatter |
14 | { | 14 | { |
15 | protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; | ||
16 | protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; | ||
17 | |||
15 | /** | 18 | /** |
16 | * @inheritdoc | 19 | * @inheritdoc |
17 | */ | 20 | */ |
18 | public function formatTitle($bookmark) | 21 | protected function formatTitle($bookmark) |
19 | { | 22 | { |
20 | return escape($bookmark->getTitle()); | 23 | return escape($bookmark->getTitle()); |
21 | } | 24 | } |
@@ -23,10 +26,33 @@ class BookmarkDefaultFormatter extends BookmarkFormatter | |||
23 | /** | 26 | /** |
24 | * @inheritdoc | 27 | * @inheritdoc |
25 | */ | 28 | */ |
26 | public function formatDescription($bookmark) | 29 | protected function formatTitleHtml($bookmark) |
30 | { | ||
31 | $title = $this->tokenizeSearchHighlightField( | ||
32 | $bookmark->getTitle() ?? '', | ||
33 | $bookmark->getAdditionalContentEntry('search_highlight')['title'] ?? [] | ||
34 | ); | ||
35 | |||
36 | return $this->replaceTokens(escape($title)); | ||
37 | } | ||
38 | |||
39 | /** | ||
40 | * @inheritdoc | ||
41 | */ | ||
42 | protected function formatDescription($bookmark) | ||
27 | { | 43 | { |
28 | $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; | 44 | $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; |
29 | return format_description(escape($bookmark->getDescription()), $indexUrl); | 45 | $description = $this->tokenizeSearchHighlightField( |
46 | $bookmark->getDescription() ?? '', | ||
47 | $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] | ||
48 | ); | ||
49 | $description = format_description( | ||
50 | escape($description), | ||
51 | $indexUrl, | ||
52 | $this->conf->get('formatter_settings.autolink', true) | ||
53 | ); | ||
54 | |||
55 | return $this->replaceTokens($description); | ||
30 | } | 56 | } |
31 | 57 | ||
32 | /** | 58 | /** |
@@ -40,15 +66,36 @@ class BookmarkDefaultFormatter extends BookmarkFormatter | |||
40 | /** | 66 | /** |
41 | * @inheritdoc | 67 | * @inheritdoc |
42 | */ | 68 | */ |
43 | public function formatTagString($bookmark) | 69 | protected function formatTagListHtml($bookmark) |
44 | { | 70 | { |
45 | return implode(' ', $this->formatTagList($bookmark)); | 71 | $tagsSeparator = $this->conf->get('general.tags_separator', ' '); |
72 | if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) { | ||
73 | return $this->formatTagList($bookmark); | ||
74 | } | ||
75 | |||
76 | $tags = $this->tokenizeSearchHighlightField( | ||
77 | $bookmark->getTagsString($tagsSeparator), | ||
78 | $bookmark->getAdditionalContentEntry('search_highlight')['tags'] | ||
79 | ); | ||
80 | $tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator)); | ||
81 | $tags = escape($tags); | ||
82 | $tags = $this->replaceTokensArray($tags); | ||
83 | |||
84 | return $tags; | ||
46 | } | 85 | } |
47 | 86 | ||
48 | /** | 87 | /** |
49 | * @inheritdoc | 88 | * @inheritdoc |
50 | */ | 89 | */ |
51 | public function formatUrl($bookmark) | 90 | protected function formatTagString($bookmark) |
91 | { | ||
92 | return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark)); | ||
93 | } | ||
94 | |||
95 | /** | ||
96 | * @inheritdoc | ||
97 | */ | ||
98 | protected function formatUrl($bookmark) | ||
52 | { | 99 | { |
53 | if ($bookmark->isNote() && isset($this->contextData['index_url'])) { | 100 | if ($bookmark->isNote() && isset($this->contextData['index_url'])) { |
54 | return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); | 101 | return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); |
@@ -80,8 +127,89 @@ class BookmarkDefaultFormatter extends BookmarkFormatter | |||
80 | /** | 127 | /** |
81 | * @inheritdoc | 128 | * @inheritdoc |
82 | */ | 129 | */ |
130 | protected function formatUrlHtml($bookmark) | ||
131 | { | ||
132 | $url = $this->tokenizeSearchHighlightField( | ||
133 | $bookmark->getUrl() ?? '', | ||
134 | $bookmark->getAdditionalContentEntry('search_highlight')['url'] ?? [] | ||
135 | ); | ||
136 | |||
137 | return $this->replaceTokens(escape($url)); | ||
138 | } | ||
139 | |||
140 | /** | ||
141 | * @inheritdoc | ||
142 | */ | ||
83 | protected function formatThumbnail($bookmark) | 143 | protected function formatThumbnail($bookmark) |
84 | { | 144 | { |
85 | return escape($bookmark->getThumbnail()); | 145 | return escape($bookmark->getThumbnail()); |
86 | } | 146 | } |
147 | |||
148 | /** | ||
149 | * Insert search highlight token in provided field content based on a list of search result positions | ||
150 | * | ||
151 | * @param string $fieldContent | ||
152 | * @param array|null $positions List of of search results with 'start' and 'end' positions. | ||
153 | * | ||
154 | * @return string Updated $fieldContent. | ||
155 | */ | ||
156 | protected function tokenizeSearchHighlightField(string $fieldContent, ?array $positions): string | ||
157 | { | ||
158 | if (empty($positions)) { | ||
159 | return $fieldContent; | ||
160 | } | ||
161 | |||
162 | $insertedTokens = 0; | ||
163 | $tokenLength = strlen(static::SEARCH_HIGHLIGHT_OPEN); | ||
164 | foreach ($positions as $position) { | ||
165 | $position = [ | ||
166 | 'start' => $position['start'] + ($insertedTokens * $tokenLength), | ||
167 | 'end' => $position['end'] + ($insertedTokens * $tokenLength), | ||
168 | ]; | ||
169 | |||
170 | $content = mb_substr($fieldContent, 0, $position['start']); | ||
171 | $content .= static::SEARCH_HIGHLIGHT_OPEN; | ||
172 | $content .= mb_substr($fieldContent, $position['start'], $position['end'] - $position['start']); | ||
173 | $content .= static::SEARCH_HIGHLIGHT_CLOSE; | ||
174 | $content .= mb_substr($fieldContent, $position['end']); | ||
175 | |||
176 | $fieldContent = $content; | ||
177 | |||
178 | $insertedTokens += 2; | ||
179 | } | ||
180 | |||
181 | return $fieldContent; | ||
182 | } | ||
183 | |||
184 | /** | ||
185 | * Replace search highlight tokens with HTML highlighted span. | ||
186 | * | ||
187 | * @param string $fieldContent | ||
188 | * | ||
189 | * @return string updated content. | ||
190 | */ | ||
191 | protected function replaceTokens(string $fieldContent): string | ||
192 | { | ||
193 | return str_replace( | ||
194 | [static::SEARCH_HIGHLIGHT_OPEN, static::SEARCH_HIGHLIGHT_CLOSE], | ||
195 | ['<span class="search-highlight">', '</span>'], | ||
196 | $fieldContent | ||
197 | ); | ||
198 | } | ||
199 | |||
200 | /** | ||
201 | * Apply replaceTokens to an array of content strings. | ||
202 | * | ||
203 | * @param string[] $fieldContents | ||
204 | * | ||
205 | * @return array | ||
206 | */ | ||
207 | protected function replaceTokensArray(array $fieldContents): array | ||
208 | { | ||
209 | foreach ($fieldContents as &$entry) { | ||
210 | $entry = $this->replaceTokens($entry); | ||
211 | } | ||
212 | |||
213 | return $fieldContents; | ||
214 | } | ||
87 | } | 215 | } |
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php index 0042dafe..124ce78b 100644 --- a/application/formatter/BookmarkFormatter.php +++ b/application/formatter/BookmarkFormatter.php | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Formatter; | 3 | namespace Shaarli\Formatter; |
4 | 4 | ||
5 | use DateTime; | 5 | use DateTimeInterface; |
6 | use Shaarli\Bookmark\Bookmark; | 6 | use Shaarli\Bookmark\Bookmark; |
7 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
8 | 8 | ||
@@ -11,6 +11,29 @@ use Shaarli\Config\ConfigManager; | |||
11 | * | 11 | * |
12 | * Abstract class processing all bookmark attributes through methods designed to be overridden. | 12 | * Abstract class processing all bookmark attributes through methods designed to be overridden. |
13 | * | 13 | * |
14 | * List of available formatted fields: | ||
15 | * - id ID | ||
16 | * - shorturl Unique identifier, used in permalinks | ||
17 | * - url URL, can be altered in some way, e.g. passing through an HTTP reverse proxy | ||
18 | * - real_url (legacy) same as `url` | ||
19 | * - url_html URL to be displayed in HTML content (it can contain HTML tags) | ||
20 | * - title Title | ||
21 | * - title_html Title to be displayed in HTML content (it can contain HTML tags) | ||
22 | * - description Description content. It most likely contains HTML tags | ||
23 | * - thumbnail Thumbnail: path to local cache file, false if there is none, null if hasn't been retrieved | ||
24 | * - taglist List of tags (array) | ||
25 | * - taglist_urlencoded List of tags (array) URL encoded: it must be used to create a link to a URL containing a tag | ||
26 | * - taglist_html List of tags (array) to be displayed in HTML content (it can contain HTML tags) | ||
27 | * - tags Tags separated by a single whitespace | ||
28 | * - tags_urlencoded Tags separated by a single whitespace, URL encoded: must be used to create a link | ||
29 | * - sticky Is sticky (bool) | ||
30 | * - private Is private (bool) | ||
31 | * - class Additional CSS class | ||
32 | * - created Creation DateTime | ||
33 | * - updated Last edit DateTime | ||
34 | * - timestamp Creation timestamp | ||
35 | * - updated_timestamp Last edit timestamp | ||
36 | * | ||
14 | * @package Shaarli\Formatter | 37 | * @package Shaarli\Formatter |
15 | */ | 38 | */ |
16 | abstract class BookmarkFormatter | 39 | abstract class BookmarkFormatter |
@@ -55,13 +78,16 @@ abstract class BookmarkFormatter | |||
55 | $out['shorturl'] = $this->formatShortUrl($bookmark); | 78 | $out['shorturl'] = $this->formatShortUrl($bookmark); |
56 | $out['url'] = $this->formatUrl($bookmark); | 79 | $out['url'] = $this->formatUrl($bookmark); |
57 | $out['real_url'] = $this->formatRealUrl($bookmark); | 80 | $out['real_url'] = $this->formatRealUrl($bookmark); |
81 | $out['url_html'] = $this->formatUrlHtml($bookmark); | ||
58 | $out['title'] = $this->formatTitle($bookmark); | 82 | $out['title'] = $this->formatTitle($bookmark); |
83 | $out['title_html'] = $this->formatTitleHtml($bookmark); | ||
59 | $out['description'] = $this->formatDescription($bookmark); | 84 | $out['description'] = $this->formatDescription($bookmark); |
60 | $out['thumbnail'] = $this->formatThumbnail($bookmark); | 85 | $out['thumbnail'] = $this->formatThumbnail($bookmark); |
61 | $out['urlencoded_taglist'] = $this->formatUrlEncodedTagList($bookmark); | ||
62 | $out['taglist'] = $this->formatTagList($bookmark); | 86 | $out['taglist'] = $this->formatTagList($bookmark); |
63 | $out['urlencoded_tags'] = $this->formatUrlEncodedTagString($bookmark); | 87 | $out['taglist_urlencoded'] = $this->formatTagListUrlEncoded($bookmark); |
88 | $out['taglist_html'] = $this->formatTagListHtml($bookmark); | ||
64 | $out['tags'] = $this->formatTagString($bookmark); | 89 | $out['tags'] = $this->formatTagString($bookmark); |
90 | $out['tags_urlencoded'] = $this->formatTagStringUrlEncoded($bookmark); | ||
65 | $out['sticky'] = $bookmark->isSticky(); | 91 | $out['sticky'] = $bookmark->isSticky(); |
66 | $out['private'] = $bookmark->isPrivate(); | 92 | $out['private'] = $bookmark->isPrivate(); |
67 | $out['class'] = $this->formatClass($bookmark); | 93 | $out['class'] = $this->formatClass($bookmark); |
@@ -69,6 +95,7 @@ abstract class BookmarkFormatter | |||
69 | $out['updated'] = $this->formatUpdated($bookmark); | 95 | $out['updated'] = $this->formatUpdated($bookmark); |
70 | $out['timestamp'] = $this->formatCreatedTimestamp($bookmark); | 96 | $out['timestamp'] = $this->formatCreatedTimestamp($bookmark); |
71 | $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark); | 97 | $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark); |
98 | |||
72 | return $out; | 99 | return $out; |
73 | } | 100 | } |
74 | 101 | ||
@@ -136,6 +163,18 @@ abstract class BookmarkFormatter | |||
136 | } | 163 | } |
137 | 164 | ||
138 | /** | 165 | /** |
166 | * Format Url Html: to be displayed in HTML content, it can contains HTML tags. | ||
167 | * | ||
168 | * @param Bookmark $bookmark instance | ||
169 | * | ||
170 | * @return string formatted Url HTML | ||
171 | */ | ||
172 | protected function formatUrlHtml($bookmark) | ||
173 | { | ||
174 | return $this->formatUrl($bookmark); | ||
175 | } | ||
176 | |||
177 | /** | ||
139 | * Format Title | 178 | * Format Title |
140 | * | 179 | * |
141 | * @param Bookmark $bookmark instance | 180 | * @param Bookmark $bookmark instance |
@@ -148,6 +187,18 @@ abstract class BookmarkFormatter | |||
148 | } | 187 | } |
149 | 188 | ||
150 | /** | 189 | /** |
190 | * Format Title HTML: to be displayed in HTML content, it can contains HTML tags. | ||
191 | * | ||
192 | * @param Bookmark $bookmark instance | ||
193 | * | ||
194 | * @return string formatted Title | ||
195 | */ | ||
196 | protected function formatTitleHtml($bookmark) | ||
197 | { | ||
198 | return $bookmark->getTitle(); | ||
199 | } | ||
200 | |||
201 | /** | ||
151 | * Format Description | 202 | * Format Description |
152 | * | 203 | * |
153 | * @param Bookmark $bookmark instance | 204 | * @param Bookmark $bookmark instance |
@@ -190,12 +241,24 @@ abstract class BookmarkFormatter | |||
190 | * | 241 | * |
191 | * @return array formatted Tags | 242 | * @return array formatted Tags |
192 | */ | 243 | */ |
193 | protected function formatUrlEncodedTagList($bookmark) | 244 | protected function formatTagListUrlEncoded($bookmark) |
194 | { | 245 | { |
195 | return array_map('urlencode', $this->filterTagList($bookmark->getTags())); | 246 | return array_map('urlencode', $this->filterTagList($bookmark->getTags())); |
196 | } | 247 | } |
197 | 248 | ||
198 | /** | 249 | /** |
250 | * Format Tags HTML: to be displayed in HTML content, it can contains HTML tags. | ||
251 | * | ||
252 | * @param Bookmark $bookmark instance | ||
253 | * | ||
254 | * @return array formatted Tags | ||
255 | */ | ||
256 | protected function formatTagListHtml($bookmark) | ||
257 | { | ||
258 | return $this->formatTagList($bookmark); | ||
259 | } | ||
260 | |||
261 | /** | ||
199 | * Format TagString | 262 | * Format TagString |
200 | * | 263 | * |
201 | * @param Bookmark $bookmark instance | 264 | * @param Bookmark $bookmark instance |
@@ -204,7 +267,7 @@ abstract class BookmarkFormatter | |||
204 | */ | 267 | */ |
205 | protected function formatTagString($bookmark) | 268 | protected function formatTagString($bookmark) |
206 | { | 269 | { |
207 | return implode(' ', $this->formatTagList($bookmark)); | 270 | return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark)); |
208 | } | 271 | } |
209 | 272 | ||
210 | /** | 273 | /** |
@@ -214,9 +277,9 @@ abstract class BookmarkFormatter | |||
214 | * | 277 | * |
215 | * @return string formatted TagString | 278 | * @return string formatted TagString |
216 | */ | 279 | */ |
217 | protected function formatUrlEncodedTagString($bookmark) | 280 | protected function formatTagStringUrlEncoded($bookmark) |
218 | { | 281 | { |
219 | return implode(' ', $this->formatUrlEncodedTagList($bookmark)); | 282 | return implode(' ', $this->formatTagListUrlEncoded($bookmark)); |
220 | } | 283 | } |
221 | 284 | ||
222 | /** | 285 | /** |
@@ -237,7 +300,7 @@ abstract class BookmarkFormatter | |||
237 | * | 300 | * |
238 | * @param Bookmark $bookmark instance | 301 | * @param Bookmark $bookmark instance |
239 | * | 302 | * |
240 | * @return DateTime instance | 303 | * @return DateTimeInterface instance |
241 | */ | 304 | */ |
242 | protected function formatCreated(Bookmark $bookmark) | 305 | protected function formatCreated(Bookmark $bookmark) |
243 | { | 306 | { |
@@ -249,7 +312,7 @@ abstract class BookmarkFormatter | |||
249 | * | 312 | * |
250 | * @param Bookmark $bookmark instance | 313 | * @param Bookmark $bookmark instance |
251 | * | 314 | * |
252 | * @return DateTime instance | 315 | * @return DateTimeInterface instance |
253 | */ | 316 | */ |
254 | protected function formatUpdated(Bookmark $bookmark) | 317 | protected function formatUpdated(Bookmark $bookmark) |
255 | { | 318 | { |
@@ -288,6 +351,7 @@ abstract class BookmarkFormatter | |||
288 | 351 | ||
289 | /** | 352 | /** |
290 | * Format tag list, e.g. remove private tags if the user is not logged in. | 353 | * Format tag list, e.g. remove private tags if the user is not logged in. |
354 | * TODO: this method is called multiple time to format tags, the result should be cached. | ||
291 | * | 355 | * |
292 | * @param array $tags | 356 | * @param array $tags |
293 | * | 357 | * |
diff --git a/application/formatter/BookmarkMarkdownExtraFormatter.php b/application/formatter/BookmarkMarkdownExtraFormatter.php new file mode 100644 index 00000000..0694b23f --- /dev/null +++ b/application/formatter/BookmarkMarkdownExtraFormatter.php | |||
@@ -0,0 +1,24 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Formatter; | ||
4 | |||
5 | use Shaarli\Config\ConfigManager; | ||
6 | |||
7 | /** | ||
8 | * Class BookmarkMarkdownExtraFormatter | ||
9 | * | ||
10 | * Format bookmark description into MarkdownExtra format. | ||
11 | * | ||
12 | * @see https://michelf.ca/projects/php-markdown/extra/ | ||
13 | * | ||
14 | * @package Shaarli\Formatter | ||
15 | */ | ||
16 | class BookmarkMarkdownExtraFormatter extends BookmarkMarkdownFormatter | ||
17 | { | ||
18 | public function __construct(ConfigManager $conf, bool $isLoggedIn) | ||
19 | { | ||
20 | parent::__construct($conf, $isLoggedIn); | ||
21 | |||
22 | $this->parsedown = new \ParsedownExtra(); | ||
23 | } | ||
24 | } | ||
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index 5d244d4c..ee4e8dca 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php | |||
@@ -16,7 +16,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
16 | /** | 16 | /** |
17 | * When this tag is present in a bookmark, its description should not be processed with Markdown | 17 | * When this tag is present in a bookmark, its description should not be processed with Markdown |
18 | */ | 18 | */ |
19 | const NO_MD_TAG = 'nomarkdown'; | 19 | public const NO_MD_TAG = 'nomarkdown'; |
20 | 20 | ||
21 | /** @var \Parsedown instance */ | 21 | /** @var \Parsedown instance */ |
22 | protected $parsedown; | 22 | protected $parsedown; |
@@ -56,7 +56,10 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
56 | return parent::formatDescription($bookmark); | 56 | return parent::formatDescription($bookmark); |
57 | } | 57 | } |
58 | 58 | ||
59 | $processedDescription = $bookmark->getDescription(); | 59 | $processedDescription = $this->tokenizeSearchHighlightField( |
60 | $bookmark->getDescription() ?? '', | ||
61 | $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] | ||
62 | ); | ||
60 | $processedDescription = $this->filterProtocols($processedDescription); | 63 | $processedDescription = $this->filterProtocols($processedDescription); |
61 | $processedDescription = $this->formatHashTags($processedDescription); | 64 | $processedDescription = $this->formatHashTags($processedDescription); |
62 | $processedDescription = $this->reverseEscapedHtml($processedDescription); | 65 | $processedDescription = $this->reverseEscapedHtml($processedDescription); |
@@ -65,9 +68,10 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
65 | ->setBreaksEnabled(true) | 68 | ->setBreaksEnabled(true) |
66 | ->text($processedDescription); | 69 | ->text($processedDescription); |
67 | $processedDescription = $this->sanitizeHtml($processedDescription); | 70 | $processedDescription = $this->sanitizeHtml($processedDescription); |
71 | $processedDescription = $this->replaceTokens($processedDescription); | ||
68 | 72 | ||
69 | if (!empty($processedDescription)) { | 73 | if (!empty($processedDescription)) { |
70 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; | 74 | $processedDescription = '<div class="markdown">' . $processedDescription . '</div>'; |
71 | } | 75 | } |
72 | 76 | ||
73 | return $processedDescription; | 77 | return $processedDescription; |
@@ -106,7 +110,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
106 | function ($match) use ($allowedProtocols, $indexUrl) { | 110 | function ($match) use ($allowedProtocols, $indexUrl) { |
107 | $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; | 111 | $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; |
108 | $link .= whitelist_protocols($match[1], $allowedProtocols); | 112 | $link .= whitelist_protocols($match[1], $allowedProtocols); |
109 | return ']('. $link.')'; | 113 | return '](' . $link . ')'; |
110 | }, | 114 | }, |
111 | $description | 115 | $description |
112 | ); | 116 | ); |
@@ -133,7 +137,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
133 | * \p{Mn} - any non marking space (accents, umlauts, etc) | 137 | * \p{Mn} - any non marking space (accents, umlauts, etc) |
134 | */ | 138 | */ |
135 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; | 139 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; |
136 | $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)'; | 140 | $replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)'; |
137 | 141 | ||
138 | $descriptionLines = explode(PHP_EOL, $description); | 142 | $descriptionLines = explode(PHP_EOL, $description); |
139 | $descriptionOut = ''; | 143 | $descriptionOut = ''; |
@@ -174,17 +178,17 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
174 | */ | 178 | */ |
175 | protected function sanitizeHtml($description) | 179 | protected function sanitizeHtml($description) |
176 | { | 180 | { |
177 | $escapeTags = array( | 181 | $escapeTags = [ |
178 | 'script', | 182 | 'script', |
179 | 'style', | 183 | 'style', |
180 | 'link', | 184 | 'link', |
181 | 'iframe', | 185 | 'iframe', |
182 | 'frameset', | 186 | 'frameset', |
183 | 'frame', | 187 | 'frame', |
184 | ); | 188 | ]; |
185 | foreach ($escapeTags as $tag) { | 189 | foreach ($escapeTags as $tag) { |
186 | $description = preg_replace_callback( | 190 | $description = preg_replace_callback( |
187 | '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is', | 191 | '#<\s*' . $tag . '[^>]*>(.*</\s*' . $tag . '[^>]*>)?#is', |
188 | function ($match) { | 192 | function ($match) { |
189 | return escape($match[0]); | 193 | return escape($match[0]); |
190 | }, | 194 | }, |
diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php index bc372273..4ff07cdf 100644 --- a/application/formatter/BookmarkRawFormatter.php +++ b/application/formatter/BookmarkRawFormatter.php | |||
@@ -10,4 +10,6 @@ namespace Shaarli\Formatter; | |||
10 | * | 10 | * |
11 | * @package Shaarli\Formatter | 11 | * @package Shaarli\Formatter |
12 | */ | 12 | */ |
13 | class BookmarkRawFormatter extends BookmarkFormatter {} | 13 | class BookmarkRawFormatter extends BookmarkFormatter |
14 | { | ||
15 | } | ||
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php index a029579f..bb865aed 100644 --- a/application/formatter/FormatterFactory.php +++ b/application/formatter/FormatterFactory.php | |||
@@ -41,7 +41,7 @@ class FormatterFactory | |||
41 | public function getFormatter(string $type = null): BookmarkFormatter | 41 | public function getFormatter(string $type = null): BookmarkFormatter |
42 | { | 42 | { |
43 | $type = $type ? $type : $this->conf->get('formatter', 'default'); | 43 | $type = $type ? $type : $this->conf->get('formatter', 'default'); |
44 | $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; | 44 | $className = '\\Shaarli\\Formatter\\Bookmark' . ucfirst($type) . 'Formatter'; |
45 | if (!class_exists($className)) { | 45 | if (!class_exists($className)) { |
46 | $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter'; | 46 | $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter'; |
47 | } | 47 | } |
diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index d1aa1399..164217f4 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php | |||
@@ -42,7 +42,8 @@ class ShaarliMiddleware | |||
42 | $this->initBasePath($request); | 42 | $this->initBasePath($request); |
43 | 43 | ||
44 | try { | 44 | try { |
45 | if (!is_file($this->container->conf->getConfigFileExt()) | 45 | if ( |
46 | !is_file($this->container->conf->getConfigFileExt()) | ||
46 | && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true) | 47 | && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true) |
47 | ) { | 48 | ) { |
48 | return $response->withRedirect($this->container->basePath . '/install'); | 49 | return $response->withRedirect($this->container->basePath . '/install'); |
@@ -86,7 +87,8 @@ class ShaarliMiddleware | |||
86 | */ | 87 | */ |
87 | protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool | 88 | protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool |
88 | { | 89 | { |
89 | if (// if the user isn't logged in | 90 | if ( |
91 | // if the user isn't logged in | ||
90 | !$this->container->loginManager->isLoggedIn() | 92 | !$this->container->loginManager->isLoggedIn() |
91 | // and Shaarli doesn't have public content... | 93 | // and Shaarli doesn't have public content... |
92 | && $this->container->conf->get('privacy.hide_public_links') | 94 | && $this->container->conf->get('privacy.hide_public_links') |
diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index e675fcca..dc421661 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php | |||
@@ -30,7 +30,7 @@ class ConfigureController extends ShaarliAdminController | |||
30 | 'theme_available', | 30 | 'theme_available', |
31 | ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl')) | 31 | ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl')) |
32 | ); | 32 | ); |
33 | $this->assignView('formatter_available', ['default', 'markdown']); | 33 | $this->assignView('formatter_available', ['default', 'markdown', 'markdownExtra']); |
34 | list($continents, $cities) = generateTimeZoneData( | 34 | list($continents, $cities) = generateTimeZoneData( |
35 | timezone_identifiers_list(), | 35 | timezone_identifiers_list(), |
36 | $this->container->conf->get('general.timezone') | 36 | $this->container->conf->get('general.timezone') |
@@ -51,7 +51,10 @@ class ConfigureController extends ShaarliAdminController | |||
51 | $this->assignView('languages', Languages::getAvailableLanguages()); | 51 | $this->assignView('languages', Languages::getAvailableLanguages()); |
52 | $this->assignView('gd_enabled', extension_loaded('gd')); | 52 | $this->assignView('gd_enabled', extension_loaded('gd')); |
53 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); | 53 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); |
54 | $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | 54 | $this->assignView( |
55 | 'pagetitle', | ||
56 | t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
57 | ); | ||
55 | 58 | ||
56 | return $response->write($this->render(TemplatePage::CONFIGURE)); | 59 | return $response->write($this->render(TemplatePage::CONFIGURE)); |
57 | } | 60 | } |
@@ -95,12 +98,15 @@ class ConfigureController extends ShaarliAdminController | |||
95 | } | 98 | } |
96 | 99 | ||
97 | $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; | 100 | $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; |
98 | if ($thumbnailsMode !== Thumbnailer::MODE_NONE | 101 | if ( |
102 | $thumbnailsMode !== Thumbnailer::MODE_NONE | ||
99 | && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) | 103 | && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) |
100 | ) { | 104 | ) { |
101 | $this->saveWarningMessage( | 105 | $this->saveWarningMessage( |
102 | t('You have enabled or changed thumbnails mode.') . | 106 | t('You have enabled or changed thumbnails mode.') . |
103 | '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>' | 107 | '<a href="' . $this->container->basePath . '/admin/thumbnails">' . |
108 | t('Please synchronize them.') . | ||
109 | '</a>' | ||
104 | ); | 110 | ); |
105 | } | 111 | } |
106 | $this->container->conf->set('thumbnails.mode', $thumbnailsMode); | 112 | $this->container->conf->set('thumbnails.mode', $thumbnailsMode); |
diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php index 2be957fa..f01d7e9b 100644 --- a/application/front/controller/admin/ExportController.php +++ b/application/front/controller/admin/ExportController.php | |||
@@ -23,7 +23,7 @@ class ExportController extends ShaarliAdminController | |||
23 | */ | 23 | */ |
24 | public function index(Request $request, Response $response): Response | 24 | public function index(Request $request, Response $response): Response |
25 | { | 25 | { |
26 | $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | 26 | $this->assignView('pagetitle', t('Export') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); |
27 | 27 | ||
28 | return $response->write($this->render(TemplatePage::EXPORT)); | 28 | return $response->write($this->render(TemplatePage::EXPORT)); |
29 | } | 29 | } |
@@ -68,7 +68,7 @@ class ExportController extends ShaarliAdminController | |||
68 | $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); | 68 | $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); |
69 | $response = $response->withHeader( | 69 | $response = $response->withHeader( |
70 | 'Content-disposition', | 70 | 'Content-disposition', |
71 | 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' | 71 | 'attachment; filename=bookmarks_' . $selection . '_' . $now->format(Bookmark::LINK_DATE_FORMAT) . '.html' |
72 | ); | 72 | ); |
73 | 73 | ||
74 | $this->assignView('date', $now->format(DateTime::RFC822)); | 74 | $this->assignView('date', $now->format(DateTime::RFC822)); |
diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php index 758d5ef9..c2ad6a09 100644 --- a/application/front/controller/admin/ImportController.php +++ b/application/front/controller/admin/ImportController.php | |||
@@ -38,7 +38,7 @@ class ImportController extends ShaarliAdminController | |||
38 | true | 38 | true |
39 | ) | 39 | ) |
40 | ); | 40 | ); |
41 | $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | 41 | $this->assignView('pagetitle', t('Import') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); |
42 | 42 | ||
43 | return $response->write($this->render(TemplatePage::IMPORT)); | 43 | return $response->write($this->render(TemplatePage::IMPORT)); |
44 | } | 44 | } |
@@ -64,7 +64,7 @@ class ImportController extends ShaarliAdminController | |||
64 | $msg = sprintf( | 64 | $msg = sprintf( |
65 | t( | 65 | t( |
66 | 'The file you are trying to upload is probably bigger than what this webserver can accept' | 66 | 'The file you are trying to upload is probably bigger than what this webserver can accept' |
67 | .' (%s). Please upload in smaller chunks.' | 67 | . ' (%s). Please upload in smaller chunks.' |
68 | ), | 68 | ), |
69 | get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) | 69 | get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) |
70 | ); | 70 | ); |
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php deleted file mode 100644 index bb083486..00000000 --- a/application/front/controller/admin/ManageShaareController.php +++ /dev/null | |||
@@ -1,371 +0,0 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Bookmark\Bookmark; | ||
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
9 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | ||
10 | use Shaarli\Render\TemplatePage; | ||
11 | use Shaarli\Thumbnailer; | ||
12 | use Slim\Http\Request; | ||
13 | use Slim\Http\Response; | ||
14 | |||
15 | /** | ||
16 | * Class PostBookmarkController | ||
17 | * | ||
18 | * Slim controller used to handle Shaarli create or edit bookmarks. | ||
19 | */ | ||
20 | class ManageShaareController extends ShaarliAdminController | ||
21 | { | ||
22 | /** | ||
23 | * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL | ||
24 | */ | ||
25 | public function addShaare(Request $request, Response $response): Response | ||
26 | { | ||
27 | $this->assignView( | ||
28 | 'pagetitle', | ||
29 | t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
30 | ); | ||
31 | |||
32 | return $response->write($this->render(TemplatePage::ADDLINK)); | ||
33 | } | ||
34 | |||
35 | /** | ||
36 | * GET /admin/shaare - Displays the bookmark form for creation. | ||
37 | * Note that if the URL is found in existing bookmarks, then it will be in edit mode. | ||
38 | */ | ||
39 | public function displayCreateForm(Request $request, Response $response): Response | ||
40 | { | ||
41 | $url = cleanup_url($request->getParam('post')); | ||
42 | |||
43 | $linkIsNew = false; | ||
44 | // Check if URL is not already in database (in this case, we will edit the existing link) | ||
45 | $bookmark = $this->container->bookmarkService->findByUrl($url); | ||
46 | if (null === $bookmark) { | ||
47 | $linkIsNew = true; | ||
48 | // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). | ||
49 | $title = $request->getParam('title'); | ||
50 | $description = $request->getParam('description'); | ||
51 | $tags = $request->getParam('tags'); | ||
52 | $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); | ||
53 | |||
54 | // If this is an HTTP(S) link, we try go get the page to extract | ||
55 | // the title (otherwise we will to straight to the edit form.) | ||
56 | if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { | ||
57 | $retrieveDescription = $this->container->conf->get('general.retrieve_description'); | ||
58 | // Short timeout to keep the application responsive | ||
59 | // The callback will fill $charset and $title with data from the downloaded page. | ||
60 | $this->container->httpAccess->getHttpResponse( | ||
61 | $url, | ||
62 | $this->container->conf->get('general.download_timeout', 30), | ||
63 | $this->container->conf->get('general.download_max_size', 4194304), | ||
64 | $this->container->httpAccess->getCurlDownloadCallback( | ||
65 | $charset, | ||
66 | $title, | ||
67 | $description, | ||
68 | $tags, | ||
69 | $retrieveDescription | ||
70 | ) | ||
71 | ); | ||
72 | if (! empty($title) && strtolower($charset) !== 'utf-8' && mb_check_encoding($charset)) { | ||
73 | $title = mb_convert_encoding($title, 'utf-8', $charset); | ||
74 | } | ||
75 | } | ||
76 | |||
77 | if (empty($url) && empty($title)) { | ||
78 | $title = $this->container->conf->get('general.default_note_title', t('Note: ')); | ||
79 | } | ||
80 | |||
81 | $link = [ | ||
82 | 'title' => $title, | ||
83 | 'url' => $url ?? '', | ||
84 | 'description' => $description ?? '', | ||
85 | 'tags' => $tags ?? '', | ||
86 | 'private' => $private, | ||
87 | ]; | ||
88 | } else { | ||
89 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
90 | $link = $formatter->format($bookmark); | ||
91 | } | ||
92 | |||
93 | return $this->displayForm($link, $linkIsNew, $request, $response); | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. | ||
98 | */ | ||
99 | public function displayEditForm(Request $request, Response $response, array $args): Response | ||
100 | { | ||
101 | $id = $args['id'] ?? ''; | ||
102 | try { | ||
103 | if (false === ctype_digit($id)) { | ||
104 | throw new BookmarkNotFoundException(); | ||
105 | } | ||
106 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
107 | } catch (BookmarkNotFoundException $e) { | ||
108 | $this->saveErrorMessage(sprintf( | ||
109 | t('Bookmark with identifier %s could not be found.'), | ||
110 | $id | ||
111 | )); | ||
112 | |||
113 | return $this->redirect($response, '/'); | ||
114 | } | ||
115 | |||
116 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
117 | $link = $formatter->format($bookmark); | ||
118 | |||
119 | return $this->displayForm($link, false, $request, $response); | ||
120 | } | ||
121 | |||
122 | /** | ||
123 | * POST /admin/shaare | ||
124 | */ | ||
125 | public function save(Request $request, Response $response): Response | ||
126 | { | ||
127 | $this->checkToken($request); | ||
128 | |||
129 | // lf_id should only be present if the link exists. | ||
130 | $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; | ||
131 | if (null !== $id && true === $this->container->bookmarkService->exists($id)) { | ||
132 | // Edit | ||
133 | $bookmark = $this->container->bookmarkService->get($id); | ||
134 | } else { | ||
135 | // New link | ||
136 | $bookmark = new Bookmark(); | ||
137 | } | ||
138 | |||
139 | $bookmark->setTitle($request->getParam('lf_title')); | ||
140 | $bookmark->setDescription($request->getParam('lf_description')); | ||
141 | $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); | ||
142 | $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); | ||
143 | $bookmark->setTagsString($request->getParam('lf_tags')); | ||
144 | |||
145 | if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
146 | && false === $bookmark->isNote() | ||
147 | ) { | ||
148 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
149 | } | ||
150 | $this->container->bookmarkService->addOrSet($bookmark, false); | ||
151 | |||
152 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
153 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
154 | $data = $formatter->format($bookmark); | ||
155 | $this->executePageHooks('save_link', $data); | ||
156 | |||
157 | $bookmark->fromArray($data); | ||
158 | $this->container->bookmarkService->set($bookmark); | ||
159 | |||
160 | // If we are called from the bookmarklet, we must close the popup: | ||
161 | if ($request->getParam('source') === 'bookmarklet') { | ||
162 | return $response->write('<script>self.close();</script>'); | ||
163 | } | ||
164 | |||
165 | if (!empty($request->getParam('returnurl'))) { | ||
166 | $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); | ||
167 | } | ||
168 | |||
169 | return $this->redirectFromReferer( | ||
170 | $request, | ||
171 | $response, | ||
172 | ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], | ||
173 | $bookmark->getShortUrl() | ||
174 | ); | ||
175 | } | ||
176 | |||
177 | /** | ||
178 | * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). | ||
179 | */ | ||
180 | public function deleteBookmark(Request $request, Response $response): Response | ||
181 | { | ||
182 | $this->checkToken($request); | ||
183 | |||
184 | $ids = escape(trim($request->getParam('id') ?? '')); | ||
185 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
186 | // multiple, space-separated ids provided | ||
187 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
188 | } else { | ||
189 | $ids = [$ids]; | ||
190 | } | ||
191 | |||
192 | // assert at least one id is given | ||
193 | if (0 === count($ids)) { | ||
194 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
195 | |||
196 | return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); | ||
197 | } | ||
198 | |||
199 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
200 | $count = 0; | ||
201 | foreach ($ids as $id) { | ||
202 | try { | ||
203 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
204 | } catch (BookmarkNotFoundException $e) { | ||
205 | $this->saveErrorMessage(sprintf( | ||
206 | t('Bookmark with identifier %s could not be found.'), | ||
207 | $id | ||
208 | )); | ||
209 | |||
210 | continue; | ||
211 | } | ||
212 | |||
213 | $data = $formatter->format($bookmark); | ||
214 | $this->executePageHooks('delete_link', $data); | ||
215 | $this->container->bookmarkService->remove($bookmark, false); | ||
216 | ++ $count; | ||
217 | } | ||
218 | |||
219 | if ($count > 0) { | ||
220 | $this->container->bookmarkService->save(); | ||
221 | } | ||
222 | |||
223 | // If we are called from the bookmarklet, we must close the popup: | ||
224 | if ($request->getParam('source') === 'bookmarklet') { | ||
225 | return $response->write('<script>self.close();</script>'); | ||
226 | } | ||
227 | |||
228 | // Don't redirect to where we were previously because the datastore has changed. | ||
229 | return $this->redirect($response, '/'); | ||
230 | } | ||
231 | |||
232 | /** | ||
233 | * GET /admin/shaare/visibility | ||
234 | * | ||
235 | * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). | ||
236 | */ | ||
237 | public function changeVisibility(Request $request, Response $response): Response | ||
238 | { | ||
239 | $this->checkToken($request); | ||
240 | |||
241 | $ids = trim(escape($request->getParam('id') ?? '')); | ||
242 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
243 | // multiple, space-separated ids provided | ||
244 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
245 | } else { | ||
246 | // only a single id provided | ||
247 | $ids = [$ids]; | ||
248 | } | ||
249 | |||
250 | // assert at least one id is given | ||
251 | if (0 === count($ids)) { | ||
252 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
253 | |||
254 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
255 | } | ||
256 | |||
257 | // assert that the visibility is valid | ||
258 | $visibility = $request->getParam('newVisibility'); | ||
259 | if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { | ||
260 | $this->saveErrorMessage(t('Invalid visibility provided.')); | ||
261 | |||
262 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
263 | } else { | ||
264 | $isPrivate = $visibility === 'private'; | ||
265 | } | ||
266 | |||
267 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
268 | $count = 0; | ||
269 | |||
270 | foreach ($ids as $id) { | ||
271 | try { | ||
272 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
273 | } catch (BookmarkNotFoundException $e) { | ||
274 | $this->saveErrorMessage(sprintf( | ||
275 | t('Bookmark with identifier %s could not be found.'), | ||
276 | $id | ||
277 | )); | ||
278 | |||
279 | continue; | ||
280 | } | ||
281 | |||
282 | $bookmark->setPrivate($isPrivate); | ||
283 | |||
284 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
285 | $data = $formatter->format($bookmark); | ||
286 | $this->executePageHooks('save_link', $data); | ||
287 | $bookmark->fromArray($data); | ||
288 | |||
289 | $this->container->bookmarkService->set($bookmark, false); | ||
290 | ++$count; | ||
291 | } | ||
292 | |||
293 | if ($count > 0) { | ||
294 | $this->container->bookmarkService->save(); | ||
295 | } | ||
296 | |||
297 | return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); | ||
298 | } | ||
299 | |||
300 | /** | ||
301 | * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. | ||
302 | */ | ||
303 | public function pinBookmark(Request $request, Response $response, array $args): Response | ||
304 | { | ||
305 | $this->checkToken($request); | ||
306 | |||
307 | $id = $args['id'] ?? ''; | ||
308 | try { | ||
309 | if (false === ctype_digit($id)) { | ||
310 | throw new BookmarkNotFoundException(); | ||
311 | } | ||
312 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
313 | } catch (BookmarkNotFoundException $e) { | ||
314 | $this->saveErrorMessage(sprintf( | ||
315 | t('Bookmark with identifier %s could not be found.'), | ||
316 | $id | ||
317 | )); | ||
318 | |||
319 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
320 | } | ||
321 | |||
322 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
323 | |||
324 | $bookmark->setSticky(!$bookmark->isSticky()); | ||
325 | |||
326 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
327 | $data = $formatter->format($bookmark); | ||
328 | $this->executePageHooks('save_link', $data); | ||
329 | $bookmark->fromArray($data); | ||
330 | |||
331 | $this->container->bookmarkService->set($bookmark); | ||
332 | |||
333 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
334 | } | ||
335 | |||
336 | /** | ||
337 | * Helper function used to display the shaare form whether it's a new or existing bookmark. | ||
338 | * | ||
339 | * @param array $link data used in template, either from parameters or from the data store | ||
340 | */ | ||
341 | protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response | ||
342 | { | ||
343 | $tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
344 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
345 | $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
346 | } | ||
347 | |||
348 | $data = escape([ | ||
349 | 'link' => $link, | ||
350 | 'link_is_new' => $isNew, | ||
351 | 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', | ||
352 | 'source' => $request->getParam('source') ?? '', | ||
353 | 'tags' => $tags, | ||
354 | 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), | ||
355 | ]); | ||
356 | |||
357 | $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); | ||
358 | |||
359 | foreach ($data as $key => $value) { | ||
360 | $this->assignView($key, $value); | ||
361 | } | ||
362 | |||
363 | $editLabel = false === $isNew ? t('Edit') .' ' : ''; | ||
364 | $this->assignView( | ||
365 | 'pagetitle', | ||
366 | $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
367 | ); | ||
368 | |||
369 | return $response->write($this->render(TemplatePage::EDIT_LINK)); | ||
370 | } | ||
371 | } | ||
diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index 2065c3e2..8675a0c5 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php | |||
@@ -24,9 +24,15 @@ class ManageTagController extends ShaarliAdminController | |||
24 | $fromTag = $request->getParam('fromtag') ?? ''; | 24 | $fromTag = $request->getParam('fromtag') ?? ''; |
25 | 25 | ||
26 | $this->assignView('fromtag', escape($fromTag)); | 26 | $this->assignView('fromtag', escape($fromTag)); |
27 | $separator = escape($this->container->conf->get('general.tags_separator', ' ')); | ||
28 | if ($separator === ' ') { | ||
29 | $separator = ' '; | ||
30 | $this->assignView('tags_separator_desc', t('whitespace')); | ||
31 | } | ||
32 | $this->assignView('tags_separator', $separator); | ||
27 | $this->assignView( | 33 | $this->assignView( |
28 | 'pagetitle', | 34 | 'pagetitle', |
29 | t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 35 | t('Manage tags') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
30 | ); | 36 | ); |
31 | 37 | ||
32 | return $response->write($this->render(TemplatePage::CHANGE_TAG)); | 38 | return $response->write($this->render(TemplatePage::CHANGE_TAG)); |
@@ -81,8 +87,35 @@ class ManageTagController extends ShaarliAdminController | |||
81 | 87 | ||
82 | $this->saveSuccessMessage($alert); | 88 | $this->saveSuccessMessage($alert); |
83 | 89 | ||
84 | $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag); | 90 | $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags=' . urlencode($toTag); |
85 | 91 | ||
86 | return $this->redirect($response, $redirect); | 92 | return $this->redirect($response, $redirect); |
87 | } | 93 | } |
94 | |||
95 | /** | ||
96 | * POST /admin/tags/change-separator - Change tag separator | ||
97 | */ | ||
98 | public function changeSeparator(Request $request, Response $response): Response | ||
99 | { | ||
100 | $this->checkToken($request); | ||
101 | |||
102 | $reservedCharacters = ['-', '.', '*']; | ||
103 | $newSeparator = $request->getParam('separator'); | ||
104 | if ($newSeparator === null || mb_strlen($newSeparator) !== 1) { | ||
105 | $this->saveErrorMessage(t('Tags separator must be a single character.')); | ||
106 | } elseif (in_array($newSeparator, $reservedCharacters, true)) { | ||
107 | $reservedCharacters = implode(' ', array_map(function (string $character) { | ||
108 | return '<code>' . $character . '</code>'; | ||
109 | }, $reservedCharacters)); | ||
110 | $this->saveErrorMessage( | ||
111 | t('These characters are reserved and can\'t be used as tags separator: ') . $reservedCharacters | ||
112 | ); | ||
113 | } else { | ||
114 | $this->container->conf->set('general.tags_separator', $newSeparator, true, true); | ||
115 | |||
116 | $this->saveSuccessMessage('Your tags separator setting has been updated!'); | ||
117 | } | ||
118 | |||
119 | return $this->redirect($response, '/admin/tags'); | ||
120 | } | ||
88 | } | 121 | } |
diff --git a/application/front/controller/admin/MetadataController.php b/application/front/controller/admin/MetadataController.php new file mode 100644 index 00000000..ff845944 --- /dev/null +++ b/application/front/controller/admin/MetadataController.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Slim\Http\Request; | ||
8 | use Slim\Http\Response; | ||
9 | |||
10 | /** | ||
11 | * Controller used to retrieve/update bookmark's metadata. | ||
12 | */ | ||
13 | class MetadataController extends ShaarliAdminController | ||
14 | { | ||
15 | /** | ||
16 | * GET /admin/metadata/{url} - Attempt to retrieve the bookmark title from provided URL. | ||
17 | */ | ||
18 | public function ajaxRetrieveTitle(Request $request, Response $response): Response | ||
19 | { | ||
20 | $url = $request->getParam('url'); | ||
21 | |||
22 | // Only try to extract metadata from URL with HTTP(s) scheme | ||
23 | if (!empty($url) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { | ||
24 | return $response->withJson($this->container->metadataRetriever->retrieve($url)); | ||
25 | } | ||
26 | |||
27 | return $response->withJson([]); | ||
28 | } | ||
29 | } | ||
diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php index 5ec0d24b..4aaf1f82 100644 --- a/application/front/controller/admin/PasswordController.php +++ b/application/front/controller/admin/PasswordController.php | |||
@@ -25,7 +25,7 @@ class PasswordController extends ShaarliAdminController | |||
25 | 25 | ||
26 | $this->assignView( | 26 | $this->assignView( |
27 | 'pagetitle', | 27 | 'pagetitle', |
28 | t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 28 | t('Change password') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
29 | ); | 29 | ); |
30 | } | 30 | } |
31 | 31 | ||
@@ -78,7 +78,7 @@ class PasswordController extends ShaarliAdminController | |||
78 | 78 | ||
79 | // Save new password | 79 | // Save new password |
80 | // Salt renders rainbow-tables attacks useless. | 80 | // Salt renders rainbow-tables attacks useless. |
81 | $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); | 81 | $this->container->conf->set('credentials.salt', sha1(uniqid('', true) . '_' . mt_rand())); |
82 | $this->container->conf->set( | 82 | $this->container->conf->set( |
83 | 'credentials.hash', | 83 | 'credentials.hash', |
84 | sha1( | 84 | sha1( |
diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index 8e059681..ae47c1af 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php | |||
@@ -42,7 +42,7 @@ class PluginsController extends ShaarliAdminController | |||
42 | $this->assignView('disabledPlugins', $disabledPlugins); | 42 | $this->assignView('disabledPlugins', $disabledPlugins); |
43 | $this->assignView( | 43 | $this->assignView( |
44 | 'pagetitle', | 44 | 'pagetitle', |
45 | t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 45 | t('Plugin Administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
46 | ); | 46 | ); |
47 | 47 | ||
48 | return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); | 48 | return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); |
@@ -64,7 +64,7 @@ class PluginsController extends ShaarliAdminController | |||
64 | unset($parameters['parameters_form']); | 64 | unset($parameters['parameters_form']); |
65 | unset($parameters['token']); | 65 | unset($parameters['token']); |
66 | foreach ($parameters as $param => $value) { | 66 | foreach ($parameters as $param => $value) { |
67 | $this->container->conf->set('plugins.'. $param, escape($value)); | 67 | $this->container->conf->set('plugins.' . $param, escape($value)); |
68 | } | 68 | } |
69 | } else { | 69 | } else { |
70 | $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); | 70 | $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); |
diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php new file mode 100644 index 00000000..fabeaf2f --- /dev/null +++ b/application/front/controller/admin/ServerController.php | |||
@@ -0,0 +1,96 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Helper\ApplicationUtils; | ||
8 | use Shaarli\Helper\FileUtils; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Slim controller used to handle Server administration page, and actions. | ||
14 | */ | ||
15 | class ServerController extends ShaarliAdminController | ||
16 | { | ||
17 | /** @var string Cache type - main - by default pagecache/ and tmp/ */ | ||
18 | protected const CACHE_MAIN = 'main'; | ||
19 | |||
20 | /** @var string Cache type - thumbnails - by default cache/ */ | ||
21 | protected const CACHE_THUMB = 'thumbnails'; | ||
22 | |||
23 | /** | ||
24 | * GET /admin/server - Display page Server administration | ||
25 | */ | ||
26 | public function index(Request $request, Response $response): Response | ||
27 | { | ||
28 | $releaseUrl = ApplicationUtils::$GITHUB_URL . '/releases/'; | ||
29 | if ($this->container->conf->get('updates.check_updates', true)) { | ||
30 | $latestVersion = 'v' . ApplicationUtils::getVersion( | ||
31 | ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE | ||
32 | ); | ||
33 | $releaseUrl .= 'tag/' . $latestVersion; | ||
34 | } else { | ||
35 | $latestVersion = t('Check disabled'); | ||
36 | } | ||
37 | |||
38 | $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php'); | ||
39 | $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; | ||
40 | $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); | ||
41 | |||
42 | $this->assignView('php_version', PHP_VERSION); | ||
43 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
44 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
45 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
46 | $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); | ||
47 | $this->assignView('release_url', $releaseUrl); | ||
48 | $this->assignView('latest_version', $latestVersion); | ||
49 | $this->assignView('current_version', $currentVersion); | ||
50 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode')); | ||
51 | $this->assignView('index_url', index_url($this->container->environment)); | ||
52 | $this->assignView('client_ip', client_ip_id($this->container->environment)); | ||
53 | $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', [])); | ||
54 | |||
55 | $this->assignView( | ||
56 | 'pagetitle', | ||
57 | t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
58 | ); | ||
59 | |||
60 | return $response->write($this->render('server')); | ||
61 | } | ||
62 | |||
63 | /** | ||
64 | * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails). | ||
65 | */ | ||
66 | public function clearCache(Request $request, Response $response): Response | ||
67 | { | ||
68 | $exclude = ['.htaccess']; | ||
69 | |||
70 | if ($request->getQueryParam('type') === static::CACHE_THUMB) { | ||
71 | $folders = [$this->container->conf->get('resource.thumbnails_cache')]; | ||
72 | |||
73 | $this->saveWarningMessage( | ||
74 | t('Thumbnails cache has been cleared.') . ' ' . | ||
75 | '<a href="' . $this->container->basePath . '/admin/thumbnails">' . | ||
76 | t('Please synchronize them.') . | ||
77 | '</a>' | ||
78 | ); | ||
79 | } else { | ||
80 | $folders = [ | ||
81 | $this->container->conf->get('resource.page_cache'), | ||
82 | $this->container->conf->get('resource.raintpl_tmp'), | ||
83 | ]; | ||
84 | |||
85 | $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!')); | ||
86 | } | ||
87 | |||
88 | // Make sure that we don't delete root cache folder | ||
89 | $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders)))); | ||
90 | foreach ($folders as $folder) { | ||
91 | FileUtils::clearFolder($folder, false, $exclude); | ||
92 | } | ||
93 | |||
94 | return $this->redirect($response, '/admin/server'); | ||
95 | } | ||
96 | } | ||
diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php index d9a7a2e0..0917b6d2 100644 --- a/application/front/controller/admin/SessionFilterController.php +++ b/application/front/controller/admin/SessionFilterController.php | |||
@@ -45,6 +45,4 @@ class SessionFilterController extends ShaarliAdminController | |||
45 | 45 | ||
46 | return $this->redirectFromReferer($request, $response, ['visibility']); | 46 | return $this->redirectFromReferer($request, $response, ['visibility']); |
47 | } | 47 | } |
48 | |||
49 | |||
50 | } | 48 | } |
diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php new file mode 100644 index 00000000..ab8e7f40 --- /dev/null +++ b/application/front/controller/admin/ShaareAddController.php | |||
@@ -0,0 +1,34 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | ||
8 | use Shaarli\Render\TemplatePage; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | class ShaareAddController extends ShaarliAdminController | ||
13 | { | ||
14 | /** | ||
15 | * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL | ||
16 | */ | ||
17 | public function addShaare(Request $request, Response $response): Response | ||
18 | { | ||
19 | $tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
20 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
21 | $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
22 | } | ||
23 | |||
24 | $this->assignView( | ||
25 | 'pagetitle', | ||
26 | t('Shaare a new link') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
27 | ); | ||
28 | $this->assignView('tags', $tags); | ||
29 | $this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false)); | ||
30 | $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true)); | ||
31 | |||
32 | return $response->write($this->render(TemplatePage::ADDLINK)); | ||
33 | } | ||
34 | } | ||
diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php new file mode 100644 index 00000000..35837baa --- /dev/null +++ b/application/front/controller/admin/ShaareManageController.php | |||
@@ -0,0 +1,202 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
8 | use Slim\Http\Request; | ||
9 | use Slim\Http\Response; | ||
10 | |||
11 | /** | ||
12 | * Class PostBookmarkController | ||
13 | * | ||
14 | * Slim controller used to handle Shaarli create or edit bookmarks. | ||
15 | */ | ||
16 | class ShaareManageController extends ShaarliAdminController | ||
17 | { | ||
18 | /** | ||
19 | * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). | ||
20 | */ | ||
21 | public function deleteBookmark(Request $request, Response $response): Response | ||
22 | { | ||
23 | $this->checkToken($request); | ||
24 | |||
25 | $ids = escape(trim($request->getParam('id') ?? '')); | ||
26 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
27 | // multiple, space-separated ids provided | ||
28 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
29 | } else { | ||
30 | $ids = [$ids]; | ||
31 | } | ||
32 | |||
33 | // assert at least one id is given | ||
34 | if (0 === count($ids)) { | ||
35 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
36 | |||
37 | return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); | ||
38 | } | ||
39 | |||
40 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
41 | $count = 0; | ||
42 | foreach ($ids as $id) { | ||
43 | try { | ||
44 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
45 | } catch (BookmarkNotFoundException $e) { | ||
46 | $this->saveErrorMessage(sprintf( | ||
47 | t('Bookmark with identifier %s could not be found.'), | ||
48 | $id | ||
49 | )); | ||
50 | |||
51 | continue; | ||
52 | } | ||
53 | |||
54 | $data = $formatter->format($bookmark); | ||
55 | $this->executePageHooks('delete_link', $data); | ||
56 | $this->container->bookmarkService->remove($bookmark, false); | ||
57 | ++$count; | ||
58 | } | ||
59 | |||
60 | if ($count > 0) { | ||
61 | $this->container->bookmarkService->save(); | ||
62 | } | ||
63 | |||
64 | // If we are called from the bookmarklet, we must close the popup: | ||
65 | if ($request->getParam('source') === 'bookmarklet') { | ||
66 | return $response->write('<script>self.close();</script>'); | ||
67 | } | ||
68 | |||
69 | // Don't redirect to permalink after deletion. | ||
70 | return $this->redirectFromReferer($request, $response, ['shaare/']); | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * GET /admin/shaare/visibility | ||
75 | * | ||
76 | * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). | ||
77 | */ | ||
78 | public function changeVisibility(Request $request, Response $response): Response | ||
79 | { | ||
80 | $this->checkToken($request); | ||
81 | |||
82 | $ids = trim(escape($request->getParam('id') ?? '')); | ||
83 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
84 | // multiple, space-separated ids provided | ||
85 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
86 | } else { | ||
87 | // only a single id provided | ||
88 | $ids = [$ids]; | ||
89 | } | ||
90 | |||
91 | // assert at least one id is given | ||
92 | if (0 === count($ids)) { | ||
93 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
94 | |||
95 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
96 | } | ||
97 | |||
98 | // assert that the visibility is valid | ||
99 | $visibility = $request->getParam('newVisibility'); | ||
100 | if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { | ||
101 | $this->saveErrorMessage(t('Invalid visibility provided.')); | ||
102 | |||
103 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
104 | } else { | ||
105 | $isPrivate = $visibility === 'private'; | ||
106 | } | ||
107 | |||
108 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
109 | $count = 0; | ||
110 | |||
111 | foreach ($ids as $id) { | ||
112 | try { | ||
113 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
114 | } catch (BookmarkNotFoundException $e) { | ||
115 | $this->saveErrorMessage(sprintf( | ||
116 | t('Bookmark with identifier %s could not be found.'), | ||
117 | $id | ||
118 | )); | ||
119 | |||
120 | continue; | ||
121 | } | ||
122 | |||
123 | $bookmark->setPrivate($isPrivate); | ||
124 | |||
125 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
126 | $data = $formatter->format($bookmark); | ||
127 | $this->executePageHooks('save_link', $data); | ||
128 | $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); | ||
129 | |||
130 | $this->container->bookmarkService->set($bookmark, false); | ||
131 | ++$count; | ||
132 | } | ||
133 | |||
134 | if ($count > 0) { | ||
135 | $this->container->bookmarkService->save(); | ||
136 | } | ||
137 | |||
138 | return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); | ||
139 | } | ||
140 | |||
141 | /** | ||
142 | * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. | ||
143 | */ | ||
144 | public function pinBookmark(Request $request, Response $response, array $args): Response | ||
145 | { | ||
146 | $this->checkToken($request); | ||
147 | |||
148 | $id = $args['id'] ?? ''; | ||
149 | try { | ||
150 | if (false === ctype_digit($id)) { | ||
151 | throw new BookmarkNotFoundException(); | ||
152 | } | ||
153 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
154 | } catch (BookmarkNotFoundException $e) { | ||
155 | $this->saveErrorMessage(sprintf( | ||
156 | t('Bookmark with identifier %s could not be found.'), | ||
157 | $id | ||
158 | )); | ||
159 | |||
160 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
161 | } | ||
162 | |||
163 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
164 | |||
165 | $bookmark->setSticky(!$bookmark->isSticky()); | ||
166 | |||
167 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
168 | $data = $formatter->format($bookmark); | ||
169 | $this->executePageHooks('save_link', $data); | ||
170 | $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); | ||
171 | |||
172 | $this->container->bookmarkService->set($bookmark); | ||
173 | |||
174 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
175 | } | ||
176 | |||
177 | /** | ||
178 | * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL. | ||
179 | */ | ||
180 | public function sharePrivate(Request $request, Response $response, array $args): Response | ||
181 | { | ||
182 | $this->checkToken($request); | ||
183 | |||
184 | $hash = $args['hash'] ?? ''; | ||
185 | $bookmark = $this->container->bookmarkService->findByHash($hash); | ||
186 | |||
187 | if ($bookmark->isPrivate() !== true) { | ||
188 | return $this->redirect($response, '/shaare/' . $hash); | ||
189 | } | ||
190 | |||
191 | if (empty($bookmark->getAdditionalContentEntry('private_key'))) { | ||
192 | $privateKey = bin2hex(random_bytes(16)); | ||
193 | $bookmark->addAdditionalContentEntry('private_key', $privateKey); | ||
194 | $this->container->bookmarkService->set($bookmark); | ||
195 | } | ||
196 | |||
197 | return $this->redirect( | ||
198 | $response, | ||
199 | '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key') | ||
200 | ); | ||
201 | } | ||
202 | } | ||
diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php new file mode 100644 index 00000000..4cbfcdc5 --- /dev/null +++ b/application/front/controller/admin/ShaarePublishController.php | |||
@@ -0,0 +1,274 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Bookmark\Bookmark; | ||
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
9 | use Shaarli\Formatter\BookmarkFormatter; | ||
10 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | ||
11 | use Shaarli\Render\TemplatePage; | ||
12 | use Shaarli\Thumbnailer; | ||
13 | use Slim\Http\Request; | ||
14 | use Slim\Http\Response; | ||
15 | |||
16 | class ShaarePublishController extends ShaarliAdminController | ||
17 | { | ||
18 | /** | ||
19 | * @var BookmarkFormatter[] Statically cached instances of formatters | ||
20 | */ | ||
21 | protected $formatters = []; | ||
22 | |||
23 | /** | ||
24 | * @var array Statically cached bookmark's tags counts | ||
25 | */ | ||
26 | protected $tags; | ||
27 | |||
28 | /** | ||
29 | * GET /admin/shaare - Displays the bookmark form for creation. | ||
30 | * Note that if the URL is found in existing bookmarks, then it will be in edit mode. | ||
31 | */ | ||
32 | public function displayCreateForm(Request $request, Response $response): Response | ||
33 | { | ||
34 | $url = cleanup_url($request->getParam('post')); | ||
35 | $link = $this->buildLinkDataFromUrl($request, $url); | ||
36 | |||
37 | return $this->displayForm($link, $link['linkIsNew'], $request, $response); | ||
38 | } | ||
39 | |||
40 | /** | ||
41 | * POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page. | ||
42 | */ | ||
43 | public function displayCreateBatchForms(Request $request, Response $response): Response | ||
44 | { | ||
45 | $urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls'))); | ||
46 | |||
47 | $links = []; | ||
48 | foreach ($urls as $url) { | ||
49 | if (empty($url)) { | ||
50 | continue; | ||
51 | } | ||
52 | $link = $this->buildLinkDataFromUrl($request, $url); | ||
53 | $data = $this->buildFormData($link, $link['linkIsNew'], $request); | ||
54 | $data['token'] = $this->container->sessionManager->generateToken(); | ||
55 | $data['source'] = 'batch'; | ||
56 | |||
57 | $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); | ||
58 | |||
59 | $links[] = $data; | ||
60 | } | ||
61 | |||
62 | $this->assignView('links', $links); | ||
63 | $this->assignView('batch_mode', true); | ||
64 | $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true)); | ||
65 | |||
66 | return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH)); | ||
67 | } | ||
68 | |||
69 | /** | ||
70 | * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. | ||
71 | */ | ||
72 | public function displayEditForm(Request $request, Response $response, array $args): Response | ||
73 | { | ||
74 | $id = $args['id'] ?? ''; | ||
75 | try { | ||
76 | if (false === ctype_digit($id)) { | ||
77 | throw new BookmarkNotFoundException(); | ||
78 | } | ||
79 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
80 | } catch (BookmarkNotFoundException $e) { | ||
81 | $this->saveErrorMessage(sprintf( | ||
82 | t('Bookmark with identifier %s could not be found.'), | ||
83 | $id | ||
84 | )); | ||
85 | |||
86 | return $this->redirect($response, '/'); | ||
87 | } | ||
88 | |||
89 | $formatter = $this->getFormatter('raw'); | ||
90 | $link = $formatter->format($bookmark); | ||
91 | |||
92 | return $this->displayForm($link, false, $request, $response); | ||
93 | } | ||
94 | |||
95 | /** | ||
96 | * POST /admin/shaare | ||
97 | */ | ||
98 | public function save(Request $request, Response $response): Response | ||
99 | { | ||
100 | $this->checkToken($request); | ||
101 | |||
102 | // lf_id should only be present if the link exists. | ||
103 | $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; | ||
104 | if (null !== $id && true === $this->container->bookmarkService->exists($id)) { | ||
105 | // Edit | ||
106 | $bookmark = $this->container->bookmarkService->get($id); | ||
107 | } else { | ||
108 | // New link | ||
109 | $bookmark = new Bookmark(); | ||
110 | } | ||
111 | |||
112 | $bookmark->setTitle($request->getParam('lf_title')); | ||
113 | $bookmark->setDescription($request->getParam('lf_description')); | ||
114 | $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); | ||
115 | $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); | ||
116 | $bookmark->setTagsString( | ||
117 | $request->getParam('lf_tags'), | ||
118 | $this->container->conf->get('general.tags_separator', ' ') | ||
119 | ); | ||
120 | |||
121 | if ( | ||
122 | $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
123 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
124 | && $bookmark->shouldUpdateThumbnail() | ||
125 | ) { | ||
126 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
127 | } | ||
128 | $this->container->bookmarkService->addOrSet($bookmark, false); | ||
129 | |||
130 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
131 | $formatter = $this->getFormatter('raw'); | ||
132 | $data = $formatter->format($bookmark); | ||
133 | $this->executePageHooks('save_link', $data); | ||
134 | |||
135 | $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); | ||
136 | $this->container->bookmarkService->set($bookmark); | ||
137 | |||
138 | // If we are called from the bookmarklet, we must close the popup: | ||
139 | if ($request->getParam('source') === 'bookmarklet') { | ||
140 | return $response->write('<script>self.close();</script>'); | ||
141 | } elseif ($request->getParam('source') === 'batch') { | ||
142 | return $response; | ||
143 | } | ||
144 | |||
145 | if (!empty($request->getParam('returnurl'))) { | ||
146 | $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); | ||
147 | } | ||
148 | |||
149 | return $this->redirectFromReferer( | ||
150 | $request, | ||
151 | $response, | ||
152 | ['/admin/add-shaare', '/admin/shaare'], | ||
153 | ['addlink', 'post', 'edit_link'], | ||
154 | $bookmark->getShortUrl() | ||
155 | ); | ||
156 | } | ||
157 | |||
158 | /** | ||
159 | * Helper function used to display the shaare form whether it's a new or existing bookmark. | ||
160 | * | ||
161 | * @param array $link data used in template, either from parameters or from the data store | ||
162 | */ | ||
163 | protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response | ||
164 | { | ||
165 | $data = $this->buildFormData($link, $isNew, $request); | ||
166 | |||
167 | $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); | ||
168 | |||
169 | foreach ($data as $key => $value) { | ||
170 | $this->assignView($key, $value); | ||
171 | } | ||
172 | |||
173 | $editLabel = false === $isNew ? t('Edit') . ' ' : ''; | ||
174 | $this->assignView( | ||
175 | 'pagetitle', | ||
176 | $editLabel . t('Shaare') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
177 | ); | ||
178 | |||
179 | return $response->write($this->render(TemplatePage::EDIT_LINK)); | ||
180 | } | ||
181 | |||
182 | protected function buildLinkDataFromUrl(Request $request, string $url): array | ||
183 | { | ||
184 | // Check if URL is not already in database (in this case, we will edit the existing link) | ||
185 | $bookmark = $this->container->bookmarkService->findByUrl($url); | ||
186 | if (null === $bookmark) { | ||
187 | // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). | ||
188 | $title = $request->getParam('title'); | ||
189 | $description = $request->getParam('description'); | ||
190 | $tags = $request->getParam('tags'); | ||
191 | if ($request->getParam('private') !== null) { | ||
192 | $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); | ||
193 | } else { | ||
194 | $private = $this->container->conf->get('privacy.default_private_links', false); | ||
195 | } | ||
196 | |||
197 | // If this is an HTTP(S) link, we try go get the page to extract | ||
198 | // the title (otherwise we will to straight to the edit form.) | ||
199 | if ( | ||
200 | true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
201 | && empty($title) | ||
202 | && strpos(get_url_scheme($url) ?: '', 'http') !== false | ||
203 | ) { | ||
204 | $metadata = $this->container->metadataRetriever->retrieve($url); | ||
205 | } | ||
206 | |||
207 | if (empty($url)) { | ||
208 | $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); | ||
209 | } | ||
210 | |||
211 | return [ | ||
212 | 'title' => $title ?? $metadata['title'] ?? '', | ||
213 | 'url' => $url ?? '', | ||
214 | 'description' => $description ?? $metadata['description'] ?? '', | ||
215 | 'tags' => $tags ?? $metadata['tags'] ?? '', | ||
216 | 'private' => $private, | ||
217 | 'linkIsNew' => true, | ||
218 | ]; | ||
219 | } | ||
220 | |||
221 | $formatter = $this->getFormatter('raw'); | ||
222 | $link = $formatter->format($bookmark); | ||
223 | $link['linkIsNew'] = false; | ||
224 | |||
225 | return $link; | ||
226 | } | ||
227 | |||
228 | protected function buildFormData(array $link, bool $isNew, Request $request): array | ||
229 | { | ||
230 | $link['tags'] = strlen($link['tags']) > 0 | ||
231 | ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ') | ||
232 | : $link['tags'] | ||
233 | ; | ||
234 | |||
235 | return escape([ | ||
236 | 'link' => $link, | ||
237 | 'link_is_new' => $isNew, | ||
238 | 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', | ||
239 | 'source' => $request->getParam('source') ?? '', | ||
240 | 'tags' => $this->getTags(), | ||
241 | 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), | ||
242 | 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), | ||
243 | 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), | ||
244 | ]); | ||
245 | } | ||
246 | |||
247 | /** | ||
248 | * Memoize formatterFactory->getFormatter() calls. | ||
249 | */ | ||
250 | protected function getFormatter(string $type): BookmarkFormatter | ||
251 | { | ||
252 | if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) { | ||
253 | $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type); | ||
254 | } | ||
255 | |||
256 | return $this->formatters[$type]; | ||
257 | } | ||
258 | |||
259 | /** | ||
260 | * Memoize bookmarkService->bookmarksCountPerTag() calls. | ||
261 | */ | ||
262 | protected function getTags(): array | ||
263 | { | ||
264 | if ($this->tags === null) { | ||
265 | $this->tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
266 | |||
267 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
268 | $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
269 | } | ||
270 | } | ||
271 | |||
272 | return $this->tags; | ||
273 | } | ||
274 | } | ||
diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php index 81c87ed0..94d97d4b 100644 --- a/application/front/controller/admin/ThumbnailsController.php +++ b/application/front/controller/admin/ThumbnailsController.php | |||
@@ -34,7 +34,7 @@ class ThumbnailsController extends ShaarliAdminController | |||
34 | $this->assignView('ids', $ids); | 34 | $this->assignView('ids', $ids); |
35 | $this->assignView( | 35 | $this->assignView( |
36 | 'pagetitle', | 36 | 'pagetitle', |
37 | t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 37 | t('Thumbnails update') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
38 | ); | 38 | ); |
39 | 39 | ||
40 | return $response->write($this->render(TemplatePage::THUMBNAILS)); | 40 | return $response->write($this->render(TemplatePage::THUMBNAILS)); |
@@ -52,7 +52,7 @@ class ThumbnailsController extends ShaarliAdminController | |||
52 | } | 52 | } |
53 | 53 | ||
54 | try { | 54 | try { |
55 | $bookmark = $this->container->bookmarkService->get($id); | 55 | $bookmark = $this->container->bookmarkService->get((int) $id); |
56 | } catch (BookmarkNotFoundException $e) { | 56 | } catch (BookmarkNotFoundException $e) { |
57 | return $response->withStatus(404); | 57 | return $response->withStatus(404); |
58 | } | 58 | } |
diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index a87f20d2..560e5e3e 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php | |||
@@ -28,7 +28,7 @@ class ToolsController extends ShaarliAdminController | |||
28 | $this->assignView($key, $value); | 28 | $this->assignView($key, $value); |
29 | } | 29 | } |
30 | 30 | ||
31 | $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | 31 | $this->assignView('pagetitle', t('Tools') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); |
32 | 32 | ||
33 | return $response->write($this->render(TemplatePage::TOOLS)); | 33 | return $response->write($this->render(TemplatePage::TOOLS)); |
34 | } | 34 | } |
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index 18368751..fe8231be 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php | |||
@@ -35,7 +35,8 @@ class BookmarkListController extends ShaarliVisitorController | |||
35 | $formatter->addContextData('base_path', $this->container->basePath); | 35 | $formatter->addContextData('base_path', $this->container->basePath); |
36 | 36 | ||
37 | $searchTags = normalize_spaces($request->getParam('searchtags') ?? ''); | 37 | $searchTags = normalize_spaces($request->getParam('searchtags') ?? ''); |
38 | $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));; | 38 | $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? '')); |
39 | ; | ||
39 | 40 | ||
40 | // Filter bookmarks according search parameters. | 41 | // Filter bookmarks according search parameters. |
41 | $visibility = $this->container->sessionManager->getSessionParameter('visibility'); | 42 | $visibility = $this->container->sessionManager->getSessionParameter('visibility'); |
@@ -95,6 +96,10 @@ class BookmarkListController extends ShaarliVisitorController | |||
95 | $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl; | 96 | $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl; |
96 | } | 97 | } |
97 | 98 | ||
99 | $tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); | ||
100 | $searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator)); | ||
101 | $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : ''; | ||
102 | |||
98 | // Fill all template fields. | 103 | // Fill all template fields. |
99 | $data = array_merge( | 104 | $data = array_merge( |
100 | $this->initializeTemplateVars(), | 105 | $this->initializeTemplateVars(), |
@@ -106,7 +111,7 @@ class BookmarkListController extends ShaarliVisitorController | |||
106 | 'result_count' => count($linksToDisplay), | 111 | 'result_count' => count($linksToDisplay), |
107 | 'search_term' => escape($searchTerm), | 112 | 'search_term' => escape($searchTerm), |
108 | 'search_tags' => escape($searchTags), | 113 | 'search_tags' => escape($searchTags), |
109 | 'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)), | 114 | 'search_tags_url' => $searchTagsUrlEncoded, |
110 | 'visibility' => $visibility, | 115 | 'visibility' => $visibility, |
111 | 'links' => $linkDisp, | 116 | 'links' => $linkDisp, |
112 | ] | 117 | ] |
@@ -119,8 +124,9 @@ class BookmarkListController extends ShaarliVisitorController | |||
119 | return '[' . $tag . ']'; | 124 | return '[' . $tag . ']'; |
120 | }; | 125 | }; |
121 | $data['pagetitle'] .= ! empty($searchTags) | 126 | $data['pagetitle'] .= ! empty($searchTags) |
122 | ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' ' | 127 | ? implode(' ', array_map($bracketWrap, tags_str2array($searchTags, $tagsSeparator))) . ' ' |
123 | : ''; | 128 | : '' |
129 | ; | ||
124 | $data['pagetitle'] .= '- '; | 130 | $data['pagetitle'] .= '- '; |
125 | } | 131 | } |
126 | 132 | ||
@@ -137,8 +143,10 @@ class BookmarkListController extends ShaarliVisitorController | |||
137 | */ | 143 | */ |
138 | public function permalink(Request $request, Response $response, array $args): Response | 144 | public function permalink(Request $request, Response $response, array $args): Response |
139 | { | 145 | { |
146 | $privateKey = $request->getParam('key'); | ||
147 | |||
140 | try { | 148 | try { |
141 | $bookmark = $this->container->bookmarkService->findByHash($args['hash']); | 149 | $bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey); |
142 | } catch (BookmarkNotFoundException $e) { | 150 | } catch (BookmarkNotFoundException $e) { |
143 | $this->assignView('error_message', $e->getMessage()); | 151 | $this->assignView('error_message', $e->getMessage()); |
144 | 152 | ||
@@ -153,7 +161,7 @@ class BookmarkListController extends ShaarliVisitorController | |||
153 | $data = array_merge( | 161 | $data = array_merge( |
154 | $this->initializeTemplateVars(), | 162 | $this->initializeTemplateVars(), |
155 | [ | 163 | [ |
156 | 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'), | 164 | 'pagetitle' => $bookmark->getTitle() . ' - ' . $this->container->conf->get('general.title', 'Shaarli'), |
157 | 'links' => [$formatter->format($bookmark)], | 165 | 'links' => [$formatter->format($bookmark)], |
158 | ] | 166 | ] |
159 | ); | 167 | ); |
@@ -169,19 +177,25 @@ class BookmarkListController extends ShaarliVisitorController | |||
169 | */ | 177 | */ |
170 | protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool | 178 | protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool |
171 | { | 179 | { |
172 | // Logged in, thumbnails enabled, not a note, is HTTP | 180 | if (false === $this->container->loginManager->isLoggedIn()) { |
173 | // and (never retrieved yet or no valid cache file) | 181 | return false; |
174 | if ($this->container->loginManager->isLoggedIn() | 182 | } |
175 | && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | 183 | |
176 | && false !== $bookmark->getThumbnail() | 184 | // If thumbnail should be updated, we reset it to null |
177 | && !$bookmark->isNote() | 185 | if ($bookmark->shouldUpdateThumbnail()) { |
178 | && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail())) | 186 | $bookmark->setThumbnail(null); |
179 | && startsWith(strtolower($bookmark->getUrl()), 'http') | 187 | |
180 | ) { | 188 | // Requires an update, not async retrieval, thumbnails enabled |
181 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | 189 | if ( |
182 | $this->container->bookmarkService->set($bookmark, $writeDatastore); | 190 | $bookmark->shouldUpdateThumbnail() |
183 | 191 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | |
184 | return true; | 192 | && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE |
193 | ) { | ||
194 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
195 | $this->container->bookmarkService->set($bookmark, $writeDatastore); | ||
196 | |||
197 | return true; | ||
198 | } | ||
185 | } | 199 | } |
186 | 200 | ||
187 | return false; | 201 | return false; |
@@ -198,6 +212,7 @@ class BookmarkListController extends ShaarliVisitorController | |||
198 | 'page_max' => '', | 212 | 'page_max' => '', |
199 | 'search_tags' => '', | 213 | 'search_tags' => '', |
200 | 'result_count' => '', | 214 | 'result_count' => '', |
215 | 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true) | ||
201 | ]; | 216 | ]; |
202 | } | 217 | } |
203 | 218 | ||
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 07617cf1..846cfe22 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php | |||
@@ -5,8 +5,8 @@ declare(strict_types=1); | |||
5 | namespace Shaarli\Front\Controller\Visitor; | 5 | namespace Shaarli\Front\Controller\Visitor; |
6 | 6 | ||
7 | use DateTime; | 7 | use DateTime; |
8 | use DateTimeImmutable; | ||
9 | use Shaarli\Bookmark\Bookmark; | 8 | use Shaarli\Bookmark\Bookmark; |
9 | use Shaarli\Helper\DailyPageHelper; | ||
10 | use Shaarli\Render\TemplatePage; | 10 | use Shaarli\Render\TemplatePage; |
11 | use Slim\Http\Request; | 11 | use Slim\Http\Request; |
12 | use Slim\Http\Response; | 12 | use Slim\Http\Response; |
@@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController | |||
26 | */ | 26 | */ |
27 | public function index(Request $request, Response $response): Response | 27 | public function index(Request $request, Response $response): Response |
28 | { | 28 | { |
29 | $day = $request->getQueryParam('day') ?? date('Ymd'); | 29 | $type = DailyPageHelper::extractRequestedType($request); |
30 | 30 | $format = DailyPageHelper::getFormatByType($type); | |
31 | $availableDates = $this->container->bookmarkService->days(); | 31 | $latestBookmark = $this->container->bookmarkService->getLatest(); |
32 | $nbAvailableDates = count($availableDates); | 32 | $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark); |
33 | $index = array_search($day, $availableDates); | 33 | $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime); |
34 | 34 | $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime); | |
35 | if ($index === false) { | 35 | $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime); |
36 | // no bookmarks for day, but at least one day with bookmarks | 36 | |
37 | $day = $availableDates[$nbAvailableDates - 1] ?? $day; | 37 | $linksToDisplay = $this->container->bookmarkService->findByDate( |
38 | $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; | 38 | $start, |
39 | } else { | 39 | $end, |
40 | $previousDay = $availableDates[$index - 1] ?? ''; | 40 | $previousDay, |
41 | $nextDay = $availableDates[$index + 1] ?? ''; | 41 | $nextDay |
42 | } | 42 | ); |
43 | |||
44 | if ($day === date('Ymd')) { | ||
45 | $this->assignView('dayDesc', t('Today')); | ||
46 | } elseif ($day === date('Ymd', strtotime('-1 days'))) { | ||
47 | $this->assignView('dayDesc', t('Yesterday')); | ||
48 | } | ||
49 | |||
50 | try { | ||
51 | $linksToDisplay = $this->container->bookmarkService->filterDay($day); | ||
52 | } catch (\Exception $exc) { | ||
53 | $linksToDisplay = []; | ||
54 | } | ||
55 | 43 | ||
56 | $formatter = $this->container->formatterFactory->getFormatter(); | 44 | $formatter = $this->container->formatterFactory->getFormatter(); |
57 | $formatter->addContextData('base_path', $this->container->basePath); | 45 | $formatter->addContextData('base_path', $this->container->basePath); |
@@ -63,13 +51,15 @@ class DailyController extends ShaarliVisitorController | |||
63 | $linksToDisplay[$key]['description'] = $bookmark->getDescription(); | 51 | $linksToDisplay[$key]['description'] = $bookmark->getDescription(); |
64 | } | 52 | } |
65 | 53 | ||
66 | $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); | ||
67 | $data = [ | 54 | $data = [ |
68 | 'linksToDisplay' => $linksToDisplay, | 55 | 'linksToDisplay' => $linksToDisplay, |
69 | 'day' => $dayDate->getTimestamp(), | 56 | 'dayDate' => $start, |
70 | 'dayDate' => $dayDate, | 57 | 'day' => $start->getTimestamp(), |
71 | 'previousday' => $previousDay ?? '', | 58 | 'previousday' => $previousDay ? $previousDay->format($format) : '', |
72 | 'nextday' => $nextDay ?? '', | 59 | 'nextday' => $nextDay ? $nextDay->format($format) : '', |
60 | 'dayDesc' => $dailyDesc, | ||
61 | 'type' => $type, | ||
62 | 'localizedType' => $this->translateType($type), | ||
73 | ]; | 63 | ]; |
74 | 64 | ||
75 | // Hooks are called before column construction so that plugins don't have to deal with columns. | 65 | // Hooks are called before column construction so that plugins don't have to deal with columns. |
@@ -82,7 +72,7 @@ class DailyController extends ShaarliVisitorController | |||
82 | $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); | 72 | $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); |
83 | $this->assignView( | 73 | $this->assignView( |
84 | 'pagetitle', | 74 | 'pagetitle', |
85 | t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle | 75 | $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle |
86 | ); | 76 | ); |
87 | 77 | ||
88 | return $response->write($this->render(TemplatePage::DAILY)); | 78 | return $response->write($this->render(TemplatePage::DAILY)); |
@@ -106,11 +96,14 @@ class DailyController extends ShaarliVisitorController | |||
106 | } | 96 | } |
107 | 97 | ||
108 | $days = []; | 98 | $days = []; |
99 | $type = DailyPageHelper::extractRequestedType($request); | ||
100 | $format = DailyPageHelper::getFormatByType($type); | ||
101 | $length = DailyPageHelper::getRssLengthByType($type); | ||
109 | foreach ($this->container->bookmarkService->search() as $bookmark) { | 102 | foreach ($this->container->bookmarkService->search() as $bookmark) { |
110 | $day = $bookmark->getCreated()->format('Ymd'); | 103 | $day = $bookmark->getCreated()->format($format); |
111 | 104 | ||
112 | // Stop iterating after DAILY_RSS_NB_DAYS entries | 105 | // Stop iterating after DAILY_RSS_NB_DAYS entries |
113 | if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { | 106 | if (count($days) === $length && !isset($days[$day])) { |
114 | break; | 107 | break; |
115 | } | 108 | } |
116 | 109 | ||
@@ -127,12 +120,19 @@ class DailyController extends ShaarliVisitorController | |||
127 | 120 | ||
128 | /** @var Bookmark[] $bookmarks */ | 121 | /** @var Bookmark[] $bookmarks */ |
129 | foreach ($days as $day => $bookmarks) { | 122 | foreach ($days as $day => $bookmarks) { |
130 | $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); | 123 | $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day); |
124 | $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime); | ||
125 | |||
126 | // We only want the RSS entry to be published when the period is over. | ||
127 | if (new DateTime() < $endDateTime) { | ||
128 | continue; | ||
129 | } | ||
130 | |||
131 | $dataPerDay[$day] = [ | 131 | $dataPerDay[$day] = [ |
132 | 'date' => $dayDatetime, | 132 | 'date' => $endDateTime, |
133 | 'date_rss' => $dayDatetime->format(DateTime::RSS), | 133 | 'date_rss' => $endDateTime->format(DateTime::RSS), |
134 | 'date_human' => format_date($dayDatetime, false, true), | 134 | 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), |
135 | 'absolute_url' => $indexUrl . 'daily?day=' . $day, | 135 | 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day, |
136 | 'links' => [], | 136 | 'links' => [], |
137 | ]; | 137 | ]; |
138 | 138 | ||
@@ -141,16 +141,20 @@ class DailyController extends ShaarliVisitorController | |||
141 | 141 | ||
142 | // Make permalink URL absolute | 142 | // Make permalink URL absolute |
143 | if ($bookmark->isNote()) { | 143 | if ($bookmark->isNote()) { |
144 | $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); | 144 | $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl(); |
145 | } | 145 | } |
146 | } | 146 | } |
147 | } | 147 | } |
148 | 148 | ||
149 | $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); | 149 | $this->assignAllView([ |
150 | $this->assignView('index_url', $indexUrl); | 150 | 'title' => $this->container->conf->get('general.title', 'Shaarli'), |
151 | $this->assignView('page_url', $pageUrl); | 151 | 'index_url' => $indexUrl, |
152 | $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); | 152 | 'page_url' => $pageUrl, |
153 | $this->assignView('days', $dataPerDay); | 153 | 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false), |
154 | 'days' => $dataPerDay, | ||
155 | 'type' => $type, | ||
156 | 'localizedType' => $this->translateType($type), | ||
157 | ]); | ||
154 | 158 | ||
155 | $rssContent = $this->render(TemplatePage::DAILY_RSS); | 159 | $rssContent = $this->render(TemplatePage::DAILY_RSS); |
156 | 160 | ||
@@ -189,4 +193,13 @@ class DailyController extends ShaarliVisitorController | |||
189 | 193 | ||
190 | return $columns; | 194 | return $columns; |
191 | } | 195 | } |
196 | |||
197 | protected function translateType($type): string | ||
198 | { | ||
199 | return [ | ||
200 | t('day') => t('Daily'), | ||
201 | t('week') => t('Weekly'), | ||
202 | t('month') => t('Monthly'), | ||
203 | ][t($type)] ?? t('Daily'); | ||
204 | } | ||
192 | } | 205 | } |
diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php index 10aa84c8..428e8254 100644 --- a/application/front/controller/visitor/ErrorController.php +++ b/application/front/controller/visitor/ErrorController.php | |||
@@ -26,12 +26,15 @@ class ErrorController extends ShaarliVisitorController | |||
26 | $response = $response->withStatus($throwable->getCode()); | 26 | $response = $response->withStatus($throwable->getCode()); |
27 | } else { | 27 | } else { |
28 | // Internal error (any other Throwable) | 28 | // Internal error (any other Throwable) |
29 | if ($this->container->conf->get('dev.debug', false)) { | 29 | if ($this->container->conf->get('dev.debug', false) || $this->container->loginManager->isLoggedIn()) { |
30 | $this->assignView('message', $throwable->getMessage()); | 30 | $this->assignView('message', t('Error: ') . $throwable->getMessage()); |
31 | $this->assignView( | 31 | $this->assignView( |
32 | 'stacktrace', | 32 | 'text', |
33 | nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString()) | 33 | '<a href="https://github.com/shaarli/Shaarli/issues/new">' |
34 | . t('Please report it on Github.') | ||
35 | . '</a>' | ||
34 | ); | 36 | ); |
37 | $this->assignView('stacktrace', exception2text($throwable)); | ||
35 | } else { | 38 | } else { |
36 | $this->assignView('message', t('An unexpected error occurred.')); | 39 | $this->assignView('message', t('An unexpected error occurred.')); |
37 | } | 40 | } |
@@ -39,7 +42,6 @@ class ErrorController extends ShaarliVisitorController | |||
39 | $response = $response->withStatus(500); | 42 | $response = $response->withStatus(500); |
40 | } | 43 | } |
41 | 44 | ||
42 | |||
43 | return $response->write($this->render('error')); | 45 | return $response->write($this->render('error')); |
44 | } | 46 | } |
45 | } | 47 | } |
diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php index 8d8b546a..edc7ef43 100644 --- a/application/front/controller/visitor/FeedController.php +++ b/application/front/controller/visitor/FeedController.php | |||
@@ -27,7 +27,7 @@ class FeedController extends ShaarliVisitorController | |||
27 | 27 | ||
28 | protected function processRequest(string $feedType, Request $request, Response $response): Response | 28 | protected function processRequest(string $feedType, Request $request, Response $response): Response |
29 | { | 29 | { |
30 | $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8'); | 30 | $response = $response->withHeader('Content-Type', 'application/' . $feedType . '+xml; charset=utf-8'); |
31 | 31 | ||
32 | $pageUrl = page_url($this->container->environment); | 32 | $pageUrl = page_url($this->container->environment); |
33 | $cache = $this->container->pageCacheManager->getCachePage($pageUrl); | 33 | $cache = $this->container->pageCacheManager->getCachePage($pageUrl); |
diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 7cb32777..bf965929 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php | |||
@@ -4,10 +4,10 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Visitor; | 5 | namespace Shaarli\Front\Controller\Visitor; |
6 | 6 | ||
7 | use Shaarli\ApplicationUtils; | ||
8 | use Shaarli\Container\ShaarliContainer; | 7 | use Shaarli\Container\ShaarliContainer; |
9 | use Shaarli\Front\Exception\AlreadyInstalledException; | 8 | use Shaarli\Front\Exception\AlreadyInstalledException; |
10 | use Shaarli\Front\Exception\ResourcePermissionException; | 9 | use Shaarli\Front\Exception\ResourcePermissionException; |
10 | use Shaarli\Helper\ApplicationUtils; | ||
11 | use Shaarli\Languages; | 11 | use Shaarli\Languages; |
12 | use Shaarli\Security\SessionManager; | 12 | use Shaarli\Security\SessionManager; |
13 | use Slim\Http\Request; | 13 | use Slim\Http\Request; |
@@ -39,7 +39,8 @@ class InstallController extends ShaarliVisitorController | |||
39 | // Before installation, we'll make sure that permissions are set properly, and sessions are working. | 39 | // Before installation, we'll make sure that permissions are set properly, and sessions are working. |
40 | $this->checkPermissions(); | 40 | $this->checkPermissions(); |
41 | 41 | ||
42 | if (static::SESSION_TEST_VALUE | 42 | if ( |
43 | static::SESSION_TEST_VALUE | ||
43 | !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) | 44 | !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) |
44 | ) { | 45 | ) { |
45 | $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); | 46 | $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); |
@@ -53,6 +54,16 @@ class InstallController extends ShaarliVisitorController | |||
53 | $this->assignView('cities', $cities); | 54 | $this->assignView('cities', $cities); |
54 | $this->assignView('languages', Languages::getAvailableLanguages()); | 55 | $this->assignView('languages', Languages::getAvailableLanguages()); |
55 | 56 | ||
57 | $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); | ||
58 | |||
59 | $this->assignView('php_version', PHP_VERSION); | ||
60 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
61 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
62 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
63 | $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); | ||
64 | |||
65 | $this->assignView('pagetitle', t('Install Shaarli')); | ||
66 | |||
56 | return $response->write($this->render('install')); | 67 | return $response->write($this->render('install')); |
57 | } | 68 | } |
58 | 69 | ||
@@ -65,17 +76,18 @@ class InstallController extends ShaarliVisitorController | |||
65 | // This part makes sure sessions works correctly. | 76 | // This part makes sure sessions works correctly. |
66 | // (Because on some hosts, session.save_path may not be set correctly, | 77 | // (Because on some hosts, session.save_path may not be set correctly, |
67 | // or we may not have write access to it.) | 78 | // or we may not have write access to it.) |
68 | if (static::SESSION_TEST_VALUE | 79 | if ( |
80 | static::SESSION_TEST_VALUE | ||
69 | !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) | 81 | !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) |
70 | ) { | 82 | ) { |
71 | // Step 2: Check if data in session is correct. | 83 | // Step 2: Check if data in session is correct. |
72 | $msg = t( | 84 | $msg = t( |
73 | '<pre>Sessions do not seem to work correctly on your server.<br>'. | 85 | '<pre>Sessions do not seem to work correctly on your server.<br>' . |
74 | 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. | 86 | 'Make sure the variable "session.save_path" is set correctly in your PHP config, ' . |
75 | 'and that you have write access to it.<br>'. | 87 | 'and that you have write access to it.<br>' . |
76 | 'It currently points to %s.<br>'. | 88 | 'It currently points to %s.<br>' . |
77 | 'On some browsers, accessing your server via a hostname like \'localhost\' '. | 89 | 'On some browsers, accessing your server via a hostname like \'localhost\' ' . |
78 | 'or any custom hostname without a dot causes cookie storage to fail. '. | 90 | 'or any custom hostname without a dot causes cookie storage to fail. ' . |
79 | 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>' | 91 | 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>' |
80 | ); | 92 | ); |
81 | $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); | 93 | $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); |
@@ -94,7 +106,8 @@ class InstallController extends ShaarliVisitorController | |||
94 | public function save(Request $request, Response $response): Response | 106 | public function save(Request $request, Response $response): Response |
95 | { | 107 | { |
96 | $timezone = 'UTC'; | 108 | $timezone = 'UTC'; |
97 | if (!empty($request->getParam('continent')) | 109 | if ( |
110 | !empty($request->getParam('continent')) | ||
98 | && !empty($request->getParam('city')) | 111 | && !empty($request->getParam('city')) |
99 | && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) | 112 | && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) |
100 | ) { | 113 | ) { |
@@ -104,7 +117,7 @@ class InstallController extends ShaarliVisitorController | |||
104 | 117 | ||
105 | $login = $request->getParam('setlogin'); | 118 | $login = $request->getParam('setlogin'); |
106 | $this->container->conf->set('credentials.login', $login); | 119 | $this->container->conf->set('credentials.login', $login); |
107 | $salt = sha1(uniqid('', true) .'_'. mt_rand()); | 120 | $salt = sha1(uniqid('', true) . '_' . mt_rand()); |
108 | $this->container->conf->set('credentials.salt', $salt); | 121 | $this->container->conf->set('credentials.salt', $salt); |
109 | $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); | 122 | $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); |
110 | 123 | ||
@@ -113,7 +126,7 @@ class InstallController extends ShaarliVisitorController | |||
113 | } else { | 126 | } else { |
114 | $this->container->conf->set( | 127 | $this->container->conf->set( |
115 | 'general.title', | 128 | 'general.title', |
116 | 'Shared bookmarks on '.escape(index_url($this->container->environment)) | 129 | 'Shared bookmarks on ' . escape(index_url($this->container->environment)) |
117 | ); | 130 | ); |
118 | } | 131 | } |
119 | 132 | ||
@@ -150,7 +163,7 @@ class InstallController extends ShaarliVisitorController | |||
150 | protected function checkPermissions(): bool | 163 | protected function checkPermissions(): bool |
151 | { | 164 | { |
152 | // Ensure Shaarli has proper access to its resources | 165 | // Ensure Shaarli has proper access to its resources |
153 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); | 166 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true); |
154 | if (empty($errors)) { | 167 | if (empty($errors)) { |
155 | return true; | 168 | return true; |
156 | } | 169 | } |
diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index 121ba40b..4b881535 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php | |||
@@ -43,7 +43,7 @@ class LoginController extends ShaarliVisitorController | |||
43 | $this | 43 | $this |
44 | ->assignView('returnurl', escape($returnUrl)) | 44 | ->assignView('returnurl', escape($returnUrl)) |
45 | ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) | 45 | ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) |
46 | ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) | 46 | ->assignView('pagetitle', t('Login') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')) |
47 | ; | 47 | ; |
48 | 48 | ||
49 | return $response->write($this->render(TemplatePage::LOGIN)); | 49 | return $response->write($this->render(TemplatePage::LOGIN)); |
@@ -64,8 +64,8 @@ class LoginController extends ShaarliVisitorController | |||
64 | return $this->redirect($response, '/'); | 64 | return $this->redirect($response, '/'); |
65 | } | 65 | } |
66 | 66 | ||
67 | if (!$this->container->loginManager->checkCredentials( | 67 | if ( |
68 | $this->container->environment['REMOTE_ADDR'], | 68 | !$this->container->loginManager->checkCredentials( |
69 | client_ip_id($this->container->environment), | 69 | client_ip_id($this->container->environment), |
70 | $request->getParam('login'), | 70 | $request->getParam('login'), |
71 | $request->getParam('password') | 71 | $request->getParam('password') |
@@ -102,7 +102,8 @@ class LoginController extends ShaarliVisitorController | |||
102 | */ | 102 | */ |
103 | protected function checkLoginState(): bool | 103 | protected function checkLoginState(): bool |
104 | { | 104 | { |
105 | if ($this->container->loginManager->isLoggedIn() | 105 | if ( |
106 | $this->container->loginManager->isLoggedIn() | ||
106 | || $this->container->conf->get('security.open_shaarli', false) | 107 | || $this->container->conf->get('security.open_shaarli', false) |
107 | ) { | 108 | ) { |
108 | throw new CantLoginException(); | 109 | throw new CantLoginException(); |
diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php index 3c57f8dd..23553ee6 100644 --- a/application/front/controller/visitor/PictureWallController.php +++ b/application/front/controller/visitor/PictureWallController.php | |||
@@ -26,7 +26,7 @@ class PictureWallController extends ShaarliVisitorController | |||
26 | 26 | ||
27 | $this->assignView( | 27 | $this->assignView( |
28 | 'pagetitle', | 28 | 'pagetitle', |
29 | t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 29 | t('Picture wall') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
30 | ); | 30 | ); |
31 | 31 | ||
32 | // Optionally filter the results: | 32 | // Optionally filter the results: |
diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index 55c075a2..ae946c59 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php | |||
@@ -106,6 +106,7 @@ abstract class ShaarliVisitorController | |||
106 | 'target' => $template, | 106 | 'target' => $template, |
107 | 'loggedin' => $this->container->loginManager->isLoggedIn(), | 107 | 'loggedin' => $this->container->loginManager->isLoggedIn(), |
108 | 'basePath' => $this->container->basePath, | 108 | 'basePath' => $this->container->basePath, |
109 | 'rootPath' => preg_replace('#/index\.php$#', '', $this->container->basePath), | ||
109 | 'bookmarkService' => $this->container->bookmarkService | 110 | 'bookmarkService' => $this->container->bookmarkService |
110 | ]; | 111 | ]; |
111 | } | 112 | } |
@@ -143,7 +144,8 @@ abstract class ShaarliVisitorController | |||
143 | if (null !== $referer) { | 144 | if (null !== $referer) { |
144 | $currentUrl = parse_url($referer); | 145 | $currentUrl = parse_url($referer); |
145 | // If the referer is not related to Shaarli instance, redirect to default | 146 | // If the referer is not related to Shaarli instance, redirect to default |
146 | if (isset($currentUrl['host']) | 147 | if ( |
148 | isset($currentUrl['host']) | ||
147 | && strpos(index_url($this->container->environment), $currentUrl['host']) === false | 149 | && strpos(index_url($this->container->environment), $currentUrl['host']) === false |
148 | ) { | 150 | ) { |
149 | return $response->withRedirect($defaultPath); | 151 | return $response->withRedirect($defaultPath); |
@@ -172,7 +174,7 @@ abstract class ShaarliVisitorController | |||
172 | } | 174 | } |
173 | } | 175 | } |
174 | 176 | ||
175 | $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; | 177 | $queryString = count($params) > 0 ? '?' . http_build_query($params) : ''; |
176 | $anchor = $anchor ? '#' . $anchor : ''; | 178 | $anchor = $anchor ? '#' . $anchor : ''; |
177 | 179 | ||
178 | return $response->withRedirect($path . $queryString . $anchor); | 180 | return $response->withRedirect($path . $queryString . $anchor); |
diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php index 76ed7690..46d62779 100644 --- a/application/front/controller/visitor/TagCloudController.php +++ b/application/front/controller/visitor/TagCloudController.php | |||
@@ -47,13 +47,14 @@ class TagCloudController extends ShaarliVisitorController | |||
47 | */ | 47 | */ |
48 | protected function processRequest(string $type, Request $request, Response $response): Response | 48 | protected function processRequest(string $type, Request $request, Response $response): Response |
49 | { | 49 | { |
50 | $tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); | ||
50 | if ($this->container->loginManager->isLoggedIn() === true) { | 51 | if ($this->container->loginManager->isLoggedIn() === true) { |
51 | $visibility = $this->container->sessionManager->getSessionParameter('visibility'); | 52 | $visibility = $this->container->sessionManager->getSessionParameter('visibility'); |
52 | } | 53 | } |
53 | 54 | ||
54 | $sort = $request->getQueryParam('sort'); | 55 | $sort = $request->getQueryParam('sort'); |
55 | $searchTags = $request->getQueryParam('searchtags'); | 56 | $searchTags = $request->getQueryParam('searchtags'); |
56 | $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; | 57 | $filteringTags = $searchTags !== null ? explode($tagsSeparator, $searchTags) : []; |
57 | 58 | ||
58 | $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); | 59 | $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); |
59 | 60 | ||
@@ -71,8 +72,9 @@ class TagCloudController extends ShaarliVisitorController | |||
71 | $tagsUrl[escape($tag)] = urlencode((string) $tag); | 72 | $tagsUrl[escape($tag)] = urlencode((string) $tag); |
72 | } | 73 | } |
73 | 74 | ||
74 | $searchTags = implode(' ', escape($filteringTags)); | 75 | $searchTags = tags_array2str($filteringTags, $tagsSeparator); |
75 | $searchTagsUrl = urlencode(implode(' ', $filteringTags)); | 76 | $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : ''; |
77 | $searchTagsUrl = urlencode($searchTags); | ||
76 | $data = [ | 78 | $data = [ |
77 | 'search_tags' => escape($searchTags), | 79 | 'search_tags' => escape($searchTags), |
78 | 'search_tags_url' => $searchTagsUrl, | 80 | 'search_tags_url' => $searchTagsUrl, |
@@ -82,10 +84,10 @@ class TagCloudController extends ShaarliVisitorController | |||
82 | $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type); | 84 | $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type); |
83 | $this->assignAllView($data); | 85 | $this->assignAllView($data); |
84 | 86 | ||
85 | $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; | 87 | $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) . ' - ' : ''; |
86 | $this->assignView( | 88 | $this->assignView( |
87 | 'pagetitle', | 89 | 'pagetitle', |
88 | $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') | 90 | $searchTags . t('Tag ' . $type) . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
89 | ); | 91 | ); |
90 | 92 | ||
91 | return $response->write($this->render('tag.' . $type)); | 93 | return $response->write($this->render('tag.' . $type)); |
diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php index de4e7ea2..3aa58542 100644 --- a/application/front/controller/visitor/TagController.php +++ b/application/front/controller/visitor/TagController.php | |||
@@ -27,7 +27,7 @@ class TagController extends ShaarliVisitorController | |||
27 | // In case browser does not send HTTP_REFERER, we search a single tag | 27 | // In case browser does not send HTTP_REFERER, we search a single tag |
28 | if (null === $referer) { | 28 | if (null === $referer) { |
29 | if (null !== $newTag) { | 29 | if (null !== $newTag) { |
30 | return $this->redirect($response, '/?searchtags='. urlencode($newTag)); | 30 | return $this->redirect($response, '/?searchtags=' . urlencode($newTag)); |
31 | } | 31 | } |
32 | 32 | ||
33 | return $this->redirect($response, '/'); | 33 | return $this->redirect($response, '/'); |
@@ -37,7 +37,7 @@ class TagController extends ShaarliVisitorController | |||
37 | parse_str($currentUrl['query'] ?? '', $params); | 37 | parse_str($currentUrl['query'] ?? '', $params); |
38 | 38 | ||
39 | if (null === $newTag) { | 39 | if (null === $newTag) { |
40 | return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); | 40 | return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); |
41 | } | 41 | } |
42 | 42 | ||
43 | // Prevent redirection loop | 43 | // Prevent redirection loop |
@@ -45,9 +45,10 @@ class TagController extends ShaarliVisitorController | |||
45 | unset($params['addtag']); | 45 | unset($params['addtag']); |
46 | } | 46 | } |
47 | 47 | ||
48 | $tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); | ||
48 | // Check if this tag is already in the search query and ignore it if it is. | 49 | // Check if this tag is already in the search query and ignore it if it is. |
49 | // Each tag is always separated by a space | 50 | // Each tag is always separated by a space |
50 | $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : []; | 51 | $currentTags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator); |
51 | 52 | ||
52 | $addtag = true; | 53 | $addtag = true; |
53 | foreach ($currentTags as $value) { | 54 | foreach ($currentTags as $value) { |
@@ -62,12 +63,12 @@ class TagController extends ShaarliVisitorController | |||
62 | $currentTags[] = trim($newTag); | 63 | $currentTags[] = trim($newTag); |
63 | } | 64 | } |
64 | 65 | ||
65 | $params['searchtags'] = trim(implode(' ', $currentTags)); | 66 | $params['searchtags'] = tags_array2str($currentTags, $tagsSeparator); |
66 | 67 | ||
67 | // We also remove page (keeping the same page has no sense, since the results are different) | 68 | // We also remove page (keeping the same page has no sense, since the results are different) |
68 | unset($params['page']); | 69 | unset($params['page']); |
69 | 70 | ||
70 | return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); | 71 | return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); |
71 | } | 72 | } |
72 | 73 | ||
73 | /** | 74 | /** |
@@ -89,7 +90,7 @@ class TagController extends ShaarliVisitorController | |||
89 | parse_str($currentUrl['query'] ?? '', $params); | 90 | parse_str($currentUrl['query'] ?? '', $params); |
90 | 91 | ||
91 | if (null === $tagToRemove) { | 92 | if (null === $tagToRemove) { |
92 | return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); | 93 | return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); |
93 | } | 94 | } |
94 | 95 | ||
95 | // Prevent redirection loop | 96 | // Prevent redirection loop |
@@ -98,10 +99,11 @@ class TagController extends ShaarliVisitorController | |||
98 | } | 99 | } |
99 | 100 | ||
100 | if (isset($params['searchtags'])) { | 101 | if (isset($params['searchtags'])) { |
101 | $tags = explode(' ', $params['searchtags']); | 102 | $tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); |
103 | $tags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator); | ||
102 | // Remove value from array $tags. | 104 | // Remove value from array $tags. |
103 | $tags = array_diff($tags, [$tagToRemove]); | 105 | $tags = array_diff($tags, [$tagToRemove]); |
104 | $params['searchtags'] = implode(' ', $tags); | 106 | $params['searchtags'] = tags_array2str($tags, $tagsSeparator); |
105 | 107 | ||
106 | if (empty($params['searchtags'])) { | 108 | if (empty($params['searchtags'])) { |
107 | unset($params['searchtags']); | 109 | unset($params['searchtags']); |
diff --git a/application/ApplicationUtils.php b/application/helper/ApplicationUtils.php index 3aa21829..212dd8e2 100644 --- a/application/ApplicationUtils.php +++ b/application/helper/ApplicationUtils.php | |||
@@ -1,5 +1,6 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli; | 2 | |
3 | namespace Shaarli\Helper; | ||
3 | 4 | ||
4 | use Exception; | 5 | use Exception; |
5 | use Shaarli\Config\ConfigManager; | 6 | use Shaarli\Config\ConfigManager; |
@@ -14,8 +15,9 @@ class ApplicationUtils | |||
14 | */ | 15 | */ |
15 | public static $VERSION_FILE = 'shaarli_version.php'; | 16 | public static $VERSION_FILE = 'shaarli_version.php'; |
16 | 17 | ||
17 | private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; | 18 | public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli'; |
18 | private static $GIT_BRANCHES = array('latest', 'stable'); | 19 | public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; |
20 | public static $GIT_BRANCHES = ['latest', 'stable']; | ||
19 | private static $VERSION_START_TAG = '<?php /* '; | 21 | private static $VERSION_START_TAG = '<?php /* '; |
20 | private static $VERSION_END_TAG = ' */ ?>'; | 22 | private static $VERSION_END_TAG = ' */ ?>'; |
21 | 23 | ||
@@ -63,8 +65,8 @@ class ApplicationUtils | |||
63 | } | 65 | } |
64 | 66 | ||
65 | return str_replace( | 67 | return str_replace( |
66 | array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), | 68 | [self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL], |
67 | array('', '', ''), | 69 | ['', '', ''], |
68 | $data | 70 | $data |
69 | ); | 71 | ); |
70 | } | 72 | } |
@@ -125,7 +127,7 @@ class ApplicationUtils | |||
125 | // Late Static Binding allows overriding within tests | 127 | // Late Static Binding allows overriding within tests |
126 | // See http://php.net/manual/en/language.oop5.late-static-bindings.php | 128 | // See http://php.net/manual/en/language.oop5.late-static-bindings.php |
127 | $latestVersion = static::getVersion( | 129 | $latestVersion = static::getVersion( |
128 | self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE | 130 | self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE |
129 | ); | 131 | ); |
130 | 132 | ||
131 | if (!$latestVersion) { | 133 | if (!$latestVersion) { |
@@ -171,35 +173,47 @@ class ApplicationUtils | |||
171 | /** | 173 | /** |
172 | * Checks Shaarli has the proper access permissions to its resources | 174 | * Checks Shaarli has the proper access permissions to its resources |
173 | * | 175 | * |
174 | * @param ConfigManager $conf Configuration Manager instance. | 176 | * @param ConfigManager $conf Configuration Manager instance. |
177 | * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template. | ||
178 | * Currently we only need to be able to read the theme and write in raintpl cache. | ||
175 | * | 179 | * |
176 | * @return array A list of the detected configuration issues | 180 | * @return array A list of the detected configuration issues |
177 | */ | 181 | */ |
178 | public static function checkResourcePermissions($conf) | 182 | public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array |
179 | { | 183 | { |
180 | $errors = array(); | 184 | $errors = []; |
181 | $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); | 185 | $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); |
182 | 186 | ||
183 | // Check script and template directories are readable | 187 | // Check script and template directories are readable |
184 | foreach (array( | 188 | foreach ( |
185 | 'application', | 189 | [ |
186 | 'inc', | 190 | 'application', |
187 | 'plugins', | 191 | 'inc', |
188 | $rainTplDir, | 192 | 'plugins', |
189 | $rainTplDir . '/' . $conf->get('resource.theme'), | 193 | $rainTplDir, |
190 | ) as $path) { | 194 | $rainTplDir . '/' . $conf->get('resource.theme'), |
195 | ] as $path | ||
196 | ) { | ||
191 | if (!is_readable(realpath($path))) { | 197 | if (!is_readable(realpath($path))) { |
192 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); | 198 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); |
193 | } | 199 | } |
194 | } | 200 | } |
195 | 201 | ||
196 | // Check cache and data directories are readable and writable | 202 | // Check cache and data directories are readable and writable |
197 | foreach (array( | 203 | if ($minimalMode) { |
198 | $conf->get('resource.thumbnails_cache'), | 204 | $folders = [ |
199 | $conf->get('resource.data_dir'), | 205 | $conf->get('resource.raintpl_tmp'), |
200 | $conf->get('resource.page_cache'), | 206 | ]; |
201 | $conf->get('resource.raintpl_tmp'), | 207 | } else { |
202 | ) as $path) { | 208 | $folders = [ |
209 | $conf->get('resource.thumbnails_cache'), | ||
210 | $conf->get('resource.data_dir'), | ||
211 | $conf->get('resource.page_cache'), | ||
212 | $conf->get('resource.raintpl_tmp'), | ||
213 | ]; | ||
214 | } | ||
215 | |||
216 | foreach ($folders as $path) { | ||
203 | if (!is_readable(realpath($path))) { | 217 | if (!is_readable(realpath($path))) { |
204 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); | 218 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); |
205 | } | 219 | } |
@@ -208,14 +222,20 @@ class ApplicationUtils | |||
208 | } | 222 | } |
209 | } | 223 | } |
210 | 224 | ||
225 | if ($minimalMode) { | ||
226 | return $errors; | ||
227 | } | ||
228 | |||
211 | // Check configuration files are readable and writable | 229 | // Check configuration files are readable and writable |
212 | foreach (array( | 230 | foreach ( |
213 | $conf->getConfigFileExt(), | 231 | [ |
214 | $conf->get('resource.datastore'), | 232 | $conf->getConfigFileExt(), |
215 | $conf->get('resource.ban_file'), | 233 | $conf->get('resource.datastore'), |
216 | $conf->get('resource.log'), | 234 | $conf->get('resource.ban_file'), |
217 | $conf->get('resource.update_check'), | 235 | $conf->get('resource.log'), |
218 | ) as $path) { | 236 | $conf->get('resource.update_check'), |
237 | ] as $path | ||
238 | ) { | ||
219 | if (!is_file(realpath($path))) { | 239 | if (!is_file(realpath($path))) { |
220 | # the file may not exist yet | 240 | # the file may not exist yet |
221 | continue; | 241 | continue; |
@@ -246,4 +266,54 @@ class ApplicationUtils | |||
246 | { | 266 | { |
247 | return hash_hmac('sha256', $currentVersion, $salt); | 267 | return hash_hmac('sha256', $currentVersion, $salt); |
248 | } | 268 | } |
269 | |||
270 | /** | ||
271 | * Get a list of PHP extensions used by Shaarli. | ||
272 | * | ||
273 | * @return array[] List of extension with following keys: | ||
274 | * - name: extension name | ||
275 | * - required: whether the extension is required to use Shaarli | ||
276 | * - desc: short description of extension usage in Shaarli | ||
277 | * - loaded: whether the extension is properly loaded or not | ||
278 | */ | ||
279 | public static function getPhpExtensionsRequirement(): array | ||
280 | { | ||
281 | $extensions = [ | ||
282 | ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')], | ||
283 | ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')], | ||
284 | ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')], | ||
285 | ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')], | ||
286 | ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')], | ||
287 | ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')], | ||
288 | ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')], | ||
289 | ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')], | ||
290 | ]; | ||
291 | |||
292 | foreach ($extensions as &$extension) { | ||
293 | $extension['loaded'] = extension_loaded($extension['name']); | ||
294 | } | ||
295 | |||
296 | return $extensions; | ||
297 | } | ||
298 | |||
299 | /** | ||
300 | * Return the EOL date of given PHP version. If the version is unknown, | ||
301 | * we return today + 2 years. | ||
302 | * | ||
303 | * @param string $fullVersion PHP version, e.g. 7.4.7 | ||
304 | * | ||
305 | * @return string Date format: YYYY-MM-DD | ||
306 | */ | ||
307 | public static function getPhpEol(string $fullVersion): string | ||
308 | { | ||
309 | preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches); | ||
310 | |||
311 | return [ | ||
312 | '7.1' => '2019-12-01', | ||
313 | '7.2' => '2020-11-30', | ||
314 | '7.3' => '2021-12-06', | ||
315 | '7.4' => '2022-11-28', | ||
316 | '8.0' => '2023-12-01', | ||
317 | ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d'); | ||
318 | } | ||
249 | } | 319 | } |
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php new file mode 100644 index 00000000..5fabc907 --- /dev/null +++ b/application/helper/DailyPageHelper.php | |||
@@ -0,0 +1,208 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Helper; | ||
6 | |||
7 | use Shaarli\Bookmark\Bookmark; | ||
8 | use Slim\Http\Request; | ||
9 | |||
10 | class DailyPageHelper | ||
11 | { | ||
12 | public const MONTH = 'month'; | ||
13 | public const WEEK = 'week'; | ||
14 | public const DAY = 'day'; | ||
15 | |||
16 | /** | ||
17 | * Extracts the type of the daily to display from the HTTP request parameters | ||
18 | * | ||
19 | * @param Request $request HTTP request | ||
20 | * | ||
21 | * @return string month/week/day | ||
22 | */ | ||
23 | public static function extractRequestedType(Request $request): string | ||
24 | { | ||
25 | if ($request->getQueryParam(static::MONTH) !== null) { | ||
26 | return static::MONTH; | ||
27 | } elseif ($request->getQueryParam(static::WEEK) !== null) { | ||
28 | return static::WEEK; | ||
29 | } | ||
30 | |||
31 | return static::DAY; | ||
32 | } | ||
33 | |||
34 | /** | ||
35 | * Extracts a DateTimeImmutable from provided HTTP request. | ||
36 | * If no parameter is provided, we rely on the creation date of the latest provided created bookmark. | ||
37 | * If the datastore is empty or no bookmark is provided, we use the current date. | ||
38 | * | ||
39 | * @param string $type month/week/day | ||
40 | * @param string|null $requestedDate Input string extracted from the request | ||
41 | * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date) | ||
42 | * | ||
43 | * @return \DateTimeImmutable from input or latest bookmark. | ||
44 | * | ||
45 | * @throws \Exception Type not supported. | ||
46 | */ | ||
47 | public static function extractRequestedDateTime( | ||
48 | string $type, | ||
49 | ?string $requestedDate, | ||
50 | Bookmark $latestBookmark = null | ||
51 | ): \DateTimeImmutable { | ||
52 | $format = static::getFormatByType($type); | ||
53 | if (empty($requestedDate)) { | ||
54 | return $latestBookmark instanceof Bookmark | ||
55 | ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) | ||
56 | : new \DateTimeImmutable() | ||
57 | ; | ||
58 | } | ||
59 | |||
60 | // W is not supported by createFromFormat... | ||
61 | if ($type === static::WEEK) { | ||
62 | return (new \DateTimeImmutable()) | ||
63 | ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2)) | ||
64 | ; | ||
65 | } | ||
66 | |||
67 | return \DateTimeImmutable::createFromFormat($format, $requestedDate); | ||
68 | } | ||
69 | |||
70 | /** | ||
71 | * Get the DateTime format used by provided type | ||
72 | * Examples: | ||
73 | * - day: 20201016 (<year><month><day>) | ||
74 | * - week: 202041 (<year><week number>) | ||
75 | * - month: 202010 (<year><month>) | ||
76 | * | ||
77 | * @param string $type month/week/day | ||
78 | * | ||
79 | * @return string DateTime compatible format | ||
80 | * | ||
81 | * @see https://www.php.net/manual/en/datetime.format.php | ||
82 | * | ||
83 | * @throws \Exception Type not supported. | ||
84 | */ | ||
85 | public static function getFormatByType(string $type): string | ||
86 | { | ||
87 | switch ($type) { | ||
88 | case static::MONTH: | ||
89 | return 'Ym'; | ||
90 | case static::WEEK: | ||
91 | return 'YW'; | ||
92 | case static::DAY: | ||
93 | return 'Ymd'; | ||
94 | default: | ||
95 | throw new \Exception('Unsupported daily format type'); | ||
96 | } | ||
97 | } | ||
98 | |||
99 | /** | ||
100 | * Get the first DateTime of the time period depending on given datetime and type. | ||
101 | * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax | ||
102 | * and we don't want to alter original datetime. | ||
103 | * | ||
104 | * @param string $type month/week/day | ||
105 | * @param \DateTimeImmutable $requested DateTime extracted from request input | ||
106 | * (should come from extractRequestedDateTime) | ||
107 | * | ||
108 | * @return \DateTimeInterface First DateTime of the time period | ||
109 | * | ||
110 | * @throws \Exception Type not supported. | ||
111 | */ | ||
112 | public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface | ||
113 | { | ||
114 | switch ($type) { | ||
115 | case static::MONTH: | ||
116 | return $requested->modify('first day of this month midnight'); | ||
117 | case static::WEEK: | ||
118 | return $requested->modify('Monday this week midnight'); | ||
119 | case static::DAY: | ||
120 | return $requested->modify('Today midnight'); | ||
121 | default: | ||
122 | throw new \Exception('Unsupported daily format type'); | ||
123 | } | ||
124 | } | ||
125 | |||
126 | /** | ||
127 | * Get the last DateTime of the time period depending on given datetime and type. | ||
128 | * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax | ||
129 | * and we don't want to alter original datetime. | ||
130 | * | ||
131 | * @param string $type month/week/day | ||
132 | * @param \DateTimeImmutable $requested DateTime extracted from request input | ||
133 | * (should come from extractRequestedDateTime) | ||
134 | * | ||
135 | * @return \DateTimeInterface Last DateTime of the time period | ||
136 | * | ||
137 | * @throws \Exception Type not supported. | ||
138 | */ | ||
139 | public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface | ||
140 | { | ||
141 | switch ($type) { | ||
142 | case static::MONTH: | ||
143 | return $requested->modify('last day of this month 23:59:59'); | ||
144 | case static::WEEK: | ||
145 | return $requested->modify('Sunday this week 23:59:59'); | ||
146 | case static::DAY: | ||
147 | return $requested->modify('Today 23:59:59'); | ||
148 | default: | ||
149 | throw new \Exception('Unsupported daily format type'); | ||
150 | } | ||
151 | } | ||
152 | |||
153 | /** | ||
154 | * Get localized description of the time period depending on given datetime and type. | ||
155 | * Example: for a month period, it returns `October, 2020`. | ||
156 | * | ||
157 | * @param string $type month/week/day | ||
158 | * @param \DateTimeImmutable $requested DateTime extracted from request input | ||
159 | * (should come from extractRequestedDateTime) | ||
160 | * | ||
161 | * @return string Localized time period description | ||
162 | * | ||
163 | * @throws \Exception Type not supported. | ||
164 | */ | ||
165 | public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string | ||
166 | { | ||
167 | switch ($type) { | ||
168 | case static::MONTH: | ||
169 | return $requested->format('F') . ', ' . $requested->format('Y'); | ||
170 | case static::WEEK: | ||
171 | $requested = $requested->modify('Monday this week'); | ||
172 | return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')'; | ||
173 | case static::DAY: | ||
174 | $out = ''; | ||
175 | if ($requested->format('Ymd') === date('Ymd')) { | ||
176 | $out = t('Today') . ' - '; | ||
177 | } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) { | ||
178 | $out = t('Yesterday') . ' - '; | ||
179 | } | ||
180 | return $out . format_date($requested, false); | ||
181 | default: | ||
182 | throw new \Exception('Unsupported daily format type'); | ||
183 | } | ||
184 | } | ||
185 | |||
186 | /** | ||
187 | * Get the number of items to display in the RSS feed depending on the given type. | ||
188 | * | ||
189 | * @param string $type month/week/day | ||
190 | * | ||
191 | * @return int number of elements | ||
192 | * | ||
193 | * @throws \Exception Type not supported. | ||
194 | */ | ||
195 | public static function getRssLengthByType(string $type): int | ||
196 | { | ||
197 | switch ($type) { | ||
198 | case static::MONTH: | ||
199 | return 12; // 1 year | ||
200 | case static::WEEK: | ||
201 | return 26; // ~6 months | ||
202 | case static::DAY: | ||
203 | return 30; // ~1 month | ||
204 | default: | ||
205 | throw new \Exception('Unsupported daily format type'); | ||
206 | } | ||
207 | } | ||
208 | } | ||
diff --git a/application/FileUtils.php b/application/helper/FileUtils.php index 30560bfc..e8a2168c 100644 --- a/application/FileUtils.php +++ b/application/helper/FileUtils.php | |||
@@ -1,6 +1,6 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | namespace Shaarli; | 3 | namespace Shaarli\Helper; |
4 | 4 | ||
5 | use Shaarli\Exceptions\IOException; | 5 | use Shaarli\Exceptions\IOException; |
6 | 6 | ||
@@ -81,4 +81,60 @@ class FileUtils | |||
81 | ) | 81 | ) |
82 | ); | 82 | ); |
83 | } | 83 | } |
84 | |||
85 | /** | ||
86 | * Recursively deletes a folder content, and deletes itself optionally. | ||
87 | * If an excluded file is found, folders won't be deleted. | ||
88 | * | ||
89 | * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory. | ||
90 | * | ||
91 | * @param string $path | ||
92 | * @param bool $selfDelete Delete the provided folder if true, only its content if false. | ||
93 | * @param array $exclude | ||
94 | */ | ||
95 | public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool | ||
96 | { | ||
97 | $skipped = false; | ||
98 | |||
99 | if (!is_dir($path)) { | ||
100 | throw new IOException(t('Provided path is not a directory.')); | ||
101 | } | ||
102 | |||
103 | if (!static::isPathInShaarliFolder($path)) { | ||
104 | throw new IOException(t('Trying to delete a folder outside of Shaarli path.')); | ||
105 | } | ||
106 | |||
107 | foreach (new \DirectoryIterator($path) as $file) { | ||
108 | if ($file->isDot()) { | ||
109 | continue; | ||
110 | } | ||
111 | |||
112 | if (in_array($file->getBasename(), $exclude, true)) { | ||
113 | $skipped = true; | ||
114 | continue; | ||
115 | } | ||
116 | |||
117 | if ($file->isFile()) { | ||
118 | unlink($file->getPathname()); | ||
119 | } elseif ($file->isDir()) { | ||
120 | $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped; | ||
121 | } | ||
122 | } | ||
123 | |||
124 | if ($selfDelete && !$skipped) { | ||
125 | rmdir($path); | ||
126 | } | ||
127 | |||
128 | return $skipped; | ||
129 | } | ||
130 | |||
131 | /** | ||
132 | * Checks that the given path is inside Shaarli directory. | ||
133 | */ | ||
134 | public static function isPathInShaarliFolder(string $path): bool | ||
135 | { | ||
136 | $rootDirectory = dirname(dirname(dirname(__FILE__))); | ||
137 | |||
138 | return strpos(realpath($path), $rootDirectory) !== false; | ||
139 | } | ||
84 | } | 140 | } |
diff --git a/application/http/HttpAccess.php b/application/http/HttpAccess.php index 81d9e076..e80e0c01 100644 --- a/application/http/HttpAccess.php +++ b/application/http/HttpAccess.php | |||
@@ -14,9 +14,14 @@ namespace Shaarli\Http; | |||
14 | */ | 14 | */ |
15 | class HttpAccess | 15 | class HttpAccess |
16 | { | 16 | { |
17 | public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null) | 17 | public function getHttpResponse( |
18 | { | 18 | $url, |
19 | return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction); | 19 | $timeout = 30, |
20 | $maxBytes = 4194304, | ||
21 | $curlHeaderFunction = null, | ||
22 | $curlWriteFunction = null | ||
23 | ) { | ||
24 | return get_http_response($url, $timeout, $maxBytes, $curlHeaderFunction, $curlWriteFunction); | ||
20 | } | 25 | } |
21 | 26 | ||
22 | public function getCurlDownloadCallback( | 27 | public function getCurlDownloadCallback( |
@@ -25,7 +30,7 @@ class HttpAccess | |||
25 | &$description, | 30 | &$description, |
26 | &$keywords, | 31 | &$keywords, |
27 | $retrieveDescription, | 32 | $retrieveDescription, |
28 | $curlGetInfo = 'curl_getinfo' | 33 | $tagsSeparator |
29 | ) { | 34 | ) { |
30 | return get_curl_download_callback( | 35 | return get_curl_download_callback( |
31 | $charset, | 36 | $charset, |
@@ -33,7 +38,12 @@ class HttpAccess | |||
33 | $description, | 38 | $description, |
34 | $keywords, | 39 | $keywords, |
35 | $retrieveDescription, | 40 | $retrieveDescription, |
36 | $curlGetInfo | 41 | $tagsSeparator |
37 | ); | 42 | ); |
38 | } | 43 | } |
44 | |||
45 | public function getCurlHeaderCallback(&$charset, $curlGetInfo = 'curl_getinfo') | ||
46 | { | ||
47 | return get_curl_header_callback($charset, $curlGetInfo); | ||
48 | } | ||
39 | } | 49 | } |
diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php index 9f414073..4bde1d5b 100644 --- a/application/http/HttpUtils.php +++ b/application/http/HttpUtils.php | |||
@@ -6,12 +6,14 @@ use Shaarli\Http\Url; | |||
6 | * GET an HTTP URL to retrieve its content | 6 | * GET an HTTP URL to retrieve its content |
7 | * Uses the cURL library or a fallback method | 7 | * Uses the cURL library or a fallback method |
8 | * | 8 | * |
9 | * @param string $url URL to get (http://...) | 9 | * @param string $url URL to get (http://...) |
10 | * @param int $timeout network timeout (in seconds) | 10 | * @param int $timeout network timeout (in seconds) |
11 | * @param int $maxBytes maximum downloaded bytes (default: 4 MiB) | 11 | * @param int $maxBytes maximum downloaded bytes (default: 4 MiB) |
12 | * @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION). | 12 | * @param callable|string $curlHeaderFunction Optional callback called during the download of headers |
13 | * Can be used to add download conditions on the | 13 | * (CURLOPT_HEADERFUNCTION) |
14 | * headers (response code, content type, etc.). | 14 | * @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION). |
15 | * Can be used to add download conditions on the | ||
16 | * headers (response code, content type, etc.). | ||
15 | * | 17 | * |
16 | * @return array HTTP response headers, downloaded content | 18 | * @return array HTTP response headers, downloaded content |
17 | * | 19 | * |
@@ -35,13 +37,18 @@ use Shaarli\Http\Url; | |||
35 | * @see http://stackoverflow.com/q/9183178 | 37 | * @see http://stackoverflow.com/q/9183178 |
36 | * @see http://stackoverflow.com/q/1462720 | 38 | * @see http://stackoverflow.com/q/1462720 |
37 | */ | 39 | */ |
38 | function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null) | 40 | function get_http_response( |
39 | { | 41 | $url, |
42 | $timeout = 30, | ||
43 | $maxBytes = 4194304, | ||
44 | $curlHeaderFunction = null, | ||
45 | $curlWriteFunction = null | ||
46 | ) { | ||
40 | $urlObj = new Url($url); | 47 | $urlObj = new Url($url); |
41 | $cleanUrl = $urlObj->idnToAscii(); | 48 | $cleanUrl = $urlObj->idnToAscii(); |
42 | 49 | ||
43 | if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) { | 50 | if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) { |
44 | return array(array(0 => 'Invalid HTTP UrlUtils'), false); | 51 | return [[0 => 'Invalid HTTP UrlUtils'], false]; |
45 | } | 52 | } |
46 | 53 | ||
47 | $userAgent = | 54 | $userAgent = |
@@ -64,42 +71,39 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF | |||
64 | 71 | ||
65 | $ch = curl_init($cleanUrl); | 72 | $ch = curl_init($cleanUrl); |
66 | if ($ch === false) { | 73 | if ($ch === false) { |
67 | return array(array(0 => 'curl_init() error'), false); | 74 | return [[0 => 'curl_init() error'], false]; |
68 | } | 75 | } |
69 | 76 | ||
70 | // General cURL settings | 77 | // General cURL settings |
71 | curl_setopt($ch, CURLOPT_AUTOREFERER, true); | 78 | curl_setopt($ch, CURLOPT_AUTOREFERER, true); |
72 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | 79 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); |
73 | curl_setopt($ch, CURLOPT_HEADER, true); | 80 | // Default header download if the $curlHeaderFunction is not defined |
81 | curl_setopt($ch, CURLOPT_HEADER, !is_callable($curlHeaderFunction)); | ||
74 | curl_setopt( | 82 | curl_setopt( |
75 | $ch, | 83 | $ch, |
76 | CURLOPT_HTTPHEADER, | 84 | CURLOPT_HTTPHEADER, |
77 | array('Accept-Language: ' . $acceptLanguage) | 85 | ['Accept-Language: ' . $acceptLanguage] |
78 | ); | 86 | ); |
79 | curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs); | 87 | curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs); |
80 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | 88 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
81 | curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); | 89 | curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); |
82 | curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); | 90 | curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); |
83 | 91 | ||
92 | // Max download size management | ||
93 | curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024 * 16); | ||
94 | curl_setopt($ch, CURLOPT_NOPROGRESS, false); | ||
95 | if (is_callable($curlHeaderFunction)) { | ||
96 | curl_setopt($ch, CURLOPT_HEADERFUNCTION, $curlHeaderFunction); | ||
97 | } | ||
84 | if (is_callable($curlWriteFunction)) { | 98 | if (is_callable($curlWriteFunction)) { |
85 | curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction); | 99 | curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction); |
86 | } | 100 | } |
87 | |||
88 | // Max download size management | ||
89 | curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16); | ||
90 | curl_setopt($ch, CURLOPT_NOPROGRESS, false); | ||
91 | curl_setopt( | 101 | curl_setopt( |
92 | $ch, | 102 | $ch, |
93 | CURLOPT_PROGRESSFUNCTION, | 103 | CURLOPT_PROGRESSFUNCTION, |
94 | function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) { | 104 | function ($arg0, $arg1, $arg2, $arg3, $arg4) use ($maxBytes) { |
95 | if (version_compare(phpversion(), '5.5', '<')) { | 105 | $downloaded = $arg2; |
96 | // PHP version lower than 5.5 | 106 | |
97 | // Callback has 4 arguments | ||
98 | $downloaded = $arg1; | ||
99 | } else { | ||
100 | // Callback has 5 arguments | ||
101 | $downloaded = $arg2; | ||
102 | } | ||
103 | // Non-zero return stops downloading | 107 | // Non-zero return stops downloading |
104 | return ($downloaded > $maxBytes) ? 1 : 0; | 108 | return ($downloaded > $maxBytes) ? 1 : 0; |
105 | } | 109 | } |
@@ -118,9 +122,9 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF | |||
118 | * Removing this would require updating | 122 | * Removing this would require updating |
119 | * GetHttpUrlTest::testGetInvalidRemoteUrl() | 123 | * GetHttpUrlTest::testGetInvalidRemoteUrl() |
120 | */ | 124 | */ |
121 | return array(false, false); | 125 | return [false, false]; |
122 | } | 126 | } |
123 | return array(array(0 => 'curl_exec() error: ' . $errorStr), false); | 127 | return [[0 => 'curl_exec() error: ' . $errorStr], false]; |
124 | } | 128 | } |
125 | 129 | ||
126 | // Formatting output like the fallback method | 130 | // Formatting output like the fallback method |
@@ -131,7 +135,7 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF | |||
131 | $rawHeadersLastRedir = end($rawHeadersArrayRedirs); | 135 | $rawHeadersLastRedir = end($rawHeadersArrayRedirs); |
132 | 136 | ||
133 | $content = substr($response, $headSize); | 137 | $content = substr($response, $headSize); |
134 | $headers = array(); | 138 | $headers = []; |
135 | foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) { | 139 | foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) { |
136 | if (empty($line) || ctype_space($line)) { | 140 | if (empty($line) || ctype_space($line)) { |
137 | continue; | 141 | continue; |
@@ -142,7 +146,7 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF | |||
142 | $value = $splitLine[1]; | 146 | $value = $splitLine[1]; |
143 | if (array_key_exists($key, $headers)) { | 147 | if (array_key_exists($key, $headers)) { |
144 | if (!is_array($headers[$key])) { | 148 | if (!is_array($headers[$key])) { |
145 | $headers[$key] = array(0 => $headers[$key]); | 149 | $headers[$key] = [0 => $headers[$key]]; |
146 | } | 150 | } |
147 | $headers[$key][] = $value; | 151 | $headers[$key][] = $value; |
148 | } else { | 152 | } else { |
@@ -153,7 +157,7 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF | |||
153 | } | 157 | } |
154 | } | 158 | } |
155 | 159 | ||
156 | return array($headers, $content); | 160 | return [$headers, $content]; |
157 | } | 161 | } |
158 | 162 | ||
159 | /** | 163 | /** |
@@ -184,15 +188,15 @@ function get_http_response_fallback( | |||
184 | $acceptLanguage, | 188 | $acceptLanguage, |
185 | $maxRedr | 189 | $maxRedr |
186 | ) { | 190 | ) { |
187 | $options = array( | 191 | $options = [ |
188 | 'http' => array( | 192 | 'http' => [ |
189 | 'method' => 'GET', | 193 | 'method' => 'GET', |
190 | 'timeout' => $timeout, | 194 | 'timeout' => $timeout, |
191 | 'user_agent' => $userAgent, | 195 | 'user_agent' => $userAgent, |
192 | 'header' => "Accept: */*\r\n" | 196 | 'header' => "Accept: */*\r\n" |
193 | . 'Accept-Language: ' . $acceptLanguage | 197 | . 'Accept-Language: ' . $acceptLanguage |
194 | ) | 198 | ] |
195 | ); | 199 | ]; |
196 | 200 | ||
197 | stream_context_set_default($options); | 201 | stream_context_set_default($options); |
198 | list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); | 202 | list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); |
@@ -203,7 +207,7 @@ function get_http_response_fallback( | |||
203 | } | 207 | } |
204 | 208 | ||
205 | if (! $headers) { | 209 | if (! $headers) { |
206 | return array($headers, false); | 210 | return [$headers, false]; |
207 | } | 211 | } |
208 | 212 | ||
209 | try { | 213 | try { |
@@ -211,10 +215,10 @@ function get_http_response_fallback( | |||
211 | $context = stream_context_create($options); | 215 | $context = stream_context_create($options); |
212 | $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes); | 216 | $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes); |
213 | } catch (Exception $exc) { | 217 | } catch (Exception $exc) { |
214 | return array(array(0 => 'HTTP Error'), $exc->getMessage()); | 218 | return [[0 => 'HTTP Error'], $exc->getMessage()]; |
215 | } | 219 | } |
216 | 220 | ||
217 | return array($headers, $content); | 221 | return [$headers, $content]; |
218 | } | 222 | } |
219 | 223 | ||
220 | /** | 224 | /** |
@@ -233,10 +237,12 @@ function get_redirected_headers($url, $redirectionLimit = 3) | |||
233 | } | 237 | } |
234 | 238 | ||
235 | // Headers found, redirection found, and limit not reached. | 239 | // Headers found, redirection found, and limit not reached. |
236 | if ($redirectionLimit-- > 0 | 240 | if ( |
241 | $redirectionLimit-- > 0 | ||
237 | && !empty($headers) | 242 | && !empty($headers) |
238 | && (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false) | 243 | && (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false) |
239 | && !empty($headers['Location'])) { | 244 | && !empty($headers['Location']) |
245 | ) { | ||
240 | $redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location']; | 246 | $redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location']; |
241 | if ($redirection != $url) { | 247 | if ($redirection != $url) { |
242 | $redirection = getAbsoluteUrl($url, $redirection); | 248 | $redirection = getAbsoluteUrl($url, $redirection); |
@@ -244,7 +250,7 @@ function get_redirected_headers($url, $redirectionLimit = 3) | |||
244 | } | 250 | } |
245 | } | 251 | } |
246 | 252 | ||
247 | return array($headers, $url); | 253 | return [$headers, $url]; |
248 | } | 254 | } |
249 | 255 | ||
250 | /** | 256 | /** |
@@ -266,7 +272,7 @@ function getAbsoluteUrl($originalUrl, $newUrl) | |||
266 | } | 272 | } |
267 | 273 | ||
268 | $parts = parse_url($originalUrl); | 274 | $parts = parse_url($originalUrl); |
269 | $final = $parts['scheme'] .'://'. $parts['host']; | 275 | $final = $parts['scheme'] . '://' . $parts['host']; |
270 | $final .= (!empty($parts['port'])) ? $parts['port'] : ''; | 276 | $final .= (!empty($parts['port'])) ? $parts['port'] : ''; |
271 | $final .= '/'; | 277 | $final .= '/'; |
272 | if ($newUrl[0] != '/') { | 278 | if ($newUrl[0] != '/') { |
@@ -319,7 +325,8 @@ function server_url($server) | |||
319 | $scheme = 'https'; | 325 | $scheme = 'https'; |
320 | } | 326 | } |
321 | 327 | ||
322 | if (($scheme == 'http' && $port != '80') | 328 | if ( |
329 | ($scheme == 'http' && $port != '80') | ||
323 | || ($scheme == 'https' && $port != '443') | 330 | || ($scheme == 'https' && $port != '443') |
324 | ) { | 331 | ) { |
325 | $port = ':' . $port; | 332 | $port = ':' . $port; |
@@ -340,22 +347,26 @@ function server_url($server) | |||
340 | $host = $server['SERVER_NAME']; | 347 | $host = $server['SERVER_NAME']; |
341 | } | 348 | } |
342 | 349 | ||
343 | return $scheme.'://'.$host.$port; | 350 | return $scheme . '://' . $host . $port; |
344 | } | 351 | } |
345 | 352 | ||
346 | // SSL detection | 353 | // SSL detection |
347 | if ((! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on') | 354 | if ( |
348 | || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')) { | 355 | (! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on') |
356 | || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443') | ||
357 | ) { | ||
349 | $scheme = 'https'; | 358 | $scheme = 'https'; |
350 | } | 359 | } |
351 | 360 | ||
352 | // Do not append standard port values | 361 | // Do not append standard port values |
353 | if (($scheme == 'http' && $server['SERVER_PORT'] != '80') | 362 | if ( |
354 | || ($scheme == 'https' && $server['SERVER_PORT'] != '443')) { | 363 | ($scheme == 'http' && $server['SERVER_PORT'] != '80') |
355 | $port = ':'.$server['SERVER_PORT']; | 364 | || ($scheme == 'https' && $server['SERVER_PORT'] != '443') |
365 | ) { | ||
366 | $port = ':' . $server['SERVER_PORT']; | ||
356 | } | 367 | } |
357 | 368 | ||
358 | return $scheme.'://'.$server['SERVER_NAME'].$port; | 369 | return $scheme . '://' . $server['SERVER_NAME'] . $port; |
359 | } | 370 | } |
360 | 371 | ||
361 | /** | 372 | /** |
@@ -493,6 +504,46 @@ function is_https($server) | |||
493 | * Get cURL callback function for CURLOPT_WRITEFUNCTION | 504 | * Get cURL callback function for CURLOPT_WRITEFUNCTION |
494 | * | 505 | * |
495 | * @param string $charset to extract from the downloaded page (reference) | 506 | * @param string $charset to extract from the downloaded page (reference) |
507 | * @param string $curlGetInfo Optionally overrides curl_getinfo function | ||
508 | * | ||
509 | * @return Closure | ||
510 | */ | ||
511 | function get_curl_header_callback( | ||
512 | &$charset, | ||
513 | $curlGetInfo = 'curl_getinfo' | ||
514 | ) { | ||
515 | $isRedirected = false; | ||
516 | |||
517 | return function ($ch, $data) use ($curlGetInfo, &$charset, &$isRedirected) { | ||
518 | $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); | ||
519 | $chunkLength = strlen($data); | ||
520 | if (!empty($responseCode) && in_array($responseCode, [301, 302])) { | ||
521 | $isRedirected = true; | ||
522 | return $chunkLength; | ||
523 | } | ||
524 | if (!empty($responseCode) && $responseCode !== 200) { | ||
525 | return false; | ||
526 | } | ||
527 | // After a redirection, the content type will keep the previous request value | ||
528 | // until it finds the next content-type header. | ||
529 | if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { | ||
530 | $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); | ||
531 | } | ||
532 | if (!empty($contentType) && strpos($contentType, 'text/html') === false) { | ||
533 | return false; | ||
534 | } | ||
535 | if (!empty($contentType) && empty($charset)) { | ||
536 | $charset = header_extract_charset($contentType); | ||
537 | } | ||
538 | |||
539 | return $chunkLength; | ||
540 | }; | ||
541 | } | ||
542 | |||
543 | /** | ||
544 | * Get cURL callback function for CURLOPT_WRITEFUNCTION | ||
545 | * | ||
546 | * @param string $charset to extract from the downloaded page (reference) | ||
496 | * @param string $title to extract from the downloaded page (reference) | 547 | * @param string $title to extract from the downloaded page (reference) |
497 | * @param string $description to extract from the downloaded page (reference) | 548 | * @param string $description to extract from the downloaded page (reference) |
498 | * @param string $keywords to extract from the downloaded page (reference) | 549 | * @param string $keywords to extract from the downloaded page (reference) |
@@ -507,9 +558,8 @@ function get_curl_download_callback( | |||
507 | &$description, | 558 | &$description, |
508 | &$keywords, | 559 | &$keywords, |
509 | $retrieveDescription, | 560 | $retrieveDescription, |
510 | $curlGetInfo = 'curl_getinfo' | 561 | $tagsSeparator |
511 | ) { | 562 | ) { |
512 | $isRedirected = false; | ||
513 | $currentChunk = 0; | 563 | $currentChunk = 0; |
514 | $foundChunk = null; | 564 | $foundChunk = null; |
515 | 565 | ||
@@ -524,37 +574,22 @@ function get_curl_download_callback( | |||
524 | * | 574 | * |
525 | * @return int|bool length of $data or false if we need to stop the download | 575 | * @return int|bool length of $data or false if we need to stop the download |
526 | */ | 576 | */ |
527 | return function (&$ch, $data) use ( | 577 | return function ( |
578 | $ch, | ||
579 | $data | ||
580 | ) use ( | ||
528 | $retrieveDescription, | 581 | $retrieveDescription, |
529 | $curlGetInfo, | 582 | $tagsSeparator, |
530 | &$charset, | 583 | &$charset, |
531 | &$title, | 584 | &$title, |
532 | &$description, | 585 | &$description, |
533 | &$keywords, | 586 | &$keywords, |
534 | &$isRedirected, | ||
535 | &$currentChunk, | 587 | &$currentChunk, |
536 | &$foundChunk | 588 | &$foundChunk |
537 | ) { | 589 | ) { |
590 | $chunkLength = strlen($data); | ||
538 | $currentChunk++; | 591 | $currentChunk++; |
539 | $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); | 592 | |
540 | if (!empty($responseCode) && in_array($responseCode, [301, 302])) { | ||
541 | $isRedirected = true; | ||
542 | return strlen($data); | ||
543 | } | ||
544 | if (!empty($responseCode) && $responseCode !== 200) { | ||
545 | return false; | ||
546 | } | ||
547 | // After a redirection, the content type will keep the previous request value | ||
548 | // until it finds the next content-type header. | ||
549 | if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { | ||
550 | $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); | ||
551 | } | ||
552 | if (!empty($contentType) && strpos($contentType, 'text/html') === false) { | ||
553 | return false; | ||
554 | } | ||
555 | if (!empty($contentType) && empty($charset)) { | ||
556 | $charset = header_extract_charset($contentType); | ||
557 | } | ||
558 | if (empty($charset)) { | 593 | if (empty($charset)) { |
559 | $charset = html_extract_charset($data); | 594 | $charset = html_extract_charset($data); |
560 | } | 595 | } |
@@ -562,6 +597,10 @@ function get_curl_download_callback( | |||
562 | $title = html_extract_title($data); | 597 | $title = html_extract_title($data); |
563 | $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; | 598 | $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; |
564 | } | 599 | } |
600 | if (empty($title)) { | ||
601 | $title = html_extract_tag('title', $data); | ||
602 | $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; | ||
603 | } | ||
565 | if ($retrieveDescription && empty($description)) { | 604 | if ($retrieveDescription && empty($description)) { |
566 | $description = html_extract_tag('description', $data); | 605 | $description = html_extract_tag('description', $data); |
567 | $foundChunk = ! empty($description) ? $currentChunk : $foundChunk; | 606 | $foundChunk = ! empty($description) ? $currentChunk : $foundChunk; |
@@ -571,10 +610,10 @@ function get_curl_download_callback( | |||
571 | if (! empty($keywords)) { | 610 | if (! empty($keywords)) { |
572 | $foundChunk = $currentChunk; | 611 | $foundChunk = $currentChunk; |
573 | // Keywords use the format tag1, tag2 multiple words, tag | 612 | // Keywords use the format tag1, tag2 multiple words, tag |
574 | // So we format them to match Shaarli's separator and glue multiple words with '-' | 613 | // So we split the result with `,`, then if a tag contains the separator we replace it by `-`. |
575 | $keywords = implode(' ', array_map(function($keyword) { | 614 | $keywords = tags_array2str(array_map(function (string $keyword) use ($tagsSeparator): string { |
576 | return implode('-', preg_split('/\s+/', trim($keyword))); | 615 | return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-'); |
577 | }, explode(',', $keywords))); | 616 | }, tags_str2array($keywords, ',')), $tagsSeparator); |
578 | } | 617 | } |
579 | } | 618 | } |
580 | 619 | ||
@@ -582,7 +621,8 @@ function get_curl_download_callback( | |||
582 | // If we already found either the title, description or keywords, | 621 | // If we already found either the title, description or keywords, |
583 | // it's highly unlikely that we'll found the other metas further than | 622 | // it's highly unlikely that we'll found the other metas further than |
584 | // in the same chunk of data or the next one. So we also stop the download after that. | 623 | // in the same chunk of data or the next one. So we also stop the download after that. |
585 | if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null | 624 | if ( |
625 | (!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null | ||
586 | && (! $retrieveDescription | 626 | && (! $retrieveDescription |
587 | || $foundChunk < $currentChunk | 627 | || $foundChunk < $currentChunk |
588 | || (!empty($title) && !empty($description) && !empty($keywords)) | 628 | || (!empty($title) && !empty($description) && !empty($keywords)) |
@@ -591,6 +631,6 @@ function get_curl_download_callback( | |||
591 | return false; | 631 | return false; |
592 | } | 632 | } |
593 | 633 | ||
594 | return strlen($data); | 634 | return $chunkLength; |
595 | }; | 635 | }; |
596 | } | 636 | } |
diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php new file mode 100644 index 00000000..2e1401ec --- /dev/null +++ b/application/http/MetadataRetriever.php | |||
@@ -0,0 +1,69 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Http; | ||
6 | |||
7 | use Shaarli\Config\ConfigManager; | ||
8 | |||
9 | /** | ||
10 | * HTTP Tool used to extract metadata from external URL (title, description, etc.). | ||
11 | */ | ||
12 | class MetadataRetriever | ||
13 | { | ||
14 | /** @var ConfigManager */ | ||
15 | protected $conf; | ||
16 | |||
17 | /** @var HttpAccess */ | ||
18 | protected $httpAccess; | ||
19 | |||
20 | public function __construct(ConfigManager $conf, HttpAccess $httpAccess) | ||
21 | { | ||
22 | $this->conf = $conf; | ||
23 | $this->httpAccess = $httpAccess; | ||
24 | } | ||
25 | |||
26 | /** | ||
27 | * Retrieve metadata for given URL. | ||
28 | * | ||
29 | * @return array [ | ||
30 | * 'title' => <remote title>, | ||
31 | * 'description' => <remote description>, | ||
32 | * 'tags' => <remote keywords>, | ||
33 | * ] | ||
34 | */ | ||
35 | public function retrieve(string $url): array | ||
36 | { | ||
37 | $charset = null; | ||
38 | $title = null; | ||
39 | $description = null; | ||
40 | $tags = null; | ||
41 | |||
42 | // Short timeout to keep the application responsive | ||
43 | // The callback will fill $charset and $title with data from the downloaded page. | ||
44 | $this->httpAccess->getHttpResponse( | ||
45 | $url, | ||
46 | $this->conf->get('general.download_timeout', 30), | ||
47 | $this->conf->get('general.download_max_size', 4194304), | ||
48 | $this->httpAccess->getCurlHeaderCallback($charset), | ||
49 | $this->httpAccess->getCurlDownloadCallback( | ||
50 | $charset, | ||
51 | $title, | ||
52 | $description, | ||
53 | $tags, | ||
54 | $this->conf->get('general.retrieve_description'), | ||
55 | $this->conf->get('general.tags_separator', ' ') | ||
56 | ) | ||
57 | ); | ||
58 | |||
59 | if (!empty($title) && strtolower($charset) !== 'utf-8') { | ||
60 | $title = mb_convert_encoding($title, 'utf-8', $charset); | ||
61 | } | ||
62 | |||
63 | return [ | ||
64 | 'title' => $title, | ||
65 | 'description' => $description, | ||
66 | 'tags' => $tags, | ||
67 | ]; | ||
68 | } | ||
69 | } | ||
diff --git a/application/http/Url.php b/application/http/Url.php index 90444a2f..fe87088f 100644 --- a/application/http/Url.php +++ b/application/http/Url.php | |||
@@ -17,7 +17,7 @@ namespace Shaarli\Http; | |||
17 | */ | 17 | */ |
18 | class Url | 18 | class Url |
19 | { | 19 | { |
20 | private static $annoyingQueryParams = array( | 20 | private static $annoyingQueryParams = [ |
21 | 21 | ||
22 | 'action_object_map=', | 22 | 'action_object_map=', |
23 | 'action_ref_map=', | 23 | 'action_ref_map=', |
@@ -37,15 +37,15 @@ class Url | |||
37 | 37 | ||
38 | // Other | 38 | // Other |
39 | 'campaign_' | 39 | 'campaign_' |
40 | ); | 40 | ]; |
41 | 41 | ||
42 | private static $annoyingFragments = array( | 42 | private static $annoyingFragments = [ |
43 | // ATInternet | 43 | // ATInternet |
44 | 'xtor=RSS-', | 44 | 'xtor=RSS-', |
45 | 45 | ||
46 | // Misc. | 46 | // Misc. |
47 | 'tk.rss_all' | 47 | 'tk.rss_all' |
48 | ); | 48 | ]; |
49 | 49 | ||
50 | /* | 50 | /* |
51 | * URL parts represented as an array | 51 | * URL parts represented as an array |
@@ -120,7 +120,7 @@ class Url | |||
120 | foreach (self::$annoyingQueryParams as $annoying) { | 120 | foreach (self::$annoyingQueryParams as $annoying) { |
121 | foreach ($queryParams as $param) { | 121 | foreach ($queryParams as $param) { |
122 | if (startsWith($param, $annoying)) { | 122 | if (startsWith($param, $annoying)) { |
123 | $queryParams = array_diff($queryParams, array($param)); | 123 | $queryParams = array_diff($queryParams, [$param]); |
124 | continue; | 124 | continue; |
125 | } | 125 | } |
126 | } | 126 | } |
diff --git a/application/http/UrlUtils.php b/application/http/UrlUtils.php index e8d1a283..de5b7db1 100644 --- a/application/http/UrlUtils.php +++ b/application/http/UrlUtils.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Converts an array-represented URL to a string | 4 | * Converts an array-represented URL to a string |
4 | * | 5 | * |
@@ -12,15 +13,15 @@ | |||
12 | */ | 13 | */ |
13 | function unparse_url($parsedUrl) | 14 | function unparse_url($parsedUrl) |
14 | { | 15 | { |
15 | $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'].'://' : ''; | 16 | $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : ''; |
16 | $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : ''; | 17 | $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : ''; |
17 | $port = isset($parsedUrl['port']) ? ':'.$parsedUrl['port'] : ''; | 18 | $port = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : ''; |
18 | $user = isset($parsedUrl['user']) ? $parsedUrl['user'] : ''; | 19 | $user = isset($parsedUrl['user']) ? $parsedUrl['user'] : ''; |
19 | $pass = isset($parsedUrl['pass']) ? ':'.$parsedUrl['pass'] : ''; | 20 | $pass = isset($parsedUrl['pass']) ? ':' . $parsedUrl['pass'] : ''; |
20 | $pass = ($user || $pass) ? "$pass@" : ''; | 21 | $pass = ($user || $pass) ? "$pass@" : ''; |
21 | $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : ''; | 22 | $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : ''; |
22 | $query = isset($parsedUrl['query']) ? '?'.$parsedUrl['query'] : ''; | 23 | $query = isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : ''; |
23 | $fragment = isset($parsedUrl['fragment']) ? '#'.$parsedUrl['fragment'] : ''; | 24 | $fragment = isset($parsedUrl['fragment']) ? '#' . $parsedUrl['fragment'] : ''; |
24 | 25 | ||
25 | return "$scheme$user$pass$host$port$path$query$fragment"; | 26 | return "$scheme$user$pass$host$port$path$query$fragment"; |
26 | } | 27 | } |
diff --git a/application/legacy/LegacyController.php b/application/legacy/LegacyController.php index 826604e7..1fed418b 100644 --- a/application/legacy/LegacyController.php +++ b/application/legacy/LegacyController.php | |||
@@ -51,7 +51,7 @@ class LegacyController extends ShaarliVisitorController | |||
51 | 51 | ||
52 | if (!$this->container->loginManager->isLoggedIn()) { | 52 | if (!$this->container->loginManager->isLoggedIn()) { |
53 | $parameters = $buildParameters($request->getQueryParams(), true); | 53 | $parameters = $buildParameters($request->getQueryParams(), true); |
54 | return $this->redirect($response, '/login?returnurl='. $this->getBasePath() . $route . $parameters); | 54 | return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route . $parameters); |
55 | } | 55 | } |
56 | 56 | ||
57 | $parameters = $buildParameters($request->getQueryParams(), false); | 57 | $parameters = $buildParameters($request->getQueryParams(), false); |
diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php index 7bf76fd4..d3beafe0 100644 --- a/application/legacy/LegacyLinkDB.php +++ b/application/legacy/LegacyLinkDB.php | |||
@@ -8,7 +8,7 @@ use DateTime; | |||
8 | use Iterator; | 8 | use Iterator; |
9 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 9 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
10 | use Shaarli\Exceptions\IOException; | 10 | use Shaarli\Exceptions\IOException; |
11 | use Shaarli\FileUtils; | 11 | use Shaarli\Helper\FileUtils; |
12 | use Shaarli\Render\PageCacheManager; | 12 | use Shaarli\Render\PageCacheManager; |
13 | 13 | ||
14 | /** | 14 | /** |
@@ -62,7 +62,7 @@ class LegacyLinkDB implements Iterator, Countable, ArrayAccess | |||
62 | private $datastore; | 62 | private $datastore; |
63 | 63 | ||
64 | // Link date storage format | 64 | // Link date storage format |
65 | const LINK_DATE_FORMAT = 'Ymd_His'; | 65 | public const LINK_DATE_FORMAT = 'Ymd_His'; |
66 | 66 | ||
67 | // List of bookmarks (associative array) | 67 | // List of bookmarks (associative array) |
68 | // - key: link date (e.g. "20110823_124546"), | 68 | // - key: link date (e.g. "20110823_124546"), |
@@ -240,8 +240,8 @@ class LegacyLinkDB implements Iterator, Countable, ArrayAccess | |||
240 | } | 240 | } |
241 | 241 | ||
242 | // Create a dummy database for example | 242 | // Create a dummy database for example |
243 | $this->links = array(); | 243 | $this->links = []; |
244 | $link = array( | 244 | $link = [ |
245 | 'id' => 1, | 245 | 'id' => 1, |
246 | 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'), | 246 | 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'), |
247 | 'url' => 'https://shaarli.readthedocs.io', | 247 | 'url' => 'https://shaarli.readthedocs.io', |
@@ -257,11 +257,11 @@ You use the community supported version of the original Shaarli project, by Seba | |||
257 | 'created' => new DateTime(), | 257 | 'created' => new DateTime(), |
258 | 'tags' => 'opensource software', | 258 | 'tags' => 'opensource software', |
259 | 'sticky' => false, | 259 | 'sticky' => false, |
260 | ); | 260 | ]; |
261 | $link['shorturl'] = link_small_hash($link['created'], $link['id']); | 261 | $link['shorturl'] = link_small_hash($link['created'], $link['id']); |
262 | $this->links[1] = $link; | 262 | $this->links[1] = $link; |
263 | 263 | ||
264 | $link = array( | 264 | $link = [ |
265 | 'id' => 0, | 265 | 'id' => 0, |
266 | 'title' => t('My secret stuff... - Pastebin.com'), | 266 | 'title' => t('My secret stuff... - Pastebin.com'), |
267 | 'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', | 267 | 'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', |
@@ -270,7 +270,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
270 | 'created' => new DateTime('1 minute ago'), | 270 | 'created' => new DateTime('1 minute ago'), |
271 | 'tags' => 'secretstuff', | 271 | 'tags' => 'secretstuff', |
272 | 'sticky' => false, | 272 | 'sticky' => false, |
273 | ); | 273 | ]; |
274 | $link['shorturl'] = link_small_hash($link['created'], $link['id']); | 274 | $link['shorturl'] = link_small_hash($link['created'], $link['id']); |
275 | $this->links[0] = $link; | 275 | $this->links[0] = $link; |
276 | 276 | ||
@@ -285,7 +285,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
285 | { | 285 | { |
286 | // Public bookmarks are hidden and user not logged in => nothing to show | 286 | // Public bookmarks are hidden and user not logged in => nothing to show |
287 | if ($this->hidePublicLinks && !$this->loggedIn) { | 287 | if ($this->hidePublicLinks && !$this->loggedIn) { |
288 | $this->links = array(); | 288 | $this->links = []; |
289 | return; | 289 | return; |
290 | } | 290 | } |
291 | 291 | ||
@@ -293,7 +293,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
293 | $this->ids = []; | 293 | $this->ids = []; |
294 | $this->links = FileUtils::readFlatDB($this->datastore, []); | 294 | $this->links = FileUtils::readFlatDB($this->datastore, []); |
295 | 295 | ||
296 | $toremove = array(); | 296 | $toremove = []; |
297 | foreach ($this->links as $key => &$link) { | 297 | foreach ($this->links as $key => &$link) { |
298 | if (!$this->loggedIn && $link['private'] != 0) { | 298 | if (!$this->loggedIn && $link['private'] != 0) { |
299 | // Transition for not upgraded databases. | 299 | // Transition for not upgraded databases. |
@@ -414,7 +414,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
414 | * @return array filtered bookmarks, all bookmarks if no suitable filter was provided. | 414 | * @return array filtered bookmarks, all bookmarks if no suitable filter was provided. |
415 | */ | 415 | */ |
416 | public function filterSearch( | 416 | public function filterSearch( |
417 | $filterRequest = array(), | 417 | $filterRequest = [], |
418 | $casesensitive = false, | 418 | $casesensitive = false, |
419 | $visibility = 'all', | 419 | $visibility = 'all', |
420 | $untaggedonly = false | 420 | $untaggedonly = false |
@@ -512,7 +512,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
512 | */ | 512 | */ |
513 | public function days() | 513 | public function days() |
514 | { | 514 | { |
515 | $linkDays = array(); | 515 | $linkDays = []; |
516 | foreach ($this->links as $link) { | 516 | foreach ($this->links as $link) { |
517 | $linkDays[$link['created']->format('Ymd')] = 0; | 517 | $linkDays[$link['created']->format('Ymd')] = 0; |
518 | } | 518 | } |
diff --git a/application/legacy/LegacyLinkFilter.php b/application/legacy/LegacyLinkFilter.php index 7cf93d60..e6d186c4 100644 --- a/application/legacy/LegacyLinkFilter.php +++ b/application/legacy/LegacyLinkFilter.php | |||
@@ -120,7 +120,7 @@ class LegacyLinkFilter | |||
120 | return $this->links; | 120 | return $this->links; |
121 | } | 121 | } |
122 | 122 | ||
123 | $out = array(); | 123 | $out = []; |
124 | foreach ($this->links as $key => $value) { | 124 | foreach ($this->links as $key => $value) { |
125 | if ($value['private'] && $visibility === 'private') { | 125 | if ($value['private'] && $visibility === 'private') { |
126 | $out[$key] = $value; | 126 | $out[$key] = $value; |
@@ -143,7 +143,7 @@ class LegacyLinkFilter | |||
143 | */ | 143 | */ |
144 | private function filterSmallHash($smallHash) | 144 | private function filterSmallHash($smallHash) |
145 | { | 145 | { |
146 | $filtered = array(); | 146 | $filtered = []; |
147 | foreach ($this->links as $key => $l) { | 147 | foreach ($this->links as $key => $l) { |
148 | if ($smallHash == $l['shorturl']) { | 148 | if ($smallHash == $l['shorturl']) { |
149 | // Yes, this is ugly and slow | 149 | // Yes, this is ugly and slow |
@@ -186,7 +186,7 @@ class LegacyLinkFilter | |||
186 | return $this->noFilter($visibility); | 186 | return $this->noFilter($visibility); |
187 | } | 187 | } |
188 | 188 | ||
189 | $filtered = array(); | 189 | $filtered = []; |
190 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); | 190 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); |
191 | $exactRegex = '/"([^"]+)"/'; | 191 | $exactRegex = '/"([^"]+)"/'; |
192 | // Retrieve exact search terms. | 192 | // Retrieve exact search terms. |
@@ -198,8 +198,8 @@ class LegacyLinkFilter | |||
198 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); | 198 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); |
199 | 199 | ||
200 | // Filter excluding terms and update andSearch. | 200 | // Filter excluding terms and update andSearch. |
201 | $excludeSearch = array(); | 201 | $excludeSearch = []; |
202 | $andSearch = array(); | 202 | $andSearch = []; |
203 | foreach ($explodedSearchAnd as $needle) { | 203 | foreach ($explodedSearchAnd as $needle) { |
204 | if ($needle[0] == '-' && strlen($needle) > 1) { | 204 | if ($needle[0] == '-' && strlen($needle) > 1) { |
205 | $excludeSearch[] = substr($needle, 1); | 205 | $excludeSearch[] = substr($needle, 1); |
@@ -208,7 +208,7 @@ class LegacyLinkFilter | |||
208 | } | 208 | } |
209 | } | 209 | } |
210 | 210 | ||
211 | $keys = array('title', 'description', 'url', 'tags'); | 211 | $keys = ['title', 'description', 'url', 'tags']; |
212 | 212 | ||
213 | // Iterate over every stored link. | 213 | // Iterate over every stored link. |
214 | foreach ($this->links as $id => $link) { | 214 | foreach ($this->links as $id => $link) { |
@@ -336,7 +336,7 @@ class LegacyLinkFilter | |||
336 | } | 336 | } |
337 | 337 | ||
338 | // create resulting array | 338 | // create resulting array |
339 | $filtered = array(); | 339 | $filtered = []; |
340 | 340 | ||
341 | // iterate over each link | 341 | // iterate over each link |
342 | foreach ($this->links as $key => $link) { | 342 | foreach ($this->links as $key => $link) { |
@@ -352,7 +352,7 @@ class LegacyLinkFilter | |||
352 | $search = $link['tags']; // build search string, start with tags of current link | 352 | $search = $link['tags']; // build search string, start with tags of current link |
353 | if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) { | 353 | if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) { |
354 | // description given and at least one possible tag found | 354 | // description given and at least one possible tag found |
355 | $descTags = array(); | 355 | $descTags = []; |
356 | // find all tags in the form of #tag in the description | 356 | // find all tags in the form of #tag in the description |
357 | preg_match_all( | 357 | preg_match_all( |
358 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', | 358 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', |
@@ -419,7 +419,7 @@ class LegacyLinkFilter | |||
419 | throw new Exception('Invalid date format'); | 419 | throw new Exception('Invalid date format'); |
420 | } | 420 | } |
421 | 421 | ||
422 | $filtered = array(); | 422 | $filtered = []; |
423 | foreach ($this->links as $key => $l) { | 423 | foreach ($this->links as $key => $l) { |
424 | if ($l['created']->format('Ymd') == $day) { | 424 | if ($l['created']->format('Ymd') == $day) { |
425 | $filtered[$key] = $l; | 425 | $filtered[$key] = $l; |
diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index 0ab3a55b..9bda54b8 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php | |||
@@ -7,7 +7,6 @@ use RainTPL; | |||
7 | use ReflectionClass; | 7 | use ReflectionClass; |
8 | use ReflectionException; | 8 | use ReflectionException; |
9 | use ReflectionMethod; | 9 | use ReflectionMethod; |
10 | use Shaarli\ApplicationUtils; | ||
11 | use Shaarli\Bookmark\Bookmark; | 10 | use Shaarli\Bookmark\Bookmark; |
12 | use Shaarli\Bookmark\BookmarkArray; | 11 | use Shaarli\Bookmark\BookmarkArray; |
13 | use Shaarli\Bookmark\BookmarkFilter; | 12 | use Shaarli\Bookmark\BookmarkFilter; |
@@ -17,6 +16,7 @@ use Shaarli\Config\ConfigJson; | |||
17 | use Shaarli\Config\ConfigManager; | 16 | use Shaarli\Config\ConfigManager; |
18 | use Shaarli\Config\ConfigPhp; | 17 | use Shaarli\Config\ConfigPhp; |
19 | use Shaarli\Exceptions\IOException; | 18 | use Shaarli\Exceptions\IOException; |
19 | use Shaarli\Helper\ApplicationUtils; | ||
20 | use Shaarli\Thumbnailer; | 20 | use Shaarli\Thumbnailer; |
21 | use Shaarli\Updater\Exception\UpdaterException; | 21 | use Shaarli\Updater\Exception\UpdaterException; |
22 | 22 | ||
@@ -93,7 +93,7 @@ class LegacyUpdater | |||
93 | */ | 93 | */ |
94 | public function update() | 94 | public function update() |
95 | { | 95 | { |
96 | $updatesRan = array(); | 96 | $updatesRan = []; |
97 | 97 | ||
98 | // If the user isn't logged in, exit without updating. | 98 | // If the user isn't logged in, exit without updating. |
99 | if ($this->isLoggedIn !== true) { | 99 | if ($this->isLoggedIn !== true) { |
@@ -106,7 +106,8 @@ class LegacyUpdater | |||
106 | 106 | ||
107 | foreach ($this->methods as $method) { | 107 | foreach ($this->methods as $method) { |
108 | // Not an update method or already done, pass. | 108 | // Not an update method or already done, pass. |
109 | if (!startsWith($method->getName(), 'updateMethod') | 109 | if ( |
110 | !startsWith($method->getName(), 'updateMethod') | ||
110 | || in_array($method->getName(), $this->doneUpdates) | 111 | || in_array($method->getName(), $this->doneUpdates) |
111 | ) { | 112 | ) { |
112 | continue; | 113 | continue; |
@@ -189,7 +190,7 @@ class LegacyUpdater | |||
189 | } | 190 | } |
190 | 191 | ||
191 | // Set sub config keys (config and plugins) | 192 | // Set sub config keys (config and plugins) |
192 | $subConfig = array('config', 'plugins'); | 193 | $subConfig = ['config', 'plugins']; |
193 | foreach ($subConfig as $sub) { | 194 | foreach ($subConfig as $sub) { |
194 | foreach ($oldConfig[$sub] as $key => $value) { | 195 | foreach ($oldConfig[$sub] as $key => $value) { |
195 | if (isset($legacyMap[$sub . '.' . $key])) { | 196 | if (isset($legacyMap[$sub . '.' . $key])) { |
@@ -259,7 +260,7 @@ class LegacyUpdater | |||
259 | $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php'; | 260 | $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php'; |
260 | copy($this->conf->get('resource.datastore'), $save); | 261 | copy($this->conf->get('resource.datastore'), $save); |
261 | 262 | ||
262 | $links = array(); | 263 | $links = []; |
263 | foreach ($this->linkDB as $offset => $value) { | 264 | foreach ($this->linkDB as $offset => $value) { |
264 | $links[] = $value; | 265 | $links[] = $value; |
265 | unset($this->linkDB[$offset]); | 266 | unset($this->linkDB[$offset]); |
@@ -498,7 +499,8 @@ class LegacyUpdater | |||
498 | */ | 499 | */ |
499 | public function updateMethodDownloadSizeAndTimeoutConf() | 500 | public function updateMethodDownloadSizeAndTimeoutConf() |
500 | { | 501 | { |
501 | if ($this->conf->exists('general.download_max_size') | 502 | if ( |
503 | $this->conf->exists('general.download_max_size') | ||
502 | && $this->conf->exists('general.download_timeout') | 504 | && $this->conf->exists('general.download_timeout') |
503 | ) { | 505 | ) { |
504 | return true; | 506 | return true; |
@@ -585,7 +587,7 @@ class LegacyUpdater | |||
585 | 587 | ||
586 | $linksArray = new BookmarkArray(); | 588 | $linksArray = new BookmarkArray(); |
587 | foreach ($this->linkDB as $key => $link) { | 589 | foreach ($this->linkDB as $key => $link) { |
588 | $linksArray[$key] = (new Bookmark())->fromArray($link); | 590 | $linksArray[$key] = (new Bookmark())->fromArray($link, $this->conf->get('general.tags_separator', ' ')); |
589 | } | 591 | } |
590 | $linksIo = new BookmarkIO($this->conf); | 592 | $linksIo = new BookmarkIO($this->conf); |
591 | $linksIo->write($linksArray); | 593 | $linksIo->write($linksArray); |
diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index b83f16f8..2d97b4c8 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php | |||
@@ -59,11 +59,11 @@ class NetscapeBookmarkUtils | |||
59 | $indexUrl | 59 | $indexUrl |
60 | ) { | 60 | ) { |
61 | // see tpl/export.html for possible values | 61 | // see tpl/export.html for possible values |
62 | if (!in_array($selection, array('all', 'public', 'private'))) { | 62 | if (!in_array($selection, ['all', 'public', 'private'])) { |
63 | throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"'); | 63 | throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"'); |
64 | } | 64 | } |
65 | 65 | ||
66 | $bookmarkLinks = array(); | 66 | $bookmarkLinks = []; |
67 | foreach ($this->bookmarkService->search([], $selection) as $bookmark) { | 67 | foreach ($this->bookmarkService->search([], $selection) as $bookmark) { |
68 | $link = $formatter->format($bookmark); | 68 | $link = $formatter->format($bookmark); |
69 | $link['taglist'] = implode(',', $bookmark->getTags()); | 69 | $link['taglist'] = implode(',', $bookmark->getTags()); |
@@ -101,11 +101,11 @@ class NetscapeBookmarkUtils | |||
101 | 101 | ||
102 | // Add tags to all imported bookmarks? | 102 | // Add tags to all imported bookmarks? |
103 | if (empty($post['default_tags'])) { | 103 | if (empty($post['default_tags'])) { |
104 | $defaultTags = array(); | 104 | $defaultTags = []; |
105 | } else { | 105 | } else { |
106 | $defaultTags = preg_split( | 106 | $defaultTags = tags_str2array( |
107 | '/[\s,]+/', | 107 | escape($post['default_tags']), |
108 | escape($post['default_tags']) | 108 | $this->conf->get('general.tags_separator', ' ') |
109 | ); | 109 | ); |
110 | } | 110 | } |
111 | 111 | ||
@@ -171,7 +171,7 @@ class NetscapeBookmarkUtils | |||
171 | $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols')); | 171 | $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols')); |
172 | $link->setDescription($bkm['note']); | 172 | $link->setDescription($bkm['note']); |
173 | $link->setPrivate($private); | 173 | $link->setPrivate($private); |
174 | $link->setTagsString($bkm['tags']); | 174 | $link->setTags($bkm['tags']); |
175 | 175 | ||
176 | $this->bookmarkService->addOrSet($link, false); | 176 | $this->bookmarkService->addOrSet($link, false); |
177 | $importCount++; | 177 | $importCount++; |
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index 1b2197c9..3ea55728 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Plugin; | 3 | namespace Shaarli\Plugin; |
3 | 4 | ||
4 | use Shaarli\Config\ConfigManager; | 5 | use Shaarli\Config\ConfigManager; |
@@ -23,7 +24,7 @@ class PluginManager | |||
23 | * | 24 | * |
24 | * @var array $loadedPlugins | 25 | * @var array $loadedPlugins |
25 | */ | 26 | */ |
26 | private $loadedPlugins = array(); | 27 | private $loadedPlugins = []; |
27 | 28 | ||
28 | /** | 29 | /** |
29 | * @var ConfigManager Configuration Manager instance. | 30 | * @var ConfigManager Configuration Manager instance. |
@@ -57,7 +58,7 @@ class PluginManager | |||
57 | public function __construct(&$conf) | 58 | public function __construct(&$conf) |
58 | { | 59 | { |
59 | $this->conf = $conf; | 60 | $this->conf = $conf; |
60 | $this->errors = array(); | 61 | $this->errors = []; |
61 | } | 62 | } |
62 | 63 | ||
63 | /** | 64 | /** |
@@ -98,12 +99,13 @@ class PluginManager | |||
98 | * | 99 | * |
99 | * @return void | 100 | * @return void |
100 | */ | 101 | */ |
101 | public function executeHooks($hook, &$data, $params = array()) | 102 | public function executeHooks($hook, &$data, $params = []) |
102 | { | 103 | { |
103 | $metadataParameters = [ | 104 | $metadataParameters = [ |
104 | 'target' => '_PAGE_', | 105 | 'target' => '_PAGE_', |
105 | 'loggedin' => '_LOGGEDIN_', | 106 | 'loggedin' => '_LOGGEDIN_', |
106 | 'basePath' => '_BASE_PATH_', | 107 | 'basePath' => '_BASE_PATH_', |
108 | 'rootPath' => '_ROOT_PATH_', | ||
107 | 'bookmarkService' => '_BOOKMARK_SERVICE_', | 109 | 'bookmarkService' => '_BOOKMARK_SERVICE_', |
108 | ]; | 110 | ]; |
109 | 111 | ||
@@ -195,7 +197,7 @@ class PluginManager | |||
195 | */ | 197 | */ |
196 | public function getPluginsMeta() | 198 | public function getPluginsMeta() |
197 | { | 199 | { |
198 | $metaData = array(); | 200 | $metaData = []; |
199 | $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK); | 201 | $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK); |
200 | 202 | ||
201 | // Browse all plugin directories. | 203 | // Browse all plugin directories. |
@@ -216,9 +218,9 @@ class PluginManager | |||
216 | if (isset($metaData[$plugin]['parameters'])) { | 218 | if (isset($metaData[$plugin]['parameters'])) { |
217 | $params = explode(';', $metaData[$plugin]['parameters']); | 219 | $params = explode(';', $metaData[$plugin]['parameters']); |
218 | } else { | 220 | } else { |
219 | $params = array(); | 221 | $params = []; |
220 | } | 222 | } |
221 | $metaData[$plugin]['parameters'] = array(); | 223 | $metaData[$plugin]['parameters'] = []; |
222 | foreach ($params as $param) { | 224 | foreach ($params as $param) { |
223 | if (empty($param)) { | 225 | if (empty($param)) { |
224 | continue; | 226 | continue; |
diff --git a/application/plugin/exception/PluginFileNotFoundException.php b/application/plugin/exception/PluginFileNotFoundException.php index e5386f02..21ac6604 100644 --- a/application/plugin/exception/PluginFileNotFoundException.php +++ b/application/plugin/exception/PluginFileNotFoundException.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Plugin\Exception; | 3 | namespace Shaarli\Plugin\Exception; |
3 | 4 | ||
4 | use Exception; | 5 | use Exception; |
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 41b357dd..bf0ae326 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php | |||
@@ -3,11 +3,11 @@ | |||
3 | namespace Shaarli\Render; | 3 | namespace Shaarli\Render; |
4 | 4 | ||
5 | use Exception; | 5 | use Exception; |
6 | use exceptions\MissingBasePathException; | 6 | use Psr\Log\LoggerInterface; |
7 | use RainTPL; | 7 | use RainTPL; |
8 | use Shaarli\ApplicationUtils; | ||
9 | use Shaarli\Bookmark\BookmarkServiceInterface; | 8 | use Shaarli\Bookmark\BookmarkServiceInterface; |
10 | use Shaarli\Config\ConfigManager; | 9 | use Shaarli\Config\ConfigManager; |
10 | use Shaarli\Helper\ApplicationUtils; | ||
11 | use Shaarli\Security\SessionManager; | 11 | use Shaarli\Security\SessionManager; |
12 | use Shaarli\Thumbnailer; | 12 | use Shaarli\Thumbnailer; |
13 | 13 | ||
@@ -35,6 +35,9 @@ class PageBuilder | |||
35 | */ | 35 | */ |
36 | protected $session; | 36 | protected $session; |
37 | 37 | ||
38 | /** @var LoggerInterface */ | ||
39 | protected $logger; | ||
40 | |||
38 | /** | 41 | /** |
39 | * @var BookmarkServiceInterface $bookmarkService instance. | 42 | * @var BookmarkServiceInterface $bookmarkService instance. |
40 | */ | 43 | */ |
@@ -54,17 +57,25 @@ class PageBuilder | |||
54 | * PageBuilder constructor. | 57 | * PageBuilder constructor. |
55 | * $tpl is initialized at false for lazy loading. | 58 | * $tpl is initialized at false for lazy loading. |
56 | * | 59 | * |
57 | * @param ConfigManager $conf Configuration Manager instance (reference). | 60 | * @param ConfigManager $conf Configuration Manager instance (reference). |
58 | * @param array $session $_SESSION array | 61 | * @param array $session $_SESSION array |
59 | * @param BookmarkServiceInterface $linkDB instance. | 62 | * @param LoggerInterface $logger |
60 | * @param string $token Session token | 63 | * @param null $linkDB instance. |
61 | * @param bool $isLoggedIn | 64 | * @param null $token Session token |
65 | * @param bool $isLoggedIn | ||
62 | */ | 66 | */ |
63 | public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) | 67 | public function __construct( |
64 | { | 68 | ConfigManager &$conf, |
69 | array $session, | ||
70 | LoggerInterface $logger, | ||
71 | $linkDB = null, | ||
72 | $token = null, | ||
73 | $isLoggedIn = false | ||
74 | ) { | ||
65 | $this->tpl = false; | 75 | $this->tpl = false; |
66 | $this->conf = $conf; | 76 | $this->conf = $conf; |
67 | $this->session = $session; | 77 | $this->session = $session; |
78 | $this->logger = $logger; | ||
68 | $this->bookmarkService = $linkDB; | 79 | $this->bookmarkService = $linkDB; |
69 | $this->token = $token; | 80 | $this->token = $token; |
70 | $this->isLoggedIn = $isLoggedIn; | 81 | $this->isLoggedIn = $isLoggedIn; |
@@ -98,7 +109,7 @@ class PageBuilder | |||
98 | $this->tpl->assign('newVersion', escape($version)); | 109 | $this->tpl->assign('newVersion', escape($version)); |
99 | $this->tpl->assign('versionError', ''); | 110 | $this->tpl->assign('versionError', ''); |
100 | } catch (Exception $exc) { | 111 | } catch (Exception $exc) { |
101 | logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); | 112 | $this->logger->error(format_log('Error: ' . $exc->getMessage(), client_ip_id($_SERVER))); |
102 | $this->tpl->assign('newVersion', ''); | 113 | $this->tpl->assign('newVersion', ''); |
103 | $this->tpl->assign('versionError', escape($exc->getMessage())); | 114 | $this->tpl->assign('versionError', escape($exc->getMessage())); |
104 | } | 115 | } |
@@ -149,7 +160,8 @@ class PageBuilder | |||
149 | 160 | ||
150 | $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); | 161 | $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); |
151 | 162 | ||
152 | $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE']); | 163 | $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20); |
164 | $this->tpl->assign('tags_separator', $this->conf->get('general.tags_separator', ' ')); | ||
153 | 165 | ||
154 | // To be removed with a proper theme configuration. | 166 | // To be removed with a proper theme configuration. |
155 | $this->tpl->assign('conf', $this->conf); | 167 | $this->tpl->assign('conf', $this->conf); |
@@ -174,10 +186,12 @@ class PageBuilder | |||
174 | } | 186 | } |
175 | } | 187 | } |
176 | 188 | ||
189 | $rootPath = preg_replace('#/index\.php$#', '', $basePath); | ||
177 | $this->assign('base_path', $basePath); | 190 | $this->assign('base_path', $basePath); |
191 | $this->assign('root_path', $rootPath); | ||
178 | $this->assign( | 192 | $this->assign( |
179 | 'asset_path', | 193 | 'asset_path', |
180 | $basePath . '/' . | 194 | $rootPath . '/' . |
181 | rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . | 195 | rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . |
182 | $this->conf->get('resource.theme', 'default') | 196 | $this->conf->get('resource.theme', 'default') |
183 | ); | 197 | ); |
diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php index 8af8228a..03b424f3 100644 --- a/application/render/TemplatePage.php +++ b/application/render/TemplatePage.php | |||
@@ -14,6 +14,7 @@ interface TemplatePage | |||
14 | public const DAILY = 'daily'; | 14 | public const DAILY = 'daily'; |
15 | public const DAILY_RSS = 'dailyrss'; | 15 | public const DAILY_RSS = 'dailyrss'; |
16 | public const EDIT_LINK = 'editlink'; | 16 | public const EDIT_LINK = 'editlink'; |
17 | public const EDIT_LINK_BATCH = 'editlink.batch'; | ||
17 | public const ERROR = 'error'; | 18 | public const ERROR = 'error'; |
18 | public const EXPORT = 'export'; | 19 | public const EXPORT = 'export'; |
19 | public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks'; | 20 | public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks'; |
diff --git a/application/render/ThemeUtils.php b/application/render/ThemeUtils.php index 86096c64..18471f0a 100644 --- a/application/render/ThemeUtils.php +++ b/application/render/ThemeUtils.php | |||
@@ -23,10 +23,10 @@ class ThemeUtils | |||
23 | public static function getThemes($tplDir) | 23 | public static function getThemes($tplDir) |
24 | { | 24 | { |
25 | $tplDir = rtrim($tplDir, '/'); | 25 | $tplDir = rtrim($tplDir, '/'); |
26 | $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR); | 26 | $allTheme = glob($tplDir . '/*', GLOB_ONLYDIR); |
27 | $themes = []; | 27 | $themes = []; |
28 | foreach ($allTheme as $value) { | 28 | foreach ($allTheme as $value) { |
29 | $themes[] = str_replace($tplDir.'/', '', $value); | 29 | $themes[] = str_replace($tplDir . '/', '', $value); |
30 | } | 30 | } |
31 | 31 | ||
32 | return $themes; | 32 | return $themes; |
diff --git a/application/security/BanManager.php b/application/security/BanManager.php index 68190c54..7077af5b 100644 --- a/application/security/BanManager.php +++ b/application/security/BanManager.php | |||
@@ -1,9 +1,9 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | |||
4 | namespace Shaarli\Security; | 3 | namespace Shaarli\Security; |
5 | 4 | ||
6 | use Shaarli\FileUtils; | 5 | use Psr\Log\LoggerInterface; |
6 | use Shaarli\Helper\FileUtils; | ||
7 | 7 | ||
8 | /** | 8 | /** |
9 | * Class BanManager | 9 | * Class BanManager |
@@ -28,8 +28,8 @@ class BanManager | |||
28 | /** @var string Path to the file containing IP bans and failures */ | 28 | /** @var string Path to the file containing IP bans and failures */ |
29 | protected $banFile; | 29 | protected $banFile; |
30 | 30 | ||
31 | /** @var string Path to the log file, used to log bans */ | 31 | /** @var LoggerInterface Path to the log file, used to log bans */ |
32 | protected $logFile; | 32 | protected $logger; |
33 | 33 | ||
34 | /** @var array List of IP with their associated number of failed attempts */ | 34 | /** @var array List of IP with their associated number of failed attempts */ |
35 | protected $failures = []; | 35 | protected $failures = []; |
@@ -40,18 +40,20 @@ class BanManager | |||
40 | /** | 40 | /** |
41 | * BanManager constructor. | 41 | * BanManager constructor. |
42 | * | 42 | * |
43 | * @param array $trustedProxies List of allowed proxies IP | 43 | * @param array $trustedProxies List of allowed proxies IP |
44 | * @param int $nbAttempts Number of allowed failed attempt before the ban | 44 | * @param int $nbAttempts Number of allowed failed attempt before the ban |
45 | * @param int $banDuration Ban duration in seconds | 45 | * @param int $banDuration Ban duration in seconds |
46 | * @param string $banFile Path to the file containing IP bans and failures | 46 | * @param string $banFile Path to the file containing IP bans and failures |
47 | * @param string $logFile Path to the log file, used to log bans | 47 | * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory |
48 | */ | 48 | */ |
49 | public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) { | 49 | public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger) |
50 | { | ||
50 | $this->trustedProxies = $trustedProxies; | 51 | $this->trustedProxies = $trustedProxies; |
51 | $this->nbAttempts = $nbAttempts; | 52 | $this->nbAttempts = $nbAttempts; |
52 | $this->banDuration = $banDuration; | 53 | $this->banDuration = $banDuration; |
53 | $this->banFile = $banFile; | 54 | $this->banFile = $banFile; |
54 | $this->logFile = $logFile; | 55 | $this->logger = $logger; |
56 | |||
55 | $this->readBanFile(); | 57 | $this->readBanFile(); |
56 | } | 58 | } |
57 | 59 | ||
@@ -78,11 +80,7 @@ class BanManager | |||
78 | 80 | ||
79 | if ($this->failures[$ip] >= $this->nbAttempts) { | 81 | if ($this->failures[$ip] >= $this->nbAttempts) { |
80 | $this->bans[$ip] = time() + $this->banDuration; | 82 | $this->bans[$ip] = time() + $this->banDuration; |
81 | logm( | 83 | $this->logger->info(format_log('IP address banned from login: ' . $ip, $ip)); |
82 | $this->logFile, | ||
83 | $server['REMOTE_ADDR'], | ||
84 | 'IP address banned from login: '. $ip | ||
85 | ); | ||
86 | } | 84 | } |
87 | $this->writeBanFile(); | 85 | $this->writeBanFile(); |
88 | } | 86 | } |
@@ -138,7 +136,7 @@ class BanManager | |||
138 | unset($this->failures[$ip]); | 136 | unset($this->failures[$ip]); |
139 | } | 137 | } |
140 | unset($this->bans[$ip]); | 138 | unset($this->bans[$ip]); |
141 | logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); | 139 | $this->logger->info(format_log('Ban lifted for: ' . $ip, $ip)); |
142 | 140 | ||
143 | $this->writeBanFile(); | 141 | $this->writeBanFile(); |
144 | return false; | 142 | return false; |
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index d74c3118..b795b80e 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php | |||
@@ -1,7 +1,9 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Security; | 3 | namespace Shaarli\Security; |
3 | 4 | ||
4 | use Exception; | 5 | use Exception; |
6 | use Psr\Log\LoggerInterface; | ||
5 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
6 | 8 | ||
7 | /** | 9 | /** |
@@ -31,26 +33,30 @@ class LoginManager | |||
31 | protected $staySignedInToken = ''; | 33 | protected $staySignedInToken = ''; |
32 | /** @var CookieManager */ | 34 | /** @var CookieManager */ |
33 | protected $cookieManager; | 35 | protected $cookieManager; |
36 | /** @var LoggerInterface */ | ||
37 | protected $logger; | ||
34 | 38 | ||
35 | /** | 39 | /** |
36 | * Constructor | 40 | * Constructor |
37 | * | 41 | * |
38 | * @param ConfigManager $configManager Configuration Manager instance | 42 | * @param ConfigManager $configManager Configuration Manager instance |
39 | * @param SessionManager $sessionManager SessionManager instance | 43 | * @param SessionManager $sessionManager SessionManager instance |
40 | * @param CookieManager $cookieManager CookieManager instance | 44 | * @param CookieManager $cookieManager CookieManager instance |
45 | * @param BanManager $banManager | ||
46 | * @param LoggerInterface $logger Used to log login attempts | ||
41 | */ | 47 | */ |
42 | public function __construct($configManager, $sessionManager, $cookieManager) | 48 | public function __construct( |
43 | { | 49 | ConfigManager $configManager, |
50 | SessionManager $sessionManager, | ||
51 | CookieManager $cookieManager, | ||
52 | BanManager $banManager, | ||
53 | LoggerInterface $logger | ||
54 | ) { | ||
44 | $this->configManager = $configManager; | 55 | $this->configManager = $configManager; |
45 | $this->sessionManager = $sessionManager; | 56 | $this->sessionManager = $sessionManager; |
46 | $this->cookieManager = $cookieManager; | 57 | $this->cookieManager = $cookieManager; |
47 | $this->banManager = new BanManager( | 58 | $this->banManager = $banManager; |
48 | $this->configManager->get('security.trusted_proxies', []), | 59 | $this->logger = $logger; |
49 | $this->configManager->get('security.ban_after'), | ||
50 | $this->configManager->get('security.ban_duration'), | ||
51 | $this->configManager->get('resource.ban_file', 'data/ipbans.php'), | ||
52 | $this->configManager->get('resource.log') | ||
53 | ); | ||
54 | 60 | ||
55 | if ($this->configManager->get('security.open_shaarli') === true) { | 61 | if ($this->configManager->get('security.open_shaarli') === true) { |
56 | $this->openShaarli = true; | 62 | $this->openShaarli = true; |
@@ -101,7 +107,8 @@ class LoginManager | |||
101 | // The user client has a valid stay-signed-in cookie | 107 | // The user client has a valid stay-signed-in cookie |
102 | // Session information is updated with the current client information | 108 | // Session information is updated with the current client information |
103 | $this->sessionManager->storeLoginInfo($clientIpId); | 109 | $this->sessionManager->storeLoginInfo($clientIpId); |
104 | } elseif ($this->sessionManager->hasSessionExpired() | 110 | } elseif ( |
111 | $this->sessionManager->hasSessionExpired() | ||
105 | || $this->sessionManager->hasClientIpChanged($clientIpId) | 112 | || $this->sessionManager->hasClientIpChanged($clientIpId) |
106 | ) { | 113 | ) { |
107 | $this->sessionManager->logout(); | 114 | $this->sessionManager->logout(); |
@@ -118,7 +125,7 @@ class LoginManager | |||
118 | * | 125 | * |
119 | * @return true when the user is logged in, false otherwise | 126 | * @return true when the user is logged in, false otherwise |
120 | */ | 127 | */ |
121 | public function isLoggedIn() | 128 | public function isLoggedIn(): bool |
122 | { | 129 | { |
123 | if ($this->openShaarli) { | 130 | if ($this->openShaarli) { |
124 | return true; | 131 | return true; |
@@ -129,48 +136,35 @@ class LoginManager | |||
129 | /** | 136 | /** |
130 | * Check user credentials are valid | 137 | * Check user credentials are valid |
131 | * | 138 | * |
132 | * @param string $remoteIp Remote client IP address | ||
133 | * @param string $clientIpId Client IP address identifier | 139 | * @param string $clientIpId Client IP address identifier |
134 | * @param string $login Username | 140 | * @param string $login Username |
135 | * @param string $password Password | 141 | * @param string $password Password |
136 | * | 142 | * |
137 | * @return bool true if the provided credentials are valid, false otherwise | 143 | * @return bool true if the provided credentials are valid, false otherwise |
138 | */ | 144 | */ |
139 | public function checkCredentials($remoteIp, $clientIpId, $login, $password) | 145 | public function checkCredentials($clientIpId, $login, $password) |
140 | { | 146 | { |
141 | // Check login matches config | ||
142 | if ($login !== $this->configManager->get('credentials.login')) { | ||
143 | return false; | ||
144 | } | ||
145 | |||
146 | // Check credentials | 147 | // Check credentials |
147 | try { | 148 | try { |
148 | $useLdapLogin = !empty($this->configManager->get('ldap.host')); | 149 | $useLdapLogin = !empty($this->configManager->get('ldap.host')); |
149 | if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) | 150 | if ( |
150 | || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) | 151 | $login === $this->configManager->get('credentials.login') |
152 | && ( | ||
153 | (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) | ||
154 | || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) | ||
155 | ) | ||
151 | ) { | 156 | ) { |
152 | $this->sessionManager->storeLoginInfo($clientIpId); | 157 | $this->sessionManager->storeLoginInfo($clientIpId); |
153 | logm( | 158 | $this->logger->info(format_log('Login successful', $clientIpId)); |
154 | $this->configManager->get('resource.log'), | 159 | |
155 | $remoteIp, | 160 | return true; |
156 | 'Login successful' | ||
157 | ); | ||
158 | return true; | ||
159 | } | 161 | } |
160 | } | 162 | } catch (Exception $exception) { |
161 | catch(Exception $exception) { | 163 | $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId)); |
162 | logm( | ||
163 | $this->configManager->get('resource.log'), | ||
164 | $remoteIp, | ||
165 | 'Exception while checking credentials: ' . $exception | ||
166 | ); | ||
167 | } | 164 | } |
168 | 165 | ||
169 | logm( | 166 | $this->logger->info(format_log('Login failed for user ' . $login, $clientIpId)); |
170 | $this->configManager->get('resource.log'), | 167 | |
171 | $remoteIp, | ||
172 | 'Login failed for user ' . $login | ||
173 | ); | ||
174 | return false; | 168 | return false; |
175 | } | 169 | } |
176 | 170 | ||
@@ -183,7 +177,8 @@ class LoginManager | |||
183 | * | 177 | * |
184 | * @return bool true if the provided credentials are valid, false otherwise | 178 | * @return bool true if the provided credentials are valid, false otherwise |
185 | */ | 179 | */ |
186 | public function checkCredentialsFromLocalConfig($login, $password) { | 180 | public function checkCredentialsFromLocalConfig($login, $password) |
181 | { | ||
187 | $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); | 182 | $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); |
188 | 183 | ||
189 | return $login == $this->configManager->get('credentials.login') | 184 | return $login == $this->configManager->get('credentials.login') |
@@ -202,14 +197,14 @@ class LoginManager | |||
202 | */ | 197 | */ |
203 | public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null) | 198 | public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null) |
204 | { | 199 | { |
205 | $connect = $connect ?? function($host) { | 200 | $connect = $connect ?? function ($host) { |
206 | $resource = ldap_connect($host); | 201 | $resource = ldap_connect($host); |
207 | 202 | ||
208 | ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3); | 203 | ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3); |
209 | 204 | ||
210 | return $resource; | 205 | return $resource; |
211 | }; | 206 | }; |
212 | $bind = $bind ?? function($handle, $dn, $password) { | 207 | $bind = $bind ?? function ($handle, $dn, $password) { |
213 | return ldap_bind($handle, $dn, $password); | 208 | return ldap_bind($handle, $dn, $password); |
214 | }; | 209 | }; |
215 | 210 | ||
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 36df8c1c..f957b91a 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Security; | 3 | namespace Shaarli\Security; |
3 | 4 | ||
4 | use Shaarli\Config\ConfigManager; | 5 | use Shaarli\Config\ConfigManager; |
@@ -79,7 +80,7 @@ class SessionManager | |||
79 | */ | 80 | */ |
80 | public function generateToken() | 81 | public function generateToken() |
81 | { | 82 | { |
82 | $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt')); | 83 | $token = sha1(uniqid('', true) . '_' . mt_rand() . $this->conf->get('credentials.salt')); |
83 | $this->session['tokens'][$token] = 1; | 84 | $this->session['tokens'][$token] = 1; |
84 | return $token; | 85 | return $token; |
85 | } | 86 | } |
@@ -293,9 +294,12 @@ class SessionManager | |||
293 | return session_start(); | 294 | return session_start(); |
294 | } | 295 | } |
295 | 296 | ||
296 | public function cookieParameters(int $lifeTime, string $path, string $domain): bool | 297 | /** |
298 | * Be careful, return type of session_set_cookie_params() changed between PHP 7.1 and 7.2. | ||
299 | */ | ||
300 | public function cookieParameters(int $lifeTime, string $path, string $domain): void | ||
297 | { | 301 | { |
298 | return session_set_cookie_params($lifeTime, $path, $domain); | 302 | session_set_cookie_params($lifeTime, $path, $domain); |
299 | } | 303 | } |
300 | 304 | ||
301 | public function regenerateId(bool $deleteOldSession = false): bool | 305 | public function regenerateId(bool $deleteOldSession = false): bool |
diff --git a/application/updater/Updater.php b/application/updater/Updater.php index 88a7bc7b..4f557d0f 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php | |||
@@ -88,7 +88,8 @@ class Updater | |||
88 | 88 | ||
89 | foreach ($this->methods as $method) { | 89 | foreach ($this->methods as $method) { |
90 | // Not an update method or already done, pass. | 90 | // Not an update method or already done, pass. |
91 | if (! startsWith($method->getName(), 'updateMethod') | 91 | if ( |
92 | ! startsWith($method->getName(), 'updateMethod') | ||
92 | || in_array($method->getName(), $this->doneUpdates) | 93 | || in_array($method->getName(), $this->doneUpdates) |
93 | ) { | 94 | ) { |
94 | continue; | 95 | continue; |
@@ -121,12 +122,12 @@ class Updater | |||
121 | 122 | ||
122 | public function readUpdates(string $updatesFilepath): array | 123 | public function readUpdates(string $updatesFilepath): array |
123 | { | 124 | { |
124 | return UpdaterUtils::read_updates_file($updatesFilepath); | 125 | return UpdaterUtils::readUpdatesFile($updatesFilepath); |
125 | } | 126 | } |
126 | 127 | ||
127 | public function writeUpdates(string $updatesFilepath, array $updates): void | 128 | public function writeUpdates(string $updatesFilepath, array $updates): void |
128 | { | 129 | { |
129 | UpdaterUtils::write_updates_file($updatesFilepath, $updates); | 130 | UpdaterUtils::writeUpdatesFile($updatesFilepath, $updates); |
130 | } | 131 | } |
131 | 132 | ||
132 | /** | 133 | /** |
@@ -152,7 +153,8 @@ class Updater | |||
152 | $updated = false; | 153 | $updated = false; |
153 | 154 | ||
154 | foreach ($this->bookmarkService->search() as $bookmark) { | 155 | foreach ($this->bookmarkService->search() as $bookmark) { |
155 | if ($bookmark->isNote() | 156 | if ( |
157 | $bookmark->isNote() | ||
156 | && startsWith($bookmark->getUrl(), '?') | 158 | && startsWith($bookmark->getUrl(), '?') |
157 | && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) | 159 | && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) |
158 | ) { | 160 | ) { |
diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php index 828a49fc..206f826e 100644 --- a/application/updater/UpdaterUtils.php +++ b/application/updater/UpdaterUtils.php | |||
@@ -11,7 +11,7 @@ class UpdaterUtils | |||
11 | * | 11 | * |
12 | * @return array Already done update methods. | 12 | * @return array Already done update methods. |
13 | */ | 13 | */ |
14 | public static function read_updates_file($updatesFilepath) | 14 | public static function readUpdatesFile($updatesFilepath) |
15 | { | 15 | { |
16 | if (! empty($updatesFilepath) && is_file($updatesFilepath)) { | 16 | if (! empty($updatesFilepath) && is_file($updatesFilepath)) { |
17 | $content = file_get_contents($updatesFilepath); | 17 | $content = file_get_contents($updatesFilepath); |
@@ -19,7 +19,7 @@ class UpdaterUtils | |||
19 | return explode(';', $content); | 19 | return explode(';', $content); |
20 | } | 20 | } |
21 | } | 21 | } |
22 | return array(); | 22 | return []; |
23 | } | 23 | } |
24 | 24 | ||
25 | /** | 25 | /** |
@@ -30,7 +30,7 @@ class UpdaterUtils | |||
30 | * | 30 | * |
31 | * @throws \Exception Couldn't write version number. | 31 | * @throws \Exception Couldn't write version number. |
32 | */ | 32 | */ |
33 | public static function write_updates_file($updatesFilepath, $updates) | 33 | public static function writeUpdatesFile($updatesFilepath, $updates) |
34 | { | 34 | { |
35 | if (empty($updatesFilepath)) { | 35 | if (empty($updatesFilepath)) { |
36 | throw new \Exception('Updates file path is not set, can\'t write updates.'); | 36 | throw new \Exception('Updates file path is not set, can\'t write updates.'); |
@@ -38,7 +38,7 @@ class UpdaterUtils | |||
38 | 38 | ||
39 | $res = file_put_contents($updatesFilepath, implode(';', $updates)); | 39 | $res = file_put_contents($updatesFilepath, implode(';', $updates)); |
40 | if ($res === false) { | 40 | if ($res === false) { |
41 | throw new \Exception('Unable to write updates in '. $updatesFilepath . '.'); | 41 | throw new \Exception('Unable to write updates in ' . $updatesFilepath . '.'); |
42 | } | 42 | } |
43 | } | 43 | } |
44 | } | 44 | } |
diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js new file mode 100644 index 00000000..d5a28a35 --- /dev/null +++ b/assets/common/js/metadata.js | |||
@@ -0,0 +1,107 @@ | |||
1 | import he from 'he'; | ||
2 | |||
3 | /** | ||
4 | * This script is used to retrieve bookmarks metadata asynchronously: | ||
5 | * - title, description and keywords while creating a new bookmark | ||
6 | * - thumbnails while visiting the bookmark list | ||
7 | * | ||
8 | * Note: it should only be included if the user is logged in | ||
9 | * and the setting general.enable_async_metadata is enabled. | ||
10 | */ | ||
11 | |||
12 | /** | ||
13 | * Removes given input loaders - used in edit link template. | ||
14 | * | ||
15 | * @param {object} loaders List of input DOM element that need to be cleared | ||
16 | */ | ||
17 | function clearLoaders(loaders) { | ||
18 | if (loaders != null && loaders.length > 0) { | ||
19 | [...loaders].forEach((loader) => { | ||
20 | loader.classList.remove('loading-input'); | ||
21 | }); | ||
22 | } | ||
23 | } | ||
24 | |||
25 | /** | ||
26 | * AJAX request to update the thumbnail of a bookmark with the provided ID. | ||
27 | * If a thumbnail is retrieved, it updates the divElement with the image src, and displays it. | ||
28 | * | ||
29 | * @param {string} basePath Shaarli subfolder for XHR requests | ||
30 | * @param {object} divElement Main <div> DOM element containing the thumbnail placeholder | ||
31 | * @param {int} id Bookmark ID to update | ||
32 | */ | ||
33 | function updateThumb(basePath, divElement, id) { | ||
34 | const xhr = new XMLHttpRequest(); | ||
35 | xhr.open('PATCH', `${basePath}/admin/shaare/${id}/update-thumbnail`); | ||
36 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); | ||
37 | xhr.responseType = 'json'; | ||
38 | xhr.onload = () => { | ||
39 | if (xhr.status !== 200) { | ||
40 | alert(`An error occurred. Return code: ${xhr.status}`); | ||
41 | } else { | ||
42 | const { response } = xhr; | ||
43 | |||
44 | if (response.thumbnail !== false) { | ||
45 | const imgElement = divElement.querySelector('img'); | ||
46 | |||
47 | imgElement.src = response.thumbnail; | ||
48 | imgElement.dataset.src = response.thumbnail; | ||
49 | imgElement.style.opacity = '1'; | ||
50 | divElement.classList.remove('hidden'); | ||
51 | } | ||
52 | } | ||
53 | }; | ||
54 | xhr.send(); | ||
55 | } | ||
56 | |||
57 | (() => { | ||
58 | const basePath = document.querySelector('input[name="js_base_path"]').value; | ||
59 | |||
60 | /* | ||
61 | * METADATA FOR EDIT BOOKMARK PAGE | ||
62 | */ | ||
63 | const inputTitles = document.querySelectorAll('input[name="lf_title"]'); | ||
64 | if (inputTitles != null) { | ||
65 | [...inputTitles].forEach((inputTitle) => { | ||
66 | const form = inputTitle.closest('form[name="linkform"]'); | ||
67 | const loaders = form.querySelectorAll('.loading-input'); | ||
68 | |||
69 | if (inputTitle.value.length > 0) { | ||
70 | clearLoaders(loaders); | ||
71 | return; | ||
72 | } | ||
73 | |||
74 | const url = form.querySelector('input[name="lf_url"]').value; | ||
75 | |||
76 | const xhr = new XMLHttpRequest(); | ||
77 | xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); | ||
78 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); | ||
79 | xhr.onload = () => { | ||
80 | const result = JSON.parse(xhr.response); | ||
81 | Object.keys(result).forEach((key) => { | ||
82 | if (result[key] !== null && result[key].length) { | ||
83 | const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); | ||
84 | if (element != null && element.value.length === 0) { | ||
85 | element.value = he.decode(result[key]); | ||
86 | } | ||
87 | } | ||
88 | }); | ||
89 | clearLoaders(loaders); | ||
90 | }; | ||
91 | |||
92 | xhr.send(); | ||
93 | }); | ||
94 | } | ||
95 | |||
96 | /* | ||
97 | * METADATA FOR THUMBNAIL RETRIEVAL | ||
98 | */ | ||
99 | const thumbsToLoad = document.querySelectorAll('div[data-async-thumbnail]'); | ||
100 | if (thumbsToLoad != null) { | ||
101 | [...thumbsToLoad].forEach((divElement) => { | ||
102 | const { id } = divElement.closest('[data-id]').dataset; | ||
103 | |||
104 | updateThumb(basePath, divElement, id); | ||
105 | }); | ||
106 | } | ||
107 | })(); | ||
diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js new file mode 100644 index 00000000..557325ee --- /dev/null +++ b/assets/common/js/shaare-batch.js | |||
@@ -0,0 +1,121 @@ | |||
1 | const sendBookmarkForm = (basePath, formElement) => { | ||
2 | const inputs = formElement | ||
3 | .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]'); | ||
4 | |||
5 | const formData = new FormData(); | ||
6 | [...inputs].forEach((input) => { | ||
7 | formData.append(input.getAttribute('name'), input.value); | ||
8 | }); | ||
9 | |||
10 | return new Promise((resolve, reject) => { | ||
11 | const xhr = new XMLHttpRequest(); | ||
12 | xhr.open('POST', `${basePath}/admin/shaare`); | ||
13 | xhr.onload = () => { | ||
14 | if (xhr.status !== 200) { | ||
15 | alert(`An error occurred. Return code: ${xhr.status}`); | ||
16 | reject(); | ||
17 | } else { | ||
18 | formElement.closest('.edit-link-container').remove(); | ||
19 | resolve(); | ||
20 | } | ||
21 | }; | ||
22 | xhr.send(formData); | ||
23 | }); | ||
24 | }; | ||
25 | |||
26 | const sendBookmarkDelete = (buttonElement, formElement) => ( | ||
27 | new Promise((resolve, reject) => { | ||
28 | const xhr = new XMLHttpRequest(); | ||
29 | xhr.open('GET', buttonElement.href); | ||
30 | xhr.onload = () => { | ||
31 | if (xhr.status !== 200) { | ||
32 | alert(`An error occurred. Return code: ${xhr.status}`); | ||
33 | reject(); | ||
34 | } else { | ||
35 | formElement.closest('.edit-link-container').remove(); | ||
36 | resolve(); | ||
37 | } | ||
38 | }; | ||
39 | xhr.send(); | ||
40 | }) | ||
41 | ); | ||
42 | |||
43 | const redirectIfEmptyBatch = (basePath, formElements, path) => { | ||
44 | if (formElements == null || formElements.length === 0) { | ||
45 | window.location.href = `${basePath}${path}`; | ||
46 | } | ||
47 | }; | ||
48 | |||
49 | (() => { | ||
50 | const basePath = document.querySelector('input[name="js_base_path"]').value; | ||
51 | const getForms = () => document.querySelectorAll('form[name="linkform"]'); | ||
52 | |||
53 | const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]'); | ||
54 | if (cancelButtons != null) { | ||
55 | [...cancelButtons].forEach((cancelButton) => { | ||
56 | cancelButton.addEventListener('click', (e) => { | ||
57 | e.preventDefault(); | ||
58 | e.target.closest('form[name="linkform"]').remove(); | ||
59 | redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare'); | ||
60 | }); | ||
61 | }); | ||
62 | } | ||
63 | |||
64 | const saveButtons = document.querySelectorAll('[name="save_edit"]'); | ||
65 | if (saveButtons != null) { | ||
66 | [...saveButtons].forEach((saveButton) => { | ||
67 | saveButton.addEventListener('click', (e) => { | ||
68 | e.preventDefault(); | ||
69 | |||
70 | const formElement = e.target.closest('form[name="linkform"]'); | ||
71 | sendBookmarkForm(basePath, formElement) | ||
72 | .then(() => redirectIfEmptyBatch(basePath, getForms(), '/')); | ||
73 | }); | ||
74 | }); | ||
75 | } | ||
76 | |||
77 | const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]'); | ||
78 | if (saveAllButtons != null) { | ||
79 | [...saveAllButtons].forEach((saveAllButton) => { | ||
80 | saveAllButton.addEventListener('click', (e) => { | ||
81 | e.preventDefault(); | ||
82 | |||
83 | const forms = [...getForms()]; | ||
84 | const nbForm = forms.length; | ||
85 | let current = 0; | ||
86 | const progressBar = document.querySelector('.progressbar > div'); | ||
87 | const progressBarCurrent = document.querySelector('.progressbar-current'); | ||
88 | |||
89 | document.querySelector('.dark-layer').style.display = 'block'; | ||
90 | document.querySelector('.progressbar-max').innerHTML = nbForm; | ||
91 | progressBarCurrent.innerHTML = current; | ||
92 | |||
93 | const promises = []; | ||
94 | forms.forEach((formElement) => { | ||
95 | promises.push(sendBookmarkForm(basePath, formElement).then(() => { | ||
96 | current += 1; | ||
97 | progressBar.style.width = `${(current * 100) / nbForm}%`; | ||
98 | progressBarCurrent.innerHTML = current; | ||
99 | })); | ||
100 | }); | ||
101 | |||
102 | Promise.all(promises).then(() => { | ||
103 | window.location.href = basePath || '/'; | ||
104 | }); | ||
105 | }); | ||
106 | }); | ||
107 | } | ||
108 | |||
109 | const deleteButtons = document.querySelectorAll('[name="delete_link"]'); | ||
110 | if (deleteButtons != null) { | ||
111 | [...deleteButtons].forEach((deleteButton) => { | ||
112 | deleteButton.addEventListener('click', (e) => { | ||
113 | e.preventDefault(); | ||
114 | |||
115 | const formElement = e.target.closest('form[name="linkform"]'); | ||
116 | sendBookmarkDelete(e.target, formElement) | ||
117 | .then(() => redirectIfEmptyBatch(basePath, getForms(), '/')); | ||
118 | }); | ||
119 | }); | ||
120 | } | ||
121 | })(); | ||
diff --git a/assets/default/js/base.js b/assets/default/js/base.js index be986ae0..dd532bb7 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js | |||
@@ -1,4 +1,5 @@ | |||
1 | import Awesomplete from 'awesomplete'; | 1 | import Awesomplete from 'awesomplete'; |
2 | import he from 'he'; | ||
2 | 3 | ||
3 | /** | 4 | /** |
4 | * Find a parent element according to its tag and its attributes | 5 | * Find a parent element according to its tag and its attributes |
@@ -41,19 +42,21 @@ function refreshToken(basePath, callback) { | |||
41 | xhr.send(); | 42 | xhr.send(); |
42 | } | 43 | } |
43 | 44 | ||
44 | function createAwesompleteInstance(element, tags = []) { | 45 | function createAwesompleteInstance(element, separator, tags = []) { |
45 | const awesome = new Awesomplete(Awesomplete.$(element)); | 46 | const awesome = new Awesomplete(Awesomplete.$(element)); |
46 | // Tags are separated by a space | 47 | |
47 | awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]); | 48 | // Tags are separated by separator |
49 | awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(new RegExp(`[^${separator}]*$`))[0]); | ||
48 | // Insert new selected tag in the input | 50 | // Insert new selected tag in the input |
49 | awesome.replace = (text) => { | 51 | awesome.replace = (text) => { |
50 | const before = awesome.input.value.match(/^.+ \s*|/)[0]; | 52 | const before = awesome.input.value.match(new RegExp(`^.+${separator}+|`))[0]; |
51 | awesome.input.value = `${before}${text} `; | 53 | awesome.input.value = `${before}${text}${separator}`; |
52 | }; | 54 | }; |
53 | // Highlight found items | 55 | // Highlight found items |
54 | awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(/[^ ]*$/)[0]); | 56 | awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${separator}]*$`))[0]); |
55 | // Don't display already selected items | 57 | // Don't display already selected items |
56 | const reg = /(\w+) /g; | 58 | // WARNING: pseudo classes does not seem to work with string litterals... |
59 | const reg = new RegExp(`([^${separator}]+)${separator}`, 'g'); | ||
57 | let match; | 60 | let match; |
58 | awesome.data = (item, input) => { | 61 | awesome.data = (item, input) => { |
59 | while ((match = reg.exec(input))) { | 62 | while ((match = reg.exec(input))) { |
@@ -77,13 +80,14 @@ function createAwesompleteInstance(element, tags = []) { | |||
77 | * @param selector CSS selector | 80 | * @param selector CSS selector |
78 | * @param tags Array of tags | 81 | * @param tags Array of tags |
79 | * @param instances List of existing awesomplete instances | 82 | * @param instances List of existing awesomplete instances |
83 | * @param separator Tags separator character | ||
80 | */ | 84 | */ |
81 | function updateAwesompleteList(selector, tags, instances) { | 85 | function updateAwesompleteList(selector, tags, instances, separator) { |
82 | if (instances.length === 0) { | 86 | if (instances.length === 0) { |
83 | // First load: create Awesomplete instances | 87 | // First load: create Awesomplete instances |
84 | const elements = document.querySelectorAll(selector); | 88 | const elements = document.querySelectorAll(selector); |
85 | [...elements].forEach((element) => { | 89 | [...elements].forEach((element) => { |
86 | instances.push(createAwesompleteInstance(element, tags)); | 90 | instances.push(createAwesompleteInstance(element, separator, tags)); |
87 | }); | 91 | }); |
88 | } else { | 92 | } else { |
89 | // Update awesomplete tag list | 93 | // Update awesomplete tag list |
@@ -96,15 +100,6 @@ function updateAwesompleteList(selector, tags, instances) { | |||
96 | } | 100 | } |
97 | 101 | ||
98 | /** | 102 | /** |
99 | * html_entities in JS | ||
100 | * | ||
101 | * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript | ||
102 | */ | ||
103 | function htmlEntities(str) { | ||
104 | return str.replace(/[\u00A0-\u9999<>&]/gim, (i) => `&#${i.charCodeAt(0)};`); | ||
105 | } | ||
106 | |||
107 | /** | ||
108 | * Add the class 'hidden' to city options not attached to the current selected continent. | 103 | * Add the class 'hidden' to city options not attached to the current selected continent. |
109 | * | 104 | * |
110 | * @param cities List of <option> elements | 105 | * @param cities List of <option> elements |
@@ -222,6 +217,8 @@ function init(description) { | |||
222 | 217 | ||
223 | (() => { | 218 | (() => { |
224 | const basePath = document.querySelector('input[name="js_base_path"]').value; | 219 | const basePath = document.querySelector('input[name="js_base_path"]').value; |
220 | const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]'); | ||
221 | const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' '; | ||
225 | 222 | ||
226 | /** | 223 | /** |
227 | * Handle responsive menu. | 224 | * Handle responsive menu. |
@@ -302,7 +299,8 @@ function init(description) { | |||
302 | const deleteLinks = document.querySelectorAll('.confirm-delete'); | 299 | const deleteLinks = document.querySelectorAll('.confirm-delete'); |
303 | [...deleteLinks].forEach((deleteLink) => { | 300 | [...deleteLinks].forEach((deleteLink) => { |
304 | deleteLink.addEventListener('click', (event) => { | 301 | deleteLink.addEventListener('click', (event) => { |
305 | if (!confirm(document.getElementById('translation-delete-link').innerHTML)) { | 302 | const type = event.currentTarget.getAttribute('data-type') || 'link'; |
303 | if (!confirm(document.getElementById(`translation-delete-${type}`).innerHTML)) { | ||
306 | event.preventDefault(); | 304 | event.preventDefault(); |
307 | } | 305 | } |
308 | }); | 306 | }); |
@@ -569,7 +567,7 @@ function init(description) { | |||
569 | input.setAttribute('name', totag); | 567 | input.setAttribute('name', totag); |
570 | input.setAttribute('value', totag); | 568 | input.setAttribute('value', totag); |
571 | findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; | 569 | findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; |
572 | block.querySelector('a.tag-link').innerHTML = htmlEntities(totag); | 570 | block.querySelector('a.tag-link').innerHTML = he.encode(totag); |
573 | block | 571 | block |
574 | .querySelector('a.tag-link') | 572 | .querySelector('a.tag-link') |
575 | .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`); | 573 | .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`); |
@@ -582,7 +580,7 @@ function init(description) { | |||
582 | 580 | ||
583 | // Refresh awesomplete values | 581 | // Refresh awesomplete values |
584 | existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag)); | 582 | existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag)); |
585 | awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); | 583 | awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator); |
586 | } | 584 | } |
587 | }; | 585 | }; |
588 | xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`); | 586 | xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`); |
@@ -622,14 +620,14 @@ function init(description) { | |||
622 | refreshToken(basePath); | 620 | refreshToken(basePath); |
623 | 621 | ||
624 | existingTags = existingTags.filter((tagItem) => tagItem !== tag); | 622 | existingTags = existingTags.filter((tagItem) => tagItem !== tag); |
625 | awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); | 623 | awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator); |
626 | } | 624 | } |
627 | }); | 625 | }); |
628 | }); | 626 | }); |
629 | 627 | ||
630 | const autocompleteFields = document.querySelectorAll('input[data-multiple]'); | 628 | const autocompleteFields = document.querySelectorAll('input[data-multiple]'); |
631 | [...autocompleteFields].forEach((autocompleteField) => { | 629 | [...autocompleteFields].forEach((autocompleteField) => { |
632 | awesomepletes.push(createAwesompleteInstance(autocompleteField)); | 630 | awesomepletes.push(createAwesompleteInstance(autocompleteField, tagsSeparator)); |
633 | }); | 631 | }); |
634 | 632 | ||
635 | const exportForm = document.querySelector('#exportform'); | 633 | const exportForm = document.querySelector('#exportform'); |
@@ -642,4 +640,33 @@ function init(description) { | |||
642 | }); | 640 | }); |
643 | }); | 641 | }); |
644 | } | 642 | } |
643 | |||
644 | const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block'); | ||
645 | if (bulkCreationButton != null) { | ||
646 | const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => { | ||
647 | if (bulkCreationButton.classList.contains('pure-u-0')) { | ||
648 | showMoreBlockElement.classList.remove('pure-u-0'); | ||
649 | formElement.classList.add('pure-u-0'); | ||
650 | } else { | ||
651 | showMoreBlockElement.classList.add('pure-u-0'); | ||
652 | formElement.classList.remove('pure-u-0'); | ||
653 | } | ||
654 | }; | ||
655 | |||
656 | const bulkCreationForm = document.querySelector('.addlink-batch-form-block'); | ||
657 | |||
658 | toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm); | ||
659 | bulkCreationButton.querySelector('a').addEventListener('click', (e) => { | ||
660 | e.preventDefault(); | ||
661 | toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm); | ||
662 | }); | ||
663 | |||
664 | // Force to send falsy value if the checkbox is not checked. | ||
665 | const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]'); | ||
666 | const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]'); | ||
667 | privateButton.addEventListener('click', () => { | ||
668 | privateHiddenButton.disabled = !privateHiddenButton.disabled; | ||
669 | }); | ||
670 | privateHiddenButton.disabled = privateButton.checked; | ||
671 | } | ||
645 | })(); | 672 | })(); |
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index a528adb0..cc8ccc1e 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss | |||
@@ -139,6 +139,16 @@ body, | |||
139 | } | 139 | } |
140 | } | 140 | } |
141 | 141 | ||
142 | .page-form, | ||
143 | .pure-alert { | ||
144 | code { | ||
145 | display: inline-block; | ||
146 | padding: 0 2px; | ||
147 | color: $dark-grey; | ||
148 | background-color: var(--background-color); | ||
149 | } | ||
150 | } | ||
151 | |||
142 | // Make pure-extras alert closable. | 152 | // Make pure-extras alert closable. |
143 | .pure-alert-closable { | 153 | .pure-alert-closable { |
144 | .fa-times { | 154 | .fa-times { |
@@ -671,6 +681,10 @@ body, | |||
671 | content: ''; | 681 | content: ''; |
672 | } | 682 | } |
673 | } | 683 | } |
684 | |||
685 | .search-highlight { | ||
686 | background-color: yellow; | ||
687 | } | ||
674 | } | 688 | } |
675 | 689 | ||
676 | .linklist-item-buttons { | 690 | .linklist-item-buttons { |
@@ -1019,6 +1033,10 @@ body, | |||
1019 | &.button-red { | 1033 | &.button-red { |
1020 | background: $red; | 1034 | background: $red; |
1021 | } | 1035 | } |
1036 | |||
1037 | &.button-grey { | ||
1038 | background: $light-grey; | ||
1039 | } | ||
1022 | } | 1040 | } |
1023 | 1041 | ||
1024 | .submit-buttons { | 1042 | .submit-buttons { |
@@ -1043,7 +1061,7 @@ body, | |||
1043 | } | 1061 | } |
1044 | 1062 | ||
1045 | table { | 1063 | table { |
1046 | margin: auto; | 1064 | margin: 10px auto 25px auto; |
1047 | width: 90%; | 1065 | width: 90%; |
1048 | 1066 | ||
1049 | .order { | 1067 | .order { |
@@ -1079,6 +1097,11 @@ body, | |||
1079 | position: absolute; | 1097 | position: absolute; |
1080 | right: 5%; | 1098 | right: 5%; |
1081 | } | 1099 | } |
1100 | |||
1101 | &.button-grey { | ||
1102 | position: absolute; | ||
1103 | left: 5%; | ||
1104 | } | ||
1082 | } | 1105 | } |
1083 | } | 1106 | } |
1084 | } | 1107 | } |
@@ -1253,11 +1276,15 @@ form { | |||
1253 | margin: 70px 0 25px; | 1276 | margin: 70px 0 25px; |
1254 | } | 1277 | } |
1255 | 1278 | ||
1279 | a { | ||
1280 | color: var(--main-color); | ||
1281 | } | ||
1282 | |||
1256 | pre { | 1283 | pre { |
1257 | margin: 0 20%; | 1284 | margin: 0 20%; |
1258 | padding: 20px 0; | 1285 | padding: 20px 0; |
1259 | text-align: left; | 1286 | text-align: left; |
1260 | line-height: .7em; | 1287 | line-height: 1em; |
1261 | } | 1288 | } |
1262 | } | 1289 | } |
1263 | 1290 | ||
@@ -1269,6 +1296,57 @@ form { | |||
1269 | } | 1296 | } |
1270 | } | 1297 | } |
1271 | 1298 | ||
1299 | .loading-input { | ||
1300 | position: relative; | ||
1301 | |||
1302 | @keyframes around { | ||
1303 | 0% { | ||
1304 | transform: rotate(0deg); | ||
1305 | } | ||
1306 | |||
1307 | 100% { | ||
1308 | transform: rotate(360deg); | ||
1309 | } | ||
1310 | } | ||
1311 | |||
1312 | .icon-container { | ||
1313 | position: absolute; | ||
1314 | right: 60px; | ||
1315 | top: calc(50% - 10px); | ||
1316 | } | ||
1317 | |||
1318 | .loader { | ||
1319 | position: relative; | ||
1320 | height: 20px; | ||
1321 | width: 20px; | ||
1322 | display: inline-block; | ||
1323 | animation: around 5.4s infinite; | ||
1324 | |||
1325 | &::after, | ||
1326 | &::before { | ||
1327 | content: ""; | ||
1328 | background: $form-input-background; | ||
1329 | position: absolute; | ||
1330 | display: inline-block; | ||
1331 | width: 100%; | ||
1332 | height: 100%; | ||
1333 | border-width: 2px; | ||
1334 | border-color: #333 #333 transparent transparent; | ||
1335 | border-style: solid; | ||
1336 | border-radius: 20px; | ||
1337 | box-sizing: border-box; | ||
1338 | top: 0; | ||
1339 | left: 0; | ||
1340 | animation: around 0.7s ease-in-out infinite; | ||
1341 | } | ||
1342 | |||
1343 | &::after { | ||
1344 | animation: around 0.7s ease-in-out 0.1s infinite; | ||
1345 | background: transparent; | ||
1346 | } | ||
1347 | } | ||
1348 | } | ||
1349 | |||
1272 | // LOGIN | 1350 | // LOGIN |
1273 | .login-form-container { | 1351 | .login-form-container { |
1274 | .remember-me { | 1352 | .remember-me { |
@@ -1641,6 +1719,123 @@ form { | |||
1641 | } | 1719 | } |
1642 | } | 1720 | } |
1643 | 1721 | ||
1722 | // SERVER PAGE | ||
1723 | |||
1724 | .server-tables-page, | ||
1725 | .server-tables { | ||
1726 | .window-subtitle { | ||
1727 | &::before { | ||
1728 | display: block; | ||
1729 | margin: 8px auto; | ||
1730 | background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color)); | ||
1731 | width: 50%; | ||
1732 | height: 1px; | ||
1733 | content: ''; | ||
1734 | } | ||
1735 | } | ||
1736 | |||
1737 | .server-row { | ||
1738 | p { | ||
1739 | height: 25px; | ||
1740 | padding: 0 10px; | ||
1741 | } | ||
1742 | } | ||
1743 | |||
1744 | .server-label { | ||
1745 | text-align: right; | ||
1746 | font-weight: bold; | ||
1747 | } | ||
1748 | |||
1749 | i { | ||
1750 | &.fa-color-green { | ||
1751 | color: $main-green; | ||
1752 | } | ||
1753 | |||
1754 | &.fa-color-orange { | ||
1755 | color: $orange; | ||
1756 | } | ||
1757 | |||
1758 | &.fa-color-red { | ||
1759 | color: $red; | ||
1760 | } | ||
1761 | } | ||
1762 | |||
1763 | @media screen and (max-width: 64em) { | ||
1764 | .server-label { | ||
1765 | text-align: center; | ||
1766 | } | ||
1767 | |||
1768 | .server-row { | ||
1769 | p { | ||
1770 | text-align: center; | ||
1771 | } | ||
1772 | } | ||
1773 | } | ||
1774 | } | ||
1775 | |||
1776 | // Batch creation | ||
1777 | input[name='save_edit_batch'] { | ||
1778 | @extend %page-form-button; | ||
1779 | } | ||
1780 | |||
1781 | .addlink-batch-show-more { | ||
1782 | display: flex; | ||
1783 | align-items: center; | ||
1784 | margin: 20px 0 8px; | ||
1785 | |||
1786 | a { | ||
1787 | color: var(--main-color); | ||
1788 | text-decoration: none; | ||
1789 | } | ||
1790 | |||
1791 | &::before, | ||
1792 | &::after { | ||
1793 | content: ""; | ||
1794 | flex-grow: 1; | ||
1795 | background: rgba(0, 0, 0, 0.35); | ||
1796 | height: 1px; | ||
1797 | font-size: 0; | ||
1798 | line-height: 0; | ||
1799 | } | ||
1800 | |||
1801 | &::before { | ||
1802 | margin: 0 16px 0 0; | ||
1803 | } | ||
1804 | |||
1805 | &::after { | ||
1806 | margin: 0 0 0 16px; | ||
1807 | } | ||
1808 | } | ||
1809 | |||
1810 | .dark-layer { | ||
1811 | display: none; | ||
1812 | position: fixed; | ||
1813 | height: 100%; | ||
1814 | width: 100%; | ||
1815 | z-index: 998; | ||
1816 | background-color: rgba(0, 0, 0, .75); | ||
1817 | color: #fff; | ||
1818 | |||
1819 | .screen-center { | ||
1820 | display: flex; | ||
1821 | flex-direction: column; | ||
1822 | justify-content: center; | ||
1823 | align-items: center; | ||
1824 | text-align: center; | ||
1825 | min-height: 100vh; | ||
1826 | } | ||
1827 | |||
1828 | .progressbar { | ||
1829 | width: 33%; | ||
1830 | } | ||
1831 | } | ||
1832 | |||
1833 | .addlink-batch-form-block { | ||
1834 | .pure-alert { | ||
1835 | margin: 25px 0 0 0; | ||
1836 | } | ||
1837 | } | ||
1838 | |||
1644 | // Print rules | 1839 | // Print rules |
1645 | @media print { | 1840 | @media print { |
1646 | .shaarli-menu { | 1841 | .shaarli-menu { |
diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css index 1688dce0..33e178af 100644 --- a/assets/vintage/css/shaarli.css +++ b/assets/vintage/css/shaarli.css | |||
@@ -1122,6 +1122,16 @@ ul.errors { | |||
1122 | float: left; | 1122 | float: left; |
1123 | } | 1123 | } |
1124 | 1124 | ||
1125 | ul.warnings { | ||
1126 | color: orange; | ||
1127 | float: left; | ||
1128 | } | ||
1129 | |||
1130 | ul.successes { | ||
1131 | color: green; | ||
1132 | float: left; | ||
1133 | } | ||
1134 | |||
1125 | #pluginsadmin { | 1135 | #pluginsadmin { |
1126 | width: 80%; | 1136 | width: 80%; |
1127 | padding: 20px 0 0 20px; | 1137 | padding: 20px 0 0 20px; |
@@ -1248,3 +1258,54 @@ ul.errors { | |||
1248 | width: 0%; | 1258 | width: 0%; |
1249 | height: 10px; | 1259 | height: 10px; |
1250 | } | 1260 | } |
1261 | |||
1262 | .loading-input { | ||
1263 | position: relative; | ||
1264 | } | ||
1265 | |||
1266 | @keyframes around { | ||
1267 | 0% { | ||
1268 | transform: rotate(0deg); | ||
1269 | } | ||
1270 | |||
1271 | 100% { | ||
1272 | transform: rotate(360deg); | ||
1273 | } | ||
1274 | } | ||
1275 | |||
1276 | .loading-input .icon-container { | ||
1277 | position: absolute; | ||
1278 | right: 60px; | ||
1279 | top: calc(50% - 10px); | ||
1280 | } | ||
1281 | |||
1282 | .loading-input .loader { | ||
1283 | position: relative; | ||
1284 | height: 20px; | ||
1285 | width: 20px; | ||
1286 | display: inline-block; | ||
1287 | animation: around 5.4s infinite; | ||
1288 | } | ||
1289 | |||
1290 | .loading-input .loader::after, | ||
1291 | .loading-input .loader::before { | ||
1292 | content: ""; | ||
1293 | background: #eee; | ||
1294 | position: absolute; | ||
1295 | display: inline-block; | ||
1296 | width: 100%; | ||
1297 | height: 100%; | ||
1298 | border-width: 2px; | ||
1299 | border-color: #333 #333 transparent transparent; | ||
1300 | border-style: solid; | ||
1301 | border-radius: 20px; | ||
1302 | box-sizing: border-box; | ||
1303 | top: 0; | ||
1304 | left: 0; | ||
1305 | animation: around 0.7s ease-in-out infinite; | ||
1306 | } | ||
1307 | |||
1308 | .loading-input .loader::after { | ||
1309 | animation: around 0.7s ease-in-out 0.1s infinite; | ||
1310 | background: transparent; | ||
1311 | } | ||
diff --git a/assets/vintage/js/base.js b/assets/vintage/js/base.js index 66830b59..55f1c37d 100644 --- a/assets/vintage/js/base.js +++ b/assets/vintage/js/base.js | |||
@@ -2,29 +2,38 @@ import Awesomplete from 'awesomplete'; | |||
2 | import 'awesomplete/awesomplete.css'; | 2 | import 'awesomplete/awesomplete.css'; |
3 | 3 | ||
4 | (() => { | 4 | (() => { |
5 | const awp = Awesomplete.$; | ||
6 | const autocompleteFields = document.querySelectorAll('input[data-multiple]'); | 5 | const autocompleteFields = document.querySelectorAll('input[data-multiple]'); |
6 | const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]'); | ||
7 | const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' '; | ||
8 | |||
7 | [...autocompleteFields].forEach((autocompleteField) => { | 9 | [...autocompleteFields].forEach((autocompleteField) => { |
8 | const awesomplete = new Awesomplete(awp(autocompleteField)); | 10 | const awesome = new Awesomplete(Awesomplete.$(autocompleteField)); |
9 | awesomplete.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]); | 11 | |
10 | awesomplete.replace = (text) => { | 12 | // Tags are separated by separator |
11 | const before = awesomplete.input.value.match(/^.+ \s*|/)[0]; | 13 | awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS( |
12 | awesomplete.input.value = `${before}${text} `; | 14 | text, |
15 | input.match(new RegExp(`[^${tagsSeparator}]*$`))[0], | ||
16 | ); | ||
17 | // Insert new selected tag in the input | ||
18 | awesome.replace = (text) => { | ||
19 | const before = awesome.input.value.match(new RegExp(`^.+${tagsSeparator}+|`))[0]; | ||
20 | awesome.input.value = `${before}${text}${tagsSeparator}`; | ||
13 | }; | 21 | }; |
14 | awesomplete.minChars = 1; | 22 | // Highlight found items |
23 | awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]); | ||
15 | 24 | ||
16 | autocompleteField.addEventListener('input', () => { | 25 | // Don't display already selected items |
17 | const proposedTags = autocompleteField.getAttribute('data-list').replace(/,/g, '').split(' '); | 26 | // WARNING: pseudo classes does not seem to work with string litterals... |
18 | const reg = /(\w+) /g; | 27 | const reg = new RegExp(`([^${tagsSeparator}]+)${tagsSeparator}`, 'g'); |
19 | let match; | 28 | let match; |
20 | while ((match = reg.exec(autocompleteField.value)) !== null) { | 29 | awesome.data = (item, input) => { |
21 | const id = proposedTags.indexOf(match[1]); | 30 | while ((match = reg.exec(input))) { |
22 | if (id !== -1) { | 31 | if (item === match[1]) { |
23 | proposedTags.splice(id, 1); | 32 | return ''; |
24 | } | 33 | } |
25 | } | 34 | } |
26 | 35 | return item; | |
27 | awesomplete.list = proposedTags; | 36 | }; |
28 | }); | 37 | awesome.minChars = 1; |
29 | }); | 38 | }); |
30 | })(); | 39 | })(); |
diff --git a/composer.json b/composer.json index cd9fcf5b..138319ca 100644 --- a/composer.json +++ b/composer.json | |||
@@ -10,6 +10,7 @@ | |||
10 | }, | 10 | }, |
11 | "keywords": ["bookmark", "link", "share", "web"], | 11 | "keywords": ["bookmark", "link", "share", "web"], |
12 | "config": { | 12 | "config": { |
13 | "sort-packages": true, | ||
13 | "platform": { | 14 | "platform": { |
14 | "php": "7.1.29" | 15 | "php": "7.1.29" |
15 | } | 16 | } |
@@ -18,12 +19,15 @@ | |||
18 | "php": ">=7.1", | 19 | "php": ">=7.1", |
19 | "ext-json": "*", | 20 | "ext-json": "*", |
20 | "ext-zlib": "*", | 21 | "ext-zlib": "*", |
21 | "shaarli/netscape-bookmark-parser": "^2.1", | ||
22 | "erusev/parsedown": "^1.6", | ||
23 | "slim/slim": "^3.0", | ||
24 | "arthurhoaro/web-thumbnailer": "^2.0", | 22 | "arthurhoaro/web-thumbnailer": "^2.0", |
23 | "erusev/parsedown": "^1.6", | ||
24 | "erusev/parsedown-extra": "^0.8.1", | ||
25 | "gettext/gettext": "^4.4", | ||
26 | "katzgrau/klogger": "^1.2", | ||
27 | "malkusch/lock": "^2.1", | ||
25 | "pubsubhubbub/publisher": "dev-master", | 28 | "pubsubhubbub/publisher": "dev-master", |
26 | "gettext/gettext": "^4.4" | 29 | "shaarli/netscape-bookmark-parser": "^3.0", |
30 | "slim/slim": "^3.0" | ||
27 | }, | 31 | }, |
28 | "require-dev": { | 32 | "require-dev": { |
29 | "roave/security-advisories": "dev-master", | 33 | "roave/security-advisories": "dev-master", |
@@ -55,6 +59,7 @@ | |||
55 | "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin", | 59 | "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin", |
56 | "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor", | 60 | "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor", |
57 | "Shaarli\\Front\\Exception\\": "application/front/exceptions", | 61 | "Shaarli\\Front\\Exception\\": "application/front/exceptions", |
62 | "Shaarli\\Helper\\": "application/helper", | ||
58 | "Shaarli\\Http\\": "application/http", | 63 | "Shaarli\\Http\\": "application/http", |
59 | "Shaarli\\Legacy\\": "application/legacy", | 64 | "Shaarli\\Legacy\\": "application/legacy", |
60 | "Shaarli\\Netscape\\": "application/netscape", | 65 | "Shaarli\\Netscape\\": "application/netscape", |
diff --git a/composer.lock b/composer.lock index 2c8b0ea7..0023df88 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#installing-dependencies", | 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", |
5 | "This file is @generated automatically" | 5 | "This file is @generated automatically" |
6 | ], | 6 | ], |
7 | "content-hash": "98520a05a7185503ee13d05ffaa535f6", | 7 | "content-hash": "83852dec81e299a117a81206a5091472", |
8 | "packages": [ | 8 | "packages": [ |
9 | { | 9 | { |
10 | "name": "arthurhoaro/web-thumbnailer", | 10 | "name": "arthurhoaro/web-thumbnailer", |
@@ -108,6 +108,57 @@ | |||
108 | "time": "2019-12-30T22:54:17+00:00" | 108 | "time": "2019-12-30T22:54:17+00:00" |
109 | }, | 109 | }, |
110 | { | 110 | { |
111 | "name": "erusev/parsedown-extra", | ||
112 | "version": "0.8.1", | ||
113 | "source": { | ||
114 | "type": "git", | ||
115 | "url": "https://github.com/erusev/parsedown-extra.git", | ||
116 | "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef" | ||
117 | }, | ||
118 | "dist": { | ||
119 | "type": "zip", | ||
120 | "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/91ac3ff98f0cea243bdccc688df43810f044dcef", | ||
121 | "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef", | ||
122 | "shasum": "" | ||
123 | }, | ||
124 | "require": { | ||
125 | "erusev/parsedown": "^1.7.4" | ||
126 | }, | ||
127 | "require-dev": { | ||
128 | "phpunit/phpunit": "^4.8.35" | ||
129 | }, | ||
130 | "type": "library", | ||
131 | "autoload": { | ||
132 | "psr-0": { | ||
133 | "ParsedownExtra": "" | ||
134 | } | ||
135 | }, | ||
136 | "notification-url": "https://packagist.org/downloads/", | ||
137 | "license": [ | ||
138 | "MIT" | ||
139 | ], | ||
140 | "authors": [ | ||
141 | { | ||
142 | "name": "Emanuil Rusev", | ||
143 | "email": "hello@erusev.com", | ||
144 | "homepage": "http://erusev.com" | ||
145 | } | ||
146 | ], | ||
147 | "description": "An extension of Parsedown that adds support for Markdown Extra.", | ||
148 | "homepage": "https://github.com/erusev/parsedown-extra", | ||
149 | "keywords": [ | ||
150 | "markdown", | ||
151 | "markdown extra", | ||
152 | "parsedown", | ||
153 | "parser" | ||
154 | ], | ||
155 | "support": { | ||
156 | "issues": "https://github.com/erusev/parsedown-extra/issues", | ||
157 | "source": "https://github.com/erusev/parsedown-extra/tree/0.8.x" | ||
158 | }, | ||
159 | "time": "2019-12-30T23:20:37+00:00" | ||
160 | }, | ||
161 | { | ||
111 | "name": "gettext/gettext", | 162 | "name": "gettext/gettext", |
112 | "version": "v4.8.2", | 163 | "version": "v4.8.2", |
113 | "source": { | 164 | "source": { |
@@ -294,6 +345,91 @@ | |||
294 | "time": "2016-11-07T19:29:14+00:00" | 345 | "time": "2016-11-07T19:29:14+00:00" |
295 | }, | 346 | }, |
296 | { | 347 | { |
348 | "name": "malkusch/lock", | ||
349 | "version": "v2.1", | ||
350 | "source": { | ||
351 | "type": "git", | ||
352 | "url": "https://github.com/php-lock/lock.git", | ||
353 | "reference": "093f389ec2f38fc8686d2f70e23378182fce7714" | ||
354 | }, | ||
355 | "dist": { | ||
356 | "type": "zip", | ||
357 | "url": "https://api.github.com/repos/php-lock/lock/zipball/093f389ec2f38fc8686d2f70e23378182fce7714", | ||
358 | "reference": "093f389ec2f38fc8686d2f70e23378182fce7714", | ||
359 | "shasum": "" | ||
360 | }, | ||
361 | "require": { | ||
362 | "php": ">=7.1", | ||
363 | "psr/log": "^1" | ||
364 | }, | ||
365 | "require-dev": { | ||
366 | "eloquent/liberator": "^2.0", | ||
367 | "ext-memcached": "*", | ||
368 | "ext-pcntl": "*", | ||
369 | "ext-pdo_mysql": "*", | ||
370 | "ext-pdo_sqlite": "*", | ||
371 | "ext-redis": "*", | ||
372 | "ext-sysvsem": "*", | ||
373 | "johnkary/phpunit-speedtrap": "^3.0", | ||
374 | "kriswallsmith/spork": "^0.3", | ||
375 | "mikey179/vfsstream": "^1.6", | ||
376 | "php-mock/php-mock-phpunit": "^2.1", | ||
377 | "phpunit/phpunit": "^7.4", | ||
378 | "predis/predis": "^1.1", | ||
379 | "squizlabs/php_codesniffer": "^3.3" | ||
380 | }, | ||
381 | "suggest": { | ||
382 | "ext-pnctl": "Enables locking with flock without busy waiting in CLI scripts.", | ||
383 | "ext-redis": "To use this library with the PHP Redis extension.", | ||
384 | "ext-sysvsem": "Enables locking using semaphores.", | ||
385 | "predis/predis": "To use this library with predis." | ||
386 | }, | ||
387 | "type": "library", | ||
388 | "autoload": { | ||
389 | "psr-4": { | ||
390 | "malkusch\\lock\\": "classes/" | ||
391 | } | ||
392 | }, | ||
393 | "notification-url": "https://packagist.org/downloads/", | ||
394 | "license": [ | ||
395 | "WTFPL" | ||
396 | ], | ||
397 | "authors": [ | ||
398 | { | ||
399 | "name": "Markus Malkusch", | ||
400 | "email": "markus@malkusch.de", | ||
401 | "homepage": "http://markus.malkusch.de", | ||
402 | "role": "Developer" | ||
403 | }, | ||
404 | { | ||
405 | "name": "Willem Stuursma-Ruwen", | ||
406 | "email": "willem@stuursma.name", | ||
407 | "role": "Developer" | ||
408 | } | ||
409 | ], | ||
410 | "description": "Mutex library for exclusive code execution.", | ||
411 | "homepage": "https://github.com/malkusch/lock", | ||
412 | "keywords": [ | ||
413 | "advisory-locks", | ||
414 | "cas", | ||
415 | "flock", | ||
416 | "lock", | ||
417 | "locking", | ||
418 | "memcache", | ||
419 | "mutex", | ||
420 | "mysql", | ||
421 | "postgresql", | ||
422 | "redis", | ||
423 | "redlock", | ||
424 | "semaphore" | ||
425 | ], | ||
426 | "support": { | ||
427 | "issues": "https://github.com/php-lock/lock/issues", | ||
428 | "source": "https://github.com/php-lock/lock/tree/v2.1" | ||
429 | }, | ||
430 | "time": "2018-12-12T19:53:29+00:00" | ||
431 | }, | ||
432 | { | ||
297 | "name": "nikic/fast-route", | 433 | "name": "nikic/fast-route", |
298 | "version": "v1.3.0", | 434 | "version": "v1.3.0", |
299 | "source": { | 435 | "source": { |
@@ -650,24 +786,25 @@ | |||
650 | }, | 786 | }, |
651 | { | 787 | { |
652 | "name": "shaarli/netscape-bookmark-parser", | 788 | "name": "shaarli/netscape-bookmark-parser", |
653 | "version": "v2.2.0", | 789 | "version": "v3.0.1", |
654 | "source": { | 790 | "source": { |
655 | "type": "git", | 791 | "type": "git", |
656 | "url": "https://github.com/shaarli/netscape-bookmark-parser.git", | 792 | "url": "https://github.com/shaarli/netscape-bookmark-parser.git", |
657 | "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df" | 793 | "reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305" |
658 | }, | 794 | }, |
659 | "dist": { | 795 | "dist": { |
660 | "type": "zip", | 796 | "type": "zip", |
661 | "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/432a010af2bb1832d6fbc4763e6b0100b980a1df", | 797 | "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/d2321f30413944b2d0a9844bf8cc588c71ae6305", |
662 | "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df", | 798 | "reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305", |
663 | "shasum": "" | 799 | "shasum": "" |
664 | }, | 800 | }, |
665 | "require": { | 801 | "require": { |
666 | "katzgrau/klogger": "~1.0", | 802 | "katzgrau/klogger": "~1.0", |
667 | "php": ">=5.6" | 803 | "php": ">=7.1" |
668 | }, | 804 | }, |
669 | "require-dev": { | 805 | "require-dev": { |
670 | "phpunit/phpunit": "^5.0" | 806 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", |
807 | "squizlabs/php_codesniffer": "^3.5" | ||
671 | }, | 808 | }, |
672 | "type": "library", | 809 | "type": "library", |
673 | "autoload": { | 810 | "autoload": { |
@@ -703,9 +840,9 @@ | |||
703 | ], | 840 | ], |
704 | "support": { | 841 | "support": { |
705 | "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues", | 842 | "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues", |
706 | "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v2.2.0" | 843 | "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v3.0.1" |
707 | }, | 844 | }, |
708 | "time": "2020-06-06T15:53:53+00:00" | 845 | "time": "2020-11-03T12:27:58+00:00" |
709 | }, | 846 | }, |
710 | { | 847 | { |
711 | "name": "slim/slim", | 848 | "name": "slim/slim", |
@@ -1577,12 +1714,12 @@ | |||
1577 | "source": { | 1714 | "source": { |
1578 | "type": "git", | 1715 | "type": "git", |
1579 | "url": "https://github.com/Roave/SecurityAdvisories.git", | 1716 | "url": "https://github.com/Roave/SecurityAdvisories.git", |
1580 | "reference": "0749ceaf15c136d085b722a5bb88141398a54142" | 1717 | "reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6" |
1581 | }, | 1718 | }, |
1582 | "dist": { | 1719 | "dist": { |
1583 | "type": "zip", | 1720 | "type": "zip", |
1584 | "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0749ceaf15c136d085b722a5bb88141398a54142", | 1721 | "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/065a018d3b5c2c84a53db3347cca4e1b7fa362a6", |
1585 | "reference": "0749ceaf15c136d085b722a5bb88141398a54142", | 1722 | "reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6", |
1586 | "shasum": "" | 1723 | "shasum": "" |
1587 | }, | 1724 | }, |
1588 | "conflict": { | 1725 | "conflict": { |
@@ -1598,7 +1735,7 @@ | |||
1598 | "bagisto/bagisto": "<0.1.5", | 1735 | "bagisto/bagisto": "<0.1.5", |
1599 | "barrelstrength/sprout-base-email": "<1.2.7", | 1736 | "barrelstrength/sprout-base-email": "<1.2.7", |
1600 | "barrelstrength/sprout-forms": "<3.9", | 1737 | "barrelstrength/sprout-forms": "<3.9", |
1601 | "baserproject/basercms": ">=4,<=4.3.6", | 1738 | "baserproject/basercms": ">=4,<=4.3.6|>=4.4,<4.4.1", |
1602 | "bolt/bolt": "<3.7.1", | 1739 | "bolt/bolt": "<3.7.1", |
1603 | "brightlocal/phpwhois": "<=4.2.5", | 1740 | "brightlocal/phpwhois": "<=4.2.5", |
1604 | "buddypress/buddypress": "<5.1.2", | 1741 | "buddypress/buddypress": "<5.1.2", |
@@ -1642,7 +1779,7 @@ | |||
1642 | "ezsystems/ezplatform-kernel": ">=1,<1.0.2.1", | 1779 | "ezsystems/ezplatform-kernel": ">=1,<1.0.2.1", |
1643 | "ezsystems/ezplatform-user": ">=1,<1.0.1", | 1780 | "ezsystems/ezplatform-user": ">=1,<1.0.1", |
1644 | "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.14.2|>=6,<6.7.9.1|>=6.8,<6.13.6.3|>=7,<7.2.4.1|>=7.3,<7.3.2.1|>=7.5,<7.5.7.1", | 1781 | "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.14.2|>=6,<6.7.9.1|>=6.8,<6.13.6.3|>=7,<7.2.4.1|>=7.3,<7.3.2.1|>=7.5,<7.5.7.1", |
1645 | "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.1|>=2011,<2017.12.7.2|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.4.2", | 1782 | "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.2|>=2011,<2017.12.7.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.5.1", |
1646 | "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", | 1783 | "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", |
1647 | "ezsystems/repository-forms": ">=2.3,<2.3.2.1", | 1784 | "ezsystems/repository-forms": ">=2.3,<2.3.2.1", |
1648 | "ezyang/htmlpurifier": "<4.1.1", | 1785 | "ezyang/htmlpurifier": "<4.1.1", |
@@ -1682,9 +1819,12 @@ | |||
1682 | "magento/magento1ee": ">=1,<1.14.4.3", | 1819 | "magento/magento1ee": ">=1,<1.14.4.3", |
1683 | "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", | 1820 | "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", |
1684 | "marcwillmann/turn": "<0.3.3", | 1821 | "marcwillmann/turn": "<0.3.3", |
1822 | "mediawiki/core": ">=1.31,<1.31.9|>=1.32,<1.32.4|>=1.33,<1.33.3|>=1.34,<1.34.3|>=1.34.99,<1.35", | ||
1685 | "mittwald/typo3_forum": "<1.2.1", | 1823 | "mittwald/typo3_forum": "<1.2.1", |
1686 | "monolog/monolog": ">=1.8,<1.12", | 1824 | "monolog/monolog": ">=1.8,<1.12", |
1687 | "namshi/jose": "<2.2", | 1825 | "namshi/jose": "<2.2", |
1826 | "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", | ||
1827 | "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", | ||
1688 | "nystudio107/craft-seomatic": "<3.3", | 1828 | "nystudio107/craft-seomatic": "<3.3", |
1689 | "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", | 1829 | "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", |
1690 | "october/backend": ">=1.0.319,<1.0.467", | 1830 | "october/backend": ">=1.0.319,<1.0.467", |
@@ -1694,7 +1834,8 @@ | |||
1694 | "onelogin/php-saml": "<2.10.4", | 1834 | "onelogin/php-saml": "<2.10.4", |
1695 | "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5", | 1835 | "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5", |
1696 | "openid/php-openid": "<2.3", | 1836 | "openid/php-openid": "<2.3", |
1697 | "openmage/magento-lts": "<19.4.6|>=20,<20.0.2", | 1837 | "openmage/magento-lts": "<19.4.8|>=20,<20.0.4", |
1838 | "orchid/platform": ">=9,<9.4.4", | ||
1698 | "oro/crm": ">=1.7,<1.7.4", | 1839 | "oro/crm": ">=1.7,<1.7.4", |
1699 | "oro/platform": ">=1.7,<1.7.4", | 1840 | "oro/platform": ">=1.7,<1.7.4", |
1700 | "padraic/humbug_get_contents": "<1.1.2", | 1841 | "padraic/humbug_get_contents": "<1.1.2", |
@@ -1720,6 +1861,7 @@ | |||
1720 | "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2", | 1861 | "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2", |
1721 | "propel/propel": ">=2-alpha.1,<=2-alpha.7", | 1862 | "propel/propel": ">=2-alpha.1,<=2-alpha.7", |
1722 | "propel/propel1": ">=1,<=1.7.1", | 1863 | "propel/propel1": ">=1,<=1.7.1", |
1864 | "pterodactyl/panel": "<0.7.19|>=1-rc.0,<=1-rc.6", | ||
1723 | "pusher/pusher-php-server": "<2.2.1", | 1865 | "pusher/pusher-php-server": "<2.2.1", |
1724 | "rainlab/debugbar-plugin": "<3.1", | 1866 | "rainlab/debugbar-plugin": "<3.1", |
1725 | "robrichards/xmlseclibs": "<3.0.4", | 1867 | "robrichards/xmlseclibs": "<3.0.4", |
@@ -1728,8 +1870,8 @@ | |||
1728 | "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", | 1870 | "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", |
1729 | "sensiolabs/connect": "<4.2.3", | 1871 | "sensiolabs/connect": "<4.2.3", |
1730 | "serluck/phpwhois": "<=4.2.6", | 1872 | "serluck/phpwhois": "<=4.2.6", |
1731 | "shopware/core": "<=6.3.1", | 1873 | "shopware/core": "<=6.3.2", |
1732 | "shopware/platform": "<=6.3.1", | 1874 | "shopware/platform": "<=6.3.2", |
1733 | "shopware/shopware": "<5.3.7", | 1875 | "shopware/shopware": "<5.3.7", |
1734 | "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1", | 1876 | "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1", |
1735 | "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2", | 1877 | "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2", |
@@ -1762,7 +1904,7 @@ | |||
1762 | "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", | 1904 | "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", |
1763 | "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", | 1905 | "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", |
1764 | "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", | 1906 | "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", |
1765 | "sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5", | 1907 | "sylius/sylius": "<1.6.9|>=1.7,<1.7.9|>=1.8,<1.8.3", |
1766 | "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", | 1908 | "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", |
1767 | "symbiote/silverstripe-versionedfiles": "<=2.0.3", | 1909 | "symbiote/silverstripe-versionedfiles": "<=2.0.3", |
1768 | "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", | 1910 | "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", |
@@ -1805,6 +1947,7 @@ | |||
1805 | "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5", | 1947 | "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5", |
1806 | "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4", | 1948 | "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4", |
1807 | "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", | 1949 | "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", |
1950 | "typo3fluid/fluid": ">=2,<2.0.5|>=2.1,<2.1.4|>=2.2,<2.2.1|>=2.3,<2.3.5|>=2.4,<2.4.1|>=2.5,<2.5.5|>=2.6,<2.6.1", | ||
1808 | "ua-parser/uap-php": "<3.8", | 1951 | "ua-parser/uap-php": "<3.8", |
1809 | "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", | 1952 | "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", |
1810 | "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4", | 1953 | "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4", |
@@ -1878,7 +2021,7 @@ | |||
1878 | "type": "tidelift" | 2021 | "type": "tidelift" |
1879 | } | 2022 | } |
1880 | ], | 2023 | ], |
1881 | "time": "2020-09-24T17:02:11+00:00" | 2024 | "time": "2020-11-01T20:01:47+00:00" |
1882 | }, | 2025 | }, |
1883 | { | 2026 | { |
1884 | "name": "sebastian/code-unit-reverse-lookup", | 2027 | "name": "sebastian/code-unit-reverse-lookup", |
@@ -2492,16 +2635,16 @@ | |||
2492 | }, | 2635 | }, |
2493 | { | 2636 | { |
2494 | "name": "squizlabs/php_codesniffer", | 2637 | "name": "squizlabs/php_codesniffer", |
2495 | "version": "3.5.6", | 2638 | "version": "3.5.8", |
2496 | "source": { | 2639 | "source": { |
2497 | "type": "git", | 2640 | "type": "git", |
2498 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", | 2641 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", |
2499 | "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" | 2642 | "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" |
2500 | }, | 2643 | }, |
2501 | "dist": { | 2644 | "dist": { |
2502 | "type": "zip", | 2645 | "type": "zip", |
2503 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", | 2646 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", |
2504 | "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", | 2647 | "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", |
2505 | "shasum": "" | 2648 | "shasum": "" |
2506 | }, | 2649 | }, |
2507 | "require": { | 2650 | "require": { |
@@ -2544,24 +2687,24 @@ | |||
2544 | "source": "https://github.com/squizlabs/PHP_CodeSniffer", | 2687 | "source": "https://github.com/squizlabs/PHP_CodeSniffer", |
2545 | "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" | 2688 | "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" |
2546 | }, | 2689 | }, |
2547 | "time": "2020-08-10T04:50:15+00:00" | 2690 | "time": "2020-10-23T02:01:07+00:00" |
2548 | }, | 2691 | }, |
2549 | { | 2692 | { |
2550 | "name": "symfony/polyfill-ctype", | 2693 | "name": "symfony/polyfill-ctype", |
2551 | "version": "v1.18.1", | 2694 | "version": "v1.20.0", |
2552 | "source": { | 2695 | "source": { |
2553 | "type": "git", | 2696 | "type": "git", |
2554 | "url": "https://github.com/symfony/polyfill-ctype.git", | 2697 | "url": "https://github.com/symfony/polyfill-ctype.git", |
2555 | "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" | 2698 | "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" |
2556 | }, | 2699 | }, |
2557 | "dist": { | 2700 | "dist": { |
2558 | "type": "zip", | 2701 | "type": "zip", |
2559 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", | 2702 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", |
2560 | "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", | 2703 | "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", |
2561 | "shasum": "" | 2704 | "shasum": "" |
2562 | }, | 2705 | }, |
2563 | "require": { | 2706 | "require": { |
2564 | "php": ">=5.3.3" | 2707 | "php": ">=7.1" |
2565 | }, | 2708 | }, |
2566 | "suggest": { | 2709 | "suggest": { |
2567 | "ext-ctype": "For best performance" | 2710 | "ext-ctype": "For best performance" |
@@ -2569,7 +2712,7 @@ | |||
2569 | "type": "library", | 2712 | "type": "library", |
2570 | "extra": { | 2713 | "extra": { |
2571 | "branch-alias": { | 2714 | "branch-alias": { |
2572 | "dev-master": "1.18-dev" | 2715 | "dev-main": "1.20-dev" |
2573 | }, | 2716 | }, |
2574 | "thanks": { | 2717 | "thanks": { |
2575 | "name": "symfony/polyfill", | 2718 | "name": "symfony/polyfill", |
@@ -2607,7 +2750,7 @@ | |||
2607 | "portable" | 2750 | "portable" |
2608 | ], | 2751 | ], |
2609 | "support": { | 2752 | "support": { |
2610 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.18.0" | 2753 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" |
2611 | }, | 2754 | }, |
2612 | "funding": [ | 2755 | "funding": [ |
2613 | { | 2756 | { |
@@ -2623,7 +2766,7 @@ | |||
2623 | "type": "tidelift" | 2766 | "type": "tidelift" |
2624 | } | 2767 | } |
2625 | ], | 2768 | ], |
2626 | "time": "2020-07-14T12:35:20+00:00" | 2769 | "time": "2020-10-23T14:02:19+00:00" |
2627 | }, | 2770 | }, |
2628 | { | 2771 | { |
2629 | "name": "theseer/tokenizer", | 2772 | "name": "theseer/tokenizer", |
diff --git a/doc/md/Docker.md b/doc/md/Docker.md index c152fe92..fc406c00 100644 --- a/doc/md/Docker.md +++ b/doc/md/Docker.md | |||
@@ -1,3 +1,4 @@ | |||
1 | |||
1 | # Docker | 2 | # Docker |
2 | 3 | ||
3 | [Docker](https://docs.docker.com/get-started/overview/) is an open platform for developing, shipping, and running applications | 4 | [Docker](https://docs.docker.com/get-started/overview/) is an open platform for developing, shipping, and running applications |
@@ -113,9 +114,11 @@ $ mkdir shaarli && cd shaarli | |||
113 | # Download the latest version of Shaarli's docker-compose.yml | 114 | # Download the latest version of Shaarli's docker-compose.yml |
114 | $ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/latest/docker-compose.yml -o docker-compose.yml | 115 | $ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/latest/docker-compose.yml -o docker-compose.yml |
115 | # Create the .env file and fill in your VPS and domain information | 116 | # Create the .env file and fill in your VPS and domain information |
116 | # (replace <MY_SHAARLI_DOMAIN> and <MY_CONTACT_EMAIL> with your actual information) | 117 | # (replace <shaarli.mydomain.org>, <admin@mydomain.org> and <latest> with your actual information) |
117 | $ echo 'SHAARLI_VIRTUAL_HOST=shaarli.mydomain.org' > .env | 118 | $ echo 'SHAARLI_VIRTUAL_HOST=shaarli.mydomain.org' > .env |
118 | $ echo 'SHAARLI_LETSENCRYPT_EMAIL=admin@mydomain.org' >> .env | 119 | $ echo 'SHAARLI_LETSENCRYPT_EMAIL=admin@mydomain.org' >> .env |
120 | # Available Docker tags can be found at https://hub.docker.com/r/shaarli/shaarli/tags | ||
121 | $ echo 'SHAARLI_DOCKER_TAG=latest' >> .env | ||
119 | # Pull the Docker images | 122 | # Pull the Docker images |
120 | $ docker-compose pull | 123 | $ docker-compose pull |
121 | # Run! | 124 | # Run! |
@@ -224,4 +227,4 @@ $ docker system prune | |||
224 | - [docker pull](https://docs.docker.com/engine/reference/commandline/pull/) | 227 | - [docker pull](https://docs.docker.com/engine/reference/commandline/pull/) |
225 | - [docker run](https://docs.docker.com/engine/reference/commandline/run/) | 228 | - [docker run](https://docs.docker.com/engine/reference/commandline/run/) |
226 | - [docker-compose logs](https://docs.docker.com/compose/reference/logs/) | 229 | - [docker-compose logs](https://docs.docker.com/compose/reference/logs/) |
227 | - Træfik: [Getting Started](https://docs.traefik.io/), [Docker backend](https://docs.traefik.io/configuration/backends/docker/), [Let's Encrypt](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/), [Docker image](https://hub.docker.com/_/traefik/) \ No newline at end of file | 230 | - Træfik: [Getting Started](https://docs.traefik.io/), [Docker backend](https://docs.traefik.io/configuration/backends/docker/), [Let's Encrypt](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/), [Docker image](https://hub.docker.com/_/traefik/) |
diff --git a/doc/md/Server-configuration.md b/doc/md/Server-configuration.md index 297d7c29..a49b6033 100644 --- a/doc/md/Server-configuration.md +++ b/doc/md/Server-configuration.md | |||
@@ -40,6 +40,8 @@ Supported PHP versions: | |||
40 | 40 | ||
41 | Version | Status | Shaarli compatibility | 41 | Version | Status | Shaarli compatibility |
42 | :---:|:---:|:---: | 42 | :---:|:---:|:---: |
43 | 8.0 | Supported | Yes | ||
44 | 7.4 | Supported | Yes | ||
43 | 7.3 | Supported | Yes | 45 | 7.3 | Supported | Yes |
44 | 7.2 | Supported | Yes | 46 | 7.2 | Supported | Yes |
45 | 7.1 | Supported | Yes | 47 | 7.1 | Supported | Yes |
@@ -53,7 +55,7 @@ Required PHP extensions: | |||
53 | 55 | ||
54 | Extension | Required? | Usage | 56 | Extension | Required? | Usage |
55 | ---|:---:|--- | 57 | ---|:---:|--- |
56 | [`openssl`](http://php.net/manual/en/book.openssl.php) | requires | OpenSSL, HTTPS | 58 | [`openssl`](http://php.net/manual/en/book.openssl.php) | required | OpenSSL, HTTPS |
57 | [`php-json`](http://php.net/manual/en/book.json.php) | required | configuration parsing | 59 | [`php-json`](http://php.net/manual/en/book.json.php) | required | configuration parsing |
58 | [`php-simplexml`](https://www.php.net/manual/en/book.simplexml.php) | required | REST API (Slim framework) | 60 | [`php-simplexml`](https://www.php.net/manual/en/book.simplexml.php) | required | REST API (Slim framework) |
59 | [`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support | 61 | [`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support |
@@ -191,19 +193,24 @@ sudo nano /etc/apache2/sites-available/shaarli.mydomain.org.conf | |||
191 | Require all granted | 193 | Require all granted |
192 | </Directory> | 194 | </Directory> |
193 | 195 | ||
194 | <LocationMatch "/\."> | 196 | # BE CAREFUL: directives order matter! |
195 | # Prevent accessing dotfiles | ||
196 | RedirectMatch 404 ".*" | ||
197 | </LocationMatch> | ||
198 | 197 | ||
199 | <LocationMatch "\.(?:ico|css|js|gif|jpe?g|png)$"> | 198 | <FilesMatch ".*\.(?!(ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$)[^\.]*$"> |
199 | Require all denied | ||
200 | </FilesMatch> | ||
201 | |||
202 | <Files "index.php"> | ||
203 | Require all granted | ||
204 | </Files> | ||
205 | |||
206 | <FilesMatch "\.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2)$"> | ||
200 | # allow client-side caching of static files | 207 | # allow client-side caching of static files |
201 | Header set Cache-Control "max-age=2628000, public, must-revalidate, proxy-revalidate" | 208 | Header set Cache-Control "max-age=2628000, public, must-revalidate, proxy-revalidate" |
202 | </LocationMatch> | 209 | </FilesMatch> |
210 | |||
203 | 211 | ||
204 | # serve the Shaarli favicon from its custom location | 212 | # serve the Shaarli favicon from its custom location |
205 | Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico | 213 | Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico |
206 | |||
207 | </VirtualHost> | 214 | </VirtualHost> |
208 | ``` | 215 | ``` |
209 | 216 | ||
@@ -294,7 +301,7 @@ server { | |||
294 | location / { | 301 | location / { |
295 | # default index file when no file URI is requested | 302 | # default index file when no file URI is requested |
296 | index index.php; | 303 | index index.php; |
297 | try_files $uri /index.php$is_args$args; | 304 | try_files _ /index.php$is_args$args; |
298 | } | 305 | } |
299 | 306 | ||
300 | location ~ (index)\.php$ { | 307 | location ~ (index)\.php$ { |
@@ -307,20 +314,9 @@ server { | |||
307 | include fastcgi.conf; | 314 | include fastcgi.conf; |
308 | } | 315 | } |
309 | 316 | ||
310 | location ~ \.php$ { | 317 | location ~ /doc/html/ { |
311 | # deny access to all other PHP scripts | 318 | default_type "text/html"; |
312 | # disable this if you host other PHP applications on the same virtualhost | 319 | try_files $uri $uri/ $uri.html =404; |
313 | deny all; | ||
314 | } | ||
315 | |||
316 | location ~ /\. { | ||
317 | # deny access to dotfiles | ||
318 | deny all; | ||
319 | } | ||
320 | |||
321 | location ~ ~$ { | ||
322 | # deny access to temp editor files, e.g. "script.php~" | ||
323 | deny all; | ||
324 | } | 320 | } |
325 | 321 | ||
326 | location = /favicon.ico { | 322 | location = /favicon.ico { |
@@ -329,13 +325,12 @@ server { | |||
329 | } | 325 | } |
330 | 326 | ||
331 | # allow client-side caching of static files | 327 | # allow client-side caching of static files |
332 | location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { | 328 | location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$ { |
333 | expires max; | 329 | expires max; |
334 | add_header Cache-Control "public, must-revalidate, proxy-revalidate"; | 330 | add_header Cache-Control "public, must-revalidate, proxy-revalidate"; |
335 | # HTTP 1.0 compatibility | 331 | # HTTP 1.0 compatibility |
336 | add_header Pragma public; | 332 | add_header Pragma public; |
337 | } | 333 | } |
338 | |||
339 | } | 334 | } |
340 | ``` | 335 | ``` |
341 | 336 | ||
@@ -360,7 +355,23 @@ sudo systemctl reload nginx | |||
360 | 355 | ||
361 | If Shaarli is hosted on a server behind a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) (i.e. there is a proxy server between clients and the web server hosting Shaarli), configure it accordingly. See [Reverse proxy](Reverse-proxy.md) configuration. | 356 | If Shaarli is hosted on a server behind a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) (i.e. there is a proxy server between clients and the web server hosting Shaarli), configure it accordingly. See [Reverse proxy](Reverse-proxy.md) configuration. |
362 | 357 | ||
358 | ## Using Shaarli without URL rewriting | ||
359 | |||
360 | By default, Shaarli uses Slim framework's URL, which requires | ||
361 | URL rewriting. | ||
362 | |||
363 | If you can't use URL rewriting for any reason (not supported by | ||
364 | your web server, shared hosting, etc.), you *can* use Shaarli | ||
365 | without URL rewriting. | ||
366 | |||
367 | You just need to prefix your URL by `/index.php/`. | ||
368 | Example: instead of accessing `https://shaarli.mydomain.org/`, | ||
369 | use `https://shaarli.mydomain.org/index.php/`. | ||
363 | 370 | ||
371 | **Recommended:** | ||
372 | * after installation, in the configuration page, set your header link to `/index.php/`. | ||
373 | * in your configuration file `config.json.php` set `general.root_url` to | ||
374 | `https://shaarli.mydomain.org/index.php/`. | ||
364 | 375 | ||
365 | ## Allow import of large browser bookmarks export | 376 | ## Allow import of large browser bookmarks export |
366 | 377 | ||
@@ -421,7 +432,7 @@ By default Shaarli already disallows indexing of your local copy of the document | |||
421 | before = common.conf | 432 | before = common.conf |
422 | [Definition] | 433 | [Definition] |
423 | failregex = \s-\s<HOST>\s-\sLogin failed for user.*$ | 434 | failregex = \s-\s<HOST>\s-\sLogin failed for user.*$ |
424 | ignoreregex = | 435 | ignoreregex = |
425 | ``` | 436 | ``` |
426 | 437 | ||
427 | ```ini | 438 | ```ini |
diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md index 263fb761..b1326cce 100644 --- a/doc/md/Shaarli-configuration.md +++ b/doc/md/Shaarli-configuration.md | |||
@@ -74,6 +74,7 @@ Some settings can be configured directly from a web browser by accesing the `Too | |||
74 | "timezone": "Europe\/Paris", | 74 | "timezone": "Europe\/Paris", |
75 | "title": "My Shaarli", | 75 | "title": "My Shaarli", |
76 | "header_link": "?" | 76 | "header_link": "?" |
77 | "tags_separator": " " | ||
77 | }, | 78 | }, |
78 | "dev": { | 79 | "dev": { |
79 | "debug": false, | 80 | "debug": false, |
@@ -150,8 +151,10 @@ _These settings should not be edited_ | |||
150 | - **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php). | 151 | - **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php). |
151 | - **enabled_plugins**: List of enabled plugins. | 152 | - **enabled_plugins**: List of enabled plugins. |
152 | - **default_note_title**: Default title of a new note. | 153 | - **default_note_title**: Default title of a new note. |
154 | - **enable_async_metadata** (boolean): Retrieve external bookmark metadata asynchronously to prevent bookmark creation slowdown. | ||
153 | - **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags. | 155 | - **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags. |
154 | - **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`. | 156 | - **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`. |
157 | - **tags_separator**: Defines your tags separator (default: whitespace). | ||
155 | 158 | ||
156 | ### Security | 159 | ### Security |
157 | 160 | ||
@@ -163,6 +166,22 @@ _These settings should not be edited_ | |||
163 | - **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy. | 166 | - **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy. |
164 | - **allowed_protocols**: List of allowed protocols in shaare URLs or markdown-rendered descriptions. Useful if you want to store `javascript:` links (bookmarklets) in Shaarli (default: `["ftp", "ftps", "magnet"]`). | 167 | - **allowed_protocols**: List of allowed protocols in shaare URLs or markdown-rendered descriptions. Useful if you want to store `javascript:` links (bookmarklets) in Shaarli (default: `["ftp", "ftps", "magnet"]`). |
165 | 168 | ||
169 | ### Formatter | ||
170 | |||
171 | Single string value. Default available: | ||
172 | |||
173 | - `default`: supports line breaks, URL and hashtag auto-links. | ||
174 | - `markdown`: supports [Markdown](https://daringfireball.net/projects/markdown/syntax). | ||
175 | - `markdownExtra`: adds [extra](https://michelf.ca/projects/php-markdown/extra/) flavor to Markdown. | ||
176 | |||
177 | ### Formatter Settings | ||
178 | |||
179 | Additional settings applied to formatters. | ||
180 | |||
181 | #### default | ||
182 | |||
183 | - **autolink**: boolean to enable or disable automatic linkification of URL and hashtags. | ||
184 | |||
166 | ### Resources | 185 | ### Resources |
167 | 186 | ||
168 | - **data_dir**: Data directory. | 187 | - **data_dir**: Data directory. |
diff --git a/doc/md/dev/Development.md b/doc/md/dev/Development.md index 5c085e03..c42e8ffe 100644 --- a/doc/md/dev/Development.md +++ b/doc/md/dev/Development.md | |||
@@ -6,7 +6,7 @@ Please read [Contributing to Shaarli](https://github.com/shaarli/Shaarli/tree/ma | |||
6 | 6 | ||
7 | 7 | ||
8 | - [Unit tests](Unit-tests) | 8 | - [Unit tests](Unit-tests) |
9 | - Javascript linting - Shaarli uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). | 9 | - Javascript linting - Shaarli uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). |
10 | Run `make eslint` to check JS style. | 10 | Run `make eslint` to check JS style. |
11 | - [GnuPG signature](GnuPG-signature) for tags/releases | 11 | - [GnuPG signature](GnuPG-signature) for tags/releases |
12 | 12 | ||
@@ -51,12 +51,12 @@ PHP (managed through [`composer.json`](https://github.com/shaarli/Shaarli/blob/m | |||
51 | 51 | ||
52 | ## Link structure | 52 | ## Link structure |
53 | 53 | ||
54 | Every link available through the `LinkDB` object is represented as an array | 54 | Every link available through the `LinkDB` object is represented as an array |
55 | containing the following fields: | 55 | containing the following fields: |
56 | 56 | ||
57 | * `id` (integer): Unique identifier. | 57 | * `id` (integer): Unique identifier. |
58 | * `title` (string): Title of the link. | 58 | * `title` (string): Title of the link. |
59 | * `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.). | 59 | * `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.). |
60 | Can be absolute or relative for Notes. | 60 | Can be absolute or relative for Notes. |
61 | * `real_url` (string): Real destination URL, can be redirected, encoded, etc. | 61 | * `real_url` (string): Real destination URL, can be redirected, encoded, etc. |
62 | * `shorturl` (string): Permalink small hash. | 62 | * `shorturl` (string): Permalink small hash. |
@@ -66,7 +66,7 @@ containing the following fields: | |||
66 | * `thumbnail` (string|boolean): relative path of the thumbnail cache file, or false if there isn't any. | 66 | * `thumbnail` (string|boolean): relative path of the thumbnail cache file, or false if there isn't any. |
67 | * `created` (DateTime): link creation date time. | 67 | * `created` (DateTime): link creation date time. |
68 | * `updated` (DateTime): last modification date time. | 68 | * `updated` (DateTime): last modification date time. |
69 | 69 | ||
70 | Small hashes are used to make a link to an entry in Shaarli. They are unique: the date of the item (eg. `20110923_150523`) is hashed with CRC32, then converted to base64 and some characters are replaced. They are always 6 characters longs and use only `A-Z a-z 0-9 - _` and `@`. | 70 | Small hashes are used to make a link to an entry in Shaarli. They are unique: the date of the item (eg. `20110923_150523`) is hashed with CRC32, then converted to base64 and some characters are replaced. They are always 6 characters longs and use only `A-Z a-z 0-9 - _` and `@`. |
71 | 71 | ||
72 | 72 | ||
@@ -163,11 +163,13 @@ See [`.travis.yml`](https://github.com/shaarli/Shaarli/blob/master/.travis.yml). | |||
163 | 163 | ||
164 | ## Static analysis | 164 | ## Static analysis |
165 | 165 | ||
166 | Patches should try to stick to the [PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially: | 166 | Patches should try to stick to the [PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), and must follow: |
167 | 167 | ||
168 | - [PSR-1](http://www.php-fig.org/psr/psr-1/) - Basic Coding Standard | 168 | - [PSR-1](http://www.php-fig.org/psr/psr-1/) - Basic Coding Standard |
169 | - [PSR-2](http://www.php-fig.org/psr/psr-2/) - Coding Style Guide | 169 | - [PSR-2](http://www.php-fig.org/psr/psr-2/) - Coding Style Guide |
170 | - [PSR-12](http://www.php-fig.org/psr/psr-12/) - Extended Coding Style Guide | ||
170 | 171 | ||
172 | These are enforced on pull requests using our Continuous Integration tools. | ||
171 | 173 | ||
172 | **Work in progress:** Static analysis is currently being discussed here: in [#95 - Fix coding style (static analysis)](https://github.com/shaarli/Shaarli/issues/95), [#130 - Continuous Integration tools & features](https://github.com/shaarli/Shaarli/issues/130) | 174 | **Work in progress:** Static analysis is currently being discussed here: in [#95 - Fix coding style (static analysis)](https://github.com/shaarli/Shaarli/issues/95), [#130 - Continuous Integration tools & features](https://github.com/shaarli/Shaarli/issues/130) |
173 | 175 | ||
diff --git a/doc/md/dev/Plugin-system.md b/doc/md/dev/Plugin-system.md index c29774de..f09fadc2 100644 --- a/doc/md/dev/Plugin-system.md +++ b/doc/md/dev/Plugin-system.md | |||
@@ -148,11 +148,16 @@ If a file needs to be included in server end, use simple relative path: | |||
148 | `PluginManager::$PLUGINS_PATH . '/mything/template.html'`. | 148 | `PluginManager::$PLUGINS_PATH . '/mything/template.html'`. |
149 | 149 | ||
150 | If it needs to be included in front end side (e.g. an image), | 150 | If it needs to be included in front end side (e.g. an image), |
151 | the relative path must be prefixed with special data `_BASE_PATH_`: | 151 | the relative path must be prefixed with special data: |
152 | `($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH . '/mything/picture.png`. | 152 | |
153 | * if it's a link that will need to be processed by Shaarli, use `_BASE_PATH_`: | ||
154 | for e.g. `$data['_BASE_PATH_'] . '/admin/tools`. | ||
155 | * if you want to include an asset, you need to add the root URL (base path without `/index.php`, for people using Shaarli without URL rewriting), then use `_ROOT_PATH_`: | ||
156 | for e.g | ||
157 | `$['_ROOT_PATH_'] . '/' . PluginManager::$PLUGINS_PATH . '/mything/picture.png`. | ||
153 | 158 | ||
154 | Note that special placeholders for CSS and JS files (respectively `css_files` and `js_files`) are already prefixed | 159 | Note that special placeholders for CSS and JS files (respectively `css_files` and `js_files`) are already prefixed |
155 | with the base path in template files. | 160 | with the root path in template files. |
156 | 161 | ||
157 | ### It's not working! | 162 | ### It's not working! |
158 | 163 | ||
diff --git a/docker-compose.yml b/docker-compose.yml index a3de4b1c..4ebae447 100644 --- a/docker-compose.yml +++ b/docker-compose.yml | |||
@@ -2,12 +2,13 @@ | |||
2 | # Shaarli - Docker Compose example configuration | 2 | # Shaarli - Docker Compose example configuration |
3 | # | 3 | # |
4 | # See: | 4 | # See: |
5 | # - https://shaarli.readthedocs.io/en/master/docker/shaarli-images/ | 5 | # - https://shaarli.readthedocs.io/en/master/Docker/#docker-compose |
6 | # - https://shaarli.readthedocs.io/en/master/guides/install-shaarli-with-debian9-and-docker/ | ||
7 | # | 6 | # |
8 | # Environment variables: | 7 | # Environment variables: |
9 | # - SHAARLI_VIRTUAL_HOST Fully Qualified Domain Name for the Shaarli instance | 8 | # - SHAARLI_VIRTUAL_HOST Fully Qualified Domain Name for the Shaarli instance |
10 | # - SHAARLI_LETSENCRYPT_EMAIL Contact email for certificate renewal | 9 | # - SHAARLI_LETSENCRYPT_EMAIL Contact email for certificate renewal |
10 | # - SHAARLI_DOCKER_TAG Shaarli docker tag to use | ||
11 | # See: https://hub.docker.com/r/shaarli/shaarli/tags | ||
11 | version: '3' | 12 | version: '3' |
12 | 13 | ||
13 | networks: | 14 | networks: |
@@ -20,7 +21,7 @@ volumes: | |||
20 | 21 | ||
21 | services: | 22 | services: |
22 | shaarli: | 23 | shaarli: |
23 | image: shaarli/shaarli:master | 24 | image: shaarli/shaarli:${SHAARLI_DOCKER_TAG} |
24 | build: ./ | 25 | build: ./ |
25 | networks: | 26 | networks: |
26 | - http-proxy | 27 | - http-proxy |
@@ -40,7 +41,7 @@ services: | |||
40 | - "--entrypoints=Name:https Address::443 TLS" | 41 | - "--entrypoints=Name:https Address::443 TLS" |
41 | - "--retry" | 42 | - "--retry" |
42 | - "--docker" | 43 | - "--docker" |
43 | - "--docker.domain=docker.localhost" | 44 | - "--docker.domain=${SHAARLI_VIRTUAL_HOST}" |
44 | - "--docker.exposedbydefault=true" | 45 | - "--docker.exposedbydefault=true" |
45 | - "--docker.watch=true" | 46 | - "--docker.watch=true" |
46 | - "--acme" | 47 | - "--acme" |
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 9a6e3958..26dede4e 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po | |||
@@ -1,8 +1,8 @@ | |||
1 | msgid "" | 1 | msgid "" |
2 | msgstr "" | 2 | msgstr "" |
3 | "Project-Id-Version: Shaarli\n" | 3 | "Project-Id-Version: Shaarli\n" |
4 | "POT-Creation-Date: 2020-09-10 16:06+0200\n" | 4 | "POT-Creation-Date: 2020-11-09 14:39+0100\n" |
5 | "PO-Revision-Date: 2020-09-10 16:07+0200\n" | 5 | "PO-Revision-Date: 2020-11-09 14:42+0100\n" |
6 | "Last-Translator: \n" | 6 | "Last-Translator: \n" |
7 | "Language-Team: Shaarli\n" | 7 | "Language-Team: Shaarli\n" |
8 | "Language: fr_FR\n" | 8 | "Language: fr_FR\n" |
@@ -20,38 +20,11 @@ msgstr "" | |||
20 | "X-Poedit-SearchPath-3: init.php\n" | 20 | "X-Poedit-SearchPath-3: init.php\n" |
21 | "X-Poedit-SearchPath-4: plugins\n" | 21 | "X-Poedit-SearchPath-4: plugins\n" |
22 | 22 | ||
23 | #: application/ApplicationUtils.php:161 | 23 | #: application/History.php:180 |
24 | #, php-format | ||
25 | msgid "" | ||
26 | "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " | ||
27 | "cannot run. Your PHP version has known security vulnerabilities and should " | ||
28 | "be updated as soon as possible." | ||
29 | msgstr "" | ||
30 | "Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne " | ||
31 | "peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " | ||
32 | "connues et devrait être mise à jour au plus tôt." | ||
33 | |||
34 | #: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204 | ||
35 | msgid "directory is not readable" | ||
36 | msgstr "le répertoire n'est pas accessible en lecture" | ||
37 | |||
38 | #: application/ApplicationUtils.php:207 | ||
39 | msgid "directory is not writable" | ||
40 | msgstr "le répertoire n'est pas accessible en écriture" | ||
41 | |||
42 | #: application/ApplicationUtils.php:225 | ||
43 | msgid "file is not readable" | ||
44 | msgstr "le fichier n'est pas accessible en lecture" | ||
45 | |||
46 | #: application/ApplicationUtils.php:228 | ||
47 | msgid "file is not writable" | ||
48 | msgstr "le fichier n'est pas accessible en écriture" | ||
49 | |||
50 | #: application/History.php:179 | ||
51 | msgid "History file isn't readable or writable" | 24 | msgid "History file isn't readable or writable" |
52 | msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" | 25 | msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" |
53 | 26 | ||
54 | #: application/History.php:190 | 27 | #: application/History.php:191 |
55 | msgid "Could not parse history file" | 28 | msgid "Could not parse history file" |
56 | msgstr "Format incorrect pour le fichier d'historique" | 29 | msgstr "Format incorrect pour le fichier d'historique" |
57 | 30 | ||
@@ -83,52 +56,46 @@ msgstr "" | |||
83 | "l'extension php-gd doit être chargée pour utiliser les miniatures. Les " | 56 | "l'extension php-gd doit être chargée pour utiliser les miniatures. Les " |
84 | "miniatures sont désormais désactivées. Rechargez la page." | 57 | "miniatures sont désormais désactivées. Rechargez la page." |
85 | 58 | ||
86 | #: application/Utils.php:383 | 59 | #: application/Utils.php:402 |
87 | msgid "Setting not set" | 60 | msgid "Setting not set" |
88 | msgstr "Paramètre non défini" | 61 | msgstr "Paramètre non défini" |
89 | 62 | ||
90 | #: application/Utils.php:390 | 63 | #: application/Utils.php:409 |
91 | msgid "Unlimited" | 64 | msgid "Unlimited" |
92 | msgstr "Illimité" | 65 | msgstr "Illimité" |
93 | 66 | ||
94 | #: application/Utils.php:393 | 67 | #: application/Utils.php:412 |
95 | msgid "B" | 68 | msgid "B" |
96 | msgstr "o" | 69 | msgstr "o" |
97 | 70 | ||
98 | #: application/Utils.php:393 | 71 | #: application/Utils.php:412 |
99 | msgid "kiB" | 72 | msgid "kiB" |
100 | msgstr "ko" | 73 | msgstr "ko" |
101 | 74 | ||
102 | #: application/Utils.php:393 | 75 | #: application/Utils.php:412 |
103 | msgid "MiB" | 76 | msgid "MiB" |
104 | msgstr "Mo" | 77 | msgstr "Mo" |
105 | 78 | ||
106 | #: application/Utils.php:393 | 79 | #: application/Utils.php:412 |
107 | msgid "GiB" | 80 | msgid "GiB" |
108 | msgstr "Go" | 81 | msgstr "Go" |
109 | 82 | ||
110 | #: application/bookmark/BookmarkFileService.php:174 | 83 | #: application/bookmark/BookmarkFileService.php:183 |
111 | #: application/bookmark/BookmarkFileService.php:199 | 84 | #: application/bookmark/BookmarkFileService.php:205 |
112 | #: application/bookmark/BookmarkFileService.php:224 | 85 | #: application/bookmark/BookmarkFileService.php:227 |
113 | #: application/bookmark/BookmarkFileService.php:241 | 86 | #: application/bookmark/BookmarkFileService.php:241 |
114 | msgid "You're not authorized to alter the datastore" | 87 | msgid "You're not authorized to alter the datastore" |
115 | msgstr "Vous n'êtes pas autorisé à modifier les données" | 88 | msgstr "Vous n'êtes pas autorisé à modifier les données" |
116 | 89 | ||
117 | #: application/bookmark/BookmarkFileService.php:177 | 90 | #: application/bookmark/BookmarkFileService.php:208 |
118 | #: application/bookmark/BookmarkFileService.php:202 | ||
119 | #: application/bookmark/BookmarkFileService.php:244 | ||
120 | msgid "Provided data is invalid" | ||
121 | msgstr "Les informations fournies ne sont pas valides" | ||
122 | |||
123 | #: application/bookmark/BookmarkFileService.php:205 | ||
124 | msgid "This bookmarks already exists" | 91 | msgid "This bookmarks already exists" |
125 | msgstr "Ce marque-page existe déjà ." | 92 | msgstr "Ce marque-page existe déjà " |
126 | 93 | ||
127 | #: application/bookmark/BookmarkInitializer.php:37 | 94 | #: application/bookmark/BookmarkInitializer.php:39 |
128 | msgid "(private bookmark with thumbnail demo)" | 95 | msgid "(private bookmark with thumbnail demo)" |
129 | msgstr "(marque page privé avec une miniature)" | 96 | msgstr "(marque page privé avec une miniature)" |
130 | 97 | ||
131 | #: application/bookmark/BookmarkInitializer.php:40 | 98 | #: application/bookmark/BookmarkInitializer.php:42 |
132 | msgid "" | 99 | msgid "" |
133 | "Shaarli will automatically pick up the thumbnail for links to a variety of " | 100 | "Shaarli will automatically pick up the thumbnail for links to a variety of " |
134 | "websites.\n" | 101 | "websites.\n" |
@@ -151,11 +118,11 @@ msgstr "" | |||
151 | "\n" | 118 | "\n" |
152 | "Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n" | 119 | "Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n" |
153 | 120 | ||
154 | #: application/bookmark/BookmarkInitializer.php:53 | 121 | #: application/bookmark/BookmarkInitializer.php:55 |
155 | msgid "Note: Shaare descriptions" | 122 | msgid "Note: Shaare descriptions" |
156 | msgstr "Note : Description des Shaares" | 123 | msgstr "Note : Description des Shaares" |
157 | 124 | ||
158 | #: application/bookmark/BookmarkInitializer.php:55 | 125 | #: application/bookmark/BookmarkInitializer.php:57 |
159 | msgid "" | 126 | msgid "" |
160 | "Adding a shaare without entering a URL creates a text-only \"note\" post " | 127 | "Adding a shaare without entering a URL creates a text-only \"note\" post " |
161 | "such as this one.\n" | 128 | "such as this one.\n" |
@@ -219,19 +186,19 @@ msgstr "" | |||
219 | "| Citron | Fruit | Jaune | 30 |\n" | 186 | "| Citron | Fruit | Jaune | 30 |\n" |
220 | "| Carotte | Légume | Orange | 14 |\n" | 187 | "| Carotte | Légume | Orange | 14 |\n" |
221 | 188 | ||
222 | #: application/bookmark/BookmarkInitializer.php:89 | 189 | #: application/bookmark/BookmarkInitializer.php:91 |
223 | #: application/legacy/LegacyLinkDB.php:246 | 190 | #: application/legacy/LegacyLinkDB.php:246 |
224 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | 191 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 |
225 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | 192 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 |
226 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 | 193 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 |
227 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49 | 194 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 |
228 | msgid "" | 195 | msgid "" |
229 | "The personal, minimalist, super-fast, database free, bookmarking service" | 196 | "The personal, minimalist, super-fast, database free, bookmarking service" |
230 | msgstr "" | 197 | msgstr "" |
231 | "Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " | 198 | "Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " |
232 | "données" | 199 | "données" |
233 | 200 | ||
234 | #: application/bookmark/BookmarkInitializer.php:92 | 201 | #: application/bookmark/BookmarkInitializer.php:94 |
235 | msgid "" | 202 | msgid "" |
236 | "Welcome to Shaarli!\n" | 203 | "Welcome to Shaarli!\n" |
237 | "\n" | 204 | "\n" |
@@ -320,7 +287,8 @@ msgid "Direct link" | |||
320 | msgstr "Liens directs" | 287 | msgstr "Liens directs" |
321 | 288 | ||
322 | #: application/feed/FeedBuilder.php:181 | 289 | #: application/feed/FeedBuilder.php:181 |
323 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 | 290 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 |
291 | #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | ||
324 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 | 292 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 |
325 | msgid "Permalink" | 293 | msgid "Permalink" |
326 | msgstr "Permalien" | 294 | msgstr "Permalien" |
@@ -336,12 +304,13 @@ msgid "You have enabled or changed thumbnails mode." | |||
336 | msgstr "Vous avez activé ou changé le mode de miniatures." | 304 | msgstr "Vous avez activé ou changé le mode de miniatures." |
337 | 305 | ||
338 | #: application/front/controller/admin/ConfigureController.php:103 | 306 | #: application/front/controller/admin/ConfigureController.php:103 |
307 | #: application/front/controller/admin/ServerController.php:75 | ||
339 | #: application/legacy/LegacyUpdater.php:538 | 308 | #: application/legacy/LegacyUpdater.php:538 |
340 | msgid "Please synchronize them." | 309 | msgid "Please synchronize them." |
341 | msgstr "Merci de les synchroniser." | 310 | msgstr "Merci de les synchroniser." |
342 | 311 | ||
343 | #: application/front/controller/admin/ConfigureController.php:113 | 312 | #: application/front/controller/admin/ConfigureController.php:113 |
344 | #: application/front/controller/visitor/InstallController.php:136 | 313 | #: application/front/controller/visitor/InstallController.php:146 |
345 | msgid "Error while writing config file after configuration update." | 314 | msgid "Error while writing config file after configuration update." |
346 | msgstr "" | 315 | msgstr "" |
347 | "Une erreur s'est produite lors de la sauvegarde du fichier de configuration." | 316 | "Une erreur s'est produite lors de la sauvegarde du fichier de configuration." |
@@ -378,70 +347,47 @@ msgstr "" | |||
378 | "le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " | 347 | "le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " |
379 | "légères." | 348 | "légères." |
380 | 349 | ||
381 | #: application/front/controller/admin/ManageShaareController.php:29 | 350 | #: application/front/controller/admin/ManageTagController.php:30 |
382 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 351 | msgid "whitespace" |
383 | msgid "Shaare a new link" | 352 | msgstr "espace" |
384 | msgstr "Partager un nouveau lien" | ||
385 | |||
386 | #: application/front/controller/admin/ManageShaareController.php:78 | ||
387 | msgid "Note: " | ||
388 | msgstr "Note : " | ||
389 | |||
390 | #: application/front/controller/admin/ManageShaareController.php:109 | ||
391 | #: application/front/controller/admin/ManageShaareController.php:206 | ||
392 | #: application/front/controller/admin/ManageShaareController.php:275 | ||
393 | #: application/front/controller/admin/ManageShaareController.php:315 | ||
394 | #, php-format | ||
395 | msgid "Bookmark with identifier %s could not be found." | ||
396 | msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." | ||
397 | |||
398 | #: application/front/controller/admin/ManageShaareController.php:194 | ||
399 | #: application/front/controller/admin/ManageShaareController.php:252 | ||
400 | msgid "Invalid bookmark ID provided." | ||
401 | msgstr "ID du lien non valide." | ||
402 | 353 | ||
403 | #: application/front/controller/admin/ManageShaareController.php:260 | 354 | #: application/front/controller/admin/ManageTagController.php:35 |
404 | msgid "Invalid visibility provided." | ||
405 | msgstr "Visibilité du lien non valide." | ||
406 | |||
407 | #: application/front/controller/admin/ManageShaareController.php:363 | ||
408 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 | ||
409 | msgid "Edit" | ||
410 | msgstr "Modifier" | ||
411 | |||
412 | #: application/front/controller/admin/ManageShaareController.php:366 | ||
413 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
414 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 | ||
415 | msgid "Shaare" | ||
416 | msgstr "Shaare" | ||
417 | |||
418 | #: application/front/controller/admin/ManageTagController.php:29 | ||
419 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 355 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 |
420 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | 356 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 |
421 | msgid "Manage tags" | 357 | msgid "Manage tags" |
422 | msgstr "Gérer les tags" | 358 | msgstr "Gérer les tags" |
423 | 359 | ||
424 | #: application/front/controller/admin/ManageTagController.php:48 | 360 | #: application/front/controller/admin/ManageTagController.php:54 |
425 | msgid "Invalid tags provided." | 361 | msgid "Invalid tags provided." |
426 | msgstr "Les tags fournis ne sont pas valides." | 362 | msgstr "Les tags fournis ne sont pas valides." |
427 | 363 | ||
428 | #: application/front/controller/admin/ManageTagController.php:72 | 364 | #: application/front/controller/admin/ManageTagController.php:78 |
429 | #, php-format | 365 | #, php-format |
430 | msgid "The tag was removed from %d bookmark." | 366 | msgid "The tag was removed from %d bookmark." |
431 | msgid_plural "The tag was removed from %d bookmarks." | 367 | msgid_plural "The tag was removed from %d bookmarks." |
432 | msgstr[0] "Le tag a été supprimé du %d lien." | 368 | msgstr[0] "Le tag a été supprimé du %d lien." |
433 | msgstr[1] "Le tag a été supprimé de %d liens." | 369 | msgstr[1] "Le tag a été supprimé de %d liens." |
434 | 370 | ||
435 | #: application/front/controller/admin/ManageTagController.php:77 | 371 | #: application/front/controller/admin/ManageTagController.php:83 |
436 | #, php-format | 372 | #, php-format |
437 | msgid "The tag was renamed in %d bookmark." | 373 | msgid "The tag was renamed in %d bookmark." |
438 | msgid_plural "The tag was renamed in %d bookmarks." | 374 | msgid_plural "The tag was renamed in %d bookmarks." |
439 | msgstr[0] "Le tag a été renommé dans %d lien." | 375 | msgstr[0] "Le tag a été renommé dans %d lien." |
440 | msgstr[1] "Le tag a été renommé dans %d liens." | 376 | msgstr[1] "Le tag a été renommé dans %d liens." |
441 | 377 | ||
378 | #: application/front/controller/admin/ManageTagController.php:105 | ||
379 | msgid "Tags separator must be a single character." | ||
380 | msgstr "Un séparateur de tags doit contenir un seul caractère." | ||
381 | |||
382 | #: application/front/controller/admin/ManageTagController.php:111 | ||
383 | msgid "These characters are reserved and can't be used as tags separator: " | ||
384 | msgstr "" | ||
385 | "Ces caractères sont réservés et ne peuvent être utilisés comme des " | ||
386 | "séparateurs de tags : " | ||
387 | |||
442 | #: application/front/controller/admin/PasswordController.php:28 | 388 | #: application/front/controller/admin/PasswordController.php:28 |
443 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 389 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 |
444 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | 390 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 |
445 | msgid "Change password" | 391 | msgid "Change password" |
446 | msgstr "Modifier le mot de passe" | 392 | msgstr "Modifier le mot de passe" |
447 | 393 | ||
@@ -463,16 +409,71 @@ msgstr "Votre mot de passe a été modifié" | |||
463 | msgid "Plugin Administration" | 409 | msgid "Plugin Administration" |
464 | msgstr "Administration des plugins" | 410 | msgstr "Administration des plugins" |
465 | 411 | ||
466 | #: application/front/controller/admin/PluginsController.php:75 | 412 | #: application/front/controller/admin/PluginsController.php:76 |
467 | msgid "Setting successfully saved." | 413 | msgid "Setting successfully saved." |
468 | msgstr "Les paramètres ont été sauvegardés avec succès." | 414 | msgstr "Les paramètres ont été sauvegardés avec succès." |
469 | 415 | ||
470 | #: application/front/controller/admin/PluginsController.php:78 | 416 | #: application/front/controller/admin/PluginsController.php:79 |
471 | msgid "Error while saving plugin configuration: " | 417 | msgid "Error while saving plugin configuration: " |
472 | msgstr "" | 418 | msgstr "" |
473 | "Une erreur s'est produite lors de la sauvegarde de la configuration des " | 419 | "Une erreur s'est produite lors de la sauvegarde de la configuration des " |
474 | "plugins : " | 420 | "plugins : " |
475 | 421 | ||
422 | #: application/front/controller/admin/ServerController.php:35 | ||
423 | msgid "Check disabled" | ||
424 | msgstr "Vérification désactivée" | ||
425 | |||
426 | #: application/front/controller/admin/ServerController.php:57 | ||
427 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
428 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
429 | msgid "Server administration" | ||
430 | msgstr "Administration serveur" | ||
431 | |||
432 | #: application/front/controller/admin/ServerController.php:74 | ||
433 | msgid "Thumbnails cache has been cleared." | ||
434 | msgstr "Le cache des miniatures a été vidé." | ||
435 | |||
436 | #: application/front/controller/admin/ServerController.php:83 | ||
437 | msgid "Shaarli's cache folder has been cleared!" | ||
438 | msgstr "Le dossier de cache de Shaarli a été vidé !" | ||
439 | |||
440 | #: application/front/controller/admin/ShaareAddController.php:26 | ||
441 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
442 | msgid "Shaare a new link" | ||
443 | msgstr "Partagez un nouveau lien" | ||
444 | |||
445 | #: application/front/controller/admin/ShaareManageController.php:35 | ||
446 | #: application/front/controller/admin/ShaareManageController.php:93 | ||
447 | msgid "Invalid bookmark ID provided." | ||
448 | msgstr "L'ID du marque-page fourni n'est pas valide." | ||
449 | |||
450 | #: application/front/controller/admin/ShaareManageController.php:47 | ||
451 | #: application/front/controller/admin/ShaareManageController.php:116 | ||
452 | #: application/front/controller/admin/ShaareManageController.php:156 | ||
453 | #: application/front/controller/admin/ShaarePublishController.php:82 | ||
454 | #, php-format | ||
455 | msgid "Bookmark with identifier %s could not be found." | ||
456 | msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." | ||
457 | |||
458 | #: application/front/controller/admin/ShaareManageController.php:101 | ||
459 | msgid "Invalid visibility provided." | ||
460 | msgstr "Visibilité du lien non valide." | ||
461 | |||
462 | #: application/front/controller/admin/ShaarePublishController.php:171 | ||
463 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 | ||
464 | msgid "Edit" | ||
465 | msgstr "Modifier" | ||
466 | |||
467 | #: application/front/controller/admin/ShaarePublishController.php:174 | ||
468 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
469 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 | ||
470 | msgid "Shaare" | ||
471 | msgstr "Shaare" | ||
472 | |||
473 | #: application/front/controller/admin/ShaarePublishController.php:205 | ||
474 | msgid "Note: " | ||
475 | msgstr "Note : " | ||
476 | |||
476 | #: application/front/controller/admin/ThumbnailsController.php:37 | 477 | #: application/front/controller/admin/ThumbnailsController.php:37 |
477 | #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | 478 | #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 |
478 | msgid "Thumbnails update" | 479 | msgid "Thumbnails update" |
@@ -484,29 +485,62 @@ msgstr "Mise à jour des miniatures" | |||
484 | msgid "Tools" | 485 | msgid "Tools" |
485 | msgstr "Outils" | 486 | msgstr "Outils" |
486 | 487 | ||
487 | #: application/front/controller/visitor/BookmarkListController.php:115 | 488 | #: application/front/controller/visitor/BookmarkListController.php:120 |
488 | msgid "Search: " | 489 | msgid "Search: " |
489 | msgstr "Recherche : " | 490 | msgstr "Recherche : " |
490 | 491 | ||
491 | #: application/front/controller/visitor/DailyController.php:45 | 492 | #: application/front/controller/visitor/DailyController.php:200 |
492 | msgid "Today" | 493 | msgid "day" |
493 | msgstr "Aujourd'hui" | 494 | msgstr "jour" |
494 | |||
495 | #: application/front/controller/visitor/DailyController.php:47 | ||
496 | msgid "Yesterday" | ||
497 | msgstr "Hier" | ||
498 | 495 | ||
499 | #: application/front/controller/visitor/DailyController.php:85 | 496 | #: application/front/controller/visitor/DailyController.php:200 |
497 | #: application/front/controller/visitor/DailyController.php:203 | ||
498 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
500 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | 499 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 |
501 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 | 500 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 |
502 | msgid "Daily" | 501 | msgid "Daily" |
503 | msgstr "Quotidien" | 502 | msgstr "Quotidien" |
504 | 503 | ||
505 | #: application/front/controller/visitor/ErrorController.php:36 | 504 | #: application/front/controller/visitor/DailyController.php:201 |
505 | msgid "week" | ||
506 | msgstr "semaine" | ||
507 | |||
508 | #: application/front/controller/visitor/DailyController.php:201 | ||
509 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
510 | msgid "Weekly" | ||
511 | msgstr "Hebdomadaire" | ||
512 | |||
513 | #: application/front/controller/visitor/DailyController.php:202 | ||
514 | msgid "month" | ||
515 | msgstr "mois" | ||
516 | |||
517 | #: application/front/controller/visitor/DailyController.php:202 | ||
518 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
519 | msgid "Monthly" | ||
520 | msgstr "Mensuel" | ||
521 | |||
522 | #: application/front/controller/visitor/ErrorController.php:30 | ||
523 | msgid "Error: " | ||
524 | msgstr "Erreur : " | ||
525 | |||
526 | #: application/front/controller/visitor/ErrorController.php:34 | ||
527 | msgid "Please report it on Github." | ||
528 | msgstr "Merci de la rapporter sur Github." | ||
529 | |||
530 | #: application/front/controller/visitor/ErrorController.php:39 | ||
506 | msgid "An unexpected error occurred." | 531 | msgid "An unexpected error occurred." |
507 | msgstr "Une erreur inattendue s'est produite." | 532 | msgstr "Une erreur inattendue s'est produite." |
508 | 533 | ||
509 | #: application/front/controller/visitor/InstallController.php:73 | 534 | #: application/front/controller/visitor/ErrorNotFoundController.php:25 |
535 | msgid "Requested page could not be found." | ||
536 | msgstr "La page demandée n'a pas pu être trouvée." | ||
537 | |||
538 | #: application/front/controller/visitor/InstallController.php:64 | ||
539 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | ||
540 | msgid "Install Shaarli" | ||
541 | msgstr "Installation de Shaarli" | ||
542 | |||
543 | #: application/front/controller/visitor/InstallController.php:83 | ||
510 | #, php-format | 544 | #, php-format |
511 | msgid "" | 545 | msgid "" |
512 | "<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " | 546 | "<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " |
@@ -525,14 +559,14 @@ msgstr "" | |||
525 | "des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " | 559 | "des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " |
526 | "adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" | 560 | "adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" |
527 | 561 | ||
528 | #: application/front/controller/visitor/InstallController.php:144 | 562 | #: application/front/controller/visitor/InstallController.php:154 |
529 | msgid "" | 563 | msgid "" |
530 | "Shaarli is now configured. Please login and start shaaring your bookmarks!" | 564 | "Shaarli is now configured. Please login and start shaaring your bookmarks!" |
531 | msgstr "" | 565 | msgstr "" |
532 | "Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " | 566 | "Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " |
533 | "shaare vos liens !" | 567 | "shaare vos liens !" |
534 | 568 | ||
535 | #: application/front/controller/visitor/InstallController.php:158 | 569 | #: application/front/controller/visitor/InstallController.php:168 |
536 | msgid "Insufficient permissions:" | 570 | msgid "Insufficient permissions:" |
537 | msgstr "Permissions insuffisantes :" | 571 | msgstr "Permissions insuffisantes :" |
538 | 572 | ||
@@ -546,7 +580,7 @@ msgstr "Permissions insuffisantes :" | |||
546 | msgid "Login" | 580 | msgid "Login" |
547 | msgstr "Connexion" | 581 | msgstr "Connexion" |
548 | 582 | ||
549 | #: application/front/controller/visitor/LoginController.php:78 | 583 | #: application/front/controller/visitor/LoginController.php:77 |
550 | msgid "Wrong login/password." | 584 | msgid "Wrong login/password." |
551 | msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." | 585 | msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." |
552 | 586 | ||
@@ -556,11 +590,9 @@ msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." | |||
556 | msgid "Picture wall" | 590 | msgid "Picture wall" |
557 | msgstr "Mur d'images" | 591 | msgstr "Mur d'images" |
558 | 592 | ||
559 | #: application/front/controller/visitor/TagCloudController.php:80 | 593 | #: application/front/controller/visitor/TagCloudController.php:90 |
560 | #, fuzzy | ||
561 | #| msgid "Tag list" | ||
562 | msgid "Tag " | 594 | msgid "Tag " |
563 | msgstr "Liste des tags" | 595 | msgstr "Tag " |
564 | 596 | ||
565 | #: application/front/exceptions/AlreadyInstalledException.php:11 | 597 | #: application/front/exceptions/AlreadyInstalledException.php:11 |
566 | msgid "Shaarli has already been installed. Login to edit the configuration." | 598 | msgid "Shaarli has already been installed. Login to edit the configuration." |
@@ -588,6 +620,86 @@ msgstr "" | |||
588 | msgid "Wrong token." | 620 | msgid "Wrong token." |
589 | msgstr "Jeton invalide." | 621 | msgstr "Jeton invalide." |
590 | 622 | ||
623 | #: application/helper/ApplicationUtils.php:162 | ||
624 | #, php-format | ||
625 | msgid "" | ||
626 | "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " | ||
627 | "cannot run. Your PHP version has known security vulnerabilities and should " | ||
628 | "be updated as soon as possible." | ||
629 | msgstr "" | ||
630 | "Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne " | ||
631 | "peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " | ||
632 | "connues et devrait être mise à jour au plus tôt." | ||
633 | |||
634 | #: application/helper/ApplicationUtils.php:195 | ||
635 | #: application/helper/ApplicationUtils.php:215 | ||
636 | msgid "directory is not readable" | ||
637 | msgstr "le répertoire n'est pas accessible en lecture" | ||
638 | |||
639 | #: application/helper/ApplicationUtils.php:218 | ||
640 | msgid "directory is not writable" | ||
641 | msgstr "le répertoire n'est pas accessible en écriture" | ||
642 | |||
643 | #: application/helper/ApplicationUtils.php:240 | ||
644 | msgid "file is not readable" | ||
645 | msgstr "le fichier n'est pas accessible en lecture" | ||
646 | |||
647 | #: application/helper/ApplicationUtils.php:243 | ||
648 | msgid "file is not writable" | ||
649 | msgstr "le fichier n'est pas accessible en écriture" | ||
650 | |||
651 | #: application/helper/ApplicationUtils.php:277 | ||
652 | msgid "Configuration parsing" | ||
653 | msgstr "Chargement de la configuration" | ||
654 | |||
655 | #: application/helper/ApplicationUtils.php:278 | ||
656 | msgid "Slim Framework (routing, etc.)" | ||
657 | msgstr "Slim Framwork (routage, etc.)" | ||
658 | |||
659 | #: application/helper/ApplicationUtils.php:279 | ||
660 | msgid "Multibyte (Unicode) string support" | ||
661 | msgstr "Support des chaînes de caractère multibytes (Unicode)" | ||
662 | |||
663 | #: application/helper/ApplicationUtils.php:280 | ||
664 | msgid "Required to use thumbnails" | ||
665 | msgstr "Obligatoire pour utiliser les miniatures" | ||
666 | |||
667 | #: application/helper/ApplicationUtils.php:281 | ||
668 | msgid "Localized text sorting (e.g. e->è->f)" | ||
669 | msgstr "Tri des textes traduits (ex : e->è->f)" | ||
670 | |||
671 | #: application/helper/ApplicationUtils.php:282 | ||
672 | msgid "Better retrieval of bookmark metadata and thumbnail" | ||
673 | msgstr "Meilleure récupération des meta-données des marque-pages et minatures" | ||
674 | |||
675 | #: application/helper/ApplicationUtils.php:283 | ||
676 | msgid "Use the translation system in gettext mode" | ||
677 | msgstr "Utiliser le système de traduction en mode gettext" | ||
678 | |||
679 | #: application/helper/ApplicationUtils.php:284 | ||
680 | msgid "Login using LDAP server" | ||
681 | msgstr "Authentification via un serveur LDAP" | ||
682 | |||
683 | #: application/helper/DailyPageHelper.php:172 | ||
684 | msgid "Week" | ||
685 | msgstr "Semaine" | ||
686 | |||
687 | #: application/helper/DailyPageHelper.php:176 | ||
688 | msgid "Today" | ||
689 | msgstr "Aujourd'hui" | ||
690 | |||
691 | #: application/helper/DailyPageHelper.php:178 | ||
692 | msgid "Yesterday" | ||
693 | msgstr "Hier" | ||
694 | |||
695 | #: application/helper/FileUtils.php:100 | ||
696 | msgid "Provided path is not a directory." | ||
697 | msgstr "Le chemin fourni n'est pas un dossier." | ||
698 | |||
699 | #: application/helper/FileUtils.php:104 | ||
700 | msgid "Trying to delete a folder outside of Shaarli path." | ||
701 | msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli." | ||
702 | |||
591 | #: application/legacy/LegacyLinkDB.php:131 | 703 | #: application/legacy/LegacyLinkDB.php:131 |
592 | msgid "You are not authorized to add a link." | 704 | msgid "You are not authorized to add a link." |
593 | msgstr "Vous n'êtes pas autorisé à ajouter un lien." | 705 | msgstr "Vous n'êtes pas autorisé à ajouter un lien." |
@@ -664,7 +776,7 @@ msgstr "" | |||
664 | "a été importé avec succès en %d secondes : %d liens importés, %d liens " | 776 | "a été importé avec succès en %d secondes : %d liens importés, %d liens " |
665 | "écrasés, %d liens ignorés." | 777 | "écrasés, %d liens ignorés." |
666 | 778 | ||
667 | #: application/plugin/PluginManager.php:122 | 779 | #: application/plugin/PluginManager.php:124 |
668 | msgid " [plugin incompatibility]: " | 780 | msgid " [plugin incompatibility]: " |
669 | msgstr " [incompatibilité de l'extension] : " | 781 | msgstr " [incompatibilité de l'extension] : " |
670 | 782 | ||
@@ -682,7 +794,7 @@ msgstr "Impossible de purger %s : le répertoire n'existe pas" | |||
682 | msgid "An error occurred while running the update " | 794 | msgid "An error occurred while running the update " |
683 | msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " | 795 | msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " |
684 | 796 | ||
685 | #: index.php:62 | 797 | #: index.php:80 |
686 | msgid "Shared bookmarks on " | 798 | msgid "Shared bookmarks on " |
687 | msgstr "Liens partagés sur " | 799 | msgstr "Liens partagés sur " |
688 | 800 | ||
@@ -699,11 +811,11 @@ msgstr "Shaare" | |||
699 | msgid "Adds the addlink input on the linklist page." | 811 | msgid "Adds the addlink input on the linklist page." |
700 | msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." | 812 | msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." |
701 | 813 | ||
702 | #: plugins/archiveorg/archiveorg.php:26 | 814 | #: plugins/archiveorg/archiveorg.php:28 |
703 | msgid "View on archive.org" | 815 | msgid "View on archive.org" |
704 | msgstr "Voir sur archive.org" | 816 | msgstr "Voir sur archive.org" |
705 | 817 | ||
706 | #: plugins/archiveorg/archiveorg.php:39 | 818 | #: plugins/archiveorg/archiveorg.php:41 |
707 | msgid "For each link, add an Archive.org icon." | 819 | msgid "For each link, add an Archive.org icon." |
708 | msgstr "Pour chaque lien, ajoute une icône pour Archive.org." | 820 | msgstr "Pour chaque lien, ajoute une icône pour Archive.org." |
709 | 821 | ||
@@ -823,7 +935,7 @@ msgstr "Mauvaise réponse du hub %s" | |||
823 | msgid "Enable PubSubHubbub feed publishing." | 935 | msgid "Enable PubSubHubbub feed publishing." |
824 | msgstr "Active la publication de flux vers PubSubHubbub." | 936 | msgstr "Active la publication de flux vers PubSubHubbub." |
825 | 937 | ||
826 | #: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70 | 938 | #: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:71 |
827 | msgid "For each link, add a QRCode icon." | 939 | msgid "For each link, add a QRCode icon." |
828 | msgstr "Pour chaque lien, ajouter une icône de QRCode." | 940 | msgstr "Pour chaque lien, ajouter une icône de QRCode." |
829 | 941 | ||
@@ -835,15 +947,15 @@ msgstr "" | |||
835 | "Erreur de l'extension Wallabag : Merci de définir le paramètre « " | 947 | "Erreur de l'extension Wallabag : Merci de définir le paramètre « " |
836 | "WALLABAG_URL » dans la page d'administration des extensions." | 948 | "WALLABAG_URL » dans la page d'administration des extensions." |
837 | 949 | ||
838 | #: plugins/wallabag/wallabag.php:47 | 950 | #: plugins/wallabag/wallabag.php:48 |
839 | msgid "Save to wallabag" | 951 | msgid "Save to wallabag" |
840 | msgstr "Sauvegarder dans Wallabag" | 952 | msgstr "Sauvegarder dans Wallabag" |
841 | 953 | ||
842 | #: plugins/wallabag/wallabag.php:71 | 954 | #: plugins/wallabag/wallabag.php:72 |
843 | msgid "Wallabag API URL" | 955 | msgid "Wallabag API URL" |
844 | msgstr "URL de l'API Wallabag" | 956 | msgstr "URL de l'API Wallabag" |
845 | 957 | ||
846 | #: plugins/wallabag/wallabag.php:72 | 958 | #: plugins/wallabag/wallabag.php:73 |
847 | msgid "Wallabag API version (1 or 2)" | 959 | msgid "Wallabag API version (1 or 2)" |
848 | msgstr "Version de l'API Wallabag (1 ou 2)" | 960 | msgstr "Version de l'API Wallabag (1 ou 2)" |
849 | 961 | ||
@@ -855,6 +967,48 @@ msgstr "Désolé, il y a rien à voir ici." | |||
855 | msgid "URL or leave empty to post a note" | 967 | msgid "URL or leave empty to post a note" |
856 | msgstr "URL ou laisser vide pour créer une note" | 968 | msgstr "URL ou laisser vide pour créer une note" |
857 | 969 | ||
970 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
971 | msgid "BULK CREATION" | ||
972 | msgstr "CRÉATION DE MASSE" | ||
973 | |||
974 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
975 | msgid "Metadata asynchronous retrieval is disabled." | ||
976 | msgstr "La récupération asynchrone des meta-données est désactivée." | ||
977 | |||
978 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
979 | msgid "" | ||
980 | "We recommend that you enable the setting <em>general > " | ||
981 | "enable_async_metadata</em> in your configuration file to use bulk link " | ||
982 | "creation." | ||
983 | msgstr "" | ||
984 | "Nous recommandons d'activer le paramètre <em>general > " | ||
985 | "enable_async_metadata</em> dans votre fichier de configuration pour utiliser " | ||
986 | "la création de masse." | ||
987 | |||
988 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 | ||
989 | msgid "Shaare multiple new links" | ||
990 | msgstr "Partagez plusieurs nouveaux liens" | ||
991 | |||
992 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 | ||
993 | msgid "Add one URL per line to create multiple bookmarks." | ||
994 | msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages." | ||
995 | |||
996 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 | ||
997 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 | ||
998 | msgid "Tags" | ||
999 | msgstr "Tags" | ||
1000 | |||
1001 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 | ||
1002 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 | ||
1003 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
1004 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
1005 | msgid "Private" | ||
1006 | msgstr "Privé" | ||
1007 | |||
1008 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 | ||
1009 | msgid "Add links" | ||
1010 | msgstr "Ajouter des liens" | ||
1011 | |||
858 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | 1012 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 |
859 | msgid "Current password" | 1013 | msgid "Current password" |
860 | msgstr "Mot de passe actuel" | 1014 | msgstr "Mot de passe actuel" |
@@ -881,26 +1035,48 @@ msgid "Case sensitive" | |||
881 | msgstr "Sensible à la casse" | 1035 | msgstr "Sensible à la casse" |
882 | 1036 | ||
883 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | 1037 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 |
884 | msgid "Rename" | 1038 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 |
885 | msgstr "Renommer" | 1039 | msgid "Rename tag" |
1040 | msgstr "Renommer le tag" | ||
886 | 1041 | ||
887 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | 1042 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 |
888 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93 | 1043 | msgid "Delete tag" |
889 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 | 1044 | msgstr "Supprimer le tag" |
890 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 | ||
891 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 | ||
892 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 | ||
893 | msgid "Delete" | ||
894 | msgstr "Supprimer" | ||
895 | 1045 | ||
896 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | 1046 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 |
897 | msgid "You can also edit tags in the" | 1047 | msgid "You can also edit tags in the" |
898 | msgstr "Vous pouvez aussi modifier les tags dans la" | 1048 | msgstr "Vous pouvez aussi modifier les tags dans la" |
899 | 1049 | ||
900 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | 1050 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 |
901 | msgid "tag list" | 1051 | msgid "tag list" |
902 | msgstr "liste des tags" | 1052 | msgstr "liste des tags" |
903 | 1053 | ||
1054 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | ||
1055 | msgid "Change tags separator" | ||
1056 | msgstr "Changer le séparateur de tags" | ||
1057 | |||
1058 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 | ||
1059 | msgid "Your current tag separator is" | ||
1060 | msgstr "Votre séparateur actuel est" | ||
1061 | |||
1062 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 | ||
1063 | msgid "New separator" | ||
1064 | msgstr "Nouveau séparateur" | ||
1065 | |||
1066 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1067 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 | ||
1068 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1069 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | ||
1070 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 | ||
1071 | msgid "Save" | ||
1072 | msgstr "Enregistrer" | ||
1073 | |||
1074 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 | ||
1075 | msgid "Note that hashtags won't fully work with a non-whitespace separator." | ||
1076 | msgstr "" | ||
1077 | "Notez que les hashtags ne sont pas complètement fonctionnels avec un " | ||
1078 | "séparateur qui n'est pas un espace." | ||
1079 | |||
904 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | 1080 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 |
905 | msgid "title" | 1081 | msgid "title" |
906 | msgstr "titre" | 1082 | msgstr "titre" |
@@ -1024,71 +1200,72 @@ msgstr "" | |||
1024 | "miniatures." | 1200 | "miniatures." |
1025 | 1201 | ||
1026 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 | 1202 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 |
1027 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 | 1203 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 |
1028 | msgid "Synchronize thumbnails" | 1204 | msgid "Synchronize thumbnails" |
1029 | msgstr "Synchroniser les miniatures" | 1205 | msgstr "Synchroniser les miniatures" |
1030 | 1206 | ||
1031 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 | 1207 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 |
1032 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 | 1208 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 |
1209 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
1033 | msgid "All" | 1210 | msgid "All" |
1034 | msgstr "Tous" | 1211 | msgstr "Tous" |
1035 | 1212 | ||
1036 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 | 1213 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 |
1214 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 | ||
1037 | msgid "Only common media hosts" | 1215 | msgid "Only common media hosts" |
1038 | msgstr "Seulement les hébergeurs de média connus" | 1216 | msgstr "Seulement les hébergeurs de média connus" |
1039 | 1217 | ||
1040 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 | 1218 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 |
1219 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 | ||
1041 | msgid "None" | 1220 | msgid "None" |
1042 | msgstr "Aucune" | 1221 | msgstr "Aucune" |
1043 | 1222 | ||
1044 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 | 1223 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 |
1045 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 | 1224 | msgid "1 RSS entry per :type" |
1046 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | 1225 | msgid_plural "" |
1047 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 | 1226 | msgstr[0] "1 entrée RSS par :type" |
1048 | msgid "Save" | 1227 | msgstr[1] "" |
1049 | msgstr "Enregistrer" | 1228 | |
1050 | 1229 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | |
1051 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | 1230 | msgid "Previous :type" |
1052 | msgid "The Daily Shaarli" | 1231 | msgid_plural "" |
1053 | msgstr "Le Quotidien Shaarli" | 1232 | msgstr[0] ":type précédent" |
1054 | 1233 | msgstr[1] "Jour précédent" | |
1055 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 | 1234 | |
1056 | msgid "1 RSS entry per day" | 1235 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 |
1057 | msgstr "1 entrée RSS par jour" | 1236 | #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 |
1058 | 1237 | msgid "All links of one :type in a single page." | |
1059 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 | 1238 | msgid_plural "" |
1060 | msgid "Previous day" | 1239 | msgstr[0] "Tous les liens d'un :type sur une page." |
1061 | msgstr "Jour précédent" | 1240 | msgstr[1] "Tous les liens d'un jour sur une page." |
1062 | 1241 | ||
1063 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | 1242 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 |
1064 | msgid "All links of one day in a single page." | 1243 | msgid "Next :type" |
1065 | msgstr "Tous les liens d'un jour sur une page." | 1244 | msgid_plural "" |
1066 | 1245 | msgstr[0] ":type suivant" | |
1067 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 | 1246 | msgstr[1] "" |
1068 | msgid "Next day" | 1247 | |
1069 | msgstr "Jour suivant" | 1248 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 |
1070 | |||
1071 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | ||
1072 | msgid "Edit Shaare" | 1249 | msgid "Edit Shaare" |
1073 | msgstr "Modifier le Shaare" | 1250 | msgstr "Modifier le Shaare" |
1074 | 1251 | ||
1075 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | 1252 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 |
1076 | msgid "New Shaare" | 1253 | msgid "New Shaare" |
1077 | msgstr "Nouveau Shaare" | 1254 | msgstr "Nouveau Shaare" |
1078 | 1255 | ||
1079 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | 1256 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 |
1080 | msgid "Created:" | 1257 | msgid "Created:" |
1081 | msgstr "Création :" | 1258 | msgstr "Création :" |
1082 | 1259 | ||
1083 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | 1260 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 |
1084 | msgid "URL" | 1261 | msgid "URL" |
1085 | msgstr "URL" | 1262 | msgstr "URL" |
1086 | 1263 | ||
1087 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | 1264 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 |
1088 | msgid "Title" | 1265 | msgid "Title" |
1089 | msgstr "Titre" | 1266 | msgstr "Titre" |
1090 | 1267 | ||
1091 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | 1268 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 |
1092 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | 1269 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 |
1093 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 | 1270 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 |
1094 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 | 1271 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 |
@@ -1096,32 +1273,39 @@ msgstr "Titre" | |||
1096 | msgid "Description" | 1273 | msgid "Description" |
1097 | msgstr "Description" | 1274 | msgstr "Description" |
1098 | 1275 | ||
1099 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | 1276 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 |
1100 | msgid "Tags" | ||
1101 | msgstr "Tags" | ||
1102 | |||
1103 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 | ||
1104 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
1105 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
1106 | msgid "Private" | ||
1107 | msgstr "Privé" | ||
1108 | |||
1109 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 | ||
1110 | msgid "Description will be rendered with" | 1277 | msgid "Description will be rendered with" |
1111 | msgstr "La description sera générée avec" | 1278 | msgstr "La description sera générée avec" |
1112 | 1279 | ||
1113 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 | 1280 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 |
1114 | msgid "Markdown syntax documentation" | 1281 | msgid "Markdown syntax documentation" |
1115 | msgstr "Documentation sur la syntaxe Markdown" | 1282 | msgstr "Documentation sur la syntaxe Markdown" |
1116 | 1283 | ||
1117 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 | 1284 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 |
1118 | msgid "Markdown syntax" | 1285 | msgid "Markdown syntax" |
1119 | msgstr "la syntaxe Markdown" | 1286 | msgstr "la syntaxe Markdown" |
1120 | 1287 | ||
1121 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 | 1288 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115 |
1289 | msgid "Cancel" | ||
1290 | msgstr "Annuler" | ||
1291 | |||
1292 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1122 | msgid "Apply Changes" | 1293 | msgid "Apply Changes" |
1123 | msgstr "Appliquer les changements" | 1294 | msgstr "Appliquer les changements" |
1124 | 1295 | ||
1296 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126 | ||
1297 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 | ||
1298 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 | ||
1299 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 | ||
1300 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 | ||
1301 | msgid "Delete" | ||
1302 | msgstr "Supprimer" | ||
1303 | |||
1304 | #: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 | ||
1305 | #: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 | ||
1306 | msgid "Save all" | ||
1307 | msgstr "Tout enregistrer" | ||
1308 | |||
1125 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | 1309 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 |
1126 | msgid "Export Database" | 1310 | msgid "Export Database" |
1127 | msgstr "Exporter les données" | 1311 | msgstr "Exporter les données" |
@@ -1179,10 +1363,6 @@ msgstr "Les doublons s'appuient sur les URL" | |||
1179 | msgid "Add default tags" | 1363 | msgid "Add default tags" |
1180 | msgstr "Ajouter des tags par défaut" | 1364 | msgstr "Ajouter des tags par défaut" |
1181 | 1365 | ||
1182 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | ||
1183 | msgid "Install Shaarli" | ||
1184 | msgstr "Installation de Shaarli" | ||
1185 | |||
1186 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 | 1366 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 |
1187 | msgid "It looks like it's the first time you run Shaarli. Please configure it." | 1367 | msgid "It looks like it's the first time you run Shaarli. Please configure it." |
1188 | msgstr "" | 1368 | msgstr "" |
@@ -1215,6 +1395,10 @@ msgstr "Mes liens" | |||
1215 | msgid "Install" | 1395 | msgid "Install" |
1216 | msgstr "Installer" | 1396 | msgstr "Installer" |
1217 | 1397 | ||
1398 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190 | ||
1399 | msgid "Server requirements" | ||
1400 | msgstr "Pré-requis serveur" | ||
1401 | |||
1218 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | 1402 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 |
1219 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 | 1403 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 |
1220 | msgid "shaare" | 1404 | msgid "shaare" |
@@ -1288,8 +1472,8 @@ msgid "without any tag" | |||
1288 | msgstr "sans tag" | 1472 | msgstr "sans tag" |
1289 | 1473 | ||
1290 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 | 1474 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 |
1291 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | 1475 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 |
1292 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 | 1476 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41 |
1293 | msgid "Fold" | 1477 | msgid "Fold" |
1294 | msgstr "Replier" | 1478 | msgstr "Replier" |
1295 | 1479 | ||
@@ -1313,6 +1497,10 @@ msgstr "Changer statut épinglé" | |||
1313 | msgid "Sticky" | 1497 | msgid "Sticky" |
1314 | msgstr "Épinglé" | 1498 | msgstr "Épinglé" |
1315 | 1499 | ||
1500 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 | ||
1501 | msgid "Share a private link" | ||
1502 | msgstr "Partager un lien privé" | ||
1503 | |||
1316 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 | 1504 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 |
1317 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 | 1505 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 |
1318 | msgid "Filters" | 1506 | msgid "Filters" |
@@ -1331,7 +1519,7 @@ msgstr "Afficher uniquement les liens publics" | |||
1331 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | 1519 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 |
1332 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 | 1520 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 |
1333 | msgid "Filter untagged links" | 1521 | msgid "Filter untagged links" |
1334 | msgstr "Filtrer par liens privés" | 1522 | msgstr "Filtrer par liens sans tag" |
1335 | 1523 | ||
1336 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | 1524 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 |
1337 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 | 1525 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 |
@@ -1342,8 +1530,8 @@ msgstr "Tout sélectionner" | |||
1342 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 | 1530 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 |
1343 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 | 1531 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 |
1344 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 | 1532 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 |
1345 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | 1533 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 |
1346 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 | 1534 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 |
1347 | msgid "Fold all" | 1535 | msgid "Fold all" |
1348 | msgstr "Replier tout" | 1536 | msgstr "Replier tout" |
1349 | 1537 | ||
@@ -1359,9 +1547,9 @@ msgid "Remember me" | |||
1359 | msgstr "Rester connecté" | 1547 | msgstr "Rester connecté" |
1360 | 1548 | ||
1361 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | 1549 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 |
1362 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | 1550 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 |
1363 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 | 1551 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 |
1364 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49 | 1552 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 |
1365 | msgid "by the Shaarli community" | 1553 | msgid "by the Shaarli community" |
1366 | msgstr "par la communauté Shaarli" | 1554 | msgstr "par la communauté Shaarli" |
1367 | 1555 | ||
@@ -1370,21 +1558,26 @@ msgstr "par la communauté Shaarli" | |||
1370 | msgid "Documentation" | 1558 | msgid "Documentation" |
1371 | msgstr "Documentation" | 1559 | msgstr "Documentation" |
1372 | 1560 | ||
1373 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 | 1561 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 |
1374 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 | 1562 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 |
1375 | msgid "Expand" | 1563 | msgid "Expand" |
1376 | msgstr "Déplier" | 1564 | msgstr "Déplier" |
1377 | 1565 | ||
1378 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | 1566 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 |
1379 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 | 1567 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 |
1380 | msgid "Expand all" | 1568 | msgid "Expand all" |
1381 | msgstr "Déplier tout" | 1569 | msgstr "Déplier tout" |
1382 | 1570 | ||
1383 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | 1571 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 |
1384 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:47 | 1572 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 |
1385 | msgid "Are you sure you want to delete this link?" | 1573 | msgid "Are you sure you want to delete this link?" |
1386 | msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" | 1574 | msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" |
1387 | 1575 | ||
1576 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
1577 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 | ||
1578 | msgid "Are you sure you want to delete this tag?" | ||
1579 | msgstr "Êtes-vous sûr de vouloir supprimer ce tag ?" | ||
1580 | |||
1388 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11 | 1581 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11 |
1389 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11 | 1582 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11 |
1390 | msgid "Menu" | 1583 | msgid "Menu" |
@@ -1511,6 +1704,100 @@ msgstr "Configuration des extensions" | |||
1511 | msgid "No parameter available." | 1704 | msgid "No parameter available." |
1512 | msgstr "Aucun paramètre disponible." | 1705 | msgstr "Aucun paramètre disponible." |
1513 | 1706 | ||
1707 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1708 | msgid "General" | ||
1709 | msgstr "Général" | ||
1710 | |||
1711 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 | ||
1712 | msgid "Index URL" | ||
1713 | msgstr "URL de l'index" | ||
1714 | |||
1715 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
1716 | msgid "Base path" | ||
1717 | msgstr "Chemin de base" | ||
1718 | |||
1719 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1720 | msgid "Client IP" | ||
1721 | msgstr "IP du client" | ||
1722 | |||
1723 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
1724 | msgid "Trusted reverse proxies" | ||
1725 | msgstr "Reverse proxies de confiance" | ||
1726 | |||
1727 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1728 | msgid "N/A" | ||
1729 | msgstr "N/A" | ||
1730 | |||
1731 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 | ||
1732 | msgid "Visit releases page on Github" | ||
1733 | msgstr "Visiter la page des releases sur Github" | ||
1734 | |||
1735 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1736 | msgid "Synchronize all link thumbnails" | ||
1737 | msgstr "Synchroniser toutes les miniatures" | ||
1738 | |||
1739 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2 | ||
1740 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2 | ||
1741 | msgid "Permissions" | ||
1742 | msgstr "Permissions" | ||
1743 | |||
1744 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8 | ||
1745 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8 | ||
1746 | msgid "There are permissions that need to be fixed." | ||
1747 | msgstr "Il y a des permissions qui doivent être corrigées." | ||
1748 | |||
1749 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
1750 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23 | ||
1751 | msgid "All read/write permissions are properly set." | ||
1752 | msgstr "Toutes les permissions de lecture/écriture sont définies correctement." | ||
1753 | |||
1754 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 | ||
1755 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32 | ||
1756 | msgid "Running PHP" | ||
1757 | msgstr "Fonctionnant avec PHP" | ||
1758 | |||
1759 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1760 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36 | ||
1761 | msgid "End of life: " | ||
1762 | msgstr "Fin de vie : " | ||
1763 | |||
1764 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
1765 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48 | ||
1766 | msgid "Extension" | ||
1767 | msgstr "Extension" | ||
1768 | |||
1769 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | ||
1770 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49 | ||
1771 | msgid "Usage" | ||
1772 | msgstr "Utilisation" | ||
1773 | |||
1774 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 | ||
1775 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50 | ||
1776 | msgid "Status" | ||
1777 | msgstr "Statut" | ||
1778 | |||
1779 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 | ||
1780 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 | ||
1781 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51 | ||
1782 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66 | ||
1783 | msgid "Loaded" | ||
1784 | msgstr "Chargé" | ||
1785 | |||
1786 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 | ||
1787 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 | ||
1788 | msgid "Required" | ||
1789 | msgstr "Obligatoire" | ||
1790 | |||
1791 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 | ||
1792 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 | ||
1793 | msgid "Optional" | ||
1794 | msgstr "Optionnel" | ||
1795 | |||
1796 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 | ||
1797 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70 | ||
1798 | msgid "Not loaded" | ||
1799 | msgstr "Non chargé" | ||
1800 | |||
1514 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | 1801 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 |
1515 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | 1802 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 |
1516 | msgid "tags" | 1803 | msgid "tags" |
@@ -1525,10 +1812,6 @@ msgstr "Lister tous les liens avec ces tags" | |||
1525 | msgid "Tag list" | 1812 | msgid "Tag list" |
1526 | msgstr "Liste des tags" | 1813 | msgstr "Liste des tags" |
1527 | 1814 | ||
1528 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 | ||
1529 | msgid "Rename tag" | ||
1530 | msgstr "Renommer le tag" | ||
1531 | |||
1532 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 | 1815 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 |
1533 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 | 1816 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 |
1534 | msgid "Sort by:" | 1817 | msgid "Sort by:" |
@@ -1565,15 +1848,19 @@ msgstr "Configurer Shaarli" | |||
1565 | msgid "Enable, disable and configure plugins" | 1848 | msgid "Enable, disable and configure plugins" |
1566 | msgstr "Activer, désactiver et configurer les extensions" | 1849 | msgstr "Activer, désactiver et configurer les extensions" |
1567 | 1850 | ||
1568 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | 1851 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 |
1852 | msgid "Check instance's server configuration" | ||
1853 | msgstr "Vérifier la configuration serveur de l'instance" | ||
1854 | |||
1855 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
1569 | msgid "Change your password" | 1856 | msgid "Change your password" |
1570 | msgstr "Modifier le mot de passe" | 1857 | msgstr "Modifier le mot de passe" |
1571 | 1858 | ||
1572 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | 1859 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 |
1573 | msgid "Rename or delete a tag in all links" | 1860 | msgid "Rename or delete a tag in all links" |
1574 | msgstr "Renommer ou supprimer un tag dans tous les liens" | 1861 | msgstr "Renommer ou supprimer un tag dans tous les liens" |
1575 | 1862 | ||
1576 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | 1863 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 |
1577 | msgid "" | 1864 | msgid "" |
1578 | "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " | 1865 | "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " |
1579 | "delicious...)" | 1866 | "delicious...)" |
@@ -1581,11 +1868,11 @@ msgstr "" | |||
1581 | "Importer des marques pages au format Netscape HTML (comme exportés depuis " | 1868 | "Importer des marques pages au format Netscape HTML (comme exportés depuis " |
1582 | "Firefox, Chrome, Opera, delicious...)" | 1869 | "Firefox, Chrome, Opera, delicious...)" |
1583 | 1870 | ||
1584 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | 1871 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 |
1585 | msgid "Import links" | 1872 | msgid "Import links" |
1586 | msgstr "Importer des liens" | 1873 | msgstr "Importer des liens" |
1587 | 1874 | ||
1588 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | 1875 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 |
1589 | msgid "" | 1876 | msgid "" |
1590 | "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " | 1877 | "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " |
1591 | "Opera, delicious...)" | 1878 | "Opera, delicious...)" |
@@ -1593,15 +1880,11 @@ msgstr "" | |||
1593 | "Exporter les marques pages au format Netscape HTML (comme exportés depuis " | 1880 | "Exporter les marques pages au format Netscape HTML (comme exportés depuis " |
1594 | "Firefox, Chrome, Opera, delicious...)" | 1881 | "Firefox, Chrome, Opera, delicious...)" |
1595 | 1882 | ||
1596 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | 1883 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54 |
1597 | msgid "Export database" | 1884 | msgid "Export database" |
1598 | msgstr "Exporter les données" | 1885 | msgstr "Exporter les données" |
1599 | 1886 | ||
1600 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55 | 1887 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 |
1601 | msgid "Synchronize all link thumbnails" | ||
1602 | msgstr "Synchroniser toutes les miniatures" | ||
1603 | |||
1604 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81 | ||
1605 | msgid "" | 1888 | msgid "" |
1606 | "Drag one of these button to your bookmarks toolbar or right-click it and " | 1889 | "Drag one of these button to your bookmarks toolbar or right-click it and " |
1607 | "\"Bookmark This Link\"" | 1890 | "\"Bookmark This Link\"" |
@@ -1609,13 +1892,13 @@ msgstr "" | |||
1609 | "Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " | 1892 | "Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " |
1610 | "dessus et « Ajouter aux favoris »" | 1893 | "dessus et « Ajouter aux favoris »" |
1611 | 1894 | ||
1612 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 | 1895 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 |
1613 | msgid "then click on the bookmarklet in any page you want to share." | 1896 | msgid "then click on the bookmarklet in any page you want to share." |
1614 | msgstr "" | 1897 | msgstr "" |
1615 | "puis cliquer sur le marque-page depuis un site que vous souhaitez partager." | 1898 | "puis cliquer sur le marque-page depuis un site que vous souhaitez partager." |
1616 | 1899 | ||
1617 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 | 1900 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 |
1618 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 | 1901 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 |
1619 | msgid "" | 1902 | msgid "" |
1620 | "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " | 1903 | "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " |
1621 | "Link" | 1904 | "Link" |
@@ -1623,40 +1906,40 @@ msgstr "" | |||
1623 | "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " | 1906 | "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " |
1624 | "Ajouter aux favoris »" | 1907 | "Ajouter aux favoris »" |
1625 | 1908 | ||
1626 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 | 1909 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 |
1627 | msgid "then click ✚Shaare link button in any page you want to share" | 1910 | msgid "then click ✚Shaare link button in any page you want to share" |
1628 | msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" | 1911 | msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" |
1629 | 1912 | ||
1630 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 | 1913 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 |
1631 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 | 1914 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 |
1632 | msgid "The selected text is too long, it will be truncated." | 1915 | msgid "The selected text is too long, it will be truncated." |
1633 | msgstr "Le texte sélectionné est trop long, il sera tronqué." | 1916 | msgstr "Le texte sélectionné est trop long, il sera tronqué." |
1634 | 1917 | ||
1635 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 | 1918 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 |
1636 | msgid "Shaare link" | 1919 | msgid "Shaare link" |
1637 | msgstr "Shaare" | 1920 | msgstr "Shaare" |
1638 | 1921 | ||
1639 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 | 1922 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 |
1640 | msgid "" | 1923 | msgid "" |
1641 | "Then click ✚Add Note button anytime to start composing a private Note (text " | 1924 | "Then click ✚Add Note button anytime to start composing a private Note (text " |
1642 | "post) to your Shaarli" | 1925 | "post) to your Shaarli" |
1643 | msgstr "" | 1926 | msgstr "" |
1644 | "Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" | 1927 | "Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" |
1645 | 1928 | ||
1646 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 | 1929 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 |
1647 | msgid "Add Note" | 1930 | msgid "Add Note" |
1648 | msgstr "Ajouter une Note" | 1931 | msgstr "Ajouter une Note" |
1649 | 1932 | ||
1650 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 | 1933 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132 |
1651 | msgid "3rd party" | 1934 | msgid "3rd party" |
1652 | msgstr "Applications tierces" | 1935 | msgstr "Applications tierces" |
1653 | 1936 | ||
1654 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | 1937 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 |
1655 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 | 1938 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140 |
1656 | msgid "plugin" | 1939 | msgid "plugin" |
1657 | msgstr "extension" | 1940 | msgstr "extension" |
1658 | 1941 | ||
1659 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | 1942 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 |
1660 | msgid "" | 1943 | msgid "" |
1661 | "Drag this link to your bookmarks toolbar, or right-click it and choose " | 1944 | "Drag this link to your bookmarks toolbar, or right-click it and choose " |
1662 | "Bookmark This Link" | 1945 | "Bookmark This Link" |
@@ -1664,6 +1947,12 @@ msgstr "" | |||
1664 | "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " | 1947 | "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " |
1665 | "Ajouter aux favoris »" | 1948 | "Ajouter aux favoris »" |
1666 | 1949 | ||
1950 | #~ msgid "Display:" | ||
1951 | #~ msgstr "Afficher :" | ||
1952 | |||
1953 | #~ msgid "The Daily Shaarli" | ||
1954 | #~ msgstr "Le Quotidien Shaarli" | ||
1955 | |||
1667 | #, fuzzy | 1956 | #, fuzzy |
1668 | #~| msgid "Selection" | 1957 | #~| msgid "Selection" |
1669 | #~ msgid ".ui-selecting" | 1958 | #~ msgid ".ui-selecting" |
diff --git a/inc/languages/jp/LC_MESSAGES/shaarli.po b/inc/languages/jp/LC_MESSAGES/shaarli.po index b420bb51..57f42fc2 100644 --- a/inc/languages/jp/LC_MESSAGES/shaarli.po +++ b/inc/languages/jp/LC_MESSAGES/shaarli.po | |||
@@ -2,15 +2,15 @@ msgid "" | |||
2 | msgstr "" | 2 | msgstr "" |
3 | "Project-Id-Version: Shaarli\n" | 3 | "Project-Id-Version: Shaarli\n" |
4 | "Report-Msgid-Bugs-To: \n" | 4 | "Report-Msgid-Bugs-To: \n" |
5 | "POT-Creation-Date: 2020-02-11 09:31+0900\n" | 5 | "POT-Creation-Date: 2020-10-19 10:19+0900\n" |
6 | "PO-Revision-Date: 2020-02-11 10:54+0900\n" | 6 | "PO-Revision-Date: 2020-10-19 10:25+0900\n" |
7 | "Last-Translator: yude <yudesleepy@gmail.com>\n" | 7 | "Last-Translator: yude <yudesleepy@gmail.com>\n" |
8 | "Language-Team: Shaarli\n" | 8 | "Language-Team: Shaarli\n" |
9 | "Language: ja\n" | 9 | "Language: ja\n" |
10 | "MIME-Version: 1.0\n" | 10 | "MIME-Version: 1.0\n" |
11 | "Content-Type: text/plain; charset=UTF-8\n" | 11 | "Content-Type: text/plain; charset=UTF-8\n" |
12 | "Content-Transfer-Encoding: 8bit\n" | 12 | "Content-Transfer-Encoding: 8bit\n" |
13 | "X-Generator: Poedit 2.3\n" | 13 | "X-Generator: Poedit 2.2.3\n" |
14 | "X-Poedit-Basepath: ../../../..\n" | 14 | "X-Poedit-Basepath: ../../../..\n" |
15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" | 15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" |
16 | "X-Poedit-SourceCharset: UTF-8\n" | 16 | "X-Poedit-SourceCharset: UTF-8\n" |
@@ -19,7 +19,7 @@ msgstr "" | |||
19 | "X-Poedit-SearchPathExcluded-0: node_modules\n" | 19 | "X-Poedit-SearchPathExcluded-0: node_modules\n" |
20 | "X-Poedit-SearchPathExcluded-1: vendor\n" | 20 | "X-Poedit-SearchPathExcluded-1: vendor\n" |
21 | 21 | ||
22 | #: application/ApplicationUtils.php:153 | 22 | #: application/ApplicationUtils.php:161 |
23 | #, php-format | 23 | #, php-format |
24 | msgid "" | 24 | msgid "" |
25 | "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " | 25 | "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " |
@@ -30,200 +30,250 @@ msgstr "" | |||
30 | "ãŒå¿…è¦ã§ã™ã€‚ ç¾åœ¨ä½¿ç”¨ã—ã¦ã„ã‚‹ PHP ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã¯è„†å¼±æ€§ãŒã‚ã‚Šã€ã§ãã‚‹ã ã‘速" | 30 | "ãŒå¿…è¦ã§ã™ã€‚ ç¾åœ¨ä½¿ç”¨ã—ã¦ã„ã‚‹ PHP ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã¯è„†å¼±æ€§ãŒã‚ã‚Šã€ã§ãã‚‹ã ã‘速" |
31 | "ã‚„ã‹ã«ã‚¢ãƒƒãƒ—デートã™ã‚‹ã¹ãã§ã™ã€‚" | 31 | "ã‚„ã‹ã«ã‚¢ãƒƒãƒ—デートã™ã‚‹ã¹ãã§ã™ã€‚" |
32 | 32 | ||
33 | #: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195 | 33 | #: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204 |
34 | msgid "directory is not readable" | 34 | msgid "directory is not readable" |
35 | msgstr "ディレクトリをèªã¿è¾¼ã‚ã¾ã›ã‚“" | 35 | msgstr "ディレクトリをèªã¿è¾¼ã‚ã¾ã›ã‚“" |
36 | 36 | ||
37 | #: application/ApplicationUtils.php:198 | 37 | #: application/ApplicationUtils.php:207 |
38 | msgid "directory is not writable" | 38 | msgid "directory is not writable" |
39 | msgstr "ディレクトリã«æ›¸ãè¾¼ã‚ã¾ã›ã‚“" | 39 | msgstr "ディレクトリã«æ›¸ãè¾¼ã‚ã¾ã›ã‚“" |
40 | 40 | ||
41 | #: application/ApplicationUtils.php:216 | 41 | #: application/ApplicationUtils.php:225 |
42 | msgid "file is not readable" | 42 | msgid "file is not readable" |
43 | msgstr "ファイルをèªã¿å–る権é™ãŒã‚ã‚Šã¾ã›ã‚“" | 43 | msgstr "ファイルをèªã¿å–る権é™ãŒã‚ã‚Šã¾ã›ã‚“" |
44 | 44 | ||
45 | #: application/ApplicationUtils.php:219 | 45 | #: application/ApplicationUtils.php:228 |
46 | msgid "file is not writable" | 46 | msgid "file is not writable" |
47 | msgstr "ファイルを書ã込む権é™ãŒã‚ã‚Šã¾ã›ã‚“" | 47 | msgstr "ファイルを書ã込む権é™ãŒã‚ã‚Šã¾ã›ã‚“" |
48 | 48 | ||
49 | #: application/Cache.php:16 | 49 | #: application/History.php:179 |
50 | #, php-format | ||
51 | msgid "Cannot purge %s: no directory" | ||
52 | msgstr "%s を削除ã§ãã¾ã›ã‚“: ディレクトリãŒå˜åœ¨ã—ã¾ã›ã‚“" | ||
53 | |||
54 | #: application/FeedBuilder.php:151 | ||
55 | msgid "Direct link" | ||
56 | msgstr "ダイレクトリンク" | ||
57 | |||
58 | #: application/FeedBuilder.php:153 | ||
59 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 | ||
60 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178 | ||
61 | msgid "Permalink" | ||
62 | msgstr "パーマリンク" | ||
63 | |||
64 | #: application/History.php:174 | ||
65 | msgid "History file isn't readable or writable" | 50 | msgid "History file isn't readable or writable" |
66 | msgstr "å±¥æ´ãƒ•ã‚¡ã‚¤ãƒ«ã‚’èªã¿è¾¼ã‚€ã€ã¾ãŸã¯æ›¸ã込むãŸã‚ã®æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“" | 51 | msgstr "å±¥æ´ãƒ•ã‚¡ã‚¤ãƒ«ã‚’èªã¿è¾¼ã‚€ã€ã¾ãŸã¯æ›¸ã込むãŸã‚ã®æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“" |
67 | 52 | ||
68 | #: application/History.php:185 | 53 | #: application/History.php:190 |
69 | msgid "Could not parse history file" | 54 | msgid "Could not parse history file" |
70 | msgstr "å±¥æ´ãƒ•ã‚¡ã‚¤ãƒ«ã‚’æ£å¸¸ã«å¾©å…ƒã§ãã¾ã›ã‚“ã§ã—ãŸ" | 55 | msgstr "å±¥æ´ãƒ•ã‚¡ã‚¤ãƒ«ã‚’æ£å¸¸ã«å¾©å…ƒã§ãã¾ã›ã‚“ã§ã—ãŸ" |
71 | 56 | ||
72 | #: application/Languages.php:177 | 57 | #: application/Languages.php:181 |
73 | msgid "Automatic" | 58 | msgid "Automatic" |
74 | msgstr "自動" | 59 | msgstr "自動" |
75 | 60 | ||
76 | #: application/Languages.php:178 | 61 | #: application/Languages.php:182 |
62 | msgid "German" | ||
63 | msgstr "ドイツ語" | ||
64 | |||
65 | #: application/Languages.php:183 | ||
77 | msgid "English" | 66 | msgid "English" |
78 | msgstr "英語" | 67 | msgstr "英語" |
79 | 68 | ||
80 | #: application/Languages.php:179 | 69 | #: application/Languages.php:184 |
81 | msgid "French" | 70 | msgid "French" |
82 | msgstr "フランス語" | 71 | msgstr "フランス語" |
83 | 72 | ||
84 | #: application/Languages.php:180 | 73 | #: application/Languages.php:185 |
85 | msgid "German" | 74 | msgid "Japanese" |
86 | msgstr "ドイツ語" | 75 | msgstr "日本語" |
87 | |||
88 | #: application/LinkDB.php:136 | ||
89 | msgid "You are not authorized to add a link." | ||
90 | msgstr "ãƒªãƒ³ã‚¯ã‚’è¿½åŠ ã™ã‚‹ã«ã¯ã€ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚" | ||
91 | |||
92 | #: application/LinkDB.php:139 | ||
93 | msgid "Internal Error: A link should always have an id and URL." | ||
94 | msgstr "エラー: リンクã«ã¯IDã¨URLを登録ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。" | ||
95 | |||
96 | #: application/LinkDB.php:142 | ||
97 | msgid "You must specify an integer as a key." | ||
98 | msgstr "æ£å¸¸ãªã‚ーã®å€¤ã§ã¯ã‚ã‚Šã¾ã›ã‚“。" | ||
99 | |||
100 | #: application/LinkDB.php:145 | ||
101 | msgid "Array offset and link ID must be equal." | ||
102 | msgstr "Array オフセットã¨ãƒªãƒ³ã‚¯ã®IDã¯åŒã˜ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。" | ||
103 | |||
104 | #: application/LinkDB.php:251 | ||
105 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
106 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
107 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14 | ||
108 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 | ||
109 | msgid "" | ||
110 | "The personal, minimalist, super-fast, database free, bookmarking service" | ||
111 | msgstr "" | ||
112 | "個人å‘ã‘ã®ã€ãƒŸãƒ‹ãƒžãƒ ã§é«˜é€Ÿã§ã‹ã¤ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã®ã„らãªã„ブックマークサービス" | ||
113 | |||
114 | #: application/LinkDB.php:253 | ||
115 | msgid "" | ||
116 | "Welcome to Shaarli! This is your first public bookmark. To edit or delete " | ||
117 | "me, you must first login.\n" | ||
118 | "\n" | ||
119 | "To learn how to use Shaarli, consult the link \"Documentation\" at the " | ||
120 | "bottom of this page.\n" | ||
121 | "\n" | ||
122 | "You use the community supported version of the original Shaarli project, by " | ||
123 | "Sebastien Sauvage." | ||
124 | msgstr "" | ||
125 | "Shaarli ã¸ã‚ˆã†ã“ãï¼ ã“ã‚Œã¯ã‚ãªãŸã®æœ€åˆã®å…¬é–‹ãƒ–ックマークã§ã™ã€‚ã“れを編集ã—ãŸ" | ||
126 | "り削除ã—ãŸã‚Šã™ã‚‹ã«ã¯ã€ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚\n" | ||
127 | "\n" | ||
128 | "Shaarli ã®ä½¿ã„方を知るã«ã¯ã€ã“ã®ãƒšãƒ¼ã‚¸ã®ä¸‹ã«ã‚る「ドã‚ュメントã€ã®ãƒªãƒ³ã‚¯ã‚’é–‹" | ||
129 | "ã„ã¦ãã ã•ã„。\n" | ||
130 | "\n" | ||
131 | "ã‚ãªãŸã¯ Sebastien Sauvage ã«ã‚ˆã‚‹ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ãƒ¼ã‚µãƒãƒ¼ãƒˆã®ã‚ã‚‹ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ã‚ª" | ||
132 | "リジナルã®Shaarli プãƒã‚¸ã‚§ã‚¯ãƒˆã‚’使用ã—ã¦ã„ã¾ã™ã€‚" | ||
133 | |||
134 | #: application/LinkDB.php:267 | ||
135 | msgid "My secret stuff... - Pastebin.com" | ||
136 | msgstr "ã‚ãŸã—ã®ã²ðŸ’—ã¿ðŸ’—ã¤ðŸ’— - Pastebin.com" | ||
137 | |||
138 | #: application/LinkDB.php:269 | ||
139 | msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." | ||
140 | msgstr "" | ||
141 | "ã‚·ãƒ¼ãƒƒï¼ ã“ã‚Œã¯ã‚ãªãŸã—ã‹è¦‹ã‚‰ã‚Œãªã„プライベートリンクã§ã™ã€‚消ã™ã“ã¨ã‚‚ã§ãã¾" | ||
142 | "ã™ã€‚" | ||
143 | |||
144 | #: application/LinkFilter.php:452 | ||
145 | msgid "The link you are trying to reach does not exist or has been deleted." | ||
146 | msgstr "é–‹ã“ã†ã¨ã—ãŸãƒªãƒ³ã‚¯ã¯å˜åœ¨ã—ãªã„ã‹ã€å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚" | ||
147 | |||
148 | #: application/NetscapeBookmarkUtils.php:35 | ||
149 | msgid "Invalid export selection:" | ||
150 | msgstr "ä¸æ£ãªã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã®é¸æŠž:" | ||
151 | |||
152 | #: application/NetscapeBookmarkUtils.php:81 | ||
153 | #, php-format | ||
154 | msgid "File %s (%d bytes) " | ||
155 | msgstr "ファイル %s (%d ãƒã‚¤ãƒˆ) " | ||
156 | |||
157 | #: application/NetscapeBookmarkUtils.php:83 | ||
158 | msgid "has an unknown file format. Nothing was imported." | ||
159 | msgstr "ã¯ä¸æ˜Žãªãƒ•ã‚¡ã‚¤ãƒ«å½¢å¼ã§ã™ã€‚インãƒãƒ¼ãƒˆã¯ä¸æ¢ã•ã‚Œã¾ã—ãŸã€‚" | ||
160 | 76 | ||
161 | #: application/NetscapeBookmarkUtils.php:86 | 77 | #: application/Thumbnailer.php:62 |
162 | #, php-format | ||
163 | msgid "" | 78 | msgid "" |
164 | "was successfully processed in %d seconds: %d links imported, %d links " | 79 | "php-gd extension must be loaded to use thumbnails. Thumbnails are now " |
165 | "overwritten, %d links skipped." | 80 | "disabled. Please reload the page." |
166 | msgstr "" | 81 | msgstr "" |
167 | "㌠%d 秒ã§å‡¦ç†ã•ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒã‚¤ãƒ³ãƒãƒ¼ãƒˆã•ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒä¸Šæ›¸ãã•" | 82 | "サムãƒã‚¤ãƒ«ã‚’使用ã™ã‚‹ã«ã¯ã€php-gd エクステンションãŒèªã¿è¾¼ã¾ã‚Œã¦ã„ã‚‹å¿…è¦ãŒã‚ã‚Š" |
168 | "ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒã‚¹ã‚ップã•ã‚Œã¾ã—ãŸã€‚" | 83 | "ã¾ã™ã€‚サムãƒã‚¤ãƒ«ã¯ç„¡åŠ¹åŒ–ã•ã‚Œã¾ã—ãŸã€‚ページをå†èªè¾¼ã—ã¦ãã ã•ã„。" |
169 | |||
170 | #: application/PageBuilder.php:168 | ||
171 | msgid "The page you are trying to reach does not exist or has been deleted." | ||
172 | msgstr "ã‚ãªãŸãŒé–‹ã“ã†ã¨ã—ãŸãƒšãƒ¼ã‚¸ã¯å˜åœ¨ã—ãªã„ã‹ã€å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚" | ||
173 | |||
174 | #: application/PageBuilder.php:170 | ||
175 | msgid "404 Not Found" | ||
176 | msgstr "404 ページãŒå˜åœ¨ã—ã¾ã›ã‚“" | ||
177 | |||
178 | #: application/PluginManager.php:243 | ||
179 | #, php-format | ||
180 | msgid "Plugin \"%s\" files not found." | ||
181 | msgstr "プラグイン「%sã€ã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒå˜åœ¨ã—ã¾ã›ã‚“。" | ||
182 | |||
183 | #: application/Updater.php:76 | ||
184 | msgid "Couldn't retrieve Updater class methods." | ||
185 | msgstr "アップデーターã®ã‚¯ãƒ©ã‚¹ãƒ¡ã‚¾ãƒƒãƒˆã‚’å—ä¿¡ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" | ||
186 | |||
187 | #: application/Updater.php:532 | ||
188 | msgid "An error occurred while running the update " | ||
189 | msgstr "æ›´æ–°ä¸ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—㟠" | ||
190 | |||
191 | #: application/Updater.php:572 | ||
192 | msgid "Updates file path is not set, can't write updates." | ||
193 | msgstr "æ›´æ–°ã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã®ãƒ‘スãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ãŸã‚ã€æ›´æ–°ã‚’書ãè¾¼ã‚ã¾ã›ã‚“。" | ||
194 | 84 | ||
195 | #: application/Updater.php:577 | 85 | #: application/Utils.php:383 tests/UtilsTest.php:343 |
196 | msgid "Unable to write updates in " | ||
197 | msgstr "更新を次ã®é …ç›®ã«æ›¸ãè¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸ: " | ||
198 | |||
199 | #: application/Utils.php:376 tests/UtilsTest.php:340 | ||
200 | msgid "Setting not set" | 86 | msgid "Setting not set" |
201 | msgstr "未è¨å®š" | 87 | msgstr "未è¨å®š" |
202 | 88 | ||
203 | #: application/Utils.php:383 tests/UtilsTest.php:338 tests/UtilsTest.php:339 | 89 | #: application/Utils.php:390 tests/UtilsTest.php:341 tests/UtilsTest.php:342 |
204 | msgid "Unlimited" | 90 | msgid "Unlimited" |
205 | msgstr "無制é™" | 91 | msgstr "無制é™" |
206 | 92 | ||
207 | #: application/Utils.php:386 tests/UtilsTest.php:335 tests/UtilsTest.php:336 | 93 | #: application/Utils.php:393 tests/UtilsTest.php:338 tests/UtilsTest.php:339 |
208 | #: tests/UtilsTest.php:350 | 94 | #: tests/UtilsTest.php:353 |
209 | msgid "B" | 95 | msgid "B" |
210 | msgstr "B" | 96 | msgstr "B" |
211 | 97 | ||
212 | #: application/Utils.php:386 tests/UtilsTest.php:329 tests/UtilsTest.php:330 | 98 | #: application/Utils.php:393 tests/UtilsTest.php:332 tests/UtilsTest.php:333 |
213 | #: tests/UtilsTest.php:337 | 99 | #: tests/UtilsTest.php:340 |
214 | msgid "kiB" | 100 | msgid "kiB" |
215 | msgstr "kiB" | 101 | msgstr "kiB" |
216 | 102 | ||
217 | #: application/Utils.php:386 tests/UtilsTest.php:331 tests/UtilsTest.php:332 | 103 | #: application/Utils.php:393 tests/UtilsTest.php:334 tests/UtilsTest.php:335 |
218 | #: tests/UtilsTest.php:348 tests/UtilsTest.php:349 | 104 | #: tests/UtilsTest.php:351 tests/UtilsTest.php:352 |
219 | msgid "MiB" | 105 | msgid "MiB" |
220 | msgstr "MiB" | 106 | msgstr "MiB" |
221 | 107 | ||
222 | #: application/Utils.php:386 tests/UtilsTest.php:333 tests/UtilsTest.php:334 | 108 | #: application/Utils.php:393 tests/UtilsTest.php:336 tests/UtilsTest.php:337 |
223 | msgid "GiB" | 109 | msgid "GiB" |
224 | msgstr "GiB" | 110 | msgstr "GiB" |
225 | 111 | ||
226 | #: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121 | 112 | #: application/bookmark/BookmarkFileService.php:180 |
113 | #: application/bookmark/BookmarkFileService.php:202 | ||
114 | #: application/bookmark/BookmarkFileService.php:224 | ||
115 | #: application/bookmark/BookmarkFileService.php:238 | ||
116 | msgid "You're not authorized to alter the datastore" | ||
117 | msgstr "è¨å®šã‚’変更ã™ã‚‹æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“" | ||
118 | |||
119 | #: application/bookmark/BookmarkFileService.php:205 | ||
120 | msgid "This bookmarks already exists" | ||
121 | msgstr "ã“ã®ãƒ–ックマークã¯æ—¢ã«å˜åœ¨ã—ã¾ã™ã€‚" | ||
122 | |||
123 | #: application/bookmark/BookmarkInitializer.php:39 | ||
124 | msgid "(private bookmark with thumbnail demo)" | ||
125 | msgstr "(サムãƒã‚¤ãƒ«ãƒ‡ãƒ¢ãŒä»˜å±žã—ã¦ã„るプライベートブックマーク)" | ||
126 | |||
127 | #: application/bookmark/BookmarkInitializer.php:42 | ||
128 | msgid "" | ||
129 | "Shaarli will automatically pick up the thumbnail for links to a variety of " | ||
130 | "websites.\n" | ||
131 | "\n" | ||
132 | "Explore your new Shaarli instance by trying out controls and menus.\n" | ||
133 | "Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the " | ||
134 | "documentation](https://shaarli.readthedocs.io/en/master/) to learn more " | ||
135 | "about Shaarli.\n" | ||
136 | "\n" | ||
137 | "Now you can edit or delete the default shaares.\n" | ||
138 | msgstr "" | ||
139 | "Shaarli ã¯è‡ªå‹•çš„ã«å¤šæ§˜ãªã‚¦ã‚§ãƒ–サイトã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’å–å¾—ã—ã¾ã™ã€‚\n" | ||
140 | "\n" | ||
141 | "ã‚ãªãŸã®æ–°ã—ã„ Shaarli インスタンスをコントãƒãƒ¼ãƒ«ã‚„メニューを試ã—ãŸã‚Šã—ã¦ã€æŽ¢" | ||
142 | "検ã—ã¦ãã ã•ã„。\n" | ||
143 | " [Github](https://github.com/shaarli/Shaarli) ã¾ãŸã¯ [the documentation]" | ||
144 | "(https://shaarli.readthedocs.io/en/master/) ã§ãƒ—ãƒã‚¸ã‚§ã‚¯ãƒˆã‚’訪å•ã—ã¦ã€" | ||
145 | "Shaarli ã‚’ã‚‚ã£ã¨ã‚ˆã知るã“ã¨ãŒã§ãã¾ã™ã€‚\n" | ||
146 | "\n" | ||
147 | "今ã‹ã‚‰ã€æ—¢å®šã® shaares を編集ã—ãŸã‚Šã€å‰Šé™¤ã—ãŸã‚Šã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚\n" | ||
148 | |||
149 | #: application/bookmark/BookmarkInitializer.php:55 | ||
150 | msgid "Note: Shaare descriptions" | ||
151 | msgstr "説明: Shaare ã®æ¦‚è¦" | ||
152 | |||
153 | #: application/bookmark/BookmarkInitializer.php:57 | ||
154 | msgid "" | ||
155 | "Adding a shaare without entering a URL creates a text-only \"note\" post " | ||
156 | "such as this one.\n" | ||
157 | "This note is private, so you are the only one able to see it while logged " | ||
158 | "in.\n" | ||
159 | "\n" | ||
160 | "You can use this to keep notes, post articles, code snippets, and much " | ||
161 | "more.\n" | ||
162 | "\n" | ||
163 | "The Markdown formatting setting allows you to format your notes and bookmark " | ||
164 | "description:\n" | ||
165 | "\n" | ||
166 | "### Title headings\n" | ||
167 | "\n" | ||
168 | "#### Multiple headings levels\n" | ||
169 | " * bullet lists\n" | ||
170 | " * _italic_ text\n" | ||
171 | " * **bold** text\n" | ||
172 | " * ~~strike through~~ text\n" | ||
173 | " * `code` blocks\n" | ||
174 | " * images\n" | ||
175 | " * [links](https://en.wikipedia.org/wiki/Markdown)\n" | ||
176 | "\n" | ||
177 | "Markdown also supports tables:\n" | ||
178 | "\n" | ||
179 | "| Name | Type | Color | Qty |\n" | ||
180 | "| ------- | --------- | ------ | ----- |\n" | ||
181 | "| Orange | Fruit | Orange | 126 |\n" | ||
182 | "| Apple | Fruit | Any | 62 |\n" | ||
183 | "| Lemon | Fruit | Yellow | 30 |\n" | ||
184 | "| Carrot | Vegetable | Red | 14 |\n" | ||
185 | msgstr "" | ||
186 | "URL ã‚’è¿½åŠ ã›ãšã« shaare を作æˆã™ã‚‹ã¨ã€ãƒ†ã‚ストã®ã¿ã®ã“ã®ã‚ˆã†ãª \"ノート\" ãŒ" | ||
187 | "作æˆã•ã‚Œã¾ã™ã€‚\n" | ||
188 | "ã“ã®ãƒŽãƒ¼ãƒˆã¯ãƒ—ライベートãªã®ã§ã€ãƒã‚°ã‚¤ãƒ³ä¸ã®ã‚ãªãŸã—ã‹è¦‹ã‚‹ã“ã¨ã¯ã§ãã¾ã›" | ||
189 | "ん。\n" | ||
190 | "\n" | ||
191 | "ã‚ãªãŸã¯ã“れをメモ帳ã¨ã—ã¦ä½¿ã£ãŸã‚Šã€è¨˜äº‹ã‚’投稿ã—ãŸã‚Šã€ã‚³ãƒ¼ãƒ‰ スニペットã¨ã—ãŸ" | ||
192 | "ã‚Šã™ã‚‹ãªã©ã¨ã„ã£ãŸã“ã¨ã«ä½¿ãˆã¾ã™ã€‚\n" | ||
193 | "\n" | ||
194 | "Markdown フォーマットã®è¨å®šã«ã‚ˆã‚Šã€ãƒŽãƒ¼ãƒˆã‚„ブックマークã®æ¦‚è¦ã‚’以下ã®ã‚ˆã†ã«" | ||
195 | "フォーマットã§ãã¾ã™:\n" | ||
196 | "\n" | ||
197 | "### タイトル ヘッダー\n" | ||
198 | "\n" | ||
199 | "#### 複数ã®è¦‹å‡ºã—\n" | ||
200 | " * 箇æ¡æ›¸ãリスト\n" | ||
201 | " * _イタリック_ æ–‡å—\n" | ||
202 | " * **ボールド** æ–‡å—\n" | ||
203 | " * ~~打ã¡æ¶ˆã—~~ æ–‡å—\n" | ||
204 | " * `コード` ブãƒãƒƒã‚¯\n" | ||
205 | " * ç”»åƒ\n" | ||
206 | " * [リンク](https://en.wikipedia.org/wiki/Markdown)\n" | ||
207 | "\n" | ||
208 | "Markdown ã¯è¡¨ã‚‚サãƒãƒ¼ãƒˆã—ã¾ã™:\n" | ||
209 | "\n" | ||
210 | "| åå‰ | 種類 | 色 | æ•°é‡ |\n" | ||
211 | "| ------- | --------- | ------ | ----- |\n" | ||
212 | "| オレンジ | 果物 | 橙 | 126 |\n" | ||
213 | "| リンゴ | 果物 | ä»»æ„ | 62 |\n" | ||
214 | "| レモン | 果物 | 黄 | 30 |\n" | ||
215 | "| äººå‚ | é‡Žèœ | 赤 | 14 |\n" | ||
216 | |||
217 | #: application/bookmark/BookmarkInitializer.php:91 | ||
218 | #: application/legacy/LegacyLinkDB.php:246 | ||
219 | msgid "" | ||
220 | "The personal, minimalist, super-fast, database free, bookmarking service" | ||
221 | msgstr "" | ||
222 | "個人å‘ã‘ã®ã€ãƒŸãƒ‹ãƒžãƒ ã§é«˜é€Ÿã§ã‹ã¤ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã®ã„らãªã„ブックマークサービス" | ||
223 | |||
224 | #: application/bookmark/BookmarkInitializer.php:94 | ||
225 | msgid "" | ||
226 | "Welcome to Shaarli!\n" | ||
227 | "\n" | ||
228 | "Shaarli allows you to bookmark your favorite pages, and share them with " | ||
229 | "others or store them privately.\n" | ||
230 | "You can add a description to your bookmarks, such as this one, and tag " | ||
231 | "them.\n" | ||
232 | "\n" | ||
233 | "Create a new shaare by clicking the `+Shaare` button, or using any of the " | ||
234 | "recommended tools (browser extension, mobile app, bookmarklet, REST API, " | ||
235 | "etc.).\n" | ||
236 | "\n" | ||
237 | "You can easily retrieve your links, even with thousands of them, using the " | ||
238 | "internal search engine, or search through tags (e.g. this Shaare is tagged " | ||
239 | "with `shaarli` and `help`).\n" | ||
240 | "Hashtags such as #shaarli #help are also supported.\n" | ||
241 | "You can also filter the available [RSS feed](/feed/atom) and picture wall by " | ||
242 | "tag or plaintext search.\n" | ||
243 | "\n" | ||
244 | "We hope that you will enjoy using Shaarli, maintained with â¤ï¸ by the " | ||
245 | "community!\n" | ||
246 | "Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if " | ||
247 | "you have a suggestion or encounter an issue.\n" | ||
248 | msgstr "" | ||
249 | "Shaarli ã¸ã‚ˆã†ã“ãï¼\n" | ||
250 | "\n" | ||
251 | "Shaarli ã§ã¯ã€ã‚ãªãŸã®ãŠæ°—ã«å…¥ã‚Šã®ãƒšãƒ¼ã‚¸ã‚’ブックマークã—ãŸã‚Šã€ãれを他ã®äººã¨" | ||
252 | "共有ã™ã‚‹ã‹ã€ã¾ãŸã¯ãƒ—ライベートãªã‚‚ã®ã¨ã—ã¦ä¿ç®¡ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚\n" | ||
253 | "åŠ ãˆã¦ã€ã‚ãªãŸã®ãƒ–ックマークã«ã“ã®é …ç›®ã®ã‚ˆã†ã«æ¦‚è¦ã‚’è¿½åŠ ã—ãŸã‚Šã€ã‚¿ã‚°ä»˜ã‘ã—ãŸ" | ||
254 | "ã‚Šã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚\n" | ||
255 | "\n" | ||
256 | "`+Shaare` ボタンをクリックã™ã‚‹ã“ã¨ã§æ–°ã—ã„ shaare を作æˆã§ãã¾ã™ã€‚ã¾ãŸã€æŽ¨å¥¨" | ||
257 | "ã•ã‚ŒãŸãƒ„ールを使ã†ã“ã¨ã‚‚ã§ãã¾ã™ (ブラウザー 拡張機能ã€ãƒ¢ãƒã‚¤ãƒ« アプリã€ãƒ–ッ" | ||
258 | "クマークレットã€REST API ãªã©...)。\n" | ||
259 | "\n" | ||
260 | "ã¾ãŸã€ç°¡å˜ã«ã‚ãªãŸã®ãƒªãƒ³ã‚¯ã‚’å–å¾—ã§ãã¾ã™ã€‚ãã‚ŒãŒä½•åƒã¨ç™»ã‚‹æ•°ã§ã‚ã£ã¦ã‚‚ã€å†…部" | ||
261 | "ã®æ¤œç´¢ã‚¨ãƒ³ã‚¸ãƒ³ã‚„ã€ã‚¿ã‚°ã‚’使ã£ã¦æ¤œç´¢ã§ãã¾ã™ (例ãˆã°ã€ã“ã® Shaare 㯠`shaarli` " | ||
262 | "㨠`help` ã¨ã„ã†ã‚¿ã‚°ãŒä»˜ã„ã¦ã„ã¾ã™)。\n" | ||
263 | "#shaarli ã‚„ #help ã¨ã„ã£ãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚‚サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã™ã€‚\n" | ||
264 | "タグやテã‚スト検索ã«ã‚ˆã‚‹ [RSS フィード](/feed/atom) ã‚„ ピクãƒãƒ£ãƒ¼ ウォール ã§" | ||
265 | "é …ç›®ã‚’çµžã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚\n" | ||
266 | "\n" | ||
267 | "ç§ãŸã¡ã¯ã‚ãªãŸãŒ Shaarli を楽ã—ã‚“ã§ãれるã“ã¨ã‚’願ã£ã¦ã„ã¾ã™ã€‚Shaarli ã¯ã‚³ãƒŸãƒ¥" | ||
268 | "ニティーã«ã‚ˆã£ã¦ ♡ ã¨å…±ã«ãƒ¡ãƒ³ãƒ†ãƒŠãƒ³ã‚¹ã•ã‚Œã¦ã„ã¾ã™ï¼\n" | ||
269 | "何ã‹å•é¡Œã«éé‡ã—ãŸã‚Šã€æ案ãŒã‚ã‚Œã°ã€æ°—軽㫠[Issue](https://github.com/" | ||
270 | "shaarli/Shaarli/issues) ã‚’é–‹ã„ã¦ãã ã•ã„。\n" | ||
271 | |||
272 | #: application/bookmark/exception/BookmarkNotFoundException.php:13 | ||
273 | msgid "The link you are trying to reach does not exist or has been deleted." | ||
274 | msgstr "é–‹ã“ã†ã¨ã—ãŸãƒªãƒ³ã‚¯ã¯å˜åœ¨ã—ãªã„ã‹ã€å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚" | ||
275 | |||
276 | #: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129 | ||
227 | msgid "" | 277 | msgid "" |
228 | "Shaarli could not create the config file. Please make sure Shaarli has the " | 278 | "Shaarli could not create the config file. Please make sure Shaarli has the " |
229 | "right to write in the folder is it installed in." | 279 | "right to write in the folder is it installed in." |
@@ -232,7 +282,8 @@ msgstr "" | |||
232 | "ã¦ã„ã¦ã€ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„るディレクトリã«æ›¸ãè¾¼ã¿ã§ãã‚‹ã“ã¨ã‚’確èªã—ã¦ãã " | 282 | "ã¦ã„ã¦ã€ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„るディレクトリã«æ›¸ãè¾¼ã¿ã§ãã‚‹ã“ã¨ã‚’確èªã—ã¦ãã " |
233 | "ã•ã„。" | 283 | "ã•ã„。" |
234 | 284 | ||
235 | #: application/config/ConfigManager.php:135 | 285 | #: application/config/ConfigManager.php:136 |
286 | #: application/config/ConfigManager.php:163 | ||
236 | msgid "Invalid setting key parameter. String expected, got: " | 287 | msgid "Invalid setting key parameter. String expected, got: " |
237 | msgstr "" | 288 | msgstr "" |
238 | "ä¸æ£ãªã‚ーã®å€¤ã§ã™ã€‚æ–‡å—列ãŒæƒ³å®šã•ã‚Œã¦ã„ã¾ã™ãŒã€æ¬¡ã®ã‚ˆã†ã«å…¥åŠ›ã•ã‚Œã¾ã—ãŸ: " | 289 | "ä¸æ£ãªã‚ーã®å€¤ã§ã™ã€‚æ–‡å—列ãŒæƒ³å®šã•ã‚Œã¦ã„ã¾ã™ãŒã€æ¬¡ã®ã‚ˆã†ã«å…¥åŠ›ã•ã‚Œã¾ã—ãŸ: " |
@@ -250,159 +301,185 @@ msgstr "プラグインã®èªè¾¼é †ã‚’変更ã™ã‚‹éš›ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ | |||
250 | msgid "You are not authorized to alter config." | 301 | msgid "You are not authorized to alter config." |
251 | msgstr "è¨å®šã‚’変更ã™ã‚‹æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“。" | 302 | msgstr "è¨å®šã‚’変更ã™ã‚‹æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“。" |
252 | 303 | ||
253 | #: application/exceptions/IOException.php:19 | 304 | #: application/exceptions/IOException.php:22 |
254 | msgid "Error accessing" | 305 | msgid "Error accessing" |
255 | msgstr "èªè¾¼ä¸ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ" | 306 | msgstr "èªè¾¼ä¸ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ" |
256 | 307 | ||
257 | #: index.php:142 | 308 | #: application/feed/FeedBuilder.php:179 |
258 | msgid "Shared links on " | 309 | msgid "Direct link" |
259 | msgstr "次ã«ãŠã„ã¦å…±æœ‰ã•ã‚ŒãŸãƒªãƒ³ã‚¯:" | 310 | msgstr "ダイレクトリンク" |
260 | 311 | ||
261 | #: index.php:164 | 312 | #: application/feed/FeedBuilder.php:181 |
262 | msgid "Insufficient permissions:" | 313 | msgid "Permalink" |
263 | msgstr "権é™ãŒã‚ã‚Šã¾ã›ã‚“:" | 314 | msgstr "パーマリンク" |
264 | 315 | ||
265 | #: index.php:303 | 316 | #: application/front/controller/admin/ConfigureController.php:54 |
266 | msgid "I said: NO. You are banned for the moment. Go away." | 317 | msgid "Configure" |
267 | msgstr "ã‚ãªãŸã¯ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰BANã•ã‚Œã¦ã„ã¾ã™ã€‚" | 318 | msgstr "è¨å®š" |
268 | 319 | ||
269 | #: index.php:368 | 320 | #: application/front/controller/admin/ConfigureController.php:102 |
270 | msgid "Wrong login/password." | 321 | #: application/legacy/LegacyUpdater.php:537 |
271 | msgstr "ä¸æ£ãªãƒ¦ãƒ¼ã‚¶ãƒ¼åã€ã¾ãŸã¯ãƒ‘スワードã§ã™ã€‚" | 322 | msgid "You have enabled or changed thumbnails mode." |
323 | msgstr "サムãƒã‚¤ãƒ«ã®ãƒ¢ãƒ¼ãƒ‰ã‚’有効化ã€ã¾ãŸã¯å¤‰æ›´ã—ã¾ã—ãŸã€‚" | ||
272 | 324 | ||
273 | #: index.php:576 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | 325 | #: application/front/controller/admin/ConfigureController.php:103 |
274 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42 | 326 | #: application/legacy/LegacyUpdater.php:538 |
275 | msgid "Daily" | 327 | msgid "Please synchronize them." |
276 | msgstr "デイリー" | 328 | msgstr "ãれらをåŒæœŸã—ã¦ãã ã•ã„。" |
277 | 329 | ||
278 | #: index.php:681 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | 330 | #: application/front/controller/admin/ConfigureController.php:113 |
279 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | 331 | #: application/front/controller/visitor/InstallController.php:136 |
280 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 | 332 | msgid "Error while writing config file after configuration update." |
281 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95 | 333 | msgstr "è¨å®šãƒ•ã‚¡ã‚¤ãƒ«ã‚’æ›´æ–°ã—ãŸå¾Œã®æ›¸ãè¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸã€‚" |
282 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71 | ||
283 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95 | ||
284 | msgid "Login" | ||
285 | msgstr "ãƒã‚°ã‚¤ãƒ³" | ||
286 | 334 | ||
287 | #: index.php:722 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | 335 | #: application/front/controller/admin/ConfigureController.php:122 |
288 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39 | 336 | msgid "Configuration was saved." |
289 | msgid "Picture wall" | 337 | msgstr "è¨å®šã¯ä¿å˜ã•ã‚Œã¾ã—ãŸã€‚" |
290 | msgstr "ピクãƒãƒ£ã‚¦ã‚©ãƒ¼ãƒ«" | ||
291 | 338 | ||
292 | #: index.php:770 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | 339 | #: application/front/controller/admin/ExportController.php:26 |
293 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 | 340 | msgid "Export" |
294 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | 341 | msgstr "エクスãƒãƒ¼ãƒˆ" |
295 | msgid "Tag cloud" | ||
296 | msgstr "タグクラウド" | ||
297 | 342 | ||
298 | #: index.php:803 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | 343 | #: application/front/controller/admin/ExportController.php:42 |
299 | msgid "Tag list" | 344 | msgid "Please select an export mode." |
300 | msgstr "タグ一覧" | 345 | msgstr "エクスãƒãƒ¼ãƒˆ モードを指定ã—ã¦ãã ã•ã„。" |
301 | 346 | ||
302 | #: index.php:1028 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 | 347 | #: application/front/controller/admin/ImportController.php:41 |
303 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 | 348 | msgid "Import" |
304 | msgid "Tools" | 349 | msgstr "インãƒãƒ¼ãƒˆ" |
305 | msgstr "ツール" | ||
306 | 350 | ||
307 | #: index.php:1037 | 351 | #: application/front/controller/admin/ImportController.php:55 |
308 | msgid "You are not supposed to change a password on an Open Shaarli." | 352 | msgid "No import file provided." |
353 | msgstr "何ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆå…ƒãƒ•ã‚¡ã‚¤ãƒ«ã‚‚指定ã•ã‚Œã¾ã›ã‚“ã§ã—ãŸã€‚" | ||
354 | |||
355 | #: application/front/controller/admin/ImportController.php:66 | ||
356 | #, php-format | ||
357 | msgid "" | ||
358 | "The file you are trying to upload is probably bigger than what this " | ||
359 | "webserver can accept (%s). Please upload in smaller chunks." | ||
309 | msgstr "" | 360 | msgstr "" |
310 | "公開ã•ã‚Œã¦ã„ã‚‹ Shaarli ã«ãŠã„ã¦ã€ãƒ‘スワードを変更ã™ã‚‹ã“ã¨ã¯æƒ³å®šã•ã‚Œã¦ã„ã¾ã›" | 361 | "ã‚ãªãŸãŒã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã—よã†ã¨ã—ã¦ã„るファイルã¯ã€ã‚µãƒ¼ãƒãƒ¼ãŒè¨±å¯ã—ã¦ã„るファイ" |
311 | "ん。" | 362 | "ルサイズ (%s) よりも大ãã„ã§ã™ã€‚ã‚‚ã†å°‘ã—å°ã•ã„ã‚‚ã®ã‚’アップãƒãƒ¼ãƒ‰ã—ã¦ãã ã•" |
363 | "ã„。" | ||
312 | 364 | ||
313 | #: index.php:1042 index.php:1084 index.php:1160 index.php:1191 index.php:1291 | 365 | #: application/front/controller/admin/ManageShaareController.php:29 |
314 | msgid "Wrong token." | 366 | msgid "Shaare a new link" |
315 | msgstr "ä¸æ£ãªãƒˆãƒ¼ã‚¯ãƒ³ã§ã™ã€‚" | 367 | msgstr "æ–°ã—ã„ãƒªãƒ³ã‚¯ã‚’è¿½åŠ " |
316 | 368 | ||
317 | #: index.php:1047 | 369 | #: application/front/controller/admin/ManageShaareController.php:78 |
318 | msgid "The old password is not correct." | 370 | msgid "Note: " |
319 | msgstr "å…ƒã®ãƒ‘スワードãŒæ£ã—ãã‚ã‚Šã¾ã›ã‚“。" | 371 | msgstr "注: " |
320 | 372 | ||
321 | #: index.php:1067 | 373 | #: application/front/controller/admin/ManageShaareController.php:109 |
322 | msgid "Your password has been changed" | 374 | #: application/front/controller/admin/ManageShaareController.php:206 |
323 | msgstr "ã‚ãªãŸã®ãƒ‘スワードã¯å¤‰æ›´ã•ã‚Œã¾ã—ãŸ" | 375 | #: application/front/controller/admin/ManageShaareController.php:275 |
376 | #: application/front/controller/admin/ManageShaareController.php:315 | ||
377 | #, php-format | ||
378 | msgid "Bookmark with identifier %s could not be found." | ||
379 | msgstr "%s ã¨ã„ã†è˜åˆ¥åã‚’æŒã£ãŸãƒ–ックマークã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚" | ||
324 | 380 | ||
325 | #: index.php:1072 | 381 | #: application/front/controller/admin/ManageShaareController.php:194 |
326 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 382 | #: application/front/controller/admin/ManageShaareController.php:252 |
327 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | 383 | msgid "Invalid bookmark ID provided." |
328 | msgid "Change password" | 384 | msgstr "ä¸æ£ãªãƒ–ックマーク ID ãŒå…¥åŠ›ã•ã‚Œã¾ã—ãŸã€‚" |
329 | msgstr "パスワードを変更" | ||
330 | 385 | ||
331 | #: index.php:1120 | 386 | #: application/front/controller/admin/ManageShaareController.php:260 |
332 | msgid "Configuration was saved." | 387 | msgid "Invalid visibility provided." |
333 | msgstr "è¨å®šã¯ä¿å˜ã•ã‚Œã¾ã—ãŸã€‚" | 388 | msgstr "ä¸æ£ãªå…¬é–‹è¨å®šãŒå…¥åŠ›ã•ã‚Œã¾ã—ãŸã€‚" |
334 | 389 | ||
335 | #: index.php:1143 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | 390 | #: application/front/controller/admin/ManageShaareController.php:363 |
336 | msgid "Configure" | 391 | msgid "Edit" |
337 | msgstr "è¨å®š" | 392 | msgstr "共有" |
393 | |||
394 | #: application/front/controller/admin/ManageShaareController.php:366 | ||
395 | msgid "Shaare" | ||
396 | msgstr "Shaare" | ||
338 | 397 | ||
339 | #: index.php:1154 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 398 | #: application/front/controller/admin/ManageTagController.php:29 |
340 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
341 | msgid "Manage tags" | 399 | msgid "Manage tags" |
342 | msgstr "ã‚¿ã‚°ã‚’è¨å®š" | 400 | msgstr "ã‚¿ã‚°ã‚’è¨å®š" |
343 | 401 | ||
344 | #: index.php:1172 | 402 | #: application/front/controller/admin/ManageTagController.php:48 |
403 | msgid "Invalid tags provided." | ||
404 | msgstr "ä¸æ£ãªã‚¿ã‚°ãŒå…¥åŠ›ã•ã‚Œã¾ã—ãŸã€‚" | ||
405 | |||
406 | #: application/front/controller/admin/ManageTagController.php:72 | ||
345 | #, php-format | 407 | #, php-format |
346 | msgid "The tag was removed from %d link." | 408 | msgid "The tag was removed from %d bookmark." |
347 | msgid_plural "The tag was removed from %d links." | 409 | msgid_plural "The tag was removed from %d bookmarks." |
348 | msgstr[0] "%d 件ã®ãƒªãƒ³ã‚¯ã‹ã‚‰ã‚¿ã‚°ãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚" | 410 | msgstr[0] "%d 件ã®ãƒªãƒ³ã‚¯ã‹ã‚‰ã‚¿ã‚°ãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚" |
349 | msgstr[1] "The tag was removed from %d links." | 411 | msgstr[1] "%d 件ã®ãƒªãƒ³ã‚¯ã‹ã‚‰ã‚¿ã‚°ãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚" |
350 | 412 | ||
351 | #: index.php:1173 | 413 | #: application/front/controller/admin/ManageTagController.php:77 |
352 | #, php-format | 414 | #, php-format |
353 | msgid "The tag was renamed in %d link." | 415 | msgid "The tag was renamed in %d bookmark." |
354 | msgid_plural "The tag was renamed in %d links." | 416 | msgid_plural "The tag was renamed in %d bookmarks." |
355 | msgstr[0] "タグ㌠%d 件ã®ãƒªãƒ³ã‚¯ã«ãŠã„ã¦ã€åå‰ãŒå¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚" | 417 | msgstr[0] "ã“ã®ã‚¿ã‚°ã‚’æŒã¤ %d 件ã®ãƒªãƒ³ã‚¯ã«ãŠã„ã¦ã€åå‰ãŒå¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚" |
356 | msgstr[1] "タグ㌠%d 件ã®ãƒªãƒ³ã‚¯ã«ãŠã„ã¦ã€åå‰ãŒå¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚" | 418 | msgstr[1] "ã“ã®ã‚¿ã‚°ã‚’æŒã¤ %d 件ã®ãƒªãƒ³ã‚¯ã«ãŠã„ã¦ã€åå‰ãŒå¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚" |
357 | 419 | ||
358 | #: index.php:1181 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 420 | #: application/front/controller/admin/PasswordController.php:28 |
359 | msgid "Shaare a new link" | 421 | msgid "Change password" |
360 | msgstr "æ–°ã—ã„ãƒªãƒ³ã‚¯ã‚’è¿½åŠ " | 422 | msgstr "パスワードを変更" |
361 | 423 | ||
362 | #: index.php:1351 tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | 424 | #: application/front/controller/admin/PasswordController.php:55 |
363 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 | 425 | msgid "You must provide the current and new password to change it." |
364 | msgid "Edit" | 426 | msgstr "" |
365 | msgstr "共有" | 427 | "パスワードを変更ã™ã‚‹ã«ã¯ã€ç¾åœ¨ã®ãƒ‘スワードã¨ã€æ–°ã—ã„パスワードを入力ã™ã‚‹å¿…è¦" |
428 | "ãŒã‚ã‚Šã¾ã™ã€‚" | ||
366 | 429 | ||
367 | #: index.php:1351 index.php:1421 | 430 | #: application/front/controller/admin/PasswordController.php:71 |
368 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | 431 | msgid "The old password is not correct." |
369 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | 432 | msgstr "å…ƒã®ãƒ‘スワードãŒæ£ã—ãã‚ã‚Šã¾ã›ã‚“。" |
370 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26 | ||
371 | msgid "Shaare" | ||
372 | msgstr "Shaare" | ||
373 | 433 | ||
374 | #: index.php:1390 | 434 | #: application/front/controller/admin/PasswordController.php:97 |
375 | msgid "Note: " | 435 | msgid "Your password has been changed" |
376 | msgstr "注: " | 436 | msgstr "ã‚ãªãŸã®ãƒ‘スワードã¯å¤‰æ›´ã•ã‚Œã¾ã—ãŸ" |
377 | 437 | ||
378 | #: index.php:1430 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 | 438 | #: application/front/controller/admin/PluginsController.php:45 |
379 | msgid "Export" | 439 | msgid "Plugin Administration" |
380 | msgstr "エクスãƒãƒ¼ãƒˆ" | 440 | msgstr "プラグイン管ç†" |
381 | 441 | ||
382 | #: index.php:1492 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 | 442 | #: application/front/controller/admin/PluginsController.php:76 |
383 | msgid "Import" | 443 | msgid "Setting successfully saved." |
384 | msgstr "インãƒãƒ¼ãƒˆ" | 444 | msgstr "è¨å®šãŒæ£å¸¸ã«ä¿å˜ã•ã‚Œã¾ã—ãŸã€‚" |
385 | 445 | ||
386 | #: index.php:1502 | 446 | #: application/front/controller/admin/PluginsController.php:79 |
387 | #, php-format | 447 | msgid "Error while saving plugin configuration: " |
388 | msgid "" | 448 | msgstr "プラグインã®è¨å®šãƒ•ã‚¡ã‚¤ãƒ«ã‚’ä¿å˜ã™ã‚‹ã¨ãã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ: " |
389 | "The file you are trying to upload is probably bigger than what this " | ||
390 | "webserver can accept (%s). Please upload in smaller chunks." | ||
391 | msgstr "" | ||
392 | "ã‚ãªãŸãŒã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã—よã†ã¨ã—ã¦ã„るファイルã¯ã€ã‚µãƒ¼ãƒãƒ¼ãŒè¨±å¯ã—ã¦ã„るファイ" | ||
393 | "ルサイズ (%s) よりも大ãã„ã§ã™ã€‚ã‚‚ã†å°‘ã—å°ã•ã„ã‚‚ã®ã‚’アップãƒãƒ¼ãƒ‰ã—ã¦ãã ã•" | ||
394 | "ã„。" | ||
395 | 449 | ||
396 | #: index.php:1541 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | 450 | #: application/front/controller/admin/ThumbnailsController.php:37 |
397 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | 451 | msgid "Thumbnails update" |
398 | msgid "Plugin administration" | 452 | msgstr "サムãƒã‚¤ãƒ«ã®æ›´æ–°" |
399 | msgstr "プラグイン管ç†" | 453 | |
454 | #: application/front/controller/admin/ToolsController.php:31 | ||
455 | msgid "Tools" | ||
456 | msgstr "ツール" | ||
400 | 457 | ||
401 | #: index.php:1706 | 458 | #: application/front/controller/visitor/BookmarkListController.php:116 |
402 | msgid "Search: " | 459 | msgid "Search: " |
403 | msgstr "検索: " | 460 | msgstr "検索: " |
404 | 461 | ||
405 | #: index.php:1933 | 462 | #: application/front/controller/visitor/DailyController.php:45 |
463 | msgid "Today" | ||
464 | msgstr "今日" | ||
465 | |||
466 | #: application/front/controller/visitor/DailyController.php:47 | ||
467 | msgid "Yesterday" | ||
468 | msgstr "昨日" | ||
469 | |||
470 | #: application/front/controller/visitor/DailyController.php:85 | ||
471 | msgid "Daily" | ||
472 | msgstr "デイリー" | ||
473 | |||
474 | #: application/front/controller/visitor/ErrorController.php:36 | ||
475 | msgid "An unexpected error occurred." | ||
476 | msgstr "予期ã—ãªã„エラーãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚" | ||
477 | |||
478 | #: application/front/controller/visitor/ErrorNotFoundController.php:25 | ||
479 | msgid "Requested page could not be found." | ||
480 | msgstr "リクエストã•ã‚ŒãŸãƒšãƒ¼ã‚¸ã¯å˜åœ¨ã—ã¾ã›ã‚“。" | ||
481 | |||
482 | #: application/front/controller/visitor/InstallController.php:73 | ||
406 | #, php-format | 483 | #, php-format |
407 | msgid "" | 484 | msgid "" |
408 | "<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " | 485 | "<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " |
@@ -420,32 +497,205 @@ msgstr "" | |||
420 | "ã‚Šã¾ã™ã€‚IP アドレスや完全ãªãƒ‰ãƒ¡ã‚¤ãƒ³åã§ã‚µãƒ¼ãƒãƒ¼ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—" | 497 | "ã‚Šã¾ã™ã€‚IP アドレスや完全ãªãƒ‰ãƒ¡ã‚¤ãƒ³åã§ã‚µãƒ¼ãƒãƒ¼ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—" |
421 | "ã¾ã™ã€‚<br>" | 498 | "ã¾ã™ã€‚<br>" |
422 | 499 | ||
423 | #: index.php:1943 | 500 | #: application/front/controller/visitor/InstallController.php:144 |
424 | msgid "Click to try again." | 501 | msgid "" |
425 | msgstr "クリックã—ã¦å†åº¦è©¦ã—ã¾ã™ã€‚" | 502 | "Shaarli is now configured. Please login and start shaaring your bookmarks!" |
503 | msgstr "" | ||
504 | "Shaarli ã®è¨å®šãŒå®Œäº†ã—ã¾ã—ãŸã€‚ãƒã‚°ã‚¤ãƒ³ã—ã¦ã€ã‚ãªãŸã®ãƒ–ックマークを登録ã—ã¾" | ||
505 | "ã—ょã†ï¼" | ||
506 | |||
507 | #: application/front/controller/visitor/InstallController.php:158 | ||
508 | msgid "Insufficient permissions:" | ||
509 | msgstr "権é™ãŒã‚ã‚Šã¾ã›ã‚“:" | ||
510 | |||
511 | #: application/front/controller/visitor/LoginController.php:46 | ||
512 | msgid "Login" | ||
513 | msgstr "ãƒã‚°ã‚¤ãƒ³" | ||
514 | |||
515 | #: application/front/controller/visitor/LoginController.php:78 | ||
516 | msgid "Wrong login/password." | ||
517 | msgstr "ä¸æ£ãªãƒ¦ãƒ¼ã‚¶ãƒ¼åã€ã¾ãŸã¯ãƒ‘スワードã§ã™ã€‚" | ||
518 | |||
519 | #: application/front/controller/visitor/PictureWallController.php:29 | ||
520 | msgid "Picture wall" | ||
521 | msgstr "ピクãƒãƒ£ã‚¦ã‚©ãƒ¼ãƒ«" | ||
522 | |||
523 | #: application/front/controller/visitor/TagCloudController.php:88 | ||
524 | msgid "Tag " | ||
525 | msgstr "ã‚¿ã‚° " | ||
526 | |||
527 | #: application/front/exceptions/AlreadyInstalledException.php:11 | ||
528 | msgid "Shaarli has already been installed. Login to edit the configuration." | ||
529 | msgstr "Shaarli ãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¾ã—ãŸã€‚ãƒã‚°ã‚¤ãƒ³ã—ã¦è¨å®šã‚’変更ã§ãã¾ã™ã€‚" | ||
530 | |||
531 | #: application/front/exceptions/LoginBannedException.php:11 | ||
532 | msgid "" | ||
533 | "You have been banned after too many failed login attempts. Try again later." | ||
534 | msgstr "複数回ã«æ¸¡ã‚‹ãƒã‚°ã‚¤ãƒ³ã¸ã®å¤±æ•—を検出ã—ã¾ã—ãŸã€‚後ã§ã¾ãŸè©¦ã—ã¦ãã ã•ã„。" | ||
535 | |||
536 | #: application/front/exceptions/OpenShaarliPasswordException.php:16 | ||
537 | msgid "You are not supposed to change a password on an Open Shaarli." | ||
538 | msgstr "" | ||
539 | "公開ã•ã‚Œã¦ã„ã‚‹ Shaarli ã«ãŠã„ã¦ã€ãƒ‘スワードを変更ã™ã‚‹ã“ã¨ã¯æƒ³å®šã•ã‚Œã¦ã„ã¾ã›" | ||
540 | "ん。" | ||
541 | |||
542 | #: application/front/exceptions/ThumbnailsDisabledException.php:11 | ||
543 | msgid "Picture wall unavailable (thumbnails are disabled)." | ||
544 | msgstr "ピクãƒãƒ£ ウォールã¯åˆ©ç”¨ã§ãã¾ã›ã‚“ (サムãƒã‚¤ãƒ«ãŒç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™)。" | ||
545 | |||
546 | #: application/front/exceptions/WrongTokenException.php:16 | ||
547 | msgid "Wrong token." | ||
548 | msgstr "ä¸æ£ãªãƒˆãƒ¼ã‚¯ãƒ³ã§ã™ã€‚" | ||
549 | |||
550 | #: application/legacy/LegacyLinkDB.php:131 | ||
551 | msgid "You are not authorized to add a link." | ||
552 | msgstr "ãƒªãƒ³ã‚¯ã‚’è¿½åŠ ã™ã‚‹ã«ã¯ã€ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚" | ||
553 | |||
554 | #: application/legacy/LegacyLinkDB.php:134 | ||
555 | msgid "Internal Error: A link should always have an id and URL." | ||
556 | msgstr "エラー: リンクã«ã¯IDã¨URLを登録ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。" | ||
557 | |||
558 | #: application/legacy/LegacyLinkDB.php:137 | ||
559 | msgid "You must specify an integer as a key." | ||
560 | msgstr "æ£å¸¸ãªã‚ーã®å€¤ã§ã¯ã‚ã‚Šã¾ã›ã‚“。" | ||
561 | |||
562 | #: application/legacy/LegacyLinkDB.php:140 | ||
563 | msgid "Array offset and link ID must be equal." | ||
564 | msgstr "Array オフセットã¨ãƒªãƒ³ã‚¯ã®IDã¯åŒã˜ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。" | ||
565 | |||
566 | #: application/legacy/LegacyLinkDB.php:249 | ||
567 | msgid "" | ||
568 | "Welcome to Shaarli! This is your first public bookmark. To edit or delete " | ||
569 | "me, you must first login.\n" | ||
570 | "\n" | ||
571 | "To learn how to use Shaarli, consult the link \"Documentation\" at the " | ||
572 | "bottom of this page.\n" | ||
573 | "\n" | ||
574 | "You use the community supported version of the original Shaarli project, by " | ||
575 | "Sebastien Sauvage." | ||
576 | msgstr "" | ||
577 | "Shaarli ã¸ã‚ˆã†ã“ãï¼ ã“ã‚Œã¯ã‚ãªãŸã®æœ€åˆã®å…¬é–‹ãƒ–ックマークã§ã™ã€‚ã“れを編集ã—ãŸ" | ||
578 | "り削除ã—ãŸã‚Šã™ã‚‹ã«ã¯ã€ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚\n" | ||
579 | "\n" | ||
580 | "Shaarli ã®ä½¿ã„方を知るã«ã¯ã€ã“ã®ãƒšãƒ¼ã‚¸ã®ä¸‹ã«ã‚る「ドã‚ュメントã€ã®ãƒªãƒ³ã‚¯ã‚’é–‹" | ||
581 | "ã„ã¦ãã ã•ã„。\n" | ||
582 | "\n" | ||
583 | "ã‚ãªãŸã¯ Sebastien Sauvage ã«ã‚ˆã‚‹ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ãƒ¼ã‚µãƒãƒ¼ãƒˆã®ã‚ã‚‹ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ã‚ª" | ||
584 | "リジナルã®Shaarli プãƒã‚¸ã‚§ã‚¯ãƒˆã‚’使用ã—ã¦ã„ã¾ã™ã€‚" | ||
585 | |||
586 | #: application/legacy/LegacyLinkDB.php:266 | ||
587 | msgid "My secret stuff... - Pastebin.com" | ||
588 | msgstr "ã‚ãŸã—ã®ã²ðŸ’—ã¿ðŸ’—ã¤ðŸ’— - Pastebin.com" | ||
589 | |||
590 | #: application/legacy/LegacyLinkDB.php:268 | ||
591 | msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." | ||
592 | msgstr "" | ||
593 | "ã‚·ãƒ¼ãƒƒï¼ ã“ã‚Œã¯ã‚ãªãŸã—ã‹è¦‹ã‚‰ã‚Œãªã„プライベートリンクã§ã™ã€‚消ã™ã“ã¨ã‚‚ã§ãã¾" | ||
594 | "ã™ã€‚" | ||
595 | |||
596 | #: application/legacy/LegacyUpdater.php:104 | ||
597 | #, fuzzy | ||
598 | #| msgid "Couldn't retrieve Updater class methods." | ||
599 | msgid "Couldn't retrieve updater class methods." | ||
600 | msgstr "アップデーターã®ã‚¯ãƒ©ã‚¹ãƒ¡ã‚¾ãƒƒãƒˆã‚’å—ä¿¡ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" | ||
601 | |||
602 | #: application/legacy/LegacyUpdater.php:538 | ||
603 | msgid "<a href=\"./admin/thumbnails\">" | ||
604 | msgstr "<a href=\"./admin/thumbnails\">" | ||
605 | |||
606 | #: application/netscape/NetscapeBookmarkUtils.php:63 | ||
607 | msgid "Invalid export selection:" | ||
608 | msgstr "ä¸æ£ãªã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã®é¸æŠž:" | ||
609 | |||
610 | #: application/netscape/NetscapeBookmarkUtils.php:215 | ||
611 | #, php-format | ||
612 | msgid "File %s (%d bytes) " | ||
613 | msgstr "ファイル %s (%d ãƒã‚¤ãƒˆ) " | ||
614 | |||
615 | #: application/netscape/NetscapeBookmarkUtils.php:217 | ||
616 | msgid "has an unknown file format. Nothing was imported." | ||
617 | msgstr "ã¯ä¸æ˜Žãªãƒ•ã‚¡ã‚¤ãƒ«å½¢å¼ã§ã™ã€‚インãƒãƒ¼ãƒˆã¯ä¸æ¢ã•ã‚Œã¾ã—ãŸã€‚" | ||
618 | |||
619 | #: application/netscape/NetscapeBookmarkUtils.php:221 | ||
620 | #, fuzzy, php-format | ||
621 | #| msgid "" | ||
622 | #| "was successfully processed in %d seconds: %d links imported, %d links " | ||
623 | #| "overwritten, %d links skipped." | ||
624 | msgid "" | ||
625 | "was successfully processed in %d seconds: %d bookmarks imported, %d " | ||
626 | "bookmarks overwritten, %d bookmarks skipped." | ||
627 | msgstr "" | ||
628 | "㌠%d 秒ã§å‡¦ç†ã•ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒã‚¤ãƒ³ãƒãƒ¼ãƒˆã•ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒä¸Šæ›¸ãã•" | ||
629 | "ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒã‚¹ã‚ップã•ã‚Œã¾ã—ãŸã€‚" | ||
630 | |||
631 | #: application/plugin/PluginManager.php:124 | ||
632 | msgid " [plugin incompatibility]: " | ||
633 | msgstr "[éžå¯¾å¿œã®ãƒ—ラグイン]: " | ||
634 | |||
635 | #: application/plugin/exception/PluginFileNotFoundException.php:21 | ||
636 | #, php-format | ||
637 | msgid "Plugin \"%s\" files not found." | ||
638 | msgstr "プラグイン「%sã€ã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒå˜åœ¨ã—ã¾ã›ã‚“。" | ||
639 | |||
640 | #: application/render/PageCacheManager.php:32 | ||
641 | #, php-format | ||
642 | msgid "Cannot purge %s: no directory" | ||
643 | msgstr "%s を削除ã§ãã¾ã›ã‚“: ディレクトリãŒå˜åœ¨ã—ã¾ã›ã‚“" | ||
644 | |||
645 | #: application/updater/exception/UpdaterException.php:51 | ||
646 | msgid "An error occurred while running the update " | ||
647 | msgstr "æ›´æ–°ä¸ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—㟠" | ||
648 | |||
649 | #: index.php:65 | ||
650 | msgid "Shared bookmarks on " | ||
651 | msgstr "次ã«ãŠã„ã¦å…±æœ‰ã•ã‚ŒãŸãƒªãƒ³ã‚¯ " | ||
426 | 652 | ||
427 | #: plugins/addlink_toolbar/addlink_toolbar.php:29 | 653 | #: plugins/addlink_toolbar/addlink_toolbar.php:31 |
428 | msgid "URI" | 654 | msgid "URI" |
429 | msgstr "URI" | 655 | msgstr "URI" |
430 | 656 | ||
431 | #: plugins/addlink_toolbar/addlink_toolbar.php:33 | 657 | #: plugins/addlink_toolbar/addlink_toolbar.php:35 |
432 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
433 | msgid "Add link" | 658 | msgid "Add link" |
434 | msgstr "ãƒªãƒ³ã‚¯ã‚’è¿½åŠ " | 659 | msgstr "ãƒªãƒ³ã‚¯ã‚’è¿½åŠ " |
435 | 660 | ||
436 | #: plugins/addlink_toolbar/addlink_toolbar.php:50 | 661 | #: plugins/addlink_toolbar/addlink_toolbar.php:52 |
437 | msgid "Adds the addlink input on the linklist page." | 662 | msgid "Adds the addlink input on the linklist page." |
438 | msgstr "リンク一覧ã®ãƒšãƒ¼ã‚¸ã«ã€ãƒªãƒ³ã‚¯ã‚’è¿½åŠ ã™ã‚‹ãŸã‚ã®ãƒ•ã‚©ãƒ¼ãƒ を表示ã™ã‚‹ã€‚" | 663 | msgstr "リンク一覧ã®ãƒšãƒ¼ã‚¸ã«ã€ãƒªãƒ³ã‚¯ã‚’è¿½åŠ ã™ã‚‹ãŸã‚ã®ãƒ•ã‚©ãƒ¼ãƒ を表示ã™ã‚‹ã€‚" |
439 | 664 | ||
440 | #: plugins/archiveorg/archiveorg.php:23 | 665 | #: plugins/archiveorg/archiveorg.php:28 |
441 | msgid "View on archive.org" | 666 | msgid "View on archive.org" |
442 | msgstr "archive.org 上ã§è¡¨ç¤ºã™ã‚‹" | 667 | msgstr "archive.org 上ã§è¡¨ç¤ºã™ã‚‹" |
443 | 668 | ||
444 | #: plugins/archiveorg/archiveorg.php:36 | 669 | #: plugins/archiveorg/archiveorg.php:41 |
445 | msgid "For each link, add an Archive.org icon." | 670 | msgid "For each link, add an Archive.org icon." |
446 | msgstr "ãã‚Œãžã‚Œã®ãƒªãƒ³ã‚¯ã«ã€Archive.org ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’è¿½åŠ ã™ã‚‹ã€‚" | 671 | msgstr "ãã‚Œãžã‚Œã®ãƒªãƒ³ã‚¯ã«ã€Archive.org ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’è¿½åŠ ã™ã‚‹ã€‚" |
447 | 672 | ||
448 | #: plugins/demo_plugin/demo_plugin.php:465 | 673 | #: plugins/default_colors/default_colors.php:38 |
674 | msgid "" | ||
675 | "Default colors plugin error: This plugin is active and no custom color is " | ||
676 | "configured." | ||
677 | msgstr "" | ||
678 | "既定ã®è‰²ã®ãƒ—ラグインã«ãŠã‘るエラー: ã“ã®ãƒ—ラグインã¯æœ‰åŠ¹ãªã®ã§ã€ã‚«ã‚¹ã‚¿ãƒ ã‚«" | ||
679 | "ラーã¯é©ç”¨ã•ã‚Œã¾ã›ã‚“。" | ||
680 | |||
681 | #: plugins/default_colors/default_colors.php:113 | ||
682 | msgid "Override default theme colors. Use any CSS valid color." | ||
683 | msgstr "" | ||
684 | "既定ã®ãƒ†ãƒ¼ãƒžã®è‰²ã‚’上書ãã—ã¾ã™ã€‚ã©ã®ã‚ˆã†ãª CSS カラーコードã§ã‚‚使ãˆã¾ã™ã€‚" | ||
685 | |||
686 | #: plugins/default_colors/default_colors.php:114 | ||
687 | msgid "Main color (navbar green)" | ||
688 | msgstr "メイン カラー (ナビãƒãƒ¼ã®ç·‘)" | ||
689 | |||
690 | #: plugins/default_colors/default_colors.php:115 | ||
691 | msgid "Background color (light grey)" | ||
692 | msgstr "背景色 (ç°è‰²)" | ||
693 | |||
694 | #: plugins/default_colors/default_colors.php:116 | ||
695 | msgid "Dark main color (e.g. visited links)" | ||
696 | msgstr "æš—ã„方㮠メイン カラー (例: 閲覧済ã¿ãƒªãƒ³ã‚¯)" | ||
697 | |||
698 | #: plugins/demo_plugin/demo_plugin.php:477 | ||
449 | msgid "" | 699 | msgid "" |
450 | "A demo plugin covering all use cases for template designers and plugin " | 700 | "A demo plugin covering all use cases for template designers and plugin " |
451 | "developers." | 701 | "developers." |
@@ -453,7 +703,15 @@ msgstr "" | |||
453 | "テンプレートã®ãƒ‡ã‚¶ã‚¤ãƒŠãƒ¼ã‚„ã€ãƒ—ラグインã®é–‹ç™ºè€…ã®ãŸã‚ã®ã™ã¹ã¦ã®çŠ¶æ³ã«å¯¾å¿œã§ã" | 703 | "テンプレートã®ãƒ‡ã‚¶ã‚¤ãƒŠãƒ¼ã‚„ã€ãƒ—ラグインã®é–‹ç™ºè€…ã®ãŸã‚ã®ã™ã¹ã¦ã®çŠ¶æ³ã«å¯¾å¿œã§ã" |
454 | "るデモプラグインã§ã™ã€‚" | 704 | "るデモプラグインã§ã™ã€‚" |
455 | 705 | ||
456 | #: plugins/isso/isso.php:20 | 706 | #: plugins/demo_plugin/demo_plugin.php:478 |
707 | msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." | ||
708 | msgstr "ã“ã‚Œã¯ãƒ‡ãƒ¢ãƒ—ラグイン専用ã®ãƒ‘ラメーターã§ã™ã€‚末尾ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚" | ||
709 | |||
710 | #: plugins/demo_plugin/demo_plugin.php:479 | ||
711 | msgid "Other demo parameter" | ||
712 | msgstr "ä»–ã®ãƒ‡ãƒ¢ パラメーター" | ||
713 | |||
714 | #: plugins/isso/isso.php:22 | ||
457 | msgid "" | 715 | msgid "" |
458 | "Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " | 716 | "Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " |
459 | "administration page." | 717 | "administration page." |
@@ -461,45 +719,17 @@ msgstr "" | |||
461 | "Isso プラグインエラー: \"ISSO_SERVER\" ã®å€¤ã‚’プラグイン管ç†ãƒšãƒ¼ã‚¸ã«ã¦æŒ‡å®šã—ã¦" | 719 | "Isso プラグインエラー: \"ISSO_SERVER\" ã®å€¤ã‚’プラグイン管ç†ãƒšãƒ¼ã‚¸ã«ã¦æŒ‡å®šã—ã¦" |
462 | "ãã ã•ã„。" | 720 | "ãã ã•ã„。" |
463 | 721 | ||
464 | #: plugins/isso/isso.php:63 | 722 | #: plugins/isso/isso.php:92 |
465 | msgid "Let visitor comment your shaares on permalinks with Isso." | 723 | msgid "Let visitor comment your shaares on permalinks with Isso." |
466 | msgstr "" | 724 | msgstr "" |
467 | "Isso を使ã£ã¦ã€ã‚ãªãŸã®ãƒ‘ーマリンク上ã®ãƒªãƒ³ã‚¯ã«ç¬¬ä¸‰è€…ãŒã‚³ãƒ¡ãƒ³ãƒˆã‚’残ã™ã“ã¨ãŒã§" | 725 | "Isso を使ã£ã¦ã€ã‚ãªãŸã®ãƒ‘ーマリンク上ã®ãƒªãƒ³ã‚¯ã«ç¬¬ä¸‰è€…ãŒã‚³ãƒ¡ãƒ³ãƒˆã‚’残ã™ã“ã¨ãŒã§" |
468 | "ãã¾ã™ã€‚" | 726 | "ãã¾ã™ã€‚" |
469 | 727 | ||
470 | #: plugins/isso/isso.php:64 | 728 | #: plugins/isso/isso.php:93 |
471 | msgid "Isso server URL (without 'http://')" | 729 | msgid "Isso server URL (without 'http://')" |
472 | msgstr "Isso server URL ('http://' 抜ã)" | 730 | msgstr "Isso server URL ('http://' 抜ã)" |
473 | 731 | ||
474 | #: plugins/markdown/markdown.php:158 | 732 | #: plugins/piwik/piwik.php:23 |
475 | msgid "Description will be rendered with" | ||
476 | msgstr "説明ã¯æ¬¡ã®æ–¹æ³•ã§æç”»ã•ã‚Œã¾ã™:" | ||
477 | |||
478 | #: plugins/markdown/markdown.php:159 | ||
479 | msgid "Markdown syntax documentation" | ||
480 | msgstr "マークダウン形å¼ã®ãƒ‰ã‚ュメント" | ||
481 | |||
482 | #: plugins/markdown/markdown.php:160 | ||
483 | msgid "Markdown syntax" | ||
484 | msgstr "マークダウン形å¼" | ||
485 | |||
486 | #: plugins/markdown/markdown.php:339 | ||
487 | msgid "" | ||
488 | "Render shaare description with Markdown syntax.<br><strong>Warning</" | ||
489 | "strong>:\n" | ||
490 | "If your shaared descriptions contained HTML tags before enabling the " | ||
491 | "markdown plugin,\n" | ||
492 | "enabling it might break your page.\n" | ||
493 | "See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/" | ||
494 | "markdown#html-rendering\">README</a>." | ||
495 | msgstr "" | ||
496 | "リンクã®èª¬æ˜Žã‚’マークダウン形å¼ã§è¡¨ç¤ºã—ã¾ã™ã€‚<br><strong>è¦å‘Š</strong>:\n" | ||
497 | "リンクã®èª¬æ˜Žã«HTMLã‚¿ã‚°ãŒã“ã®ãƒ—ラグインを有効ã«ã™ã‚‹å‰ã«å«ã¾ã‚Œã¦ã„ãŸå ´åˆã€\n" | ||
498 | "æ£å¸¸ã«ãƒšãƒ¼ã‚¸ã‚’表示ã§ããªããªã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。\n" | ||
499 | "詳ã—ã㯠<a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/" | ||
500 | "markdown#html-rendering\">README</a> ã‚’ã”覧ãã ã•ã„。" | ||
501 | |||
502 | #: plugins/piwik/piwik.php:21 | ||
503 | msgid "" | 733 | msgid "" |
504 | "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " | 734 | "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " |
505 | "administration page." | 735 | "administration page." |
@@ -507,27 +737,27 @@ msgstr "" | |||
507 | "Piwik プラグインエラー: PIWIK_URL 㨠PIWIK_SITEID ã®å€¤ã‚’プラグイン管ç†ãƒšãƒ¼ã‚¸" | 737 | "Piwik プラグインエラー: PIWIK_URL 㨠PIWIK_SITEID ã®å€¤ã‚’プラグイン管ç†ãƒšãƒ¼ã‚¸" |
508 | "ã§æŒ‡å®šã—ã¦ãã ã•ã„。" | 738 | "ã§æŒ‡å®šã—ã¦ãã ã•ã„。" |
509 | 739 | ||
510 | #: plugins/piwik/piwik.php:70 | 740 | #: plugins/piwik/piwik.php:72 |
511 | msgid "A plugin that adds Piwik tracking code to Shaarli pages." | 741 | msgid "A plugin that adds Piwik tracking code to Shaarli pages." |
512 | msgstr "Piwik ã®ãƒˆãƒ©ãƒƒã‚ングコードをShaarliã«è¿½åŠ ã™ã‚‹ãƒ—ラグインã§ã™ã€‚" | 742 | msgstr "Piwik ã®ãƒˆãƒ©ãƒƒã‚ングコードをShaarliã«è¿½åŠ ã™ã‚‹ãƒ—ラグインã§ã™ã€‚" |
513 | 743 | ||
514 | #: plugins/piwik/piwik.php:71 | 744 | #: plugins/piwik/piwik.php:73 |
515 | msgid "Piwik URL" | 745 | msgid "Piwik URL" |
516 | msgstr "Piwik URL" | 746 | msgstr "Piwik URL" |
517 | 747 | ||
518 | #: plugins/piwik/piwik.php:72 | 748 | #: plugins/piwik/piwik.php:74 |
519 | msgid "Piwik site ID" | 749 | msgid "Piwik site ID" |
520 | msgstr "Piwik サイトID" | 750 | msgstr "Piwik サイトID" |
521 | 751 | ||
522 | #: plugins/playvideos/playvideos.php:22 | 752 | #: plugins/playvideos/playvideos.php:25 |
523 | msgid "Video player" | 753 | msgid "Video player" |
524 | msgstr "動画プレイヤー" | 754 | msgstr "動画プレイヤー" |
525 | 755 | ||
526 | #: plugins/playvideos/playvideos.php:25 | 756 | #: plugins/playvideos/playvideos.php:28 |
527 | msgid "Play Videos" | 757 | msgid "Play Videos" |
528 | msgstr "動画をå†ç”Ÿ" | 758 | msgstr "動画をå†ç”Ÿ" |
529 | 759 | ||
530 | #: plugins/playvideos/playvideos.php:56 | 760 | #: plugins/playvideos/playvideos.php:59 |
531 | msgid "Add a button in the toolbar allowing to watch all videos." | 761 | msgid "Add a button in the toolbar allowing to watch all videos." |
532 | msgstr "ã™ã¹ã¦ã®å‹•ç”»ã‚’閲覧ã™ã‚‹ãƒœã‚¿ãƒ³ã‚’ツールãƒãƒ¼ã«è¿½åŠ ã—ã¾ã™ã€‚" | 762 | msgstr "ã™ã¹ã¦ã®å‹•ç”»ã‚’閲覧ã™ã‚‹ãƒœã‚¿ãƒ³ã‚’ツールãƒãƒ¼ã«è¿½åŠ ã—ã¾ã™ã€‚" |
533 | 763 | ||
@@ -535,26 +765,26 @@ msgstr "ã™ã¹ã¦ã®å‹•ç”»ã‚’閲覧ã™ã‚‹ãƒœã‚¿ãƒ³ã‚’ツールãƒãƒ¼ã«è¿½åŠ ã— | |||
535 | msgid "plugins/playvideos/jquery-1.11.2.min.js" | 765 | msgid "plugins/playvideos/jquery-1.11.2.min.js" |
536 | msgstr "plugins/playvideos/jquery-1.11.2.min.js" | 766 | msgstr "plugins/playvideos/jquery-1.11.2.min.js" |
537 | 767 | ||
538 | #: plugins/pubsubhubbub/pubsubhubbub.php:69 | 768 | #: plugins/pubsubhubbub/pubsubhubbub.php:72 |
539 | #, php-format | 769 | #, php-format |
540 | msgid "Could not publish to PubSubHubbub: %s" | 770 | msgid "Could not publish to PubSubHubbub: %s" |
541 | msgstr "PubSubHubbub ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ: %s" | 771 | msgstr "PubSubHubbub ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ: %s" |
542 | 772 | ||
543 | #: plugins/pubsubhubbub/pubsubhubbub.php:95 | 773 | #: plugins/pubsubhubbub/pubsubhubbub.php:99 |
544 | #, php-format | 774 | #, php-format |
545 | msgid "Could not post to %s" | 775 | msgid "Could not post to %s" |
546 | msgstr "%s ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ" | 776 | msgstr "%s ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ" |
547 | 777 | ||
548 | #: plugins/pubsubhubbub/pubsubhubbub.php:99 | 778 | #: plugins/pubsubhubbub/pubsubhubbub.php:103 |
549 | #, php-format | 779 | #, php-format |
550 | msgid "Bad response from the hub %s" | 780 | msgid "Bad response from the hub %s" |
551 | msgstr "ãƒãƒ– %s ã‹ã‚‰ã®ä¸æ£ãªãƒ¬ã‚¹ãƒãƒ³ã‚¹" | 781 | msgstr "ãƒãƒ– %s ã‹ã‚‰ã®ä¸æ£ãªãƒ¬ã‚¹ãƒãƒ³ã‚¹" |
552 | 782 | ||
553 | #: plugins/pubsubhubbub/pubsubhubbub.php:110 | 783 | #: plugins/pubsubhubbub/pubsubhubbub.php:114 |
554 | msgid "Enable PubSubHubbub feed publishing." | 784 | msgid "Enable PubSubHubbub feed publishing." |
555 | msgstr "PubSubHubbub ã¸ã®ãƒ•ã‚£ãƒ¼ãƒ‰ã‚’公開ã™ã‚‹ã€‚" | 785 | msgstr "PubSubHubbub ã¸ã®ãƒ•ã‚£ãƒ¼ãƒ‰ã‚’公開ã™ã‚‹ã€‚" |
556 | 786 | ||
557 | #: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68 | 787 | #: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70 |
558 | msgid "For each link, add a QRCode icon." | 788 | msgid "For each link, add a QRCode icon." |
559 | msgstr "ãã‚Œãžã‚Œã®ãƒªãƒ³ã‚¯ã«ã¤ã„ã¦ã€QRコードã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’è¿½åŠ ã™ã‚‹ã€‚" | 789 | msgstr "ãã‚Œãžã‚Œã®ãƒªãƒ³ã‚¯ã«ã¤ã„ã¦ã€QRコードã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’è¿½åŠ ã™ã‚‹ã€‚" |
560 | 790 | ||
@@ -570,724 +800,534 @@ msgstr "" | |||
570 | msgid "Save to wallabag" | 800 | msgid "Save to wallabag" |
571 | msgstr "Wallabag ã«ä¿å˜" | 801 | msgstr "Wallabag ã«ä¿å˜" |
572 | 802 | ||
573 | #: plugins/wallabag/wallabag.php:69 | 803 | #: plugins/wallabag/wallabag.php:71 |
574 | msgid "Wallabag API URL" | 804 | msgid "Wallabag API URL" |
575 | msgstr "Wallabag ã®APIã®URL" | 805 | msgstr "Wallabag ã®APIã®URL" |
576 | 806 | ||
577 | #: plugins/wallabag/wallabag.php:70 | 807 | #: plugins/wallabag/wallabag.php:72 |
578 | msgid "Wallabag API version (1 or 2)" | 808 | msgid "Wallabag API version (1 or 2)" |
579 | msgstr "Wallabag ã®APIã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ (1 ã¾ãŸã¯ 2)" | 809 | msgstr "Wallabag ã®APIã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ (1 ã¾ãŸã¯ 2)" |
580 | 810 | ||
581 | #: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227 | 811 | #: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227 |
582 | #: tests/languages/fr/LanguagesFrTest.php:160 | 812 | #: tests/languages/fr/LanguagesFrTest.php:159 |
583 | #: tests/languages/fr/LanguagesFrTest.php:173 | 813 | #: tests/languages/fr/LanguagesFrTest.php:172 |
584 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81 | ||
585 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81 | ||
586 | msgid "Search" | 814 | msgid "Search" |
587 | msgid_plural "Search" | 815 | msgid_plural "Search" |
588 | msgstr[0] "検索" | 816 | msgstr[0] "検索" |
589 | msgstr[1] "検索" | 817 | msgstr[1] "検索" |
590 | 818 | ||
591 | #: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 | 819 | #~ msgid "The page you are trying to reach does not exist or has been deleted." |
592 | msgid "Sorry, nothing to see here." | 820 | #~ msgstr "ã‚ãªãŸãŒé–‹ã“ã†ã¨ã—ãŸãƒšãƒ¼ã‚¸ã¯å˜åœ¨ã—ãªã„ã‹ã€å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚" |
593 | msgstr "ã™ã¿ã¾ã›ã‚“ãŒã€ã“ã“ã«ã¯ä½•ã‚‚ã‚ã‚Šã¾ã›ã‚“。" | ||
594 | |||
595 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
596 | msgid "URL or leave empty to post a note" | ||
597 | msgstr "URL を入力ã™ã‚‹ã‹ã€ç©ºæ¬„ã«ã™ã‚‹ã¨ãƒŽãƒ¼ãƒˆã‚’投稿ã—ã¾ã™" | ||
598 | |||
599 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
600 | msgid "Current password" | ||
601 | msgstr "ç¾åœ¨ã®ãƒ‘スワード" | ||
602 | |||
603 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
604 | msgid "New password" | ||
605 | msgstr "æ–°ã—ã„パスワード" | ||
606 | |||
607 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
608 | msgid "Change" | ||
609 | msgstr "変更" | ||
610 | |||
611 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
612 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 | ||
613 | msgid "Tag" | ||
614 | msgstr "ã‚¿ã‚°" | ||
615 | |||
616 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
617 | msgid "New name" | ||
618 | msgstr "変更先ã®åå‰" | ||
619 | |||
620 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 | ||
621 | msgid "Case sensitive" | ||
622 | msgstr "大文å—ã¨å°æ–‡å—を区別" | ||
623 | |||
624 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
625 | msgid "Rename" | ||
626 | msgstr "åå‰ã‚’変更" | ||
627 | |||
628 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
629 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 | ||
630 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172 | ||
631 | msgid "Delete" | ||
632 | msgstr "削除" | ||
633 | |||
634 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | ||
635 | msgid "You can also edit tags in the" | ||
636 | msgstr "次ã«å«ã¾ã‚Œã‚‹ã‚¿ã‚°ã‚’編集ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™:" | ||
637 | |||
638 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | ||
639 | msgid "tag list" | ||
640 | msgstr "タグ一覧" | ||
641 | |||
642 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
643 | msgid "title" | ||
644 | msgstr "タイトル" | ||
645 | |||
646 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
647 | msgid "Home link" | ||
648 | msgstr "ホームã®ãƒªãƒ³ã‚¯å…ˆ" | ||
649 | |||
650 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
651 | msgid "Default value" | ||
652 | msgstr "既定ã®å€¤" | ||
653 | |||
654 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
655 | msgid "Theme" | ||
656 | msgstr "テーマ" | ||
657 | |||
658 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 | ||
659 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 | ||
660 | msgid "Language" | ||
661 | msgstr "言語" | ||
662 | |||
663 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116 | ||
664 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
665 | msgid "Timezone" | ||
666 | msgstr "タイムゾーン" | ||
667 | |||
668 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 | ||
669 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 | ||
670 | msgid "Continent" | ||
671 | msgstr "大陸" | ||
672 | |||
673 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 | ||
674 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 | ||
675 | msgid "City" | ||
676 | msgstr "町" | ||
677 | |||
678 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164 | ||
679 | msgid "Disable session cookie hijacking protection" | ||
680 | msgstr "ä¸æ£ãƒã‚°ã‚¤ãƒ³é˜²æ¢ã®ãŸã‚ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¯ãƒƒã‚ーを無効化" | ||
681 | |||
682 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 | ||
683 | msgid "Check this if you get disconnected or if your IP address changes often" | ||
684 | msgstr "" | ||
685 | "ã‚ãªãŸãŒåˆ‡æ–ã•ã‚ŒãŸã‚Šã€IPアドレスãŒé »ç¹ã«å¤‰ã‚る環境下ã§ã‚ã‚‹ãªã‚‰ãƒã‚§ãƒƒã‚¯ã‚’入れ" | ||
686 | "ã¦ãã ã•ã„" | ||
687 | |||
688 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 | ||
689 | msgid "Private links by default" | ||
690 | msgstr "既定ã§ãƒ—ライベートリンク" | ||
691 | |||
692 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184 | ||
693 | msgid "All new links are private by default" | ||
694 | msgstr "ã™ã¹ã¦ã®æ–°è¦ãƒªãƒ³ã‚¯ã‚’プライベートã§ä½œæˆ" | ||
695 | |||
696 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 | ||
697 | msgid "RSS direct links" | ||
698 | msgstr "RSS 直リンク" | ||
699 | |||
700 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200 | ||
701 | msgid "Check this to use direct URL instead of permalink in feeds" | ||
702 | msgstr "フィードã§ãƒ‘ーマリンクã®ä»£ã‚ã‚Šã«ç›´ãƒªãƒ³ã‚¯ã‚’使ã†" | ||
703 | |||
704 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215 | ||
705 | msgid "Hide public links" | ||
706 | msgstr "å…¬é–‹ãƒªãƒ³ã‚¯ã‚’éš ã™" | ||
707 | |||
708 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216 | ||
709 | msgid "Do not show any links if the user is not logged in" | ||
710 | msgstr "ãƒã‚°ã‚¤ãƒ³ã—ã¦ã„ãªã„ユーザーã«ã¯ä½•ã®ãƒªãƒ³ã‚¯ã‚‚表示ã—ãªã„" | ||
711 | |||
712 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231 | ||
713 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 | ||
714 | msgid "Check updates" | ||
715 | msgstr "更新を確èª" | ||
716 | |||
717 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232 | ||
718 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 | ||
719 | msgid "Notify me when a new release is ready" | ||
720 | msgstr "æ–°ã—ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒãƒªãƒªãƒ¼ã‚¹ã•ã‚ŒãŸã¨ãã«é€šçŸ¥" | ||
721 | |||
722 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247 | ||
723 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
724 | msgid "Enable REST API" | ||
725 | msgstr "REST API を有効化" | ||
726 | |||
727 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248 | ||
728 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 | ||
729 | msgid "Allow third party software to use Shaarli such as mobile application" | ||
730 | msgstr "" | ||
731 | "モãƒã‚¤ãƒ«ã‚¢ãƒ—リã¨ã„ã£ãŸã‚µãƒ¼ãƒ‰ãƒ‘ーティーã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã«Shaarliを使用ã™ã‚‹ã“ã¨ã‚’" | ||
732 | "許å¯" | ||
733 | |||
734 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263 | ||
735 | msgid "API secret" | ||
736 | msgstr "API シークレット" | ||
737 | |||
738 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274 | ||
739 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 | ||
740 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | ||
741 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 | ||
742 | msgid "Save" | ||
743 | msgstr "ä¿å˜" | ||
744 | |||
745 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
746 | msgid "The Daily Shaarli" | ||
747 | msgstr "デイリーSharli" | ||
748 | |||
749 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 | ||
750 | msgid "1 RSS entry per day" | ||
751 | msgstr "å„æ—¥1ã¤ãšã¤ã®RSSé …ç›®" | ||
752 | |||
753 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 | ||
754 | msgid "Previous day" | ||
755 | msgstr "å‰æ—¥" | ||
756 | |||
757 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
758 | msgid "All links of one day in a single page." | ||
759 | msgstr "1æ—¥ã«ä½œæˆã•ã‚ŒãŸã™ã¹ã¦ã®ãƒªãƒ³ã‚¯ã§ã™ã€‚" | ||
760 | |||
761 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 | ||
762 | msgid "Next day" | ||
763 | msgstr "翌日" | ||
764 | |||
765 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 | ||
766 | msgid "Created:" | ||
767 | msgstr "作æˆ:" | ||
768 | |||
769 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
770 | msgid "URL" | ||
771 | msgstr "URL" | ||
772 | |||
773 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
774 | msgid "Title" | ||
775 | msgstr "タイトル" | ||
776 | |||
777 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
778 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
779 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 | ||
780 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 | ||
781 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 | ||
782 | msgid "Description" | ||
783 | msgstr "説明" | ||
784 | |||
785 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
786 | msgid "Tags" | ||
787 | msgstr "ã‚¿ã‚°" | ||
788 | |||
789 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 | ||
790 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
791 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 | ||
792 | msgid "Private" | ||
793 | msgstr "プライベート" | ||
794 | |||
795 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 | ||
796 | msgid "Apply Changes" | ||
797 | msgstr "変更をé©ç”¨" | ||
798 | |||
799 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
800 | msgid "Export Database" | ||
801 | msgstr "データベースをエクスãƒãƒ¼ãƒˆ" | ||
802 | |||
803 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
804 | msgid "Selection" | ||
805 | msgstr "é¸æŠžæ¸ˆã¿" | ||
806 | |||
807 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 | ||
808 | msgid "All" | ||
809 | msgstr "ã™ã¹ã¦" | ||
810 | |||
811 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
812 | msgid "Public" | ||
813 | msgstr "公開" | ||
814 | |||
815 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 | ||
816 | msgid "Prepend note permalinks with this Shaarli instance's URL" | ||
817 | msgstr "ã“ã® Shaarli ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®URL ã«ãƒŽãƒ¼ãƒˆã¸ã®ãƒ‘ーマリンクを付ã‘åŠ ãˆã‚‹" | ||
818 | |||
819 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 | ||
820 | msgid "Useful to import bookmarks in a web browser" | ||
821 | msgstr "ウェブブラウザーã®ãƒªãƒ³ã‚¯ã‚’インãƒãƒ¼ãƒˆã™ã‚‹ã®ã«æœ‰åŠ¹ã§ã™" | ||
822 | |||
823 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
824 | msgid "Import Database" | ||
825 | msgstr "データベースをインãƒãƒ¼ãƒˆ" | ||
826 | |||
827 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
828 | msgid "Maximum size allowed:" | ||
829 | msgstr "最大サイズ:" | ||
830 | |||
831 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
832 | msgid "Visibility" | ||
833 | msgstr "å¯è¦–性" | ||
834 | |||
835 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
836 | msgid "Use values from the imported file, default to public" | ||
837 | msgstr "インãƒãƒ¼ãƒˆå…ƒã®ãƒ•ã‚¡ã‚¤ãƒ«ã®å€¤ã‚’使用 (既定ã¯å…¬é–‹ãƒªãƒ³ã‚¯ã¨ãªã‚Šã¾ã™)" | ||
838 | |||
839 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
840 | msgid "Import all bookmarks as private" | ||
841 | msgstr "ã™ã¹ã¦ã®ãƒ–ãƒƒã‚¯ãƒžãƒ¼ã‚¯é …ç›®ã‚’ãƒ—ãƒ©ã‚¤ãƒ™ãƒ¼ãƒˆãƒªãƒ³ã‚¯ã¨ã—ã¦ã‚¤ãƒ³ãƒãƒ¼ãƒˆ" | ||
842 | |||
843 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
844 | msgid "Import all bookmarks as public" | ||
845 | msgstr "ã™ã¹ã¦ã®ãƒ–ãƒƒã‚¯ãƒžãƒ¼ã‚¯é …ç›®ã‚’å…¬é–‹ãƒªãƒ³ã‚¯ã¨ã—ã¦ã‚¤ãƒ³ãƒãƒ¼ãƒˆ" | ||
846 | |||
847 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 | ||
848 | msgid "Overwrite existing bookmarks" | ||
849 | msgstr "æ—¢ã«å˜åœ¨ã—ã¦ã„るブックマークを上書ã" | ||
850 | |||
851 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
852 | msgid "Duplicates based on URL" | ||
853 | msgstr "URL ã«ã‚ˆã‚‹é‡è¤‡" | ||
854 | |||
855 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 | ||
856 | msgid "Add default tags" | ||
857 | msgstr "既定ã®ã‚¿ã‚°ã‚’è¿½åŠ " | ||
858 | |||
859 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | ||
860 | msgid "Install Shaarli" | ||
861 | msgstr "Shaarli をインストール" | ||
862 | |||
863 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 | ||
864 | msgid "It looks like it's the first time you run Shaarli. Please configure it." | ||
865 | msgstr "ã©ã†ã‚„ら Shaarli ã‚’åˆã‚ã¦èµ·å‹•ã—ã¦ã„るよã†ã§ã™ã€‚è¨å®šã—ã¦ãã ã•ã„。" | ||
866 | |||
867 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 | ||
868 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 | ||
869 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 | ||
870 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 | ||
871 | msgid "Username" | ||
872 | msgstr "ユーザーå" | ||
873 | |||
874 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
875 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
876 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 | ||
877 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148 | ||
878 | msgid "Password" | ||
879 | msgstr "パスワード" | ||
880 | |||
881 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 | ||
882 | msgid "Shaarli title" | ||
883 | msgstr "Shaarli ã®ã‚¿ã‚¤ãƒˆãƒ«" | ||
884 | |||
885 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 | ||
886 | msgid "My links" | ||
887 | msgstr "自分ã®ãƒªãƒ³ã‚¯" | ||
888 | |||
889 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 | ||
890 | msgid "Install" | ||
891 | msgstr "インストール" | ||
892 | |||
893 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
894 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 | ||
895 | msgid "shaare" | ||
896 | msgid_plural "shaares" | ||
897 | msgstr[0] "共有" | ||
898 | msgstr[1] "共有" | ||
899 | |||
900 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | ||
901 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 | ||
902 | msgid "private link" | ||
903 | msgid_plural "private links" | ||
904 | msgstr[0] "プライベートリンク" | ||
905 | msgstr[1] "プライベートリンク" | ||
906 | |||
907 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 | ||
908 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 | ||
909 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117 | ||
910 | msgid "Search text" | ||
911 | msgstr "æ–‡å—列ã§æ¤œç´¢" | ||
912 | |||
913 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 | ||
914 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 | ||
915 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124 | ||
916 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
917 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 | ||
918 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
919 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 | ||
920 | msgid "Filter by tag" | ||
921 | msgstr "ã‚¿ã‚°ã«ã‚ˆã£ã¦åˆ†é¡ž" | ||
922 | |||
923 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 | ||
924 | msgid "Nothing found." | ||
925 | msgstr "何も見ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚" | ||
926 | |||
927 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119 | ||
928 | #, php-format | ||
929 | msgid "%s result" | ||
930 | msgid_plural "%s results" | ||
931 | msgstr[0] "%s 件ã®çµæžœ" | ||
932 | msgstr[1] "%s 件ã®çµæžœ" | ||
933 | |||
934 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 | ||
935 | msgid "for" | ||
936 | msgstr "for" | ||
937 | |||
938 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 | ||
939 | msgid "tagged" | ||
940 | msgstr "タグ付ã‘ã•ã‚ŒãŸ" | ||
941 | |||
942 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 | ||
943 | msgid "Remove tag" | ||
944 | msgstr "タグを削除" | ||
945 | |||
946 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 | ||
947 | msgid "with status" | ||
948 | msgstr "with status" | ||
949 | |||
950 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 | ||
951 | msgid "without any tag" | ||
952 | msgstr "ã‚¿ã‚°ãªã—" | ||
953 | |||
954 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174 | ||
955 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
956 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 | ||
957 | msgid "Fold" | ||
958 | msgstr "畳む" | ||
959 | |||
960 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 | ||
961 | msgid "Edited: " | ||
962 | msgstr "編集済ã¿: " | ||
963 | |||
964 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180 | ||
965 | msgid "permalink" | ||
966 | msgstr "パーマリンク" | ||
967 | 821 | ||
968 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 | 822 | #~ msgid "404 Not Found" |
969 | msgid "Add tag" | 823 | #~ msgstr "404 ページãŒå˜åœ¨ã—ã¾ã›ã‚“" |
970 | msgstr "ã‚¿ã‚°ã‚’è¿½åŠ " | ||
971 | |||
972 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 | ||
973 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7 | ||
974 | msgid "Filters" | ||
975 | msgstr "分類" | ||
976 | |||
977 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 | ||
978 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12 | ||
979 | msgid "Only display private links" | ||
980 | msgstr "プライベートリンクã®ã¿ã‚’表示" | ||
981 | |||
982 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
983 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:15 | ||
984 | msgid "Only display public links" | ||
985 | msgstr "公開リンクã®ã¿ã‚’表示" | ||
986 | |||
987 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 | ||
988 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:20 | ||
989 | msgid "Filter untagged links" | ||
990 | msgstr "タグ付ã‘ã•ã‚Œã¦ã„ãªã„リンクã§åˆ†é¡ž" | ||
991 | |||
992 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
993 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 | ||
994 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 | ||
995 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76 | ||
996 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
997 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 | ||
998 | msgid "Fold all" | ||
999 | msgstr "ã™ã¹ã¦ç•³ã‚€" | ||
1000 | |||
1001 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 | ||
1002 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:69 | ||
1003 | msgid "Links per page" | ||
1004 | msgstr "å„ページをリンク" | ||
1005 | |||
1006 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
1007 | msgid "" | ||
1008 | "You have been banned after too many failed login attempts. Try again later." | ||
1009 | msgstr "複数回ã«æ¸¡ã‚‹ãƒã‚°ã‚¤ãƒ³ã¸ã®å¤±æ•—を検出ã—ã¾ã—ãŸã€‚後ã§ã¾ãŸè©¦ã—ã¦ãã ã•ã„。" | ||
1010 | 824 | ||
1011 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | 825 | #~ msgid "Updates file path is not set, can't write updates." |
1012 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 | 826 | #~ msgstr "" |
1013 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151 | 827 | #~ "æ›´æ–°ã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã®ãƒ‘スãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ãŸã‚ã€æ›´æ–°ã‚’書ãè¾¼ã‚ã¾ã›ã‚“。" |
1014 | msgid "Remember me" | ||
1015 | msgstr "パスワードをä¿å˜" | ||
1016 | |||
1017 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
1018 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
1019 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14 | ||
1020 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 | ||
1021 | msgid "by the Shaarli community" | ||
1022 | msgstr "by Shaarli コミュニティ" | ||
1023 | |||
1024 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
1025 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 | ||
1026 | msgid "Documentation" | ||
1027 | msgstr "ドã‚ュメント" | ||
1028 | |||
1029 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
1030 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 | ||
1031 | msgid "Expand" | ||
1032 | msgstr "展開ã™ã‚‹" | ||
1033 | |||
1034 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 | ||
1035 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 | ||
1036 | msgid "Expand all" | ||
1037 | msgstr "ã™ã¹ã¦å±•é–‹ã™ã‚‹" | ||
1038 | |||
1039 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
1040 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 | ||
1041 | msgid "Are you sure you want to delete this link?" | ||
1042 | msgstr "本当ã«ã“ã®ãƒªãƒ³ã‚¯ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" | ||
1043 | |||
1044 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 | ||
1045 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 | ||
1046 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61 | ||
1047 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86 | ||
1048 | msgid "RSS Feed" | ||
1049 | msgstr "RSS フィード" | ||
1050 | |||
1051 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 | ||
1052 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
1053 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66 | ||
1054 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102 | ||
1055 | msgid "Logout" | ||
1056 | msgstr "ãƒã‚°ã‚¢ã‚¦ãƒˆ" | ||
1057 | |||
1058 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
1059 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169 | ||
1060 | msgid "is available" | ||
1061 | msgstr "ãŒåˆ©ç”¨å¯èƒ½" | ||
1062 | |||
1063 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 | ||
1064 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176 | ||
1065 | msgid "Error" | ||
1066 | msgstr "エラー" | ||
1067 | |||
1068 | #: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1069 | msgid "Picture Wall" | ||
1070 | msgstr "ピクãƒãƒ£ãƒ¼ã‚¦ã‚©ãƒ¼ãƒ«" | ||
1071 | |||
1072 | #: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1073 | msgid "pics" | ||
1074 | msgstr "ç”»åƒ" | ||
1075 | |||
1076 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
1077 | msgid "You need to enable Javascript to change plugin loading order." | ||
1078 | msgstr "" | ||
1079 | "プラグインをèªã¿è¾¼ã‚€é †ç•ªã‚’変更ã™ã‚‹ã«ã¯ã€Javascriptを有効ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾" | ||
1080 | "ã™ã€‚" | ||
1081 | 828 | ||
1082 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | 829 | #~ msgid "Unable to write updates in " |
1083 | msgid "Enabled Plugins" | 830 | #~ msgstr "更新を次ã®é …ç›®ã«æ›¸ãè¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸ: " |
1084 | msgstr "有効ãªãƒ—ラグイン" | ||
1085 | |||
1086 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
1087 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 | ||
1088 | msgid "No plugin enabled." | ||
1089 | msgstr "有効ãªãƒ—ラグインã¯ã‚ã‚Šã¾ã›ã‚“。" | ||
1090 | |||
1091 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
1092 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 | ||
1093 | msgid "Disable" | ||
1094 | msgstr "無効化" | ||
1095 | |||
1096 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
1097 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 | ||
1098 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98 | ||
1099 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 | ||
1100 | msgid "Name" | ||
1101 | msgstr "åå‰" | ||
1102 | |||
1103 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
1104 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 | ||
1105 | msgid "Order" | ||
1106 | msgstr "é †åº" | ||
1107 | |||
1108 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 | ||
1109 | msgid "Disabled Plugins" | ||
1110 | msgstr "無効ãªãƒ—ラグイン" | ||
1111 | |||
1112 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 | ||
1113 | msgid "No plugin disabled." | ||
1114 | msgstr "無効ãªãƒ—ラグインã¯ã‚ã‚Šã¾ã›ã‚“。" | ||
1115 | |||
1116 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97 | ||
1117 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 | ||
1118 | msgid "Enable" | ||
1119 | msgstr "有効化" | ||
1120 | |||
1121 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 | ||
1122 | msgid "More plugins available" | ||
1123 | msgstr "ã•ã‚‰ã«åˆ©ç”¨ã§ãるプラグインãŒã‚ã‚Šã¾ã™" | ||
1124 | |||
1125 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 | ||
1126 | msgid "in the documentation" | ||
1127 | msgstr "ドã‚ュメント内" | ||
1128 | |||
1129 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 | ||
1130 | msgid "Plugin configuration" | ||
1131 | msgstr "プラグインè¨å®š" | ||
1132 | |||
1133 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195 | ||
1134 | msgid "No parameter available." | ||
1135 | msgstr "利用å¯èƒ½ãªè¨å®šé …ç›®ã¯ã‚ã‚Šã¾ã›ã‚“。" | ||
1136 | |||
1137 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1138 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1139 | msgid "tags" | ||
1140 | msgstr "ã‚¿ã‚°" | ||
1141 | |||
1142 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
1143 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
1144 | msgid "List all links with those tags" | ||
1145 | msgstr "ã“ã®ã‚¿ã‚°ãŒä»˜ã„ã¦ã„るリンクをリスト化ã™ã‚‹" | ||
1146 | |||
1147 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 | ||
1148 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 | ||
1149 | msgid "Sort by:" | ||
1150 | msgstr "分類:" | ||
1151 | |||
1152 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 | ||
1153 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5 | ||
1154 | msgid "Cloud" | ||
1155 | msgstr "クラウド" | ||
1156 | |||
1157 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6 | ||
1158 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6 | ||
1159 | msgid "Most used" | ||
1160 | msgstr "ã‚‚ã£ã¨ã‚‚使ã‚ã‚ŒãŸ" | ||
1161 | |||
1162 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 | ||
1163 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7 | ||
1164 | msgid "Alphabetical" | ||
1165 | msgstr "ã‚¢ãƒ«ãƒ•ã‚¡ãƒ™ãƒƒãƒˆé †" | ||
1166 | |||
1167 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
1168 | msgid "Settings" | ||
1169 | msgstr "è¨å®š" | ||
1170 | 831 | ||
1171 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | 832 | #~ msgid "I said: NO. You are banned for the moment. Go away." |
1172 | msgid "Change Shaarli settings: title, timezone, etc." | 833 | #~ msgstr "ã‚ãªãŸã¯ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰BANã•ã‚Œã¦ã„ã¾ã™ã€‚" |
1173 | msgstr "Shaarli ã®è¨å®šã‚’変更: タイトルã€ã‚¿ã‚¤ãƒ ゾーンãªã©ã€‚" | ||
1174 | 834 | ||
1175 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 | 835 | #~ msgid "Tag cloud" |
1176 | msgid "Configure your Shaarli" | 836 | #~ msgstr "タグクラウド" |
1177 | msgstr "ã‚ãªãŸã® Shaarli ã‚’è¨å®š" | ||
1178 | 837 | ||
1179 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 | 838 | #~ msgid "Click to try again." |
1180 | msgid "Enable, disable and configure plugins" | 839 | #~ msgstr "クリックã—ã¦å†åº¦è©¦ã—ã¾ã™ã€‚" |
1181 | msgstr "プラグインを有効化ã€ç„¡åŠ¹åŒ–ã€è¨å®šã™ã‚‹" | ||
1182 | 840 | ||
1183 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | 841 | #~ msgid "Description will be rendered with" |
1184 | msgid "Change your password" | 842 | #~ msgstr "説明ã¯æ¬¡ã®æ–¹æ³•ã§æç”»ã•ã‚Œã¾ã™:" |
1185 | msgstr "パスワードを変更" | ||
1186 | 843 | ||
1187 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | 844 | #~ msgid "Markdown syntax documentation" |
1188 | msgid "Rename or delete a tag in all links" | 845 | #~ msgstr "マークダウン形å¼ã®ãƒ‰ã‚ュメント" |
1189 | msgstr "ã™ã¹ã¦ã®ãƒªãƒ³ã‚¯ã®ã‚¿ã‚°ã®åå‰ã‚’変更ã™ã‚‹ã€ã¾ãŸã¯å‰Šé™¤ã™ã‚‹" | ||
1190 | 846 | ||
1191 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | 847 | #~ msgid "Markdown syntax" |
1192 | msgid "" | 848 | #~ msgstr "マークダウン形å¼" |
1193 | "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " | ||
1194 | "delicious...)" | ||
1195 | msgstr "" | ||
1196 | "Netscape HTML å½¢å¼ã®ãƒ–ックマークをインãƒãƒ¼ãƒˆã™ã‚‹ (Firefoxã€Chromeã€Operaã¨" | ||
1197 | "ã„ã£ãŸãƒ–ラウザーãŒå«ã¾ã‚Œã¾ã™)" | ||
1198 | 849 | ||
1199 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | 850 | #~ msgid "" |
1200 | msgid "Import links" | 851 | #~ "Render shaare description with Markdown syntax.<br><strong>Warning</" |
1201 | msgstr "リンクをインãƒãƒ¼ãƒˆ" | 852 | #~ "strong>:\n" |
853 | #~ "If your shaared descriptions contained HTML tags before enabling the " | ||
854 | #~ "markdown plugin,\n" | ||
855 | #~ "enabling it might break your page.\n" | ||
856 | #~ "See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/" | ||
857 | #~ "markdown#html-rendering\">README</a>." | ||
858 | #~ msgstr "" | ||
859 | #~ "リンクã®èª¬æ˜Žã‚’マークダウン形å¼ã§è¡¨ç¤ºã—ã¾ã™ã€‚<br><strong>è¦å‘Š</strong>:\n" | ||
860 | #~ "リンクã®èª¬æ˜Žã«HTMLã‚¿ã‚°ãŒã“ã®ãƒ—ラグインを有効ã«ã™ã‚‹å‰ã«å«ã¾ã‚Œã¦ã„ãŸå ´åˆã€\n" | ||
861 | #~ "æ£å¸¸ã«ãƒšãƒ¼ã‚¸ã‚’表示ã§ããªããªã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。\n" | ||
862 | #~ "詳ã—ã㯠<a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/" | ||
863 | #~ "markdown#html-rendering\">README</a> ã‚’ã”覧ãã ã•ã„。" | ||
1202 | 864 | ||
1203 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | 865 | #~ msgid "Sorry, nothing to see here." |
1204 | msgid "" | 866 | #~ msgstr "ã™ã¿ã¾ã›ã‚“ãŒã€ã“ã“ã«ã¯ä½•ã‚‚ã‚ã‚Šã¾ã›ã‚“。" |
1205 | "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " | ||
1206 | "Opera, delicious...)" | ||
1207 | msgstr "" | ||
1208 | "Netscape HTML å½¢å¼ã®ãƒ–ックマークをエクスãƒãƒ¼ãƒˆã™ã‚‹ (Firefoxã€Chromeã€Operaã¨" | ||
1209 | "ã„ã£ãŸãƒ–ラウザーãŒå«ã¾ã‚Œã¾ã™)" | ||
1210 | 867 | ||
1211 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | 868 | #~ msgid "URL or leave empty to post a note" |
1212 | msgid "Export database" | 869 | #~ msgstr "URL を入力ã™ã‚‹ã‹ã€ç©ºæ¬„ã«ã™ã‚‹ã¨ãƒŽãƒ¼ãƒˆã‚’投稿ã—ã¾ã™" |
1213 | msgstr "リンクをエクスãƒãƒ¼ãƒˆ" | ||
1214 | 870 | ||
1215 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 | 871 | #~ msgid "Current password" |
1216 | msgid "" | 872 | #~ msgstr "ç¾åœ¨ã®ãƒ‘スワード" |
1217 | "Drag one of these button to your bookmarks toolbar or right-click it and " | ||
1218 | "\"Bookmark This Link\"" | ||
1219 | msgstr "" | ||
1220 | "ã“れらã®ãƒœã‚¿ãƒ³ã®ã†ã¡1ã¤ã‚’をブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦" | ||
1221 | "「ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„" | ||
1222 | 873 | ||
1223 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 | 874 | #~ msgid "New password" |
1224 | msgid "then click on the bookmarklet in any page you want to share." | 875 | #~ msgstr "æ–°ã—ã„パスワード" |
1225 | msgstr "共有ã—ãŸã„ページã§ãƒ–ックマークレットをクリックã—ã¦ãã ã•ã„。" | ||
1226 | 876 | ||
1227 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 | 877 | #~ msgid "Change" |
1228 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100 | 878 | #~ msgstr "変更" |
1229 | msgid "" | ||
1230 | "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " | ||
1231 | "Link" | ||
1232 | msgstr "" | ||
1233 | "ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’" | ||
1234 | "ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„" | ||
1235 | 879 | ||
1236 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 | 880 | #~ msgid "Tag" |
1237 | msgid "then click ✚Shaare link button in any page you want to share" | 881 | #~ msgstr "タグ" |
1238 | msgstr "✚リンクを共有 ボタンをクリックã™ã‚‹ã“ã¨ã§ã€ã©ã“ã§ã‚‚リンクを共有ã§ãã¾ã™" | ||
1239 | 882 | ||
1240 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 | 883 | #~ msgid "New name" |
1241 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 | 884 | #~ msgstr "変更先ã®åå‰" |
1242 | msgid "The selected text is too long, it will be truncated." | ||
1243 | msgstr "é¸æŠžã•ã‚ŒãŸæ–‡å—列ã¯é•·ã™ãŽã‚‹ã®ã§ã€ä¸€éƒ¨ãŒåˆ‡ã‚Šæ¨ã¦ã‚‰ã‚Œã¾ã™ã€‚" | ||
1244 | 885 | ||
1245 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 | 886 | #~ msgid "Case sensitive" |
1246 | msgid "Shaare link" | 887 | #~ msgstr "大文å—ã¨å°æ–‡å—を区別" |
1247 | msgstr "共有リンク" | ||
1248 | 888 | ||
1249 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 | 889 | #~ msgid "Rename" |
1250 | msgid "" | 890 | #~ msgstr "åå‰ã‚’変更" |
1251 | "Then click ✚Add Note button anytime to start composing a private Note (text " | ||
1252 | "post) to your Shaarli" | ||
1253 | msgstr "" | ||
1254 | "âœšãƒŽãƒ¼ãƒˆã‚’è¿½åŠ ãƒœã‚¿ãƒ³ã‚’ã‚¯ãƒªãƒƒã‚¯ã™ã‚‹ã“ã¨ã§ã€ã„ã¤ã§ã‚‚プライベートノート(テã‚スト" | ||
1255 | "å½¢å¼)ã‚’Shaarli上ã«ä½œæˆã§ãã¾ã™" | ||
1256 | 891 | ||
1257 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 | 892 | #~ msgid "Delete" |
1258 | msgid "Add Note" | 893 | #~ msgstr "削除" |
1259 | msgstr "ãƒŽãƒ¼ãƒˆã‚’è¿½åŠ " | ||
1260 | 894 | ||
1261 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 | 895 | #~ msgid "You can also edit tags in the" |
1262 | msgid "" | 896 | #~ msgstr "次ã«å«ã¾ã‚Œã‚‹ã‚¿ã‚°ã‚’編集ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™:" |
1263 | "You need to browse your Shaarli over <strong>HTTPS</strong> to use this " | ||
1264 | "functionality." | ||
1265 | msgstr "" | ||
1266 | "ã“ã®æ©Ÿèƒ½ã‚’使用ã™ã‚‹ã«ã¯ã€<strong>HTTPS</strong> 経由ã§Shaarliã«æŽ¥ç¶šã—ã¦ãã ã•" | ||
1267 | "ã„。" | ||
1268 | 897 | ||
1269 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 | 898 | #~ msgid "tag list" |
1270 | msgid "Add to" | 899 | #~ msgstr "タグ一覧" |
1271 | msgstr "次ã«è¿½åŠ :" | ||
1272 | 900 | ||
1273 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145 | 901 | #~ msgid "title" |
1274 | msgid "3rd party" | 902 | #~ msgstr "タイトル" |
1275 | msgstr "サードパーティー" | ||
1276 | 903 | ||
1277 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 | 904 | #~ msgid "Home link" |
1278 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 | 905 | #~ msgstr "ホームã®ãƒªãƒ³ã‚¯å…ˆ" |
1279 | msgid "Plugin" | ||
1280 | msgstr "プラグイン" | ||
1281 | 906 | ||
1282 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 | 907 | #~ msgid "Default value" |
1283 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 | 908 | #~ msgstr "既定ã®å€¤" |
1284 | msgid "plugin" | ||
1285 | msgstr "プラグイン" | ||
1286 | 909 | ||
1287 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 | 910 | #~ msgid "Theme" |
1288 | msgid "" | 911 | #~ msgstr "テーマ" |
1289 | "Drag this link to your bookmarks toolbar, or right-click it and choose " | 912 | |
1290 | "Bookmark This Link" | 913 | #~ msgid "Language" |
1291 | msgstr "" | 914 | #~ msgstr "言語" |
1292 | "ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’" | 915 | |
1293 | "ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„" | 916 | #~ msgid "Timezone" |
917 | #~ msgstr "タイムゾーン" | ||
918 | |||
919 | #~ msgid "Continent" | ||
920 | #~ msgstr "大陸" | ||
921 | |||
922 | #~ msgid "City" | ||
923 | #~ msgstr "町" | ||
924 | |||
925 | #~ msgid "Disable session cookie hijacking protection" | ||
926 | #~ msgstr "ä¸æ£ãƒã‚°ã‚¤ãƒ³é˜²æ¢ã®ãŸã‚ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¯ãƒƒã‚ーを無効化" | ||
927 | |||
928 | #~ msgid "" | ||
929 | #~ "Check this if you get disconnected or if your IP address changes often" | ||
930 | #~ msgstr "" | ||
931 | #~ "ã‚ãªãŸãŒåˆ‡æ–ã•ã‚ŒãŸã‚Šã€IPアドレスãŒé »ç¹ã«å¤‰ã‚る環境下ã§ã‚ã‚‹ãªã‚‰ãƒã‚§ãƒƒã‚¯ã‚’å…¥" | ||
932 | #~ "ã‚Œã¦ãã ã•ã„" | ||
933 | |||
934 | #~ msgid "Private links by default" | ||
935 | #~ msgstr "既定ã§ãƒ—ライベートリンク" | ||
936 | |||
937 | #~ msgid "All new links are private by default" | ||
938 | #~ msgstr "ã™ã¹ã¦ã®æ–°è¦ãƒªãƒ³ã‚¯ã‚’プライベートã§ä½œæˆ" | ||
939 | |||
940 | #~ msgid "RSS direct links" | ||
941 | #~ msgstr "RSS 直リンク" | ||
942 | |||
943 | #~ msgid "Check this to use direct URL instead of permalink in feeds" | ||
944 | #~ msgstr "フィードã§ãƒ‘ーマリンクã®ä»£ã‚ã‚Šã«ç›´ãƒªãƒ³ã‚¯ã‚’使ã†" | ||
945 | |||
946 | #~ msgid "Hide public links" | ||
947 | #~ msgstr "å…¬é–‹ãƒªãƒ³ã‚¯ã‚’éš ã™" | ||
948 | |||
949 | #~ msgid "Do not show any links if the user is not logged in" | ||
950 | #~ msgstr "ãƒã‚°ã‚¤ãƒ³ã—ã¦ã„ãªã„ユーザーã«ã¯ä½•ã®ãƒªãƒ³ã‚¯ã‚‚表示ã—ãªã„" | ||
951 | |||
952 | #~ msgid "Check updates" | ||
953 | #~ msgstr "更新を確èª" | ||
954 | |||
955 | #~ msgid "Notify me when a new release is ready" | ||
956 | #~ msgstr "æ–°ã—ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒãƒªãƒªãƒ¼ã‚¹ã•ã‚ŒãŸã¨ãã«é€šçŸ¥" | ||
957 | |||
958 | #~ msgid "Enable REST API" | ||
959 | #~ msgstr "REST API を有効化" | ||
960 | |||
961 | #~ msgid "Allow third party software to use Shaarli such as mobile application" | ||
962 | #~ msgstr "" | ||
963 | #~ "モãƒã‚¤ãƒ«ã‚¢ãƒ—リã¨ã„ã£ãŸã‚µãƒ¼ãƒ‰ãƒ‘ーティーã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã«Shaarliを使用ã™ã‚‹ã“" | ||
964 | #~ "ã¨ã‚’許å¯" | ||
965 | |||
966 | #~ msgid "API secret" | ||
967 | #~ msgstr "API シークレット" | ||
968 | |||
969 | #~ msgid "Save" | ||
970 | #~ msgstr "ä¿å˜" | ||
971 | |||
972 | #~ msgid "The Daily Shaarli" | ||
973 | #~ msgstr "デイリーSharli" | ||
974 | |||
975 | #~ msgid "1 RSS entry per day" | ||
976 | #~ msgstr "å„æ—¥1ã¤ãšã¤ã®RSSé …ç›®" | ||
977 | |||
978 | #~ msgid "Previous day" | ||
979 | #~ msgstr "å‰æ—¥" | ||
980 | |||
981 | #~ msgid "All links of one day in a single page." | ||
982 | #~ msgstr "1æ—¥ã«ä½œæˆã•ã‚ŒãŸã™ã¹ã¦ã®ãƒªãƒ³ã‚¯ã§ã™ã€‚" | ||
983 | |||
984 | #~ msgid "Next day" | ||
985 | #~ msgstr "翌日" | ||
986 | |||
987 | #~ msgid "Created:" | ||
988 | #~ msgstr "作æˆ:" | ||
989 | |||
990 | #~ msgid "URL" | ||
991 | #~ msgstr "URL" | ||
992 | |||
993 | #~ msgid "Title" | ||
994 | #~ msgstr "タイトル" | ||
995 | |||
996 | #~ msgid "Description" | ||
997 | #~ msgstr "説明" | ||
998 | |||
999 | #~ msgid "Tags" | ||
1000 | #~ msgstr "ã‚¿ã‚°" | ||
1001 | |||
1002 | #~ msgid "Private" | ||
1003 | #~ msgstr "プライベート" | ||
1004 | |||
1005 | #~ msgid "Apply Changes" | ||
1006 | #~ msgstr "変更をé©ç”¨" | ||
1007 | |||
1008 | #~ msgid "Export Database" | ||
1009 | #~ msgstr "データベースをエクスãƒãƒ¼ãƒˆ" | ||
1010 | |||
1011 | #~ msgid "Selection" | ||
1012 | #~ msgstr "é¸æŠžæ¸ˆã¿" | ||
1013 | |||
1014 | #~ msgid "All" | ||
1015 | #~ msgstr "ã™ã¹ã¦" | ||
1016 | |||
1017 | #~ msgid "Public" | ||
1018 | #~ msgstr "公開" | ||
1019 | |||
1020 | #~ msgid "Prepend note permalinks with this Shaarli instance's URL" | ||
1021 | #~ msgstr "" | ||
1022 | #~ "ã“ã® Shaarli ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®URL ã«ãƒŽãƒ¼ãƒˆã¸ã®ãƒ‘ーマリンクを付ã‘åŠ ãˆã‚‹" | ||
1023 | |||
1024 | #~ msgid "Useful to import bookmarks in a web browser" | ||
1025 | #~ msgstr "ウェブブラウザーã®ãƒªãƒ³ã‚¯ã‚’インãƒãƒ¼ãƒˆã™ã‚‹ã®ã«æœ‰åŠ¹ã§ã™" | ||
1026 | |||
1027 | #~ msgid "Import Database" | ||
1028 | #~ msgstr "データベースをインãƒãƒ¼ãƒˆ" | ||
1029 | |||
1030 | #~ msgid "Maximum size allowed:" | ||
1031 | #~ msgstr "最大サイズ:" | ||
1032 | |||
1033 | #~ msgid "Visibility" | ||
1034 | #~ msgstr "å¯è¦–性" | ||
1035 | |||
1036 | #~ msgid "Use values from the imported file, default to public" | ||
1037 | #~ msgstr "インãƒãƒ¼ãƒˆå…ƒã®ãƒ•ã‚¡ã‚¤ãƒ«ã®å€¤ã‚’使用 (既定ã¯å…¬é–‹ãƒªãƒ³ã‚¯ã¨ãªã‚Šã¾ã™)" | ||
1038 | |||
1039 | #~ msgid "Import all bookmarks as public" | ||
1040 | #~ msgstr "ã™ã¹ã¦ã®ãƒ–ãƒƒã‚¯ãƒžãƒ¼ã‚¯é …ç›®ã‚’å…¬é–‹ãƒªãƒ³ã‚¯ã¨ã—ã¦ã‚¤ãƒ³ãƒãƒ¼ãƒˆ" | ||
1041 | |||
1042 | #~ msgid "Overwrite existing bookmarks" | ||
1043 | #~ msgstr "æ—¢ã«å˜åœ¨ã—ã¦ã„るブックマークを上書ã" | ||
1044 | |||
1045 | #~ msgid "Duplicates based on URL" | ||
1046 | #~ msgstr "URL ã«ã‚ˆã‚‹é‡è¤‡" | ||
1047 | |||
1048 | #~ msgid "Add default tags" | ||
1049 | #~ msgstr "既定ã®ã‚¿ã‚°ã‚’è¿½åŠ " | ||
1050 | |||
1051 | #~ msgid "Install Shaarli" | ||
1052 | #~ msgstr "Shaarli をインストール" | ||
1053 | |||
1054 | #~ msgid "" | ||
1055 | #~ "It looks like it's the first time you run Shaarli. Please configure it." | ||
1056 | #~ msgstr "ã©ã†ã‚„ら Shaarli ã‚’åˆã‚ã¦èµ·å‹•ã—ã¦ã„るよã†ã§ã™ã€‚è¨å®šã—ã¦ãã ã•ã„。" | ||
1057 | |||
1058 | #~ msgid "Username" | ||
1059 | #~ msgstr "ユーザーå" | ||
1060 | |||
1061 | #~ msgid "Password" | ||
1062 | #~ msgstr "パスワード" | ||
1063 | |||
1064 | #~ msgid "Shaarli title" | ||
1065 | #~ msgstr "Shaarli ã®ã‚¿ã‚¤ãƒˆãƒ«" | ||
1066 | |||
1067 | #~ msgid "My links" | ||
1068 | #~ msgstr "自分ã®ãƒªãƒ³ã‚¯" | ||
1069 | |||
1070 | #~ msgid "Install" | ||
1071 | #~ msgstr "インストール" | ||
1072 | |||
1073 | #~ msgid "shaare" | ||
1074 | #~ msgid_plural "shaares" | ||
1075 | #~ msgstr[0] "共有" | ||
1076 | #~ msgstr[1] "共有" | ||
1077 | |||
1078 | #~ msgid "private link" | ||
1079 | #~ msgid_plural "private links" | ||
1080 | #~ msgstr[0] "プライベートリンク" | ||
1081 | #~ msgstr[1] "プライベートリンク" | ||
1082 | |||
1083 | #~ msgid "Search text" | ||
1084 | #~ msgstr "æ–‡å—列ã§æ¤œç´¢" | ||
1085 | |||
1086 | #~ msgid "Filter by tag" | ||
1087 | #~ msgstr "ã‚¿ã‚°ã«ã‚ˆã£ã¦åˆ†é¡ž" | ||
1088 | |||
1089 | #~ msgid "Nothing found." | ||
1090 | #~ msgstr "何も見ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚" | ||
1091 | |||
1092 | #~ msgid "%s result" | ||
1093 | #~ msgid_plural "%s results" | ||
1094 | #~ msgstr[0] "%s 件ã®çµæžœ" | ||
1095 | #~ msgstr[1] "%s 件ã®çµæžœ" | ||
1096 | |||
1097 | #~ msgid "for" | ||
1098 | #~ msgstr "for" | ||
1099 | |||
1100 | #~ msgid "tagged" | ||
1101 | #~ msgstr "タグ付ã‘ã•ã‚ŒãŸ" | ||
1102 | |||
1103 | #~ msgid "Remove tag" | ||
1104 | #~ msgstr "タグを削除" | ||
1105 | |||
1106 | #~ msgid "with status" | ||
1107 | #~ msgstr "with status" | ||
1108 | |||
1109 | #~ msgid "without any tag" | ||
1110 | #~ msgstr "ã‚¿ã‚°ãªã—" | ||
1111 | |||
1112 | #~ msgid "Fold" | ||
1113 | #~ msgstr "畳む" | ||
1114 | |||
1115 | #~ msgid "Edited: " | ||
1116 | #~ msgstr "編集済ã¿: " | ||
1117 | |||
1118 | #~ msgid "permalink" | ||
1119 | #~ msgstr "パーマリンク" | ||
1120 | |||
1121 | #~ msgid "Add tag" | ||
1122 | #~ msgstr "ã‚¿ã‚°ã‚’è¿½åŠ " | ||
1123 | |||
1124 | #~ msgid "Filters" | ||
1125 | #~ msgstr "分類" | ||
1126 | |||
1127 | #~ msgid "Only display private links" | ||
1128 | #~ msgstr "プライベートリンクã®ã¿ã‚’表示" | ||
1129 | |||
1130 | #~ msgid "Only display public links" | ||
1131 | #~ msgstr "公開リンクã®ã¿ã‚’表示" | ||
1132 | |||
1133 | #~ msgid "Filter untagged links" | ||
1134 | #~ msgstr "タグ付ã‘ã•ã‚Œã¦ã„ãªã„リンクã§åˆ†é¡ž" | ||
1135 | |||
1136 | #~ msgid "Fold all" | ||
1137 | #~ msgstr "ã™ã¹ã¦ç•³ã‚€" | ||
1138 | |||
1139 | #~ msgid "Links per page" | ||
1140 | #~ msgstr "å„ページをリンク" | ||
1141 | |||
1142 | #~ msgid "Remember me" | ||
1143 | #~ msgstr "パスワードをä¿å˜" | ||
1144 | |||
1145 | #~ msgid "by the Shaarli community" | ||
1146 | #~ msgstr "by Shaarli コミュニティ" | ||
1147 | |||
1148 | #~ msgid "Documentation" | ||
1149 | #~ msgstr "ドã‚ュメント" | ||
1150 | |||
1151 | #~ msgid "Expand" | ||
1152 | #~ msgstr "展開ã™ã‚‹" | ||
1153 | |||
1154 | #~ msgid "Expand all" | ||
1155 | #~ msgstr "ã™ã¹ã¦å±•é–‹ã™ã‚‹" | ||
1156 | |||
1157 | #~ msgid "Are you sure you want to delete this link?" | ||
1158 | #~ msgstr "本当ã«ã“ã®ãƒªãƒ³ã‚¯ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" | ||
1159 | |||
1160 | #~ msgid "RSS Feed" | ||
1161 | #~ msgstr "RSS フィード" | ||
1162 | |||
1163 | #~ msgid "Logout" | ||
1164 | #~ msgstr "ãƒã‚°ã‚¢ã‚¦ãƒˆ" | ||
1165 | |||
1166 | #~ msgid "is available" | ||
1167 | #~ msgstr "ãŒåˆ©ç”¨å¯èƒ½" | ||
1168 | |||
1169 | #~ msgid "Error" | ||
1170 | #~ msgstr "エラー" | ||
1171 | |||
1172 | #~ msgid "Picture Wall" | ||
1173 | #~ msgstr "ピクãƒãƒ£ãƒ¼ã‚¦ã‚©ãƒ¼ãƒ«" | ||
1174 | |||
1175 | #~ msgid "pics" | ||
1176 | #~ msgstr "ç”»åƒ" | ||
1177 | |||
1178 | #~ msgid "You need to enable Javascript to change plugin loading order." | ||
1179 | #~ msgstr "" | ||
1180 | #~ "プラグインをèªã¿è¾¼ã‚€é †ç•ªã‚’変更ã™ã‚‹ã«ã¯ã€Javascriptを有効ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾" | ||
1181 | #~ "ã™ã€‚" | ||
1182 | |||
1183 | #~ msgid "Enabled Plugins" | ||
1184 | #~ msgstr "有効ãªãƒ—ラグイン" | ||
1185 | |||
1186 | #~ msgid "No plugin enabled." | ||
1187 | #~ msgstr "有効ãªãƒ—ラグインã¯ã‚ã‚Šã¾ã›ã‚“。" | ||
1188 | |||
1189 | #~ msgid "Disable" | ||
1190 | #~ msgstr "無効化" | ||
1191 | |||
1192 | #~ msgid "Name" | ||
1193 | #~ msgstr "åå‰" | ||
1194 | |||
1195 | #~ msgid "Order" | ||
1196 | #~ msgstr "é †åº" | ||
1197 | |||
1198 | #~ msgid "Disabled Plugins" | ||
1199 | #~ msgstr "無効ãªãƒ—ラグイン" | ||
1200 | |||
1201 | #~ msgid "No plugin disabled." | ||
1202 | #~ msgstr "無効ãªãƒ—ラグインã¯ã‚ã‚Šã¾ã›ã‚“。" | ||
1203 | |||
1204 | #~ msgid "Enable" | ||
1205 | #~ msgstr "有効化" | ||
1206 | |||
1207 | #~ msgid "More plugins available" | ||
1208 | #~ msgstr "ã•ã‚‰ã«åˆ©ç”¨ã§ãるプラグインãŒã‚ã‚Šã¾ã™" | ||
1209 | |||
1210 | #~ msgid "in the documentation" | ||
1211 | #~ msgstr "ドã‚ュメント内" | ||
1212 | |||
1213 | #~ msgid "No parameter available." | ||
1214 | #~ msgstr "利用å¯èƒ½ãªè¨å®šé …ç›®ã¯ã‚ã‚Šã¾ã›ã‚“。" | ||
1215 | |||
1216 | #~ msgid "tags" | ||
1217 | #~ msgstr "ã‚¿ã‚°" | ||
1218 | |||
1219 | #~ msgid "List all links with those tags" | ||
1220 | #~ msgstr "ã“ã®ã‚¿ã‚°ãŒä»˜ã„ã¦ã„るリンクをリスト化ã™ã‚‹" | ||
1221 | |||
1222 | #~ msgid "Sort by:" | ||
1223 | #~ msgstr "分類:" | ||
1224 | |||
1225 | #~ msgid "Cloud" | ||
1226 | #~ msgstr "クラウド" | ||
1227 | |||
1228 | #~ msgid "Most used" | ||
1229 | #~ msgstr "ã‚‚ã£ã¨ã‚‚使ã‚ã‚ŒãŸ" | ||
1230 | |||
1231 | #~ msgid "Alphabetical" | ||
1232 | #~ msgstr "ã‚¢ãƒ«ãƒ•ã‚¡ãƒ™ãƒƒãƒˆé †" | ||
1233 | |||
1234 | #~ msgid "Settings" | ||
1235 | #~ msgstr "è¨å®š" | ||
1236 | |||
1237 | #~ msgid "Change Shaarli settings: title, timezone, etc." | ||
1238 | #~ msgstr "Shaarli ã®è¨å®šã‚’変更: タイトルã€ã‚¿ã‚¤ãƒ ゾーンãªã©ã€‚" | ||
1239 | |||
1240 | #~ msgid "Configure your Shaarli" | ||
1241 | #~ msgstr "ã‚ãªãŸã® Shaarli ã‚’è¨å®š" | ||
1242 | |||
1243 | #~ msgid "Enable, disable and configure plugins" | ||
1244 | #~ msgstr "プラグインを有効化ã€ç„¡åŠ¹åŒ–ã€è¨å®šã™ã‚‹" | ||
1245 | |||
1246 | #~ msgid "Change your password" | ||
1247 | #~ msgstr "パスワードを変更" | ||
1248 | |||
1249 | #~ msgid "Rename or delete a tag in all links" | ||
1250 | #~ msgstr "ã™ã¹ã¦ã®ãƒªãƒ³ã‚¯ã®ã‚¿ã‚°ã®åå‰ã‚’変更ã™ã‚‹ã€ã¾ãŸã¯å‰Šé™¤ã™ã‚‹" | ||
1251 | |||
1252 | #~ msgid "" | ||
1253 | #~ "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " | ||
1254 | #~ "delicious...)" | ||
1255 | #~ msgstr "" | ||
1256 | #~ "Netscape HTML å½¢å¼ã®ãƒ–ックマークをインãƒãƒ¼ãƒˆã™ã‚‹ (Firefoxã€Chromeã€Operaã¨" | ||
1257 | #~ "ã„ã£ãŸãƒ–ラウザーãŒå«ã¾ã‚Œã¾ã™)" | ||
1258 | |||
1259 | #~ msgid "Import links" | ||
1260 | #~ msgstr "リンクをインãƒãƒ¼ãƒˆ" | ||
1261 | |||
1262 | #~ msgid "" | ||
1263 | #~ "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " | ||
1264 | #~ "Opera, delicious...)" | ||
1265 | #~ msgstr "" | ||
1266 | #~ "Netscape HTML å½¢å¼ã®ãƒ–ックマークをエクスãƒãƒ¼ãƒˆã™ã‚‹ (Firefoxã€Chromeã€Opera" | ||
1267 | #~ "ã¨ã„ã£ãŸãƒ–ラウザーãŒå«ã¾ã‚Œã¾ã™)" | ||
1268 | |||
1269 | #~ msgid "Export database" | ||
1270 | #~ msgstr "リンクをエクスãƒãƒ¼ãƒˆ" | ||
1271 | |||
1272 | #~ msgid "" | ||
1273 | #~ "Drag one of these button to your bookmarks toolbar or right-click it and " | ||
1274 | #~ "\"Bookmark This Link\"" | ||
1275 | #~ msgstr "" | ||
1276 | #~ "ã“れらã®ãƒœã‚¿ãƒ³ã®ã†ã¡1ã¤ã‚’をブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—" | ||
1277 | #~ "ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„" | ||
1278 | |||
1279 | #~ msgid "then click on the bookmarklet in any page you want to share." | ||
1280 | #~ msgstr "共有ã—ãŸã„ページã§ãƒ–ックマークレットをクリックã—ã¦ãã ã•ã„。" | ||
1281 | |||
1282 | #~ msgid "" | ||
1283 | #~ "Drag this link to your bookmarks toolbar or right-click it and Bookmark " | ||
1284 | #~ "This Link" | ||
1285 | #~ msgstr "" | ||
1286 | #~ "ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’" | ||
1287 | #~ "ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„" | ||
1288 | |||
1289 | #~ msgid "then click ✚Shaare link button in any page you want to share" | ||
1290 | #~ msgstr "" | ||
1291 | #~ "✚リンクを共有 ボタンをクリックã™ã‚‹ã“ã¨ã§ã€ã©ã“ã§ã‚‚リンクを共有ã§ãã¾ã™" | ||
1292 | |||
1293 | #~ msgid "The selected text is too long, it will be truncated." | ||
1294 | #~ msgstr "é¸æŠžã•ã‚ŒãŸæ–‡å—列ã¯é•·ã™ãŽã‚‹ã®ã§ã€ä¸€éƒ¨ãŒåˆ‡ã‚Šæ¨ã¦ã‚‰ã‚Œã¾ã™ã€‚" | ||
1295 | |||
1296 | #~ msgid "Shaare link" | ||
1297 | #~ msgstr "共有リンク" | ||
1298 | |||
1299 | #~ msgid "" | ||
1300 | #~ "Then click ✚Add Note button anytime to start composing a private Note " | ||
1301 | #~ "(text post) to your Shaarli" | ||
1302 | #~ msgstr "" | ||
1303 | #~ "âœšãƒŽãƒ¼ãƒˆã‚’è¿½åŠ ãƒœã‚¿ãƒ³ã‚’ã‚¯ãƒªãƒƒã‚¯ã™ã‚‹ã“ã¨ã§ã€ã„ã¤ã§ã‚‚プライベートノート(テã‚" | ||
1304 | #~ "スト形å¼)ã‚’Shaarli上ã«ä½œæˆã§ãã¾ã™" | ||
1305 | |||
1306 | #~ msgid "Add Note" | ||
1307 | #~ msgstr "ãƒŽãƒ¼ãƒˆã‚’è¿½åŠ " | ||
1308 | |||
1309 | #~ msgid "" | ||
1310 | #~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this " | ||
1311 | #~ "functionality." | ||
1312 | #~ msgstr "" | ||
1313 | #~ "ã“ã®æ©Ÿèƒ½ã‚’使用ã™ã‚‹ã«ã¯ã€<strong>HTTPS</strong> 経由ã§Shaarliã«æŽ¥ç¶šã—ã¦ãã " | ||
1314 | #~ "ã•ã„。" | ||
1315 | |||
1316 | #~ msgid "Add to" | ||
1317 | #~ msgstr "次ã«è¿½åŠ :" | ||
1318 | |||
1319 | #~ msgid "3rd party" | ||
1320 | #~ msgstr "サードパーティー" | ||
1321 | |||
1322 | #~ msgid "Plugin" | ||
1323 | #~ msgstr "プラグイン" | ||
1324 | |||
1325 | #~ msgid "plugin" | ||
1326 | #~ msgstr "プラグイン" | ||
1327 | |||
1328 | #~ msgid "" | ||
1329 | #~ "Drag this link to your bookmarks toolbar, or right-click it and choose " | ||
1330 | #~ "Bookmark This Link" | ||
1331 | #~ msgstr "" | ||
1332 | #~ "ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’" | ||
1333 | #~ "ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„" | ||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Shaarli - The personal, minimalist, super-fast, database free, bookmarking service. | 4 | * Shaarli - The personal, minimalist, super-fast, database free, bookmarking service. |
4 | * | 5 | * |
@@ -25,9 +26,12 @@ require_once 'application/Utils.php'; | |||
25 | 26 | ||
26 | require_once __DIR__ . '/init.php'; | 27 | require_once __DIR__ . '/init.php'; |
27 | 28 | ||
29 | use Katzgrau\KLogger\Logger; | ||
30 | use Psr\Log\LogLevel; | ||
28 | use Shaarli\Config\ConfigManager; | 31 | use Shaarli\Config\ConfigManager; |
29 | use Shaarli\Container\ContainerBuilder; | 32 | use Shaarli\Container\ContainerBuilder; |
30 | use Shaarli\Languages; | 33 | use Shaarli\Languages; |
34 | use Shaarli\Security\BanManager; | ||
31 | use Shaarli\Security\CookieManager; | 35 | use Shaarli\Security\CookieManager; |
32 | use Shaarli\Security\LoginManager; | 36 | use Shaarli\Security\LoginManager; |
33 | use Shaarli\Security\SessionManager; | 37 | use Shaarli\Security\SessionManager; |
@@ -48,10 +52,22 @@ if ($conf->get('dev.debug', false)) { | |||
48 | }); | 52 | }); |
49 | } | 53 | } |
50 | 54 | ||
55 | $logger = new Logger( | ||
56 | dirname($conf->get('resource.log')), | ||
57 | !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, | ||
58 | ['filename' => basename($conf->get('resource.log'))] | ||
59 | ); | ||
51 | $sessionManager = new SessionManager($_SESSION, $conf, session_save_path()); | 60 | $sessionManager = new SessionManager($_SESSION, $conf, session_save_path()); |
52 | $sessionManager->initialize(); | 61 | $sessionManager->initialize(); |
53 | $cookieManager = new CookieManager($_COOKIE); | 62 | $cookieManager = new CookieManager($_COOKIE); |
54 | $loginManager = new LoginManager($conf, $sessionManager, $cookieManager); | 63 | $banManager = new BanManager( |
64 | $conf->get('security.trusted_proxies', []), | ||
65 | $conf->get('security.ban_after'), | ||
66 | $conf->get('security.ban_duration'), | ||
67 | $conf->get('resource.ban_file', 'data/ipbans.php'), | ||
68 | $logger | ||
69 | ); | ||
70 | $loginManager = new LoginManager($conf, $sessionManager, $cookieManager, $banManager, $logger); | ||
55 | $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); | 71 | $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); |
56 | 72 | ||
57 | // Sniff browser language and set date format accordingly. | 73 | // Sniff browser language and set date format accordingly. |
@@ -62,16 +78,16 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { | |||
62 | new Languages(setlocale(LC_MESSAGES, 0), $conf); | 78 | new Languages(setlocale(LC_MESSAGES, 0), $conf); |
63 | 79 | ||
64 | $conf->setEmpty('general.timezone', date_default_timezone_get()); | 80 | $conf->setEmpty('general.timezone', date_default_timezone_get()); |
65 | $conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER))); | 81 | $conf->setEmpty('general.title', t('Shared bookmarks on ') . escape(index_url($_SERVER))); |
66 | 82 | ||
67 | RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory | 83 | RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl') . '/' . $conf->get('resource.theme') . '/'; // template directory |
68 | RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory | 84 | RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory |
69 | 85 | ||
70 | date_default_timezone_set($conf->get('general.timezone', 'UTC')); | 86 | date_default_timezone_set($conf->get('general.timezone', 'UTC')); |
71 | 87 | ||
72 | $loginManager->checkLoginState(client_ip_id($_SERVER)); | 88 | $loginManager->checkLoginState(client_ip_id($_SERVER)); |
73 | 89 | ||
74 | $containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager); | 90 | $containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger); |
75 | $container = $containerBuilder->build(); | 91 | $container = $containerBuilder->build(); |
76 | $app = new App($container); | 92 | $app = new App($container); |
77 | 93 | ||
@@ -110,13 +126,16 @@ $app->group('/admin', function () { | |||
110 | $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save'); | 126 | $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save'); |
111 | $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index'); | 127 | $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index'); |
112 | $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save'); | 128 | $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save'); |
113 | $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare'); | 129 | $this->post('/tags/change-separator', '\Shaarli\Front\Controller\Admin\ManageTagController:changeSeparator'); |
114 | $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm'); | 130 | $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare'); |
115 | $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm'); | 131 | $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm'); |
116 | $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); | 132 | $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm'); |
117 | $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); | 133 | $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate'); |
118 | $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); | 134 | $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms'); |
119 | $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); | 135 | $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save'); |
136 | $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark'); | ||
137 | $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility'); | ||
138 | $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark'); | ||
120 | $this->patch( | 139 | $this->patch( |
121 | '/shaare/{id:[0-9]+}/update-thumbnail', | 140 | '/shaare/{id:[0-9]+}/update-thumbnail', |
122 | '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate' | 141 | '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate' |
@@ -128,8 +147,10 @@ $app->group('/admin', function () { | |||
128 | $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index'); | 147 | $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index'); |
129 | $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save'); | 148 | $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save'); |
130 | $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken'); | 149 | $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken'); |
150 | $this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index'); | ||
151 | $this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache'); | ||
131 | $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index'); | 152 | $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index'); |
132 | 153 | $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle'); | |
133 | $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); | 154 | $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); |
134 | })->add('\Shaarli\Front\ShaarliAdminMiddleware'); | 155 | })->add('\Shaarli\Front\ShaarliAdminMiddleware'); |
135 | 156 | ||
@@ -151,6 +172,12 @@ $app->group('/api/v1', function () { | |||
151 | $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); | 172 | $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); |
152 | })->add('\Shaarli\Api\ApiMiddleware'); | 173 | })->add('\Shaarli\Api\ApiMiddleware'); |
153 | 174 | ||
154 | $response = $app->run(true); | 175 | try { |
155 | 176 | $response = $app->run(true); | |
156 | $app->respond($response); | 177 | $app->respond($response); |
178 | } catch (Throwable $e) { | ||
179 | die(nl2br( | ||
180 | 'An unexpected error happened, and the error template could not be displayed.' . PHP_EOL . PHP_EOL . | ||
181 | exception2text($e) | ||
182 | )); | ||
183 | } | ||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | require_once __DIR__ . '/vendor/autoload.php'; | 3 | require_once __DIR__ . '/vendor/autoload.php'; |
4 | 4 | ||
5 | use Shaarli\ApplicationUtils; | 5 | use Shaarli\Helper\ApplicationUtils; |
6 | use Shaarli\Security\SessionManager; | 6 | use Shaarli\Security\SessionManager; |
7 | 7 | ||
8 | // Set 'UTC' as the default timezone if it is not defined in php.ini | 8 | // Set 'UTC' as the default timezone if it is not defined in php.ini |
@@ -60,6 +60,7 @@ ini_set('session.use_only_cookies', 1); | |||
60 | ini_set('session.use_trans_sid', false); | 60 | ini_set('session.use_trans_sid', false); |
61 | 61 | ||
62 | define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); | 62 | define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); |
63 | define('SHAARLI_MUTEX_FILE', __FILE__); | ||
63 | 64 | ||
64 | session_name('shaarli'); | 65 | session_name('shaarli'); |
65 | // Start session if needed (Some server auto-start sessions). | 66 | // Start session if needed (Some server auto-start sessions). |
diff --git a/package.json b/package.json index 8a24512a..b879b223 100644 --- a/package.json +++ b/package.json | |||
@@ -7,6 +7,7 @@ | |||
7 | "awesomplete": "^1.1.2", | 7 | "awesomplete": "^1.1.2", |
8 | "blazy": "^1.8.2", | 8 | "blazy": "^1.8.2", |
9 | "fork-awesome": "^1.1.7", | 9 | "fork-awesome": "^1.1.7", |
10 | "he": "^1.2.0", | ||
10 | "pure-extras": "^1.0.0", | 11 | "pure-extras": "^1.0.0", |
11 | "purecss": "^1.0.0" | 12 | "purecss": "^1.0.0" |
12 | }, | 13 | }, |
@@ -5,13 +5,18 @@ | |||
5 | <file>index.php</file> | 5 | <file>index.php</file> |
6 | <file>application</file> | 6 | <file>application</file> |
7 | <file>plugins</file> | 7 | <file>plugins</file> |
8 | <file>tests</file> | 8 | <!-- <file>tests</file>--> |
9 | 9 | ||
10 | <exclude-pattern>*/*.css</exclude-pattern> | 10 | <exclude-pattern>*/*.css</exclude-pattern> |
11 | <exclude-pattern>*/*.js</exclude-pattern> | 11 | <exclude-pattern>*/*.js</exclude-pattern> |
12 | 12 | ||
13 | <arg name="colors"/> | 13 | <arg name="colors"/> |
14 | 14 | ||
15 | <rule ref="PSR1"/> | 15 | <rule ref="PSR12"/> |
16 | <rule ref="PSR2"/> | 16 | <rule ref="Generic.Arrays.DisallowLongArraySyntax"/> |
17 | |||
18 | <rule ref="PSR1.Files.SideEffects.FoundWithSymbols"> | ||
19 | <!-- index.php bootstraps everything, so yes mixed symbols with side effects --> | ||
20 | <exclude-pattern>index.php</exclude-pattern> | ||
21 | </rule> | ||
17 | </ruleset> | 22 | </ruleset> |
diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php index ab6ed6de..80b1dd95 100644 --- a/plugins/addlink_toolbar/addlink_toolbar.php +++ b/plugins/addlink_toolbar/addlink_toolbar.php | |||
@@ -17,26 +17,26 @@ use Shaarli\Render\TemplatePage; | |||
17 | function hook_addlink_toolbar_render_header($data) | 17 | function hook_addlink_toolbar_render_header($data) |
18 | { | 18 | { |
19 | if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) { | 19 | if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) { |
20 | $form = array( | 20 | $form = [ |
21 | 'attr' => array( | 21 | 'attr' => [ |
22 | 'method' => 'GET', | 22 | 'method' => 'GET', |
23 | 'action' => $data['_BASE_PATH_'] . '/admin/shaare', | 23 | 'action' => $data['_BASE_PATH_'] . '/admin/shaare', |
24 | 'name' => 'addform', | 24 | 'name' => 'addform', |
25 | 'class' => 'addform', | 25 | 'class' => 'addform', |
26 | ), | 26 | ], |
27 | 'inputs' => array( | 27 | 'inputs' => [ |
28 | array( | 28 | [ |
29 | 'type' => 'text', | 29 | 'type' => 'text', |
30 | 'name' => 'post', | 30 | 'name' => 'post', |
31 | 'placeholder' => t('URI'), | 31 | 'placeholder' => t('URI'), |
32 | ), | 32 | ], |
33 | array( | 33 | [ |
34 | 'type' => 'submit', | 34 | 'type' => 'submit', |
35 | 'value' => t('Add link'), | 35 | 'value' => t('Add link'), |
36 | 'class' => 'bigbutton', | 36 | 'class' => 'bigbutton', |
37 | ), | 37 | ], |
38 | ), | 38 | ], |
39 | ); | 39 | ]; |
40 | $data['fields_toolbar'][] = $form; | 40 | $data['fields_toolbar'][] = $form; |
41 | } | 41 | } |
42 | 42 | ||
diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php index 922b5966..88f2b653 100644 --- a/plugins/archiveorg/archiveorg.php +++ b/plugins/archiveorg/archiveorg.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Plugin Archive.org. | 4 | * Plugin Archive.org. |
4 | * | 5 | * |
@@ -17,7 +18,7 @@ use Shaarli\Plugin\PluginManager; | |||
17 | function hook_archiveorg_render_linklist($data) | 18 | function hook_archiveorg_render_linklist($data) |
18 | { | 19 | { |
19 | $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html'); | 20 | $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html'); |
20 | $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; | 21 | $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; |
21 | 22 | ||
22 | foreach ($data['links'] as &$value) { | 23 | foreach ($data['links'] as &$value) { |
23 | $isNote = startsWith($value['real_url'], '/shaare/'); | 24 | $isNote = startsWith($value['real_url'], '/shaare/'); |
diff --git a/plugins/default_colors/default_colors.php b/plugins/default_colors/default_colors.php index e1fd5cfb..574a0bd4 100644 --- a/plugins/default_colors/default_colors.php +++ b/plugins/default_colors/default_colors.php | |||
@@ -28,14 +28,14 @@ function default_colors_init($conf) | |||
28 | { | 28 | { |
29 | $params = []; | 29 | $params = []; |
30 | foreach (DEFAULT_COLORS_PLACEHOLDERS as $placeholder) { | 30 | foreach (DEFAULT_COLORS_PLACEHOLDERS as $placeholder) { |
31 | $value = trim($conf->get('plugins.'. $placeholder, '')); | 31 | $value = trim($conf->get('plugins.' . $placeholder, '')); |
32 | if (strlen($value) > 0) { | 32 | if (strlen($value) > 0) { |
33 | $params[$placeholder] = $value; | 33 | $params[$placeholder] = $value; |
34 | } | 34 | } |
35 | } | 35 | } |
36 | 36 | ||
37 | if (empty($params)) { | 37 | if (empty($params)) { |
38 | $error = t('Default colors plugin error: '. | 38 | $error = t('Default colors plugin error: ' . |
39 | 'This plugin is active and no custom color is configured.'); | 39 | 'This plugin is active and no custom color is configured.'); |
40 | return [$error]; | 40 | return [$error]; |
41 | } | 41 | } |
@@ -56,7 +56,7 @@ function default_colors_init($conf) | |||
56 | function hook_default_colors_render_includes($data) | 56 | function hook_default_colors_render_includes($data) |
57 | { | 57 | { |
58 | $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css'; | 58 | $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css'; |
59 | if (file_exists($file )) { | 59 | if (file_exists($file)) { |
60 | $data['css_files'][] = $file ; | 60 | $data['css_files'][] = $file ; |
61 | } | 61 | } |
62 | 62 | ||
@@ -75,7 +75,7 @@ function default_colors_generate_css_file($params): void | |||
75 | $content = ''; | 75 | $content = ''; |
76 | foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) { | 76 | foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) { |
77 | $content .= !empty($params[$rule]) | 77 | $content .= !empty($params[$rule]) |
78 | ? default_colors_format_css_rule($params, $rule) .';'. PHP_EOL | 78 | ? default_colors_format_css_rule($params, $rule) . ';' . PHP_EOL |
79 | : ''; | 79 | : ''; |
80 | } | 80 | } |
81 | 81 | ||
@@ -99,8 +99,8 @@ function default_colors_format_css_rule($data, $parameter) | |||
99 | } | 99 | } |
100 | 100 | ||
101 | $key = str_replace('DEFAULT_COLORS_', '', $parameter); | 101 | $key = str_replace('DEFAULT_COLORS_', '', $parameter); |
102 | $key = str_replace('_', '-', strtolower($key)) .'-color'; | 102 | $key = str_replace('_', '-', strtolower($key)) . '-color'; |
103 | return ' --'. $key .': '. $data[$parameter]; | 103 | return ' --' . $key . ': ' . $data[$parameter]; |
104 | } | 104 | } |
105 | 105 | ||
106 | 106 | ||
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php index defb01f7..22d27b68 100644 --- a/plugins/demo_plugin/demo_plugin.php +++ b/plugins/demo_plugin/demo_plugin.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Demo Plugin. | 4 | * Demo Plugin. |
4 | * | 5 | * |
@@ -82,14 +83,14 @@ function hook_demo_plugin_render_header($data) | |||
82 | * A link is an array of its attributes (key="value"), | 83 | * A link is an array of its attributes (key="value"), |
83 | * and a mandatory `html` key, which contains its value. | 84 | * and a mandatory `html` key, which contains its value. |
84 | */ | 85 | */ |
85 | $button = array( | 86 | $button = [ |
86 | 'attr' => array ( | 87 | 'attr' => [ |
87 | 'href' => '#', | 88 | 'href' => '#', |
88 | 'class' => 'mybutton', | 89 | 'class' => 'mybutton', |
89 | 'title' => 'hover me', | 90 | 'title' => 'hover me', |
90 | ), | 91 | ], |
91 | 'html' => 'DEMO buttons toolbar', | 92 | 'html' => 'DEMO buttons toolbar', |
92 | ); | 93 | ]; |
93 | $data['buttons_toolbar'][] = $button; | 94 | $data['buttons_toolbar'][] = $button; |
94 | } | 95 | } |
95 | 96 | ||
@@ -115,29 +116,29 @@ function hook_demo_plugin_render_header($data) | |||
115 | * <input input-2-attribute-1="input 2 attribute 1 value"> | 116 | * <input input-2-attribute-1="input 2 attribute 1 value"> |
116 | * </form> | 117 | * </form> |
117 | */ | 118 | */ |
118 | $form = array( | 119 | $form = [ |
119 | 'attr' => array( | 120 | 'attr' => [ |
120 | 'method' => 'GET', | 121 | 'method' => 'GET', |
121 | 'action' => $data['_BASE_PATH_'] . '/', | 122 | 'action' => $data['_BASE_PATH_'] . '/', |
122 | 'class' => 'addform', | 123 | 'class' => 'addform', |
123 | ), | 124 | ], |
124 | 'inputs' => array( | 125 | 'inputs' => [ |
125 | array( | 126 | [ |
126 | 'type' => 'text', | 127 | 'type' => 'text', |
127 | 'name' => 'demo', | 128 | 'name' => 'demo', |
128 | 'placeholder' => 'demo', | 129 | 'placeholder' => 'demo', |
129 | ) | 130 | ] |
130 | ) | 131 | ] |
131 | ); | 132 | ]; |
132 | $data['fields_toolbar'][] = $form; | 133 | $data['fields_toolbar'][] = $form; |
133 | } | 134 | } |
134 | // Another button always displayed | 135 | // Another button always displayed |
135 | $button = array( | 136 | $button = [ |
136 | 'attr' => array( | 137 | 'attr' => [ |
137 | 'href' => '#', | 138 | 'href' => '#', |
138 | ), | 139 | ], |
139 | 'html' => 'Demo', | 140 | 'html' => 'Demo', |
140 | ); | 141 | ]; |
141 | $data['buttons_toolbar'][] = $button; | 142 | $data['buttons_toolbar'][] = $button; |
142 | 143 | ||
143 | return $data; | 144 | return $data; |
@@ -187,7 +188,7 @@ function hook_demo_plugin_render_includes($data) | |||
187 | function hook_demo_plugin_render_footer($data) | 188 | function hook_demo_plugin_render_footer($data) |
188 | { | 189 | { |
189 | // Footer text | 190 | // Footer text |
190 | $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); | 191 | $data['text'][] = '<br>' . demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); |
191 | 192 | ||
192 | // Free elements at the end of the page. | 193 | // Free elements at the end of the page. |
193 | $data['endofpage'][] = '<marquee id="demo_marquee">' . | 194 | $data['endofpage'][] = '<marquee id="demo_marquee">' . |
@@ -229,13 +230,13 @@ function hook_demo_plugin_render_linklist($data) | |||
229 | * and a mandatory `html` key, which contains its value. | 230 | * and a mandatory `html` key, which contains its value. |
230 | * It's also recommended to add key 'on' or 'off' for theme rendering. | 231 | * It's also recommended to add key 'on' or 'off' for theme rendering. |
231 | */ | 232 | */ |
232 | $action = array( | 233 | $action = [ |
233 | 'attr' => array( | 234 | 'attr' => [ |
234 | 'href' => '?up', | 235 | 'href' => '?up', |
235 | 'title' => 'Uppercase!', | 236 | 'title' => 'Uppercase!', |
236 | ), | 237 | ], |
237 | 'html' => 'â†', | 238 | 'html' => 'â†', |
238 | ); | 239 | ]; |
239 | 240 | ||
240 | if (isset($_GET['up'])) { | 241 | if (isset($_GET['up'])) { |
241 | // Manipulate link data | 242 | // Manipulate link data |
@@ -275,7 +276,7 @@ function hook_demo_plugin_render_linklist($data) | |||
275 | function hook_demo_plugin_render_editlink($data) | 276 | function hook_demo_plugin_render_editlink($data) |
276 | { | 277 | { |
277 | // Load HTML into a string | 278 | // Load HTML into a string |
278 | $html = file_get_contents(PluginManager::$PLUGINS_PATH .'/demo_plugin/field.html'); | 279 | $html = file_get_contents(PluginManager::$PLUGINS_PATH . '/demo_plugin/field.html'); |
279 | 280 | ||
280 | // Replace value in HTML if it exists in $data | 281 | // Replace value in HTML if it exists in $data |
281 | if (!empty($data['link']['stuff'])) { | 282 | if (!empty($data['link']['stuff'])) { |
diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php index 79e7380b..a5450989 100644 --- a/plugins/isso/isso.php +++ b/plugins/isso/isso.php | |||
@@ -19,9 +19,9 @@ function isso_init($conf) | |||
19 | { | 19 | { |
20 | $issoUrl = $conf->get('plugins.ISSO_SERVER'); | 20 | $issoUrl = $conf->get('plugins.ISSO_SERVER'); |
21 | if (empty($issoUrl)) { | 21 | if (empty($issoUrl)) { |
22 | $error = t('Isso plugin error: '. | 22 | $error = t('Isso plugin error: ' . |
23 | 'Please define the "ISSO_SERVER" setting in the plugin administration page.'); | 23 | 'Please define the "ISSO_SERVER" setting in the plugin administration page.'); |
24 | return array($error); | 24 | return [$error]; |
25 | } | 25 | } |
26 | } | 26 | } |
27 | 27 | ||
@@ -49,12 +49,12 @@ function hook_isso_render_linklist($data, $conf) | |||
49 | $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']); | 49 | $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']); |
50 | $data['plugin_end_zone'][] = $isso; | 50 | $data['plugin_end_zone'][] = $isso; |
51 | } else { | 51 | } else { |
52 | $button = '<span><a href="'. ($data['_BASE_PATH_'] ?? '') . '/shaare/%s#isso-thread">'; | 52 | $button = '<span><a href="' . ($data['_BASE_PATH_'] ?? '') . '/shaare/%s#isso-thread">'; |
53 | // For the default theme we use a FontAwesome icon which is better than an image | 53 | // For the default theme we use a FontAwesome icon which is better than an image |
54 | if ($conf->get('resource.theme') === 'default') { | 54 | if ($conf->get('resource.theme') === 'default') { |
55 | $button .= '<i class="linklist-plugin-icon fa fa-comment"></i>'; | 55 | $button .= '<i class="linklist-plugin-icon fa fa-comment"></i>'; |
56 | } else { | 56 | } else { |
57 | $button .= '<img class="linklist-plugin-icon" src="plugins/isso/comment.png" '; | 57 | $button .= '<img class="linklist-plugin-icon" src="' . $data['_ROOT_PATH_'] . '/plugins/isso/comment.png" '; |
58 | $button .= 'title="Comment on this shaare" alt="Comments" />'; | 58 | $button .= 'title="Comment on this shaare" alt="Comments" />'; |
59 | } | 59 | } |
60 | $button .= '</a></span>'; | 60 | $button .= '</a></span>'; |
diff --git a/plugins/piwik/piwik.php b/plugins/piwik/piwik.php index 17b1aecc..efea8610 100644 --- a/plugins/piwik/piwik.php +++ b/plugins/piwik/piwik.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Piwik plugin. | 4 | * Piwik plugin. |
4 | * Adds tracking code on each page. | 5 | * Adds tracking code on each page. |
@@ -22,7 +23,7 @@ function piwik_init($conf) | |||
22 | if (empty($piwikUrl) || empty($piwikSiteid)) { | 23 | if (empty($piwikUrl) || empty($piwikSiteid)) { |
23 | $error = t('Piwik plugin error: ' . | 24 | $error = t('Piwik plugin error: ' . |
24 | 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'); | 25 | 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'); |
25 | return array($error); | 26 | return [$error]; |
26 | } | 27 | } |
27 | } | 28 | } |
28 | 29 | ||
diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php index 91a9c1e5..4f874f92 100644 --- a/plugins/playvideos/playvideos.php +++ b/plugins/playvideos/playvideos.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Plugin PlayVideos | 4 | * Plugin PlayVideos |
4 | * | 5 | * |
@@ -19,14 +20,14 @@ use Shaarli\Render\TemplatePage; | |||
19 | function hook_playvideos_render_header($data) | 20 | function hook_playvideos_render_header($data) |
20 | { | 21 | { |
21 | if ($data['_PAGE_'] == TemplatePage::LINKLIST) { | 22 | if ($data['_PAGE_'] == TemplatePage::LINKLIST) { |
22 | $playvideo = array( | 23 | $playvideo = [ |
23 | 'attr' => array( | 24 | 'attr' => [ |
24 | 'href' => '#', | 25 | 'href' => '#', |
25 | 'title' => t('Video player'), | 26 | 'title' => t('Video player'), |
26 | 'id' => 'playvideos', | 27 | 'id' => 'playvideos', |
27 | ), | 28 | ], |
28 | 'html' => 'â–º '. t('Play Videos') | 29 | 'html' => 'â–º ' . t('Play Videos') |
29 | ); | 30 | ]; |
30 | $data['buttons_toolbar'][] = $playvideo; | 31 | $data['buttons_toolbar'][] = $playvideo; |
31 | } | 32 | } |
32 | 33 | ||
diff --git a/plugins/pubsubhubbub/pubsubhubbub.php b/plugins/pubsubhubbub/pubsubhubbub.php index 8fe6799c..299b84fb 100644 --- a/plugins/pubsubhubbub/pubsubhubbub.php +++ b/plugins/pubsubhubbub/pubsubhubbub.php | |||
@@ -42,7 +42,7 @@ function pubsubhubbub_init($conf) | |||
42 | function hook_pubsubhubbub_render_feed($data, $conf) | 42 | function hook_pubsubhubbub_render_feed($data, $conf) |
43 | { | 43 | { |
44 | $feedType = $data['_PAGE_'] == TemplatePage::FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM; | 44 | $feedType = $data['_PAGE_'] == TemplatePage::FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM; |
45 | $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/pubsubhubbub/hub.'. $feedType .'.xml'); | 45 | $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/pubsubhubbub/hub.' . $feedType . '.xml'); |
46 | $data['feed_plugins_header'][] = sprintf($template, $conf->get('plugins.PUBSUBHUB_URL')); | 46 | $data['feed_plugins_header'][] = sprintf($template, $conf->get('plugins.PUBSUBHUB_URL')); |
47 | 47 | ||
48 | return $data; | 48 | return $data; |
@@ -59,10 +59,10 @@ function hook_pubsubhubbub_render_feed($data, $conf) | |||
59 | */ | 59 | */ |
60 | function hook_pubsubhubbub_save_link($data, $conf) | 60 | function hook_pubsubhubbub_save_link($data, $conf) |
61 | { | 61 | { |
62 | $feeds = array( | 62 | $feeds = [ |
63 | index_url($_SERVER) .'feed/atom', | 63 | index_url($_SERVER) . 'feed/atom', |
64 | index_url($_SERVER) .'feed/rss', | 64 | index_url($_SERVER) . 'feed/rss', |
65 | ); | 65 | ]; |
66 | 66 | ||
67 | $httpPost = function_exists('curl_version') ? false : 'nocurl_http_post'; | 67 | $httpPost = function_exists('curl_version') ? false : 'nocurl_http_post'; |
68 | try { | 68 | try { |
@@ -87,11 +87,11 @@ function hook_pubsubhubbub_save_link($data, $conf) | |||
87 | */ | 87 | */ |
88 | function nocurl_http_post($url, $postString) | 88 | function nocurl_http_post($url, $postString) |
89 | { | 89 | { |
90 | $params = array('http' => array( | 90 | $params = ['http' => [ |
91 | 'method' => 'POST', | 91 | 'method' => 'POST', |
92 | 'content' => $postString, | 92 | 'content' => $postString, |
93 | 'user_agent' => 'PubSubHubbub-Publisher-PHP/1.0', | 93 | 'user_agent' => 'PubSubHubbub-Publisher-PHP/1.0', |
94 | )); | 94 | ]]; |
95 | 95 | ||
96 | $context = stream_context_create($params); | 96 | $context = stream_context_create($params); |
97 | $fp = @fopen($url, 'rb', false, $context); | 97 | $fp = @fopen($url, 'rb', false, $context); |
diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php index 95499e39..2ae10476 100644 --- a/plugins/qrcode/qrcode.php +++ b/plugins/qrcode/qrcode.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Plugin qrcode | 4 | * Plugin qrcode |
4 | * Add QRCode containing URL for each links. | 5 | * Add QRCode containing URL for each links. |
@@ -19,7 +20,7 @@ function hook_qrcode_render_linklist($data) | |||
19 | { | 20 | { |
20 | $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); | 21 | $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); |
21 | 22 | ||
22 | $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; | 23 | $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; |
23 | foreach ($data['links'] as &$value) { | 24 | foreach ($data['links'] as &$value) { |
24 | $qrcode = sprintf( | 25 | $qrcode = sprintf( |
25 | $qrcode_html, | 26 | $qrcode_html, |
diff --git a/plugins/wallabag/WallabagInstance.php b/plugins/wallabag/WallabagInstance.php index f4a0a92b..88f84ae3 100644 --- a/plugins/wallabag/WallabagInstance.php +++ b/plugins/wallabag/WallabagInstance.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Plugin\Wallabag; | 3 | namespace Shaarli\Plugin\Wallabag; |
3 | 4 | ||
4 | /** | 5 | /** |
@@ -11,20 +12,20 @@ class WallabagInstance | |||
11 | * - key: version ID, must match plugin settings. | 12 | * - key: version ID, must match plugin settings. |
12 | * - value: version name. | 13 | * - value: version name. |
13 | */ | 14 | */ |
14 | private static $wallabagVersions = array( | 15 | private static $wallabagVersions = [ |
15 | 1 => '1.x', | 16 | 1 => '1.x', |
16 | 2 => '2.x', | 17 | 2 => '2.x', |
17 | ); | 18 | ]; |
18 | 19 | ||
19 | /** | 20 | /** |
20 | * @var array Static reference to WB endpoint according to the API version. | 21 | * @var array Static reference to WB endpoint according to the API version. |
21 | * - key: version name. | 22 | * - key: version name. |
22 | * - value: endpoint. | 23 | * - value: endpoint. |
23 | */ | 24 | */ |
24 | private static $wallabagEndpoints = array( | 25 | private static $wallabagEndpoints = [ |
25 | '1.x' => '?plainurl=', | 26 | '1.x' => '?plainurl=', |
26 | '2.x' => 'bookmarklet?url=', | 27 | '2.x' => 'bookmarklet?url=', |
27 | ); | 28 | ]; |
28 | 29 | ||
29 | /** | 30 | /** |
30 | * @var string Wallabag user instance URL. | 31 | * @var string Wallabag user instance URL. |
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php index 805c1ad9..f2003cb9 100644 --- a/plugins/wallabag/wallabag.php +++ b/plugins/wallabag/wallabag.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Wallabag plugin | 4 | * Wallabag plugin |
4 | */ | 5 | */ |
@@ -18,10 +19,11 @@ function wallabag_init($conf) | |||
18 | { | 19 | { |
19 | $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); | 20 | $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); |
20 | if (empty($wallabagUrl)) { | 21 | if (empty($wallabagUrl)) { |
21 | $error = t('Wallabag plugin error: '. | 22 | $error = t('Wallabag plugin error: ' . |
22 | 'Please define the "WALLABAG_URL" setting in the plugin administration page.'); | 23 | 'Please define the "WALLABAG_URL" setting in the plugin administration page.'); |
23 | return array($error); | 24 | return [$error]; |
24 | } | 25 | } |
26 | $conf->setEmpty('plugins.WALLABAG_URL', '2'); | ||
25 | } | 27 | } |
26 | 28 | ||
27 | /** | 29 | /** |
@@ -35,7 +37,7 @@ function wallabag_init($conf) | |||
35 | function hook_wallabag_render_linklist($data, $conf) | 37 | function hook_wallabag_render_linklist($data, $conf) |
36 | { | 38 | { |
37 | $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); | 39 | $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); |
38 | if (empty($wallabagUrl)) { | 40 | if (empty($wallabagUrl) || !$data['_LOGGEDIN_']) { |
39 | return $data; | 41 | return $data; |
40 | } | 42 | } |
41 | 43 | ||
@@ -45,13 +47,13 @@ function hook_wallabag_render_linklist($data, $conf) | |||
45 | $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); | 47 | $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); |
46 | 48 | ||
47 | $linkTitle = t('Save to wallabag'); | 49 | $linkTitle = t('Save to wallabag'); |
48 | $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; | 50 | $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; |
49 | 51 | ||
50 | foreach ($data['links'] as &$value) { | 52 | foreach ($data['links'] as &$value) { |
51 | $wallabag = sprintf( | 53 | $wallabag = sprintf( |
52 | $wallabagHtml, | 54 | $wallabagHtml, |
53 | $wallabagInstance->getWallabagUrl(), | 55 | $wallabagInstance->getWallabagUrl(), |
54 | urlencode($value['url']), | 56 | urlencode(unescape($value['url'])), |
55 | $path, | 57 | $path, |
56 | $linkTitle | 58 | $linkTitle |
57 | ); | 59 | ); |
diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php index 6dc0e5b7..e810104e 100644 --- a/tests/HistoryTest.php +++ b/tests/HistoryTest.php | |||
@@ -89,14 +89,6 @@ class HistoryTest extends \Shaarli\TestCase | |||
89 | $this->assertEquals(History::CREATED, $actual['event']); | 89 | $this->assertEquals(History::CREATED, $actual['event']); |
90 | $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); | 90 | $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); |
91 | $this->assertEquals(1, $actual['id']); | 91 | $this->assertEquals(1, $actual['id']); |
92 | |||
93 | $history = new History(self::$historyFilePath); | ||
94 | $bookmark = (new Bookmark())->setId('str'); | ||
95 | $history->addLink($bookmark); | ||
96 | $actual = $history->getHistory()[0]; | ||
97 | $this->assertEquals(History::CREATED, $actual['event']); | ||
98 | $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); | ||
99 | $this->assertEquals('str', $actual['id']); | ||
100 | } | 92 | } |
101 | 93 | ||
102 | // /** | 94 | // /** |
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 6e787d7f..59dca75f 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php | |||
@@ -63,41 +63,25 @@ class UtilsTest extends \Shaarli\TestCase | |||
63 | } | 63 | } |
64 | 64 | ||
65 | /** | 65 | /** |
66 | * Log a message to a file - IPv4 client address | 66 | * Format a log a message - IPv4 client address |
67 | */ | 67 | */ |
68 | public function testLogmIp4() | 68 | public function testFormatLogIp4() |
69 | { | 69 | { |
70 | $logMessage = 'IPv4 client connected'; | 70 | $message = 'IPv4 client connected'; |
71 | logm(self::$testLogFile, '127.0.0.1', $logMessage); | 71 | $log = format_log($message, '127.0.0.1'); |
72 | list($date, $ip, $message) = $this->getLastLogEntry(); | ||
73 | 72 | ||
74 | $this->assertInstanceOf( | 73 | static::assertSame('- 127.0.0.1 - IPv4 client connected', $log); |
75 | 'DateTime', | ||
76 | DateTime::createFromFormat(self::$dateFormat, $date) | ||
77 | ); | ||
78 | $this->assertTrue( | ||
79 | filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false | ||
80 | ); | ||
81 | $this->assertEquals($logMessage, $message); | ||
82 | } | 74 | } |
83 | 75 | ||
84 | /** | 76 | /** |
85 | * Log a message to a file - IPv6 client address | 77 | * Format a log a message - IPv6 client address |
86 | */ | 78 | */ |
87 | public function testLogmIp6() | 79 | public function testFormatLogIp6() |
88 | { | 80 | { |
89 | $logMessage = 'IPv6 client connected'; | 81 | $message = 'IPv6 client connected'; |
90 | logm(self::$testLogFile, '2001:db8::ff00:42:8329', $logMessage); | 82 | $log = format_log($message, '2001:db8::ff00:42:8329'); |
91 | list($date, $ip, $message) = $this->getLastLogEntry(); | ||
92 | 83 | ||
93 | $this->assertInstanceOf( | 84 | static::assertSame('- 2001:db8::ff00:42:8329 - IPv6 client connected', $log); |
94 | 'DateTime', | ||
95 | DateTime::createFromFormat(self::$dateFormat, $date) | ||
96 | ); | ||
97 | $this->assertTrue( | ||
98 | filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false | ||
99 | ); | ||
100 | $this->assertEquals($logMessage, $message); | ||
101 | } | 85 | } |
102 | 86 | ||
103 | /** | 87 | /** |
diff --git a/tests/api/controllers/info/InfoTest.php b/tests/api/controllers/info/InfoTest.php index 1598e1e8..10b29ab2 100644 --- a/tests/api/controllers/info/InfoTest.php +++ b/tests/api/controllers/info/InfoTest.php | |||
@@ -1,6 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli\Api\Controllers; | 2 | namespace Shaarli\Api\Controllers; |
3 | 3 | ||
4 | use malkusch\lock\mutex\NoMutex; | ||
4 | use Shaarli\Bookmark\BookmarkFileService; | 5 | use Shaarli\Bookmark\BookmarkFileService; |
5 | use Shaarli\Config\ConfigManager; | 6 | use Shaarli\Config\ConfigManager; |
6 | use Shaarli\History; | 7 | use Shaarli\History; |
@@ -49,6 +50,7 @@ class InfoTest extends TestCase | |||
49 | */ | 50 | */ |
50 | protected function setUp(): void | 51 | protected function setUp(): void |
51 | { | 52 | { |
53 | $mutex = new NoMutex(); | ||
52 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 54 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
53 | $this->conf->set('resource.datastore', self::$testDatastore); | 55 | $this->conf->set('resource.datastore', self::$testDatastore); |
54 | $this->refDB = new \ReferenceLinkDB(); | 56 | $this->refDB = new \ReferenceLinkDB(); |
@@ -58,7 +60,7 @@ class InfoTest extends TestCase | |||
58 | 60 | ||
59 | $this->container = new Container(); | 61 | $this->container = new Container(); |
60 | $this->container['conf'] = $this->conf; | 62 | $this->container['conf'] = $this->conf; |
61 | $this->container['db'] = new BookmarkFileService($this->conf, $history, true); | 63 | $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true); |
62 | $this->container['history'] = null; | 64 | $this->container['history'] = null; |
63 | 65 | ||
64 | $this->controller = new Info($this->container); | 66 | $this->controller = new Info($this->container); |
diff --git a/tests/api/controllers/links/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php index cf9464f0..805c9be3 100644 --- a/tests/api/controllers/links/DeleteLinkTest.php +++ b/tests/api/controllers/links/DeleteLinkTest.php | |||
@@ -3,6 +3,7 @@ | |||
3 | 3 | ||
4 | namespace Shaarli\Api\Controllers; | 4 | namespace Shaarli\Api\Controllers; |
5 | 5 | ||
6 | use malkusch\lock\mutex\NoMutex; | ||
6 | use Shaarli\Bookmark\BookmarkFileService; | 7 | use Shaarli\Bookmark\BookmarkFileService; |
7 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
8 | use Shaarli\History; | 9 | use Shaarli\History; |
@@ -53,11 +54,15 @@ class DeleteLinkTest extends \Shaarli\TestCase | |||
53 | */ | 54 | */ |
54 | protected $controller; | 55 | protected $controller; |
55 | 56 | ||
57 | /** @var NoMutex */ | ||
58 | protected $mutex; | ||
59 | |||
56 | /** | 60 | /** |
57 | * Before each test, instantiate a new Api with its config, plugins and bookmarks. | 61 | * Before each test, instantiate a new Api with its config, plugins and bookmarks. |
58 | */ | 62 | */ |
59 | protected function setUp(): void | 63 | protected function setUp(): void |
60 | { | 64 | { |
65 | $this->mutex = new NoMutex(); | ||
61 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 66 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
62 | $this->conf->set('resource.datastore', self::$testDatastore); | 67 | $this->conf->set('resource.datastore', self::$testDatastore); |
63 | $this->refDB = new \ReferenceLinkDB(); | 68 | $this->refDB = new \ReferenceLinkDB(); |
@@ -65,7 +70,7 @@ class DeleteLinkTest extends \Shaarli\TestCase | |||
65 | $refHistory = new \ReferenceHistory(); | 70 | $refHistory = new \ReferenceHistory(); |
66 | $refHistory->write(self::$testHistory); | 71 | $refHistory->write(self::$testHistory); |
67 | $this->history = new History(self::$testHistory); | 72 | $this->history = new History(self::$testHistory); |
68 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 73 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
69 | 74 | ||
70 | $this->container = new Container(); | 75 | $this->container = new Container(); |
71 | $this->container['conf'] = $this->conf; | 76 | $this->container['conf'] = $this->conf; |
@@ -100,7 +105,7 @@ class DeleteLinkTest extends \Shaarli\TestCase | |||
100 | $this->assertEquals(204, $response->getStatusCode()); | 105 | $this->assertEquals(204, $response->getStatusCode()); |
101 | $this->assertEmpty((string) $response->getBody()); | 106 | $this->assertEmpty((string) $response->getBody()); |
102 | 107 | ||
103 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 108 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
104 | $this->assertFalse($this->bookmarkService->exists($id)); | 109 | $this->assertFalse($this->bookmarkService->exists($id)); |
105 | 110 | ||
106 | $historyEntry = $this->history->getHistory()[0]; | 111 | $historyEntry = $this->history->getHistory()[0]; |
diff --git a/tests/api/controllers/links/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php index 99dc606f..1ec56ef3 100644 --- a/tests/api/controllers/links/GetLinkIdTest.php +++ b/tests/api/controllers/links/GetLinkIdTest.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
4 | 4 | ||
5 | use malkusch\lock\mutex\NoMutex; | ||
5 | use Shaarli\Bookmark\Bookmark; | 6 | use Shaarli\Bookmark\Bookmark; |
6 | use Shaarli\Bookmark\BookmarkFileService; | 7 | use Shaarli\Bookmark\BookmarkFileService; |
7 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
@@ -57,6 +58,7 @@ class GetLinkIdTest extends \Shaarli\TestCase | |||
57 | */ | 58 | */ |
58 | protected function setUp(): void | 59 | protected function setUp(): void |
59 | { | 60 | { |
61 | $mutex = new NoMutex(); | ||
60 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 62 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
61 | $this->conf->set('resource.datastore', self::$testDatastore); | 63 | $this->conf->set('resource.datastore', self::$testDatastore); |
62 | $this->refDB = new \ReferenceLinkDB(); | 64 | $this->refDB = new \ReferenceLinkDB(); |
@@ -65,7 +67,7 @@ class GetLinkIdTest extends \Shaarli\TestCase | |||
65 | 67 | ||
66 | $this->container = new Container(); | 68 | $this->container = new Container(); |
67 | $this->container['conf'] = $this->conf; | 69 | $this->container['conf'] = $this->conf; |
68 | $this->container['db'] = new BookmarkFileService($this->conf, $history, true); | 70 | $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true); |
69 | $this->container['history'] = null; | 71 | $this->container['history'] = null; |
70 | 72 | ||
71 | $this->controller = new Links($this->container); | 73 | $this->controller = new Links($this->container); |
diff --git a/tests/api/controllers/links/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php index ca1bfc63..b1c46ee2 100644 --- a/tests/api/controllers/links/GetLinksTest.php +++ b/tests/api/controllers/links/GetLinksTest.php | |||
@@ -1,6 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli\Api\Controllers; | 2 | namespace Shaarli\Api\Controllers; |
3 | 3 | ||
4 | use malkusch\lock\mutex\NoMutex; | ||
4 | use Shaarli\Bookmark\Bookmark; | 5 | use Shaarli\Bookmark\Bookmark; |
5 | use Shaarli\Bookmark\BookmarkFileService; | 6 | use Shaarli\Bookmark\BookmarkFileService; |
6 | use Shaarli\Bookmark\LinkDB; | 7 | use Shaarli\Bookmark\LinkDB; |
@@ -57,6 +58,7 @@ class GetLinksTest extends \Shaarli\TestCase | |||
57 | */ | 58 | */ |
58 | protected function setUp(): void | 59 | protected function setUp(): void |
59 | { | 60 | { |
61 | $mutex = new NoMutex(); | ||
60 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 62 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
61 | $this->conf->set('resource.datastore', self::$testDatastore); | 63 | $this->conf->set('resource.datastore', self::$testDatastore); |
62 | $this->refDB = new \ReferenceLinkDB(); | 64 | $this->refDB = new \ReferenceLinkDB(); |
@@ -65,7 +67,7 @@ class GetLinksTest extends \Shaarli\TestCase | |||
65 | 67 | ||
66 | $this->container = new Container(); | 68 | $this->container = new Container(); |
67 | $this->container['conf'] = $this->conf; | 69 | $this->container['conf'] = $this->conf; |
68 | $this->container['db'] = new BookmarkFileService($this->conf, $history, true); | 70 | $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true); |
69 | $this->container['history'] = null; | 71 | $this->container['history'] = null; |
70 | 72 | ||
71 | $this->controller = new Links($this->container); | 73 | $this->controller = new Links($this->container); |
@@ -396,7 +398,7 @@ class GetLinksTest extends \Shaarli\TestCase | |||
396 | $response = $this->controller->getLinks($request, new Response()); | 398 | $response = $this->controller->getLinks($request, new Response()); |
397 | $this->assertEquals(200, $response->getStatusCode()); | 399 | $this->assertEquals(200, $response->getStatusCode()); |
398 | $data = json_decode((string) $response->getBody(), true); | 400 | $data = json_decode((string) $response->getBody(), true); |
399 | $this->assertEquals(4, count($data)); | 401 | $this->assertEquals(5, count($data)); |
400 | $this->assertEquals(6, $data[0]['id']); | 402 | $this->assertEquals(6, $data[0]['id']); |
401 | 403 | ||
402 | // wildcard: placeholder at the middle | 404 | // wildcard: placeholder at the middle |
diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php index fe3de66f..e12f803b 100644 --- a/tests/api/controllers/links/PostLinkTest.php +++ b/tests/api/controllers/links/PostLinkTest.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
4 | 4 | ||
5 | use malkusch\lock\mutex\NoMutex; | ||
5 | use Shaarli\Bookmark\Bookmark; | 6 | use Shaarli\Bookmark\Bookmark; |
6 | use Shaarli\Bookmark\BookmarkFileService; | 7 | use Shaarli\Bookmark\BookmarkFileService; |
7 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
@@ -72,6 +73,7 @@ class PostLinkTest extends TestCase | |||
72 | */ | 73 | */ |
73 | protected function setUp(): void | 74 | protected function setUp(): void |
74 | { | 75 | { |
76 | $mutex = new NoMutex(); | ||
75 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 77 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
76 | $this->conf->set('resource.datastore', self::$testDatastore); | 78 | $this->conf->set('resource.datastore', self::$testDatastore); |
77 | $this->refDB = new \ReferenceLinkDB(); | 79 | $this->refDB = new \ReferenceLinkDB(); |
@@ -79,7 +81,7 @@ class PostLinkTest extends TestCase | |||
79 | $refHistory = new \ReferenceHistory(); | 81 | $refHistory = new \ReferenceHistory(); |
80 | $refHistory->write(self::$testHistory); | 82 | $refHistory->write(self::$testHistory); |
81 | $this->history = new History(self::$testHistory); | 83 | $this->history = new History(self::$testHistory); |
82 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 84 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true); |
83 | 85 | ||
84 | $this->container = new Container(); | 86 | $this->container = new Container(); |
85 | $this->container['conf'] = $this->conf; | 87 | $this->container['conf'] = $this->conf; |
@@ -90,8 +92,8 @@ class PostLinkTest extends TestCase | |||
90 | 92 | ||
91 | $mock = $this->createMock(Router::class); | 93 | $mock = $this->createMock(Router::class); |
92 | $mock->expects($this->any()) | 94 | $mock->expects($this->any()) |
93 | ->method('relativePathFor') | 95 | ->method('pathFor') |
94 | ->willReturn('api/v1/bookmarks/1'); | 96 | ->willReturn('/api/v1/bookmarks/1'); |
95 | 97 | ||
96 | // affect @property-read... seems to work | 98 | // affect @property-read... seems to work |
97 | $this->controller->getCi()->router = $mock; | 99 | $this->controller->getCi()->router = $mock; |
@@ -126,7 +128,7 @@ class PostLinkTest extends TestCase | |||
126 | 128 | ||
127 | $response = $this->controller->postLink($request, new Response()); | 129 | $response = $this->controller->postLink($request, new Response()); |
128 | $this->assertEquals(201, $response->getStatusCode()); | 130 | $this->assertEquals(201, $response->getStatusCode()); |
129 | $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); | 131 | $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); |
130 | $data = json_decode((string) $response->getBody(), true); | 132 | $data = json_decode((string) $response->getBody(), true); |
131 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); | 133 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); |
132 | $this->assertEquals(43, $data['id']); | 134 | $this->assertEquals(43, $data['id']); |
@@ -160,6 +162,8 @@ class PostLinkTest extends TestCase | |||
160 | 'description' => 'shaare description', | 162 | 'description' => 'shaare description', |
161 | 'tags' => ['one', 'two'], | 163 | 'tags' => ['one', 'two'], |
162 | 'private' => true, | 164 | 'private' => true, |
165 | 'created' => '2015-05-05T12:30:00+03:00', | ||
166 | 'updated' => '2016-06-05T14:32:10+03:00', | ||
163 | ]; | 167 | ]; |
164 | $env = Environment::mock([ | 168 | $env = Environment::mock([ |
165 | 'REQUEST_METHOD' => 'POST', | 169 | 'REQUEST_METHOD' => 'POST', |
@@ -171,7 +175,7 @@ class PostLinkTest extends TestCase | |||
171 | $response = $this->controller->postLink($request, new Response()); | 175 | $response = $this->controller->postLink($request, new Response()); |
172 | 176 | ||
173 | $this->assertEquals(201, $response->getStatusCode()); | 177 | $this->assertEquals(201, $response->getStatusCode()); |
174 | $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); | 178 | $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); |
175 | $data = json_decode((string) $response->getBody(), true); | 179 | $data = json_decode((string) $response->getBody(), true); |
176 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); | 180 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); |
177 | $this->assertEquals(43, $data['id']); | 181 | $this->assertEquals(43, $data['id']); |
@@ -181,10 +185,8 @@ class PostLinkTest extends TestCase | |||
181 | $this->assertEquals($link['description'], $data['description']); | 185 | $this->assertEquals($link['description'], $data['description']); |
182 | $this->assertEquals($link['tags'], $data['tags']); | 186 | $this->assertEquals($link['tags'], $data['tags']); |
183 | $this->assertEquals(true, $data['private']); | 187 | $this->assertEquals(true, $data['private']); |
184 | $this->assertTrue( | 188 | $this->assertSame($link['created'], $data['created']); |
185 | new \DateTime('2 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) | 189 | $this->assertSame($link['updated'], $data['updated']); |
186 | ); | ||
187 | $this->assertEquals('', $data['updated']); | ||
188 | } | 190 | } |
189 | 191 | ||
190 | /** | 192 | /** |
diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php index a2e87c59..240ee323 100644 --- a/tests/api/controllers/links/PutLinkTest.php +++ b/tests/api/controllers/links/PutLinkTest.php | |||
@@ -3,6 +3,7 @@ | |||
3 | 3 | ||
4 | namespace Shaarli\Api\Controllers; | 4 | namespace Shaarli\Api\Controllers; |
5 | 5 | ||
6 | use malkusch\lock\mutex\NoMutex; | ||
6 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
7 | use Shaarli\Bookmark\BookmarkFileService; | 8 | use Shaarli\Bookmark\BookmarkFileService; |
8 | use Shaarli\Config\ConfigManager; | 9 | use Shaarli\Config\ConfigManager; |
@@ -64,6 +65,7 @@ class PutLinkTest extends \Shaarli\TestCase | |||
64 | */ | 65 | */ |
65 | protected function setUp(): void | 66 | protected function setUp(): void |
66 | { | 67 | { |
68 | $mutex = new NoMutex(); | ||
67 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 69 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
68 | $this->conf->set('resource.datastore', self::$testDatastore); | 70 | $this->conf->set('resource.datastore', self::$testDatastore); |
69 | $this->refDB = new \ReferenceLinkDB(); | 71 | $this->refDB = new \ReferenceLinkDB(); |
@@ -71,7 +73,7 @@ class PutLinkTest extends \Shaarli\TestCase | |||
71 | $refHistory = new \ReferenceHistory(); | 73 | $refHistory = new \ReferenceHistory(); |
72 | $refHistory->write(self::$testHistory); | 74 | $refHistory->write(self::$testHistory); |
73 | $this->history = new History(self::$testHistory); | 75 | $this->history = new History(self::$testHistory); |
74 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 76 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true); |
75 | 77 | ||
76 | $this->container = new Container(); | 78 | $this->container = new Container(); |
77 | $this->container['conf'] = $this->conf; | 79 | $this->container['conf'] = $this->conf; |
diff --git a/tests/api/controllers/tags/DeleteTagTest.php b/tests/api/controllers/tags/DeleteTagTest.php index 1326eb47..37f07229 100644 --- a/tests/api/controllers/tags/DeleteTagTest.php +++ b/tests/api/controllers/tags/DeleteTagTest.php | |||
@@ -3,6 +3,7 @@ | |||
3 | 3 | ||
4 | namespace Shaarli\Api\Controllers; | 4 | namespace Shaarli\Api\Controllers; |
5 | 5 | ||
6 | use malkusch\lock\mutex\NoMutex; | ||
6 | use Shaarli\Bookmark\BookmarkFileService; | 7 | use Shaarli\Bookmark\BookmarkFileService; |
7 | use Shaarli\Bookmark\LinkDB; | 8 | use Shaarli\Bookmark\LinkDB; |
8 | use Shaarli\Config\ConfigManager; | 9 | use Shaarli\Config\ConfigManager; |
@@ -54,11 +55,15 @@ class DeleteTagTest extends \Shaarli\TestCase | |||
54 | */ | 55 | */ |
55 | protected $controller; | 56 | protected $controller; |
56 | 57 | ||
58 | /** @var NoMutex */ | ||
59 | protected $mutex; | ||
60 | |||
57 | /** | 61 | /** |
58 | * Before each test, instantiate a new Api with its config, plugins and bookmarks. | 62 | * Before each test, instantiate a new Api with its config, plugins and bookmarks. |
59 | */ | 63 | */ |
60 | protected function setUp(): void | 64 | protected function setUp(): void |
61 | { | 65 | { |
66 | $this->mutex = new NoMutex(); | ||
62 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 67 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
63 | $this->conf->set('resource.datastore', self::$testDatastore); | 68 | $this->conf->set('resource.datastore', self::$testDatastore); |
64 | $this->refDB = new \ReferenceLinkDB(); | 69 | $this->refDB = new \ReferenceLinkDB(); |
@@ -66,7 +71,7 @@ class DeleteTagTest extends \Shaarli\TestCase | |||
66 | $refHistory = new \ReferenceHistory(); | 71 | $refHistory = new \ReferenceHistory(); |
67 | $refHistory->write(self::$testHistory); | 72 | $refHistory->write(self::$testHistory); |
68 | $this->history = new History(self::$testHistory); | 73 | $this->history = new History(self::$testHistory); |
69 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 74 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
70 | 75 | ||
71 | $this->container = new Container(); | 76 | $this->container = new Container(); |
72 | $this->container['conf'] = $this->conf; | 77 | $this->container['conf'] = $this->conf; |
@@ -102,7 +107,7 @@ class DeleteTagTest extends \Shaarli\TestCase | |||
102 | $this->assertEquals(204, $response->getStatusCode()); | 107 | $this->assertEquals(204, $response->getStatusCode()); |
103 | $this->assertEmpty((string) $response->getBody()); | 108 | $this->assertEmpty((string) $response->getBody()); |
104 | 109 | ||
105 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 110 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
106 | $tags = $this->bookmarkService->bookmarksCountPerTag(); | 111 | $tags = $this->bookmarkService->bookmarksCountPerTag(); |
107 | $this->assertFalse(isset($tags[$tagName])); | 112 | $this->assertFalse(isset($tags[$tagName])); |
108 | 113 | ||
@@ -136,7 +141,7 @@ class DeleteTagTest extends \Shaarli\TestCase | |||
136 | $this->assertEquals(204, $response->getStatusCode()); | 141 | $this->assertEquals(204, $response->getStatusCode()); |
137 | $this->assertEmpty((string) $response->getBody()); | 142 | $this->assertEmpty((string) $response->getBody()); |
138 | 143 | ||
139 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 144 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
140 | $tags = $this->bookmarkService->bookmarksCountPerTag(); | 145 | $tags = $this->bookmarkService->bookmarksCountPerTag(); |
141 | $this->assertFalse(isset($tags[$tagName])); | 146 | $this->assertFalse(isset($tags[$tagName])); |
142 | $this->assertTrue($tags[strtolower($tagName)] > 0); | 147 | $this->assertTrue($tags[strtolower($tagName)] > 0); |
diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php index 9c05954b..878de5a4 100644 --- a/tests/api/controllers/tags/GetTagNameTest.php +++ b/tests/api/controllers/tags/GetTagNameTest.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
4 | 4 | ||
5 | use malkusch\lock\mutex\NoMutex; | ||
5 | use Shaarli\Bookmark\BookmarkFileService; | 6 | use Shaarli\Bookmark\BookmarkFileService; |
6 | use Shaarli\Bookmark\LinkDB; | 7 | use Shaarli\Bookmark\LinkDB; |
7 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
@@ -55,6 +56,7 @@ class GetTagNameTest extends \Shaarli\TestCase | |||
55 | */ | 56 | */ |
56 | protected function setUp(): void | 57 | protected function setUp(): void |
57 | { | 58 | { |
59 | $mutex = new NoMutex(); | ||
58 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 60 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
59 | $this->conf->set('resource.datastore', self::$testDatastore); | 61 | $this->conf->set('resource.datastore', self::$testDatastore); |
60 | $this->refDB = new \ReferenceLinkDB(); | 62 | $this->refDB = new \ReferenceLinkDB(); |
@@ -63,7 +65,7 @@ class GetTagNameTest extends \Shaarli\TestCase | |||
63 | 65 | ||
64 | $this->container = new Container(); | 66 | $this->container = new Container(); |
65 | $this->container['conf'] = $this->conf; | 67 | $this->container['conf'] = $this->conf; |
66 | $this->container['db'] = new BookmarkFileService($this->conf, $history, true); | 68 | $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true); |
67 | $this->container['history'] = null; | 69 | $this->container['history'] = null; |
68 | 70 | ||
69 | $this->controller = new Tags($this->container); | 71 | $this->controller = new Tags($this->container); |
diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php index 3459fdfa..b565a8c4 100644 --- a/tests/api/controllers/tags/GetTagsTest.php +++ b/tests/api/controllers/tags/GetTagsTest.php | |||
@@ -1,6 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli\Api\Controllers; | 2 | namespace Shaarli\Api\Controllers; |
3 | 3 | ||
4 | use malkusch\lock\mutex\NoMutex; | ||
4 | use Shaarli\Bookmark\BookmarkFileService; | 5 | use Shaarli\Bookmark\BookmarkFileService; |
5 | use Shaarli\Bookmark\LinkDB; | 6 | use Shaarli\Bookmark\LinkDB; |
6 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
@@ -59,13 +60,14 @@ class GetTagsTest extends \Shaarli\TestCase | |||
59 | */ | 60 | */ |
60 | protected function setUp(): void | 61 | protected function setUp(): void |
61 | { | 62 | { |
63 | $mutex = new NoMutex(); | ||
62 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 64 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
63 | $this->conf->set('resource.datastore', self::$testDatastore); | 65 | $this->conf->set('resource.datastore', self::$testDatastore); |
64 | $this->refDB = new \ReferenceLinkDB(); | 66 | $this->refDB = new \ReferenceLinkDB(); |
65 | $this->refDB->write(self::$testDatastore); | 67 | $this->refDB->write(self::$testDatastore); |
66 | $history = new History('sandbox/history.php'); | 68 | $history = new History('sandbox/history.php'); |
67 | 69 | ||
68 | $this->bookmarkService = new BookmarkFileService($this->conf, $history, true); | 70 | $this->bookmarkService = new BookmarkFileService($this->conf, $history, $mutex, true); |
69 | 71 | ||
70 | $this->container = new Container(); | 72 | $this->container = new Container(); |
71 | $this->container['conf'] = $this->conf; | 73 | $this->container['conf'] = $this->conf; |
diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php index 74edde78..c73f6d3b 100644 --- a/tests/api/controllers/tags/PutTagTest.php +++ b/tests/api/controllers/tags/PutTagTest.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
4 | 4 | ||
5 | use malkusch\lock\mutex\NoMutex; | ||
5 | use Shaarli\Api\Exceptions\ApiBadParametersException; | 6 | use Shaarli\Api\Exceptions\ApiBadParametersException; |
6 | use Shaarli\Bookmark\BookmarkFileService; | 7 | use Shaarli\Bookmark\BookmarkFileService; |
7 | use Shaarli\Bookmark\LinkDB; | 8 | use Shaarli\Bookmark\LinkDB; |
@@ -64,6 +65,7 @@ class PutTagTest extends \Shaarli\TestCase | |||
64 | */ | 65 | */ |
65 | protected function setUp(): void | 66 | protected function setUp(): void |
66 | { | 67 | { |
68 | $mutex = new NoMutex(); | ||
67 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 69 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
68 | $this->conf->set('resource.datastore', self::$testDatastore); | 70 | $this->conf->set('resource.datastore', self::$testDatastore); |
69 | $this->refDB = new \ReferenceLinkDB(); | 71 | $this->refDB = new \ReferenceLinkDB(); |
@@ -71,7 +73,7 @@ class PutTagTest extends \Shaarli\TestCase | |||
71 | $refHistory = new \ReferenceHistory(); | 73 | $refHistory = new \ReferenceHistory(); |
72 | $refHistory->write(self::$testHistory); | 74 | $refHistory->write(self::$testHistory); |
73 | $this->history = new History(self::$testHistory); | 75 | $this->history = new History(self::$testHistory); |
74 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 76 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true); |
75 | 77 | ||
76 | $this->container = new Container(); | 78 | $this->container = new Container(); |
77 | $this->container['conf'] = $this->conf; | 79 | $this->container['conf'] = $this->conf; |
diff --git a/tests/bookmark/BookmarkArrayTest.php b/tests/bookmark/BookmarkArrayTest.php index ebed9bfc..1953078c 100644 --- a/tests/bookmark/BookmarkArrayTest.php +++ b/tests/bookmark/BookmarkArrayTest.php | |||
@@ -91,19 +91,6 @@ class BookmarkArrayTest extends TestCase | |||
91 | } | 91 | } |
92 | 92 | ||
93 | /** | 93 | /** |
94 | * Test adding a bad entry: invalid ID type | ||
95 | */ | ||
96 | public function testArrayAccessAddBadEntryIdType() | ||
97 | { | ||
98 | $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class); | ||
99 | |||
100 | $array = new BookmarkArray(); | ||
101 | $bookmark = (new Bookmark())->setId('nope'); | ||
102 | $bookmark->validate(); | ||
103 | $array[] = $bookmark; | ||
104 | } | ||
105 | |||
106 | /** | ||
107 | * Test adding a bad entry: ID/offset not consistent | 94 | * Test adding a bad entry: ID/offset not consistent |
108 | */ | 95 | */ |
109 | public function testArrayAccessAddBadEntryIdOffset() | 96 | public function testArrayAccessAddBadEntryIdOffset() |
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php index c399822b..f619aff3 100644 --- a/tests/bookmark/BookmarkFileServiceTest.php +++ b/tests/bookmark/BookmarkFileServiceTest.php | |||
@@ -6,6 +6,7 @@ | |||
6 | namespace Shaarli\Bookmark; | 6 | namespace Shaarli\Bookmark; |
7 | 7 | ||
8 | use DateTime; | 8 | use DateTime; |
9 | use malkusch\lock\mutex\NoMutex; | ||
9 | use ReferenceLinkDB; | 10 | use ReferenceLinkDB; |
10 | use ReflectionClass; | 11 | use ReflectionClass; |
11 | use Shaarli; | 12 | use Shaarli; |
@@ -52,6 +53,9 @@ class BookmarkFileServiceTest extends TestCase | |||
52 | */ | 53 | */ |
53 | protected $privateLinkDB = null; | 54 | protected $privateLinkDB = null; |
54 | 55 | ||
56 | /** @var NoMutex */ | ||
57 | protected $mutex; | ||
58 | |||
55 | /** | 59 | /** |
56 | * Instantiates public and private LinkDBs with test data | 60 | * Instantiates public and private LinkDBs with test data |
57 | * | 61 | * |
@@ -68,6 +72,8 @@ class BookmarkFileServiceTest extends TestCase | |||
68 | */ | 72 | */ |
69 | protected function setUp(): void | 73 | protected function setUp(): void |
70 | { | 74 | { |
75 | $this->mutex = new NoMutex(); | ||
76 | |||
71 | if (file_exists(self::$testDatastore)) { | 77 | if (file_exists(self::$testDatastore)) { |
72 | unlink(self::$testDatastore); | 78 | unlink(self::$testDatastore); |
73 | } | 79 | } |
@@ -87,8 +93,8 @@ class BookmarkFileServiceTest extends TestCase | |||
87 | $this->refDB = new \ReferenceLinkDB(); | 93 | $this->refDB = new \ReferenceLinkDB(); |
88 | $this->refDB->write(self::$testDatastore); | 94 | $this->refDB->write(self::$testDatastore); |
89 | $this->history = new History('sandbox/history.php'); | 95 | $this->history = new History('sandbox/history.php'); |
90 | $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, false); | 96 | $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); |
91 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 97 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
92 | } | 98 | } |
93 | 99 | ||
94 | /** | 100 | /** |
@@ -105,7 +111,7 @@ class BookmarkFileServiceTest extends TestCase | |||
105 | $db = self::getMethod('migrate'); | 111 | $db = self::getMethod('migrate'); |
106 | $db->invokeArgs($this->privateLinkDB, []); | 112 | $db->invokeArgs($this->privateLinkDB, []); |
107 | 113 | ||
108 | $db = new \FakeBookmarkService($this->conf, $this->history, true); | 114 | $db = new \FakeBookmarkService($this->conf, $this->history, $this->mutex, true); |
109 | $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); | 115 | $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); |
110 | $this->assertEquals($this->refDB->countLinks(), $db->count()); | 116 | $this->assertEquals($this->refDB->countLinks(), $db->count()); |
111 | } | 117 | } |
@@ -174,7 +180,7 @@ class BookmarkFileServiceTest extends TestCase | |||
174 | $this->assertEquals($updated, $bookmark->getUpdated()); | 180 | $this->assertEquals($updated, $bookmark->getUpdated()); |
175 | 181 | ||
176 | // reload from file | 182 | // reload from file |
177 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 183 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
178 | 184 | ||
179 | $bookmark = $this->privateLinkDB->get(43); | 185 | $bookmark = $this->privateLinkDB->get(43); |
180 | $this->assertEquals(43, $bookmark->getId()); | 186 | $this->assertEquals(43, $bookmark->getId()); |
@@ -212,7 +218,7 @@ class BookmarkFileServiceTest extends TestCase | |||
212 | $this->assertNull($bookmark->getUpdated()); | 218 | $this->assertNull($bookmark->getUpdated()); |
213 | 219 | ||
214 | // reload from file | 220 | // reload from file |
215 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 221 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
216 | 222 | ||
217 | $bookmark = $this->privateLinkDB->get(43); | 223 | $bookmark = $this->privateLinkDB->get(43); |
218 | $this->assertEquals(43, $bookmark->getId()); | 224 | $this->assertEquals(43, $bookmark->getId()); |
@@ -242,7 +248,7 @@ class BookmarkFileServiceTest extends TestCase | |||
242 | $this->assertEquals(43, $bookmark->getId()); | 248 | $this->assertEquals(43, $bookmark->getId()); |
243 | 249 | ||
244 | // reload from file | 250 | // reload from file |
245 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 251 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
246 | 252 | ||
247 | $this->privateLinkDB->get(43); | 253 | $this->privateLinkDB->get(43); |
248 | } | 254 | } |
@@ -259,17 +265,6 @@ class BookmarkFileServiceTest extends TestCase | |||
259 | } | 265 | } |
260 | 266 | ||
261 | /** | 267 | /** |
262 | * Test add() method with an entry which is not a bookmark instance | ||
263 | */ | ||
264 | public function testAddNotABookmark() | ||
265 | { | ||
266 | $this->expectException(\Exception::class); | ||
267 | $this->expectExceptionMessage('Provided data is invalid'); | ||
268 | |||
269 | $this->privateLinkDB->add(['title' => 'hi!']); | ||
270 | } | ||
271 | |||
272 | /** | ||
273 | * Test add() method with a Bookmark already containing an ID | 268 | * Test add() method with a Bookmark already containing an ID |
274 | */ | 269 | */ |
275 | public function testAddWithId() | 270 | public function testAddWithId() |
@@ -314,7 +309,7 @@ class BookmarkFileServiceTest extends TestCase | |||
314 | $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); | 309 | $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); |
315 | 310 | ||
316 | // reload from file | 311 | // reload from file |
317 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 312 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
318 | 313 | ||
319 | $bookmark = $this->privateLinkDB->get(42); | 314 | $bookmark = $this->privateLinkDB->get(42); |
320 | $this->assertEquals(42, $bookmark->getId()); | 315 | $this->assertEquals(42, $bookmark->getId()); |
@@ -355,7 +350,7 @@ class BookmarkFileServiceTest extends TestCase | |||
355 | $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); | 350 | $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); |
356 | 351 | ||
357 | // reload from file | 352 | // reload from file |
358 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 353 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
359 | 354 | ||
360 | $bookmark = $this->privateLinkDB->get(42); | 355 | $bookmark = $this->privateLinkDB->get(42); |
361 | $this->assertEquals(42, $bookmark->getId()); | 356 | $this->assertEquals(42, $bookmark->getId()); |
@@ -388,7 +383,7 @@ class BookmarkFileServiceTest extends TestCase | |||
388 | $this->assertEquals($title, $bookmark->getTitle()); | 383 | $this->assertEquals($title, $bookmark->getTitle()); |
389 | 384 | ||
390 | // reload from file | 385 | // reload from file |
391 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 386 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
392 | 387 | ||
393 | $bookmark = $this->privateLinkDB->get(42); | 388 | $bookmark = $this->privateLinkDB->get(42); |
394 | $this->assertEquals(42, $bookmark->getId()); | 389 | $this->assertEquals(42, $bookmark->getId()); |
@@ -407,17 +402,6 @@ class BookmarkFileServiceTest extends TestCase | |||
407 | } | 402 | } |
408 | 403 | ||
409 | /** | 404 | /** |
410 | * Test set() method with an entry which is not a bookmark instance | ||
411 | */ | ||
412 | public function testSetNotABookmark() | ||
413 | { | ||
414 | $this->expectException(\Exception::class); | ||
415 | $this->expectExceptionMessage('Provided data is invalid'); | ||
416 | |||
417 | $this->privateLinkDB->set(['title' => 'hi!']); | ||
418 | } | ||
419 | |||
420 | /** | ||
421 | * Test set() method with a Bookmark without an ID defined. | 405 | * Test set() method with a Bookmark without an ID defined. |
422 | */ | 406 | */ |
423 | public function testSetWithoutId() | 407 | public function testSetWithoutId() |
@@ -452,7 +436,7 @@ class BookmarkFileServiceTest extends TestCase | |||
452 | $this->assertEquals(43, $bookmark->getId()); | 436 | $this->assertEquals(43, $bookmark->getId()); |
453 | 437 | ||
454 | // reload from file | 438 | // reload from file |
455 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 439 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
456 | 440 | ||
457 | $bookmark = $this->privateLinkDB->get(43); | 441 | $bookmark = $this->privateLinkDB->get(43); |
458 | $this->assertEquals(43, $bookmark->getId()); | 442 | $this->assertEquals(43, $bookmark->getId()); |
@@ -472,7 +456,7 @@ class BookmarkFileServiceTest extends TestCase | |||
472 | $this->assertEquals($title, $bookmark->getTitle()); | 456 | $this->assertEquals($title, $bookmark->getTitle()); |
473 | 457 | ||
474 | // reload from file | 458 | // reload from file |
475 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 459 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
476 | 460 | ||
477 | $bookmark = $this->privateLinkDB->get(42); | 461 | $bookmark = $this->privateLinkDB->get(42); |
478 | $this->assertEquals(42, $bookmark->getId()); | 462 | $this->assertEquals(42, $bookmark->getId()); |
@@ -491,17 +475,6 @@ class BookmarkFileServiceTest extends TestCase | |||
491 | } | 475 | } |
492 | 476 | ||
493 | /** | 477 | /** |
494 | * Test addOrSet() method with an entry which is not a bookmark instance | ||
495 | */ | ||
496 | public function testAddOrSetNotABookmark() | ||
497 | { | ||
498 | $this->expectException(\Exception::class); | ||
499 | $this->expectExceptionMessage('Provided data is invalid'); | ||
500 | |||
501 | $this->privateLinkDB->addOrSet(['title' => 'hi!']); | ||
502 | } | ||
503 | |||
504 | /** | ||
505 | * Test addOrSet() method for a bookmark without any field set and without writing the data store | 478 | * Test addOrSet() method for a bookmark without any field set and without writing the data store |
506 | */ | 479 | */ |
507 | public function testAddOrSetMinimalNoWrite() | 480 | public function testAddOrSetMinimalNoWrite() |
@@ -515,7 +488,7 @@ class BookmarkFileServiceTest extends TestCase | |||
515 | $this->assertEquals($title, $bookmark->getTitle()); | 488 | $this->assertEquals($title, $bookmark->getTitle()); |
516 | 489 | ||
517 | // reload from file | 490 | // reload from file |
518 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 491 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
519 | 492 | ||
520 | $bookmark = $this->privateLinkDB->get(42); | 493 | $bookmark = $this->privateLinkDB->get(42); |
521 | $this->assertEquals(42, $bookmark->getId()); | 494 | $this->assertEquals(42, $bookmark->getId()); |
@@ -541,7 +514,7 @@ class BookmarkFileServiceTest extends TestCase | |||
541 | $this->assertInstanceOf(BookmarkNotFoundException::class, $exception); | 514 | $this->assertInstanceOf(BookmarkNotFoundException::class, $exception); |
542 | 515 | ||
543 | // reload from file | 516 | // reload from file |
544 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); | 517 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
545 | 518 | ||
546 | $this->privateLinkDB->get(42); | 519 | $this->privateLinkDB->get(42); |
547 | } | 520 | } |
@@ -559,17 +532,6 @@ class BookmarkFileServiceTest extends TestCase | |||
559 | } | 532 | } |
560 | 533 | ||
561 | /** | 534 | /** |
562 | * Test remove() method with an entry which is not a bookmark instance | ||
563 | */ | ||
564 | public function testRemoveNotABookmark() | ||
565 | { | ||
566 | $this->expectException(\Exception::class); | ||
567 | $this->expectExceptionMessage('Provided data is invalid'); | ||
568 | |||
569 | $this->privateLinkDB->remove(['title' => 'hi!']); | ||
570 | } | ||
571 | |||
572 | /** | ||
573 | * Test remove() method with a Bookmark with an unknown ID | 535 | * Test remove() method with a Bookmark with an unknown ID |
574 | */ | 536 | */ |
575 | public function testRemoveWithUnknownId() | 537 | public function testRemoveWithUnknownId() |
@@ -645,7 +607,7 @@ class BookmarkFileServiceTest extends TestCase | |||
645 | 607 | ||
646 | $conf = new ConfigManager('tests/utils/config/configJson'); | 608 | $conf = new ConfigManager('tests/utils/config/configJson'); |
647 | $conf->set('resource.datastore', 'null/store.db'); | 609 | $conf->set('resource.datastore', 'null/store.db'); |
648 | new BookmarkFileService($conf, $this->history, true); | 610 | new BookmarkFileService($conf, $this->history, $this->mutex, true); |
649 | } | 611 | } |
650 | 612 | ||
651 | /** | 613 | /** |
@@ -655,7 +617,7 @@ class BookmarkFileServiceTest extends TestCase | |||
655 | { | 617 | { |
656 | unlink(self::$testDatastore); | 618 | unlink(self::$testDatastore); |
657 | $this->assertFileNotExists(self::$testDatastore); | 619 | $this->assertFileNotExists(self::$testDatastore); |
658 | new BookmarkFileService($this->conf, $this->history, true); | 620 | new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
659 | $this->assertFileExists(self::$testDatastore); | 621 | $this->assertFileExists(self::$testDatastore); |
660 | 622 | ||
661 | // ensure the correct data has been written | 623 | // ensure the correct data has been written |
@@ -669,7 +631,7 @@ class BookmarkFileServiceTest extends TestCase | |||
669 | { | 631 | { |
670 | unlink(self::$testDatastore); | 632 | unlink(self::$testDatastore); |
671 | $this->assertFileNotExists(self::$testDatastore); | 633 | $this->assertFileNotExists(self::$testDatastore); |
672 | $db = new \FakeBookmarkService($this->conf, $this->history, false); | 634 | $db = new \FakeBookmarkService($this->conf, $this->history, $this->mutex, false); |
673 | $this->assertFileNotExists(self::$testDatastore); | 635 | $this->assertFileNotExists(self::$testDatastore); |
674 | $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); | 636 | $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); |
675 | $this->assertCount(0, $db->getBookmarks()); | 637 | $this->assertCount(0, $db->getBookmarks()); |
@@ -702,13 +664,13 @@ class BookmarkFileServiceTest extends TestCase | |||
702 | */ | 664 | */ |
703 | public function testSave() | 665 | public function testSave() |
704 | { | 666 | { |
705 | $testDB = new BookmarkFileService($this->conf, $this->history, true); | 667 | $testDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
706 | $dbSize = $testDB->count(); | 668 | $dbSize = $testDB->count(); |
707 | 669 | ||
708 | $bookmark = new Bookmark(); | 670 | $bookmark = new Bookmark(); |
709 | $testDB->add($bookmark); | 671 | $testDB->add($bookmark); |
710 | 672 | ||
711 | $testDB = new BookmarkFileService($this->conf, $this->history, true); | 673 | $testDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
712 | $this->assertEquals($dbSize + 1, $testDB->count()); | 674 | $this->assertEquals($dbSize + 1, $testDB->count()); |
713 | } | 675 | } |
714 | 676 | ||
@@ -718,28 +680,12 @@ class BookmarkFileServiceTest extends TestCase | |||
718 | public function testCountHiddenPublic() | 680 | public function testCountHiddenPublic() |
719 | { | 681 | { |
720 | $this->conf->set('privacy.hide_public_links', true); | 682 | $this->conf->set('privacy.hide_public_links', true); |
721 | $linkDB = new BookmarkFileService($this->conf, $this->history, false); | 683 | $linkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); |
722 | 684 | ||
723 | $this->assertEquals(0, $linkDB->count()); | 685 | $this->assertEquals(0, $linkDB->count()); |
724 | } | 686 | } |
725 | 687 | ||
726 | /** | 688 | /** |
727 | * List the days for which bookmarks have been posted | ||
728 | */ | ||
729 | public function testDays() | ||
730 | { | ||
731 | $this->assertEquals( | ||
732 | ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'], | ||
733 | $this->publicLinkDB->days() | ||
734 | ); | ||
735 | |||
736 | $this->assertEquals( | ||
737 | ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'], | ||
738 | $this->privateLinkDB->days() | ||
739 | ); | ||
740 | } | ||
741 | |||
742 | /** | ||
743 | * The URL corresponds to an existing entry in the DB | 689 | * The URL corresponds to an existing entry in the DB |
744 | */ | 690 | */ |
745 | public function testGetKnownLinkFromURL() | 691 | public function testGetKnownLinkFromURL() |
@@ -786,6 +732,10 @@ class BookmarkFileServiceTest extends TestCase | |||
786 | // They need to be grouped with the first case found - order by date DESC: `sTuff`. | 732 | // They need to be grouped with the first case found - order by date DESC: `sTuff`. |
787 | 'sTuff' => 2, | 733 | 'sTuff' => 2, |
788 | 'ut' => 1, | 734 | 'ut' => 1, |
735 | 'assurance' => 1, | ||
736 | 'coding-style' => 1, | ||
737 | 'quality' => 1, | ||
738 | 'standards' => 1, | ||
789 | ], | 739 | ], |
790 | $this->publicLinkDB->bookmarksCountPerTag() | 740 | $this->publicLinkDB->bookmarksCountPerTag() |
791 | ); | 741 | ); |
@@ -814,6 +764,10 @@ class BookmarkFileServiceTest extends TestCase | |||
814 | 'tag3' => 1, | 764 | 'tag3' => 1, |
815 | 'tag4' => 1, | 765 | 'tag4' => 1, |
816 | 'ut' => 1, | 766 | 'ut' => 1, |
767 | 'assurance' => 1, | ||
768 | 'coding-style' => 1, | ||
769 | 'quality' => 1, | ||
770 | 'standards' => 1, | ||
817 | ], | 771 | ], |
818 | $this->privateLinkDB->bookmarksCountPerTag() | 772 | $this->privateLinkDB->bookmarksCountPerTag() |
819 | ); | 773 | ); |
@@ -928,6 +882,37 @@ class BookmarkFileServiceTest extends TestCase | |||
928 | } | 882 | } |
929 | 883 | ||
930 | /** | 884 | /** |
885 | * Test filterHash() on a private bookmark while logged out. | ||
886 | */ | ||
887 | public function testFilterHashPrivateWhileLoggedOut() | ||
888 | { | ||
889 | $this->expectException(BookmarkNotFoundException::class); | ||
890 | $this->expectExceptionMessage('The link you are trying to reach does not exist or has been deleted'); | ||
891 | |||
892 | $hash = smallHash('20141125_084734' . 6); | ||
893 | |||
894 | $this->publicLinkDB->findByHash($hash); | ||
895 | } | ||
896 | |||
897 | /** | ||
898 | * Test filterHash() with private key. | ||
899 | */ | ||
900 | public function testFilterHashWithPrivateKey() | ||
901 | { | ||
902 | $hash = smallHash('20141125_084734' . 6); | ||
903 | $privateKey = 'this is usually auto generated'; | ||
904 | |||
905 | $bookmark = $this->privateLinkDB->findByHash($hash); | ||
906 | $bookmark->addAdditionalContentEntry('private_key', $privateKey); | ||
907 | $this->privateLinkDB->save(); | ||
908 | |||
909 | $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); | ||
910 | $bookmark = $this->privateLinkDB->findByHash($hash, $privateKey); | ||
911 | |||
912 | static::assertSame(6, $bookmark->getId()); | ||
913 | } | ||
914 | |||
915 | /** | ||
931 | * Test linksCountPerTag all tags without filter. | 916 | * Test linksCountPerTag all tags without filter. |
932 | * Equal occurrences should be sorted alphabetically. | 917 | * Equal occurrences should be sorted alphabetically. |
933 | */ | 918 | */ |
@@ -956,6 +941,10 @@ class BookmarkFileServiceTest extends TestCase | |||
956 | 'tag4' => 1, | 941 | 'tag4' => 1, |
957 | 'ut' => 1, | 942 | 'ut' => 1, |
958 | 'w3c' => 1, | 943 | 'w3c' => 1, |
944 | 'assurance' => 1, | ||
945 | 'coding-style' => 1, | ||
946 | 'quality' => 1, | ||
947 | 'standards' => 1, | ||
959 | ]; | 948 | ]; |
960 | $tags = $this->privateLinkDB->bookmarksCountPerTag(); | 949 | $tags = $this->privateLinkDB->bookmarksCountPerTag(); |
961 | 950 | ||
@@ -1054,6 +1043,10 @@ class BookmarkFileServiceTest extends TestCase | |||
1054 | 'stallman' => 1, | 1043 | 'stallman' => 1, |
1055 | 'ut' => 1, | 1044 | 'ut' => 1, |
1056 | 'w3c' => 1, | 1045 | 'w3c' => 1, |
1046 | 'assurance' => 1, | ||
1047 | 'coding-style' => 1, | ||
1048 | 'quality' => 1, | ||
1049 | 'standards' => 1, | ||
1057 | ]; | 1050 | ]; |
1058 | $bookmark = new Bookmark(); | 1051 | $bookmark = new Bookmark(); |
1059 | $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]); | 1052 | $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]); |
@@ -1065,33 +1058,105 @@ class BookmarkFileServiceTest extends TestCase | |||
1065 | } | 1058 | } |
1066 | 1059 | ||
1067 | /** | 1060 | /** |
1068 | * Test filterDay while logged in | 1061 | * Test find by dates in the middle of the datastore (sorted by dates) with a single bookmark as a result. |
1069 | */ | 1062 | */ |
1070 | public function testFilterDayLoggedIn(): void | 1063 | public function testFilterByDateMidTimePeriodSingleBookmark(): void |
1071 | { | 1064 | { |
1072 | $bookmarks = $this->privateLinkDB->filterDay('20121206'); | 1065 | $bookmarks = $this->privateLinkDB->findByDate( |
1073 | $expectedIds = [4, 9, 1, 0]; | 1066 | DateTime::createFromFormat('Ymd_His', '20121206_150000'), |
1067 | DateTime::createFromFormat('Ymd_His', '20121206_160000'), | ||
1068 | $before, | ||
1069 | $after | ||
1070 | ); | ||
1074 | 1071 | ||
1075 | static::assertCount(4, $bookmarks); | 1072 | static::assertCount(1, $bookmarks); |
1076 | foreach ($bookmarks as $bookmark) { | 1073 | |
1077 | $i = ($i ?? -1) + 1; | 1074 | static::assertSame(9, $bookmarks[0]->getId()); |
1078 | static::assertSame($expectedIds[$i], $bookmark->getId()); | 1075 | static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before); |
1079 | } | 1076 | static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after); |
1080 | } | 1077 | } |
1081 | 1078 | ||
1082 | /** | 1079 | /** |
1083 | * Test filterDay while logged out | 1080 | * Test find by dates in the middle of the datastore (sorted by dates) with a multiple bookmarks as a result. |
1084 | */ | 1081 | */ |
1085 | public function testFilterDayLoggedOut(): void | 1082 | public function testFilterByDateMidTimePeriodMultipleBookmarks(): void |
1086 | { | 1083 | { |
1087 | $bookmarks = $this->publicLinkDB->filterDay('20121206'); | 1084 | $bookmarks = $this->privateLinkDB->findByDate( |
1088 | $expectedIds = [4, 9, 1]; | 1085 | DateTime::createFromFormat('Ymd_His', '20121206_150000'), |
1086 | DateTime::createFromFormat('Ymd_His', '20121206_180000'), | ||
1087 | $before, | ||
1088 | $after | ||
1089 | ); | ||
1089 | 1090 | ||
1090 | static::assertCount(3, $bookmarks); | 1091 | static::assertCount(2, $bookmarks); |
1091 | foreach ($bookmarks as $bookmark) { | 1092 | |
1092 | $i = ($i ?? -1) + 1; | 1093 | static::assertSame(1, $bookmarks[0]->getId()); |
1093 | static::assertSame($expectedIds[$i], $bookmark->getId()); | 1094 | static::assertSame(9, $bookmarks[1]->getId()); |
1094 | } | 1095 | static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before); |
1096 | static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_182539'), $after); | ||
1097 | } | ||
1098 | |||
1099 | /** | ||
1100 | * Test find by dates at the end of the datastore (sorted by dates). | ||
1101 | */ | ||
1102 | public function testFilterByDateLastTimePeriod(): void | ||
1103 | { | ||
1104 | $after = new DateTime(); | ||
1105 | $bookmarks = $this->privateLinkDB->findByDate( | ||
1106 | DateTime::createFromFormat('Ymd_His', '20150310_114640'), | ||
1107 | DateTime::createFromFormat('Ymd_His', '20450101_010101'), | ||
1108 | $before, | ||
1109 | $after | ||
1110 | ); | ||
1111 | |||
1112 | static::assertCount(1, $bookmarks); | ||
1113 | |||
1114 | static::assertSame(41, $bookmarks[0]->getId()); | ||
1115 | static::assertEquals(DateTime::createFromFormat('Ymd_His', '20150310_114633'), $before); | ||
1116 | static::assertNull($after); | ||
1117 | } | ||
1118 | |||
1119 | /** | ||
1120 | * Test find by dates at the beginning of the datastore (sorted by dates). | ||
1121 | */ | ||
1122 | public function testFilterByDateFirstTimePeriod(): void | ||
1123 | { | ||
1124 | $before = new DateTime(); | ||
1125 | $bookmarks = $this->privateLinkDB->findByDate( | ||
1126 | DateTime::createFromFormat('Ymd_His', '20000101_101010'), | ||
1127 | DateTime::createFromFormat('Ymd_His', '20100309_110000'), | ||
1128 | $before, | ||
1129 | $after | ||
1130 | ); | ||
1131 | |||
1132 | static::assertCount(1, $bookmarks); | ||
1133 | |||
1134 | static::assertSame(11, $bookmarks[0]->getId()); | ||
1135 | static::assertNull($before); | ||
1136 | static::assertEquals(DateTime::createFromFormat('Ymd_His', '20100310_101010'), $after); | ||
1137 | } | ||
1138 | |||
1139 | /** | ||
1140 | * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead. | ||
1141 | */ | ||
1142 | public function testGetLatestWithSticky(): void | ||
1143 | { | ||
1144 | $bookmark = $this->publicLinkDB->getLatest(); | ||
1145 | |||
1146 | static::assertSame(41, $bookmark->getId()); | ||
1147 | } | ||
1148 | |||
1149 | /** | ||
1150 | * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead. | ||
1151 | */ | ||
1152 | public function testGetLatestEmptyDatastore(): void | ||
1153 | { | ||
1154 | unlink($this->conf->get('resource.datastore')); | ||
1155 | $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); | ||
1156 | |||
1157 | $bookmark = $this->publicLinkDB->getLatest(); | ||
1158 | |||
1159 | static::assertNull($bookmark); | ||
1095 | } | 1160 | } |
1096 | 1161 | ||
1097 | /** | 1162 | /** |
diff --git a/tests/bookmark/BookmarkFilterTest.php b/tests/bookmark/BookmarkFilterTest.php index 48c7f824..835674f2 100644 --- a/tests/bookmark/BookmarkFilterTest.php +++ b/tests/bookmark/BookmarkFilterTest.php | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Bookmark; | 3 | namespace Shaarli\Bookmark; |
4 | 4 | ||
5 | use Exception; | 5 | use malkusch\lock\mutex\NoMutex; |
6 | use ReferenceLinkDB; | 6 | use ReferenceLinkDB; |
7 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
8 | use Shaarli\History; | 8 | use Shaarli\History; |
@@ -37,13 +37,14 @@ class BookmarkFilterTest extends TestCase | |||
37 | */ | 37 | */ |
38 | public static function setUpBeforeClass(): void | 38 | public static function setUpBeforeClass(): void |
39 | { | 39 | { |
40 | $mutex = new NoMutex(); | ||
40 | $conf = new ConfigManager('tests/utils/config/configJson'); | 41 | $conf = new ConfigManager('tests/utils/config/configJson'); |
41 | $conf->set('resource.datastore', self::$testDatastore); | 42 | $conf->set('resource.datastore', self::$testDatastore); |
42 | self::$refDB = new \ReferenceLinkDB(); | 43 | self::$refDB = new \ReferenceLinkDB(); |
43 | self::$refDB->write(self::$testDatastore); | 44 | self::$refDB->write(self::$testDatastore); |
44 | $history = new History('sandbox/history.php'); | 45 | $history = new History('sandbox/history.php'); |
45 | self::$bookmarkService = new \FakeBookmarkService($conf, $history, true); | 46 | self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true); |
46 | self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks()); | 47 | self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks(), $conf); |
47 | } | 48 | } |
48 | 49 | ||
49 | /** | 50 | /** |
@@ -523,4 +524,43 @@ class BookmarkFilterTest extends TestCase | |||
523 | )) | 524 | )) |
524 | ); | 525 | ); |
525 | } | 526 | } |
527 | |||
528 | /** | ||
529 | * Test search result highlights in every field of bookmark reference #9. | ||
530 | */ | ||
531 | public function testFullTextSearchHighlight(): void | ||
532 | { | ||
533 | $bookmarks = self::$linkFilter->filter( | ||
534 | BookmarkFilter::$FILTER_TEXT, | ||
535 | '"psr-2" coding guide http fig "psr-2/" "This guide" basic standard. coding-style quality assurance' | ||
536 | ); | ||
537 | |||
538 | static::assertCount(1, $bookmarks); | ||
539 | static::assertArrayHasKey(9, $bookmarks); | ||
540 | |||
541 | $bookmark = $bookmarks[9]; | ||
542 | $expectedHighlights = [ | ||
543 | 'title' => [ | ||
544 | ['start' => 0, 'end' => 5], // "psr-2" | ||
545 | ['start' => 7, 'end' => 13], // coding | ||
546 | ['start' => 20, 'end' => 25], // guide | ||
547 | ], | ||
548 | 'description' => [ | ||
549 | ['start' => 0, 'end' => 10], // "This guide" | ||
550 | ['start' => 45, 'end' => 50], // basic | ||
551 | ['start' => 58, 'end' => 67], // standard. | ||
552 | ], | ||
553 | 'url' => [ | ||
554 | ['start' => 0, 'end' => 4], // http | ||
555 | ['start' => 15, 'end' => 18], // fig | ||
556 | ['start' => 27, 'end' => 33], // "psr-2/" | ||
557 | ], | ||
558 | 'tags' => [ | ||
559 | ['start' => 0, 'end' => 12], // coding-style | ||
560 | ['start' => 23, 'end' => 30], // quality | ||
561 | ['start' => 31, 'end' => 40], // assurance | ||
562 | ], | ||
563 | ]; | ||
564 | static::assertSame($expectedHighlights, $bookmark->getAdditionalContentEntry('search_highlight')); | ||
565 | } | ||
526 | } | 566 | } |
diff --git a/tests/bookmark/BookmarkInitializerTest.php b/tests/bookmark/BookmarkInitializerTest.php index 25704004..0c8420ce 100644 --- a/tests/bookmark/BookmarkInitializerTest.php +++ b/tests/bookmark/BookmarkInitializerTest.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Bookmark; | 3 | namespace Shaarli\Bookmark; |
4 | 4 | ||
5 | use malkusch\lock\mutex\NoMutex; | ||
5 | use Shaarli\Config\ConfigManager; | 6 | use Shaarli\Config\ConfigManager; |
6 | use Shaarli\History; | 7 | use Shaarli\History; |
7 | use Shaarli\TestCase; | 8 | use Shaarli\TestCase; |
@@ -34,11 +35,15 @@ class BookmarkInitializerTest extends TestCase | |||
34 | /** @var BookmarkInitializer instance */ | 35 | /** @var BookmarkInitializer instance */ |
35 | protected $initializer; | 36 | protected $initializer; |
36 | 37 | ||
38 | /** @var NoMutex */ | ||
39 | protected $mutex; | ||
40 | |||
37 | /** | 41 | /** |
38 | * Initialize an empty BookmarkFileService | 42 | * Initialize an empty BookmarkFileService |
39 | */ | 43 | */ |
40 | public function setUp(): void | 44 | public function setUp(): void |
41 | { | 45 | { |
46 | $this->mutex = new NoMutex(); | ||
42 | if (file_exists(self::$testDatastore)) { | 47 | if (file_exists(self::$testDatastore)) { |
43 | unlink(self::$testDatastore); | 48 | unlink(self::$testDatastore); |
44 | } | 49 | } |
@@ -47,7 +52,7 @@ class BookmarkInitializerTest extends TestCase | |||
47 | $this->conf = new ConfigManager(self::$testConf); | 52 | $this->conf = new ConfigManager(self::$testConf); |
48 | $this->conf->set('resource.datastore', self::$testDatastore); | 53 | $this->conf->set('resource.datastore', self::$testDatastore); |
49 | $this->history = new History('sandbox/history.php'); | 54 | $this->history = new History('sandbox/history.php'); |
50 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 55 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
51 | 56 | ||
52 | $this->initializer = new BookmarkInitializer($this->bookmarkService); | 57 | $this->initializer = new BookmarkInitializer($this->bookmarkService); |
53 | } | 58 | } |
@@ -59,7 +64,7 @@ class BookmarkInitializerTest extends TestCase | |||
59 | { | 64 | { |
60 | $refDB = new \ReferenceLinkDB(); | 65 | $refDB = new \ReferenceLinkDB(); |
61 | $refDB->write(self::$testDatastore); | 66 | $refDB->write(self::$testDatastore); |
62 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 67 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
63 | $this->initializer = new BookmarkInitializer($this->bookmarkService); | 68 | $this->initializer = new BookmarkInitializer($this->bookmarkService); |
64 | 69 | ||
65 | $this->initializer->initialize(); | 70 | $this->initializer->initialize(); |
@@ -90,7 +95,7 @@ class BookmarkInitializerTest extends TestCase | |||
90 | $this->bookmarkService->save(); | 95 | $this->bookmarkService->save(); |
91 | 96 | ||
92 | // Reload from file | 97 | // Reload from file |
93 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 98 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
94 | $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count()); | 99 | $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count()); |
95 | 100 | ||
96 | $bookmark = $this->bookmarkService->get(43); | 101 | $bookmark = $this->bookmarkService->get(43); |
@@ -121,7 +126,7 @@ class BookmarkInitializerTest extends TestCase | |||
121 | public function testInitializeNonExistentDataStore(): void | 126 | public function testInitializeNonExistentDataStore(): void |
122 | { | 127 | { |
123 | $this->conf->set('resource.datastore', static::$testDatastore . '_empty'); | 128 | $this->conf->set('resource.datastore', static::$testDatastore . '_empty'); |
124 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 129 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); |
125 | 130 | ||
126 | $this->initializer->initialize(); | 131 | $this->initializer->initialize(); |
127 | 132 | ||
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php index afec2440..cb91b26b 100644 --- a/tests/bookmark/BookmarkTest.php +++ b/tests/bookmark/BookmarkTest.php | |||
@@ -79,6 +79,23 @@ class BookmarkTest extends TestCase | |||
79 | } | 79 | } |
80 | 80 | ||
81 | /** | 81 | /** |
82 | * Test fromArray() with a link with a custom tags separator | ||
83 | */ | ||
84 | public function testFromArrayCustomTagsSeparator() | ||
85 | { | ||
86 | $data = [ | ||
87 | 'id' => 1, | ||
88 | 'tags' => ['tag1', 'tag2', 'chair'], | ||
89 | ]; | ||
90 | |||
91 | $bookmark = (new Bookmark())->fromArray($data, '@'); | ||
92 | $this->assertEquals($data['id'], $bookmark->getId()); | ||
93 | $this->assertEquals($data['tags'], $bookmark->getTags()); | ||
94 | $this->assertEquals('tag1@tag2@chair', $bookmark->getTagsString('@')); | ||
95 | } | ||
96 | |||
97 | |||
98 | /** | ||
82 | * Test validate() with a valid minimal bookmark | 99 | * Test validate() with a valid minimal bookmark |
83 | */ | 100 | */ |
84 | public function testValidateValidFullBookmark() | 101 | public function testValidateValidFullBookmark() |
@@ -154,25 +171,6 @@ class BookmarkTest extends TestCase | |||
154 | } | 171 | } |
155 | 172 | ||
156 | /** | 173 | /** |
157 | * Test validate() with a a bookmark with a non integer ID. | ||
158 | */ | ||
159 | public function testValidateNotValidStringId() | ||
160 | { | ||
161 | $bookmark = new Bookmark(); | ||
162 | $bookmark->setId('str'); | ||
163 | $bookmark->setShortUrl('abc'); | ||
164 | $bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102')); | ||
165 | $exception = null; | ||
166 | try { | ||
167 | $bookmark->validate(); | ||
168 | } catch (InvalidBookmarkException $e) { | ||
169 | $exception = $e; | ||
170 | } | ||
171 | $this->assertNotNull($exception); | ||
172 | $this->assertContainsPolyfill('- ID: str'. PHP_EOL, $exception->getMessage()); | ||
173 | } | ||
174 | |||
175 | /** | ||
176 | * Test validate() with a a bookmark without short url. | 174 | * Test validate() with a a bookmark without short url. |
177 | */ | 175 | */ |
178 | public function testValidateNotValidNoShortUrl() | 176 | public function testValidateNotValidNoShortUrl() |
@@ -211,25 +209,6 @@ class BookmarkTest extends TestCase | |||
211 | } | 209 | } |
212 | 210 | ||
213 | /** | 211 | /** |
214 | * Test validate() with a a bookmark with a bad created datetime. | ||
215 | */ | ||
216 | public function testValidateNotValidBadCreated() | ||
217 | { | ||
218 | $bookmark = new Bookmark(); | ||
219 | $bookmark->setId(1); | ||
220 | $bookmark->setShortUrl('abc'); | ||
221 | $bookmark->setCreated('hi!'); | ||
222 | $exception = null; | ||
223 | try { | ||
224 | $bookmark->validate(); | ||
225 | } catch (InvalidBookmarkException $e) { | ||
226 | $exception = $e; | ||
227 | } | ||
228 | $this->assertNotNull($exception); | ||
229 | $this->assertContainsPolyfill('- Created: Not a DateTime object'. PHP_EOL, $exception->getMessage()); | ||
230 | } | ||
231 | |||
232 | /** | ||
233 | * Test setId() and make sure that default fields are generated. | 212 | * Test setId() and make sure that default fields are generated. |
234 | */ | 213 | */ |
235 | public function testSetIdEmptyGeneratedFields() | 214 | public function testSetIdEmptyGeneratedFields() |
@@ -290,7 +269,7 @@ class BookmarkTest extends TestCase | |||
290 | { | 269 | { |
291 | $bookmark = new Bookmark(); | 270 | $bookmark = new Bookmark(); |
292 | 271 | ||
293 | $str = 'tag1 tag2 tag3.tag3-2, tag4 , -tag5 '; | 272 | $str = 'tag1 tag2 tag3.tag3-2 tag4 -tag5 '; |
294 | $bookmark->setTagsString($str); | 273 | $bookmark->setTagsString($str); |
295 | $this->assertEquals( | 274 | $this->assertEquals( |
296 | [ | 275 | [ |
@@ -314,9 +293,9 @@ class BookmarkTest extends TestCase | |||
314 | $array = [ | 293 | $array = [ |
315 | 'tag1 ', | 294 | 'tag1 ', |
316 | ' tag2', | 295 | ' tag2', |
317 | 'tag3.tag3-2,', | 296 | 'tag3.tag3-2', |
318 | ', tag4', | 297 | ' tag4', |
319 | ', ', | 298 | ' ', |
320 | '-tag5 ', | 299 | '-tag5 ', |
321 | ]; | 300 | ]; |
322 | $bookmark->setTags($array); | 301 | $bookmark->setTags($array); |
@@ -385,4 +364,48 @@ class BookmarkTest extends TestCase | |||
385 | $bookmark->deleteTag('nope'); | 364 | $bookmark->deleteTag('nope'); |
386 | $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); | 365 | $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); |
387 | } | 366 | } |
367 | |||
368 | /** | ||
369 | * Test shouldUpdateThumbnail() with bookmarks needing an update. | ||
370 | */ | ||
371 | public function testShouldUpdateThumbnail(): void | ||
372 | { | ||
373 | $bookmark = (new Bookmark())->setUrl('http://domain.tld/with-image'); | ||
374 | |||
375 | static::assertTrue($bookmark->shouldUpdateThumbnail()); | ||
376 | |||
377 | $bookmark = (new Bookmark()) | ||
378 | ->setUrl('http://domain.tld/with-image') | ||
379 | ->setThumbnail('unknown file') | ||
380 | ; | ||
381 | |||
382 | static::assertTrue($bookmark->shouldUpdateThumbnail()); | ||
383 | } | ||
384 | |||
385 | /** | ||
386 | * Test shouldUpdateThumbnail() with bookmarks that should not update. | ||
387 | */ | ||
388 | public function testShouldNotUpdateThumbnail(): void | ||
389 | { | ||
390 | $bookmark = (new Bookmark()); | ||
391 | |||
392 | static::assertFalse($bookmark->shouldUpdateThumbnail()); | ||
393 | |||
394 | $bookmark = (new Bookmark()) | ||
395 | ->setUrl('ftp://domain.tld/other-protocol', ['ftp']) | ||
396 | ; | ||
397 | |||
398 | static::assertFalse($bookmark->shouldUpdateThumbnail()); | ||
399 | |||
400 | $bookmark = (new Bookmark()) | ||
401 | ->setUrl('http://domain.tld/with-image') | ||
402 | ->setThumbnail(__FILE__) | ||
403 | ; | ||
404 | |||
405 | static::assertFalse($bookmark->shouldUpdateThumbnail()); | ||
406 | |||
407 | $bookmark = (new Bookmark())->setUrl('/shaare/abcdef'); | ||
408 | |||
409 | static::assertFalse($bookmark->shouldUpdateThumbnail()); | ||
410 | } | ||
388 | } | 411 | } |
diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php index ef00b92f..ddab4e3c 100644 --- a/tests/bookmark/LinkUtilsTest.php +++ b/tests/bookmark/LinkUtilsTest.php | |||
@@ -94,8 +94,108 @@ class LinkUtilsTest extends TestCase | |||
94 | public function testHtmlExtractExistentNameTag() | 94 | public function testHtmlExtractExistentNameTag() |
95 | { | 95 | { |
96 | $description = 'Bob and Alice share cookies.'; | 96 | $description = 'Bob and Alice share cookies.'; |
97 | |||
98 | // Simple one line | ||
97 | $html = '<html><meta>stuff2</meta><meta name="description" content="' . $description . '"/></html>'; | 99 | $html = '<html><meta>stuff2</meta><meta name="description" content="' . $description . '"/></html>'; |
98 | $this->assertEquals($description, html_extract_tag('description', $html)); | 100 | $this->assertEquals($description, html_extract_tag('description', $html)); |
101 | |||
102 | // Simple OpenGraph | ||
103 | $html = '<meta property="og:description" content="' . $description . '">'; | ||
104 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
105 | |||
106 | // Simple reversed OpenGraph | ||
107 | $html = '<meta content="' . $description . '" property="og:description">'; | ||
108 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
109 | |||
110 | // ItemProp OpenGraph | ||
111 | $html = '<meta itemprop="og:description" content="' . $description . '">'; | ||
112 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
113 | |||
114 | // OpenGraph without quotes | ||
115 | $html = '<meta property=og:description content="' . $description . '">'; | ||
116 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
117 | |||
118 | // OpenGraph reversed without quotes | ||
119 | $html = '<meta content="' . $description . '" property=og:description>'; | ||
120 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
121 | |||
122 | // OpenGraph with noise | ||
123 | $html = '<meta tag1="content1" property="og:description" tag2="content2" content="' . | ||
124 | $description . '" tag3="content3">'; | ||
125 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
126 | |||
127 | // OpenGraph reversed with noise | ||
128 | $html = '<meta tag1="content1" content="' . $description . '" ' . | ||
129 | 'tag3="content3" tag2="content2" property="og:description">'; | ||
130 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
131 | |||
132 | // OpenGraph multiple properties start | ||
133 | $html = '<meta property="unrelated og:description" content="' . $description . '">'; | ||
134 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
135 | |||
136 | // OpenGraph multiple properties end | ||
137 | $html = '<meta property="og:description unrelated" content="' . $description . '">'; | ||
138 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
139 | |||
140 | // OpenGraph multiple properties both end | ||
141 | $html = '<meta property="og:unrelated1 og:description og:unrelated2" content="' . $description . '">'; | ||
142 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
143 | |||
144 | // OpenGraph multiple properties both end with noise | ||
145 | $html = '<meta tag1="content1" property="og:unrelated1 og:description og:unrelated2" '. | ||
146 | 'tag2="content2" content="' . $description . '" tag3="content3">'; | ||
147 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
148 | |||
149 | // OpenGraph reversed multiple properties start | ||
150 | $html = '<meta content="' . $description . '" property="unrelated og:description">'; | ||
151 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
152 | |||
153 | // OpenGraph reversed multiple properties end | ||
154 | $html = '<meta content="' . $description . '" property="og:description unrelated">'; | ||
155 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
156 | |||
157 | // OpenGraph reversed multiple properties both end | ||
158 | $html = '<meta content="' . $description . '" property="og:unrelated1 og:description og:unrelated2">'; | ||
159 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
160 | |||
161 | // OpenGraph reversed multiple properties both end with noise | ||
162 | $html = '<meta tag1="content1" content="' . $description . '" tag2="content2" '. | ||
163 | 'property="og:unrelated1 og:description og:unrelated2" tag3="content3">'; | ||
164 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
165 | |||
166 | // Suggestion from #1375 | ||
167 | $html = '<meta property="og:description" name="description" content="' . $description . '">'; | ||
168 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
169 | } | ||
170 | |||
171 | /** | ||
172 | * Test html_extract_tag() with double quoted content containing single quote, and the opposite. | ||
173 | */ | ||
174 | public function testHtmlExtractExistentNameTagWithMixedQuotes(): void | ||
175 | { | ||
176 | $description = 'Bob and Alice share M&M\'s.'; | ||
177 | |||
178 | $html = '<meta property="og:description" content="' . $description . '">'; | ||
179 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
180 | |||
181 | $html = '<meta tag1="content1" property="og:unrelated1 og:description og:unrelated2" '. | ||
182 | 'tag2="content2" content="' . $description . '" tag3="content3">'; | ||
183 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
184 | |||
185 | $html = '<meta property="og:description" name="description" content="' . $description . '">'; | ||
186 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
187 | |||
188 | $description = 'Bob and Alice share "cookies".'; | ||
189 | |||
190 | $html = '<meta property="og:description" content=\'' . $description . '\'>'; | ||
191 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
192 | |||
193 | $html = '<meta tag1="content1" property="og:unrelated1 og:description og:unrelated2" '. | ||
194 | 'tag2="content2" content=\'' . $description . '\' tag3="content3">'; | ||
195 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
196 | |||
197 | $html = '<meta property="og:description" name="description" content=\'' . $description . '\'>'; | ||
198 | $this->assertEquals($description, html_extract_tag('description', $html)); | ||
99 | } | 199 | } |
100 | 200 | ||
101 | /** | 201 | /** |
@@ -105,6 +205,25 @@ class LinkUtilsTest extends TestCase | |||
105 | { | 205 | { |
106 | $html = '<html><meta>stuff2</meta><meta name="image" content="img"/></html>'; | 206 | $html = '<html><meta>stuff2</meta><meta name="image" content="img"/></html>'; |
107 | $this->assertFalse(html_extract_tag('description', $html)); | 207 | $this->assertFalse(html_extract_tag('description', $html)); |
208 | |||
209 | // Partial meta tag | ||
210 | $html = '<meta content="Brief description">'; | ||
211 | $this->assertFalse(html_extract_tag('description', $html)); | ||
212 | |||
213 | $html = '<meta property="og:description">'; | ||
214 | $this->assertFalse(html_extract_tag('description', $html)); | ||
215 | |||
216 | $html = '<meta tag1="content1" property="og:description">'; | ||
217 | $this->assertFalse(html_extract_tag('description', $html)); | ||
218 | |||
219 | $html = '<meta property="og:description" tag1="content1">'; | ||
220 | $this->assertFalse(html_extract_tag('description', $html)); | ||
221 | |||
222 | $html = '<meta tag1="content1" content="Brief description">'; | ||
223 | $this->assertFalse(html_extract_tag('description', $html)); | ||
224 | |||
225 | $html = '<meta content="Brief description" tag1="content1">'; | ||
226 | $this->assertFalse(html_extract_tag('description', $html)); | ||
108 | } | 227 | } |
109 | 228 | ||
110 | /** | 229 | /** |
@@ -127,60 +246,93 @@ class LinkUtilsTest extends TestCase | |||
127 | } | 246 | } |
128 | 247 | ||
129 | /** | 248 | /** |
249 | * Test the header callback with valid value | ||
250 | */ | ||
251 | public function testCurlHeaderCallbackOk(): void | ||
252 | { | ||
253 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ok'); | ||
254 | $data = [ | ||
255 | 'HTTP/1.1 200 OK', | ||
256 | 'Server: GitHub.com', | ||
257 | 'Date: Sat, 28 Oct 2017 12:01:33 GMT', | ||
258 | 'Content-Type: text/html; charset=utf-8', | ||
259 | 'Status: 200 OK', | ||
260 | ]; | ||
261 | |||
262 | foreach ($data as $chunk) { | ||
263 | static::assertIsInt($callback(null, $chunk)); | ||
264 | } | ||
265 | |||
266 | static::assertSame('utf-8', $charset); | ||
267 | } | ||
268 | |||
269 | /** | ||
130 | * Test the download callback with valid value | 270 | * Test the download callback with valid value |
131 | */ | 271 | */ |
132 | public function testCurlDownloadCallbackOk() | 272 | public function testCurlDownloadCallbackOk(): void |
133 | { | 273 | { |
274 | $charset = 'utf-8'; | ||
134 | $callback = get_curl_download_callback( | 275 | $callback = get_curl_download_callback( |
135 | $charset, | 276 | $charset, |
136 | $title, | 277 | $title, |
137 | $desc, | 278 | $desc, |
138 | $keywords, | 279 | $keywords, |
139 | false, | 280 | false, |
140 | 'ut_curl_getinfo_ok' | 281 | ' ' |
141 | ); | 282 | ); |
283 | |||
142 | $data = [ | 284 | $data = [ |
143 | 'HTTP/1.1 200 OK', | 285 | 'th=device-width">' |
144 | 'Server: GitHub.com', | ||
145 | 'Date: Sat, 28 Oct 2017 12:01:33 GMT', | ||
146 | 'Content-Type: text/html; charset=utf-8', | ||
147 | 'Status: 200 OK', | ||
148 | 'end' => 'th=device-width">' | ||
149 | . '<title>Refactoring · GitHub</title>' | 286 | . '<title>Refactoring · GitHub</title>' |
150 | . '<link rel="search" type="application/opensea', | 287 | . '<link rel="search" type="application/opensea', |
151 | '<title>ignored</title>' | 288 | '<title>ignored</title>' |
152 | . '<meta name="description" content="desc" />' | 289 | . '<meta name="description" content="desc" />' |
153 | . '<meta name="keywords" content="key1,key2" />', | 290 | . '<meta name="keywords" content="key1,key2" />', |
154 | ]; | 291 | ]; |
155 | foreach ($data as $key => $line) { | 292 | |
156 | $ignore = null; | 293 | foreach ($data as $chunk) { |
157 | $expected = $key !== 'end' ? strlen($line) : false; | 294 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
158 | $this->assertEquals($expected, $callback($ignore, $line)); | ||
159 | if ($expected === false) { | ||
160 | break; | ||
161 | } | ||
162 | } | 295 | } |
163 | $this->assertEquals('utf-8', $charset); | 296 | |
164 | $this->assertEquals('Refactoring · GitHub', $title); | 297 | static::assertSame('utf-8', $charset); |
165 | $this->assertEmpty($desc); | 298 | static::assertSame('Refactoring · GitHub', $title); |
166 | $this->assertEmpty($keywords); | 299 | static::assertEmpty($desc); |
300 | static::assertEmpty($keywords); | ||
301 | } | ||
302 | |||
303 | /** | ||
304 | * Test the header callback with valid value | ||
305 | */ | ||
306 | public function testCurlHeaderCallbackNoCharset(): void | ||
307 | { | ||
308 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_no_charset'); | ||
309 | $data = [ | ||
310 | 'HTTP/1.1 200 OK', | ||
311 | ]; | ||
312 | |||
313 | foreach ($data as $chunk) { | ||
314 | static::assertSame(strlen($chunk), $callback(null, $chunk)); | ||
315 | } | ||
316 | |||
317 | static::assertFalse($charset); | ||
167 | } | 318 | } |
168 | 319 | ||
169 | /** | 320 | /** |
170 | * Test the download callback with valid values and no charset | 321 | * Test the download callback with valid values and no charset |
171 | */ | 322 | */ |
172 | public function testCurlDownloadCallbackOkNoCharset() | 323 | public function testCurlDownloadCallbackOkNoCharset(): void |
173 | { | 324 | { |
325 | $charset = null; | ||
174 | $callback = get_curl_download_callback( | 326 | $callback = get_curl_download_callback( |
175 | $charset, | 327 | $charset, |
176 | $title, | 328 | $title, |
177 | $desc, | 329 | $desc, |
178 | $keywords, | 330 | $keywords, |
179 | false, | 331 | false, |
180 | 'ut_curl_getinfo_no_charset' | 332 | ' ' |
181 | ); | 333 | ); |
334 | |||
182 | $data = [ | 335 | $data = [ |
183 | 'HTTP/1.1 200 OK', | ||
184 | 'end' => 'th=device-width">' | 336 | 'end' => 'th=device-width">' |
185 | . '<title>Refactoring · GitHub</title>' | 337 | . '<title>Refactoring · GitHub</title>' |
186 | . '<link rel="search" type="application/opensea', | 338 | . '<link rel="search" type="application/opensea', |
@@ -188,10 +340,11 @@ class LinkUtilsTest extends TestCase | |||
188 | . '<meta name="description" content="desc" />' | 340 | . '<meta name="description" content="desc" />' |
189 | . '<meta name="keywords" content="key1,key2" />', | 341 | . '<meta name="keywords" content="key1,key2" />', |
190 | ]; | 342 | ]; |
191 | foreach ($data as $key => $line) { | 343 | |
192 | $ignore = null; | 344 | foreach ($data as $chunk) { |
193 | $this->assertEquals(strlen($line), $callback($ignore, $line)); | 345 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
194 | } | 346 | } |
347 | |||
195 | $this->assertEmpty($charset); | 348 | $this->assertEmpty($charset); |
196 | $this->assertEquals('Refactoring · GitHub', $title); | 349 | $this->assertEquals('Refactoring · GitHub', $title); |
197 | $this->assertEmpty($desc); | 350 | $this->assertEmpty($desc); |
@@ -201,18 +354,19 @@ class LinkUtilsTest extends TestCase | |||
201 | /** | 354 | /** |
202 | * Test the download callback with valid values and no charset | 355 | * Test the download callback with valid values and no charset |
203 | */ | 356 | */ |
204 | public function testCurlDownloadCallbackOkHtmlCharset() | 357 | public function testCurlDownloadCallbackOkHtmlCharset(): void |
205 | { | 358 | { |
359 | $charset = null; | ||
206 | $callback = get_curl_download_callback( | 360 | $callback = get_curl_download_callback( |
207 | $charset, | 361 | $charset, |
208 | $title, | 362 | $title, |
209 | $desc, | 363 | $desc, |
210 | $keywords, | 364 | $keywords, |
211 | false, | 365 | false, |
212 | 'ut_curl_getinfo_no_charset' | 366 | ' ' |
213 | ); | 367 | ); |
368 | |||
214 | $data = [ | 369 | $data = [ |
215 | 'HTTP/1.1 200 OK', | ||
216 | '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />', | 370 | '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />', |
217 | 'end' => 'th=device-width">' | 371 | 'end' => 'th=device-width">' |
218 | . '<title>Refactoring · GitHub</title>' | 372 | . '<title>Refactoring · GitHub</title>' |
@@ -221,14 +375,10 @@ class LinkUtilsTest extends TestCase | |||
221 | . '<meta name="description" content="desc" />' | 375 | . '<meta name="description" content="desc" />' |
222 | . '<meta name="keywords" content="key1,key2" />', | 376 | . '<meta name="keywords" content="key1,key2" />', |
223 | ]; | 377 | ]; |
224 | foreach ($data as $key => $line) { | 378 | foreach ($data as $chunk) { |
225 | $ignore = null; | 379 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
226 | $expected = $key !== 'end' ? strlen($line) : false; | ||
227 | $this->assertEquals($expected, $callback($ignore, $line)); | ||
228 | if ($expected === false) { | ||
229 | break; | ||
230 | } | ||
231 | } | 380 | } |
381 | |||
232 | $this->assertEquals('utf-8', $charset); | 382 | $this->assertEquals('utf-8', $charset); |
233 | $this->assertEquals('Refactoring · GitHub', $title); | 383 | $this->assertEquals('Refactoring · GitHub', $title); |
234 | $this->assertEmpty($desc); | 384 | $this->assertEmpty($desc); |
@@ -238,25 +388,27 @@ class LinkUtilsTest extends TestCase | |||
238 | /** | 388 | /** |
239 | * Test the download callback with valid values and no title | 389 | * Test the download callback with valid values and no title |
240 | */ | 390 | */ |
241 | public function testCurlDownloadCallbackOkNoTitle() | 391 | public function testCurlDownloadCallbackOkNoTitle(): void |
242 | { | 392 | { |
393 | $charset = 'utf-8'; | ||
243 | $callback = get_curl_download_callback( | 394 | $callback = get_curl_download_callback( |
244 | $charset, | 395 | $charset, |
245 | $title, | 396 | $title, |
246 | $desc, | 397 | $desc, |
247 | $keywords, | 398 | $keywords, |
248 | false, | 399 | false, |
249 | 'ut_curl_getinfo_ok' | 400 | ' ' |
250 | ); | 401 | ); |
402 | |||
251 | $data = [ | 403 | $data = [ |
252 | 'HTTP/1.1 200 OK', | ||
253 | 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea', | 404 | 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea', |
254 | 'ignored', | 405 | 'ignored', |
255 | ]; | 406 | ]; |
256 | foreach ($data as $key => $line) { | 407 | |
257 | $ignore = null; | 408 | foreach ($data as $chunk) { |
258 | $this->assertEquals(strlen($line), $callback($ignore, $line)); | 409 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
259 | } | 410 | } |
411 | |||
260 | $this->assertEquals('utf-8', $charset); | 412 | $this->assertEquals('utf-8', $charset); |
261 | $this->assertEmpty($title); | 413 | $this->assertEmpty($title); |
262 | $this->assertEmpty($desc); | 414 | $this->assertEmpty($desc); |
@@ -264,81 +416,56 @@ class LinkUtilsTest extends TestCase | |||
264 | } | 416 | } |
265 | 417 | ||
266 | /** | 418 | /** |
267 | * Test the download callback with an invalid content type. | 419 | * Test the header callback with an invalid content type. |
268 | */ | 420 | */ |
269 | public function testCurlDownloadCallbackInvalidContentType() | 421 | public function testCurlHeaderCallbackInvalidContentType(): void |
270 | { | 422 | { |
271 | $callback = get_curl_download_callback( | 423 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ct_ko'); |
272 | $charset, | 424 | $data = [ |
273 | $title, | 425 | 'HTTP/1.1 200 OK', |
274 | $desc, | 426 | ]; |
275 | $keywords, | 427 | |
276 | false, | 428 | static::assertFalse($callback(null, $data[0])); |
277 | 'ut_curl_getinfo_ct_ko' | 429 | static::assertNull($charset); |
278 | ); | ||
279 | $ignore = null; | ||
280 | $this->assertFalse($callback($ignore, '')); | ||
281 | $this->assertEmpty($charset); | ||
282 | $this->assertEmpty($title); | ||
283 | } | 430 | } |
284 | 431 | ||
285 | /** | 432 | /** |
286 | * Test the download callback with an invalid response code. | 433 | * Test the header callback with an invalid response code. |
287 | */ | 434 | */ |
288 | public function testCurlDownloadCallbackInvalidResponseCode() | 435 | public function testCurlHeaderCallbackInvalidResponseCode(): void |
289 | { | 436 | { |
290 | $callback = $callback = get_curl_download_callback( | 437 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rc_ko'); |
291 | $charset, | 438 | |
292 | $title, | 439 | static::assertFalse($callback(null, '')); |
293 | $desc, | 440 | static::assertNull($charset); |
294 | $keywords, | ||
295 | false, | ||
296 | 'ut_curl_getinfo_rc_ko' | ||
297 | ); | ||
298 | $ignore = null; | ||
299 | $this->assertFalse($callback($ignore, '')); | ||
300 | $this->assertEmpty($charset); | ||
301 | $this->assertEmpty($title); | ||
302 | } | 441 | } |
303 | 442 | ||
304 | /** | 443 | /** |
305 | * Test the download callback with an invalid content type and response code. | 444 | * Test the header callback with an invalid content type and response code. |
306 | */ | 445 | */ |
307 | public function testCurlDownloadCallbackInvalidContentTypeAndResponseCode() | 446 | public function testCurlHeaderCallbackInvalidContentTypeAndResponseCode(): void |
308 | { | 447 | { |
309 | $callback = $callback = get_curl_download_callback( | 448 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rs_ct_ko'); |
310 | $charset, | 449 | |
311 | $title, | 450 | static::assertFalse($callback(null, '')); |
312 | $desc, | 451 | static::assertNull($charset); |
313 | $keywords, | ||
314 | false, | ||
315 | 'ut_curl_getinfo_rs_ct_ko' | ||
316 | ); | ||
317 | $ignore = null; | ||
318 | $this->assertFalse($callback($ignore, '')); | ||
319 | $this->assertEmpty($charset); | ||
320 | $this->assertEmpty($title); | ||
321 | } | 452 | } |
322 | 453 | ||
323 | /** | 454 | /** |
324 | * Test the download callback with valid value, and retrieve_description option enabled. | 455 | * Test the download callback with valid value, and retrieve_description option enabled. |
325 | */ | 456 | */ |
326 | public function testCurlDownloadCallbackOkWithDesc() | 457 | public function testCurlDownloadCallbackOkWithDesc(): void |
327 | { | 458 | { |
459 | $charset = 'utf-8'; | ||
328 | $callback = get_curl_download_callback( | 460 | $callback = get_curl_download_callback( |
329 | $charset, | 461 | $charset, |
330 | $title, | 462 | $title, |
331 | $desc, | 463 | $desc, |
332 | $keywords, | 464 | $keywords, |
333 | true, | 465 | true, |
334 | 'ut_curl_getinfo_ok' | 466 | ' ' |
335 | ); | 467 | ); |
336 | $data = [ | 468 | $data = [ |
337 | 'HTTP/1.1 200 OK', | ||
338 | 'Server: GitHub.com', | ||
339 | 'Date: Sat, 28 Oct 2017 12:01:33 GMT', | ||
340 | 'Content-Type: text/html; charset=utf-8', | ||
341 | 'Status: 200 OK', | ||
342 | 'th=device-width">' | 469 | 'th=device-width">' |
343 | . '<title>Refactoring · GitHub</title>' | 470 | . '<title>Refactoring · GitHub</title>' |
344 | . '<link rel="search" type="application/opensea', | 471 | . '<link rel="search" type="application/opensea', |
@@ -346,14 +473,11 @@ class LinkUtilsTest extends TestCase | |||
346 | . '<meta name="description" content="link desc" />' | 473 | . '<meta name="description" content="link desc" />' |
347 | . '<meta name="keywords" content="key1,key2" />', | 474 | . '<meta name="keywords" content="key1,key2" />', |
348 | ]; | 475 | ]; |
349 | foreach ($data as $key => $line) { | 476 | |
350 | $ignore = null; | 477 | foreach ($data as $chunk) { |
351 | $expected = $key !== 'end' ? strlen($line) : false; | 478 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
352 | $this->assertEquals($expected, $callback($ignore, $line)); | ||
353 | if ($expected === false) { | ||
354 | break; | ||
355 | } | ||
356 | } | 479 | } |
480 | |||
357 | $this->assertEquals('utf-8', $charset); | 481 | $this->assertEquals('utf-8', $charset); |
358 | $this->assertEquals('Refactoring · GitHub', $title); | 482 | $this->assertEquals('Refactoring · GitHub', $title); |
359 | $this->assertEquals('link desc', $desc); | 483 | $this->assertEquals('link desc', $desc); |
@@ -364,8 +488,9 @@ class LinkUtilsTest extends TestCase | |||
364 | * Test the download callback with valid value, and retrieve_description option enabled, | 488 | * Test the download callback with valid value, and retrieve_description option enabled, |
365 | * but no desc or keyword defined in the page. | 489 | * but no desc or keyword defined in the page. |
366 | */ | 490 | */ |
367 | public function testCurlDownloadCallbackOkWithDescNotFound() | 491 | public function testCurlDownloadCallbackOkWithDescNotFound(): void |
368 | { | 492 | { |
493 | $charset = 'utf-8'; | ||
369 | $callback = get_curl_download_callback( | 494 | $callback = get_curl_download_callback( |
370 | $charset, | 495 | $charset, |
371 | $title, | 496 | $title, |
@@ -375,24 +500,16 @@ class LinkUtilsTest extends TestCase | |||
375 | 'ut_curl_getinfo_ok' | 500 | 'ut_curl_getinfo_ok' |
376 | ); | 501 | ); |
377 | $data = [ | 502 | $data = [ |
378 | 'HTTP/1.1 200 OK', | ||
379 | 'Server: GitHub.com', | ||
380 | 'Date: Sat, 28 Oct 2017 12:01:33 GMT', | ||
381 | 'Content-Type: text/html; charset=utf-8', | ||
382 | 'Status: 200 OK', | ||
383 | 'th=device-width">' | 503 | 'th=device-width">' |
384 | . '<title>Refactoring · GitHub</title>' | 504 | . '<title>Refactoring · GitHub</title>' |
385 | . '<link rel="search" type="application/opensea', | 505 | . '<link rel="search" type="application/opensea', |
386 | 'end' => '<title>ignored</title>', | 506 | 'end' => '<title>ignored</title>', |
387 | ]; | 507 | ]; |
388 | foreach ($data as $key => $line) { | 508 | |
389 | $ignore = null; | 509 | foreach ($data as $chunk) { |
390 | $expected = $key !== 'end' ? strlen($line) : false; | 510 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
391 | $this->assertEquals($expected, $callback($ignore, $line)); | ||
392 | if ($expected === false) { | ||
393 | break; | ||
394 | } | ||
395 | } | 511 | } |
512 | |||
396 | $this->assertEquals('utf-8', $charset); | 513 | $this->assertEquals('utf-8', $charset); |
397 | $this->assertEquals('Refactoring · GitHub', $title); | 514 | $this->assertEquals('Refactoring · GitHub', $title); |
398 | $this->assertEmpty($desc); | 515 | $this->assertEmpty($desc); |
@@ -493,6 +610,115 @@ class LinkUtilsTest extends TestCase | |||
493 | } | 610 | } |
494 | 611 | ||
495 | /** | 612 | /** |
613 | * Test tags_str2array with whitespace separator. | ||
614 | */ | ||
615 | public function testTagsStr2ArrayWithSpaceSeparator(): void | ||
616 | { | ||
617 | $separator = ' '; | ||
618 | |||
619 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator)); | ||
620 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator)); | ||
621 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array(' tag1 tag2 tag3 ', $separator)); | ||
622 | static::assertSame(['tag1@', 'tag2,', '.tag3'], tags_str2array(' tag1@ tag2, .tag3 ', $separator)); | ||
623 | static::assertSame([], tags_str2array('', $separator)); | ||
624 | static::assertSame([], tags_str2array(' ', $separator)); | ||
625 | static::assertSame([], tags_str2array(null, $separator)); | ||
626 | } | ||
627 | |||
628 | /** | ||
629 | * Test tags_str2array with @ separator. | ||
630 | */ | ||
631 | public function testTagsStr2ArrayWithCharSeparator(): void | ||
632 | { | ||
633 | $separator = '@'; | ||
634 | |||
635 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@tag2@tag3', $separator)); | ||
636 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@@@@tag2@@@@tag3', $separator)); | ||
637 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('@@@tag1@@@tag2@@@@tag3@@', $separator)); | ||
638 | static::assertSame( | ||
639 | ['tag1#', 'tag2, and other', '.tag3'], | ||
640 | tags_str2array('@@@ tag1# @@@ tag2, and other @@@@.tag3@@', $separator) | ||
641 | ); | ||
642 | static::assertSame([], tags_str2array('', $separator)); | ||
643 | static::assertSame([], tags_str2array(' ', $separator)); | ||
644 | static::assertSame([], tags_str2array(null, $separator)); | ||
645 | } | ||
646 | |||
647 | /** | ||
648 | * Test tags_array2str with ' ' separator. | ||
649 | */ | ||
650 | public function testTagsArray2StrWithSpaceSeparator(): void | ||
651 | { | ||
652 | $separator = ' '; | ||
653 | |||
654 | static::assertSame('tag1 tag2 tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator)); | ||
655 | static::assertSame('tag1, tag2@ tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator)); | ||
656 | static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', 'tag2', 'tag3 '], $separator)); | ||
657 | static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator)); | ||
658 | static::assertSame('tag1', tags_array2str([' tag1 '], $separator)); | ||
659 | static::assertSame('', tags_array2str([' '], $separator)); | ||
660 | static::assertSame('', tags_array2str([], $separator)); | ||
661 | static::assertSame('', tags_array2str(null, $separator)); | ||
662 | } | ||
663 | |||
664 | /** | ||
665 | * Test tags_array2str with @ separator. | ||
666 | */ | ||
667 | public function testTagsArray2StrWithCharSeparator(): void | ||
668 | { | ||
669 | $separator = '@'; | ||
670 | |||
671 | static::assertSame('tag1@tag2@tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator)); | ||
672 | static::assertSame('tag1,@tag2@tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator)); | ||
673 | static::assertSame( | ||
674 | 'tag1@tag2, and other@tag3', | ||
675 | tags_array2str(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator) | ||
676 | ); | ||
677 | static::assertSame('tag1@tag2@tag3', tags_array2str(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator)); | ||
678 | static::assertSame('tag1', tags_array2str(['@@@@tag1@@@@'], $separator)); | ||
679 | static::assertSame('', tags_array2str(['@@@'], $separator)); | ||
680 | static::assertSame('', tags_array2str([], $separator)); | ||
681 | static::assertSame('', tags_array2str(null, $separator)); | ||
682 | } | ||
683 | |||
684 | /** | ||
685 | * Test tags_array2str with @ separator. | ||
686 | */ | ||
687 | public function testTagsFilterWithSpaceSeparator(): void | ||
688 | { | ||
689 | $separator = ' '; | ||
690 | |||
691 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator)); | ||
692 | static::assertSame(['tag1,', 'tag2@', 'tag3'], tags_filter(['tag1,', 'tag2@', 'tag3'], $separator)); | ||
693 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', 'tag2', 'tag3 '], $separator)); | ||
694 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator)); | ||
695 | static::assertSame(['tag1'], tags_filter([' tag1 '], $separator)); | ||
696 | static::assertSame([], tags_filter([' '], $separator)); | ||
697 | static::assertSame([], tags_filter([], $separator)); | ||
698 | static::assertSame([], tags_filter(null, $separator)); | ||
699 | } | ||
700 | |||
701 | /** | ||
702 | * Test tags_array2str with @ separator. | ||
703 | */ | ||
704 | public function testTagsArrayFilterWithSpaceSeparator(): void | ||
705 | { | ||
706 | $separator = '@'; | ||
707 | |||
708 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator)); | ||
709 | static::assertSame(['tag1,', 'tag2#', 'tag3'], tags_filter(['tag1,', 'tag2#', 'tag3'], $separator)); | ||
710 | static::assertSame( | ||
711 | ['tag1', 'tag2, and other', 'tag3'], | ||
712 | tags_filter(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator) | ||
713 | ); | ||
714 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator)); | ||
715 | static::assertSame(['tag1'], tags_filter(['@@@@tag1@@@@'], $separator)); | ||
716 | static::assertSame([], tags_filter(['@@@'], $separator)); | ||
717 | static::assertSame([], tags_filter([], $separator)); | ||
718 | static::assertSame([], tags_filter(null, $separator)); | ||
719 | } | ||
720 | |||
721 | /** | ||
496 | * Util function to build an hashtag link. | 722 | * Util function to build an hashtag link. |
497 | * | 723 | * |
498 | * @param string $hashtag Hashtag name. | 724 | * @param string $hashtag Hashtag name. |
diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2d675c9a..3508a7b1 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php | |||
@@ -30,3 +30,7 @@ require_once 'tests/utils/ReferenceLinkDB.php'; | |||
30 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | 30 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; |
31 | 31 | ||
32 | \ReferenceSessionIdHashes::genAllHashes(); | 32 | \ReferenceSessionIdHashes::genAllHashes(); |
33 | |||
34 | if (!defined('SHAARLI_MUTEX_FILE')) { | ||
35 | define('SHAARLI_MUTEX_FILE', __FILE__); | ||
36 | } | ||
diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php index 5d52daef..3d43c344 100644 --- a/tests/container/ContainerBuilderTest.php +++ b/tests/container/ContainerBuilderTest.php | |||
@@ -4,6 +4,7 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Container; | 5 | namespace Shaarli\Container; |
6 | 6 | ||
7 | use Psr\Log\LoggerInterface; | ||
7 | use Shaarli\Bookmark\BookmarkServiceInterface; | 8 | use Shaarli\Bookmark\BookmarkServiceInterface; |
8 | use Shaarli\Config\ConfigManager; | 9 | use Shaarli\Config\ConfigManager; |
9 | use Shaarli\Feed\FeedBuilder; | 10 | use Shaarli\Feed\FeedBuilder; |
@@ -12,6 +13,7 @@ use Shaarli\Front\Controller\Visitor\ErrorController; | |||
12 | use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; | 13 | use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; |
13 | use Shaarli\History; | 14 | use Shaarli\History; |
14 | use Shaarli\Http\HttpAccess; | 15 | use Shaarli\Http\HttpAccess; |
16 | use Shaarli\Http\MetadataRetriever; | ||
15 | use Shaarli\Netscape\NetscapeBookmarkUtils; | 17 | use Shaarli\Netscape\NetscapeBookmarkUtils; |
16 | use Shaarli\Plugin\PluginManager; | 18 | use Shaarli\Plugin\PluginManager; |
17 | use Shaarli\Render\PageBuilder; | 19 | use Shaarli\Render\PageBuilder; |
@@ -54,7 +56,8 @@ class ContainerBuilderTest extends TestCase | |||
54 | $this->conf, | 56 | $this->conf, |
55 | $this->sessionManager, | 57 | $this->sessionManager, |
56 | $this->cookieManager, | 58 | $this->cookieManager, |
57 | $this->loginManager | 59 | $this->loginManager, |
60 | $this->createMock(LoggerInterface::class) | ||
58 | ); | 61 | ); |
59 | } | 62 | } |
60 | 63 | ||
@@ -72,6 +75,8 @@ class ContainerBuilderTest extends TestCase | |||
72 | static::assertInstanceOf(History::class, $container->history); | 75 | static::assertInstanceOf(History::class, $container->history); |
73 | static::assertInstanceOf(HttpAccess::class, $container->httpAccess); | 76 | static::assertInstanceOf(HttpAccess::class, $container->httpAccess); |
74 | static::assertInstanceOf(LoginManager::class, $container->loginManager); | 77 | static::assertInstanceOf(LoginManager::class, $container->loginManager); |
78 | static::assertInstanceOf(LoggerInterface::class, $container->logger); | ||
79 | static::assertInstanceOf(MetadataRetriever::class, $container->metadataRetriever); | ||
75 | static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils); | 80 | static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils); |
76 | static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); | 81 | static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); |
77 | static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager); | 82 | static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager); |
diff --git a/tests/feed/FeedBuilderTest.php b/tests/feed/FeedBuilderTest.php index c29e8ef3..6b9204eb 100644 --- a/tests/feed/FeedBuilderTest.php +++ b/tests/feed/FeedBuilderTest.php | |||
@@ -3,6 +3,7 @@ | |||
3 | namespace Shaarli\Feed; | 3 | namespace Shaarli\Feed; |
4 | 4 | ||
5 | use DateTime; | 5 | use DateTime; |
6 | use malkusch\lock\mutex\NoMutex; | ||
6 | use ReferenceLinkDB; | 7 | use ReferenceLinkDB; |
7 | use Shaarli\Bookmark\Bookmark; | 8 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Bookmark\BookmarkFileService; | 9 | use Shaarli\Bookmark\BookmarkFileService; |
@@ -47,6 +48,7 @@ class FeedBuilderTest extends TestCase | |||
47 | */ | 48 | */ |
48 | public static function setUpBeforeClass(): void | 49 | public static function setUpBeforeClass(): void |
49 | { | 50 | { |
51 | $mutex = new NoMutex(); | ||
50 | $conf = new ConfigManager('tests/utils/config/configJson'); | 52 | $conf = new ConfigManager('tests/utils/config/configJson'); |
51 | $conf->set('resource.datastore', self::$testDatastore); | 53 | $conf->set('resource.datastore', self::$testDatastore); |
52 | $refLinkDB = new \ReferenceLinkDB(); | 54 | $refLinkDB = new \ReferenceLinkDB(); |
@@ -54,7 +56,7 @@ class FeedBuilderTest extends TestCase | |||
54 | $history = new History('sandbox/history.php'); | 56 | $history = new History('sandbox/history.php'); |
55 | $factory = new FormatterFactory($conf, true); | 57 | $factory = new FormatterFactory($conf, true); |
56 | self::$formatter = $factory->getFormatter(); | 58 | self::$formatter = $factory->getFormatter(); |
57 | self::$bookmarkService = new BookmarkFileService($conf, $history, true); | 59 | self::$bookmarkService = new BookmarkFileService($conf, $history, $mutex, true); |
58 | 60 | ||
59 | self::$serverInfo = array( | 61 | self::$serverInfo = array( |
60 | 'HTTPS' => 'Off', | 62 | 'HTTPS' => 'Off', |
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php index 9534436e..4fcc5dd1 100644 --- a/tests/formatter/BookmarkDefaultFormatterTest.php +++ b/tests/formatter/BookmarkDefaultFormatterTest.php | |||
@@ -174,4 +174,139 @@ class BookmarkDefaultFormatterTest extends TestCase | |||
174 | $this->assertSame($tags, $link['taglist']); | 174 | $this->assertSame($tags, $link['taglist']); |
175 | $this->assertSame(implode(' ', $tags), $link['tags']); | 175 | $this->assertSame(implode(' ', $tags), $link['tags']); |
176 | } | 176 | } |
177 | |||
178 | /** | ||
179 | * Test formatTitleHtml with search result highlight. | ||
180 | */ | ||
181 | public function testFormatTitleHtmlWithSearchHighlight(): void | ||
182 | { | ||
183 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
184 | |||
185 | $bookmark = new Bookmark(); | ||
186 | $bookmark->setTitle('PSR-2: Coding Style Guide'); | ||
187 | $bookmark->addAdditionalContentEntry( | ||
188 | 'search_highlight', | ||
189 | ['title' => [ | ||
190 | ['start' => 0, 'end' => 5], // "psr-2" | ||
191 | ['start' => 7, 'end' => 13], // coding | ||
192 | ['start' => 20, 'end' => 25], // guide | ||
193 | ]] | ||
194 | ); | ||
195 | |||
196 | $link = $this->formatter->format($bookmark); | ||
197 | |||
198 | $this->assertSame( | ||
199 | '<span class="search-highlight">PSR-2</span>: ' . | ||
200 | '<span class="search-highlight">Coding</span> Style ' . | ||
201 | '<span class="search-highlight">Guide</span>', | ||
202 | $link['title_html'] | ||
203 | ); | ||
204 | } | ||
205 | |||
206 | /** | ||
207 | * Test formatDescription with search result highlight. | ||
208 | */ | ||
209 | public function testFormatDescriptionWithSearchHighlight(): void | ||
210 | { | ||
211 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
212 | |||
213 | $bookmark = new Bookmark(); | ||
214 | $bookmark->setDescription('This guide extends and expands on PSR-1, the basic coding standard.'); | ||
215 | $bookmark->addAdditionalContentEntry( | ||
216 | 'search_highlight', | ||
217 | ['description' => [ | ||
218 | ['start' => 0, 'end' => 10], // "This guide" | ||
219 | ['start' => 45, 'end' => 50], // basic | ||
220 | ['start' => 58, 'end' => 67], // standard. | ||
221 | ]] | ||
222 | ); | ||
223 | |||
224 | $link = $this->formatter->format($bookmark); | ||
225 | |||
226 | $this->assertSame( | ||
227 | '<span class="search-highlight">This guide</span> extends and expands on PSR-1, the ' . | ||
228 | '<span class="search-highlight">basic</span> coding ' . | ||
229 | '<span class="search-highlight">standard.</span>', | ||
230 | $link['description'] | ||
231 | ); | ||
232 | } | ||
233 | |||
234 | /** | ||
235 | * Test formatUrlHtml with search result highlight. | ||
236 | */ | ||
237 | public function testFormatUrlHtmlWithSearchHighlight(): void | ||
238 | { | ||
239 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
240 | |||
241 | $bookmark = new Bookmark(); | ||
242 | $bookmark->setUrl('http://www.php-fig.org/psr/psr-2/'); | ||
243 | $bookmark->addAdditionalContentEntry( | ||
244 | 'search_highlight', | ||
245 | ['url' => [ | ||
246 | ['start' => 0, 'end' => 4], // http | ||
247 | ['start' => 15, 'end' => 18], // fig | ||
248 | ['start' => 27, 'end' => 33], // "psr-2/" | ||
249 | ]] | ||
250 | ); | ||
251 | |||
252 | $link = $this->formatter->format($bookmark); | ||
253 | |||
254 | $this->assertSame( | ||
255 | '<span class="search-highlight">http</span>://www.php-' . | ||
256 | '<span class="search-highlight">fig</span>.org/psr/' . | ||
257 | '<span class="search-highlight">psr-2/</span>', | ||
258 | $link['url_html'] | ||
259 | ); | ||
260 | } | ||
261 | |||
262 | /** | ||
263 | * Test formatTagListHtml with search result highlight. | ||
264 | */ | ||
265 | public function testFormatTagListHtmlWithSearchHighlight(): void | ||
266 | { | ||
267 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
268 | |||
269 | $bookmark = new Bookmark(); | ||
270 | $bookmark->setTagsString('coding-style standards quality assurance'); | ||
271 | $bookmark->addAdditionalContentEntry( | ||
272 | 'search_highlight', | ||
273 | ['tags' => [ | ||
274 | ['start' => 0, 'end' => 12], // coding-style | ||
275 | ['start' => 23, 'end' => 30], // quality | ||
276 | ['start' => 31, 'end' => 40], // assurance | ||
277 | ],] | ||
278 | ); | ||
279 | |||
280 | $link = $this->formatter->format($bookmark); | ||
281 | |||
282 | $this->assertSame( | ||
283 | [ | ||
284 | '<span class="search-highlight">coding-style</span>', | ||
285 | 'standards', | ||
286 | '<span class="search-highlight">quality</span>', | ||
287 | '<span class="search-highlight">assurance</span>', | ||
288 | ], | ||
289 | $link['taglist_html'] | ||
290 | ); | ||
291 | } | ||
292 | |||
293 | /** | ||
294 | * Test default formatting with formatter_settings.autolink set to false: | ||
295 | * URLs and hashtags should not be transformed | ||
296 | */ | ||
297 | public function testFormatDescriptionWithoutLinkification(): void | ||
298 | { | ||
299 | $this->conf->set('formatter_settings.autolink', false); | ||
300 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
301 | |||
302 | $bookmark = new Bookmark(); | ||
303 | $bookmark->setDescription('Hi!' . PHP_EOL . 'https://thisisaurl.tld #hashtag'); | ||
304 | |||
305 | $link = $this->formatter->format($bookmark); | ||
306 | |||
307 | static::assertSame( | ||
308 | 'Hi!<br />' . PHP_EOL . 'https://thisisaurl.tld #hashtag', | ||
309 | $link['description'] | ||
310 | ); | ||
311 | } | ||
177 | } | 312 | } |
diff --git a/tests/formatter/BookmarkMarkdownExtraFormatterTest.php b/tests/formatter/BookmarkMarkdownExtraFormatterTest.php new file mode 100644 index 00000000..d4941ef3 --- /dev/null +++ b/tests/formatter/BookmarkMarkdownExtraFormatterTest.php | |||
@@ -0,0 +1,162 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Formatter; | ||
4 | |||
5 | use DateTime; | ||
6 | use PHPUnit\Framework\TestCase; | ||
7 | use Shaarli\Bookmark\Bookmark; | ||
8 | use Shaarli\Config\ConfigManager; | ||
9 | |||
10 | /** | ||
11 | * Class BookmarkMarkdownExtraFormatterTest | ||
12 | * @package Shaarli\Formatter | ||
13 | */ | ||
14 | class BookmarkMarkdownExtraFormatterTest extends TestCase | ||
15 | { | ||
16 | /** @var string Path of test config file */ | ||
17 | protected static $testConf = 'sandbox/config'; | ||
18 | |||
19 | /** @var BookmarkFormatter */ | ||
20 | protected $formatter; | ||
21 | |||
22 | /** @var ConfigManager instance */ | ||
23 | protected $conf; | ||
24 | |||
25 | /** | ||
26 | * Initialize formatter instance. | ||
27 | */ | ||
28 | public function setUp(): void | ||
29 | { | ||
30 | copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php'); | ||
31 | $this->conf = new ConfigManager(self::$testConf); | ||
32 | $this->formatter = new BookmarkMarkdownExtraFormatter($this->conf, true); | ||
33 | } | ||
34 | |||
35 | /** | ||
36 | * Test formatting a bookmark with all its attribute filled. | ||
37 | */ | ||
38 | public function testFormatExtra(): void | ||
39 | { | ||
40 | $bookmark = new Bookmark(); | ||
41 | $bookmark->setId($id = 11); | ||
42 | $bookmark->setShortUrl($short = 'abcdef'); | ||
43 | $bookmark->setUrl('https://sub.domain.tld?query=here&for=real#hash'); | ||
44 | $bookmark->setTitle($title = 'This is a <strong>bookmark</strong>'); | ||
45 | $bookmark->setDescription('<h2>Content</h2><p>`Here is some content</p>'); | ||
46 | $bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']); | ||
47 | $bookmark->setThumbnail('http://domain2.tdl2/?type=img&name=file.png'); | ||
48 | $bookmark->setSticky(true); | ||
49 | $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412')); | ||
50 | $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213')); | ||
51 | $bookmark->setPrivate(true); | ||
52 | |||
53 | $link = $this->formatter->format($bookmark); | ||
54 | $this->assertEquals($id, $link['id']); | ||
55 | $this->assertEquals($short, $link['shorturl']); | ||
56 | $this->assertEquals('https://sub.domain.tld?query=here&for=real#hash', $link['url']); | ||
57 | $this->assertEquals( | ||
58 | 'https://sub.domain.tld?query=here&for=real#hash', | ||
59 | $link['real_url'] | ||
60 | ); | ||
61 | $this->assertEquals('This is a <strong>bookmark</strong>', $link['title']); | ||
62 | $this->assertEquals( | ||
63 | '<div class="markdown"><p>'. | ||
64 | '<h2>Content</h2><p>`Here is some content</p>'. | ||
65 | '</p></div>', | ||
66 | $link['description'] | ||
67 | ); | ||
68 | $tags[3] = '<script>alert("xss");</script>'; | ||
69 | $this->assertEquals($tags, $link['taglist']); | ||
70 | $this->assertEquals(implode(' ', $tags), $link['tags']); | ||
71 | $this->assertEquals( | ||
72 | 'http://domain2.tdl2/?type=img&name=file.png', | ||
73 | $link['thumbnail'] | ||
74 | ); | ||
75 | $this->assertEquals($created, $link['created']); | ||
76 | $this->assertEquals($created->getTimestamp(), $link['timestamp']); | ||
77 | $this->assertEquals($updated, $link['updated']); | ||
78 | $this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']); | ||
79 | $this->assertTrue($link['private']); | ||
80 | $this->assertTrue($link['sticky']); | ||
81 | $this->assertEquals('private', $link['class']); | ||
82 | } | ||
83 | |||
84 | /** | ||
85 | * Test formatting a bookmark with all its attribute filled. | ||
86 | */ | ||
87 | public function testFormatExtraMinimal(): void | ||
88 | { | ||
89 | $bookmark = new Bookmark(); | ||
90 | |||
91 | $link = $this->formatter->format($bookmark); | ||
92 | $this->assertEmpty($link['id']); | ||
93 | $this->assertEmpty($link['shorturl']); | ||
94 | $this->assertEmpty($link['url']); | ||
95 | $this->assertEmpty($link['real_url']); | ||
96 | $this->assertEmpty($link['title']); | ||
97 | $this->assertEmpty($link['description']); | ||
98 | $this->assertEmpty($link['taglist']); | ||
99 | $this->assertEmpty($link['tags']); | ||
100 | $this->assertEmpty($link['thumbnail']); | ||
101 | $this->assertEmpty($link['created']); | ||
102 | $this->assertEmpty($link['timestamp']); | ||
103 | $this->assertEmpty($link['updated']); | ||
104 | $this->assertEmpty($link['updated_timestamp']); | ||
105 | $this->assertFalse($link['private']); | ||
106 | $this->assertFalse($link['sticky']); | ||
107 | $this->assertEmpty($link['class']); | ||
108 | } | ||
109 | |||
110 | /** | ||
111 | * Make sure that the description is properly formatted by the default formatter. | ||
112 | */ | ||
113 | public function testFormatExtrraDescription(): void | ||
114 | { | ||
115 | $description = 'This a <strong>description</strong>'. PHP_EOL; | ||
116 | $description .= 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL; | ||
117 | $description .= 'Also, there is an #hashtag added'. PHP_EOL; | ||
118 | $description .= ' A N D KEEP SPACES ! '. PHP_EOL; | ||
119 | $description .= '# Header {.class}'. PHP_EOL; | ||
120 | |||
121 | $bookmark = new Bookmark(); | ||
122 | $bookmark->setDescription($description); | ||
123 | $link = $this->formatter->format($bookmark); | ||
124 | |||
125 | $description = '<div class="markdown"><p>'; | ||
126 | $description .= 'This a <strong>description</strong><br />'. PHP_EOL; | ||
127 | $url = 'https://sub.domain.tld?query=here&for=real#hash'; | ||
128 | $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL; | ||
129 | $description .= 'Also, there is an <a href="./add-tag/hashtag">#hashtag</a> added<br />'. PHP_EOL; | ||
130 | $description .= 'A N D KEEP SPACES ! </p>' . PHP_EOL; | ||
131 | $description .= '<h1 class="class">Header</h1>'; | ||
132 | $description .= '</div>'; | ||
133 | |||
134 | $this->assertEquals($description, $link['description']); | ||
135 | } | ||
136 | |||
137 | /** | ||
138 | * Test formatting URL with an index_url set | ||
139 | * It should prepend relative links. | ||
140 | */ | ||
141 | public function testFormatExtraNoteWithIndexUrl(): void | ||
142 | { | ||
143 | $bookmark = new Bookmark(); | ||
144 | $bookmark->setUrl($short = '?abcdef'); | ||
145 | $description = 'Text #hashtag more text'; | ||
146 | $bookmark->setDescription($description); | ||
147 | |||
148 | $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/'); | ||
149 | |||
150 | $description = '<div class="markdown"><p>'; | ||
151 | $description .= 'Text <a href="'. $root .'./add-tag/hashtag">#hashtag</a> more text'; | ||
152 | $description .= '</p></div>'; | ||
153 | |||
154 | $link = $this->formatter->format($bookmark); | ||
155 | $this->assertEquals($root . $short, $link['url']); | ||
156 | $this->assertEquals($root . $short, $link['real_url']); | ||
157 | $this->assertEquals( | ||
158 | $description, | ||
159 | $link['description'] | ||
160 | ); | ||
161 | } | ||
162 | } | ||
diff --git a/tests/front/controller/admin/ConfigureControllerTest.php b/tests/front/controller/admin/ConfigureControllerTest.php index aca6cff3..d82db0a7 100644 --- a/tests/front/controller/admin/ConfigureControllerTest.php +++ b/tests/front/controller/admin/ConfigureControllerTest.php | |||
@@ -51,7 +51,7 @@ class ConfigureControllerTest extends TestCase | |||
51 | static::assertSame('general.title', $assignedVariables['title']); | 51 | static::assertSame('general.title', $assignedVariables['title']); |
52 | static::assertSame('resource.theme', $assignedVariables['theme']); | 52 | static::assertSame('resource.theme', $assignedVariables['theme']); |
53 | static::assertEmpty($assignedVariables['theme_available']); | 53 | static::assertEmpty($assignedVariables['theme_available']); |
54 | static::assertSame(['default', 'markdown'], $assignedVariables['formatter_available']); | 54 | static::assertSame(['default', 'markdown', 'markdownExtra'], $assignedVariables['formatter_available']); |
55 | static::assertNotEmpty($assignedVariables['continents']); | 55 | static::assertNotEmpty($assignedVariables['continents']); |
56 | static::assertNotEmpty($assignedVariables['cities']); | 56 | static::assertNotEmpty($assignedVariables['cities']); |
57 | static::assertSame('general.retrieve_description', $assignedVariables['retrieve_description']); | 57 | static::assertSame('general.retrieve_description', $assignedVariables['retrieve_description']); |
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php deleted file mode 100644 index 0f27ec2f..00000000 --- a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php +++ /dev/null | |||
@@ -1,47 +0,0 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; | ||
6 | |||
7 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | ||
8 | use Shaarli\Front\Controller\Admin\ManageShaareController; | ||
9 | use Shaarli\Http\HttpAccess; | ||
10 | use Shaarli\TestCase; | ||
11 | use Slim\Http\Request; | ||
12 | use Slim\Http\Response; | ||
13 | |||
14 | class AddShaareTest extends TestCase | ||
15 | { | ||
16 | use FrontAdminControllerMockHelper; | ||
17 | |||
18 | /** @var ManageShaareController */ | ||
19 | protected $controller; | ||
20 | |||
21 | public function setUp(): void | ||
22 | { | ||
23 | $this->createContainer(); | ||
24 | |||
25 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | ||
26 | $this->controller = new ManageShaareController($this->container); | ||
27 | } | ||
28 | |||
29 | /** | ||
30 | * Test displaying add link page | ||
31 | */ | ||
32 | public function testAddShaare(): void | ||
33 | { | ||
34 | $assignedVariables = []; | ||
35 | $this->assignTemplateVars($assignedVariables); | ||
36 | |||
37 | $request = $this->createMock(Request::class); | ||
38 | $response = new Response(); | ||
39 | |||
40 | $result = $this->controller->addShaare($request, $response); | ||
41 | |||
42 | static::assertSame(200, $result->getStatusCode()); | ||
43 | static::assertSame('addlink', (string) $result->getBody()); | ||
44 | |||
45 | static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']); | ||
46 | } | ||
47 | } | ||
diff --git a/tests/front/controller/admin/ManageTagControllerTest.php b/tests/front/controller/admin/ManageTagControllerTest.php index 8a0ff7a9..af6f273f 100644 --- a/tests/front/controller/admin/ManageTagControllerTest.php +++ b/tests/front/controller/admin/ManageTagControllerTest.php | |||
@@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller\Admin; | |||
6 | 6 | ||
7 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Bookmark\BookmarkFilter; | 8 | use Shaarli\Bookmark\BookmarkFilter; |
9 | use Shaarli\Config\ConfigManager; | ||
9 | use Shaarli\Front\Exception\WrongTokenException; | 10 | use Shaarli\Front\Exception\WrongTokenException; |
10 | use Shaarli\Security\SessionManager; | 11 | use Shaarli\Security\SessionManager; |
11 | use Shaarli\TestCase; | 12 | use Shaarli\TestCase; |
@@ -44,10 +45,33 @@ class ManageTagControllerTest extends TestCase | |||
44 | static::assertSame('changetag', (string) $result->getBody()); | 45 | static::assertSame('changetag', (string) $result->getBody()); |
45 | 46 | ||
46 | static::assertSame('fromtag', $assignedVariables['fromtag']); | 47 | static::assertSame('fromtag', $assignedVariables['fromtag']); |
48 | static::assertSame('@', $assignedVariables['tags_separator']); | ||
47 | static::assertSame('Manage tags - Shaarli', $assignedVariables['pagetitle']); | 49 | static::assertSame('Manage tags - Shaarli', $assignedVariables['pagetitle']); |
48 | } | 50 | } |
49 | 51 | ||
50 | /** | 52 | /** |
53 | * Test displaying manage tag page | ||
54 | */ | ||
55 | public function testIndexWhitespaceSeparator(): void | ||
56 | { | ||
57 | $assignedVariables = []; | ||
58 | $this->assignTemplateVars($assignedVariables); | ||
59 | |||
60 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
61 | $this->container->conf->method('get')->willReturnCallback(function (string $key) { | ||
62 | return $key === 'general.tags_separator' ? ' ' : $key; | ||
63 | }); | ||
64 | |||
65 | $request = $this->createMock(Request::class); | ||
66 | $response = new Response(); | ||
67 | |||
68 | $this->controller->index($request, $response); | ||
69 | |||
70 | static::assertSame(' ', $assignedVariables['tags_separator']); | ||
71 | static::assertSame('whitespace', $assignedVariables['tags_separator_desc']); | ||
72 | } | ||
73 | |||
74 | /** | ||
51 | * Test posting a tag update - rename tag - valid info provided. | 75 | * Test posting a tag update - rename tag - valid info provided. |
52 | */ | 76 | */ |
53 | public function testSaveRenameTagValid(): void | 77 | public function testSaveRenameTagValid(): void |
@@ -269,4 +293,116 @@ class ManageTagControllerTest extends TestCase | |||
269 | static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session); | 293 | static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session); |
270 | static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]); | 294 | static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]); |
271 | } | 295 | } |
296 | |||
297 | /** | ||
298 | * Test changeSeparator to '#': redirection + success message. | ||
299 | */ | ||
300 | public function testChangeSeparatorValid(): void | ||
301 | { | ||
302 | $toSeparator = '#'; | ||
303 | |||
304 | $session = []; | ||
305 | $this->assignSessionVars($session); | ||
306 | |||
307 | $request = $this->createMock(Request::class); | ||
308 | $request | ||
309 | ->expects(static::atLeastOnce()) | ||
310 | ->method('getParam') | ||
311 | ->willReturnCallback(function (string $key) use ($toSeparator): ?string { | ||
312 | return $key === 'separator' ? $toSeparator : $key; | ||
313 | }) | ||
314 | ; | ||
315 | $response = new Response(); | ||
316 | |||
317 | $this->container->conf | ||
318 | ->expects(static::once()) | ||
319 | ->method('set') | ||
320 | ->with('general.tags_separator', $toSeparator, true, true) | ||
321 | ; | ||
322 | |||
323 | $result = $this->controller->changeSeparator($request, $response); | ||
324 | |||
325 | static::assertSame(302, $result->getStatusCode()); | ||
326 | static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location')); | ||
327 | |||
328 | static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session); | ||
329 | static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session); | ||
330 | static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session); | ||
331 | static::assertSame( | ||
332 | ['Your tags separator setting has been updated!'], | ||
333 | $session[SessionManager::KEY_SUCCESS_MESSAGES] | ||
334 | ); | ||
335 | } | ||
336 | |||
337 | /** | ||
338 | * Test changeSeparator to '#@' (too long): redirection + error message. | ||
339 | */ | ||
340 | public function testChangeSeparatorInvalidTooLong(): void | ||
341 | { | ||
342 | $toSeparator = '#@'; | ||
343 | |||
344 | $session = []; | ||
345 | $this->assignSessionVars($session); | ||
346 | |||
347 | $request = $this->createMock(Request::class); | ||
348 | $request | ||
349 | ->expects(static::atLeastOnce()) | ||
350 | ->method('getParam') | ||
351 | ->willReturnCallback(function (string $key) use ($toSeparator): ?string { | ||
352 | return $key === 'separator' ? $toSeparator : $key; | ||
353 | }) | ||
354 | ; | ||
355 | $response = new Response(); | ||
356 | |||
357 | $this->container->conf->expects(static::never())->method('set'); | ||
358 | |||
359 | $result = $this->controller->changeSeparator($request, $response); | ||
360 | |||
361 | static::assertSame(302, $result->getStatusCode()); | ||
362 | static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location')); | ||
363 | |||
364 | static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session); | ||
365 | static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session); | ||
366 | static::assertArrayHasKey(SessionManager::KEY_ERROR_MESSAGES, $session); | ||
367 | static::assertSame( | ||
368 | ['Tags separator must be a single character.'], | ||
369 | $session[SessionManager::KEY_ERROR_MESSAGES] | ||
370 | ); | ||
371 | } | ||
372 | |||
373 | /** | ||
374 | * Test changeSeparator to '#@' (too long): redirection + error message. | ||
375 | */ | ||
376 | public function testChangeSeparatorInvalidReservedCharacter(): void | ||
377 | { | ||
378 | $toSeparator = '*'; | ||
379 | |||
380 | $session = []; | ||
381 | $this->assignSessionVars($session); | ||
382 | |||
383 | $request = $this->createMock(Request::class); | ||
384 | $request | ||
385 | ->expects(static::atLeastOnce()) | ||
386 | ->method('getParam') | ||
387 | ->willReturnCallback(function (string $key) use ($toSeparator): ?string { | ||
388 | return $key === 'separator' ? $toSeparator : $key; | ||
389 | }) | ||
390 | ; | ||
391 | $response = new Response(); | ||
392 | |||
393 | $this->container->conf->expects(static::never())->method('set'); | ||
394 | |||
395 | $result = $this->controller->changeSeparator($request, $response); | ||
396 | |||
397 | static::assertSame(302, $result->getStatusCode()); | ||
398 | static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location')); | ||
399 | |||
400 | static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session); | ||
401 | static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session); | ||
402 | static::assertArrayHasKey(SessionManager::KEY_ERROR_MESSAGES, $session); | ||
403 | static::assertStringStartsWith( | ||
404 | 'These characters are reserved and can\'t be used as tags separator', | ||
405 | $session[SessionManager::KEY_ERROR_MESSAGES][0] | ||
406 | ); | ||
407 | } | ||
272 | } | 408 | } |
diff --git a/tests/front/controller/admin/ServerControllerTest.php b/tests/front/controller/admin/ServerControllerTest.php new file mode 100644 index 00000000..355cce7d --- /dev/null +++ b/tests/front/controller/admin/ServerControllerTest.php | |||
@@ -0,0 +1,184 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Config\ConfigManager; | ||
8 | use Shaarli\Security\SessionManager; | ||
9 | use Shaarli\TestCase; | ||
10 | use Slim\Http\Request; | ||
11 | use Slim\Http\Response; | ||
12 | |||
13 | /** | ||
14 | * Test Server administration controller. | ||
15 | */ | ||
16 | class ServerControllerTest extends TestCase | ||
17 | { | ||
18 | use FrontAdminControllerMockHelper; | ||
19 | |||
20 | /** @var ServerController */ | ||
21 | protected $controller; | ||
22 | |||
23 | public function setUp(): void | ||
24 | { | ||
25 | $this->createContainer(); | ||
26 | |||
27 | $this->controller = new ServerController($this->container); | ||
28 | |||
29 | // initialize dummy cache | ||
30 | @mkdir('sandbox/'); | ||
31 | foreach (['pagecache', 'tmp', 'cache'] as $folder) { | ||
32 | @mkdir('sandbox/' . $folder); | ||
33 | @touch('sandbox/' . $folder . '/.htaccess'); | ||
34 | @touch('sandbox/' . $folder . '/1'); | ||
35 | @touch('sandbox/' . $folder . '/2'); | ||
36 | } | ||
37 | } | ||
38 | |||
39 | public function tearDown(): void | ||
40 | { | ||
41 | foreach (['pagecache', 'tmp', 'cache'] as $folder) { | ||
42 | @unlink('sandbox/' . $folder . '/.htaccess'); | ||
43 | @unlink('sandbox/' . $folder . '/1'); | ||
44 | @unlink('sandbox/' . $folder . '/2'); | ||
45 | @rmdir('sandbox/' . $folder); | ||
46 | } | ||
47 | } | ||
48 | |||
49 | /** | ||
50 | * Test default display of server administration page. | ||
51 | */ | ||
52 | public function testIndex(): void | ||
53 | { | ||
54 | $request = $this->createMock(Request::class); | ||
55 | $response = new Response(); | ||
56 | |||
57 | // Save RainTPL assigned variables | ||
58 | $assignedVariables = []; | ||
59 | $this->assignTemplateVars($assignedVariables); | ||
60 | |||
61 | $result = $this->controller->index($request, $response); | ||
62 | |||
63 | static::assertSame(200, $result->getStatusCode()); | ||
64 | static::assertSame('server', (string) $result->getBody()); | ||
65 | |||
66 | static::assertSame(PHP_VERSION, $assignedVariables['php_version']); | ||
67 | static::assertArrayHasKey('php_has_reached_eol', $assignedVariables); | ||
68 | static::assertArrayHasKey('php_eol', $assignedVariables); | ||
69 | static::assertArrayHasKey('php_extensions', $assignedVariables); | ||
70 | static::assertArrayHasKey('permissions', $assignedVariables); | ||
71 | static::assertEmpty($assignedVariables['permissions']); | ||
72 | |||
73 | static::assertRegExp( | ||
74 | '#https://github\.com/shaarli/Shaarli/releases/tag/v\d+\.\d+\.\d+#', | ||
75 | $assignedVariables['release_url'] | ||
76 | ); | ||
77 | static::assertRegExp('#v\d+\.\d+\.\d+#', $assignedVariables['latest_version']); | ||
78 | static::assertRegExp('#(v\d+\.\d+\.\d+|dev)#', $assignedVariables['current_version']); | ||
79 | static::assertArrayHasKey('index_url', $assignedVariables); | ||
80 | static::assertArrayHasKey('client_ip', $assignedVariables); | ||
81 | static::assertArrayHasKey('trusted_proxies', $assignedVariables); | ||
82 | |||
83 | static::assertSame('Server administration - Shaarli', $assignedVariables['pagetitle']); | ||
84 | } | ||
85 | |||
86 | /** | ||
87 | * Test clearing the main cache | ||
88 | */ | ||
89 | public function testClearMainCache(): void | ||
90 | { | ||
91 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
92 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | ||
93 | if ($key === 'resource.page_cache') { | ||
94 | return 'sandbox/pagecache'; | ||
95 | } elseif ($key === 'resource.raintpl_tmp') { | ||
96 | return 'sandbox/tmp'; | ||
97 | } elseif ($key === 'resource.thumbnails_cache') { | ||
98 | return 'sandbox/cache'; | ||
99 | } else { | ||
100 | return $default; | ||
101 | } | ||
102 | }); | ||
103 | |||
104 | $this->container->sessionManager | ||
105 | ->expects(static::once()) | ||
106 | ->method('setSessionParameter') | ||
107 | ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['Shaarli\'s cache folder has been cleared!']) | ||
108 | ; | ||
109 | |||
110 | $request = $this->createMock(Request::class); | ||
111 | $request->method('getQueryParam')->with('type')->willReturn('main'); | ||
112 | $response = new Response(); | ||
113 | |||
114 | $result = $this->controller->clearCache($request, $response); | ||
115 | |||
116 | static::assertSame(302, $result->getStatusCode()); | ||
117 | static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location')); | ||
118 | |||
119 | static::assertFileNotExists('sandbox/pagecache/1'); | ||
120 | static::assertFileNotExists('sandbox/pagecache/2'); | ||
121 | static::assertFileNotExists('sandbox/tmp/1'); | ||
122 | static::assertFileNotExists('sandbox/tmp/2'); | ||
123 | |||
124 | static::assertFileExists('sandbox/pagecache/.htaccess'); | ||
125 | static::assertFileExists('sandbox/tmp/.htaccess'); | ||
126 | static::assertFileExists('sandbox/cache'); | ||
127 | static::assertFileExists('sandbox/cache/.htaccess'); | ||
128 | static::assertFileExists('sandbox/cache/1'); | ||
129 | static::assertFileExists('sandbox/cache/2'); | ||
130 | } | ||
131 | |||
132 | /** | ||
133 | * Test clearing thumbnails cache | ||
134 | */ | ||
135 | public function testClearThumbnailsCache(): void | ||
136 | { | ||
137 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
138 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | ||
139 | if ($key === 'resource.page_cache') { | ||
140 | return 'sandbox/pagecache'; | ||
141 | } elseif ($key === 'resource.raintpl_tmp') { | ||
142 | return 'sandbox/tmp'; | ||
143 | } elseif ($key === 'resource.thumbnails_cache') { | ||
144 | return 'sandbox/cache'; | ||
145 | } else { | ||
146 | return $default; | ||
147 | } | ||
148 | }); | ||
149 | |||
150 | $this->container->sessionManager | ||
151 | ->expects(static::once()) | ||
152 | ->method('setSessionParameter') | ||
153 | ->willReturnCallback(function (string $key, array $value): SessionManager { | ||
154 | static::assertSame(SessionManager::KEY_WARNING_MESSAGES, $key); | ||
155 | static::assertCount(1, $value); | ||
156 | static::assertStringStartsWith('Thumbnails cache has been cleared.', $value[0]); | ||
157 | |||
158 | return $this->container->sessionManager; | ||
159 | }); | ||
160 | ; | ||
161 | |||
162 | $request = $this->createMock(Request::class); | ||
163 | $request->method('getQueryParam')->with('type')->willReturn('thumbnails'); | ||
164 | $response = new Response(); | ||
165 | |||
166 | $result = $this->controller->clearCache($request, $response); | ||
167 | |||
168 | static::assertSame(302, $result->getStatusCode()); | ||
169 | static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location')); | ||
170 | |||
171 | static::assertFileNotExists('sandbox/cache/1'); | ||
172 | static::assertFileNotExists('sandbox/cache/2'); | ||
173 | |||
174 | static::assertFileExists('sandbox/cache/.htaccess'); | ||
175 | static::assertFileExists('sandbox/pagecache'); | ||
176 | static::assertFileExists('sandbox/pagecache/.htaccess'); | ||
177 | static::assertFileExists('sandbox/pagecache/1'); | ||
178 | static::assertFileExists('sandbox/pagecache/2'); | ||
179 | static::assertFileExists('sandbox/tmp'); | ||
180 | static::assertFileExists('sandbox/tmp/.htaccess'); | ||
181 | static::assertFileExists('sandbox/tmp/1'); | ||
182 | static::assertFileExists('sandbox/tmp/2'); | ||
183 | } | ||
184 | } | ||
diff --git a/tests/front/controller/admin/ShaareAddControllerTest.php b/tests/front/controller/admin/ShaareAddControllerTest.php new file mode 100644 index 00000000..a27ebe64 --- /dev/null +++ b/tests/front/controller/admin/ShaareAddControllerTest.php | |||
@@ -0,0 +1,97 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Config\ConfigManager; | ||
8 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | ||
9 | use Shaarli\Http\HttpAccess; | ||
10 | use Shaarli\TestCase; | ||
11 | use Slim\Http\Request; | ||
12 | use Slim\Http\Response; | ||
13 | |||
14 | class ShaareAddControllerTest extends TestCase | ||
15 | { | ||
16 | use FrontAdminControllerMockHelper; | ||
17 | |||
18 | /** @var ShaareAddController */ | ||
19 | protected $controller; | ||
20 | |||
21 | public function setUp(): void | ||
22 | { | ||
23 | $this->createContainer(); | ||
24 | |||
25 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | ||
26 | $this->controller = new ShaareAddController($this->container); | ||
27 | } | ||
28 | |||
29 | /** | ||
30 | * Test displaying add link page | ||
31 | */ | ||
32 | public function testAddShaare(): void | ||
33 | { | ||
34 | $assignedVariables = []; | ||
35 | $this->assignTemplateVars($assignedVariables); | ||
36 | |||
37 | $request = $this->createMock(Request::class); | ||
38 | $response = new Response(); | ||
39 | |||
40 | $expectedTags = [ | ||
41 | 'tag1' => 32, | ||
42 | 'tag2' => 24, | ||
43 | 'tag3' => 1, | ||
44 | ]; | ||
45 | $this->container->bookmarkService | ||
46 | ->expects(static::once()) | ||
47 | ->method('bookmarksCountPerTag') | ||
48 | ->willReturn($expectedTags) | ||
49 | ; | ||
50 | $expectedTags = array_merge($expectedTags, [BookmarkMarkdownFormatter::NO_MD_TAG => 1]); | ||
51 | |||
52 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
53 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | ||
54 | return $key === 'formatter' ? 'markdown' : $default; | ||
55 | }); | ||
56 | |||
57 | $result = $this->controller->addShaare($request, $response); | ||
58 | |||
59 | static::assertSame(200, $result->getStatusCode()); | ||
60 | static::assertSame('addlink', (string) $result->getBody()); | ||
61 | |||
62 | static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']); | ||
63 | static::assertFalse($assignedVariables['default_private_links']); | ||
64 | static::assertTrue($assignedVariables['async_metadata']); | ||
65 | static::assertSame($expectedTags, $assignedVariables['tags']); | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * Test displaying add link page | ||
70 | */ | ||
71 | public function testAddShaareWithoutMd(): void | ||
72 | { | ||
73 | $assignedVariables = []; | ||
74 | $this->assignTemplateVars($assignedVariables); | ||
75 | |||
76 | $request = $this->createMock(Request::class); | ||
77 | $response = new Response(); | ||
78 | |||
79 | $expectedTags = [ | ||
80 | 'tag1' => 32, | ||
81 | 'tag2' => 24, | ||
82 | 'tag3' => 1, | ||
83 | ]; | ||
84 | $this->container->bookmarkService | ||
85 | ->expects(static::once()) | ||
86 | ->method('bookmarksCountPerTag') | ||
87 | ->willReturn($expectedTags) | ||
88 | ; | ||
89 | |||
90 | $result = $this->controller->addShaare($request, $response); | ||
91 | |||
92 | static::assertSame(200, $result->getStatusCode()); | ||
93 | static::assertSame('addlink', (string) $result->getBody()); | ||
94 | |||
95 | static::assertSame($expectedTags, $assignedVariables['tags']); | ||
96 | } | ||
97 | } | ||
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php index 096d0774..28b1c023 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | declare(strict_types=1); | 3 | declare(strict_types=1); |
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; | 5 | namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; |
6 | 6 | ||
7 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
@@ -10,7 +10,7 @@ use Shaarli\Formatter\BookmarkFormatter; | |||
10 | use Shaarli\Formatter\BookmarkRawFormatter; | 10 | use Shaarli\Formatter\BookmarkRawFormatter; |
11 | use Shaarli\Formatter\FormatterFactory; | 11 | use Shaarli\Formatter\FormatterFactory; |
12 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | 12 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; |
13 | use Shaarli\Front\Controller\Admin\ManageShaareController; | 13 | use Shaarli\Front\Controller\Admin\ShaareManageController; |
14 | use Shaarli\Http\HttpAccess; | 14 | use Shaarli\Http\HttpAccess; |
15 | use Shaarli\Security\SessionManager; | 15 | use Shaarli\Security\SessionManager; |
16 | use Shaarli\TestCase; | 16 | use Shaarli\TestCase; |
@@ -21,7 +21,7 @@ class ChangeVisibilityBookmarkTest extends TestCase | |||
21 | { | 21 | { |
22 | use FrontAdminControllerMockHelper; | 22 | use FrontAdminControllerMockHelper; |
23 | 23 | ||
24 | /** @var ManageShaareController */ | 24 | /** @var ShaareManageController */ |
25 | protected $controller; | 25 | protected $controller; |
26 | 26 | ||
27 | public function setUp(): void | 27 | public function setUp(): void |
@@ -29,7 +29,7 @@ class ChangeVisibilityBookmarkTest extends TestCase | |||
29 | $this->createContainer(); | 29 | $this->createContainer(); |
30 | 30 | ||
31 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | 31 | $this->container->httpAccess = $this->createMock(HttpAccess::class); |
32 | $this->controller = new ManageShaareController($this->container); | 32 | $this->controller = new ShaareManageController($this->container); |
33 | } | 33 | } |
34 | 34 | ||
35 | /** | 35 | /** |
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php index ba774e21..a276d988 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php | |||
@@ -2,14 +2,14 @@ | |||
2 | 2 | ||
3 | declare(strict_types=1); | 3 | declare(strict_types=1); |
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; | 5 | namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; |
6 | 6 | ||
7 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
9 | use Shaarli\Formatter\BookmarkFormatter; | 9 | use Shaarli\Formatter\BookmarkFormatter; |
10 | use Shaarli\Formatter\FormatterFactory; | 10 | use Shaarli\Formatter\FormatterFactory; |
11 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | 11 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; |
12 | use Shaarli\Front\Controller\Admin\ManageShaareController; | 12 | use Shaarli\Front\Controller\Admin\ShaareManageController; |
13 | use Shaarli\Http\HttpAccess; | 13 | use Shaarli\Http\HttpAccess; |
14 | use Shaarli\Security\SessionManager; | 14 | use Shaarli\Security\SessionManager; |
15 | use Shaarli\TestCase; | 15 | use Shaarli\TestCase; |
@@ -20,7 +20,7 @@ class DeleteBookmarkTest extends TestCase | |||
20 | { | 20 | { |
21 | use FrontAdminControllerMockHelper; | 21 | use FrontAdminControllerMockHelper; |
22 | 22 | ||
23 | /** @var ManageShaareController */ | 23 | /** @var ShaareManageController */ |
24 | protected $controller; | 24 | protected $controller; |
25 | 25 | ||
26 | public function setUp(): void | 26 | public function setUp(): void |
@@ -28,7 +28,7 @@ class DeleteBookmarkTest extends TestCase | |||
28 | $this->createContainer(); | 28 | $this->createContainer(); |
29 | 29 | ||
30 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | 30 | $this->container->httpAccess = $this->createMock(HttpAccess::class); |
31 | $this->controller = new ManageShaareController($this->container); | 31 | $this->controller = new ShaareManageController($this->container); |
32 | } | 32 | } |
33 | 33 | ||
34 | /** | 34 | /** |
@@ -38,6 +38,8 @@ class DeleteBookmarkTest extends TestCase | |||
38 | { | 38 | { |
39 | $parameters = ['id' => '123']; | 39 | $parameters = ['id' => '123']; |
40 | 40 | ||
41 | $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/shaare/abcdef'; | ||
42 | |||
41 | $request = $this->createMock(Request::class); | 43 | $request = $this->createMock(Request::class); |
42 | $request | 44 | $request |
43 | ->method('getParam') | 45 | ->method('getParam') |
@@ -90,6 +92,8 @@ class DeleteBookmarkTest extends TestCase | |||
90 | { | 92 | { |
91 | $parameters = ['id' => '123 456 789']; | 93 | $parameters = ['id' => '123 456 789']; |
92 | 94 | ||
95 | $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/?searchtags=abcdef'; | ||
96 | |||
93 | $request = $this->createMock(Request::class); | 97 | $request = $this->createMock(Request::class); |
94 | $request | 98 | $request |
95 | ->method('getParam') | 99 | ->method('getParam') |
@@ -152,7 +156,7 @@ class DeleteBookmarkTest extends TestCase | |||
152 | $result = $this->controller->deleteBookmark($request, $response); | 156 | $result = $this->controller->deleteBookmark($request, $response); |
153 | 157 | ||
154 | static::assertSame(302, $result->getStatusCode()); | 158 | static::assertSame(302, $result->getStatusCode()); |
155 | static::assertSame(['/subfolder/'], $result->getHeader('location')); | 159 | static::assertSame(['/subfolder/?searchtags=abcdef'], $result->getHeader('location')); |
156 | } | 160 | } |
157 | 161 | ||
158 | /** | 162 | /** |
@@ -356,6 +360,10 @@ class DeleteBookmarkTest extends TestCase | |||
356 | ; | 360 | ; |
357 | $response = new Response(); | 361 | $response = new Response(); |
358 | 362 | ||
363 | $this->container->bookmarkService->method('get')->with('123')->willReturn( | ||
364 | (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123') | ||
365 | ); | ||
366 | |||
359 | $this->container->formatterFactory = $this->createMock(FormatterFactory::class); | 367 | $this->container->formatterFactory = $this->createMock(FormatterFactory::class); |
360 | $this->container->formatterFactory | 368 | $this->container->formatterFactory |
361 | ->expects(static::once()) | 369 | ->expects(static::once()) |
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php index 50ce7df1..b89206ce 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php | |||
@@ -2,12 +2,12 @@ | |||
2 | 2 | ||
3 | declare(strict_types=1); | 3 | declare(strict_types=1); |
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; | 5 | namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; |
6 | 6 | ||
7 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
9 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | 9 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; |
10 | use Shaarli\Front\Controller\Admin\ManageShaareController; | 10 | use Shaarli\Front\Controller\Admin\ShaareManageController; |
11 | use Shaarli\Http\HttpAccess; | 11 | use Shaarli\Http\HttpAccess; |
12 | use Shaarli\Security\SessionManager; | 12 | use Shaarli\Security\SessionManager; |
13 | use Shaarli\TestCase; | 13 | use Shaarli\TestCase; |
@@ -18,7 +18,7 @@ class PinBookmarkTest extends TestCase | |||
18 | { | 18 | { |
19 | use FrontAdminControllerMockHelper; | 19 | use FrontAdminControllerMockHelper; |
20 | 20 | ||
21 | /** @var ManageShaareController */ | 21 | /** @var ShaareManageController */ |
22 | protected $controller; | 22 | protected $controller; |
23 | 23 | ||
24 | public function setUp(): void | 24 | public function setUp(): void |
@@ -26,7 +26,7 @@ class PinBookmarkTest extends TestCase | |||
26 | $this->createContainer(); | 26 | $this->createContainer(); |
27 | 27 | ||
28 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | 28 | $this->container->httpAccess = $this->createMock(HttpAccess::class); |
29 | $this->controller = new ManageShaareController($this->container); | 29 | $this->controller = new ShaareManageController($this->container); |
30 | } | 30 | } |
31 | 31 | ||
32 | /** | 32 | /** |
diff --git a/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php new file mode 100644 index 00000000..ae61dfb7 --- /dev/null +++ b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php | |||
@@ -0,0 +1,139 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; | ||
6 | |||
7 | use Shaarli\Bookmark\Bookmark; | ||
8 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | ||
9 | use Shaarli\Front\Controller\Admin\ShaareManageController; | ||
10 | use Shaarli\Http\HttpAccess; | ||
11 | use Shaarli\TestCase; | ||
12 | use Slim\Http\Request; | ||
13 | use Slim\Http\Response; | ||
14 | |||
15 | /** | ||
16 | * Test GET /admin/shaare/private/{hash} | ||
17 | */ | ||
18 | class SharePrivateTest extends TestCase | ||
19 | { | ||
20 | use FrontAdminControllerMockHelper; | ||
21 | |||
22 | /** @var ShaareManageController */ | ||
23 | protected $controller; | ||
24 | |||
25 | public function setUp(): void | ||
26 | { | ||
27 | $this->createContainer(); | ||
28 | |||
29 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | ||
30 | $this->controller = new ShaareManageController($this->container); | ||
31 | } | ||
32 | |||
33 | /** | ||
34 | * Test shaare private with a private bookmark which does not have a key yet. | ||
35 | */ | ||
36 | public function testSharePrivateWithNewPrivateBookmark(): void | ||
37 | { | ||
38 | $hash = 'abcdcef'; | ||
39 | $request = $this->createMock(Request::class); | ||
40 | $response = new Response(); | ||
41 | |||
42 | $bookmark = (new Bookmark()) | ||
43 | ->setId(123) | ||
44 | ->setUrl('http://domain.tld') | ||
45 | ->setTitle('Title 123') | ||
46 | ->setPrivate(true) | ||
47 | ; | ||
48 | |||
49 | $this->container->bookmarkService | ||
50 | ->expects(static::once()) | ||
51 | ->method('findByHash') | ||
52 | ->with($hash) | ||
53 | ->willReturn($bookmark) | ||
54 | ; | ||
55 | $this->container->bookmarkService | ||
56 | ->expects(static::once()) | ||
57 | ->method('set') | ||
58 | ->with($bookmark, true) | ||
59 | ->willReturnCallback(function (Bookmark $bookmark): Bookmark { | ||
60 | static::assertSame(32, strlen($bookmark->getAdditionalContentEntry('private_key'))); | ||
61 | |||
62 | return $bookmark; | ||
63 | }) | ||
64 | ; | ||
65 | |||
66 | $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]); | ||
67 | |||
68 | static::assertSame(302, $result->getStatusCode()); | ||
69 | static::assertRegExp('#/subfolder/shaare/' . $hash . '\?key=\w{32}#', $result->getHeaderLine('Location')); | ||
70 | } | ||
71 | |||
72 | /** | ||
73 | * Test shaare private with a private bookmark which does already have a key. | ||
74 | */ | ||
75 | public function testSharePrivateWithExistingPrivateBookmark(): void | ||
76 | { | ||
77 | $hash = 'abcdcef'; | ||
78 | $existingKey = 'this is a private key'; | ||
79 | $request = $this->createMock(Request::class); | ||
80 | $response = new Response(); | ||
81 | |||
82 | $bookmark = (new Bookmark()) | ||
83 | ->setId(123) | ||
84 | ->setUrl('http://domain.tld') | ||
85 | ->setTitle('Title 123') | ||
86 | ->setPrivate(true) | ||
87 | ->addAdditionalContentEntry('private_key', $existingKey) | ||
88 | ; | ||
89 | |||
90 | $this->container->bookmarkService | ||
91 | ->expects(static::once()) | ||
92 | ->method('findByHash') | ||
93 | ->with($hash) | ||
94 | ->willReturn($bookmark) | ||
95 | ; | ||
96 | $this->container->bookmarkService | ||
97 | ->expects(static::never()) | ||
98 | ->method('set') | ||
99 | ; | ||
100 | |||
101 | $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]); | ||
102 | |||
103 | static::assertSame(302, $result->getStatusCode()); | ||
104 | static::assertSame('/subfolder/shaare/' . $hash . '?key=' . $existingKey, $result->getHeaderLine('Location')); | ||
105 | } | ||
106 | |||
107 | /** | ||
108 | * Test shaare private with a public bookmark. | ||
109 | */ | ||
110 | public function testSharePrivateWithPublicBookmark(): void | ||
111 | { | ||
112 | $hash = 'abcdcef'; | ||
113 | $request = $this->createMock(Request::class); | ||
114 | $response = new Response(); | ||
115 | |||
116 | $bookmark = (new Bookmark()) | ||
117 | ->setId(123) | ||
118 | ->setUrl('http://domain.tld') | ||
119 | ->setTitle('Title 123') | ||
120 | ->setPrivate(false) | ||
121 | ; | ||
122 | |||
123 | $this->container->bookmarkService | ||
124 | ->expects(static::once()) | ||
125 | ->method('findByHash') | ||
126 | ->with($hash) | ||
127 | ->willReturn($bookmark) | ||
128 | ; | ||
129 | $this->container->bookmarkService | ||
130 | ->expects(static::never()) | ||
131 | ->method('set') | ||
132 | ; | ||
133 | |||
134 | $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]); | ||
135 | |||
136 | static::assertSame(302, $result->getStatusCode()); | ||
137 | static::assertSame('/subfolder/shaare/' . $hash, $result->getHeaderLine('Location')); | ||
138 | } | ||
139 | } | ||
diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php new file mode 100644 index 00000000..ce8e112b --- /dev/null +++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php | |||
@@ -0,0 +1,63 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; | ||
6 | |||
7 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | ||
8 | use Shaarli\Front\Controller\Admin\ShaarePublishController; | ||
9 | use Shaarli\Http\HttpAccess; | ||
10 | use Shaarli\Http\MetadataRetriever; | ||
11 | use Shaarli\TestCase; | ||
12 | use Slim\Http\Request; | ||
13 | use Slim\Http\Response; | ||
14 | |||
15 | class DisplayCreateBatchFormTest extends TestCase | ||
16 | { | ||
17 | use FrontAdminControllerMockHelper; | ||
18 | |||
19 | /** @var ShaarePublishController */ | ||
20 | protected $controller; | ||
21 | |||
22 | public function setUp(): void | ||
23 | { | ||
24 | $this->createContainer(); | ||
25 | |||
26 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | ||
27 | $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class); | ||
28 | $this->controller = new ShaarePublishController($this->container); | ||
29 | } | ||
30 | |||
31 | /** | ||
32 | * TODO | ||
33 | */ | ||
34 | public function testDisplayCreateFormBatch(): void | ||
35 | { | ||
36 | $urls = [ | ||
37 | 'https://domain1.tld/url1', | ||
38 | 'https://domain2.tld/url2', | ||
39 | ' ', | ||
40 | 'https://domain3.tld/url3', | ||
41 | ]; | ||
42 | |||
43 | $request = $this->createMock(Request::class); | ||
44 | $request->method('getParam')->willReturnCallback(function (string $key) use ($urls): ?string { | ||
45 | return $key === 'urls' ? implode(PHP_EOL, $urls) : null; | ||
46 | }); | ||
47 | $response = new Response(); | ||
48 | |||
49 | $assignedVariables = []; | ||
50 | $this->assignTemplateVars($assignedVariables); | ||
51 | |||
52 | $result = $this->controller->displayCreateBatchForms($request, $response); | ||
53 | |||
54 | static::assertSame(200, $result->getStatusCode()); | ||
55 | static::assertSame('editlink.batch', (string) $result->getBody()); | ||
56 | |||
57 | static::assertTrue($assignedVariables['batch_mode']); | ||
58 | static::assertCount(3, $assignedVariables['links']); | ||
59 | static::assertSame($urls[0], $assignedVariables['links'][0]['link']['url']); | ||
60 | static::assertSame($urls[1], $assignedVariables['links'][1]['link']['url']); | ||
61 | static::assertSame($urls[3], $assignedVariables['links'][2]['link']['url']); | ||
62 | } | ||
63 | } | ||
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php index 2eb95251..964773da 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php +++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php | |||
@@ -2,13 +2,14 @@ | |||
2 | 2 | ||
3 | declare(strict_types=1); | 3 | declare(strict_types=1); |
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; | 5 | namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; |
6 | 6 | ||
7 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
9 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | 9 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; |
10 | use Shaarli\Front\Controller\Admin\ManageShaareController; | 10 | use Shaarli\Front\Controller\Admin\ShaarePublishController; |
11 | use Shaarli\Http\HttpAccess; | 11 | use Shaarli\Http\HttpAccess; |
12 | use Shaarli\Http\MetadataRetriever; | ||
12 | use Shaarli\TestCase; | 13 | use Shaarli\TestCase; |
13 | use Slim\Http\Request; | 14 | use Slim\Http\Request; |
14 | use Slim\Http\Response; | 15 | use Slim\Http\Response; |
@@ -17,7 +18,7 @@ class DisplayCreateFormTest extends TestCase | |||
17 | { | 18 | { |
18 | use FrontAdminControllerMockHelper; | 19 | use FrontAdminControllerMockHelper; |
19 | 20 | ||
20 | /** @var ManageShaareController */ | 21 | /** @var ShaarePublishController */ |
21 | protected $controller; | 22 | protected $controller; |
22 | 23 | ||
23 | public function setUp(): void | 24 | public function setUp(): void |
@@ -25,14 +26,15 @@ class DisplayCreateFormTest extends TestCase | |||
25 | $this->createContainer(); | 26 | $this->createContainer(); |
26 | 27 | ||
27 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | 28 | $this->container->httpAccess = $this->createMock(HttpAccess::class); |
28 | $this->controller = new ManageShaareController($this->container); | 29 | $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class); |
30 | $this->controller = new ShaarePublishController($this->container); | ||
29 | } | 31 | } |
30 | 32 | ||
31 | /** | 33 | /** |
32 | * Test displaying bookmark create form | 34 | * Test displaying bookmark create form |
33 | * Ensure that every step of the standard workflow works properly. | 35 | * Ensure that every step of the standard workflow works properly. |
34 | */ | 36 | */ |
35 | public function testDisplayCreateFormWithUrl(): void | 37 | public function testDisplayCreateFormWithUrlAndWithMetadataRetrieval(): void |
36 | { | 38 | { |
37 | $this->container->environment = [ | 39 | $this->container->environment = [ |
38 | 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc' | 40 | 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc' |
@@ -53,40 +55,20 @@ class DisplayCreateFormTest extends TestCase | |||
53 | }); | 55 | }); |
54 | $response = new Response(); | 56 | $response = new Response(); |
55 | 57 | ||
56 | $this->container->httpAccess | 58 | $this->container->conf = $this->createMock(ConfigManager::class); |
57 | ->expects(static::once()) | 59 | $this->container->conf->method('get')->willReturnCallback(function (string $param, $default) { |
58 | ->method('getCurlDownloadCallback') | 60 | if ($param === 'general.enable_async_metadata') { |
59 | ->willReturnCallback( | 61 | return false; |
60 | function (&$charset, &$title, &$description, &$tags) use ( | 62 | } |
61 | $remoteTitle, | 63 | |
62 | $remoteDesc, | 64 | return $default; |
63 | $remoteTags | 65 | }); |
64 | ): callable { | 66 | |
65 | return function () use ( | 67 | $this->container->metadataRetriever->expects(static::once())->method('retrieve')->willReturn([ |
66 | &$charset, | 68 | 'title' => $remoteTitle, |
67 | &$title, | 69 | 'description' => $remoteDesc, |
68 | &$description, | 70 | 'tags' => $remoteTags, |
69 | &$tags, | 71 | ]); |
70 | $remoteTitle, | ||
71 | $remoteDesc, | ||
72 | $remoteTags | ||
73 | ): void { | ||
74 | $charset = 'ISO-8859-1'; | ||
75 | $title = $remoteTitle; | ||
76 | $description = $remoteDesc; | ||
77 | $tags = $remoteTags; | ||
78 | }; | ||
79 | } | ||
80 | ) | ||
81 | ; | ||
82 | $this->container->httpAccess | ||
83 | ->expects(static::once()) | ||
84 | ->method('getHttpResponse') | ||
85 | ->with($expectedUrl, 30, 4194304) | ||
86 | ->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void { | ||
87 | $callback(); | ||
88 | }) | ||
89 | ; | ||
90 | 72 | ||
91 | $this->container->bookmarkService | 73 | $this->container->bookmarkService |
92 | ->expects(static::once()) | 74 | ->expects(static::once()) |
@@ -119,7 +101,73 @@ class DisplayCreateFormTest extends TestCase | |||
119 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); | 101 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); |
120 | static::assertSame($remoteTitle, $assignedVariables['link']['title']); | 102 | static::assertSame($remoteTitle, $assignedVariables['link']['title']); |
121 | static::assertSame($remoteDesc, $assignedVariables['link']['description']); | 103 | static::assertSame($remoteDesc, $assignedVariables['link']['description']); |
122 | static::assertSame($remoteTags, $assignedVariables['link']['tags']); | 104 | static::assertSame($remoteTags . ' ', $assignedVariables['link']['tags']); |
105 | static::assertFalse($assignedVariables['link']['private']); | ||
106 | |||
107 | static::assertTrue($assignedVariables['link_is_new']); | ||
108 | static::assertSame($referer, $assignedVariables['http_referer']); | ||
109 | static::assertSame($tags, $assignedVariables['tags']); | ||
110 | static::assertArrayHasKey('source', $assignedVariables); | ||
111 | static::assertArrayHasKey('default_private_links', $assignedVariables); | ||
112 | static::assertArrayHasKey('async_metadata', $assignedVariables); | ||
113 | static::assertArrayHasKey('retrieve_description', $assignedVariables); | ||
114 | } | ||
115 | |||
116 | /** | ||
117 | * Test displaying bookmark create form without any external metadata retrieval attempt | ||
118 | */ | ||
119 | public function testDisplayCreateFormWithUrlAndWithoutMetadata(): void | ||
120 | { | ||
121 | $this->container->environment = [ | ||
122 | 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc' | ||
123 | ]; | ||
124 | |||
125 | $assignedVariables = []; | ||
126 | $this->assignTemplateVars($assignedVariables); | ||
127 | |||
128 | $url = 'http://url.tld/other?part=3&utm_ad=pay#hash'; | ||
129 | $expectedUrl = str_replace('&utm_ad=pay', '', $url); | ||
130 | |||
131 | $request = $this->createMock(Request::class); | ||
132 | $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string { | ||
133 | return $key === 'post' ? $url : null; | ||
134 | }); | ||
135 | $response = new Response(); | ||
136 | |||
137 | $this->container->metadataRetriever->expects(static::never())->method('retrieve'); | ||
138 | |||
139 | $this->container->bookmarkService | ||
140 | ->expects(static::once()) | ||
141 | ->method('bookmarksCountPerTag') | ||
142 | ->willReturn($tags = ['tag1' => 2, 'tag2' => 1]) | ||
143 | ; | ||
144 | |||
145 | // Make sure that PluginManager hook is triggered | ||
146 | $this->container->pluginManager | ||
147 | ->expects(static::atLeastOnce()) | ||
148 | ->method('executeHooks') | ||
149 | ->withConsecutive(['render_editlink'], ['render_includes']) | ||
150 | ->willReturnCallback(function (string $hook, array $data): array { | ||
151 | if ('render_editlink' === $hook) { | ||
152 | static::assertSame('', $data['link']['title']); | ||
153 | static::assertSame('', $data['link']['description']); | ||
154 | } | ||
155 | |||
156 | return $data; | ||
157 | }) | ||
158 | ; | ||
159 | |||
160 | $result = $this->controller->displayCreateForm($request, $response); | ||
161 | |||
162 | static::assertSame(200, $result->getStatusCode()); | ||
163 | static::assertSame('editlink', (string) $result->getBody()); | ||
164 | |||
165 | static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']); | ||
166 | |||
167 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); | ||
168 | static::assertSame('', $assignedVariables['link']['title']); | ||
169 | static::assertSame('', $assignedVariables['link']['description']); | ||
170 | static::assertSame('', $assignedVariables['link']['tags']); | ||
123 | static::assertFalse($assignedVariables['link']['private']); | 171 | static::assertFalse($assignedVariables['link']['private']); |
124 | 172 | ||
125 | static::assertTrue($assignedVariables['link_is_new']); | 173 | static::assertTrue($assignedVariables['link_is_new']); |
@@ -127,6 +175,8 @@ class DisplayCreateFormTest extends TestCase | |||
127 | static::assertSame($tags, $assignedVariables['tags']); | 175 | static::assertSame($tags, $assignedVariables['tags']); |
128 | static::assertArrayHasKey('source', $assignedVariables); | 176 | static::assertArrayHasKey('source', $assignedVariables); |
129 | static::assertArrayHasKey('default_private_links', $assignedVariables); | 177 | static::assertArrayHasKey('default_private_links', $assignedVariables); |
178 | static::assertArrayHasKey('async_metadata', $assignedVariables); | ||
179 | static::assertArrayHasKey('retrieve_description', $assignedVariables); | ||
130 | } | 180 | } |
131 | 181 | ||
132 | /** | 182 | /** |
@@ -142,7 +192,7 @@ class DisplayCreateFormTest extends TestCase | |||
142 | 'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash', | 192 | 'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash', |
143 | 'title' => 'Provided Title', | 193 | 'title' => 'Provided Title', |
144 | 'description' => 'Provided description.', | 194 | 'description' => 'Provided description.', |
145 | 'tags' => 'abc def', | 195 | 'tags' => 'abc@def', |
146 | 'private' => '1', | 196 | 'private' => '1', |
147 | 'source' => 'apps', | 197 | 'source' => 'apps', |
148 | ]; | 198 | ]; |
@@ -166,7 +216,7 @@ class DisplayCreateFormTest extends TestCase | |||
166 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); | 216 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); |
167 | static::assertSame($parameters['title'], $assignedVariables['link']['title']); | 217 | static::assertSame($parameters['title'], $assignedVariables['link']['title']); |
168 | static::assertSame($parameters['description'], $assignedVariables['link']['description']); | 218 | static::assertSame($parameters['description'], $assignedVariables['link']['description']); |
169 | static::assertSame($parameters['tags'], $assignedVariables['link']['tags']); | 219 | static::assertSame($parameters['tags'] . '@', $assignedVariables['link']['tags']); |
170 | static::assertTrue($assignedVariables['link']['private']); | 220 | static::assertTrue($assignedVariables['link']['private']); |
171 | static::assertTrue($assignedVariables['link_is_new']); | 221 | static::assertTrue($assignedVariables['link_is_new']); |
172 | static::assertSame($parameters['source'], $assignedVariables['source']); | 222 | static::assertSame($parameters['source'], $assignedVariables['source']); |
@@ -310,7 +360,7 @@ class DisplayCreateFormTest extends TestCase | |||
310 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); | 360 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); |
311 | static::assertSame($title, $assignedVariables['link']['title']); | 361 | static::assertSame($title, $assignedVariables['link']['title']); |
312 | static::assertSame($description, $assignedVariables['link']['description']); | 362 | static::assertSame($description, $assignedVariables['link']['description']); |
313 | static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']); | 363 | static::assertSame(implode('@', $tags) . '@', $assignedVariables['link']['tags']); |
314 | static::assertTrue($assignedVariables['link']['private']); | 364 | static::assertTrue($assignedVariables['link']['private']); |
315 | static::assertSame($createdAt, $assignedVariables['link']['created']); | 365 | static::assertSame($createdAt, $assignedVariables['link']['created']); |
316 | } | 366 | } |
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php index 2dc3f41c..738cea12 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php +++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php | |||
@@ -2,12 +2,12 @@ | |||
2 | 2 | ||
3 | declare(strict_types=1); | 3 | declare(strict_types=1); |
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; | 5 | namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; |
6 | 6 | ||
7 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
9 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | 9 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; |
10 | use Shaarli\Front\Controller\Admin\ManageShaareController; | 10 | use Shaarli\Front\Controller\Admin\ShaarePublishController; |
11 | use Shaarli\Http\HttpAccess; | 11 | use Shaarli\Http\HttpAccess; |
12 | use Shaarli\Security\SessionManager; | 12 | use Shaarli\Security\SessionManager; |
13 | use Shaarli\TestCase; | 13 | use Shaarli\TestCase; |
@@ -18,7 +18,7 @@ class DisplayEditFormTest extends TestCase | |||
18 | { | 18 | { |
19 | use FrontAdminControllerMockHelper; | 19 | use FrontAdminControllerMockHelper; |
20 | 20 | ||
21 | /** @var ManageShaareController */ | 21 | /** @var ShaarePublishController */ |
22 | protected $controller; | 22 | protected $controller; |
23 | 23 | ||
24 | public function setUp(): void | 24 | public function setUp(): void |
@@ -26,7 +26,7 @@ class DisplayEditFormTest extends TestCase | |||
26 | $this->createContainer(); | 26 | $this->createContainer(); |
27 | 27 | ||
28 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | 28 | $this->container->httpAccess = $this->createMock(HttpAccess::class); |
29 | $this->controller = new ManageShaareController($this->container); | 29 | $this->controller = new ShaarePublishController($this->container); |
30 | } | 30 | } |
31 | 31 | ||
32 | /** | 32 | /** |
@@ -74,7 +74,7 @@ class DisplayEditFormTest extends TestCase | |||
74 | static::assertSame($url, $assignedVariables['link']['url']); | 74 | static::assertSame($url, $assignedVariables['link']['url']); |
75 | static::assertSame($title, $assignedVariables['link']['title']); | 75 | static::assertSame($title, $assignedVariables['link']['title']); |
76 | static::assertSame($description, $assignedVariables['link']['description']); | 76 | static::assertSame($description, $assignedVariables['link']['description']); |
77 | static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']); | 77 | static::assertSame(implode('@', $tags) . '@', $assignedVariables['link']['tags']); |
78 | static::assertTrue($assignedVariables['link']['private']); | 78 | static::assertTrue($assignedVariables['link']['private']); |
79 | static::assertSame($createdAt, $assignedVariables['link']['created']); | 79 | static::assertSame($createdAt, $assignedVariables['link']['created']); |
80 | } | 80 | } |
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php index f7a68226..b6a861bc 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php +++ b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php | |||
@@ -2,12 +2,12 @@ | |||
2 | 2 | ||
3 | declare(strict_types=1); | 3 | declare(strict_types=1); |
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; | 5 | namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; |
6 | 6 | ||
7 | use Shaarli\Bookmark\Bookmark; | 7 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
9 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | 9 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; |
10 | use Shaarli\Front\Controller\Admin\ManageShaareController; | 10 | use Shaarli\Front\Controller\Admin\ShaarePublishController; |
11 | use Shaarli\Front\Exception\WrongTokenException; | 11 | use Shaarli\Front\Exception\WrongTokenException; |
12 | use Shaarli\Http\HttpAccess; | 12 | use Shaarli\Http\HttpAccess; |
13 | use Shaarli\Security\SessionManager; | 13 | use Shaarli\Security\SessionManager; |
@@ -20,7 +20,7 @@ class SaveBookmarkTest extends TestCase | |||
20 | { | 20 | { |
21 | use FrontAdminControllerMockHelper; | 21 | use FrontAdminControllerMockHelper; |
22 | 22 | ||
23 | /** @var ManageShaareController */ | 23 | /** @var ShaarePublishController */ |
24 | protected $controller; | 24 | protected $controller; |
25 | 25 | ||
26 | public function setUp(): void | 26 | public function setUp(): void |
@@ -28,7 +28,7 @@ class SaveBookmarkTest extends TestCase | |||
28 | $this->createContainer(); | 28 | $this->createContainer(); |
29 | 29 | ||
30 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | 30 | $this->container->httpAccess = $this->createMock(HttpAccess::class); |
31 | $this->controller = new ManageShaareController($this->container); | 31 | $this->controller = new ShaarePublishController($this->container); |
32 | } | 32 | } |
33 | 33 | ||
34 | /** | 34 | /** |
@@ -66,23 +66,27 @@ class SaveBookmarkTest extends TestCase | |||
66 | $this->container->bookmarkService | 66 | $this->container->bookmarkService |
67 | ->expects(static::once()) | 67 | ->expects(static::once()) |
68 | ->method('addOrSet') | 68 | ->method('addOrSet') |
69 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { | 69 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark { |
70 | static::assertFalse($save); | 70 | static::assertFalse($save); |
71 | 71 | ||
72 | $checkBookmark($bookmark); | 72 | $checkBookmark($bookmark); |
73 | 73 | ||
74 | $bookmark->setId($id); | 74 | $bookmark->setId($id); |
75 | |||
76 | return $bookmark; | ||
75 | }) | 77 | }) |
76 | ; | 78 | ; |
77 | $this->container->bookmarkService | 79 | $this->container->bookmarkService |
78 | ->expects(static::once()) | 80 | ->expects(static::once()) |
79 | ->method('set') | 81 | ->method('set') |
80 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { | 82 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark { |
81 | static::assertTrue($save); | 83 | static::assertTrue($save); |
82 | 84 | ||
83 | $checkBookmark($bookmark); | 85 | $checkBookmark($bookmark); |
84 | 86 | ||
85 | static::assertSame($id, $bookmark->getId()); | 87 | static::assertSame($id, $bookmark->getId()); |
88 | |||
89 | return $bookmark; | ||
86 | }) | 90 | }) |
87 | ; | 91 | ; |
88 | 92 | ||
@@ -155,21 +159,25 @@ class SaveBookmarkTest extends TestCase | |||
155 | $this->container->bookmarkService | 159 | $this->container->bookmarkService |
156 | ->expects(static::once()) | 160 | ->expects(static::once()) |
157 | ->method('addOrSet') | 161 | ->method('addOrSet') |
158 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { | 162 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark { |
159 | static::assertFalse($save); | 163 | static::assertFalse($save); |
160 | 164 | ||
161 | $checkBookmark($bookmark); | 165 | $checkBookmark($bookmark); |
166 | |||
167 | return $bookmark; | ||
162 | }) | 168 | }) |
163 | ; | 169 | ; |
164 | $this->container->bookmarkService | 170 | $this->container->bookmarkService |
165 | ->expects(static::once()) | 171 | ->expects(static::once()) |
166 | ->method('set') | 172 | ->method('set') |
167 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { | 173 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark { |
168 | static::assertTrue($save); | 174 | static::assertTrue($save); |
169 | 175 | ||
170 | $checkBookmark($bookmark); | 176 | $checkBookmark($bookmark); |
171 | 177 | ||
172 | static::assertSame($id, $bookmark->getId()); | 178 | static::assertSame($id, $bookmark->getId()); |
179 | |||
180 | return $bookmark; | ||
173 | }) | 181 | }) |
174 | ; | 182 | ; |
175 | 183 | ||
@@ -201,7 +209,7 @@ class SaveBookmarkTest extends TestCase | |||
201 | /** | 209 | /** |
202 | * Test save a bookmark - try to retrieve the thumbnail | 210 | * Test save a bookmark - try to retrieve the thumbnail |
203 | */ | 211 | */ |
204 | public function testSaveBookmarkWithThumbnail(): void | 212 | public function testSaveBookmarkWithThumbnailSync(): void |
205 | { | 213 | { |
206 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; | 214 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; |
207 | 215 | ||
@@ -216,7 +224,13 @@ class SaveBookmarkTest extends TestCase | |||
216 | 224 | ||
217 | $this->container->conf = $this->createMock(ConfigManager::class); | 225 | $this->container->conf = $this->createMock(ConfigManager::class); |
218 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | 226 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { |
219 | return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; | 227 | if ($key === 'thumbnails.mode') { |
228 | return Thumbnailer::MODE_ALL; | ||
229 | } elseif ($key === 'general.enable_async_metadata') { | ||
230 | return false; | ||
231 | } | ||
232 | |||
233 | return $default; | ||
220 | }); | 234 | }); |
221 | 235 | ||
222 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); | 236 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); |
@@ -230,8 +244,10 @@ class SaveBookmarkTest extends TestCase | |||
230 | $this->container->bookmarkService | 244 | $this->container->bookmarkService |
231 | ->expects(static::once()) | 245 | ->expects(static::once()) |
232 | ->method('addOrSet') | 246 | ->method('addOrSet') |
233 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void { | 247 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): Bookmark { |
234 | static::assertSame($thumb, $bookmark->getThumbnail()); | 248 | static::assertSame($thumb, $bookmark->getThumbnail()); |
249 | |||
250 | return $bookmark; | ||
235 | }) | 251 | }) |
236 | ; | 252 | ; |
237 | 253 | ||
@@ -265,6 +281,51 @@ class SaveBookmarkTest extends TestCase | |||
265 | } | 281 | } |
266 | 282 | ||
267 | /** | 283 | /** |
284 | * Test save a bookmark - do not attempt to retrieve thumbnails if async mode is enabled. | ||
285 | */ | ||
286 | public function testSaveBookmarkWithThumbnailAsync(): void | ||
287 | { | ||
288 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; | ||
289 | |||
290 | $request = $this->createMock(Request::class); | ||
291 | $request | ||
292 | ->method('getParam') | ||
293 | ->willReturnCallback(function (string $key) use ($parameters): ?string { | ||
294 | return $parameters[$key] ?? null; | ||
295 | }) | ||
296 | ; | ||
297 | $response = new Response(); | ||
298 | |||
299 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
300 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | ||
301 | if ($key === 'thumbnails.mode') { | ||
302 | return Thumbnailer::MODE_ALL; | ||
303 | } elseif ($key === 'general.enable_async_metadata') { | ||
304 | return true; | ||
305 | } | ||
306 | |||
307 | return $default; | ||
308 | }); | ||
309 | |||
310 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); | ||
311 | $this->container->thumbnailer->expects(static::never())->method('get'); | ||
312 | |||
313 | $this->container->bookmarkService | ||
314 | ->expects(static::once()) | ||
315 | ->method('addOrSet') | ||
316 | ->willReturnCallback(function (Bookmark $bookmark): Bookmark { | ||
317 | static::assertNull($bookmark->getThumbnail()); | ||
318 | |||
319 | return $bookmark; | ||
320 | }) | ||
321 | ; | ||
322 | |||
323 | $result = $this->controller->save($request, $response); | ||
324 | |||
325 | static::assertSame(302, $result->getStatusCode()); | ||
326 | } | ||
327 | |||
328 | /** | ||
268 | * Change the password with a wrong existing password | 329 | * Change the password with a wrong existing password |
269 | */ | 330 | */ |
270 | public function testSaveBookmarkFromBookmarklet(): void | 331 | public function testSaveBookmarkFromBookmarklet(): void |
diff --git a/tests/front/controller/admin/ThumbnailsControllerTest.php b/tests/front/controller/admin/ThumbnailsControllerTest.php index f4a8acff..e5749654 100644 --- a/tests/front/controller/admin/ThumbnailsControllerTest.php +++ b/tests/front/controller/admin/ThumbnailsControllerTest.php | |||
@@ -89,8 +89,10 @@ class ThumbnailsControllerTest extends TestCase | |||
89 | $this->container->bookmarkService | 89 | $this->container->bookmarkService |
90 | ->expects(static::once()) | 90 | ->expects(static::once()) |
91 | ->method('set') | 91 | ->method('set') |
92 | ->willReturnCallback(function (Bookmark $bookmark) use ($thumb) { | 92 | ->willReturnCallback(function (Bookmark $bookmark) use ($thumb): Bookmark { |
93 | static::assertSame($thumb, $bookmark->getThumbnail()); | 93 | static::assertSame($thumb, $bookmark->getThumbnail()); |
94 | |||
95 | return $bookmark; | ||
94 | }) | 96 | }) |
95 | ; | 97 | ; |
96 | 98 | ||
diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php index 0c95df97..dec938f2 100644 --- a/tests/front/controller/visitor/BookmarkListControllerTest.php +++ b/tests/front/controller/visitor/BookmarkListControllerTest.php | |||
@@ -173,7 +173,7 @@ class BookmarkListControllerTest extends TestCase | |||
173 | $request = $this->createMock(Request::class); | 173 | $request = $this->createMock(Request::class); |
174 | $request->method('getParam')->willReturnCallback(function (string $key) { | 174 | $request->method('getParam')->willReturnCallback(function (string $key) { |
175 | if ('searchtags' === $key) { | 175 | if ('searchtags' === $key) { |
176 | return 'abc def'; | 176 | return 'abc@def'; |
177 | } | 177 | } |
178 | if ('searchterm' === $key) { | 178 | if ('searchterm' === $key) { |
179 | return 'ghi jkl'; | 179 | return 'ghi jkl'; |
@@ -204,7 +204,7 @@ class BookmarkListControllerTest extends TestCase | |||
204 | ->expects(static::once()) | 204 | ->expects(static::once()) |
205 | ->method('search') | 205 | ->method('search') |
206 | ->with( | 206 | ->with( |
207 | ['searchtags' => 'abc def', 'searchterm' => 'ghi jkl'], | 207 | ['searchtags' => 'abc@def', 'searchterm' => 'ghi jkl'], |
208 | 'private', | 208 | 'private', |
209 | false, | 209 | false, |
210 | true | 210 | true |
@@ -222,7 +222,7 @@ class BookmarkListControllerTest extends TestCase | |||
222 | static::assertSame('linklist', (string) $result->getBody()); | 222 | static::assertSame('linklist', (string) $result->getBody()); |
223 | 223 | ||
224 | static::assertSame('Search: ghi jkl [abc] [def] - Shaarli', $assignedVariables['pagetitle']); | 224 | static::assertSame('Search: ghi jkl [abc] [def] - Shaarli', $assignedVariables['pagetitle']); |
225 | static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc+def', $assignedVariables['previous_page_url']); | 225 | static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc%40def', $assignedVariables['previous_page_url']); |
226 | } | 226 | } |
227 | 227 | ||
228 | /** | 228 | /** |
@@ -292,6 +292,37 @@ class BookmarkListControllerTest extends TestCase | |||
292 | } | 292 | } |
293 | 293 | ||
294 | /** | 294 | /** |
295 | * Test GET /shaare/{hash}?key={key} - Find a link by hash using a private link. | ||
296 | */ | ||
297 | public function testPermalinkWithPrivateKey(): void | ||
298 | { | ||
299 | $hash = 'abcdef'; | ||
300 | $privateKey = 'this is a private key'; | ||
301 | |||
302 | $assignedVariables = []; | ||
303 | $this->assignTemplateVars($assignedVariables); | ||
304 | |||
305 | $request = $this->createMock(Request::class); | ||
306 | $request->method('getParam')->willReturnCallback(function (string $key, $default = null) use ($privateKey) { | ||
307 | return $key === 'key' ? $privateKey : $default; | ||
308 | }); | ||
309 | $response = new Response(); | ||
310 | |||
311 | $this->container->bookmarkService | ||
312 | ->expects(static::once()) | ||
313 | ->method('findByHash') | ||
314 | ->with($hash, $privateKey) | ||
315 | ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld')) | ||
316 | ; | ||
317 | |||
318 | $result = $this->controller->permalink($request, $response, ['hash' => $hash]); | ||
319 | |||
320 | static::assertSame(200, $result->getStatusCode()); | ||
321 | static::assertSame('linklist', (string) $result->getBody()); | ||
322 | static::assertCount(1, $assignedVariables['links']); | ||
323 | } | ||
324 | |||
325 | /** | ||
295 | * Test getting link list with thumbnail updates. | 326 | * Test getting link list with thumbnail updates. |
296 | * -> 2 thumbnails update, only 1 datastore write | 327 | * -> 2 thumbnails update, only 1 datastore write |
297 | */ | 328 | */ |
@@ -307,7 +338,13 @@ class BookmarkListControllerTest extends TestCase | |||
307 | $this->container->conf | 338 | $this->container->conf |
308 | ->method('get') | 339 | ->method('get') |
309 | ->willReturnCallback(function (string $key, $default) { | 340 | ->willReturnCallback(function (string $key, $default) { |
310 | return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; | 341 | if ($key === 'thumbnails.mode') { |
342 | return Thumbnailer::MODE_ALL; | ||
343 | } elseif ($key === 'general.enable_async_metadata') { | ||
344 | return false; | ||
345 | } | ||
346 | |||
347 | return $default; | ||
311 | }) | 348 | }) |
312 | ; | 349 | ; |
313 | 350 | ||
@@ -357,7 +394,13 @@ class BookmarkListControllerTest extends TestCase | |||
357 | $this->container->conf | 394 | $this->container->conf |
358 | ->method('get') | 395 | ->method('get') |
359 | ->willReturnCallback(function (string $key, $default) { | 396 | ->willReturnCallback(function (string $key, $default) { |
360 | return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; | 397 | if ($key === 'thumbnails.mode') { |
398 | return Thumbnailer::MODE_ALL; | ||
399 | } elseif ($key === 'general.enable_async_metadata') { | ||
400 | return false; | ||
401 | } | ||
402 | |||
403 | return $default; | ||
361 | }) | 404 | }) |
362 | ; | 405 | ; |
363 | 406 | ||
@@ -379,6 +422,47 @@ class BookmarkListControllerTest extends TestCase | |||
379 | } | 422 | } |
380 | 423 | ||
381 | /** | 424 | /** |
425 | * Test getting a permalink with thumbnail update with async setting: no update should run. | ||
426 | */ | ||
427 | public function testThumbnailUpdateFromPermalinkAsync(): void | ||
428 | { | ||
429 | $request = $this->createMock(Request::class); | ||
430 | $response = new Response(); | ||
431 | |||
432 | $this->container->loginManager = $this->createMock(LoginManager::class); | ||
433 | $this->container->loginManager->method('isLoggedIn')->willReturn(true); | ||
434 | |||
435 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
436 | $this->container->conf | ||
437 | ->method('get') | ||
438 | ->willReturnCallback(function (string $key, $default) { | ||
439 | if ($key === 'thumbnails.mode') { | ||
440 | return Thumbnailer::MODE_ALL; | ||
441 | } elseif ($key === 'general.enable_async_metadata') { | ||
442 | return true; | ||
443 | } | ||
444 | |||
445 | return $default; | ||
446 | }) | ||
447 | ; | ||
448 | |||
449 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); | ||
450 | $this->container->thumbnailer->expects(static::never())->method('get'); | ||
451 | |||
452 | $this->container->bookmarkService | ||
453 | ->expects(static::once()) | ||
454 | ->method('findByHash') | ||
455 | ->willReturn((new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1')) | ||
456 | ; | ||
457 | $this->container->bookmarkService->expects(static::never())->method('set'); | ||
458 | $this->container->bookmarkService->expects(static::never())->method('save'); | ||
459 | |||
460 | $result = $this->controller->permalink($request, $response, ['hash' => 'abc']); | ||
461 | |||
462 | static::assertSame(200, $result->getStatusCode()); | ||
463 | } | ||
464 | |||
465 | /** | ||
382 | * Trigger legacy controller in link list controller: permalink | 466 | * Trigger legacy controller in link list controller: permalink |
383 | */ | 467 | */ |
384 | public function testLegacyControllerPermalink(): void | 468 | public function testLegacyControllerPermalink(): void |
diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php index fc78bc13..70fbce54 100644 --- a/tests/front/controller/visitor/DailyControllerTest.php +++ b/tests/front/controller/visitor/DailyControllerTest.php | |||
@@ -28,52 +28,49 @@ class DailyControllerTest extends TestCase | |||
28 | public function testValidIndexControllerInvokeDefault(): void | 28 | public function testValidIndexControllerInvokeDefault(): void |
29 | { | 29 | { |
30 | $currentDay = new \DateTimeImmutable('2020-05-13'); | 30 | $currentDay = new \DateTimeImmutable('2020-05-13'); |
31 | $previousDate = new \DateTime('2 days ago 00:00:00'); | ||
32 | $nextDate = new \DateTime('today 00:00:00'); | ||
31 | 33 | ||
32 | $request = $this->createMock(Request::class); | 34 | $request = $this->createMock(Request::class); |
33 | $request->method('getQueryParam')->willReturn($currentDay->format('Ymd')); | 35 | $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { |
36 | return $key === 'day' ? $currentDay->format('Ymd') : null; | ||
37 | }); | ||
34 | $response = new Response(); | 38 | $response = new Response(); |
35 | 39 | ||
36 | // Save RainTPL assigned variables | 40 | // Save RainTPL assigned variables |
37 | $assignedVariables = []; | 41 | $assignedVariables = []; |
38 | $this->assignTemplateVars($assignedVariables); | 42 | $this->assignTemplateVars($assignedVariables); |
39 | 43 | ||
40 | // Links dataset: 2 links with thumbnails | ||
41 | $this->container->bookmarkService | ||
42 | ->expects(static::once()) | ||
43 | ->method('days') | ||
44 | ->willReturnCallback(function () use ($currentDay): array { | ||
45 | return [ | ||
46 | '20200510', | ||
47 | $currentDay->format('Ymd'), | ||
48 | '20200516', | ||
49 | ]; | ||
50 | }) | ||
51 | ; | ||
52 | $this->container->bookmarkService | 44 | $this->container->bookmarkService |
53 | ->expects(static::once()) | 45 | ->expects(static::once()) |
54 | ->method('filterDay') | 46 | ->method('findByDate') |
55 | ->willReturnCallback(function (): array { | 47 | ->willReturnCallback( |
56 | return [ | 48 | function ($from, $to, &$previous, &$next) use ($currentDay, $previousDate, $nextDate): array { |
57 | (new Bookmark()) | 49 | $previous = $previousDate; |
58 | ->setId(1) | 50 | $next = $nextDate; |
59 | ->setUrl('http://url.tld') | 51 | |
60 | ->setTitle(static::generateString(50)) | 52 | return [ |
61 | ->setDescription(static::generateString(500)) | 53 | (new Bookmark()) |
62 | , | 54 | ->setId(1) |
63 | (new Bookmark()) | 55 | ->setUrl('http://url.tld') |
64 | ->setId(2) | 56 | ->setTitle(static::generateString(50)) |
65 | ->setUrl('http://url2.tld') | 57 | ->setDescription(static::generateString(500)) |
66 | ->setTitle(static::generateString(50)) | 58 | , |
67 | ->setDescription(static::generateString(500)) | 59 | (new Bookmark()) |
68 | , | 60 | ->setId(2) |
69 | (new Bookmark()) | 61 | ->setUrl('http://url2.tld') |
70 | ->setId(3) | 62 | ->setTitle(static::generateString(50)) |
71 | ->setUrl('http://url3.tld') | 63 | ->setDescription(static::generateString(500)) |
72 | ->setTitle(static::generateString(50)) | 64 | , |
73 | ->setDescription(static::generateString(500)) | 65 | (new Bookmark()) |
74 | , | 66 | ->setId(3) |
75 | ]; | 67 | ->setUrl('http://url3.tld') |
76 | }) | 68 | ->setTitle(static::generateString(50)) |
69 | ->setDescription(static::generateString(500)) | ||
70 | , | ||
71 | ]; | ||
72 | } | ||
73 | ) | ||
77 | ; | 74 | ; |
78 | 75 | ||
79 | // Make sure that PluginManager hook is triggered | 76 | // Make sure that PluginManager hook is triggered |
@@ -81,20 +78,22 @@ class DailyControllerTest extends TestCase | |||
81 | ->expects(static::atLeastOnce()) | 78 | ->expects(static::atLeastOnce()) |
82 | ->method('executeHooks') | 79 | ->method('executeHooks') |
83 | ->withConsecutive(['render_daily']) | 80 | ->withConsecutive(['render_daily']) |
84 | ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { | 81 | ->willReturnCallback( |
85 | if ('render_daily' === $hook) { | 82 | function (string $hook, array $data, array $param) use ($currentDay, $previousDate, $nextDate): array { |
86 | static::assertArrayHasKey('linksToDisplay', $data); | 83 | if ('render_daily' === $hook) { |
87 | static::assertCount(3, $data['linksToDisplay']); | 84 | static::assertArrayHasKey('linksToDisplay', $data); |
88 | static::assertSame(1, $data['linksToDisplay'][0]['id']); | 85 | static::assertCount(3, $data['linksToDisplay']); |
89 | static::assertSame($currentDay->getTimestamp(), $data['day']); | 86 | static::assertSame(1, $data['linksToDisplay'][0]['id']); |
90 | static::assertSame('20200510', $data['previousday']); | 87 | static::assertSame($currentDay->getTimestamp(), $data['day']); |
91 | static::assertSame('20200516', $data['nextday']); | 88 | static::assertSame($previousDate->format('Ymd'), $data['previousday']); |
92 | 89 | static::assertSame($nextDate->format('Ymd'), $data['nextday']); | |
93 | static::assertArrayHasKey('loggedin', $param); | 90 | |
91 | static::assertArrayHasKey('loggedin', $param); | ||
92 | } | ||
93 | |||
94 | return $data; | ||
94 | } | 95 | } |
95 | 96 | ) | |
96 | return $data; | ||
97 | }) | ||
98 | ; | 97 | ; |
99 | 98 | ||
100 | $result = $this->controller->index($request, $response); | 99 | $result = $this->controller->index($request, $response); |
@@ -107,6 +106,11 @@ class DailyControllerTest extends TestCase | |||
107 | ); | 106 | ); |
108 | static::assertEquals($currentDay, $assignedVariables['dayDate']); | 107 | static::assertEquals($currentDay, $assignedVariables['dayDate']); |
109 | static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); | 108 | static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); |
109 | static::assertSame($previousDate->format('Ymd'), $assignedVariables['previousday']); | ||
110 | static::assertSame($nextDate->format('Ymd'), $assignedVariables['nextday']); | ||
111 | static::assertSame('day', $assignedVariables['type']); | ||
112 | static::assertSame('May 13, 2020', $assignedVariables['dayDesc']); | ||
113 | static::assertSame('Daily', $assignedVariables['localizedType']); | ||
110 | static::assertCount(3, $assignedVariables['linksToDisplay']); | 114 | static::assertCount(3, $assignedVariables['linksToDisplay']); |
111 | 115 | ||
112 | $link = $assignedVariables['linksToDisplay'][0]; | 116 | $link = $assignedVariables['linksToDisplay'][0]; |
@@ -171,27 +175,20 @@ class DailyControllerTest extends TestCase | |||
171 | $currentDay = new \DateTimeImmutable('2020-05-13'); | 175 | $currentDay = new \DateTimeImmutable('2020-05-13'); |
172 | 176 | ||
173 | $request = $this->createMock(Request::class); | 177 | $request = $this->createMock(Request::class); |
178 | $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { | ||
179 | return $key === 'day' ? $currentDay->format('Ymd') : null; | ||
180 | }); | ||
174 | $response = new Response(); | 181 | $response = new Response(); |
175 | 182 | ||
176 | // Save RainTPL assigned variables | 183 | // Save RainTPL assigned variables |
177 | $assignedVariables = []; | 184 | $assignedVariables = []; |
178 | $this->assignTemplateVars($assignedVariables); | 185 | $this->assignTemplateVars($assignedVariables); |
179 | 186 | ||
180 | // Links dataset: 2 links with thumbnails | ||
181 | $this->container->bookmarkService | 187 | $this->container->bookmarkService |
182 | ->expects(static::once()) | 188 | ->expects(static::once()) |
183 | ->method('days') | 189 | ->method('findByDate') |
184 | ->willReturnCallback(function () use ($currentDay): array { | 190 | ->willReturnCallback(function () use ($currentDay): array { |
185 | return [ | 191 | return [ |
186 | $currentDay->format($currentDay->format('Ymd')), | ||
187 | ]; | ||
188 | }) | ||
189 | ; | ||
190 | $this->container->bookmarkService | ||
191 | ->expects(static::once()) | ||
192 | ->method('filterDay') | ||
193 | ->willReturnCallback(function (): array { | ||
194 | return [ | ||
195 | (new Bookmark()) | 192 | (new Bookmark()) |
196 | ->setId(1) | 193 | ->setId(1) |
197 | ->setUrl('http://url.tld') | 194 | ->setUrl('http://url.tld') |
@@ -250,21 +247,11 @@ class DailyControllerTest extends TestCase | |||
250 | $assignedVariables = []; | 247 | $assignedVariables = []; |
251 | $this->assignTemplateVars($assignedVariables); | 248 | $this->assignTemplateVars($assignedVariables); |
252 | 249 | ||
253 | // Links dataset: 2 links with thumbnails | ||
254 | $this->container->bookmarkService | 250 | $this->container->bookmarkService |
255 | ->expects(static::once()) | 251 | ->expects(static::once()) |
256 | ->method('days') | 252 | ->method('findByDate') |
257 | ->willReturnCallback(function () use ($currentDay): array { | 253 | ->willReturnCallback(function () use ($currentDay): array { |
258 | return [ | 254 | return [ |
259 | $currentDay->format($currentDay->format('Ymd')), | ||
260 | ]; | ||
261 | }) | ||
262 | ; | ||
263 | $this->container->bookmarkService | ||
264 | ->expects(static::once()) | ||
265 | ->method('filterDay') | ||
266 | ->willReturnCallback(function (): array { | ||
267 | return [ | ||
268 | (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), | 255 | (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), |
269 | (new Bookmark()) | 256 | (new Bookmark()) |
270 | ->setId(2) | 257 | ->setId(2) |
@@ -320,14 +307,7 @@ class DailyControllerTest extends TestCase | |||
320 | // Links dataset: 2 links with thumbnails | 307 | // Links dataset: 2 links with thumbnails |
321 | $this->container->bookmarkService | 308 | $this->container->bookmarkService |
322 | ->expects(static::once()) | 309 | ->expects(static::once()) |
323 | ->method('days') | 310 | ->method('findByDate') |
324 | ->willReturnCallback(function (): array { | ||
325 | return []; | ||
326 | }) | ||
327 | ; | ||
328 | $this->container->bookmarkService | ||
329 | ->expects(static::once()) | ||
330 | ->method('filterDay') | ||
331 | ->willReturnCallback(function (): array { | 311 | ->willReturnCallback(function (): array { |
332 | return []; | 312 | return []; |
333 | }) | 313 | }) |
@@ -347,7 +327,7 @@ class DailyControllerTest extends TestCase | |||
347 | static::assertSame(200, $result->getStatusCode()); | 327 | static::assertSame(200, $result->getStatusCode()); |
348 | static::assertSame('daily', (string) $result->getBody()); | 328 | static::assertSame('daily', (string) $result->getBody()); |
349 | static::assertCount(0, $assignedVariables['linksToDisplay']); | 329 | static::assertCount(0, $assignedVariables['linksToDisplay']); |
350 | static::assertSame('Today', $assignedVariables['dayDesc']); | 330 | static::assertSame('Today - ' . (new \DateTime())->format('F j, Y'), $assignedVariables['dayDesc']); |
351 | static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); | 331 | static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); |
352 | static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); | 332 | static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); |
353 | } | 333 | } |
@@ -361,6 +341,7 @@ class DailyControllerTest extends TestCase | |||
361 | new \DateTimeImmutable('2020-05-17'), | 341 | new \DateTimeImmutable('2020-05-17'), |
362 | new \DateTimeImmutable('2020-05-15'), | 342 | new \DateTimeImmutable('2020-05-15'), |
363 | new \DateTimeImmutable('2020-05-13'), | 343 | new \DateTimeImmutable('2020-05-13'), |
344 | new \DateTimeImmutable('+1 month'), | ||
364 | ]; | 345 | ]; |
365 | 346 | ||
366 | $request = $this->createMock(Request::class); | 347 | $request = $this->createMock(Request::class); |
@@ -371,6 +352,7 @@ class DailyControllerTest extends TestCase | |||
371 | (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), | 352 | (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), |
372 | (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), | 353 | (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), |
373 | (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), | 354 | (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), |
355 | (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'), | ||
374 | ]); | 356 | ]); |
375 | 357 | ||
376 | $this->container->pageCacheManager | 358 | $this->container->pageCacheManager |
@@ -397,13 +379,14 @@ class DailyControllerTest extends TestCase | |||
397 | static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); | 379 | static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); |
398 | static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']); | 380 | static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']); |
399 | static::assertFalse($assignedVariables['hide_timestamps']); | 381 | static::assertFalse($assignedVariables['hide_timestamps']); |
400 | static::assertCount(2, $assignedVariables['days']); | 382 | static::assertCount(3, $assignedVariables['days']); |
401 | 383 | ||
402 | $day = $assignedVariables['days'][$dates[0]->format('Ymd')]; | 384 | $day = $assignedVariables['days'][$dates[0]->format('Ymd')]; |
385 | $date = $dates[0]->setTime(23, 59, 59); | ||
403 | 386 | ||
404 | static::assertEquals($dates[0], $day['date']); | 387 | static::assertEquals($date, $day['date']); |
405 | static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']); | 388 | static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); |
406 | static::assertSame(format_date($dates[0], false), $day['date_human']); | 389 | static::assertSame(format_date($date, false), $day['date_human']); |
407 | static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); | 390 | static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); |
408 | static::assertCount(1, $day['links']); | 391 | static::assertCount(1, $day['links']); |
409 | static::assertSame(1, $day['links'][0]['id']); | 392 | static::assertSame(1, $day['links'][0]['id']); |
@@ -411,10 +394,11 @@ class DailyControllerTest extends TestCase | |||
411 | static::assertEquals($dates[0], $day['links'][0]['created']); | 394 | static::assertEquals($dates[0], $day['links'][0]['created']); |
412 | 395 | ||
413 | $day = $assignedVariables['days'][$dates[1]->format('Ymd')]; | 396 | $day = $assignedVariables['days'][$dates[1]->format('Ymd')]; |
397 | $date = $dates[1]->setTime(23, 59, 59); | ||
414 | 398 | ||
415 | static::assertEquals($dates[1], $day['date']); | 399 | static::assertEquals($date, $day['date']); |
416 | static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']); | 400 | static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); |
417 | static::assertSame(format_date($dates[1], false), $day['date_human']); | 401 | static::assertSame(format_date($date, false), $day['date_human']); |
418 | static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); | 402 | static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); |
419 | static::assertCount(2, $day['links']); | 403 | static::assertCount(2, $day['links']); |
420 | 404 | ||
@@ -424,6 +408,18 @@ class DailyControllerTest extends TestCase | |||
424 | static::assertSame(3, $day['links'][1]['id']); | 408 | static::assertSame(3, $day['links'][1]['id']); |
425 | static::assertSame('http://domain.tld/3', $day['links'][1]['url']); | 409 | static::assertSame('http://domain.tld/3', $day['links'][1]['url']); |
426 | static::assertEquals($dates[1], $day['links'][1]['created']); | 410 | static::assertEquals($dates[1], $day['links'][1]['created']); |
411 | |||
412 | $day = $assignedVariables['days'][$dates[2]->format('Ymd')]; | ||
413 | $date = $dates[2]->setTime(23, 59, 59); | ||
414 | |||
415 | static::assertEquals($date, $day['date']); | ||
416 | static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); | ||
417 | static::assertSame(format_date($date, false), $day['date_human']); | ||
418 | static::assertSame('http://shaarli/subfolder/daily?day='. $dates[2]->format('Ymd'), $day['absolute_url']); | ||
419 | static::assertCount(1, $day['links']); | ||
420 | static::assertSame(4, $day['links'][0]['id']); | ||
421 | static::assertSame('http://domain.tld/4', $day['links'][0]['url']); | ||
422 | static::assertEquals($dates[2], $day['links'][0]['created']); | ||
427 | } | 423 | } |
428 | 424 | ||
429 | /** | 425 | /** |
@@ -475,4 +471,246 @@ class DailyControllerTest extends TestCase | |||
475 | static::assertFalse($assignedVariables['hide_timestamps']); | 471 | static::assertFalse($assignedVariables['hide_timestamps']); |
476 | static::assertCount(0, $assignedVariables['days']); | 472 | static::assertCount(0, $assignedVariables['days']); |
477 | } | 473 | } |
474 | |||
475 | /** | ||
476 | * Test simple display index with week parameter | ||
477 | */ | ||
478 | public function testSimpleIndexWeekly(): void | ||
479 | { | ||
480 | $currentDay = new \DateTimeImmutable('2020-05-13'); | ||
481 | $expectedDay = new \DateTimeImmutable('2020-05-11'); | ||
482 | |||
483 | $request = $this->createMock(Request::class); | ||
484 | $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { | ||
485 | return $key === 'week' ? $currentDay->format('YW') : null; | ||
486 | }); | ||
487 | $response = new Response(); | ||
488 | |||
489 | // Save RainTPL assigned variables | ||
490 | $assignedVariables = []; | ||
491 | $this->assignTemplateVars($assignedVariables); | ||
492 | |||
493 | $this->container->bookmarkService | ||
494 | ->expects(static::once()) | ||
495 | ->method('findByDate') | ||
496 | ->willReturnCallback( | ||
497 | function (): array { | ||
498 | return [ | ||
499 | (new Bookmark()) | ||
500 | ->setId(1) | ||
501 | ->setUrl('http://url.tld') | ||
502 | ->setTitle(static::generateString(50)) | ||
503 | ->setDescription(static::generateString(500)) | ||
504 | , | ||
505 | (new Bookmark()) | ||
506 | ->setId(2) | ||
507 | ->setUrl('http://url2.tld') | ||
508 | ->setTitle(static::generateString(50)) | ||
509 | ->setDescription(static::generateString(500)) | ||
510 | , | ||
511 | ]; | ||
512 | } | ||
513 | ) | ||
514 | ; | ||
515 | |||
516 | $result = $this->controller->index($request, $response); | ||
517 | |||
518 | static::assertSame(200, $result->getStatusCode()); | ||
519 | static::assertSame('daily', (string) $result->getBody()); | ||
520 | static::assertSame( | ||
521 | 'Weekly - Week 20 (May 11, 2020) - Shaarli', | ||
522 | $assignedVariables['pagetitle'] | ||
523 | ); | ||
524 | |||
525 | static::assertCount(2, $assignedVariables['linksToDisplay']); | ||
526 | static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']); | ||
527 | static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); | ||
528 | static::assertSame('', $assignedVariables['previousday']); | ||
529 | static::assertSame('', $assignedVariables['nextday']); | ||
530 | static::assertSame('Week 20 (May 11, 2020)', $assignedVariables['dayDesc']); | ||
531 | static::assertSame('week', $assignedVariables['type']); | ||
532 | static::assertSame('Weekly', $assignedVariables['localizedType']); | ||
533 | } | ||
534 | |||
535 | /** | ||
536 | * Test simple display index with month parameter | ||
537 | */ | ||
538 | public function testSimpleIndexMonthly(): void | ||
539 | { | ||
540 | $currentDay = new \DateTimeImmutable('2020-05-13'); | ||
541 | $expectedDay = new \DateTimeImmutable('2020-05-01'); | ||
542 | |||
543 | $request = $this->createMock(Request::class); | ||
544 | $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { | ||
545 | return $key === 'month' ? $currentDay->format('Ym') : null; | ||
546 | }); | ||
547 | $response = new Response(); | ||
548 | |||
549 | // Save RainTPL assigned variables | ||
550 | $assignedVariables = []; | ||
551 | $this->assignTemplateVars($assignedVariables); | ||
552 | |||
553 | $this->container->bookmarkService | ||
554 | ->expects(static::once()) | ||
555 | ->method('findByDate') | ||
556 | ->willReturnCallback( | ||
557 | function (): array { | ||
558 | return [ | ||
559 | (new Bookmark()) | ||
560 | ->setId(1) | ||
561 | ->setUrl('http://url.tld') | ||
562 | ->setTitle(static::generateString(50)) | ||
563 | ->setDescription(static::generateString(500)) | ||
564 | , | ||
565 | (new Bookmark()) | ||
566 | ->setId(2) | ||
567 | ->setUrl('http://url2.tld') | ||
568 | ->setTitle(static::generateString(50)) | ||
569 | ->setDescription(static::generateString(500)) | ||
570 | , | ||
571 | ]; | ||
572 | } | ||
573 | ) | ||
574 | ; | ||
575 | |||
576 | $result = $this->controller->index($request, $response); | ||
577 | |||
578 | static::assertSame(200, $result->getStatusCode()); | ||
579 | static::assertSame('daily', (string) $result->getBody()); | ||
580 | static::assertSame( | ||
581 | 'Monthly - May, 2020 - Shaarli', | ||
582 | $assignedVariables['pagetitle'] | ||
583 | ); | ||
584 | |||
585 | static::assertCount(2, $assignedVariables['linksToDisplay']); | ||
586 | static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']); | ||
587 | static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); | ||
588 | static::assertSame('', $assignedVariables['previousday']); | ||
589 | static::assertSame('', $assignedVariables['nextday']); | ||
590 | static::assertSame('May, 2020', $assignedVariables['dayDesc']); | ||
591 | static::assertSame('month', $assignedVariables['type']); | ||
592 | static::assertSame('Monthly', $assignedVariables['localizedType']); | ||
593 | } | ||
594 | |||
595 | /** | ||
596 | * Test simple display RSS with week parameter | ||
597 | */ | ||
598 | public function testSimpleRssWeekly(): void | ||
599 | { | ||
600 | $dates = [ | ||
601 | new \DateTimeImmutable('2020-05-19'), | ||
602 | new \DateTimeImmutable('2020-05-13'), | ||
603 | ]; | ||
604 | $expectedDates = [ | ||
605 | new \DateTimeImmutable('2020-05-24 23:59:59'), | ||
606 | new \DateTimeImmutable('2020-05-17 23:59:59'), | ||
607 | ]; | ||
608 | |||
609 | $this->container->environment['QUERY_STRING'] = 'week'; | ||
610 | $request = $this->createMock(Request::class); | ||
611 | $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string { | ||
612 | return $key === 'week' ? '' : null; | ||
613 | }); | ||
614 | $response = new Response(); | ||
615 | |||
616 | $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ | ||
617 | (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), | ||
618 | (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), | ||
619 | (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), | ||
620 | ]); | ||
621 | |||
622 | // Save RainTPL assigned variables | ||
623 | $assignedVariables = []; | ||
624 | $this->assignTemplateVars($assignedVariables); | ||
625 | |||
626 | $result = $this->controller->rss($request, $response); | ||
627 | |||
628 | static::assertSame(200, $result->getStatusCode()); | ||
629 | static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); | ||
630 | static::assertSame('dailyrss', (string) $result->getBody()); | ||
631 | static::assertSame('Shaarli', $assignedVariables['title']); | ||
632 | static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); | ||
633 | static::assertSame('http://shaarli/subfolder/daily-rss?week', $assignedVariables['page_url']); | ||
634 | static::assertFalse($assignedVariables['hide_timestamps']); | ||
635 | static::assertCount(2, $assignedVariables['days']); | ||
636 | |||
637 | $day = $assignedVariables['days'][$dates[0]->format('YW')]; | ||
638 | $date = $expectedDates[0]; | ||
639 | |||
640 | static::assertEquals($date, $day['date']); | ||
641 | static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); | ||
642 | static::assertSame('Week 21 (May 18, 2020)', $day['date_human']); | ||
643 | static::assertSame('http://shaarli/subfolder/daily?week='. $dates[0]->format('YW'), $day['absolute_url']); | ||
644 | static::assertCount(1, $day['links']); | ||
645 | |||
646 | $day = $assignedVariables['days'][$dates[1]->format('YW')]; | ||
647 | $date = $expectedDates[1]; | ||
648 | |||
649 | static::assertEquals($date, $day['date']); | ||
650 | static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); | ||
651 | static::assertSame('Week 20 (May 11, 2020)', $day['date_human']); | ||
652 | static::assertSame('http://shaarli/subfolder/daily?week='. $dates[1]->format('YW'), $day['absolute_url']); | ||
653 | static::assertCount(2, $day['links']); | ||
654 | } | ||
655 | |||
656 | /** | ||
657 | * Test simple display RSS with month parameter | ||
658 | */ | ||
659 | public function testSimpleRssMonthly(): void | ||
660 | { | ||
661 | $dates = [ | ||
662 | new \DateTimeImmutable('2020-05-19'), | ||
663 | new \DateTimeImmutable('2020-04-13'), | ||
664 | ]; | ||
665 | $expectedDates = [ | ||
666 | new \DateTimeImmutable('2020-05-31 23:59:59'), | ||
667 | new \DateTimeImmutable('2020-04-30 23:59:59'), | ||
668 | ]; | ||
669 | |||
670 | $this->container->environment['QUERY_STRING'] = 'month'; | ||
671 | $request = $this->createMock(Request::class); | ||
672 | $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string { | ||
673 | return $key === 'month' ? '' : null; | ||
674 | }); | ||
675 | $response = new Response(); | ||
676 | |||
677 | $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ | ||
678 | (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), | ||
679 | (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), | ||
680 | (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), | ||
681 | ]); | ||
682 | |||
683 | // Save RainTPL assigned variables | ||
684 | $assignedVariables = []; | ||
685 | $this->assignTemplateVars($assignedVariables); | ||
686 | |||
687 | $result = $this->controller->rss($request, $response); | ||
688 | |||
689 | static::assertSame(200, $result->getStatusCode()); | ||
690 | static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); | ||
691 | static::assertSame('dailyrss', (string) $result->getBody()); | ||
692 | static::assertSame('Shaarli', $assignedVariables['title']); | ||
693 | static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); | ||
694 | static::assertSame('http://shaarli/subfolder/daily-rss?month', $assignedVariables['page_url']); | ||
695 | static::assertFalse($assignedVariables['hide_timestamps']); | ||
696 | static::assertCount(2, $assignedVariables['days']); | ||
697 | |||
698 | $day = $assignedVariables['days'][$dates[0]->format('Ym')]; | ||
699 | $date = $expectedDates[0]; | ||
700 | |||
701 | static::assertEquals($date, $day['date']); | ||
702 | static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); | ||
703 | static::assertSame('May, 2020', $day['date_human']); | ||
704 | static::assertSame('http://shaarli/subfolder/daily?month='. $dates[0]->format('Ym'), $day['absolute_url']); | ||
705 | static::assertCount(1, $day['links']); | ||
706 | |||
707 | $day = $assignedVariables['days'][$dates[1]->format('Ym')]; | ||
708 | $date = $expectedDates[1]; | ||
709 | |||
710 | static::assertEquals($date, $day['date']); | ||
711 | static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); | ||
712 | static::assertSame('April, 2020', $day['date_human']); | ||
713 | static::assertSame('http://shaarli/subfolder/daily?month='. $dates[1]->format('Ym'), $day['absolute_url']); | ||
714 | static::assertCount(2, $day['links']); | ||
715 | } | ||
478 | } | 716 | } |
diff --git a/tests/front/controller/visitor/ErrorControllerTest.php b/tests/front/controller/visitor/ErrorControllerTest.php index 75408cf4..e18a6fa2 100644 --- a/tests/front/controller/visitor/ErrorControllerTest.php +++ b/tests/front/controller/visitor/ErrorControllerTest.php | |||
@@ -50,7 +50,31 @@ class ErrorControllerTest extends TestCase | |||
50 | } | 50 | } |
51 | 51 | ||
52 | /** | 52 | /** |
53 | * Test displaying error with any exception (no debug): only display an error occurred with HTTP 500. | 53 | * Test displaying error with any exception (no debug) while logged in: |
54 | * display full error details | ||
55 | */ | ||
56 | public function testDisplayAnyExceptionErrorNoDebugLoggedIn(): void | ||
57 | { | ||
58 | $request = $this->createMock(Request::class); | ||
59 | $response = new Response(); | ||
60 | |||
61 | // Save RainTPL assigned variables | ||
62 | $assignedVariables = []; | ||
63 | $this->assignTemplateVars($assignedVariables); | ||
64 | |||
65 | $this->container->loginManager->method('isLoggedIn')->willReturn(true); | ||
66 | |||
67 | $result = ($this->controller)($request, $response, new \Exception('abc')); | ||
68 | |||
69 | static::assertSame(500, $result->getStatusCode()); | ||
70 | static::assertSame('Error: abc', $assignedVariables['message']); | ||
71 | static::assertContainsPolyfill('Please report it on Github', $assignedVariables['text']); | ||
72 | static::assertArrayHasKey('stacktrace', $assignedVariables); | ||
73 | } | ||
74 | |||
75 | /** | ||
76 | * Test displaying error with any exception (no debug) while logged out: | ||
77 | * display standard error without detail | ||
54 | */ | 78 | */ |
55 | public function testDisplayAnyExceptionErrorNoDebug(): void | 79 | public function testDisplayAnyExceptionErrorNoDebug(): void |
56 | { | 80 | { |
@@ -61,10 +85,13 @@ class ErrorControllerTest extends TestCase | |||
61 | $assignedVariables = []; | 85 | $assignedVariables = []; |
62 | $this->assignTemplateVars($assignedVariables); | 86 | $this->assignTemplateVars($assignedVariables); |
63 | 87 | ||
88 | $this->container->loginManager->method('isLoggedIn')->willReturn(false); | ||
89 | |||
64 | $result = ($this->controller)($request, $response, new \Exception('abc')); | 90 | $result = ($this->controller)($request, $response, new \Exception('abc')); |
65 | 91 | ||
66 | static::assertSame(500, $result->getStatusCode()); | 92 | static::assertSame(500, $result->getStatusCode()); |
67 | static::assertSame('An unexpected error occurred.', $assignedVariables['message']); | 93 | static::assertSame('An unexpected error occurred.', $assignedVariables['message']); |
94 | static::assertArrayNotHasKey('text', $assignedVariables); | ||
68 | static::assertArrayNotHasKey('stacktrace', $assignedVariables); | 95 | static::assertArrayNotHasKey('stacktrace', $assignedVariables); |
69 | } | 96 | } |
70 | } | 97 | } |
diff --git a/tests/front/controller/visitor/FrontControllerMockHelper.php b/tests/front/controller/visitor/FrontControllerMockHelper.php index fc0bb7d1..02229f68 100644 --- a/tests/front/controller/visitor/FrontControllerMockHelper.php +++ b/tests/front/controller/visitor/FrontControllerMockHelper.php | |||
@@ -41,6 +41,10 @@ trait FrontControllerMockHelper | |||
41 | // Config | 41 | // Config |
42 | $this->container->conf = $this->createMock(ConfigManager::class); | 42 | $this->container->conf = $this->createMock(ConfigManager::class); |
43 | $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { | 43 | $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { |
44 | if ($parameter === 'general.tags_separator') { | ||
45 | return '@'; | ||
46 | } | ||
47 | |||
44 | return $default === null ? $parameter : $default; | 48 | return $default === null ? $parameter : $default; |
45 | }); | 49 | }); |
46 | 50 | ||
diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php index 345ad544..2105ed77 100644 --- a/tests/front/controller/visitor/InstallControllerTest.php +++ b/tests/front/controller/visitor/InstallControllerTest.php | |||
@@ -79,6 +79,15 @@ class InstallControllerTest extends TestCase | |||
79 | static::assertIsArray($assignedVariables['languages']); | 79 | static::assertIsArray($assignedVariables['languages']); |
80 | static::assertSame('Automatic', $assignedVariables['languages']['auto']); | 80 | static::assertSame('Automatic', $assignedVariables['languages']['auto']); |
81 | static::assertSame('French', $assignedVariables['languages']['fr']); | 81 | static::assertSame('French', $assignedVariables['languages']['fr']); |
82 | |||
83 | static::assertSame(PHP_VERSION, $assignedVariables['php_version']); | ||
84 | static::assertArrayHasKey('php_has_reached_eol', $assignedVariables); | ||
85 | static::assertArrayHasKey('php_eol', $assignedVariables); | ||
86 | static::assertArrayHasKey('php_extensions', $assignedVariables); | ||
87 | static::assertArrayHasKey('permissions', $assignedVariables); | ||
88 | static::assertEmpty($assignedVariables['permissions']); | ||
89 | |||
90 | static::assertSame('Install Shaarli', $assignedVariables['pagetitle']); | ||
82 | } | 91 | } |
83 | 92 | ||
84 | /** | 93 | /** |
diff --git a/tests/front/controller/visitor/LoginControllerTest.php b/tests/front/controller/visitor/LoginControllerTest.php index 1312ccb7..00d9eab3 100644 --- a/tests/front/controller/visitor/LoginControllerTest.php +++ b/tests/front/controller/visitor/LoginControllerTest.php | |||
@@ -195,7 +195,7 @@ class LoginControllerTest extends TestCase | |||
195 | $this->container->loginManager | 195 | $this->container->loginManager |
196 | ->expects(static::once()) | 196 | ->expects(static::once()) |
197 | ->method('checkCredentials') | 197 | ->method('checkCredentials') |
198 | ->with('1.2.3.4', '1.2.3.4', 'bob', 'pass') | 198 | ->with('1.2.3.4', 'bob', 'pass') |
199 | ->willReturn(true) | 199 | ->willReturn(true) |
200 | ; | 200 | ; |
201 | $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8))); | 201 | $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8))); |
diff --git a/tests/front/controller/visitor/TagCloudControllerTest.php b/tests/front/controller/visitor/TagCloudControllerTest.php index 9305612e..4915573d 100644 --- a/tests/front/controller/visitor/TagCloudControllerTest.php +++ b/tests/front/controller/visitor/TagCloudControllerTest.php | |||
@@ -100,7 +100,7 @@ class TagCloudControllerTest extends TestCase | |||
100 | ->with() | 100 | ->with() |
101 | ->willReturnCallback(function (string $key): ?string { | 101 | ->willReturnCallback(function (string $key): ?string { |
102 | if ('searchtags' === $key) { | 102 | if ('searchtags' === $key) { |
103 | return 'ghi def'; | 103 | return 'ghi@def'; |
104 | } | 104 | } |
105 | 105 | ||
106 | return null; | 106 | return null; |
@@ -131,7 +131,7 @@ class TagCloudControllerTest extends TestCase | |||
131 | ->withConsecutive(['render_tagcloud']) | 131 | ->withConsecutive(['render_tagcloud']) |
132 | ->willReturnCallback(function (string $hook, array $data, array $param): array { | 132 | ->willReturnCallback(function (string $hook, array $data, array $param): array { |
133 | if ('render_tagcloud' === $hook) { | 133 | if ('render_tagcloud' === $hook) { |
134 | static::assertSame('ghi def', $data['search_tags']); | 134 | static::assertSame('ghi@def@', $data['search_tags']); |
135 | static::assertCount(1, $data['tags']); | 135 | static::assertCount(1, $data['tags']); |
136 | 136 | ||
137 | static::assertArrayHasKey('loggedin', $param); | 137 | static::assertArrayHasKey('loggedin', $param); |
@@ -147,7 +147,7 @@ class TagCloudControllerTest extends TestCase | |||
147 | static::assertSame('tag.cloud', (string) $result->getBody()); | 147 | static::assertSame('tag.cloud', (string) $result->getBody()); |
148 | static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']); | 148 | static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']); |
149 | 149 | ||
150 | static::assertSame('ghi def', $assignedVariables['search_tags']); | 150 | static::assertSame('ghi@def@', $assignedVariables['search_tags']); |
151 | static::assertCount(1, $assignedVariables['tags']); | 151 | static::assertCount(1, $assignedVariables['tags']); |
152 | 152 | ||
153 | static::assertArrayHasKey('abc', $assignedVariables['tags']); | 153 | static::assertArrayHasKey('abc', $assignedVariables['tags']); |
@@ -277,7 +277,7 @@ class TagCloudControllerTest extends TestCase | |||
277 | ->with() | 277 | ->with() |
278 | ->willReturnCallback(function (string $key): ?string { | 278 | ->willReturnCallback(function (string $key): ?string { |
279 | if ('searchtags' === $key) { | 279 | if ('searchtags' === $key) { |
280 | return 'ghi def'; | 280 | return 'ghi@def'; |
281 | } elseif ('sort' === $key) { | 281 | } elseif ('sort' === $key) { |
282 | return 'alpha'; | 282 | return 'alpha'; |
283 | } | 283 | } |
@@ -310,7 +310,7 @@ class TagCloudControllerTest extends TestCase | |||
310 | ->withConsecutive(['render_taglist']) | 310 | ->withConsecutive(['render_taglist']) |
311 | ->willReturnCallback(function (string $hook, array $data, array $param): array { | 311 | ->willReturnCallback(function (string $hook, array $data, array $param): array { |
312 | if ('render_taglist' === $hook) { | 312 | if ('render_taglist' === $hook) { |
313 | static::assertSame('ghi def', $data['search_tags']); | 313 | static::assertSame('ghi@def@', $data['search_tags']); |
314 | static::assertCount(1, $data['tags']); | 314 | static::assertCount(1, $data['tags']); |
315 | 315 | ||
316 | static::assertArrayHasKey('loggedin', $param); | 316 | static::assertArrayHasKey('loggedin', $param); |
@@ -326,7 +326,7 @@ class TagCloudControllerTest extends TestCase | |||
326 | static::assertSame('tag.list', (string) $result->getBody()); | 326 | static::assertSame('tag.list', (string) $result->getBody()); |
327 | static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']); | 327 | static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']); |
328 | 328 | ||
329 | static::assertSame('ghi def', $assignedVariables['search_tags']); | 329 | static::assertSame('ghi@def@', $assignedVariables['search_tags']); |
330 | static::assertCount(1, $assignedVariables['tags']); | 330 | static::assertCount(1, $assignedVariables['tags']); |
331 | static::assertSame(3, $assignedVariables['tags']['abc']); | 331 | static::assertSame(3, $assignedVariables['tags']['abc']); |
332 | } | 332 | } |
diff --git a/tests/front/controller/visitor/TagControllerTest.php b/tests/front/controller/visitor/TagControllerTest.php index 750ea02d..5a556c6d 100644 --- a/tests/front/controller/visitor/TagControllerTest.php +++ b/tests/front/controller/visitor/TagControllerTest.php | |||
@@ -50,7 +50,7 @@ class TagControllerTest extends TestCase | |||
50 | 50 | ||
51 | static::assertInstanceOf(Response::class, $result); | 51 | static::assertInstanceOf(Response::class, $result); |
52 | static::assertSame(302, $result->getStatusCode()); | 52 | static::assertSame(302, $result->getStatusCode()); |
53 | static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); | 53 | static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location')); |
54 | } | 54 | } |
55 | 55 | ||
56 | public function testAddTagWithoutRefererAndExistingSearch(): void | 56 | public function testAddTagWithoutRefererAndExistingSearch(): void |
@@ -80,7 +80,7 @@ class TagControllerTest extends TestCase | |||
80 | 80 | ||
81 | static::assertInstanceOf(Response::class, $result); | 81 | static::assertInstanceOf(Response::class, $result); |
82 | static::assertSame(302, $result->getStatusCode()); | 82 | static::assertSame(302, $result->getStatusCode()); |
83 | static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); | 83 | static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location')); |
84 | } | 84 | } |
85 | 85 | ||
86 | public function testAddTagResetPagination(): void | 86 | public function testAddTagResetPagination(): void |
@@ -96,7 +96,7 @@ class TagControllerTest extends TestCase | |||
96 | 96 | ||
97 | static::assertInstanceOf(Response::class, $result); | 97 | static::assertInstanceOf(Response::class, $result); |
98 | static::assertSame(302, $result->getStatusCode()); | 98 | static::assertSame(302, $result->getStatusCode()); |
99 | static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); | 99 | static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location')); |
100 | } | 100 | } |
101 | 101 | ||
102 | public function testAddTagWithRefererAndEmptySearch(): void | 102 | public function testAddTagWithRefererAndEmptySearch(): void |
diff --git a/tests/ApplicationUtilsTest.php b/tests/helper/ApplicationUtilsTest.php index a232b351..654857b9 100644 --- a/tests/ApplicationUtilsTest.php +++ b/tests/helper/ApplicationUtilsTest.php | |||
@@ -1,7 +1,8 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli; | 2 | namespace Shaarli\Helper; |
3 | 3 | ||
4 | use Shaarli\Config\ConfigManager; | 4 | use Shaarli\Config\ConfigManager; |
5 | use Shaarli\FakeApplicationUtils; | ||
5 | 6 | ||
6 | require_once 'tests/utils/FakeApplicationUtils.php'; | 7 | require_once 'tests/utils/FakeApplicationUtils.php'; |
7 | 8 | ||
@@ -340,6 +341,35 @@ class ApplicationUtilsTest extends \Shaarli\TestCase | |||
340 | } | 341 | } |
341 | 342 | ||
342 | /** | 343 | /** |
344 | * Checks resource permissions in minimal mode. | ||
345 | */ | ||
346 | public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void | ||
347 | { | ||
348 | $conf = new ConfigManager(''); | ||
349 | $conf->set('resource.thumbnails_cache', 'null/cache'); | ||
350 | $conf->set('resource.config', 'null/data/config.php'); | ||
351 | $conf->set('resource.data_dir', 'null/data'); | ||
352 | $conf->set('resource.datastore', 'null/data/store.php'); | ||
353 | $conf->set('resource.ban_file', 'null/data/ipbans.php'); | ||
354 | $conf->set('resource.log', 'null/data/log.txt'); | ||
355 | $conf->set('resource.page_cache', 'null/pagecache'); | ||
356 | $conf->set('resource.raintpl_tmp', 'null/tmp'); | ||
357 | $conf->set('resource.raintpl_tpl', 'null/tpl'); | ||
358 | $conf->set('resource.raintpl_theme', 'null/tpl/default'); | ||
359 | $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt'); | ||
360 | |||
361 | static::assertSame( | ||
362 | [ | ||
363 | '"null/tpl" directory is not readable', | ||
364 | '"null/tpl/default" directory is not readable', | ||
365 | '"null/tmp" directory is not readable', | ||
366 | '"null/tmp" directory is not writable' | ||
367 | ], | ||
368 | ApplicationUtils::checkResourcePermissions($conf, true) | ||
369 | ); | ||
370 | } | ||
371 | |||
372 | /** | ||
343 | * Check update with 'dev' as curent version (master branch). | 373 | * Check update with 'dev' as curent version (master branch). |
344 | * It should always return false. | 374 | * It should always return false. |
345 | */ | 375 | */ |
@@ -349,4 +379,37 @@ class ApplicationUtilsTest extends \Shaarli\TestCase | |||
349 | ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) | 379 | ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) |
350 | ); | 380 | ); |
351 | } | 381 | } |
382 | |||
383 | /** | ||
384 | * Basic test of getPhpExtensionsRequirement() | ||
385 | */ | ||
386 | public function testGetPhpExtensionsRequirementSimple(): void | ||
387 | { | ||
388 | static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement()); | ||
389 | static::assertSame([ | ||
390 | 'name' => 'json', | ||
391 | 'required' => true, | ||
392 | 'desc' => 'Configuration parsing', | ||
393 | 'loaded' => true, | ||
394 | ], ApplicationUtils::getPhpExtensionsRequirement()[0]); | ||
395 | } | ||
396 | |||
397 | /** | ||
398 | * Test getPhpEol with a known version: 7.4 -> 2022 | ||
399 | */ | ||
400 | public function testGetKnownPhpEol(): void | ||
401 | { | ||
402 | static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7')); | ||
403 | } | ||
404 | |||
405 | /** | ||
406 | * Test getPhpEol with an unknown version: 7.4 -> 2022 | ||
407 | */ | ||
408 | public function testGetUnknownPhpEol(): void | ||
409 | { | ||
410 | static::assertSame( | ||
411 | (((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'), | ||
412 | ApplicationUtils::getPhpEol('7.51.34') | ||
413 | ); | ||
414 | } | ||
352 | } | 415 | } |
diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php new file mode 100644 index 00000000..5255b7b1 --- /dev/null +++ b/tests/helper/DailyPageHelperTest.php | |||
@@ -0,0 +1,262 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Helper; | ||
6 | |||
7 | use Shaarli\Bookmark\Bookmark; | ||
8 | use Shaarli\TestCase; | ||
9 | use Slim\Http\Request; | ||
10 | |||
11 | class DailyPageHelperTest extends TestCase | ||
12 | { | ||
13 | /** | ||
14 | * @dataProvider getRequestedTypes | ||
15 | */ | ||
16 | public function testExtractRequestedType(array $queryParams, string $expectedType): void | ||
17 | { | ||
18 | $request = $this->createMock(Request::class); | ||
19 | $request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string { | ||
20 | return $queryParams[$key] ?? null; | ||
21 | }); | ||
22 | |||
23 | $type = DailyPageHelper::extractRequestedType($request); | ||
24 | |||
25 | static::assertSame($type, $expectedType); | ||
26 | } | ||
27 | |||
28 | /** | ||
29 | * @dataProvider getRequestedDateTimes | ||
30 | */ | ||
31 | public function testExtractRequestedDateTime( | ||
32 | string $type, | ||
33 | string $input, | ||
34 | ?Bookmark $bookmark, | ||
35 | \DateTimeInterface $expectedDateTime, | ||
36 | string $compareFormat = 'Ymd' | ||
37 | ): void { | ||
38 | $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark); | ||
39 | |||
40 | static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat)); | ||
41 | } | ||
42 | |||
43 | public function testExtractRequestedDateTimeExceptionUnknownType(): void | ||
44 | { | ||
45 | $this->expectException(\Exception::class); | ||
46 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
47 | |||
48 | DailyPageHelper::extractRequestedDateTime('nope', null, null); | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * @dataProvider getFormatsByType | ||
53 | */ | ||
54 | public function testGetFormatByType(string $type, string $expectedFormat): void | ||
55 | { | ||
56 | $format = DailyPageHelper::getFormatByType($type); | ||
57 | |||
58 | static::assertSame($expectedFormat, $format); | ||
59 | } | ||
60 | |||
61 | public function testGetFormatByTypeExceptionUnknownType(): void | ||
62 | { | ||
63 | $this->expectException(\Exception::class); | ||
64 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
65 | |||
66 | DailyPageHelper::getFormatByType('nope'); | ||
67 | } | ||
68 | |||
69 | /** | ||
70 | * @dataProvider getStartDatesByType | ||
71 | */ | ||
72 | public function testGetStartDatesByType( | ||
73 | string $type, | ||
74 | \DateTimeImmutable $dateTime, | ||
75 | \DateTimeInterface $expectedDateTime | ||
76 | ): void { | ||
77 | $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime); | ||
78 | |||
79 | static::assertEquals($expectedDateTime, $startDateTime); | ||
80 | } | ||
81 | |||
82 | public function testGetStartDatesByTypeExceptionUnknownType(): void | ||
83 | { | ||
84 | $this->expectException(\Exception::class); | ||
85 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
86 | |||
87 | DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable()); | ||
88 | } | ||
89 | |||
90 | /** | ||
91 | * @dataProvider getEndDatesByType | ||
92 | */ | ||
93 | public function testGetEndDatesByType( | ||
94 | string $type, | ||
95 | \DateTimeImmutable $dateTime, | ||
96 | \DateTimeInterface $expectedDateTime | ||
97 | ): void { | ||
98 | $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime); | ||
99 | |||
100 | static::assertEquals($expectedDateTime, $endDateTime); | ||
101 | } | ||
102 | |||
103 | public function testGetEndDatesByTypeExceptionUnknownType(): void | ||
104 | { | ||
105 | $this->expectException(\Exception::class); | ||
106 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
107 | |||
108 | DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable()); | ||
109 | } | ||
110 | |||
111 | /** | ||
112 | * @dataProvider getDescriptionsByType | ||
113 | */ | ||
114 | public function testGeDescriptionsByType( | ||
115 | string $type, | ||
116 | \DateTimeImmutable $dateTime, | ||
117 | string $expectedDescription | ||
118 | ): void { | ||
119 | $description = DailyPageHelper::getDescriptionByType($type, $dateTime); | ||
120 | |||
121 | static::assertEquals($expectedDescription, $description); | ||
122 | } | ||
123 | |||
124 | public function getDescriptionByTypeExceptionUnknownType(): void | ||
125 | { | ||
126 | $this->expectException(\Exception::class); | ||
127 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
128 | |||
129 | DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable()); | ||
130 | } | ||
131 | |||
132 | /** | ||
133 | * @dataProvider getRssLengthsByType | ||
134 | */ | ||
135 | public function testGeRssLengthsByType(string $type): void { | ||
136 | $length = DailyPageHelper::getRssLengthByType($type); | ||
137 | |||
138 | static::assertIsInt($length); | ||
139 | } | ||
140 | |||
141 | public function testGeRssLengthsByTypeExceptionUnknownType(): void | ||
142 | { | ||
143 | $this->expectException(\Exception::class); | ||
144 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
145 | |||
146 | DailyPageHelper::getRssLengthByType('nope'); | ||
147 | } | ||
148 | |||
149 | /** | ||
150 | * Data provider for testExtractRequestedType() test method. | ||
151 | */ | ||
152 | public function getRequestedTypes(): array | ||
153 | { | ||
154 | return [ | ||
155 | [['month' => null], DailyPageHelper::DAY], | ||
156 | [['month' => ''], DailyPageHelper::MONTH], | ||
157 | [['month' => 'content'], DailyPageHelper::MONTH], | ||
158 | [['week' => null], DailyPageHelper::DAY], | ||
159 | [['week' => ''], DailyPageHelper::WEEK], | ||
160 | [['week' => 'content'], DailyPageHelper::WEEK], | ||
161 | [['day' => null], DailyPageHelper::DAY], | ||
162 | [['day' => ''], DailyPageHelper::DAY], | ||
163 | [['day' => 'content'], DailyPageHelper::DAY], | ||
164 | ]; | ||
165 | } | ||
166 | |||
167 | /** | ||
168 | * Data provider for testExtractRequestedDateTime() test method. | ||
169 | */ | ||
170 | public function getRequestedDateTimes(): array | ||
171 | { | ||
172 | return [ | ||
173 | [DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')], | ||
174 | [ | ||
175 | DailyPageHelper::DAY, | ||
176 | '', | ||
177 | (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), | ||
178 | $date, | ||
179 | ], | ||
180 | [DailyPageHelper::DAY, '', null, new \DateTime()], | ||
181 | [DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')], | ||
182 | [ | ||
183 | DailyPageHelper::WEEK, | ||
184 | '', | ||
185 | (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), | ||
186 | new \DateTime('2020-10-13'), | ||
187 | ], | ||
188 | [DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'], | ||
189 | [DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'], | ||
190 | [ | ||
191 | DailyPageHelper::MONTH, | ||
192 | '', | ||
193 | (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), | ||
194 | new \DateTime('2020-10-13'), | ||
195 | 'Ym' | ||
196 | ], | ||
197 | [DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'], | ||
198 | ]; | ||
199 | } | ||
200 | |||
201 | /** | ||
202 | * Data provider for testGetFormatByType() test method. | ||
203 | */ | ||
204 | public function getFormatsByType(): array | ||
205 | { | ||
206 | return [ | ||
207 | [DailyPageHelper::DAY, 'Ymd'], | ||
208 | [DailyPageHelper::WEEK, 'YW'], | ||
209 | [DailyPageHelper::MONTH, 'Ym'], | ||
210 | ]; | ||
211 | } | ||
212 | |||
213 | /** | ||
214 | * Data provider for testGetStartDatesByType() test method. | ||
215 | */ | ||
216 | public function getStartDatesByType(): array | ||
217 | { | ||
218 | return [ | ||
219 | [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')], | ||
220 | [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')], | ||
221 | [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')], | ||
222 | ]; | ||
223 | } | ||
224 | |||
225 | /** | ||
226 | * Data provider for testGetEndDatesByType() test method. | ||
227 | */ | ||
228 | public function getEndDatesByType(): array | ||
229 | { | ||
230 | return [ | ||
231 | [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')], | ||
232 | [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')], | ||
233 | [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')], | ||
234 | ]; | ||
235 | } | ||
236 | |||
237 | /** | ||
238 | * Data provider for testGetDescriptionsByType() test method. | ||
239 | */ | ||
240 | public function getDescriptionsByType(): array | ||
241 | { | ||
242 | return [ | ||
243 | [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')], | ||
244 | [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')], | ||
245 | [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], | ||
246 | [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], | ||
247 | [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], | ||
248 | ]; | ||
249 | } | ||
250 | |||
251 | /** | ||
252 | * Data provider for testGetDescriptionsByType() test method. | ||
253 | */ | ||
254 | public function getRssLengthsByType(): array | ||
255 | { | ||
256 | return [ | ||
257 | [DailyPageHelper::DAY], | ||
258 | [DailyPageHelper::WEEK], | ||
259 | [DailyPageHelper::MONTH], | ||
260 | ]; | ||
261 | } | ||
262 | } | ||
diff --git a/tests/FileUtilsTest.php b/tests/helper/FileUtilsTest.php index 9163bdf1..8035f79c 100644 --- a/tests/FileUtilsTest.php +++ b/tests/helper/FileUtilsTest.php | |||
@@ -1,27 +1,51 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | namespace Shaarli; | 3 | namespace Shaarli\Helper; |
4 | 4 | ||
5 | use Exception; | 5 | use Exception; |
6 | use Shaarli\Exceptions\IOException; | ||
7 | use Shaarli\TestCase; | ||
6 | 8 | ||
7 | /** | 9 | /** |
8 | * Class FileUtilsTest | 10 | * Class FileUtilsTest |
9 | * | 11 | * |
10 | * Test file utility class. | 12 | * Test file utility class. |
11 | */ | 13 | */ |
12 | class FileUtilsTest extends \Shaarli\TestCase | 14 | class FileUtilsTest extends TestCase |
13 | { | 15 | { |
14 | /** | 16 | /** |
15 | * @var string Test file path. | 17 | * @var string Test file path. |
16 | */ | 18 | */ |
17 | protected static $file = 'sandbox/flat.db'; | 19 | protected static $file = 'sandbox/flat.db'; |
18 | 20 | ||
21 | protected function setUp(): void | ||
22 | { | ||
23 | @mkdir('sandbox'); | ||
24 | mkdir('sandbox/folder2'); | ||
25 | touch('sandbox/file1'); | ||
26 | touch('sandbox/file2'); | ||
27 | mkdir('sandbox/folder1'); | ||
28 | touch('sandbox/folder1/file1'); | ||
29 | touch('sandbox/folder1/file2'); | ||
30 | mkdir('sandbox/folder3'); | ||
31 | mkdir('/tmp/shaarli-to-delete'); | ||
32 | } | ||
33 | |||
19 | /** | 34 | /** |
20 | * Delete test file after every test. | 35 | * Delete test file after every test. |
21 | */ | 36 | */ |
22 | protected function tearDown(): void | 37 | protected function tearDown(): void |
23 | { | 38 | { |
24 | @unlink(self::$file); | 39 | @unlink(self::$file); |
40 | |||
41 | @unlink('sandbox/folder1/file1'); | ||
42 | @unlink('sandbox/folder1/file2'); | ||
43 | @rmdir('sandbox/folder1'); | ||
44 | @unlink('sandbox/file1'); | ||
45 | @unlink('sandbox/file2'); | ||
46 | @rmdir('sandbox/folder2'); | ||
47 | @rmdir('sandbox/folder3'); | ||
48 | @rmdir('/tmp/shaarli-to-delete'); | ||
25 | } | 49 | } |
26 | 50 | ||
27 | /** | 51 | /** |
@@ -107,4 +131,67 @@ class FileUtilsTest extends \Shaarli\TestCase | |||
107 | $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); | 131 | $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); |
108 | $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); | 132 | $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); |
109 | } | 133 | } |
134 | |||
135 | /** | ||
136 | * Test clearFolder with self delete and excluded files | ||
137 | */ | ||
138 | public function testClearFolderSelfDeleteWithExclusion(): void | ||
139 | { | ||
140 | FileUtils::clearFolder('sandbox', true, ['file2']); | ||
141 | |||
142 | static::assertFileExists('sandbox/folder1/file2'); | ||
143 | static::assertFileExists('sandbox/folder1'); | ||
144 | static::assertFileExists('sandbox/file2'); | ||
145 | static::assertFileExists('sandbox'); | ||
146 | |||
147 | static::assertFileNotExists('sandbox/folder1/file1'); | ||
148 | static::assertFileNotExists('sandbox/file1'); | ||
149 | static::assertFileNotExists('sandbox/folder3'); | ||
150 | } | ||
151 | |||
152 | /** | ||
153 | * Test clearFolder with self delete and excluded files | ||
154 | */ | ||
155 | public function testClearFolderSelfDeleteWithoutExclusion(): void | ||
156 | { | ||
157 | FileUtils::clearFolder('sandbox', true); | ||
158 | |||
159 | static::assertFileNotExists('sandbox'); | ||
160 | } | ||
161 | |||
162 | /** | ||
163 | * Test clearFolder with self delete and excluded files | ||
164 | */ | ||
165 | public function testClearFolderNoSelfDeleteWithoutExclusion(): void | ||
166 | { | ||
167 | FileUtils::clearFolder('sandbox', false); | ||
168 | |||
169 | static::assertFileExists('sandbox'); | ||
170 | |||
171 | // 2 because '.' and '..' | ||
172 | static::assertCount(2, new \DirectoryIterator('sandbox')); | ||
173 | } | ||
174 | |||
175 | /** | ||
176 | * Test clearFolder on a file instead of a folder | ||
177 | */ | ||
178 | public function testClearFolderOnANonDirectory(): void | ||
179 | { | ||
180 | $this->expectException(IOException::class); | ||
181 | $this->expectExceptionMessage('Provided path is not a directory.'); | ||
182 | |||
183 | FileUtils::clearFolder('sandbox/file1', false); | ||
184 | } | ||
185 | |||
186 | /** | ||
187 | * Test clearFolder on a file instead of a folder | ||
188 | */ | ||
189 | public function testClearFolderOutsideOfShaarliDirectory(): void | ||
190 | { | ||
191 | $this->expectException(IOException::class); | ||
192 | $this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.'); | ||
193 | |||
194 | |||
195 | FileUtils::clearFolder('/tmp/shaarli-to-delete', true); | ||
196 | } | ||
110 | } | 197 | } |
diff --git a/tests/http/MetadataRetrieverTest.php b/tests/http/MetadataRetrieverTest.php new file mode 100644 index 00000000..3c9eaa0e --- /dev/null +++ b/tests/http/MetadataRetrieverTest.php | |||
@@ -0,0 +1,154 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Http; | ||
6 | |||
7 | use PHPUnit\Framework\TestCase; | ||
8 | use Shaarli\Config\ConfigManager; | ||
9 | |||
10 | class MetadataRetrieverTest extends TestCase | ||
11 | { | ||
12 | /** @var MetadataRetriever */ | ||
13 | protected $retriever; | ||
14 | |||
15 | /** @var ConfigManager */ | ||
16 | protected $conf; | ||
17 | |||
18 | /** @var HttpAccess */ | ||
19 | protected $httpAccess; | ||
20 | |||
21 | public function setUp(): void | ||
22 | { | ||
23 | $this->conf = $this->createMock(ConfigManager::class); | ||
24 | $this->httpAccess = $this->createMock(HttpAccess::class); | ||
25 | $this->retriever = new MetadataRetriever($this->conf, $this->httpAccess); | ||
26 | |||
27 | $this->conf->method('get')->willReturnCallback(function (string $param, $default) { | ||
28 | return $default === null ? $param : $default; | ||
29 | }); | ||
30 | } | ||
31 | |||
32 | /** | ||
33 | * Test metadata retrieve() with values returned | ||
34 | */ | ||
35 | public function testFullRetrieval(): void | ||
36 | { | ||
37 | $url = 'https://domain.tld/link'; | ||
38 | $remoteTitle = 'Remote Title '; | ||
39 | $remoteDesc = 'Sometimes the meta description is relevant.'; | ||
40 | $remoteTags = 'abc def'; | ||
41 | $remoteCharset = 'utf-8'; | ||
42 | |||
43 | $expectedResult = [ | ||
44 | 'title' => $remoteTitle, | ||
45 | 'description' => $remoteDesc, | ||
46 | 'tags' => $remoteTags, | ||
47 | ]; | ||
48 | |||
49 | $this->httpAccess | ||
50 | ->expects(static::once()) | ||
51 | ->method('getCurlHeaderCallback') | ||
52 | ->willReturnCallback( | ||
53 | function (&$charset) use ( | ||
54 | $remoteCharset | ||
55 | ): callable { | ||
56 | return function () use ( | ||
57 | &$charset, | ||
58 | $remoteCharset | ||
59 | ): void { | ||
60 | $charset = $remoteCharset; | ||
61 | }; | ||
62 | } | ||
63 | ) | ||
64 | ; | ||
65 | $this->httpAccess | ||
66 | ->expects(static::once()) | ||
67 | ->method('getCurlDownloadCallback') | ||
68 | ->willReturnCallback( | ||
69 | function (&$charset, &$title, &$description, &$tags) use ( | ||
70 | $remoteCharset, | ||
71 | $remoteTitle, | ||
72 | $remoteDesc, | ||
73 | $remoteTags | ||
74 | ): callable { | ||
75 | return function () use ( | ||
76 | &$charset, | ||
77 | &$title, | ||
78 | &$description, | ||
79 | &$tags, | ||
80 | $remoteCharset, | ||
81 | $remoteTitle, | ||
82 | $remoteDesc, | ||
83 | $remoteTags | ||
84 | ): void { | ||
85 | static::assertSame($remoteCharset, $charset); | ||
86 | |||
87 | $title = $remoteTitle; | ||
88 | $description = $remoteDesc; | ||
89 | $tags = $remoteTags; | ||
90 | }; | ||
91 | } | ||
92 | ) | ||
93 | ; | ||
94 | $this->httpAccess | ||
95 | ->expects(static::once()) | ||
96 | ->method('getHttpResponse') | ||
97 | ->with($url, 30, 4194304) | ||
98 | ->willReturnCallback(function($url, $timeout, $maxBytes, $headerCallback, $dlCallback): void { | ||
99 | $headerCallback(); | ||
100 | $dlCallback(); | ||
101 | }) | ||
102 | ; | ||
103 | |||
104 | $result = $this->retriever->retrieve($url); | ||
105 | |||
106 | static::assertSame($expectedResult, $result); | ||
107 | } | ||
108 | |||
109 | /** | ||
110 | * Test metadata retrieve() without any value | ||
111 | */ | ||
112 | public function testEmptyRetrieval(): void | ||
113 | { | ||
114 | $url = 'https://domain.tld/link'; | ||
115 | |||
116 | $expectedResult = [ | ||
117 | 'title' => null, | ||
118 | 'description' => null, | ||
119 | 'tags' => null, | ||
120 | ]; | ||
121 | |||
122 | $this->httpAccess | ||
123 | ->expects(static::once()) | ||
124 | ->method('getCurlDownloadCallback') | ||
125 | ->willReturnCallback( | ||
126 | function (): callable { | ||
127 | return function (): void {}; | ||
128 | } | ||
129 | ) | ||
130 | ; | ||
131 | $this->httpAccess | ||
132 | ->expects(static::once()) | ||
133 | ->method('getCurlHeaderCallback') | ||
134 | ->willReturnCallback( | ||
135 | function (): callable { | ||
136 | return function (): void {}; | ||
137 | } | ||
138 | ) | ||
139 | ; | ||
140 | $this->httpAccess | ||
141 | ->expects(static::once()) | ||
142 | ->method('getHttpResponse') | ||
143 | ->with($url, 30, 4194304) | ||
144 | ->willReturnCallback(function($url, $timeout, $maxBytes, $headerCallback, $dlCallback): void { | ||
145 | $headerCallback(); | ||
146 | $dlCallback(); | ||
147 | }) | ||
148 | ; | ||
149 | |||
150 | $result = $this->retriever->retrieve($url); | ||
151 | |||
152 | static::assertSame($expectedResult, $result); | ||
153 | } | ||
154 | } | ||
diff --git a/tests/legacy/LegacyLinkDBTest.php b/tests/legacy/LegacyLinkDBTest.php index df2cad62..5c3fd425 100644 --- a/tests/legacy/LegacyLinkDBTest.php +++ b/tests/legacy/LegacyLinkDBTest.php | |||
@@ -296,6 +296,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase | |||
296 | // They need to be grouped with the first case found - order by date DESC: `sTuff`. | 296 | // They need to be grouped with the first case found - order by date DESC: `sTuff`. |
297 | 'sTuff' => 2, | 297 | 'sTuff' => 2, |
298 | 'ut' => 1, | 298 | 'ut' => 1, |
299 | 'assurance' => 1, | ||
300 | 'coding-style' => 1, | ||
301 | 'quality' => 1, | ||
302 | 'standards' => 1, | ||
299 | ), | 303 | ), |
300 | self::$publicLinkDB->linksCountPerTag() | 304 | self::$publicLinkDB->linksCountPerTag() |
301 | ); | 305 | ); |
@@ -324,6 +328,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase | |||
324 | 'tag3' => 1, | 328 | 'tag3' => 1, |
325 | 'tag4' => 1, | 329 | 'tag4' => 1, |
326 | 'ut' => 1, | 330 | 'ut' => 1, |
331 | 'assurance' => 1, | ||
332 | 'coding-style' => 1, | ||
333 | 'quality' => 1, | ||
334 | 'standards' => 1, | ||
327 | ), | 335 | ), |
328 | self::$privateLinkDB->linksCountPerTag() | 336 | self::$privateLinkDB->linksCountPerTag() |
329 | ); | 337 | ); |
@@ -544,6 +552,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase | |||
544 | 'tag4' => 1, | 552 | 'tag4' => 1, |
545 | 'ut' => 1, | 553 | 'ut' => 1, |
546 | 'w3c' => 1, | 554 | 'w3c' => 1, |
555 | 'assurance' => 1, | ||
556 | 'coding-style' => 1, | ||
557 | 'quality' => 1, | ||
558 | 'standards' => 1, | ||
547 | ]; | 559 | ]; |
548 | $tags = self::$privateLinkDB->linksCountPerTag(); | 560 | $tags = self::$privateLinkDB->linksCountPerTag(); |
549 | 561 | ||
diff --git a/tests/legacy/LegacyUpdaterTest.php b/tests/legacy/LegacyUpdaterTest.php index f7391b86..395dd4b7 100644 --- a/tests/legacy/LegacyUpdaterTest.php +++ b/tests/legacy/LegacyUpdaterTest.php | |||
@@ -51,10 +51,10 @@ class LegacyUpdaterTest extends \Shaarli\TestCase | |||
51 | */ | 51 | */ |
52 | public function testReadEmptyUpdatesFile() | 52 | public function testReadEmptyUpdatesFile() |
53 | { | 53 | { |
54 | $this->assertEquals(array(), UpdaterUtils::read_updates_file('')); | 54 | $this->assertEquals(array(), UpdaterUtils::readUpdatesFile('')); |
55 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; | 55 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; |
56 | touch($updatesFile); | 56 | touch($updatesFile); |
57 | $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile)); | 57 | $this->assertEquals(array(), UpdaterUtils::readUpdatesFile($updatesFile)); |
58 | unlink($updatesFile); | 58 | unlink($updatesFile); |
59 | } | 59 | } |
60 | 60 | ||
@@ -66,14 +66,14 @@ class LegacyUpdaterTest extends \Shaarli\TestCase | |||
66 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; | 66 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; |
67 | $updatesMethods = array('m1', 'm2', 'm3'); | 67 | $updatesMethods = array('m1', 'm2', 'm3'); |
68 | 68 | ||
69 | UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); | 69 | UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); |
70 | $readMethods = UpdaterUtils::read_updates_file($updatesFile); | 70 | $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); |
71 | $this->assertEquals($readMethods, $updatesMethods); | 71 | $this->assertEquals($readMethods, $updatesMethods); |
72 | 72 | ||
73 | // Update | 73 | // Update |
74 | $updatesMethods[] = 'm4'; | 74 | $updatesMethods[] = 'm4'; |
75 | UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); | 75 | UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); |
76 | $readMethods = UpdaterUtils::read_updates_file($updatesFile); | 76 | $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); |
77 | $this->assertEquals($readMethods, $updatesMethods); | 77 | $this->assertEquals($readMethods, $updatesMethods); |
78 | unlink($updatesFile); | 78 | unlink($updatesFile); |
79 | } | 79 | } |
@@ -86,7 +86,7 @@ class LegacyUpdaterTest extends \Shaarli\TestCase | |||
86 | $this->expectException(\Exception::class); | 86 | $this->expectException(\Exception::class); |
87 | $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); | 87 | $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); |
88 | 88 | ||
89 | UpdaterUtils::write_updates_file('', array('test')); | 89 | UpdaterUtils::writeUpdatesFile('', array('test')); |
90 | } | 90 | } |
91 | 91 | ||
92 | /** | 92 | /** |
@@ -101,7 +101,7 @@ class LegacyUpdaterTest extends \Shaarli\TestCase | |||
101 | touch($updatesFile); | 101 | touch($updatesFile); |
102 | chmod($updatesFile, 0444); | 102 | chmod($updatesFile, 0444); |
103 | try { | 103 | try { |
104 | @UpdaterUtils::write_updates_file($updatesFile, array('test')); | 104 | @UpdaterUtils::writeUpdatesFile($updatesFile, array('test')); |
105 | } catch (Exception $e) { | 105 | } catch (Exception $e) { |
106 | unlink($updatesFile); | 106 | unlink($updatesFile); |
107 | throw $e; | 107 | throw $e; |
diff --git a/tests/netscape/BookmarkExportTest.php b/tests/netscape/BookmarkExportTest.php index 9b95ccc9..ad288f78 100644 --- a/tests/netscape/BookmarkExportTest.php +++ b/tests/netscape/BookmarkExportTest.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Netscape; | 3 | namespace Shaarli\Netscape; |
4 | 4 | ||
5 | use malkusch\lock\mutex\NoMutex; | ||
5 | use Shaarli\Bookmark\BookmarkFileService; | 6 | use Shaarli\Bookmark\BookmarkFileService; |
6 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
7 | use Shaarli\Formatter\BookmarkFormatter; | 8 | use Shaarli\Formatter\BookmarkFormatter; |
@@ -56,12 +57,13 @@ class BookmarkExportTest extends TestCase | |||
56 | */ | 57 | */ |
57 | public static function setUpBeforeClass(): void | 58 | public static function setUpBeforeClass(): void |
58 | { | 59 | { |
60 | $mutex = new NoMutex(); | ||
59 | static::$conf = new ConfigManager('tests/utils/config/configJson'); | 61 | static::$conf = new ConfigManager('tests/utils/config/configJson'); |
60 | static::$conf->set('resource.datastore', static::$testDatastore); | 62 | static::$conf->set('resource.datastore', static::$testDatastore); |
61 | static::$refDb = new \ReferenceLinkDB(); | 63 | static::$refDb = new \ReferenceLinkDB(); |
62 | static::$refDb->write(static::$testDatastore); | 64 | static::$refDb->write(static::$testDatastore); |
63 | static::$history = new History('sandbox/history.php'); | 65 | static::$history = new History('sandbox/history.php'); |
64 | static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, true); | 66 | static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, $mutex, true); |
65 | $factory = new FormatterFactory(static::$conf, true); | 67 | $factory = new FormatterFactory(static::$conf, true); |
66 | static::$formatter = $factory->getFormatter('raw'); | 68 | static::$formatter = $factory->getFormatter('raw'); |
67 | } | 69 | } |
diff --git a/tests/netscape/BookmarkImportTest.php b/tests/netscape/BookmarkImportTest.php index c1e49b5f..6856ebca 100644 --- a/tests/netscape/BookmarkImportTest.php +++ b/tests/netscape/BookmarkImportTest.php | |||
@@ -3,6 +3,7 @@ | |||
3 | namespace Shaarli\Netscape; | 3 | namespace Shaarli\Netscape; |
4 | 4 | ||
5 | use DateTime; | 5 | use DateTime; |
6 | use malkusch\lock\mutex\NoMutex; | ||
6 | use Psr\Http\Message\UploadedFileInterface; | 7 | use Psr\Http\Message\UploadedFileInterface; |
7 | use Shaarli\Bookmark\Bookmark; | 8 | use Shaarli\Bookmark\Bookmark; |
8 | use Shaarli\Bookmark\BookmarkFileService; | 9 | use Shaarli\Bookmark\BookmarkFileService; |
@@ -87,6 +88,7 @@ class BookmarkImportTest extends TestCase | |||
87 | */ | 88 | */ |
88 | protected function setUp(): void | 89 | protected function setUp(): void |
89 | { | 90 | { |
91 | $mutex = new NoMutex(); | ||
90 | if (file_exists(self::$testDatastore)) { | 92 | if (file_exists(self::$testDatastore)) { |
91 | unlink(self::$testDatastore); | 93 | unlink(self::$testDatastore); |
92 | } | 94 | } |
@@ -97,7 +99,7 @@ class BookmarkImportTest extends TestCase | |||
97 | $this->conf->set('resource.page_cache', $this->pagecache); | 99 | $this->conf->set('resource.page_cache', $this->pagecache); |
98 | $this->conf->set('resource.datastore', self::$testDatastore); | 100 | $this->conf->set('resource.datastore', self::$testDatastore); |
99 | $this->history = new History(self::$historyFilePath); | 101 | $this->history = new History(self::$historyFilePath); |
100 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); | 102 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true); |
101 | $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history); | 103 | $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history); |
102 | } | 104 | } |
103 | 105 | ||
@@ -529,7 +531,7 @@ class BookmarkImportTest extends TestCase | |||
529 | { | 531 | { |
530 | $post = array( | 532 | $post = array( |
531 | 'privacy' => 'public', | 533 | 'privacy' => 'public', |
532 | 'default_tags' => 'tag1,tag2 tag3' | 534 | 'default_tags' => 'tag1 tag2 tag3' |
533 | ); | 535 | ); |
534 | $files = file2array('netscape_basic.htm'); | 536 | $files = file2array('netscape_basic.htm'); |
535 | $this->assertStringMatchesFormat( | 537 | $this->assertStringMatchesFormat( |
@@ -550,7 +552,7 @@ class BookmarkImportTest extends TestCase | |||
550 | { | 552 | { |
551 | $post = array( | 553 | $post = array( |
552 | 'privacy' => 'public', | 554 | 'privacy' => 'public', |
553 | 'default_tags' => 'tag1&,tag2 "tag3"' | 555 | 'default_tags' => 'tag1& tag2 "tag3"' |
554 | ); | 556 | ); |
555 | $files = file2array('netscape_basic.htm'); | 557 | $files = file2array('netscape_basic.htm'); |
556 | $this->assertStringMatchesFormat( | 558 | $this->assertStringMatchesFormat( |
@@ -571,6 +573,43 @@ class BookmarkImportTest extends TestCase | |||
571 | } | 573 | } |
572 | 574 | ||
573 | /** | 575 | /** |
576 | * Add user-specified tags to all imported bookmarks | ||
577 | */ | ||
578 | public function testSetDefaultTagsWithCustomSeparator() | ||
579 | { | ||
580 | $separator = '@'; | ||
581 | $this->conf->set('general.tags_separator', $separator); | ||
582 | $post = [ | ||
583 | 'privacy' => 'public', | ||
584 | 'default_tags' => 'tag1@tag2@tag3@multiple words tag' | ||
585 | ]; | ||
586 | $files = file2array('netscape_basic.htm'); | ||
587 | $this->assertStringMatchesFormat( | ||
588 | 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' | ||
589 | .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', | ||
590 | $this->netscapeBookmarkUtils->import($post, $files) | ||
591 | ); | ||
592 | $this->assertEquals(2, $this->bookmarkService->count()); | ||
593 | $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); | ||
594 | $this->assertEquals( | ||
595 | 'tag1@tag2@tag3@multiple words tag@private@secret', | ||
596 | $this->bookmarkService->get(0)->getTagsString($separator) | ||
597 | ); | ||
598 | $this->assertEquals( | ||
599 | ['tag1', 'tag2', 'tag3', 'multiple words tag', 'private', 'secret'], | ||
600 | $this->bookmarkService->get(0)->getTags() | ||
601 | ); | ||
602 | $this->assertEquals( | ||
603 | 'tag1@tag2@tag3@multiple words tag@public@hello@world', | ||
604 | $this->bookmarkService->get(1)->getTagsString($separator) | ||
605 | ); | ||
606 | $this->assertEquals( | ||
607 | ['tag1', 'tag2', 'tag3', 'multiple words tag', 'public', 'hello', 'world'], | ||
608 | $this->bookmarkService->get(1)->getTags() | ||
609 | ); | ||
610 | } | ||
611 | |||
612 | /** | ||
574 | * Ensure each imported bookmark has a unique id | 613 | * Ensure each imported bookmark has a unique id |
575 | * | 614 | * |
576 | * See https://github.com/shaarli/Shaarli/issues/351 | 615 | * See https://github.com/shaarli/Shaarli/issues/351 |
diff --git a/tests/plugins/PluginWallabagTest.php b/tests/plugins/PluginWallabagTest.php index 36317215..9a402fb7 100644 --- a/tests/plugins/PluginWallabagTest.php +++ b/tests/plugins/PluginWallabagTest.php | |||
@@ -49,14 +49,15 @@ class PluginWallabagTest extends \Shaarli\TestCase | |||
49 | $conf = new ConfigManager(''); | 49 | $conf = new ConfigManager(''); |
50 | $conf->set('plugins.WALLABAG_URL', 'value'); | 50 | $conf->set('plugins.WALLABAG_URL', 'value'); |
51 | $str = 'http://randomstr.com/test'; | 51 | $str = 'http://randomstr.com/test'; |
52 | $data = array( | 52 | $data = [ |
53 | 'title' => $str, | 53 | 'title' => $str, |
54 | 'links' => array( | 54 | 'links' => [ |
55 | array( | 55 | [ |
56 | 'url' => $str, | 56 | 'url' => $str, |
57 | ) | 57 | ] |
58 | ) | 58 | ], |
59 | ); | 59 | '_LOGGEDIN_' => true, |
60 | ]; | ||
60 | 61 | ||
61 | $data = hook_wallabag_render_linklist($data, $conf); | 62 | $data = hook_wallabag_render_linklist($data, $conf); |
62 | $link = $data['links'][0]; | 63 | $link = $data['links'][0]; |
@@ -69,4 +70,26 @@ class PluginWallabagTest extends \Shaarli\TestCase | |||
69 | $this->assertNotFalse(strpos($link['link_plugin'][0], urlencode($str))); | 70 | $this->assertNotFalse(strpos($link['link_plugin'][0], urlencode($str))); |
70 | $this->assertNotFalse(strpos($link['link_plugin'][0], $conf->get('plugins.WALLABAG_URL'))); | 71 | $this->assertNotFalse(strpos($link['link_plugin'][0], $conf->get('plugins.WALLABAG_URL'))); |
71 | } | 72 | } |
73 | |||
74 | /** | ||
75 | * Test render_linklist hook while logged out: no change. | ||
76 | */ | ||
77 | public function testWallabagLinklistLoggedOut(): void | ||
78 | { | ||
79 | $conf = new ConfigManager(''); | ||
80 | $str = 'http://randomstr.com/test'; | ||
81 | $data = [ | ||
82 | 'title' => $str, | ||
83 | 'links' => [ | ||
84 | [ | ||
85 | 'url' => $str, | ||
86 | ] | ||
87 | ], | ||
88 | '_LOGGEDIN_' => false, | ||
89 | ]; | ||
90 | |||
91 | $result = hook_wallabag_render_linklist($data, $conf); | ||
92 | |||
93 | static::assertSame($data, $result); | ||
94 | } | ||
72 | } | 95 | } |
diff --git a/tests/security/BanManagerTest.php b/tests/security/BanManagerTest.php index 698d3d10..29d2791b 100644 --- a/tests/security/BanManagerTest.php +++ b/tests/security/BanManagerTest.php | |||
@@ -3,7 +3,8 @@ | |||
3 | 3 | ||
4 | namespace Shaarli\Security; | 4 | namespace Shaarli\Security; |
5 | 5 | ||
6 | use Shaarli\FileUtils; | 6 | use Psr\Log\LoggerInterface; |
7 | use Shaarli\Helper\FileUtils; | ||
7 | use Shaarli\TestCase; | 8 | use Shaarli\TestCase; |
8 | 9 | ||
9 | /** | 10 | /** |
@@ -387,7 +388,7 @@ class BanManagerTest extends TestCase | |||
387 | 3, | 388 | 3, |
388 | 1800, | 389 | 1800, |
389 | $this->banFile, | 390 | $this->banFile, |
390 | $this->logFile | 391 | $this->createMock(LoggerInterface::class) |
391 | ); | 392 | ); |
392 | } | 393 | } |
393 | } | 394 | } |
diff --git a/tests/security/LoginManagerTest.php b/tests/security/LoginManagerTest.php index d302983d..f7609fc6 100644 --- a/tests/security/LoginManagerTest.php +++ b/tests/security/LoginManagerTest.php | |||
@@ -2,6 +2,8 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Security; | 3 | namespace Shaarli\Security; |
4 | 4 | ||
5 | use Psr\Log\LoggerInterface; | ||
6 | use Shaarli\FakeConfigManager; | ||
5 | use Shaarli\TestCase; | 7 | use Shaarli\TestCase; |
6 | 8 | ||
7 | /** | 9 | /** |
@@ -9,7 +11,7 @@ use Shaarli\TestCase; | |||
9 | */ | 11 | */ |
10 | class LoginManagerTest extends TestCase | 12 | class LoginManagerTest extends TestCase |
11 | { | 13 | { |
12 | /** @var \FakeConfigManager Configuration Manager instance */ | 14 | /** @var FakeConfigManager Configuration Manager instance */ |
13 | protected $configManager = null; | 15 | protected $configManager = null; |
14 | 16 | ||
15 | /** @var LoginManager Login Manager instance */ | 17 | /** @var LoginManager Login Manager instance */ |
@@ -60,6 +62,9 @@ class LoginManagerTest extends TestCase | |||
60 | /** @var CookieManager */ | 62 | /** @var CookieManager */ |
61 | protected $cookieManager; | 63 | protected $cookieManager; |
62 | 64 | ||
65 | /** @var BanManager */ | ||
66 | protected $banManager; | ||
67 | |||
63 | /** | 68 | /** |
64 | * Prepare or reset test resources | 69 | * Prepare or reset test resources |
65 | */ | 70 | */ |
@@ -71,7 +76,7 @@ class LoginManagerTest extends TestCase | |||
71 | 76 | ||
72 | $this->passwordHash = sha1($this->password . $this->login . $this->salt); | 77 | $this->passwordHash = sha1($this->password . $this->login . $this->salt); |
73 | 78 | ||
74 | $this->configManager = new \FakeConfigManager([ | 79 | $this->configManager = new FakeConfigManager([ |
75 | 'credentials.login' => $this->login, | 80 | 'credentials.login' => $this->login, |
76 | 'credentials.hash' => $this->passwordHash, | 81 | 'credentials.hash' => $this->passwordHash, |
77 | 'credentials.salt' => $this->salt, | 82 | 'credentials.salt' => $this->salt, |
@@ -91,18 +96,29 @@ class LoginManagerTest extends TestCase | |||
91 | return $this->cookie[$key] ?? null; | 96 | return $this->cookie[$key] ?? null; |
92 | }); | 97 | }); |
93 | $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path'); | 98 | $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path'); |
94 | $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager); | 99 | $this->banManager = $this->createMock(BanManager::class); |
100 | $this->loginManager = new LoginManager( | ||
101 | $this->configManager, | ||
102 | $this->sessionManager, | ||
103 | $this->cookieManager, | ||
104 | $this->banManager, | ||
105 | $this->createMock(LoggerInterface::class) | ||
106 | ); | ||
95 | $this->server['REMOTE_ADDR'] = $this->ipAddr; | 107 | $this->server['REMOTE_ADDR'] = $this->ipAddr; |
96 | } | 108 | } |
97 | 109 | ||
98 | /** | 110 | /** |
99 | * Record a failed login attempt | 111 | * Record a failed login attempt |
100 | */ | 112 | */ |
101 | public function testHandleFailedLogin() | 113 | public function testHandleFailedLogin(): void |
102 | { | 114 | { |
115 | $this->banManager->expects(static::exactly(2))->method('handleFailedAttempt'); | ||
116 | $this->banManager->method('isBanned')->willReturn(true); | ||
117 | |||
103 | $this->loginManager->handleFailedLogin($this->server); | 118 | $this->loginManager->handleFailedLogin($this->server); |
104 | $this->loginManager->handleFailedLogin($this->server); | 119 | $this->loginManager->handleFailedLogin($this->server); |
105 | $this->assertFalse($this->loginManager->canLogin($this->server)); | 120 | |
121 | static::assertFalse($this->loginManager->canLogin($this->server)); | ||
106 | } | 122 | } |
107 | 123 | ||
108 | /** | 124 | /** |
@@ -114,8 +130,13 @@ class LoginManagerTest extends TestCase | |||
114 | 'REMOTE_ADDR' => $this->trustedProxy, | 130 | 'REMOTE_ADDR' => $this->trustedProxy, |
115 | 'HTTP_X_FORWARDED_FOR' => $this->ipAddr, | 131 | 'HTTP_X_FORWARDED_FOR' => $this->ipAddr, |
116 | ]; | 132 | ]; |
133 | |||
134 | $this->banManager->expects(static::exactly(2))->method('handleFailedAttempt'); | ||
135 | $this->banManager->method('isBanned')->willReturn(true); | ||
136 | |||
117 | $this->loginManager->handleFailedLogin($server); | 137 | $this->loginManager->handleFailedLogin($server); |
118 | $this->loginManager->handleFailedLogin($server); | 138 | $this->loginManager->handleFailedLogin($server); |
139 | |||
119 | $this->assertFalse($this->loginManager->canLogin($server)); | 140 | $this->assertFalse($this->loginManager->canLogin($server)); |
120 | } | 141 | } |
121 | 142 | ||
@@ -196,10 +217,16 @@ class LoginManagerTest extends TestCase | |||
196 | */ | 217 | */ |
197 | public function testCheckLoginStateNotConfigured() | 218 | public function testCheckLoginStateNotConfigured() |
198 | { | 219 | { |
199 | $configManager = new \FakeConfigManager([ | 220 | $configManager = new FakeConfigManager([ |
200 | 'resource.ban_file' => $this->banFile, | 221 | 'resource.ban_file' => $this->banFile, |
201 | ]); | 222 | ]); |
202 | $loginManager = new LoginManager($configManager, null, $this->cookieManager); | 223 | $loginManager = new LoginManager( |
224 | $configManager, | ||
225 | $this->sessionManager, | ||
226 | $this->cookieManager, | ||
227 | $this->banManager, | ||
228 | $this->createMock(LoggerInterface::class) | ||
229 | ); | ||
203 | $loginManager->checkLoginState(''); | 230 | $loginManager->checkLoginState(''); |
204 | 231 | ||
205 | $this->assertFalse($loginManager->isLoggedIn()); | 232 | $this->assertFalse($loginManager->isLoggedIn()); |
@@ -270,7 +297,7 @@ class LoginManagerTest extends TestCase | |||
270 | public function testCheckCredentialsWrongLogin() | 297 | public function testCheckCredentialsWrongLogin() |
271 | { | 298 | { |
272 | $this->assertFalse( | 299 | $this->assertFalse( |
273 | $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password) | 300 | $this->loginManager->checkCredentials('', 'b4dl0g1n', $this->password) |
274 | ); | 301 | ); |
275 | } | 302 | } |
276 | 303 | ||
@@ -280,7 +307,7 @@ class LoginManagerTest extends TestCase | |||
280 | public function testCheckCredentialsWrongPassword() | 307 | public function testCheckCredentialsWrongPassword() |
281 | { | 308 | { |
282 | $this->assertFalse( | 309 | $this->assertFalse( |
283 | $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd') | 310 | $this->loginManager->checkCredentials('', $this->login, 'b4dp455wd') |
284 | ); | 311 | ); |
285 | } | 312 | } |
286 | 313 | ||
@@ -290,7 +317,7 @@ class LoginManagerTest extends TestCase | |||
290 | public function testCheckCredentialsWrongLoginAndPassword() | 317 | public function testCheckCredentialsWrongLoginAndPassword() |
291 | { | 318 | { |
292 | $this->assertFalse( | 319 | $this->assertFalse( |
293 | $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd') | 320 | $this->loginManager->checkCredentials('', 'b4dl0g1n', 'b4dp455wd') |
294 | ); | 321 | ); |
295 | } | 322 | } |
296 | 323 | ||
@@ -300,7 +327,7 @@ class LoginManagerTest extends TestCase | |||
300 | public function testCheckCredentialsGoodLoginAndPassword() | 327 | public function testCheckCredentialsGoodLoginAndPassword() |
301 | { | 328 | { |
302 | $this->assertTrue( | 329 | $this->assertTrue( |
303 | $this->loginManager->checkCredentials('', '', $this->login, $this->password) | 330 | $this->loginManager->checkCredentials('', $this->login, $this->password) |
304 | ); | 331 | ); |
305 | } | 332 | } |
306 | 333 | ||
@@ -311,7 +338,7 @@ class LoginManagerTest extends TestCase | |||
311 | { | 338 | { |
312 | $this->configManager->set('ldap.host', 'dummy'); | 339 | $this->configManager->set('ldap.host', 'dummy'); |
313 | $this->assertFalse( | 340 | $this->assertFalse( |
314 | $this->loginManager->checkCredentials('', '', $this->login, $this->password) | 341 | $this->loginManager->checkCredentials('', $this->login, $this->password) |
315 | ); | 342 | ); |
316 | } | 343 | } |
317 | 344 | ||
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php index 3f9c3ef5..6830d714 100644 --- a/tests/security/SessionManagerTest.php +++ b/tests/security/SessionManagerTest.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Security; | 3 | namespace Shaarli\Security; |
4 | 4 | ||
5 | use Shaarli\FakeConfigManager; | ||
5 | use Shaarli\TestCase; | 6 | use Shaarli\TestCase; |
6 | 7 | ||
7 | /** | 8 | /** |
@@ -12,7 +13,7 @@ class SessionManagerTest extends TestCase | |||
12 | /** @var array Session ID hashes */ | 13 | /** @var array Session ID hashes */ |
13 | protected static $sidHashes = null; | 14 | protected static $sidHashes = null; |
14 | 15 | ||
15 | /** @var \FakeConfigManager ConfigManager substitute for testing */ | 16 | /** @var FakeConfigManager ConfigManager substitute for testing */ |
16 | protected $conf = null; | 17 | protected $conf = null; |
17 | 18 | ||
18 | /** @var array $_SESSION array for testing */ | 19 | /** @var array $_SESSION array for testing */ |
@@ -34,7 +35,7 @@ class SessionManagerTest extends TestCase | |||
34 | */ | 35 | */ |
35 | protected function setUp(): void | 36 | protected function setUp(): void |
36 | { | 37 | { |
37 | $this->conf = new \FakeConfigManager([ | 38 | $this->conf = new FakeConfigManager([ |
38 | 'credentials.login' => 'johndoe', | 39 | 'credentials.login' => 'johndoe', |
39 | 'credentials.salt' => 'salt', | 40 | 'credentials.salt' => 'salt', |
40 | 'security.session_protection_disabled' => false, | 41 | 'security.session_protection_disabled' => false, |
diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php index a6280b8c..cadd8265 100644 --- a/tests/updater/UpdaterTest.php +++ b/tests/updater/UpdaterTest.php | |||
@@ -2,6 +2,7 @@ | |||
2 | namespace Shaarli\Updater; | 2 | namespace Shaarli\Updater; |
3 | 3 | ||
4 | use Exception; | 4 | use Exception; |
5 | use malkusch\lock\mutex\NoMutex; | ||
5 | use Shaarli\Bookmark\BookmarkFileService; | 6 | use Shaarli\Bookmark\BookmarkFileService; |
6 | use Shaarli\Bookmark\BookmarkServiceInterface; | 7 | use Shaarli\Bookmark\BookmarkServiceInterface; |
7 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
@@ -44,12 +45,13 @@ class UpdaterTest extends TestCase | |||
44 | */ | 45 | */ |
45 | protected function setUp(): void | 46 | protected function setUp(): void |
46 | { | 47 | { |
48 | $mutex = new NoMutex(); | ||
47 | $this->refDB = new \ReferenceLinkDB(); | 49 | $this->refDB = new \ReferenceLinkDB(); |
48 | $this->refDB->write(self::$testDatastore); | 50 | $this->refDB->write(self::$testDatastore); |
49 | 51 | ||
50 | copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); | 52 | copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); |
51 | $this->conf = new ConfigManager(self::$configFile); | 53 | $this->conf = new ConfigManager(self::$configFile); |
52 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true); | 54 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), $mutex, true); |
53 | $this->updater = new Updater([], $this->bookmarkService, $this->conf, true); | 55 | $this->updater = new Updater([], $this->bookmarkService, $this->conf, true); |
54 | } | 56 | } |
55 | 57 | ||
@@ -58,10 +60,10 @@ class UpdaterTest extends TestCase | |||
58 | */ | 60 | */ |
59 | public function testReadEmptyUpdatesFile() | 61 | public function testReadEmptyUpdatesFile() |
60 | { | 62 | { |
61 | $this->assertEquals(array(), UpdaterUtils::read_updates_file('')); | 63 | $this->assertEquals(array(), UpdaterUtils::readUpdatesFile('')); |
62 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; | 64 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; |
63 | touch($updatesFile); | 65 | touch($updatesFile); |
64 | $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile)); | 66 | $this->assertEquals(array(), UpdaterUtils::readUpdatesFile($updatesFile)); |
65 | unlink($updatesFile); | 67 | unlink($updatesFile); |
66 | } | 68 | } |
67 | 69 | ||
@@ -73,14 +75,14 @@ class UpdaterTest extends TestCase | |||
73 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; | 75 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; |
74 | $updatesMethods = array('m1', 'm2', 'm3'); | 76 | $updatesMethods = array('m1', 'm2', 'm3'); |
75 | 77 | ||
76 | UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); | 78 | UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); |
77 | $readMethods = UpdaterUtils::read_updates_file($updatesFile); | 79 | $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); |
78 | $this->assertEquals($readMethods, $updatesMethods); | 80 | $this->assertEquals($readMethods, $updatesMethods); |
79 | 81 | ||
80 | // Update | 82 | // Update |
81 | $updatesMethods[] = 'm4'; | 83 | $updatesMethods[] = 'm4'; |
82 | UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); | 84 | UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); |
83 | $readMethods = UpdaterUtils::read_updates_file($updatesFile); | 85 | $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); |
84 | $this->assertEquals($readMethods, $updatesMethods); | 86 | $this->assertEquals($readMethods, $updatesMethods); |
85 | unlink($updatesFile); | 87 | unlink($updatesFile); |
86 | } | 88 | } |
@@ -93,7 +95,7 @@ class UpdaterTest extends TestCase | |||
93 | $this->expectException(\Exception::class); | 95 | $this->expectException(\Exception::class); |
94 | $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); | 96 | $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); |
95 | 97 | ||
96 | UpdaterUtils::write_updates_file('', array('test')); | 98 | UpdaterUtils::writeUpdatesFile('', array('test')); |
97 | } | 99 | } |
98 | 100 | ||
99 | /** | 101 | /** |
@@ -108,7 +110,7 @@ class UpdaterTest extends TestCase | |||
108 | touch($updatesFile); | 110 | touch($updatesFile); |
109 | chmod($updatesFile, 0444); | 111 | chmod($updatesFile, 0444); |
110 | try { | 112 | try { |
111 | @UpdaterUtils::write_updates_file($updatesFile, array('test')); | 113 | @UpdaterUtils::writeUpdatesFile($updatesFile, array('test')); |
112 | } catch (Exception $e) { | 114 | } catch (Exception $e) { |
113 | unlink($updatesFile); | 115 | unlink($updatesFile); |
114 | throw $e; | 116 | throw $e; |
diff --git a/tests/utils/FakeApplicationUtils.php b/tests/utils/FakeApplicationUtils.php index de83d598..d5289ede 100644 --- a/tests/utils/FakeApplicationUtils.php +++ b/tests/utils/FakeApplicationUtils.php | |||
@@ -2,6 +2,8 @@ | |||
2 | 2 | ||
3 | namespace Shaarli; | 3 | namespace Shaarli; |
4 | 4 | ||
5 | use Shaarli\Helper\ApplicationUtils; | ||
6 | |||
5 | /** | 7 | /** |
6 | * Fake ApplicationUtils class to avoid HTTP requests | 8 | * Fake ApplicationUtils class to avoid HTTP requests |
7 | */ | 9 | */ |
diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php index 360b34a9..014c2af0 100644 --- a/tests/utils/FakeConfigManager.php +++ b/tests/utils/FakeConfigManager.php | |||
@@ -1,9 +1,13 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | namespace Shaarli; | ||
4 | |||
5 | use Shaarli\Config\ConfigManager; | ||
6 | |||
3 | /** | 7 | /** |
4 | * Fake ConfigManager | 8 | * Fake ConfigManager |
5 | */ | 9 | */ |
6 | class FakeConfigManager | 10 | class FakeConfigManager extends ConfigManager |
7 | { | 11 | { |
8 | protected $values = []; | 12 | protected $values = []; |
9 | 13 | ||
@@ -23,7 +27,7 @@ class FakeConfigManager | |||
23 | * @param string $key Key of the value to set | 27 | * @param string $key Key of the value to set |
24 | * @param mixed $value Value to set | 28 | * @param mixed $value Value to set |
25 | */ | 29 | */ |
26 | public function set($key, $value) | 30 | public function set($key, $value, $write = false, $isLoggedIn = false) |
27 | { | 31 | { |
28 | $this->values[$key] = $value; | 32 | $this->values[$key] = $value; |
29 | } | 33 | } |
@@ -35,7 +39,7 @@ class FakeConfigManager | |||
35 | * | 39 | * |
36 | * @return mixed The value if set, else the name of the key | 40 | * @return mixed The value if set, else the name of the key |
37 | */ | 41 | */ |
38 | public function get($key) | 42 | public function get($key, $default = '') |
39 | { | 43 | { |
40 | if (isset($this->values[$key])) { | 44 | if (isset($this->values[$key])) { |
41 | return $this->values[$key]; | 45 | return $this->values[$key]; |
diff --git a/tests/utils/ReferenceHistory.php b/tests/utils/ReferenceHistory.php index 516c9f51..aed5d2cf 100644 --- a/tests/utils/ReferenceHistory.php +++ b/tests/utils/ReferenceHistory.php | |||
@@ -1,6 +1,6 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | use Shaarli\FileUtils; | 3 | use Shaarli\Helper\FileUtils; |
4 | use Shaarli\History; | 4 | use Shaarli\History; |
5 | 5 | ||
6 | /** | 6 | /** |
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index fc3cb109..1f53dc3c 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php | |||
@@ -82,7 +82,7 @@ class ReferenceLinkDB | |||
82 | 'This guide extends and expands on PSR-1, the basic coding standard.', | 82 | 'This guide extends and expands on PSR-1, the basic coding standard.', |
83 | 0, | 83 | 0, |
84 | DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'), | 84 | DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'), |
85 | '' | 85 | 'coding-style standards quality assurance' |
86 | ); | 86 | ); |
87 | 87 | ||
88 | $this->addLink( | 88 | $this->addLink( |
diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html index 67d3ebd1..4aac7ff1 100644 --- a/tpl/default/addlink.html +++ b/tpl/default/addlink.html | |||
@@ -20,6 +20,62 @@ | |||
20 | </form> | 20 | </form> |
21 | </div> | 21 | </div> |
22 | </div> | 22 | </div> |
23 | |||
24 | <div class="pure-g addlink-batch-show-more-block pure-u-0"> | ||
25 | <div class="pure-u-lg-1-3 pure-u-1-24"></div> | ||
26 | <div class="pure-u-lg-1-3 pure-u-22-24 addlink-batch-show-more"> | ||
27 | <a href="#">{'BULK CREATION'|t} <i class="fa fa-plus-circle" aria-hidden="true"></i></a> | ||
28 | </div> | ||
29 | </div> | ||
30 | |||
31 | <div class="addlink-batch-form-block"> | ||
32 | {if="empty($async_metadata)"} | ||
33 | <div class="pure-g pure-alert pure-alert-warning pure-alert-closable"> | ||
34 | <div class="pure-u-2-24"></div> | ||
35 | <div class="pure-u-20-24"> | ||
36 | <p> | ||
37 | {'Metadata asynchronous retrieval is disabled.'|t} | ||
38 | {'We recommend that you enable the setting <em>general > enable_async_metadata</em> in your configuration file to use bulk link creation.'|t} | ||
39 | </p> | ||
40 | </div> | ||
41 | <div class="pure-u-2-24"> | ||
42 | <i class="fa fa-times pure-alert-close"></i> | ||
43 | </div> | ||
44 | </div> | ||
45 | {/if} | ||
46 | |||
47 | <div class="pure-g"> | ||
48 | <div class="pure-u-lg-1-3 pure-u-1-24"></div> | ||
49 | <div id="batch-addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> | ||
50 | <h2 class="window-title">{"Shaare multiple new links"|t}</h2> | ||
51 | <form method="POST" action="{$base_path}/admin/shaare-batch" name="batch-addform" class="batch-addform"> | ||
52 | <div> | ||
53 | <label for="urls">{'Add one URL per line to create multiple bookmarks.'|t}</label> | ||
54 | <textarea name="urls" id="urls"></textarea> | ||
55 | |||
56 | <div> | ||
57 | <label for="tags">{'Tags'|t}</label> | ||
58 | </div> | ||
59 | <div> | ||
60 | <input type="text" name="tags" id="tags" class="lf_input" | ||
61 | data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off"> | ||
62 | </div> | ||
63 | |||
64 | <div> | ||
65 | <input type="hidden" name="private" value="0"> | ||
66 | <input type="checkbox" name="private" {if="$default_private_links"} checked="checked"{/if}> | ||
67 | <label for="lf_private">{'Private'|t}</label> | ||
68 | </div> | ||
69 | </div> | ||
70 | <div> | ||
71 | <input type="hidden" name="token" value="{$token}"> | ||
72 | <input type="submit" value="{'Add links'|t}"> | ||
73 | </div> | ||
74 | </form> | ||
75 | </div> | ||
76 | </div> | ||
77 | </div> | ||
78 | |||
23 | {include="page.footer"} | 79 | {include="page.footer"} |
24 | </body> | 80 | </body> |
25 | </html> | 81 | </html> |
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html index 16c55896..13b7f24a 100644 --- a/tpl/default/changetag.html +++ b/tpl/default/changetag.html | |||
@@ -27,14 +27,38 @@ | |||
27 | <div><i class="fa fa-info-circle" aria-hidden="true"></i> {'Case sensitive'|t}</div> | 27 | <div><i class="fa fa-info-circle" aria-hidden="true"></i> {'Case sensitive'|t}</div> |
28 | <input type="hidden" name="token" value="{$token}"> | 28 | <input type="hidden" name="token" value="{$token}"> |
29 | <div> | 29 | <div> |
30 | <input type="submit" value="{'Rename'|t}" name="renametag"> | 30 | <input type="submit" value="{'Rename tag'|t}" name="renametag"> |
31 | <input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete"> | 31 | <input type="submit" value="{'Delete tag'|t}" name="deletetag" |
32 | class="button button-red confirm-delete" data-type="tag"> | ||
32 | </div> | 33 | </div> |
33 | </form> | 34 | </form> |
34 | 35 | ||
35 | <p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p> | 36 | <p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p> |
36 | </div> | 37 | </div> |
37 | </div> | 38 | </div> |
39 | |||
40 | <div class="pure-g"> | ||
41 | <div class="pure-u-lg-1-3 pure-u-1-24"></div> | ||
42 | <div class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> | ||
43 | <h2 class="window-title">{"Change tags separator"|t}</h2> | ||
44 | <form method="POST" action="{$base_path}/admin/tags/change-separator" name="changeseparator" id="changeseparator"> | ||
45 | <p> | ||
46 | {'Your current tag separator is'|t} <code>{$tags_separator}</code>{if="!empty($tags_separator_desc)"} ({$tags_separator_desc}){/if}. | ||
47 | </p> | ||
48 | <div> | ||
49 | <input type="text" name="separator" placeholder="{'New separator'|t}" | ||
50 | id="separator"> | ||
51 | </div> | ||
52 | <input type="hidden" name="token" value="{$token}"> | ||
53 | <div> | ||
54 | <input type="submit" value="{'Save'|t}" name="saveseparator"> | ||
55 | </div> | ||
56 | <p> | ||
57 | {'Note that hashtags won\'t fully work with a non-whitespace separator.'|t} | ||
58 | </p> | ||
59 | </form> | ||
60 | </div> | ||
61 | </div> | ||
38 | {include="page.footer"} | 62 | {include="page.footer"} |
39 | </body> | 63 | </body> |
40 | </html> | 64 | </html> |
diff --git a/tpl/default/daily.html b/tpl/default/daily.html index 3ab8053f..5e038c39 100644 --- a/tpl/default/daily.html +++ b/tpl/default/daily.html | |||
@@ -7,11 +7,24 @@ | |||
7 | {include="page.header"} | 7 | {include="page.header"} |
8 | 8 | ||
9 | <div class="pure-g"> | 9 | <div class="pure-g"> |
10 | <div class="pure-u-1 pure-alert pure-alert-success tag-sort"> | ||
11 | <a href="{$base_path}/daily?day">{'Daily'|t}</a> | ||
12 | <a href="{$base_path}/daily?week">{'Weekly'|t}</a> | ||
13 | <a href="{$base_path}/daily?month">{'Monthly'|t}</a> | ||
14 | </div> | ||
15 | </div> | ||
16 | |||
17 | |||
18 | <div class="pure-g"> | ||
10 | <div class="pure-u-lg-1-6 pure-u-1-24"></div> | 19 | <div class="pure-u-lg-1-6 pure-u-1-24"></div> |
11 | <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily"> | 20 | <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily"> |
12 | <h2 class="window-title"> | 21 | <h2 class="window-title"> |
13 | {'The Daily Shaarli'|t} | 22 | {$localizedType} Shaarli |
14 | <a href="{$base_path}/daily-rss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a> | 23 | <a href="{$base_path}/daily-rss?{$type}" |
24 | title="{function="t('1 RSS entry per :type', '', 1, 'shaarli', [':type' => t($type)])"}" | ||
25 | > | ||
26 | <i class="fa fa-rss"></i> | ||
27 | </a> | ||
15 | </h2> | 28 | </h2> |
16 | 29 | ||
17 | <div id="plugin_zone_start_daily" class="plugin_zone"> | 30 | <div id="plugin_zone_start_daily" class="plugin_zone"> |
@@ -25,19 +38,19 @@ | |||
25 | <div class="pure-g"> | 38 | <div class="pure-g"> |
26 | <div class="pure-u-lg-1-3 pure-u-1 center"> | 39 | <div class="pure-u-lg-1-3 pure-u-1 center"> |
27 | {if="$previousday"} | 40 | {if="$previousday"} |
28 | <a href="{$base_path}/daily?day={$previousday}"> | 41 | <a href="{$base_path}/daily?{$type}={$previousday}"> |
29 | <i class="fa fa-arrow-left"></i> | 42 | <i class="fa fa-arrow-left"></i> |
30 | {'Previous day'|t} | 43 | {function="t('Previous :type', '', 1, 'shaarli', [':type' => t($type)], true)"} |
31 | </a> | 44 | </a> |
32 | {/if} | 45 | {/if} |
33 | </div> | 46 | </div> |
34 | <div class="daily-desc pure-u-lg-1-3 pure-u-1 center"> | 47 | <div class="daily-desc pure-u-lg-1-3 pure-u-1 center"> |
35 | {'All links of one day in a single page.'|t} | 48 | {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"} |
36 | </div> | 49 | </div> |
37 | <div class="pure-u-lg-1-3 pure-u-1 center"> | 50 | <div class="pure-u-lg-1-3 pure-u-1 center"> |
38 | {if="$nextday"} | 51 | {if="$nextday"} |
39 | <a href="{$base_path}/daily?day={$nextday}"> | 52 | <a href="{$base_path}/daily?{$type}={$nextday}"> |
40 | {'Next day'|t} | 53 | {function="t('Next :type', '', 1, 'shaarli', [':type' => t($type)], true)"} |
41 | <i class="fa fa-arrow-right"></i> | 54 | <i class="fa fa-arrow-right"></i> |
42 | </a> | 55 | </a> |
43 | {/if} | 56 | {/if} |
@@ -45,10 +58,7 @@ | |||
45 | </div> | 58 | </div> |
46 | <div> | 59 | <div> |
47 | <h3 class="window-subtitle"> | 60 | <h3 class="window-subtitle"> |
48 | {if="!empty($dayDesc)"} | 61 | {$dayDesc} |
49 | {$dayDesc} - | ||
50 | {/if} | ||
51 | {function="format_date($dayDate, false)"} | ||
52 | </h3> | 62 | </h3> |
53 | 63 | ||
54 | <div id="plugin_zone_about_daily" class="plugin_zone"> | 64 | <div id="plugin_zone_about_daily" class="plugin_zone"> |
@@ -76,7 +86,7 @@ | |||
76 | </div> | 86 | </div> |
77 | {if="$thumbnails_enabled && !empty($link.thumbnail)"} | 87 | {if="$thumbnails_enabled && !empty($link.thumbnail)"} |
78 | <div class="daily-entry-thumbnail"> | 88 | <div class="daily-entry-thumbnail"> |
79 | <img data-src="{$link.thumbnail}#" class="b-lazy" | 89 | <img data-src="{$root_path}/{$link.thumbnail}#" class="b-lazy" |
80 | src="" | 90 | src="" |
81 | alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" /> | 91 | alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" /> |
82 | </div> | 92 | </div> |
diff --git a/tpl/default/dailyrss.html b/tpl/default/dailyrss.html index d40d9496..871a3ba7 100644 --- a/tpl/default/dailyrss.html +++ b/tpl/default/dailyrss.html | |||
@@ -1,9 +1,9 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | 1 | <?xml version="1.0" encoding="UTF-8"?> |
2 | <rss version="2.0"> | 2 | <rss version="2.0"> |
3 | <channel> | 3 | <channel> |
4 | <title>Daily - {$title}</title> | 4 | <title>{$localizedType} - {$title}</title> |
5 | <link>{$index_url}</link> | 5 | <link>{$index_url}</link> |
6 | <description>Daily shaared bookmarks</description> | 6 | <description>{function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}</description> |
7 | <language>{$language}</language> | 7 | <language>{$language}</language> |
8 | <copyright>{$index_url}</copyright> | 8 | <copyright>{$index_url}</copyright> |
9 | <generator>Shaarli</generator> | 9 | <generator>Shaarli</generator> |
@@ -18,12 +18,15 @@ | |||
18 | {loop="$value.links"} | 18 | {loop="$value.links"} |
19 | <h3><a href="{$value.url}">{$value.title}</a></h3> | 19 | <h3><a href="{$value.url}">{$value.title}</a></h3> |
20 | <small> | 20 | <small> |
21 | {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> | 21 | {if="!$hide_timestamps"}{$value.created|format_date} — {/if} |
22 | <a href="{$index_url}shaare/{$value.shorturl}">{'Permalink'|t}</a> | ||
23 | {if="$value.tags"} — {$value.tags}{/if} | ||
24 | <br> | ||
22 | {$value.url} | 25 | {$value.url} |
23 | </small><br> | 26 | </small><br> |
24 | {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br> | 27 | {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br> |
25 | {if="$value.description"}{$value.description}{/if} | 28 | {if="$value.description"}{$value.description}{/if} |
26 | <br><br><hr> | 29 | <br><hr> |
27 | {/loop} | 30 | {/loop} |
28 | ]]></description> | 31 | ]]></description> |
29 | </item> | 32 | </item> |
diff --git a/tpl/default/editlink.batch.html b/tpl/default/editlink.batch.html new file mode 100644 index 00000000..b1f8e5bd --- /dev/null +++ b/tpl/default/editlink.batch.html | |||
@@ -0,0 +1,32 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html{if="$language !== 'auto'"} lang="{$language}"{/if}> | ||
3 | <head> | ||
4 | {include="includes"} | ||
5 | </head> | ||
6 | <body> | ||
7 | <div class="dark-layer"> | ||
8 | <div class="screen-center"> | ||
9 | <div><span class="progressbar-current"></span> / <span class="progressbar-max"></span></div> | ||
10 | <div class="progressbar"> | ||
11 | <div></div> | ||
12 | </div> | ||
13 | </div> | ||
14 | </div> | ||
15 | |||
16 | {include="page.header"} | ||
17 | |||
18 | <div class="center"> | ||
19 | <input type="submit" name="save_edit_batch" class="pure-button-shaarli" value="{'Save all'|t}"> | ||
20 | </div> | ||
21 | |||
22 | {loop="$links"} | ||
23 | {include="editlink"} | ||
24 | {/loop} | ||
25 | |||
26 | <div class="center"> | ||
27 | <input type="submit" name="save_edit_batch" class="pure-button-shaarli" value="{'Save all'|t}"> | ||
28 | </div> | ||
29 | |||
30 | {include="page.footer"} | ||
31 | {if="$async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if} | ||
32 | <script src="{$asset_path}/js/shaare_batch.min.js?v={$version_hash}#"></script> | ||
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html index 568545bd..83e541fd 100644 --- a/tpl/default/editlink.html +++ b/tpl/default/editlink.html | |||
@@ -1,3 +1,4 @@ | |||
1 | {if="empty($batch_mode)"} | ||
1 | <!DOCTYPE html> | 2 | <!DOCTYPE html> |
2 | <html{if="$language !== 'auto'"} lang="{$language}"{/if}> | 3 | <html{if="$language !== 'auto'"} lang="{$language}"{/if}> |
3 | <head> | 4 | <head> |
@@ -5,6 +6,10 @@ | |||
5 | </head> | 6 | </head> |
6 | <body> | 7 | <body> |
7 | {include="page.header"} | 8 | {include="page.header"} |
9 | {else} | ||
10 | {ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore} | ||
11 | {function="extract($value) ? '' : ''"} | ||
12 | {/if} | ||
8 | <div id="editlinkform" class="edit-link-container" class="pure-g"> | 13 | <div id="editlinkform" class="edit-link-container" class="pure-g"> |
9 | <div class="pure-u-lg-1-5 pure-u-1-24"></div> | 14 | <div class="pure-u-lg-1-5 pure-u-1-24"></div> |
10 | <form method="post" | 15 | <form method="post" |
@@ -12,6 +17,8 @@ | |||
12 | action="{$base_path}/admin/shaare" | 17 | action="{$base_path}/admin/shaare" |
13 | class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light" | 18 | class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light" |
14 | > | 19 | > |
20 | {$asyncLoadClass=$link_is_new && $async_metadata && empty($link.title) ? 'loading-input' : ''} | ||
21 | |||
15 | <h2 class="window-title"> | 22 | <h2 class="window-title"> |
16 | {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} | 23 | {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} |
17 | </h2> | 24 | </h2> |
@@ -28,26 +35,37 @@ | |||
28 | <div> | 35 | <div> |
29 | <label for="lf_title">{'Title'|t}</label> | 36 | <label for="lf_title">{'Title'|t}</label> |
30 | </div> | 37 | </div> |
31 | <div> | 38 | <div class="{$asyncLoadClass}"> |
32 | <input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input autofocus"> | 39 | <input type="text" name="lf_title" id="lf_title" value="{$link.title}" |
40 | class="lf_input {if="!$async_metadata"}autofocus{/if}" | ||
41 | > | ||
42 | <div class="icon-container"> | ||
43 | <i class="loader"></i> | ||
44 | </div> | ||
33 | </div> | 45 | </div> |
34 | <div> | 46 | <div> |
35 | <label for="lf_description">{'Description'|t}</label> | 47 | <label for="lf_description">{'Description'|t}</label> |
36 | </div> | 48 | </div> |
37 | <div> | 49 | <div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}"> |
38 | <textarea name="lf_description" id="lf_description" class="autofocus">{$link.description}</textarea> | 50 | <textarea name="lf_description" id="lf_description" class="autofocus">{$link.description}</textarea> |
51 | <div class="icon-container"> | ||
52 | <i class="loader"></i> | ||
53 | </div> | ||
39 | </div> | 54 | </div> |
40 | <div> | 55 | <div> |
41 | <label for="lf_tags">{'Tags'|t}</label> | 56 | <label for="lf_tags">{'Tags'|t}</label> |
42 | </div> | 57 | </div> |
43 | <div> | 58 | <div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}"> |
44 | <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input autofocus" | 59 | <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input autofocus" |
45 | data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" > | 60 | data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" > |
61 | <div class="icon-container"> | ||
62 | <i class="loader"></i> | ||
63 | </div> | ||
46 | </div> | 64 | </div> |
47 | 65 | ||
48 | <div> | 66 | <div> |
49 | <input type="checkbox" name="lf_private" id="lf_private" | 67 | <input type="checkbox" name="lf_private" id="lf_private" |
50 | {if="($link_is_new && $default_private_links || $link.private == true)"} | 68 | {if="$link.private === true"} |
51 | checked="checked" | 69 | checked="checked" |
52 | {/if}> | 70 | {/if}> |
53 | <label for="lf_private">{'Private'|t}</label> | 71 | <label for="lf_private">{'Private'|t}</label> |
@@ -70,6 +88,13 @@ | |||
70 | 88 | ||
71 | 89 | ||
72 | <div class="submit-buttons center"> | 90 | <div class="submit-buttons center"> |
91 | {if="!empty($batch_mode)"} | ||
92 | <a href="#" class="button button-grey" name="cancel-batch-link" | ||
93 | title="{'Remove this bookmark from batch creation/modification.'}" | ||
94 | > | ||
95 | {'Cancel'|t} | ||
96 | </a> | ||
97 | {/if} | ||
73 | <input type="submit" name="save_edit" class="" id="button-save-edit" | 98 | <input type="submit" name="save_edit" class="" id="button-save-edit" |
74 | value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}"> | 99 | value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}"> |
75 | {if="!$link_is_new"} | 100 | {if="!$link_is_new"} |
@@ -87,6 +112,10 @@ | |||
87 | {/if} | 112 | {/if} |
88 | </form> | 113 | </form> |
89 | </div> | 114 | </div> |
115 | |||
116 | {if="empty($batch_mode)"} | ||
90 | {include="page.footer"} | 117 | {include="page.footer"} |
118 | {if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if} | ||
91 | </body> | 119 | </body> |
92 | </html> | 120 | </html> |
121 | {/if} | ||
diff --git a/tpl/default/error.html b/tpl/default/error.html index c3e0c3c1..34f9707d 100644 --- a/tpl/default/error.html +++ b/tpl/default/error.html | |||
@@ -9,13 +9,17 @@ | |||
9 | <div id="pageError" class="page-error-container center"> | 9 | <div id="pageError" class="page-error-container center"> |
10 | <h2>{$message}</h2> | 10 | <h2>{$message}</h2> |
11 | 11 | ||
12 | <img src="{$asset_path}/img/sad_star.png#" alt=""> | ||
13 | |||
14 | {if="!empty($text)"} | ||
15 | <p>{$text}</p> | ||
16 | {/if} | ||
17 | |||
12 | {if="!empty($stacktrace)"} | 18 | {if="!empty($stacktrace)"} |
13 | <pre> | 19 | <pre> |
14 | {$stacktrace} | 20 | {$stacktrace} |
15 | </pre> | 21 | </pre> |
16 | {/if} | 22 | {/if} |
17 | |||
18 | <img src="{$asset_path}/img/sad_star.png#" alt=""> | ||
19 | </div> | 23 | </div> |
20 | {include="page.footer"} | 24 | {include="page.footer"} |
21 | </body> | 25 | </body> |
diff --git a/tpl/default/includes.html b/tpl/default/includes.html index 227f9b52..3e3fb664 100644 --- a/tpl/default/includes.html +++ b/tpl/default/includes.html | |||
@@ -8,14 +8,14 @@ | |||
8 | <link href="{$asset_path}/img/favicon.png#" rel="shortcut icon" type="image/png" /> | 8 | <link href="{$asset_path}/img/favicon.png#" rel="shortcut icon" type="image/png" /> |
9 | <link href="{$asset_path}/img/apple-touch-icon.png#" rel="apple-touch-icon" sizes="180x180" /> | 9 | <link href="{$asset_path}/img/apple-touch-icon.png#" rel="apple-touch-icon" sizes="180x180" /> |
10 | <link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css?v={$version_hash}#" /> | 10 | <link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css?v={$version_hash}#" /> |
11 | {if="$formatter==='markdown'"} | 11 | {if="strpos($formatter, 'markdown') !== false"} |
12 | <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" /> | 12 | <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" /> |
13 | {/if} | 13 | {/if} |
14 | {loop="$plugins_includes.css_files"} | 14 | {loop="$plugins_includes.css_files"} |
15 | <link type="text/css" rel="stylesheet" href="{$base_path}/{$value}?v={$version_hash}#"/> | 15 | <link type="text/css" rel="stylesheet" href="{$root_path}/{$value}?v={$version_hash}#"/> |
16 | {/loop} | 16 | {/loop} |
17 | {if="is_file('data/user.css')"} | 17 | {if="is_file('data/user.css')"} |
18 | <link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" /> | 18 | <link type="text/css" rel="stylesheet" href="{$root_path}/data/user.css#" /> |
19 | {/if} | 19 | {/if} |
20 | <link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" | 20 | <link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" |
21 | title="Shaarli search - {$shaarlititle}" /> | 21 | title="Shaarli search - {$shaarlititle}" /> |
diff --git a/tpl/default/install.html b/tpl/default/install.html index a506a2eb..4f98d49d 100644 --- a/tpl/default/install.html +++ b/tpl/default/install.html | |||
@@ -163,6 +163,16 @@ | |||
163 | </div> | 163 | </div> |
164 | </div> | 164 | </div> |
165 | </form> | 165 | </form> |
166 | |||
167 | <div class="pure-g"> | ||
168 | <div class="pure-u-lg-1-6 pure-u-1-24"></div> | ||
169 | <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete"> | ||
170 | <h2 class="window-title">{'Server requirements'|t}</h2> | ||
171 | |||
172 | {include="server.requirements"} | ||
173 | </div> | ||
174 | </div> | ||
175 | |||
166 | {include="page.footer"} | 176 | {include="page.footer"} |
167 | </body> | 177 | </body> |
168 | </html> | 178 | </html> |
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index b08773d8..7208a3b6 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html | |||
@@ -90,7 +90,7 @@ | |||
90 | {'for'|t} <em><strong>{$search_term}</strong></em> | 90 | {'for'|t} <em><strong>{$search_term}</strong></em> |
91 | {/if} | 91 | {/if} |
92 | {if="!empty($search_tags)"} | 92 | {if="!empty($search_tags)"} |
93 | {$exploded_tags=explode(' ', $search_tags)} | 93 | {$exploded_tags=tags_str2array($search_tags, $tags_separator)} |
94 | {'tagged'|t} | 94 | {'tagged'|t} |
95 | {loop="$exploded_tags"} | 95 | {loop="$exploded_tags"} |
96 | <span class="label label-tag" title="{'Remove tag'|t}"> | 96 | <span class="label label-tag" title="{'Remove tag'|t}"> |
@@ -129,18 +129,23 @@ | |||
129 | {$strAddTag=t('Add tag')} | 129 | {$strAddTag=t('Add tag')} |
130 | {$strToggleSticky=t('Toggle sticky')} | 130 | {$strToggleSticky=t('Toggle sticky')} |
131 | {$strSticky=t('Sticky')} | 131 | {$strSticky=t('Sticky')} |
132 | {$strShaarePrivate=t('Share a private link')} | ||
132 | {ignore}End of translations{/ignore} | 133 | {ignore}End of translations{/ignore} |
133 | {loop="links"} | 134 | {loop="links"} |
134 | <div class="anchor" id="{$value.shorturl}"></div> | 135 | <div class="anchor" id="{$value.shorturl}"></div> |
135 | 136 | ||
136 | <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> | 137 | <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> |
137 | <div class="linklist-item-title"> | 138 | <div class="linklist-item-title"> |
138 | {if="$thumbnails_enabled && !empty($value.thumbnail)"} | 139 | {if="$thumbnails_enabled && $value.thumbnail !== false"} |
139 | <div class="linklist-item-thumbnail" style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;"> | 140 | <div |
141 | class="linklist-item-thumbnail {if="$value.thumbnail === null"}hidden{/if}" | ||
142 | style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;" | ||
143 | {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if} | ||
144 | > | ||
140 | <div class="thumbnail"> | 145 | <div class="thumbnail"> |
141 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} | 146 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} |
142 | <a href="{$value.real_url}" aria-hidden="true" tabindex="-1"> | 147 | <a href="{$value.real_url}" aria-hidden="true" tabindex="-1"> |
143 | <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy" | 148 | <img data-src="{$root_path}/{$value.thumbnail}#" class="b-lazy" |
144 | src="" | 149 | src="" |
145 | alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> | 150 | alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> |
146 | </a> | 151 | </a> |
@@ -158,14 +163,14 @@ | |||
158 | </div> | 163 | </div> |
159 | 164 | ||
160 | <h2> | 165 | <h2> |
161 | <a href="{$value.real_url}"> | 166 | <a href="{$value.real_url}" class="linklist-real-url"> |
162 | {if="strpos($value.url, $value.shorturl) === false"} | 167 | {if="strpos($value.url, $value.shorturl) === false"} |
163 | <i class="fa fa-external-link" aria-hidden="true"></i> | 168 | <i class="fa fa-external-link" aria-hidden="true"></i> |
164 | {else} | 169 | {else} |
165 | <i class="fa fa-sticky-note" aria-hidden="true"></i> | 170 | <i class="fa fa-sticky-note" aria-hidden="true"></i> |
166 | {/if} | 171 | {/if} |
167 | 172 | ||
168 | <span class="linklist-link">{$value.title}</span> | 173 | <span class="linklist-link">{$value.title_html}</span> |
169 | </a> | 174 | </a> |
170 | </h2> | 175 | </h2> |
171 | </div> | 176 | </div> |
@@ -183,7 +188,7 @@ | |||
183 | {$tag_counter=count($value.taglist)} | 188 | {$tag_counter=count($value.taglist)} |
184 | {loop="value.taglist"} | 189 | {loop="value.taglist"} |
185 | <span class="label label-tag" title="{$strAddTag}"> | 190 | <span class="label label-tag" title="{$strAddTag}"> |
186 | <a href="{$base_path}/add-tag/{$value1.urlencoded_taglist.$key2}">{$value}</a> | 191 | <a href="{$base_path}/add-tag/{$value1.taglist_urlencoded.$key2}">{$value1.taglist_html.$key2}</a> |
187 | </span> | 192 | </span> |
188 | {if="$tag_counter - 1 != $counter"}·{/if} | 193 | {if="$tag_counter - 1 != $counter"}·{/if} |
189 | {/loop} | 194 | {/loop} |
@@ -237,6 +242,12 @@ | |||
237 | {$strPermalinkLc} | 242 | {$strPermalinkLc} |
238 | </a> | 243 | </a> |
239 | 244 | ||
245 | {if="$is_logged_in && $value.private"} | ||
246 | <a href="{$base_path}/admin/shaare/private/{$value.shorturl}?token={$token}" title="{$strShaarePrivate}"> | ||
247 | <i class="fa fa-share-alt"></i> | ||
248 | </a> | ||
249 | {/if} | ||
250 | |||
240 | <div class="pure-u-0 pure-u-lg-visible"> | 251 | <div class="pure-u-0 pure-u-lg-visible"> |
241 | {if="isset($value.link_plugin)"} | 252 | {if="isset($value.link_plugin)"} |
242 | · | 253 | · |
@@ -251,7 +262,7 @@ | |||
251 | {ignore}do not add space or line break between these div - Firefox issue{/ignore} | 262 | {ignore}do not add space or line break between these div - Firefox issue{/ignore} |
252 | class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1"> | 263 | class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1"> |
253 | <a href="{$value.real_url}" aria-label="{$value.title}" title="{$value.title}"> | 264 | <a href="{$value.real_url}" aria-label="{$value.title}" title="{$value.title}"> |
254 | <i class="fa fa-link" aria-hidden="true"></i> {$value.url} | 265 | <i class="fa fa-link" aria-hidden="true"></i> {$value.url_html} |
255 | </a> | 266 | </a> |
256 | <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible"> | 267 | <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible"> |
257 | <a href="#" aria-label="{$strFold}" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up" aria-hidden="true"></i></a> | 268 | <a href="#" aria-label="{$strFold}" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up" aria-hidden="true"></i></a> |
@@ -308,5 +319,6 @@ | |||
308 | 319 | ||
309 | {include="page.footer"} | 320 | {include="page.footer"} |
310 | <script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script> | 321 | <script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script> |
322 | {if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if} | ||
311 | </body> | 323 | </body> |
312 | </html> | 324 | </html> |
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html index 51bdb2f0..58ca18c5 100644 --- a/tpl/default/page.footer.html +++ b/tpl/default/page.footer.html | |||
@@ -10,7 +10,7 @@ | |||
10 | {/if} | 10 | {/if} |
11 | · | 11 | · |
12 | {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} · | 12 | {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} · |
13 | <a href="{$base_path}/doc/html/index.html" rel="nofollow">{'Documentation'|t}</a> | 13 | <a href="{$root_path}/doc/html/index.html" rel="nofollow">{'Documentation'|t}</a> |
14 | {loop="$plugins_footer.text"} | 14 | {loop="$plugins_footer.text"} |
15 | {$value} | 15 | {$value} |
16 | {/loop} | 16 | {/loop} |
@@ -18,26 +18,28 @@ | |||
18 | <div class="pure-u-2-24"></div> | 18 | <div class="pure-u-2-24"></div> |
19 | </div> | 19 | </div> |
20 | 20 | ||
21 | <input type="hidden" name="token" value="{$token}" id="token" /> | ||
22 | |||
23 | {loop="$plugins_footer.endofpage"} | 21 | {loop="$plugins_footer.endofpage"} |
24 | {$value} | 22 | {$value} |
25 | {/loop} | 23 | {/loop} |
26 | 24 | ||
27 | {loop="$plugins_footer.js_files"} | 25 | {loop="$plugins_footer.js_files"} |
28 | <script src="{$base_path}/{$value}#"></script> | 26 | <script src="{$root_path}/{$value}#"></script> |
29 | {/loop} | 27 | {/loop} |
30 | 28 | ||
31 | <div id="js-translations" class="hidden"> | 29 | <div id="js-translations" class="hidden" aria-hidden="true"> |
32 | <span id="translation-fold">{'Fold'|t}</span> | 30 | <span id="translation-fold">{'Fold'|t}</span> |
33 | <span id="translation-fold-all">{'Fold all'|t}</span> | 31 | <span id="translation-fold-all">{'Fold all'|t}</span> |
34 | <span id="translation-expand">{'Expand'|t}</span> | 32 | <span id="translation-expand">{'Expand'|t}</span> |
35 | <span id="translation-expand-all">{'Expand all'|t}</span> | 33 | <span id="translation-expand-all">{'Expand all'|t}</span> |
36 | <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span> | 34 | <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span> |
35 | <span id="translation-delete-tag">{'Are you sure you want to delete this tag?'|t}</span> | ||
37 | <span id="translation-shaarli-desc"> | 36 | <span id="translation-shaarli-desc"> |
38 | {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} | 37 | {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} |
39 | </span> | 38 | </span> |
40 | </div> | 39 | </div> |
41 | 40 | ||
42 | <input type="hidden" name="js_base_path" value="{$base_path}" /> | 41 | <input type="hidden" name="js_base_path" value="{$base_path}" /> |
42 | <input type="hidden" name="token" value="{$token}" id="token" /> | ||
43 | <input type="hidden" name="tags_separator" value="{$tags_separator}" id="tags_separator" /> | ||
44 | |||
43 | <script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script> | 45 | <script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script> |
diff --git a/tpl/default/picwall.html b/tpl/default/picwall.html index b7a56c89..ac613b35 100644 --- a/tpl/default/picwall.html +++ b/tpl/default/picwall.html | |||
@@ -31,7 +31,7 @@ | |||
31 | {loop="$linksToDisplay"} | 31 | {loop="$linksToDisplay"} |
32 | <div class="picwall-pictureframe" role="listitem"> | 32 | <div class="picwall-pictureframe" role="listitem"> |
33 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} | 33 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} |
34 | <img data-src="{$value.thumbnail}#" class="b-lazy" | 34 | <img data-src="{$root_path}/{$value.thumbnail}#" class="b-lazy" |
35 | src="" | 35 | src="" |
36 | alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> | 36 | alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> |
37 | <a href="{$value.real_url}"><span class="info">{$value.title}</span></a> | 37 | <a href="{$value.real_url}"><span class="info">{$value.title}</span></a> |
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html index 05d13556..5c073da6 100644 --- a/tpl/default/pluginsadmin.html +++ b/tpl/default/pluginsadmin.html | |||
@@ -117,7 +117,7 @@ | |||
117 | 117 | ||
118 | <div class="center more"> | 118 | <div class="center more"> |
119 | {"More plugins available"|t} | 119 | {"More plugins available"|t} |
120 | <a href="doc/html/Community-&-Related-software/#third-party-plugins">{"in the documentation"|t}</a>. | 120 | <a href="{$root_path}/doc/html/Community-&-Related-software/#third-party-plugins">{"in the documentation"|t}</a>. |
121 | </div> | 121 | </div> |
122 | <div class="center"> | 122 | <div class="center"> |
123 | <input type="submit" value="{'Save'|t}" name="save"> | 123 | <input type="submit" value="{'Save'|t}" name="save"> |
diff --git a/tpl/default/server.html b/tpl/default/server.html new file mode 100644 index 00000000..de1c8b53 --- /dev/null +++ b/tpl/default/server.html | |||
@@ -0,0 +1,129 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html{if="$language !== 'auto'"} lang="{$language}"{/if}> | ||
3 | <head> | ||
4 | {include="includes"} | ||
5 | </head> | ||
6 | <body> | ||
7 | {include="page.header"} | ||
8 | |||
9 | <div class="pure-g"> | ||
10 | <div class="pure-u-lg-1-4 pure-u-1-24"></div> | ||
11 | <div class="pure-u-lg-1-2 pure-u-22-24 page-form server-tables-page"> | ||
12 | <h2 class="window-title">{'Server administration'|t}</h2> | ||
13 | |||
14 | <h3 class="window-subtitle">{'General'|t}</h3> | ||
15 | |||
16 | <div class="pure-g server-row"> | ||
17 | <div class="pure-u-lg-1-2 pure-u-1 server-label"> | ||
18 | <p>{'Index URL'|t}</p> | ||
19 | </div> | ||
20 | <div class="pure-u-lg-1-2 pure-u-1"> | ||
21 | <p><a href="{$index_url}" title="{$pagetitle}">{$index_url}</a></p> | ||
22 | </div> | ||
23 | </div> | ||
24 | <div class="pure-g server-row"> | ||
25 | <div class="pure-u-lg-1-2 pure-u-1 server-label"> | ||
26 | <p>{'Base path'|t}</p> | ||
27 | </div> | ||
28 | <div class="pure-u-lg-1-2 pure-u-1"> | ||
29 | <p>{$base_path}</p> | ||
30 | </div> | ||
31 | </div> | ||
32 | <div class="pure-g server-row"> | ||
33 | <div class="pure-u-lg-1-2 pure-u-1 server-label"> | ||
34 | <p>{'Client IP'|t}</p> | ||
35 | </div> | ||
36 | <div class="pure-u-lg-1-2 pure-u-1"> | ||
37 | <p>{$client_ip}</p> | ||
38 | </div> | ||
39 | </div> | ||
40 | <div class="pure-g server-row"> | ||
41 | <div class="pure-u-lg-1-2 pure-u-1 server-label"> | ||
42 | <p>{'Trusted reverse proxies'|t}</p> | ||
43 | </div> | ||
44 | <div class="pure-u-lg-1-2 pure-u-1"> | ||
45 | {if="count($trusted_proxies) > 0"} | ||
46 | <p> | ||
47 | {loop="$trusted_proxies"} | ||
48 | {$value}<br> | ||
49 | {/loop} | ||
50 | </p> | ||
51 | {else} | ||
52 | <p>{'N/A'|t}</p> | ||
53 | {/if} | ||
54 | </div> | ||
55 | </div> | ||
56 | |||
57 | {include="server.requirements"} | ||
58 | |||
59 | <h3 class="window-subtitle">Version</h3> | ||
60 | |||
61 | <div class="pure-g server-row"> | ||
62 | <div class="pure-u-lg-1-2 pure-u-1 server-label"> | ||
63 | <p>Current version</p> | ||
64 | </div> | ||
65 | <div class="pure-u-lg-1-2 pure-u-1"> | ||
66 | <p>{$current_version}</p> | ||
67 | </div> | ||
68 | </div> | ||
69 | |||
70 | <div class="pure-g server-row"> | ||
71 | <div class="pure-u-lg-1-2 pure-u-1 server-label"> | ||
72 | <p>Latest release</p> | ||
73 | </div> | ||
74 | <div class="pure-u-lg-1-2 pure-u-1"> | ||
75 | <p> | ||
76 | <a href="{$release_url}" title="{'Visit releases page on Github'|t}"> | ||
77 | {$latest_version} | ||
78 | </a> | ||
79 | </p> | ||
80 | </div> | ||
81 | </div> | ||
82 | |||
83 | <h3 class="window-subtitle">Thumbnails</h3> | ||
84 | |||
85 | <div class="pure-g server-row"> | ||
86 | <div class="pure-u-lg-1-2 pure-u-1 server-label"> | ||
87 | <p>Thumbnails status</p> | ||
88 | </div> | ||
89 | <div class="pure-u-lg-1-2 pure-u-1"> | ||
90 | <p> | ||
91 | {if="$thumbnails_mode==='all'"} | ||
92 | {'All'|t} | ||
93 | {elseif="$thumbnails_mode==='common'"} | ||
94 | {'Only common media hosts'|t} | ||
95 | {else} | ||
96 | {'None'|t} | ||
97 | {/if} | ||
98 | </p> | ||
99 | </div> | ||
100 | </div> | ||
101 | |||
102 | {if="$thumbnails_mode!=='none'"} | ||
103 | <div class="center tools-item"> | ||
104 | <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}"> | ||
105 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span> | ||
106 | </a> | ||
107 | </div> | ||
108 | {/if} | ||
109 | |||
110 | <h3 class="window-subtitle">Cache</h3> | ||
111 | |||
112 | <div class="center tools-item"> | ||
113 | <a href="{$base_path}/admin/clear-cache?type=main"> | ||
114 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear main cache</span> | ||
115 | </a> | ||
116 | </div> | ||
117 | |||
118 | <div class="center tools-item"> | ||
119 | <a href="{$base_path}/admin/clear-cache?type=thumbnails"> | ||
120 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear thumbnails cache</span> | ||
121 | </a> | ||
122 | </div> | ||
123 | </div> | ||
124 | </div> | ||
125 | |||
126 | {include="page.footer"} | ||
127 | |||
128 | </body> | ||
129 | </html> | ||
diff --git a/tpl/default/server.requirements.html b/tpl/default/server.requirements.html new file mode 100644 index 00000000..85def9b7 --- /dev/null +++ b/tpl/default/server.requirements.html | |||
@@ -0,0 +1,68 @@ | |||
1 | <div class="server-tables"> | ||
2 | <h3 class="window-subtitle">{'Permissions'|t}</h3> | ||
3 | |||
4 | {if="count($permissions) > 0"} | ||
5 | <p class="center"> | ||
6 | <i class="fa fa-close fa-color-red" aria-hidden="true"></i> | ||
7 | {'There are permissions that need to be fixed.'|t} | ||
8 | </p> | ||
9 | |||
10 | <p> | ||
11 | {loop="$permissions"} | ||
12 | <div class="center">{$value}</div> | ||
13 | {/loop} | ||
14 | </p> | ||
15 | {else} | ||
16 | <p class="center"> | ||
17 | <i class="fa fa-check fa-color-green" aria-hidden="true"></i> | ||
18 | {'All read/write permissions are properly set.'|t} | ||
19 | </p> | ||
20 | {/if} | ||
21 | |||
22 | <h3 class="window-subtitle">PHP</h3> | ||
23 | |||
24 | <p class="center"> | ||
25 | <strong>{'Running PHP'|t} {$php_version}</strong> | ||
26 | {if="$php_has_reached_eol"} | ||
27 | <i class="fa fa-circle fa-color-orange" aria-label="hidden"></i><br> | ||
28 | {'End of life: '|t} {$php_eol} | ||
29 | {else} | ||
30 | <i class="fa fa-circle fa-color-green" aria-label="hidden"></i><br> | ||
31 | {/if} | ||
32 | </p> | ||
33 | |||
34 | <table class="center"> | ||
35 | <thead> | ||
36 | <tr> | ||
37 | <th>{'Extension'|t}</th> | ||
38 | <th>{'Usage'|t}</th> | ||
39 | <th>{'Status'|t}</th> | ||
40 | <th>{'Loaded'|t}</th> | ||
41 | </tr> | ||
42 | </thead> | ||
43 | <tbody> | ||
44 | {loop="$php_extensions"} | ||
45 | <tr> | ||
46 | <td>{$value.name}</td> | ||
47 | <td>{$value.desc}</td> | ||
48 | <td>{$value.required ? t('Required') : t('Optional')}</td> | ||
49 | <td> | ||
50 | {if="$value.loaded"} | ||
51 | {$classLoaded="fa-color-green"} | ||
52 | {$strLoaded=t('Loaded')} | ||
53 | {else} | ||
54 | {$strLoaded=t('Not loaded')} | ||
55 | {if="$value.required"} | ||
56 | {$classLoaded="fa-color-red"} | ||
57 | {else} | ||
58 | {$classLoaded="fa-color-orange"} | ||
59 | {/if} | ||
60 | {/if} | ||
61 | |||
62 | <i class="fa fa-circle {$classLoaded}" aria-label="{$strLoaded}" title="{$strLoaded}"></i> | ||
63 | </td> | ||
64 | </tr> | ||
65 | {/loop} | ||
66 | </tbody> | ||
67 | </table> | ||
68 | </div> | ||
diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html index c067e1d4..01b50b02 100644 --- a/tpl/default/tag.cloud.html +++ b/tpl/default/tag.cloud.html | |||
@@ -48,7 +48,7 @@ | |||
48 | 48 | ||
49 | <div id="cloudtag" class="cloudtag-container"> | 49 | <div id="cloudtag" class="cloudtag-container"> |
50 | {loop="tags"} | 50 | {loop="tags"} |
51 | <a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a | 51 | <a href="{$base_path}/?searchtags={$tags_url.$key1}{$tags_separator|urlencode}{$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a |
52 | ><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a> | 52 | ><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a> |
53 | {loop="$value.tag_plugin"} | 53 | {loop="$value.tag_plugin"} |
54 | {$value} | 54 | {$value} |
diff --git a/tpl/default/tools.html b/tpl/default/tools.html index 2cb08e38..2df73598 100644 --- a/tpl/default/tools.html +++ b/tpl/default/tools.html | |||
@@ -20,6 +20,12 @@ | |||
20 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span> | 20 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span> |
21 | </a> | 21 | </a> |
22 | </div> | 22 | </div> |
23 | <div class="tools-item"> | ||
24 | <a href="{$base_path}/admin/server" | ||
25 | title="{'Check instance\'s server configuration'|t}"> | ||
26 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Server administration'|t}</span> | ||
27 | </a> | ||
28 | </div> | ||
23 | {if="!$openshaarli"} | 29 | {if="!$openshaarli"} |
24 | <div class="tools-item"> | 30 | <div class="tools-item"> |
25 | <a href="{$base_path}/admin/password" title="{'Change your password'|t}"> | 31 | <a href="{$base_path}/admin/password" title="{'Change your password'|t}"> |
@@ -45,14 +51,6 @@ | |||
45 | </a> | 51 | </a> |
46 | </div> | 52 | </div> |
47 | 53 | ||
48 | {if="$thumbnails_enabled"} | ||
49 | <div class="tools-item"> | ||
50 | <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}"> | ||
51 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span> | ||
52 | </a> | ||
53 | </div> | ||
54 | {/if} | ||
55 | |||
56 | {loop="$tools_plugin"} | 54 | {loop="$tools_plugin"} |
57 | <div class="tools-item"> | 55 | <div class="tools-item"> |
58 | {$value} | 56 | {$value} |
diff --git a/tpl/vintage/daily.html b/tpl/vintage/daily.html index 74f6cdc7..28ba9f90 100644 --- a/tpl/vintage/daily.html +++ b/tpl/vintage/daily.html | |||
@@ -14,9 +14,9 @@ | |||
14 | 14 | ||
15 | <div class="dailyAbout"> | 15 | <div class="dailyAbout"> |
16 | All links of one day<br>in a single page.<br> | 16 | All links of one day<br>in a single page.<br> |
17 | {if="$previousday"} <a href="{$base_path}/daily&day={$previousday}"><b><</b>Previous day</a>{else}<b><</b>Previous day{/if} | 17 | {if="$previousday"} <a href="{$base_path}/daily?day={$previousday}"><b><</b>Previous day</a>{else}<b><</b>Previous day{/if} |
18 | - | 18 | - |
19 | {if="$nextday"}<a href="{$base_path}/daily&day={$nextday}">Next day<b>></b></a>{else}Next day<b>></b>{/if} | 19 | {if="$nextday"}<a href="{$base_path}/daily?day={$nextday}">Next day<b>></b></a>{else}Next day<b>></b>{/if} |
20 | <br> | 20 | <br> |
21 | 21 | ||
22 | {loop="$daily_about_plugin"} | 22 | {loop="$daily_about_plugin"} |
@@ -52,13 +52,13 @@ | |||
52 | {$link=$value} | 52 | {$link=$value} |
53 | <div class="dailyEntry"> | 53 | <div class="dailyEntry"> |
54 | <div class="dailyEntryPermalink"> | 54 | <div class="dailyEntryPermalink"> |
55 | <a href="{$base_path}/?{$value.shorturl}"> | 55 | <a href="{$base_path}/shaare/{$value.shorturl}"> |
56 | <img src="{$asset_path}/img/squiggle.png#" width="25" height="26" title="permalink" alt="permalink"> | 56 | <img src="{$asset_path}/img/squiggle.png#" width="25" height="26" title="permalink" alt="permalink"> |
57 | </a> | 57 | </a> |
58 | </div> | 58 | </div> |
59 | {if="!$hide_timestamps || $is_logged_in"} | 59 | {if="!$hide_timestamps || $is_logged_in"} |
60 | <div class="dailyEntryLinkdate"> | 60 | <div class="dailyEntryLinkdate"> |
61 | <a href="{$base_path}/?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a> | 61 | <a href="{$base_path}/shaare/{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a> |
62 | </div> | 62 | </div> |
63 | {/if} | 63 | {/if} |
64 | {if="$link.tags"} | 64 | {if="$link.tags"} |
diff --git a/tpl/vintage/editlink.html b/tpl/vintage/editlink.html index eb8807b5..343418bc 100644 --- a/tpl/vintage/editlink.html +++ b/tpl/vintage/editlink.html | |||
@@ -6,6 +6,7 @@ | |||
6 | {if="$link.title==''"}onload="document.linkform.lf_title.focus();" | 6 | {if="$link.title==''"}onload="document.linkform.lf_title.focus();" |
7 | {elseif="$link.description==''"}onload="document.linkform.lf_description.focus();" | 7 | {elseif="$link.description==''"}onload="document.linkform.lf_description.focus();" |
8 | {else}onload="document.linkform.lf_tags.focus();"{/if} > | 8 | {else}onload="document.linkform.lf_tags.focus();"{/if} > |
9 | {$asyncLoadClass=$link_is_new && $async_metadata && empty($link.title) ? 'loading-input' : ''} | ||
9 | <div id="pageheader"> | 10 | <div id="pageheader"> |
10 | {include="page.header"} | 11 | {include="page.header"} |
11 | <div id="shaarli_title"><a href="{$titleLink}">{$shaarlititle}</a></div> | 12 | <div id="shaarli_title"><a href="{$titleLink}">{$shaarlititle}</a></div> |
@@ -14,12 +15,29 @@ | |||
14 | {if="isset($link.id)"} | 15 | {if="isset($link.id)"} |
15 | <input type="hidden" name="lf_id" value="{$link.id}"> | 16 | <input type="hidden" name="lf_id" value="{$link.id}"> |
16 | {/if} | 17 | {/if} |
17 | <label for="lf_url"><i>URL</i></label><br><input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input"><br> | 18 | <label for="lf_url"><i>URL</i></label><br><input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input"> |
18 | <label for="lf_title"><i>Title</i></label><br><input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input"><br> | 19 | <label for="lf_title"><i>Title</i></label> |
19 | <label for="lf_description"><i>Description</i></label><br><textarea name="lf_description" id="lf_description" rows="4" cols="25">{$link.description}</textarea><br> | 20 | <div class="{$asyncLoadClass}"> |
20 | <label for="lf_tags"><i>Tags</i></label><br> | 21 | <input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input"> |
21 | <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input" | 22 | <div class="icon-container"> |
22 | data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" ><br> | 23 | <i class="loader"></i> |
24 | </div> | ||
25 | </div> | ||
26 | <label for="lf_description"><i>Description</i></label> | ||
27 | <div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}"> | ||
28 | <textarea name="lf_description" id="lf_description" rows="4" cols="25">{$link.description}</textarea> | ||
29 | <div class="icon-container"> | ||
30 | <i class="loader"></i> | ||
31 | </div> | ||
32 | </div> | ||
33 | <label for="lf_tags"><i>Tags</i></label> | ||
34 | <div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}"> | ||
35 | <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input" | ||
36 | data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" > | ||
37 | <div class="icon-container"> | ||
38 | <i class="loader"></i> | ||
39 | </div> | ||
40 | </div> | ||
23 | 41 | ||
24 | {if="$formatter==='markdown'"} | 42 | {if="$formatter==='markdown'"} |
25 | <div class="md_help"> | 43 | <div class="md_help"> |
@@ -56,5 +74,5 @@ | |||
56 | </div> | 74 | </div> |
57 | </div> | 75 | </div> |
58 | {include="page.footer"} | 76 | {include="page.footer"} |
59 | </body> | 77 | {if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}</body> |
60 | </html> | 78 | </html> |
diff --git a/tpl/vintage/includes.html b/tpl/vintage/includes.html index eac05701..2ce9da42 100644 --- a/tpl/vintage/includes.html +++ b/tpl/vintage/includes.html | |||
@@ -5,13 +5,13 @@ | |||
5 | <meta name="referrer" content="same-origin"> | 5 | <meta name="referrer" content="same-origin"> |
6 | <link rel="alternate" type="application/rss+xml" href="{$feedurl}feed/rss?{$searchcrits}#" title="RSS Feed" /> | 6 | <link rel="alternate" type="application/rss+xml" href="{$feedurl}feed/rss?{$searchcrits}#" title="RSS Feed" /> |
7 | <link rel="alternate" type="application/atom+xml" href="{$feedurl}feed/atom?{$searchcrits}#" title="ATOM Feed" /> | 7 | <link rel="alternate" type="application/atom+xml" href="{$feedurl}feed/atom?{$searchcrits}#" title="ATOM Feed" /> |
8 | <link href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" /> | 8 | <link href="{$asset_path}/img/favicon.ico#" rel="shortcut icon" type="image/x-icon" /> |
9 | <link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css#" /> | 9 | <link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css#" /> |
10 | {if="$formatter==='markdown'"} | 10 | {if="$formatter==='markdown'"} |
11 | <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" /> | 11 | <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" /> |
12 | {/if} | 12 | {/if} |
13 | {loop="$plugins_includes.css_files"} | 13 | {loop="$plugins_includes.css_files"} |
14 | <link type="text/css" rel="stylesheet" href="{$base_path}/{$value}#"/> | 14 | <link type="text/css" rel="stylesheet" href="{$root_path}/{$value}#"/> |
15 | {/loop} | 15 | {/loop} |
16 | {if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />{/if} | 16 | {if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />{/if} |
17 | <link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" | 17 | <link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" |
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html index 00896eb5..ff0dd40c 100644 --- a/tpl/vintage/linklist.html +++ b/tpl/vintage/linklist.html | |||
@@ -61,7 +61,7 @@ | |||
61 | for <em>{$search_term}</em> | 61 | for <em>{$search_term}</em> |
62 | {/if} | 62 | {/if} |
63 | {if="!empty($search_tags)"} | 63 | {if="!empty($search_tags)"} |
64 | {$exploded_tags=explode(' ', $search_tags)} | 64 | {$exploded_tags=tags_str2array($search_tags, $tags_separator)} |
65 | tagged | 65 | tagged |
66 | {loop="$exploded_tags"} | 66 | {loop="$exploded_tags"} |
67 | <span class="linktag" title="Remove tag"> | 67 | <span class="linktag" title="Remove tag"> |
@@ -77,10 +77,10 @@ | |||
77 | {/if} | 77 | {/if} |
78 | <ul> | 78 | <ul> |
79 | {loop="$links"} | 79 | {loop="$links"} |
80 | <li{if="$value.class"} class="{$value.class}"{/if}> | 80 | <li{if="$value.class"} class="{$value.class}"{/if} data-id="{$value.id}"> |
81 | <a id="{$value.shorturl}"></a> | 81 | <a id="{$value.shorturl}"></a> |
82 | {if="$thumbnails_enabled && !empty($value.thumbnail)"} | 82 | {if="$thumbnails_enabled && $value.thumbnail !== false"} |
83 | <div class="thumbnail"> | 83 | <div class="thumbnail" {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if}> |
84 | <a href="{$value.real_url}"> | 84 | <a href="{$value.real_url}"> |
85 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} | 85 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} |
86 | <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy" | 86 | <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy" |
@@ -153,6 +153,7 @@ | |||
153 | 153 | ||
154 | {include="page.footer"} | 154 | {include="page.footer"} |
155 | <script src="{$asset_path}/js/thumbnails.min.js#"></script> | 155 | <script src="{$asset_path}/js/thumbnails.min.js#"></script> |
156 | {if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if} | ||
156 | 157 | ||
157 | </body> | 158 | </body> |
158 | </html> | 159 | </html> |
diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html index 0fe4c736..be709aeb 100644 --- a/tpl/vintage/page.footer.html +++ b/tpl/vintage/page.footer.html | |||
@@ -23,8 +23,6 @@ | |||
23 | </div> | 23 | </div> |
24 | {/if} | 24 | {/if} |
25 | 25 | ||
26 | <script src="{$asset_path}/js/shaarli.min.js#"></script> | ||
27 | |||
28 | {if="$is_logged_in"} | 26 | {if="$is_logged_in"} |
29 | <script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> | 27 | <script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> |
30 | {/if} | 28 | {/if} |
@@ -34,3 +32,7 @@ | |||
34 | {/loop} | 32 | {/loop} |
35 | 33 | ||
36 | <input type="hidden" name="js_base_path" value="{$base_path}" /> | 34 | <input type="hidden" name="js_base_path" value="{$base_path}" /> |
35 | <input type="hidden" name="token" value="{$token}" id="token" /> | ||
36 | <input type="hidden" name="tags_separator" value="{$tags_separator}" id="tags_separator" /> | ||
37 | |||
38 | <script src="{$asset_path}/js/shaarli.min.js#"></script> | ||
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html index 0a33523b..64d7f656 100644 --- a/tpl/vintage/page.header.html +++ b/tpl/vintage/page.header.html | |||
@@ -54,6 +54,30 @@ | |||
54 | </ul> | 54 | </ul> |
55 | {/if} | 55 | {/if} |
56 | 56 | ||
57 | {if="!empty($global_errors)"} | ||
58 | <ul class="errors"> | ||
59 | {loop="$global_errors"} | ||
60 | <li>{$value}</li> | ||
61 | {/loop} | ||
62 | </ul> | ||
63 | {/if} | ||
64 | |||
65 | {if="!empty($global_warnings)"} | ||
66 | <ul class="warnings"> | ||
67 | {loop="$global_warnings"} | ||
68 | <li>{$value}</li> | ||
69 | {/loop} | ||
70 | </ul> | ||
71 | {/if} | ||
72 | |||
73 | {if="!empty($global_successes)"} | ||
74 | <ul class="successes"> | ||
75 | {loop="$global_successes"} | ||
76 | <li>{$value}</li> | ||
77 | {/loop} | ||
78 | </ul> | ||
79 | {/if} | ||
80 | |||
57 | <div class="clear"></div> | 81 | <div class="clear"></div> |
58 | 82 | ||
59 | 83 | ||
diff --git a/webpack.config.js b/webpack.config.js index a73758cc..2c316d32 100644 --- a/webpack.config.js +++ b/webpack.config.js | |||
@@ -18,8 +18,10 @@ module.exports = [ | |||
18 | { | 18 | { |
19 | mode: 'production', | 19 | mode: 'production', |
20 | entry: { | 20 | entry: { |
21 | shaare_batch: './assets/common/js/shaare-batch.js', | ||
21 | thumbnails: './assets/common/js/thumbnails.js', | 22 | thumbnails: './assets/common/js/thumbnails.js', |
22 | thumbnails_update: './assets/common/js/thumbnails-update.js', | 23 | thumbnails_update: './assets/common/js/thumbnails-update.js', |
24 | metadata: './assets/common/js/metadata.js', | ||
23 | pluginsadmin: './assets/default/js/plugins-admin.js', | 25 | pluginsadmin: './assets/default/js/plugins-admin.js', |
24 | shaarli: [ | 26 | shaarli: [ |
25 | './assets/default/js/base.js', | 27 | './assets/default/js/base.js', |
@@ -99,6 +101,7 @@ module.exports = [ | |||
99 | ].concat(glob.sync('./assets/vintage/img/*')), | 101 | ].concat(glob.sync('./assets/vintage/img/*')), |
100 | markdown: './assets/common/css/markdown.css', | 102 | markdown: './assets/common/css/markdown.css', |
101 | thumbnails: './assets/common/js/thumbnails.js', | 103 | thumbnails: './assets/common/js/thumbnails.js', |
104 | metadata: './assets/common/js/metadata.js', | ||
102 | thumbnails_update: './assets/common/js/thumbnails-update.js', | 105 | thumbnails_update: './assets/common/js/thumbnails-update.js', |
103 | }, | 106 | }, |
104 | output: { | 107 | output: { |
@@ -139,7 +142,8 @@ module.exports = [ | |||
139 | loader: 'file-loader', | 142 | loader: 'file-loader', |
140 | options: { | 143 | options: { |
141 | name: '../img/[name].[ext]', | 144 | name: '../img/[name].[ext]', |
142 | publicPath: '', | 145 | // do not add a publicPath here because it's already handled by CSS's publicPath |
146 | publicPath: '../vintage', | ||
143 | } | 147 | } |
144 | } | 148 | } |
145 | ], | 149 | ], |
@@ -2912,6 +2912,11 @@ hash.js@^1.0.0, hash.js@^1.0.3: | |||
2912 | inherits "^2.0.3" | 2912 | inherits "^2.0.3" |
2913 | minimalistic-assert "^1.0.1" | 2913 | minimalistic-assert "^1.0.1" |
2914 | 2914 | ||
2915 | he@^1.2.0: | ||
2916 | version "1.2.0" | ||
2917 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" | ||
2918 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== | ||
2919 | |||
2915 | hmac-drbg@^1.0.0: | 2920 | hmac-drbg@^1.0.0: |
2916 | version "1.0.1" | 2921 | version "1.0.1" |
2917 | resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" | 2922 | resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" |