diff options
author | yude <yudesleepy@gmail.com> | 2021-01-04 18:51:10 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-04 18:51:10 +0900 |
commit | e6754f2154a79abd8e5e64bd923f6984aa9ad44b (patch) | |
tree | f074119530bb59ef155938ea367f719f1e4b70f1 | |
parent | 5256b4287021342a9f8868967b2a77e481314331 (diff) | |
parent | ed4ee8f0297941ac83300389b7de6a293312d20e (diff) | |
download | Shaarli-e6754f2154a79abd8e5e64bd923f6984aa9ad44b.tar.gz Shaarli-e6754f2154a79abd8e5e64bd923f6984aa9ad44b.tar.zst Shaarli-e6754f2154a79abd8e5e64bd923f6984aa9ad44b.zip |
Merge pull request #2 from shaarli/master
Merge fork source
208 files changed, 8969 insertions, 1991 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 | ||
@@ -26,7 +26,7 @@ RUN cd shaarli \ | |||
26 | 26 | ||
27 | # Stage 4: | 27 | # Stage 4: |
28 | # - Shaarli image | 28 | # - Shaarli image |
29 | FROM alpine:3.8 | 29 | FROM alpine:3.12 |
30 | LABEL maintainer="Shaarli Community" | 30 | LABEL maintainer="Shaarli Community" |
31 | 31 | ||
32 | RUN apk --update --no-cache add \ | 32 | RUN apk --update --no-cache add \ |
@@ -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 | ||
diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 5bbf6680..471f2397 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf | |||
@@ -1,7 +1,7 @@ | |||
1 | # Stage 1: | 1 | # Stage 1: |
2 | # - Copy Shaarli sources | 2 | # - Copy Shaarli sources |
3 | # - Build documentation | 3 | # - Build documentation |
4 | FROM arm32v6/alpine:3.8 as docs | 4 | FROM arm32v6/alpine:3.10 as docs |
5 | ADD . /usr/src/app/shaarli | 5 | ADD . /usr/src/app/shaarli |
6 | RUN apk --update --no-cache add py2-pip \ | 6 | RUN apk --update --no-cache add py2-pip \ |
7 | && cd /usr/src/app/shaarli \ | 7 | && cd /usr/src/app/shaarli \ |
@@ -10,7 +10,7 @@ RUN apk --update --no-cache add py2-pip \ | |||
10 | 10 | ||
11 | # Stage 2: | 11 | # Stage 2: |
12 | # - Resolve PHP dependencies with Composer | 12 | # - Resolve PHP dependencies with Composer |
13 | FROM arm32v6/alpine:3.8 as composer | 13 | FROM arm32v6/alpine:3.10 as composer |
14 | COPY --from=docs /usr/src/app/shaarli /app/shaarli | 14 | COPY --from=docs /usr/src/app/shaarli /app/shaarli |
15 | RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \ | 15 | RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \ |
16 | && cd /app/shaarli \ | 16 | && cd /app/shaarli \ |
@@ -18,7 +18,7 @@ RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer | |||
18 | 18 | ||
19 | # Stage 3: | 19 | # Stage 3: |
20 | # - Frontend dependencies | 20 | # - Frontend dependencies |
21 | FROM arm32v6/alpine:3.8 as node | 21 | FROM arm32v6/alpine:3.10 as node |
22 | COPY --from=composer /app/shaarli /shaarli | 22 | COPY --from=composer /app/shaarli /shaarli |
23 | RUN apk --update --no-cache add yarn nodejs-current python2 build-base \ | 23 | RUN apk --update --no-cache add yarn nodejs-current python2 build-base \ |
24 | && cd /shaarli \ | 24 | && cd /shaarli \ |
@@ -28,7 +28,7 @@ RUN apk --update --no-cache add yarn nodejs-current python2 build-base \ | |||
28 | 28 | ||
29 | # Stage 4: | 29 | # Stage 4: |
30 | # - Shaarli image | 30 | # - Shaarli image |
31 | FROM arm32v6/alpine:3.8 | 31 | FROM arm32v6/alpine:3.10 |
32 | LABEL maintainer="Shaarli Community" | 32 | LABEL maintainer="Shaarli Community" |
33 | 33 | ||
34 | RUN apk --update --no-cache add \ | 34 | RUN apk --update --no-cache add \ |
@@ -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: |
@@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._ | |||
9 | [![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) | 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.12.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) | 12 | [![](https://img.shields.io/badge/latest-v0.12.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1) |
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.12.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) |
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..7177db2c 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); |
@@ -183,6 +186,7 @@ class Languages | |||
183 | 'en' => t('English'), | 186 | 'en' => t('English'), |
184 | 'fr' => t('French'), | 187 | 'fr' => t('French'), |
185 | 'jp' => t('Japanese'), | 188 | 'jp' => t('Japanese'), |
189 | 'ru' => t('Russian'), | ||
186 | ]; | 190 | ]; |
187 | } | 191 | } |
188 | } | 192 | } |
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 adc8b266..9fb88358 100644 --- a/application/api/ApiMiddleware.php +++ b/application/api/ApiMiddleware.php | |||
@@ -1,4 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | namespace Shaarli\Api; | 3 | namespace Shaarli\Api; |
3 | 4 | ||
4 | use malkusch\lock\mutex\FlockMutex; | 5 | use malkusch\lock\mutex\FlockMutex; |
@@ -108,7 +109,8 @@ class ApiMiddleware | |||
108 | */ | 109 | */ |
109 | protected function checkToken($request) | 110 | protected function checkToken($request) |
110 | { | 111 | { |
111 | if (!$request->hasHeader('Authorization') | 112 | if ( |
113 | !$request->hasHeader('Authorization') | ||
112 | && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION']) | 114 | && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION']) |
113 | ) { | 115 | ) { |
114 | throw new ApiAuthorizationException('JWT token not provided'); | 116 | throw new ApiAuthorizationException('JWT token not provided'); |
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index eb1ca9bc..9228bb2d 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,13 +91,17 @@ 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|null $input Request Link. | 94 | * @param array|null $input Request Link. |
93 | * @param bool $defaultPrivate Setting defined if a bookmark is private by default. | 95 | * @param bool $defaultPrivate Setting defined if a bookmark is private by default. |
96 | * @param string $tagsSeparator Tags separator loaded from the config file. | ||
94 | * | 97 | * |
95 | * @return Bookmark instance. | 98 | * @return Bookmark instance. |
96 | */ | 99 | */ |
97 | public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark | 100 | public static function buildBookmarkFromRequest( |
98 | { | 101 | ?array $input, |
102 | bool $defaultPrivate, | ||
103 | string $tagsSeparator | ||
104 | ): Bookmark { | ||
99 | $bookmark = new Bookmark(); | 105 | $bookmark = new Bookmark(); |
100 | $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; | 106 | $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; |
101 | if (isset($input['private'])) { | 107 | if (isset($input['private'])) { |
@@ -107,6 +113,15 @@ class ApiUtils | |||
107 | $bookmark->setTitle(! empty($input['title']) ? $input['title'] : ''); | 113 | $bookmark->setTitle(! empty($input['title']) ? $input['title'] : ''); |
108 | $bookmark->setUrl($url); | 114 | $bookmark->setUrl($url); |
109 | $bookmark->setDescription(! empty($input['description']) ? $input['description'] : ''); | 115 | $bookmark->setDescription(! empty($input['description']) ? $input['description'] : ''); |
116 | |||
117 | // Be permissive with provided tags format | ||
118 | if (is_string($input['tags'] ?? null)) { | ||
119 | $input['tags'] = tags_str2array($input['tags'], $tagsSeparator); | ||
120 | } | ||
121 | if (is_array($input['tags'] ?? null) && count($input['tags']) === 1 && is_string($input['tags'][0])) { | ||
122 | $input['tags'] = tags_str2array($input['tags'][0], $tagsSeparator); | ||
123 | } | ||
124 | |||
110 | $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); | 125 | $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); |
111 | $bookmark->setPrivate($private); | 126 | $bookmark->setPrivate($private); |
112 | 127 | ||
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 73a1b84e..b83b2260 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php | |||
@@ -117,9 +117,14 @@ class Links extends ApiController | |||
117 | public function postLink($request, $response) | 117 | public function postLink($request, $response) |
118 | { | 118 | { |
119 | $data = (array) ($request->getParsedBody() ?? []); | 119 | $data = (array) ($request->getParsedBody() ?? []); |
120 | $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); | 120 | $bookmark = ApiUtils::buildBookmarkFromRequest( |
121 | $data, | ||
122 | $this->conf->get('privacy.default_private_links'), | ||
123 | $this->conf->get('general.tags_separator', ' ') | ||
124 | ); | ||
121 | // duplicate by URL, return 409 Conflict | 125 | // duplicate by URL, return 409 Conflict |
122 | if (! empty($bookmark->getUrl()) | 126 | if ( |
127 | ! empty($bookmark->getUrl()) | ||
123 | && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) | 128 | && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) |
124 | ) { | 129 | ) { |
125 | return $response->withJson( | 130 | return $response->withJson( |
@@ -131,7 +136,7 @@ class Links extends ApiController | |||
131 | 136 | ||
132 | $this->bookmarkService->add($bookmark); | 137 | $this->bookmarkService->add($bookmark); |
133 | $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment'])); | 138 | $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment'])); |
134 | $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]); | 139 | $redirect = $this->ci->router->pathFor('getLink', ['id' => $bookmark->getId()]); |
135 | return $response->withAddedHeader('Location', $redirect) | 140 | return $response->withAddedHeader('Location', $redirect) |
136 | ->withJson($out, 201, $this->jsonStyle); | 141 | ->withJson($out, 201, $this->jsonStyle); |
137 | } | 142 | } |
@@ -157,9 +162,14 @@ class Links extends ApiController | |||
157 | $index = index_url($this->ci['environment']); | 162 | $index = index_url($this->ci['environment']); |
158 | $data = $request->getParsedBody(); | 163 | $data = $request->getParsedBody(); |
159 | 164 | ||
160 | $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); | 165 | $requestBookmark = ApiUtils::buildBookmarkFromRequest( |
166 | $data, | ||
167 | $this->conf->get('privacy.default_private_links'), | ||
168 | $this->conf->get('general.tags_separator', ' ') | ||
169 | ); | ||
161 | // duplicate URL on a different link, return 409 Conflict | 170 | // duplicate URL on a different link, return 409 Conflict |
162 | if (! empty($requestBookmark->getUrl()) | 171 | if ( |
172 | ! empty($requestBookmark->getUrl()) | ||
163 | && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) | 173 | && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) |
164 | && $dup->getId() != $id | 174 | && $dup->getId() != $id |
165 | ) { | 175 | ) { |
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 ea565d1f..4238ef25 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php | |||
@@ -19,7 +19,7 @@ use Shaarli\Bookmark\Exception\InvalidBookmarkException; | |||
19 | class Bookmark | 19 | class Bookmark |
20 | { | 20 | { |
21 | /** @var string Date format used in string (former ID format) */ | 21 | /** @var string Date format used in string (former ID format) */ |
22 | const LINK_DATE_FORMAT = 'Ymd_His'; | 22 | public const LINK_DATE_FORMAT = 'Ymd_His'; |
23 | 23 | ||
24 | /** @var int Bookmark ID */ | 24 | /** @var int Bookmark ID */ |
25 | protected $id; | 25 | protected $id; |
@@ -60,11 +60,13 @@ class Bookmark | |||
60 | /** | 60 | /** |
61 | * 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. |
62 | * | 62 | * |
63 | * @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. | ||
64 | * | 66 | * |
65 | * @return $this | 67 | * @return $this |
66 | */ | 68 | */ |
67 | public function fromArray(array $data): Bookmark | 69 | public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark |
68 | { | 70 | { |
69 | $this->id = $data['id'] ?? null; | 71 | $this->id = $data['id'] ?? null; |
70 | $this->shortUrl = $data['shorturl'] ?? null; | 72 | $this->shortUrl = $data['shorturl'] ?? null; |
@@ -77,7 +79,7 @@ class Bookmark | |||
77 | if (is_array($data['tags'])) { | 79 | if (is_array($data['tags'])) { |
78 | $this->tags = $data['tags']; | 80 | $this->tags = $data['tags']; |
79 | } else { | 81 | } else { |
80 | $this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY); | 82 | $this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator); |
81 | } | 83 | } |
82 | if (! empty($data['updated'])) { | 84 | if (! empty($data['updated'])) { |
83 | $this->updated = $data['updated']; | 85 | $this->updated = $data['updated']; |
@@ -104,7 +106,8 @@ class Bookmark | |||
104 | */ | 106 | */ |
105 | public function validate(): void | 107 | public function validate(): void |
106 | { | 108 | { |
107 | if ($this->id === null | 109 | if ( |
110 | $this->id === null | ||
108 | || ! is_int($this->id) | 111 | || ! is_int($this->id) |
109 | || empty($this->shortUrl) | 112 | || empty($this->shortUrl) |
110 | || empty($this->created) | 113 | || empty($this->created) |
@@ -112,7 +115,7 @@ class Bookmark | |||
112 | throw new InvalidBookmarkException($this); | 115 | throw new InvalidBookmarkException($this); |
113 | } | 116 | } |
114 | if (empty($this->url)) { | 117 | if (empty($this->url)) { |
115 | $this->url = '/shaare/'. $this->shortUrl; | 118 | $this->url = '/shaare/' . $this->shortUrl; |
116 | } | 119 | } |
117 | if (empty($this->title)) { | 120 | if (empty($this->title)) { |
118 | $this->title = $this->url; | 121 | $this->title = $this->url; |
@@ -348,7 +351,12 @@ class Bookmark | |||
348 | */ | 351 | */ |
349 | public function setTags(?array $tags): Bookmark | 352 | public function setTags(?array $tags): Bookmark |
350 | { | 353 | { |
351 | $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 | ); | ||
352 | 360 | ||
353 | return $this; | 361 | return $this; |
354 | } | 362 | } |
@@ -378,6 +386,24 @@ class Bookmark | |||
378 | } | 386 | } |
379 | 387 | ||
380 | /** | 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 | /** | ||
381 | * Get the Sticky. | 407 | * Get the Sticky. |
382 | * | 408 | * |
383 | * @return bool | 409 | * @return bool |
@@ -402,11 +428,13 @@ class Bookmark | |||
402 | } | 428 | } |
403 | 429 | ||
404 | /** | 430 | /** |
405 | * @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 | ||
406 | */ | 434 | */ |
407 | public function getTagsString(): string | 435 | public function getTagsString(string $separator = ' '): string |
408 | { | 436 | { |
409 | return implode(' ', $this->getTags()); | 437 | return tags_array2str($this->getTags(), $separator); |
410 | } | 438 | } |
411 | 439 | ||
412 | /** | 440 | /** |
@@ -426,19 +454,13 @@ class Bookmark | |||
426 | * - trailing dash in tags will be removed | 454 | * - trailing dash in tags will be removed |
427 | * | 455 | * |
428 | * @param string|null $tags | 456 | * @param string|null $tags |
457 | * @param string $separator Tags separator loaded from the config file. | ||
429 | * | 458 | * |
430 | * @return $this | 459 | * @return $this |
431 | */ | 460 | */ |
432 | public function setTagsString(?string $tags): Bookmark | 461 | public function setTagsString(?string $tags, string $separator = ' '): Bookmark |
433 | { | 462 | { |
434 | // Remove first '-' char in tags. | 463 | $this->setTags(tags_str2array($tags, $separator)); |
435 | $tags = preg_replace('/(^| )\-/', '$1', $tags ?? ''); | ||
436 | // Explode all tags separted by spaces or commas | ||
437 | $tags = preg_split('/[\s,]+/', $tags); | ||
438 | // Remove eventual empty values | ||
439 | $tags = array_values(array_filter($tags)); | ||
440 | |||
441 | $this->tags = $tags; | ||
442 | 464 | ||
443 | return $this; | 465 | return $this; |
444 | } | 466 | } |
@@ -489,7 +511,7 @@ class Bookmark | |||
489 | */ | 511 | */ |
490 | public function renameTag(string $fromTag, string $toTag): void | 512 | public function renameTag(string $fromTag, string $toTag): void |
491 | { | 513 | { |
492 | if (($pos = array_search($fromTag, $this->tags)) !== false) { | 514 | if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) { |
493 | $this->tags[$pos] = trim($toTag); | 515 | $this->tags[$pos] = trim($toTag); |
494 | } | 516 | } |
495 | } | 517 | } |
@@ -501,7 +523,7 @@ class Bookmark | |||
501 | */ | 523 | */ |
502 | public function deleteTag(string $tag): void | 524 | public function deleteTag(string $tag): void |
503 | { | 525 | { |
504 | if (($pos = array_search($tag, $this->tags)) !== false) { | 526 | if (($pos = array_search($tag, $this->tags ?? [])) !== false) { |
505 | unset($this->tags[$pos]); | 527 | unset($this->tags[$pos]); |
506 | $this->tags = array_values($this->tags); | 528 | $this->tags = array_values($this->tags); |
507 | } | 529 | } |
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php index 67bb3b73..b9328116 100644 --- a/application/bookmark/BookmarkArray.php +++ b/application/bookmark/BookmarkArray.php | |||
@@ -72,7 +72,8 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
72 | */ | 72 | */ |
73 | public function offsetSet($offset, $value) | 73 | public function offsetSet($offset, $value) |
74 | { | 74 | { |
75 | if (! $value instanceof Bookmark | 75 | if ( |
76 | ! $value instanceof Bookmark | ||
76 | || $value->getId() === null || empty($value->getUrl()) | 77 | || $value->getId() === null || empty($value->getUrl()) |
77 | || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId()) | 78 | || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId()) |
78 | || $offset !== null && $offset !== $value->getId() | 79 | || $offset !== null && $offset !== $value->getId() |
@@ -222,7 +223,8 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
222 | */ | 223 | */ |
223 | public function getByUrl(string $url): ?Bookmark | 224 | public function getByUrl(string $url): ?Bookmark |
224 | { | 225 | { |
225 | if (! empty($url) | 226 | if ( |
227 | ! empty($url) | ||
226 | && isset($this->urls[$url]) | 228 | && isset($this->urls[$url]) |
227 | && isset($this->bookmarks[$this->urls[$url]]) | 229 | && isset($this->bookmarks[$this->urls[$url]]) |
228 | ) { | 230 | ) { |
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index eb7899bf..6666a251 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php | |||
@@ -69,7 +69,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
69 | } else { | 69 | } else { |
70 | try { | 70 | try { |
71 | $this->bookmarks = $this->bookmarksIO->read(); | 71 | $this->bookmarks = $this->bookmarksIO->read(); |
72 | } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { | 72 | } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) { |
73 | $this->bookmarks = new BookmarkArray(); | 73 | $this->bookmarks = new BookmarkArray(); |
74 | 74 | ||
75 | if ($this->isLoggedIn) { | 75 | if ($this->isLoggedIn) { |
@@ -85,25 +85,29 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
85 | if (! $this->bookmarks instanceof BookmarkArray) { | 85 | if (! $this->bookmarks instanceof BookmarkArray) { |
86 | $this->migrate(); | 86 | $this->migrate(); |
87 | exit( | 87 | exit( |
88 | '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 . |
89 | '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.' |
90 | ); | 90 | ); |
91 | } | 91 | } |
92 | } | 92 | } |
93 | 93 | ||
94 | $this->bookmarkFilter = new BookmarkFilter($this->bookmarks); | 94 | $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf); |
95 | } | 95 | } |
96 | 96 | ||
97 | /** | 97 | /** |
98 | * @inheritDoc | 98 | * @inheritDoc |
99 | */ | 99 | */ |
100 | public function findByHash(string $hash): Bookmark | 100 | public function findByHash(string $hash, string $privateKey = null): Bookmark |
101 | { | 101 | { |
102 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); | 102 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); |
103 | // PHP 7.3 introduced array_key_first() to avoid this hack | 103 | // PHP 7.3 introduced array_key_first() to avoid this hack |
104 | $first = reset($bookmark); | 104 | $first = reset($bookmark); |
105 | if (! $this->isLoggedIn && $first->isPrivate()) { | 105 | if ( |
106 | throw new Exception('Not authorized'); | 106 | !$this->isLoggedIn |
107 | && $first->isPrivate() | ||
108 | && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) | ||
109 | ) { | ||
110 | throw new BookmarkNotFoundException(); | ||
107 | } | 111 | } |
108 | 112 | ||
109 | return $first; | 113 | return $first; |
@@ -162,7 +166,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
162 | } | 166 | } |
163 | 167 | ||
164 | $bookmark = $this->bookmarks[$id]; | 168 | $bookmark = $this->bookmarks[$id]; |
165 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | 169 | if ( |
170 | ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | ||
166 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | 171 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') |
167 | ) { | 172 | ) { |
168 | throw new Exception('Unauthorized'); | 173 | throw new Exception('Unauthorized'); |
@@ -262,7 +267,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
262 | } | 267 | } |
263 | 268 | ||
264 | $bookmark = $this->bookmarks[$id]; | 269 | $bookmark = $this->bookmarks[$id]; |
265 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | 270 | if ( |
271 | ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | ||
266 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | 272 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') |
267 | ) { | 273 | ) { |
268 | return false; | 274 | return false; |
@@ -304,7 +310,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
304 | $caseMapping = []; | 310 | $caseMapping = []; |
305 | foreach ($bookmarks as $bookmark) { | 311 | foreach ($bookmarks as $bookmark) { |
306 | foreach ($bookmark->getTags() as $tag) { | 312 | foreach ($bookmark->getTags() as $tag) { |
307 | if (empty($tag) | 313 | if ( |
314 | empty($tag) | ||
308 | || (! $this->isLoggedIn && startsWith($tag, '.')) | 315 | || (! $this->isLoggedIn && startsWith($tag, '.')) |
309 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG | 316 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG |
310 | || in_array($tag, $filteringTags, true) | 317 | || in_array($tag, $filteringTags, true) |
@@ -340,26 +347,42 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
340 | /** | 347 | /** |
341 | * @inheritDoc | 348 | * @inheritDoc |
342 | */ | 349 | */ |
343 | public function days(): array | 350 | public function findByDate( |
344 | { | 351 | \DateTimeInterface $from, |
345 | $bookmarkDays = []; | 352 | \DateTimeInterface $to, |
346 | foreach ($this->search() as $bookmark) { | 353 | ?\DateTimeInterface &$previous, |
347 | $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 | } | ||
348 | } | 371 | } |
349 | $bookmarkDays = array_keys($bookmarkDays); | ||
350 | sort($bookmarkDays); | ||
351 | 372 | ||
352 | return array_map('strval', $bookmarkDays); | 373 | return $out; |
353 | } | 374 | } |
354 | 375 | ||
355 | /** | 376 | /** |
356 | * @inheritDoc | 377 | * @inheritDoc |
357 | */ | 378 | */ |
358 | public function filterDay(string $request) | 379 | public function getLatest(): ?Bookmark |
359 | { | 380 | { |
360 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; | 381 | foreach ($this->search([], null, false, false, true) as $bookmark) { |
382 | return $bookmark; | ||
383 | } | ||
361 | 384 | ||
362 | return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); | 385 | return null; |
363 | } | 386 | } |
364 | 387 | ||
365 | /** | 388 | /** |
@@ -386,14 +409,14 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
386 | false | 409 | false |
387 | ); | 410 | ); |
388 | $updater = new LegacyUpdater( | 411 | $updater = new LegacyUpdater( |
389 | UpdaterUtils::read_updates_file($this->conf->get('resource.updates')), | 412 | UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')), |
390 | $bookmarkDb, | 413 | $bookmarkDb, |
391 | $this->conf, | 414 | $this->conf, |
392 | true | 415 | true |
393 | ); | 416 | ); |
394 | $newUpdates = $updater->update(); | 417 | $newUpdates = $updater->update(); |
395 | if (! empty($newUpdates)) { | 418 | if (! empty($newUpdates)) { |
396 | UpdaterUtils::write_updates_file( | 419 | UpdaterUtils::writeUpdatesFile( |
397 | $this->conf->get('resource.updates'), | 420 | $this->conf->get('resource.updates'), |
398 | $updater->getDoneUpdates() | 421 | $updater->getDoneUpdates() |
399 | ); | 422 | ); |
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index c79386ea..db83c51c 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php | |||
@@ -6,6 +6,7 @@ namespace Shaarli\Bookmark; | |||
6 | 6 | ||
7 | use Exception; | 7 | use Exception; |
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
9 | use Shaarli\Config\ConfigManager; | ||
9 | 10 | ||
10 | /** | 11 | /** |
11 | * Class LinkFilter. | 12 | * Class LinkFilter. |
@@ -58,12 +59,16 @@ class BookmarkFilter | |||
58 | */ | 59 | */ |
59 | private $bookmarks; | 60 | private $bookmarks; |
60 | 61 | ||
62 | /** @var ConfigManager */ | ||
63 | protected $conf; | ||
64 | |||
61 | /** | 65 | /** |
62 | * @param Bookmark[] $bookmarks initialization. | 66 | * @param Bookmark[] $bookmarks initialization. |
63 | */ | 67 | */ |
64 | public function __construct($bookmarks) | 68 | public function __construct($bookmarks, ConfigManager $conf) |
65 | { | 69 | { |
66 | $this->bookmarks = $bookmarks; | 70 | $this->bookmarks = $bookmarks; |
71 | $this->conf = $conf; | ||
67 | } | 72 | } |
68 | 73 | ||
69 | /** | 74 | /** |
@@ -107,10 +112,14 @@ class BookmarkFilter | |||
107 | $filtered = $this->bookmarks; | 112 | $filtered = $this->bookmarks; |
108 | } | 113 | } |
109 | if (!empty($request[0])) { | 114 | if (!empty($request[0])) { |
110 | $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); | 115 | $filtered = (new BookmarkFilter($filtered, $this->conf)) |
116 | ->filterTags($request[0], $casesensitive, $visibility) | ||
117 | ; | ||
111 | } | 118 | } |
112 | if (!empty($request[1])) { | 119 | if (!empty($request[1])) { |
113 | $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility); | 120 | $filtered = (new BookmarkFilter($filtered, $this->conf)) |
121 | ->filterFulltext($request[1], $visibility) | ||
122 | ; | ||
114 | } | 123 | } |
115 | return $filtered; | 124 | return $filtered; |
116 | case self::$FILTER_TEXT: | 125 | case self::$FILTER_TEXT: |
@@ -141,7 +150,7 @@ class BookmarkFilter | |||
141 | return $this->bookmarks; | 150 | return $this->bookmarks; |
142 | } | 151 | } |
143 | 152 | ||
144 | $out = array(); | 153 | $out = []; |
145 | foreach ($this->bookmarks as $key => $value) { | 154 | foreach ($this->bookmarks as $key => $value) { |
146 | if ($value->isPrivate() && $visibility === 'private') { | 155 | if ($value->isPrivate() && $visibility === 'private') { |
147 | $out[$key] = $value; | 156 | $out[$key] = $value; |
@@ -280,8 +289,9 @@ class BookmarkFilter | |||
280 | * | 289 | * |
281 | * @return string generated regex fragment | 290 | * @return string generated regex fragment |
282 | */ | 291 | */ |
283 | private static function tag2regex(string $tag): string | 292 | protected function tag2regex(string $tag): string |
284 | { | 293 | { |
294 | $tagsSeparator = $this->conf->get('general.tags_separator', ' '); | ||
285 | $len = strlen($tag); | 295 | $len = strlen($tag); |
286 | if (!$len || $tag === "-" || $tag === "*") { | 296 | if (!$len || $tag === "-" || $tag === "*") { |
287 | // nothing to search, return empty regex | 297 | // nothing to search, return empty regex |
@@ -295,12 +305,13 @@ class BookmarkFilter | |||
295 | $i = 0; // start at first character | 305 | $i = 0; // start at first character |
296 | $regex = '(?='; // use positive lookahead | 306 | $regex = '(?='; // use positive lookahead |
297 | } | 307 | } |
298 | $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 . ')'; | ||
299 | // iterate over string, separating it into placeholder and content | 310 | // iterate over string, separating it into placeholder and content |
300 | for (; $i < $len; $i++) { | 311 | for (; $i < $len; $i++) { |
301 | if ($tag[$i] === '*') { | 312 | if ($tag[$i] === '*') { |
302 | // placeholder found | 313 | // placeholder found |
303 | $regex .= '[^ ]*?'; | 314 | $regex .= '[^' . $tagsSeparator . ']*?'; |
304 | } else { | 315 | } else { |
305 | // regular characters | 316 | // regular characters |
306 | $offset = strpos($tag, '*', $i); | 317 | $offset = strpos($tag, '*', $i); |
@@ -316,7 +327,8 @@ class BookmarkFilter | |||
316 | $i = $offset; | 327 | $i = $offset; |
317 | } | 328 | } |
318 | } | 329 | } |
319 | $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 . '))'; | ||
320 | return $regex; | 332 | return $regex; |
321 | } | 333 | } |
322 | 334 | ||
@@ -334,14 +346,15 @@ class BookmarkFilter | |||
334 | */ | 346 | */ |
335 | public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all') | 347 | public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all') |
336 | { | 348 | { |
349 | $tagsSeparator = $this->conf->get('general.tags_separator', ' '); | ||
337 | // 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) |
338 | $inputTags = $tags; | 351 | $inputTags = $tags; |
339 | if (!is_array($tags)) { | 352 | if (!is_array($tags)) { |
340 | // we got an input string, split tags | 353 | // we got an input string, split tags |
341 | $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); | 354 | $inputTags = tags_str2array($inputTags, $tagsSeparator); |
342 | } | 355 | } |
343 | 356 | ||
344 | if (!count($inputTags)) { | 357 | if (count($inputTags) === 0) { |
345 | // no input tags | 358 | // no input tags |
346 | return $this->noFilter($visibility); | 359 | return $this->noFilter($visibility); |
347 | } | 360 | } |
@@ -358,7 +371,7 @@ class BookmarkFilter | |||
358 | } | 371 | } |
359 | 372 | ||
360 | // build regex from all tags | 373 | // build regex from all tags |
361 | $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; | 374 | $re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/'; |
362 | if (!$casesensitive) { | 375 | if (!$casesensitive) { |
363 | // make regex case insensitive | 376 | // make regex case insensitive |
364 | $re .= 'i'; | 377 | $re .= 'i'; |
@@ -378,10 +391,11 @@ class BookmarkFilter | |||
378 | continue; | 391 | continue; |
379 | } | 392 | } |
380 | } | 393 | } |
381 | $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); | ||
382 | if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { | 396 | if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { |
383 | // description given and at least one possible tag found | 397 | // description given and at least one possible tag found |
384 | $descTags = array(); | 398 | $descTags = []; |
385 | // find all tags in the form of #tag in the description | 399 | // find all tags in the form of #tag in the description |
386 | preg_match_all( | 400 | preg_match_all( |
387 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', | 401 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', |
@@ -390,9 +404,9 @@ class BookmarkFilter | |||
390 | ); | 404 | ); |
391 | if (count($descTags[1])) { | 405 | if (count($descTags[1])) { |
392 | // 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 |
393 | $search .= ' ' . implode(' ', $descTags[1]); | 407 | $search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator); |
394 | } | 408 | } |
395 | }; | 409 | } |
396 | // match regular expression with search string | 410 | // match regular expression with search string |
397 | if (!preg_match($re, $search)) { | 411 | if (!preg_match($re, $search)) { |
398 | // this entry does _not_ match our regex | 412 | // this entry does _not_ match our regex |
@@ -422,7 +436,7 @@ class BookmarkFilter | |||
422 | } | 436 | } |
423 | } | 437 | } |
424 | 438 | ||
425 | if (empty(trim($link->getTagsString()))) { | 439 | if (empty($link->getTags())) { |
426 | $filtered[$key] = $link; | 440 | $filtered[$key] = $link; |
427 | } | 441 | } |
428 | } | 442 | } |
@@ -537,10 +551,11 @@ class BookmarkFilter | |||
537 | */ | 551 | */ |
538 | protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string | 552 | protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string |
539 | { | 553 | { |
540 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; | 554 | $tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' ')); |
541 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; | 555 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') . '\\'; |
542 | $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; | 556 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') . '\\'; |
543 | $content .= mb_convert_case($link->getTagsString(), 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') . '\\'; | ||
544 | 559 | ||
545 | $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; | 560 | $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; |
546 | $nextField = $lengths['title']['end'] + 1; | 561 | $nextField = $lengths['title']['end'] + 1; |
@@ -548,7 +563,7 @@ class BookmarkFilter | |||
548 | $nextField = $lengths['description']['end'] + 1; | 563 | $nextField = $lengths['description']['end'] + 1; |
549 | $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())]; | 564 | $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())]; |
550 | $nextField = $lengths['url']['end'] + 1; | 565 | $nextField = $lengths['url']['end'] + 1; |
551 | $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())]; | 566 | $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)]; |
552 | 567 | ||
553 | return $content; | 568 | return $content; |
554 | } | 569 | } |
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index f40fa476..8439d470 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php | |||
@@ -4,6 +4,7 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
6 | 6 | ||
7 | use malkusch\lock\exception\LockAcquireException; | ||
7 | use malkusch\lock\mutex\Mutex; | 8 | use malkusch\lock\mutex\Mutex; |
8 | use malkusch\lock\mutex\NoMutex; | 9 | use malkusch\lock\mutex\NoMutex; |
9 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; | 10 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; |
@@ -80,7 +81,7 @@ class BookmarkIO | |||
80 | } | 81 | } |
81 | 82 | ||
82 | $content = null; | 83 | $content = null; |
83 | $this->mutex->synchronized(function () use (&$content) { | 84 | $this->synchronized(function () use (&$content) { |
84 | $content = file_get_contents($this->datastore); | 85 | $content = file_get_contents($this->datastore); |
85 | }); | 86 | }); |
86 | 87 | ||
@@ -112,18 +113,35 @@ class BookmarkIO | |||
112 | if (is_file($this->datastore) && !is_writeable($this->datastore)) { | 113 | if (is_file($this->datastore) && !is_writeable($this->datastore)) { |
113 | // The datastore exists but is not writeable | 114 | // The datastore exists but is not writeable |
114 | throw new NotWritableDataStoreException($this->datastore); | 115 | throw new NotWritableDataStoreException($this->datastore); |
115 | } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { | 116 | } elseif (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { |
116 | // The datastore does not exist and its parent directory is not writeable | 117 | // The datastore does not exist and its parent directory is not writeable |
117 | throw new NotWritableDataStoreException(dirname($this->datastore)); | 118 | throw new NotWritableDataStoreException(dirname($this->datastore)); |
118 | } | 119 | } |
119 | 120 | ||
120 | $data = self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix; | 121 | $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix; |
121 | 122 | ||
122 | $this->mutex->synchronized(function () use ($data) { | 123 | $this->synchronized(function () use ($data) { |
123 | file_put_contents( | 124 | file_put_contents( |
124 | $this->datastore, | 125 | $this->datastore, |
125 | $data | 126 | $data |
126 | ); | 127 | ); |
127 | }); | 128 | }); |
128 | } | 129 | } |
130 | |||
131 | /** | ||
132 | * Wrapper applying mutex to provided function. | ||
133 | * If the lock can't be acquired (e.g. some shared hosting provider), we execute the function without mutex. | ||
134 | * | ||
135 | * @see https://github.com/shaarli/Shaarli/issues/1650 | ||
136 | * | ||
137 | * @param callable $function | ||
138 | */ | ||
139 | protected function synchronized(callable $function): void | ||
140 | { | ||
141 | try { | ||
142 | $this->mutex->synchronized($function); | ||
143 | } catch (LockAcquireException $exception) { | ||
144 | $function(); | ||
145 | } | ||
146 | } | ||
129 | } | 147 | } |
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 04b996f3..8ab5c441 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php | |||
@@ -13,6 +13,9 @@ namespace Shaarli\Bookmark; | |||
13 | * To prevent data corruption, it does not overwrite existing bookmarks, | 13 | * To prevent data corruption, it does not overwrite existing bookmarks, |
14 | * even though there should not be any. | 14 | * even though there should not be any. |
15 | * | 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 | * | ||
16 | * @package Shaarli\Bookmark | 19 | * @package Shaarli\Bookmark |
17 | */ | 20 | */ |
18 | class BookmarkInitializer | 21 | class BookmarkInitializer |
@@ -36,10 +39,10 @@ class BookmarkInitializer | |||
36 | public function initialize(): void | 39 | public function initialize(): void |
37 | { | 40 | { |
38 | $bookmark = new Bookmark(); | 41 | $bookmark = new Bookmark(); |
39 | $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)')); |
40 | $bookmark->setUrl('https://vimeo.com/153493904'); | 43 | $bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c'); |
41 | $bookmark->setDescription(t( | 44 | $bookmark->setDescription(t( |
42 | '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. |
43 | 46 | ||
44 | Explore your new Shaarli instance by trying out controls and menus. | 47 | Explore your new Shaarli instance by trying out controls and menus. |
45 | 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. |
@@ -54,7 +57,7 @@ Now you can edit or delete the default shaares. | |||
54 | $bookmark = new Bookmark(); | 57 | $bookmark = new Bookmark(); |
55 | $bookmark->setTitle(t('Note: Shaare descriptions')); | 58 | $bookmark->setTitle(t('Note: Shaare descriptions')); |
56 | $bookmark->setDescription(t( | 59 | $bookmark->setDescription(t( |
57 | '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. |
58 | 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. |
59 | 62 | ||
60 | 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. |
@@ -91,7 +94,7 @@ Markdown also supports tables: | |||
91 | 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service') | 94 | 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service') |
92 | ); | 95 | ); |
93 | $bookmark->setDescription(t( | 96 | $bookmark->setDescription(t( |
94 | 'Welcome to Shaarli! | 97 | 'Welcome to Shaarli! |
95 | 98 | ||
96 | 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. |
97 | 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 37a54d03..08cdbb4e 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php | |||
@@ -20,13 +20,14 @@ interface BookmarkServiceInterface | |||
20 | /** | 20 | /** |
21 | * Find a bookmark by hash | 21 | * Find a bookmark by hash |
22 | * | 22 | * |
23 | * @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 | ||
24 | * | 25 | * |
25 | * @return Bookmark | 26 | * @return Bookmark |
26 | * | 27 | * |
27 | * @throws \Exception | 28 | * @throws \Exception |
28 | */ | 29 | */ |
29 | public function findByHash(string $hash): Bookmark; | 30 | public function findByHash(string $hash, string $privateKey = null); |
30 | 31 | ||
31 | /** | 32 | /** |
32 | * @param $url | 33 | * @param $url |
@@ -155,22 +156,29 @@ interface BookmarkServiceInterface | |||
155 | public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; | 156 | public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; |
156 | 157 | ||
157 | /** | 158 | /** |
158 | * 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. | ||
159 | * | 161 | * |
160 | * @return array containing days (in format YYYYMMDD). | 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 | * | ||
167 | * @return array List of bookmarks matching provided period of time. | ||
161 | */ | 168 | */ |
162 | public function days(): array; | 169 | public function findByDate( |
170 | \DateTimeInterface $from, | ||
171 | \DateTimeInterface $to, | ||
172 | ?\DateTimeInterface &$previous, | ||
173 | ?\DateTimeInterface &$next | ||
174 | ): array; | ||
163 | 175 | ||
164 | /** | 176 | /** |
165 | * Returns the list of articles for a given day. | 177 | * Returns the latest bookmark by creation date. |
166 | * | ||
167 | * @param string $request day to filter. Format: YYYYMMDD. | ||
168 | * | 178 | * |
169 | * @return Bookmark[] list of shaare found. | 179 | * @return Bookmark|null Found Bookmark or null if the datastore is empty. |
170 | * | ||
171 | * @throws BookmarkNotFoundException | ||
172 | */ | 180 | */ |
173 | public function filterDay(string $request); | 181 | public function getLatest(): ?Bookmark; |
174 | 182 | ||
175 | /** | 183 | /** |
176 | * Creates the default database after a fresh install. | 184 | * Creates the default database after a fresh install. |
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index faf5dbfd..0ab2d213 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php | |||
@@ -67,17 +67,20 @@ function html_extract_tag($tag, $html) | |||
67 | $propertiesKey = ['property', 'name', 'itemprop']; | 67 | $propertiesKey = ['property', 'name', 'itemprop']; |
68 | $properties = implode('|', $propertiesKey); | 68 | $properties = implode('|', $propertiesKey); |
69 | // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' | 69 | // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' |
70 | $orCondition = '["\']?(?:og:)?'. $tag .'["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; | 70 | $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; |
71 | // Try to retrieve OpenGraph image. | 71 | // Support quotes in double quoted content, and the other way around |
72 | $ogRegex = '#<meta[^>]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=["\'](.*?)["\'].*?>#'; | 72 | $content = 'content=(["\'])((?:(?!\1).)*)\1'; |
73 | // Try to retrieve OpenGraph tag. | ||
74 | $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*' . $content . '.*?>#'; | ||
73 | // If the attributes are not in the order property => content (e.g. Github) | 75 | // If the attributes are not in the order property => content (e.g. Github) |
74 | // New regex to keep this readable... more or less. | 76 | // New regex to keep this readable... more or less. |
75 | $ogRegexReverse = '#<meta[^>]+content=["\'](.*?)["\'][^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; | 77 | $ogRegexReverse = '#<meta[^>]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; |
76 | 78 | ||
77 | if (preg_match($ogRegex, $html, $matches) > 0 | 79 | if ( |
80 | preg_match($ogRegex, $html, $matches) > 0 | ||
78 | || preg_match($ogRegexReverse, $html, $matches) > 0 | 81 | || preg_match($ogRegexReverse, $html, $matches) > 0 |
79 | ) { | 82 | ) { |
80 | return $matches[1]; | 83 | return $matches[2]; |
81 | } | 84 | } |
82 | 85 | ||
83 | return false; | 86 | return false; |
@@ -116,7 +119,7 @@ function hashtag_autolink($description, $indexUrl = '') | |||
116 | * \p{Mn} - any non marking space (accents, umlauts, etc) | 119 | * \p{Mn} - any non marking space (accents, umlauts, etc) |
117 | */ | 120 | */ |
118 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; | 121 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; |
119 | $replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>'; | 122 | $replacement = '$1<a href="' . $indexUrl . './add-tag/$2" title="Hashtag $2">#$2</a>'; |
120 | return preg_replace($regex, $replacement, $description); | 123 | return preg_replace($regex, $replacement, $description); |
121 | } | 124 | } |
122 | 125 | ||
@@ -138,12 +141,17 @@ function space2nbsp($text) | |||
138 | * | 141 | * |
139 | * @param string $description shaare's description. | 142 | * @param string $description shaare's description. |
140 | * @param string $indexUrl URL to Shaarli's index. | 143 | * @param string $indexUrl URL to Shaarli's index. |
141 | 144 | * @param bool $autolink Turn on/off automatic linkifications of URLs and hashtags | |
145 | * | ||
142 | * @return string formatted description. | 146 | * @return string formatted description. |
143 | */ | 147 | */ |
144 | function format_description($description, $indexUrl = '') | 148 | function format_description($description, $indexUrl = '', $autolink = true) |
145 | { | 149 | { |
146 | return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl))); | 150 | if ($autolink) { |
151 | $description = hashtag_autolink(text2clickable($description), $indexUrl); | ||
152 | } | ||
153 | |||
154 | return nl2br(space2nbsp($description)); | ||
147 | } | 155 | } |
148 | 156 | ||
149 | /** | 157 | /** |
@@ -171,3 +179,49 @@ function is_note($linkUrl) | |||
171 | { | 179 | { |
172 | return isset($linkUrl[0]) && $linkUrl[0] === '?'; | 180 | return isset($linkUrl[0]) && $linkUrl[0] === '?'; |
173 | } | 181 | } |
182 | |||
183 | /** | ||
184 | * Extract an array of tags from a given tag string, with provided separator. | ||
185 | * | ||
186 | * @param string|null $tags String containing a list of tags separated by $separator. | ||
187 | * @param string $separator Shaarli's default: ' ' (whitespace) | ||
188 | * | ||
189 | * @return array List of tags | ||
190 | */ | ||
191 | function tags_str2array(?string $tags, string $separator): array | ||
192 | { | ||
193 | // For whitespaces, we use the special \s regex character | ||
194 | $separator = $separator === ' ' ? '\s' : $separator; | ||
195 | |||
196 | return preg_split('/\s*' . $separator . '+\s*/', trim($tags) ?? '', -1, PREG_SPLIT_NO_EMPTY); | ||
197 | } | ||
198 | |||
199 | /** | ||
200 | * Return a tag string with provided separator from a list of tags. | ||
201 | * Note that given array is clean up by tags_filter(). | ||
202 | * | ||
203 | * @param array|null $tags List of tags | ||
204 | * @param string $separator | ||
205 | * | ||
206 | * @return string | ||
207 | */ | ||
208 | function tags_array2str(?array $tags, string $separator): string | ||
209 | { | ||
210 | return implode($separator, tags_filter($tags, $separator)); | ||
211 | } | ||
212 | |||
213 | /** | ||
214 | * Clean an array of tags: trim + remove empty entries | ||
215 | * | ||
216 | * @param array|null $tags List of tags | ||
217 | * @param string $separator | ||
218 | * | ||
219 | * @return array | ||
220 | */ | ||
221 | function tags_filter(?array $tags, string $separator): array | ||
222 | { | ||
223 | $trimDefault = " \t\n\r\0\x0B"; | ||
224 | return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string { | ||
225 | return trim($entry, $trimDefault . $separator); | ||
226 | }, $tags ?? []))); | ||
227 | } | ||
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 c21d58dd..6d69a880 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php | |||
@@ -5,6 +5,7 @@ declare(strict_types=1); | |||
5 | namespace Shaarli\Container; | 5 | namespace Shaarli\Container; |
6 | 6 | ||
7 | use malkusch\lock\mutex\FlockMutex; | 7 | use malkusch\lock\mutex\FlockMutex; |
8 | use Psr\Log\LoggerInterface; | ||
8 | use Shaarli\Bookmark\BookmarkFileService; | 9 | use Shaarli\Bookmark\BookmarkFileService; |
9 | use Shaarli\Bookmark\BookmarkServiceInterface; | 10 | use Shaarli\Bookmark\BookmarkServiceInterface; |
10 | use Shaarli\Config\ConfigManager; | 11 | use Shaarli\Config\ConfigManager; |
@@ -14,6 +15,7 @@ use Shaarli\Front\Controller\Visitor\ErrorController; | |||
14 | use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; | 15 | use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; |
15 | use Shaarli\History; | 16 | use Shaarli\History; |
16 | use Shaarli\Http\HttpAccess; | 17 | use Shaarli\Http\HttpAccess; |
18 | use Shaarli\Http\MetadataRetriever; | ||
17 | use Shaarli\Netscape\NetscapeBookmarkUtils; | 19 | use Shaarli\Netscape\NetscapeBookmarkUtils; |
18 | use Shaarli\Plugin\PluginManager; | 20 | use Shaarli\Plugin\PluginManager; |
19 | use Shaarli\Render\PageBuilder; | 21 | use Shaarli\Render\PageBuilder; |
@@ -48,6 +50,12 @@ class ContainerBuilder | |||
48 | /** @var LoginManager */ | 50 | /** @var LoginManager */ |
49 | protected $login; | 51 | protected $login; |
50 | 52 | ||
53 | /** @var PluginManager */ | ||
54 | protected $pluginManager; | ||
55 | |||
56 | /** @var LoggerInterface */ | ||
57 | protected $logger; | ||
58 | |||
51 | /** @var string|null */ | 59 | /** @var string|null */ |
52 | protected $basePath = null; | 60 | protected $basePath = null; |
53 | 61 | ||
@@ -55,12 +63,16 @@ class ContainerBuilder | |||
55 | ConfigManager $conf, | 63 | ConfigManager $conf, |
56 | SessionManager $session, | 64 | SessionManager $session, |
57 | CookieManager $cookieManager, | 65 | CookieManager $cookieManager, |
58 | LoginManager $login | 66 | LoginManager $login, |
67 | PluginManager $pluginManager, | ||
68 | LoggerInterface $logger | ||
59 | ) { | 69 | ) { |
60 | $this->conf = $conf; | 70 | $this->conf = $conf; |
61 | $this->session = $session; | 71 | $this->session = $session; |
62 | $this->login = $login; | 72 | $this->login = $login; |
63 | $this->cookieManager = $cookieManager; | 73 | $this->cookieManager = $cookieManager; |
74 | $this->pluginManager = $pluginManager; | ||
75 | $this->logger = $logger; | ||
64 | } | 76 | } |
65 | 77 | ||
66 | public function build(): ShaarliContainer | 78 | public function build(): ShaarliContainer |
@@ -71,11 +83,10 @@ class ContainerBuilder | |||
71 | $container['sessionManager'] = $this->session; | 83 | $container['sessionManager'] = $this->session; |
72 | $container['cookieManager'] = $this->cookieManager; | 84 | $container['cookieManager'] = $this->cookieManager; |
73 | $container['loginManager'] = $this->login; | 85 | $container['loginManager'] = $this->login; |
86 | $container['pluginManager'] = $this->pluginManager; | ||
87 | $container['logger'] = $this->logger; | ||
74 | $container['basePath'] = $this->basePath; | 88 | $container['basePath'] = $this->basePath; |
75 | 89 | ||
76 | $container['plugins'] = function (ShaarliContainer $container): PluginManager { | ||
77 | return new PluginManager($container->conf); | ||
78 | }; | ||
79 | 90 | ||
80 | $container['history'] = function (ShaarliContainer $container): History { | 91 | $container['history'] = function (ShaarliContainer $container): History { |
81 | return new History($container->conf->get('resource.history')); | 92 | return new History($container->conf->get('resource.history')); |
@@ -90,24 +101,21 @@ class ContainerBuilder | |||
90 | ); | 101 | ); |
91 | }; | 102 | }; |
92 | 103 | ||
104 | $container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever { | ||
105 | return new MetadataRetriever($container->conf, $container->httpAccess); | ||
106 | }; | ||
107 | |||
93 | $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder { | 108 | $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder { |
94 | return new PageBuilder( | 109 | return new PageBuilder( |
95 | $container->conf, | 110 | $container->conf, |
96 | $container->sessionManager->getSession(), | 111 | $container->sessionManager->getSession(), |
112 | $container->logger, | ||
97 | $container->bookmarkService, | 113 | $container->bookmarkService, |
98 | $container->sessionManager->generateToken(), | 114 | $container->sessionManager->generateToken(), |
99 | $container->loginManager->isLoggedIn() | 115 | $container->loginManager->isLoggedIn() |
100 | ); | 116 | ); |
101 | }; | 117 | }; |
102 | 118 | ||
103 | $container['pluginManager'] = function (ShaarliContainer $container): PluginManager { | ||
104 | $pluginManager = new PluginManager($container->conf); | ||
105 | |||
106 | $pluginManager->load($container->conf->get('general.enabled_plugins')); | ||
107 | |||
108 | return $pluginManager; | ||
109 | }; | ||
110 | |||
111 | $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { | 119 | $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { |
112 | return new FormatterFactory( | 120 | return new FormatterFactory( |
113 | $container->conf, | 121 | $container->conf, |
@@ -145,7 +153,7 @@ class ContainerBuilder | |||
145 | 153 | ||
146 | $container['updater'] = function (ShaarliContainer $container): Updater { | 154 | $container['updater'] = function (ShaarliContainer $container): Updater { |
147 | return new Updater( | 155 | return new Updater( |
148 | UpdaterUtils::read_updates_file($container->conf->get('resource.updates')), | 156 | UpdaterUtils::readUpdatesFile($container->conf->get('resource.updates')), |
149 | $container->bookmarkService, | 157 | $container->bookmarkService, |
150 | $container->conf, | 158 | $container->conf, |
151 | $container->loginManager->isLoggedIn() | 159 | $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/CachedPage.php b/application/feed/CachedPage.php index d809bdd9..c23c200f 100644 --- a/application/feed/CachedPage.php +++ b/application/feed/CachedPage.php | |||
@@ -1,34 +1,43 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Feed; | 5 | namespace Shaarli\Feed; |
4 | 6 | ||
7 | use DatePeriod; | ||
8 | |||
5 | /** | 9 | /** |
6 | * Simple cache system, mainly for the RSS/ATOM feeds | 10 | * Simple cache system, mainly for the RSS/ATOM feeds |
7 | */ | 11 | */ |
8 | class CachedPage | 12 | class CachedPage |
9 | { | 13 | { |
10 | // Directory containing page caches | 14 | /** Directory containing page caches */ |
11 | private $cacheDir; | 15 | protected $cacheDir; |
16 | |||
17 | /** Should this URL be cached (boolean)? */ | ||
18 | protected $shouldBeCached; | ||
12 | 19 | ||
13 | // Should this URL be cached (boolean)? | 20 | /** Name of the cache file for this URL */ |
14 | private $shouldBeCached; | 21 | protected $filename; |
15 | 22 | ||
16 | // Name of the cache file for this URL | 23 | /** @var DatePeriod|null Optionally specify a period of time for cache validity */ |
17 | private $filename; | 24 | protected $validityPeriod; |
18 | 25 | ||
19 | /** | 26 | /** |
20 | * Creates a new CachedPage | 27 | * Creates a new CachedPage |
21 | * | 28 | * |
22 | * @param string $cacheDir page cache directory | 29 | * @param string $cacheDir page cache directory |
23 | * @param string $url page URL | 30 | * @param string $url page URL |
24 | * @param bool $shouldBeCached whether this page needs to be cached | 31 | * @param bool $shouldBeCached whether this page needs to be cached |
32 | * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache | ||
25 | */ | 33 | */ |
26 | public function __construct($cacheDir, $url, $shouldBeCached) | 34 | public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod) |
27 | { | 35 | { |
28 | // TODO: check write access to the cache directory | 36 | // TODO: check write access to the cache directory |
29 | $this->cacheDir = $cacheDir; | 37 | $this->cacheDir = $cacheDir; |
30 | $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; | 38 | $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; |
31 | $this->shouldBeCached = $shouldBeCached; | 39 | $this->shouldBeCached = $shouldBeCached; |
40 | $this->validityPeriod = $validityPeriod; | ||
32 | } | 41 | } |
33 | 42 | ||
34 | /** | 43 | /** |
@@ -41,10 +50,20 @@ class CachedPage | |||
41 | if (!$this->shouldBeCached) { | 50 | if (!$this->shouldBeCached) { |
42 | return null; | 51 | return null; |
43 | } | 52 | } |
44 | if (is_file($this->filename)) { | 53 | if (!is_file($this->filename)) { |
45 | return file_get_contents($this->filename); | 54 | return null; |
55 | } | ||
56 | if ($this->validityPeriod !== null) { | ||
57 | $cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename)); | ||
58 | if ( | ||
59 | $cacheDate < $this->validityPeriod->getStartDate() | ||
60 | || $cacheDate > $this->validityPeriod->getEndDate() | ||
61 | ) { | ||
62 | return null; | ||
63 | } | ||
46 | } | 64 | } |
47 | return null; | 65 | |
66 | return file_get_contents($this->filename); | ||
48 | } | 67 | } |
49 | 68 | ||
50 | /** | 69 | /** |
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index f70fce4f..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; |
@@ -107,14 +108,14 @@ class FeedBuilder | |||
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 d58a5e39..7e0afafc 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php | |||
@@ -12,8 +12,8 @@ namespace Shaarli\Formatter; | |||
12 | */ | 12 | */ |
13 | class BookmarkDefaultFormatter extends BookmarkFormatter | 13 | class BookmarkDefaultFormatter extends BookmarkFormatter |
14 | { | 14 | { |
15 | const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; | 15 | protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; |
16 | const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; | 16 | protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; |
17 | 17 | ||
18 | /** | 18 | /** |
19 | * @inheritdoc | 19 | * @inheritdoc |
@@ -46,8 +46,13 @@ class BookmarkDefaultFormatter extends BookmarkFormatter | |||
46 | $bookmark->getDescription() ?? '', | 46 | $bookmark->getDescription() ?? '', |
47 | $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] | 47 | $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] |
48 | ); | 48 | ); |
49 | $description = format_description( | ||
50 | escape($description), | ||
51 | $indexUrl, | ||
52 | $this->conf->get('formatter_settings.autolink', true) | ||
53 | ); | ||
49 | 54 | ||
50 | return $this->replaceTokens(format_description(escape($description), $indexUrl)); | 55 | return $this->replaceTokens($description); |
51 | } | 56 | } |
52 | 57 | ||
53 | /** | 58 | /** |
@@ -63,15 +68,16 @@ class BookmarkDefaultFormatter extends BookmarkFormatter | |||
63 | */ | 68 | */ |
64 | protected function formatTagListHtml($bookmark) | 69 | protected function formatTagListHtml($bookmark) |
65 | { | 70 | { |
71 | $tagsSeparator = $this->conf->get('general.tags_separator', ' '); | ||
66 | if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) { | 72 | if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) { |
67 | return $this->formatTagList($bookmark); | 73 | return $this->formatTagList($bookmark); |
68 | } | 74 | } |
69 | 75 | ||
70 | $tags = $this->tokenizeSearchHighlightField( | 76 | $tags = $this->tokenizeSearchHighlightField( |
71 | $bookmark->getTagsString(), | 77 | $bookmark->getTagsString($tagsSeparator), |
72 | $bookmark->getAdditionalContentEntry('search_highlight')['tags'] | 78 | $bookmark->getAdditionalContentEntry('search_highlight')['tags'] |
73 | ); | 79 | ); |
74 | $tags = $this->filterTagList(explode(' ', $tags)); | 80 | $tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator)); |
75 | $tags = escape($tags); | 81 | $tags = escape($tags); |
76 | $tags = $this->replaceTokensArray($tags); | 82 | $tags = $this->replaceTokensArray($tags); |
77 | 83 | ||
@@ -83,7 +89,7 @@ class BookmarkDefaultFormatter extends BookmarkFormatter | |||
83 | */ | 89 | */ |
84 | protected function formatTagString($bookmark) | 90 | protected function formatTagString($bookmark) |
85 | { | 91 | { |
86 | return implode(' ', $this->formatTagList($bookmark)); | 92 | return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark)); |
87 | } | 93 | } |
88 | 94 | ||
89 | /** | 95 | /** |
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php index e1b7f705..124ce78b 100644 --- a/application/formatter/BookmarkFormatter.php +++ b/application/formatter/BookmarkFormatter.php | |||
@@ -267,7 +267,7 @@ abstract class BookmarkFormatter | |||
267 | */ | 267 | */ |
268 | protected function formatTagString($bookmark) | 268 | protected function formatTagString($bookmark) |
269 | { | 269 | { |
270 | return implode(' ', $this->formatTagList($bookmark)); | 270 | return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark)); |
271 | } | 271 | } |
272 | 272 | ||
273 | /** | 273 | /** |
@@ -351,6 +351,7 @@ abstract class BookmarkFormatter | |||
351 | 351 | ||
352 | /** | 352 | /** |
353 | * 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. | ||
354 | * | 355 | * |
355 | * @param array $tags | 356 | * @param array $tags |
356 | * | 357 | * |
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index f7714be9..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; |
@@ -71,7 +71,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
71 | $processedDescription = $this->replaceTokens($processedDescription); | 71 | $processedDescription = $this->replaceTokens($processedDescription); |
72 | 72 | ||
73 | if (!empty($processedDescription)) { | 73 | if (!empty($processedDescription)) { |
74 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; | 74 | $processedDescription = '<div class="markdown">' . $processedDescription . '</div>'; |
75 | } | 75 | } |
76 | 76 | ||
77 | return $processedDescription; | 77 | return $processedDescription; |
@@ -110,7 +110,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
110 | function ($match) use ($allowedProtocols, $indexUrl) { | 110 | function ($match) use ($allowedProtocols, $indexUrl) { |
111 | $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; | 111 | $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; |
112 | $link .= whitelist_protocols($match[1], $allowedProtocols); | 112 | $link .= whitelist_protocols($match[1], $allowedProtocols); |
113 | return ']('. $link.')'; | 113 | return '](' . $link . ')'; |
114 | }, | 114 | }, |
115 | $description | 115 | $description |
116 | ); | 116 | ); |
@@ -137,7 +137,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
137 | * \p{Mn} - any non marking space (accents, umlauts, etc) | 137 | * \p{Mn} - any non marking space (accents, umlauts, etc) |
138 | */ | 138 | */ |
139 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; | 139 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; |
140 | $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)'; | 140 | $replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)'; |
141 | 141 | ||
142 | $descriptionLines = explode(PHP_EOL, $description); | 142 | $descriptionLines = explode(PHP_EOL, $description); |
143 | $descriptionOut = ''; | 143 | $descriptionOut = ''; |
@@ -178,17 +178,17 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
178 | */ | 178 | */ |
179 | protected function sanitizeHtml($description) | 179 | protected function sanitizeHtml($description) |
180 | { | 180 | { |
181 | $escapeTags = array( | 181 | $escapeTags = [ |
182 | 'script', | 182 | 'script', |
183 | 'style', | 183 | 'style', |
184 | 'link', | 184 | 'link', |
185 | 'iframe', | 185 | 'iframe', |
186 | 'frameset', | 186 | 'frameset', |
187 | 'frame', | 187 | 'frame', |
188 | ); | 188 | ]; |
189 | foreach ($escapeTags as $tag) { | 189 | foreach ($escapeTags as $tag) { |
190 | $description = preg_replace_callback( | 190 | $description = preg_replace_callback( |
191 | '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is', | 191 | '#<\s*' . $tag . '[^>]*>(.*</\s*' . $tag . '[^>]*>)?#is', |
192 | function ($match) { | 192 | function ($match) { |
193 | return escape($match[0]); | 193 | return escape($match[0]); |
194 | }, | 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 0ed7ad81..dc421661 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php | |||
@@ -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..4b74f4a9 --- /dev/null +++ b/application/front/controller/admin/ServerController.php | |||
@@ -0,0 +1,101 @@ | |||
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 | $permissions = array_merge( | ||
43 | ApplicationUtils::checkResourcePermissions($this->container->conf), | ||
44 | ApplicationUtils::checkDatastoreMutex() | ||
45 | ); | ||
46 | |||
47 | $this->assignView('php_version', PHP_VERSION); | ||
48 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
49 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
50 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
51 | $this->assignView('permissions', $permissions); | ||
52 | $this->assignView('release_url', $releaseUrl); | ||
53 | $this->assignView('latest_version', $latestVersion); | ||
54 | $this->assignView('current_version', $currentVersion); | ||
55 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode')); | ||
56 | $this->assignView('index_url', index_url($this->container->environment)); | ||
57 | $this->assignView('client_ip', client_ip_id($this->container->environment)); | ||
58 | $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', [])); | ||
59 | |||
60 | $this->assignView( | ||
61 | 'pagetitle', | ||
62 | t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
63 | ); | ||
64 | |||
65 | return $response->write($this->render('server')); | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails). | ||
70 | */ | ||
71 | public function clearCache(Request $request, Response $response): Response | ||
72 | { | ||
73 | $exclude = ['.htaccess']; | ||
74 | |||
75 | if ($request->getQueryParam('type') === static::CACHE_THUMB) { | ||
76 | $folders = [$this->container->conf->get('resource.thumbnails_cache')]; | ||
77 | |||
78 | $this->saveWarningMessage( | ||
79 | t('Thumbnails cache has been cleared.') . ' ' . | ||
80 | '<a href="' . $this->container->basePath . '/admin/thumbnails">' . | ||
81 | t('Please synchronize them.') . | ||
82 | '</a>' | ||
83 | ); | ||
84 | } else { | ||
85 | $folders = [ | ||
86 | $this->container->conf->get('resource.page_cache'), | ||
87 | $this->container->conf->get('resource.raintpl_tmp'), | ||
88 | ]; | ||
89 | |||
90 | $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!')); | ||
91 | } | ||
92 | |||
93 | // Make sure that we don't delete root cache folder | ||
94 | $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders)))); | ||
95 | foreach ($folders as $folder) { | ||
96 | FileUtils::clearFolder($folder, false, $exclude); | ||
97 | } | ||
98 | |||
99 | return $this->redirect($response, '/admin/server'); | ||
100 | } | ||
101 | } | ||
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..fb9cacc2 --- /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'] = $link['tags'] !== null && 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 4dc09d38..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)); |
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..29492a5f 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)); |
@@ -96,9 +86,11 @@ class DailyController extends ShaarliVisitorController | |||
96 | public function rss(Request $request, Response $response): Response | 86 | public function rss(Request $request, Response $response): Response |
97 | { | 87 | { |
98 | $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); | 88 | $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); |
89 | $type = DailyPageHelper::extractRequestedType($request); | ||
90 | $cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type); | ||
99 | 91 | ||
100 | $pageUrl = page_url($this->container->environment); | 92 | $pageUrl = page_url($this->container->environment); |
101 | $cache = $this->container->pageCacheManager->getCachePage($pageUrl); | 93 | $cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration); |
102 | 94 | ||
103 | $cached = $cache->cachedVersion(); | 95 | $cached = $cache->cachedVersion(); |
104 | if (!empty($cached)) { | 96 | if (!empty($cached)) { |
@@ -106,11 +98,13 @@ class DailyController extends ShaarliVisitorController | |||
106 | } | 98 | } |
107 | 99 | ||
108 | $days = []; | 100 | $days = []; |
101 | $format = DailyPageHelper::getFormatByType($type); | ||
102 | $length = DailyPageHelper::getRssLengthByType($type); | ||
109 | foreach ($this->container->bookmarkService->search() as $bookmark) { | 103 | foreach ($this->container->bookmarkService->search() as $bookmark) { |
110 | $day = $bookmark->getCreated()->format('Ymd'); | 104 | $day = $bookmark->getCreated()->format($format); |
111 | 105 | ||
112 | // Stop iterating after DAILY_RSS_NB_DAYS entries | 106 | // Stop iterating after DAILY_RSS_NB_DAYS entries |
113 | if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { | 107 | if (count($days) === $length && !isset($days[$day])) { |
114 | break; | 108 | break; |
115 | } | 109 | } |
116 | 110 | ||
@@ -127,12 +121,19 @@ class DailyController extends ShaarliVisitorController | |||
127 | 121 | ||
128 | /** @var Bookmark[] $bookmarks */ | 122 | /** @var Bookmark[] $bookmarks */ |
129 | foreach ($days as $day => $bookmarks) { | 123 | foreach ($days as $day => $bookmarks) { |
130 | $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); | 124 | $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day); |
125 | $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime); | ||
126 | |||
127 | // We only want the RSS entry to be published when the period is over. | ||
128 | if (new DateTime() < $endDateTime) { | ||
129 | continue; | ||
130 | } | ||
131 | |||
131 | $dataPerDay[$day] = [ | 132 | $dataPerDay[$day] = [ |
132 | 'date' => $dayDatetime, | 133 | 'date' => $endDateTime, |
133 | 'date_rss' => $dayDatetime->format(DateTime::RSS), | 134 | 'date_rss' => $endDateTime->format(DateTime::RSS), |
134 | 'date_human' => format_date($dayDatetime, false, true), | 135 | 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime, false), |
135 | 'absolute_url' => $indexUrl . 'daily?day=' . $day, | 136 | 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day, |
136 | 'links' => [], | 137 | 'links' => [], |
137 | ]; | 138 | ]; |
138 | 139 | ||
@@ -141,16 +142,20 @@ class DailyController extends ShaarliVisitorController | |||
141 | 142 | ||
142 | // Make permalink URL absolute | 143 | // Make permalink URL absolute |
143 | if ($bookmark->isNote()) { | 144 | if ($bookmark->isNote()) { |
144 | $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); | 145 | $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl(); |
145 | } | 146 | } |
146 | } | 147 | } |
147 | } | 148 | } |
148 | 149 | ||
149 | $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); | 150 | $this->assignAllView([ |
150 | $this->assignView('index_url', $indexUrl); | 151 | 'title' => $this->container->conf->get('general.title', 'Shaarli'), |
151 | $this->assignView('page_url', $pageUrl); | 152 | 'index_url' => $indexUrl, |
152 | $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); | 153 | 'page_url' => $pageUrl, |
153 | $this->assignView('days', $dataPerDay); | 154 | 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false), |
155 | 'days' => $dataPerDay, | ||
156 | 'type' => $type, | ||
157 | 'localizedType' => $this->translateType($type), | ||
158 | ]); | ||
154 | 159 | ||
155 | $rssContent = $this->render(TemplatePage::DAILY_RSS); | 160 | $rssContent = $this->render(TemplatePage::DAILY_RSS); |
156 | 161 | ||
@@ -189,4 +194,13 @@ class DailyController extends ShaarliVisitorController | |||
189 | 194 | ||
190 | return $columns; | 195 | return $columns; |
191 | } | 196 | } |
197 | |||
198 | protected function translateType($type): string | ||
199 | { | ||
200 | return [ | ||
201 | t('day') => t('Daily'), | ||
202 | t('week') => t('Weekly'), | ||
203 | t('month') => t('Monthly'), | ||
204 | ][t($type)] ?? t('Daily'); | ||
205 | } | ||
192 | } | 206 | } |
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..418d4a49 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,21 @@ 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 | $permissions = array_merge( | ||
60 | ApplicationUtils::checkResourcePermissions($this->container->conf), | ||
61 | ApplicationUtils::checkDatastoreMutex() | ||
62 | ); | ||
63 | |||
64 | $this->assignView('php_version', PHP_VERSION); | ||
65 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
66 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
67 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
68 | $this->assignView('permissions', $permissions); | ||
69 | |||
70 | $this->assignView('pagetitle', t('Install Shaarli')); | ||
71 | |||
56 | return $response->write($this->render('install')); | 72 | return $response->write($this->render('install')); |
57 | } | 73 | } |
58 | 74 | ||
@@ -65,17 +81,18 @@ class InstallController extends ShaarliVisitorController | |||
65 | // This part makes sure sessions works correctly. | 81 | // This part makes sure sessions works correctly. |
66 | // (Because on some hosts, session.save_path may not be set correctly, | 82 | // (Because on some hosts, session.save_path may not be set correctly, |
67 | // or we may not have write access to it.) | 83 | // or we may not have write access to it.) |
68 | if (static::SESSION_TEST_VALUE | 84 | if ( |
85 | static::SESSION_TEST_VALUE | ||
69 | !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) | 86 | !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) |
70 | ) { | 87 | ) { |
71 | // Step 2: Check if data in session is correct. | 88 | // Step 2: Check if data in session is correct. |
72 | $msg = t( | 89 | $msg = t( |
73 | '<pre>Sessions do not seem to work correctly on your server.<br>'. | 90 | '<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, '. | 91 | 'Make sure the variable "session.save_path" is set correctly in your PHP config, ' . |
75 | 'and that you have write access to it.<br>'. | 92 | 'and that you have write access to it.<br>' . |
76 | 'It currently points to %s.<br>'. | 93 | 'It currently points to %s.<br>' . |
77 | 'On some browsers, accessing your server via a hostname like \'localhost\' '. | 94 | 'On some browsers, accessing your server via a hostname like \'localhost\' ' . |
78 | 'or any custom hostname without a dot causes cookie storage to fail. '. | 95 | '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>' | 96 | 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>' |
80 | ); | 97 | ); |
81 | $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); | 98 | $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); |
@@ -94,7 +111,8 @@ class InstallController extends ShaarliVisitorController | |||
94 | public function save(Request $request, Response $response): Response | 111 | public function save(Request $request, Response $response): Response |
95 | { | 112 | { |
96 | $timezone = 'UTC'; | 113 | $timezone = 'UTC'; |
97 | if (!empty($request->getParam('continent')) | 114 | if ( |
115 | !empty($request->getParam('continent')) | ||
98 | && !empty($request->getParam('city')) | 116 | && !empty($request->getParam('city')) |
99 | && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) | 117 | && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) |
100 | ) { | 118 | ) { |
@@ -104,7 +122,7 @@ class InstallController extends ShaarliVisitorController | |||
104 | 122 | ||
105 | $login = $request->getParam('setlogin'); | 123 | $login = $request->getParam('setlogin'); |
106 | $this->container->conf->set('credentials.login', $login); | 124 | $this->container->conf->set('credentials.login', $login); |
107 | $salt = sha1(uniqid('', true) .'_'. mt_rand()); | 125 | $salt = sha1(uniqid('', true) . '_' . mt_rand()); |
108 | $this->container->conf->set('credentials.salt', $salt); | 126 | $this->container->conf->set('credentials.salt', $salt); |
109 | $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); | 127 | $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); |
110 | 128 | ||
@@ -113,7 +131,7 @@ class InstallController extends ShaarliVisitorController | |||
113 | } else { | 131 | } else { |
114 | $this->container->conf->set( | 132 | $this->container->conf->set( |
115 | 'general.title', | 133 | 'general.title', |
116 | 'Shared bookmarks on '.escape(index_url($this->container->environment)) | 134 | 'Shared bookmarks on ' . escape(index_url($this->container->environment)) |
117 | ); | 135 | ); |
118 | } | 136 | } |
119 | 137 | ||
@@ -150,7 +168,7 @@ class InstallController extends ShaarliVisitorController | |||
150 | protected function checkPermissions(): bool | 168 | protected function checkPermissions(): bool |
151 | { | 169 | { |
152 | // Ensure Shaarli has proper access to its resources | 170 | // Ensure Shaarli has proper access to its resources |
153 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); | 171 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true); |
154 | if (empty($errors)) { | 172 | if (empty($errors)) { |
155 | return true; | 173 | return true; |
156 | } | 174 | } |
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 54f9fe03..ae946c59 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php | |||
@@ -144,7 +144,8 @@ abstract class ShaarliVisitorController | |||
144 | if (null !== $referer) { | 144 | if (null !== $referer) { |
145 | $currentUrl = parse_url($referer); | 145 | $currentUrl = parse_url($referer); |
146 | // 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 |
147 | if (isset($currentUrl['host']) | 147 | if ( |
148 | isset($currentUrl['host']) | ||
148 | && strpos(index_url($this->container->environment), $currentUrl['host']) === false | 149 | && strpos(index_url($this->container->environment), $currentUrl['host']) === false |
149 | ) { | 150 | ) { |
150 | return $response->withRedirect($defaultPath); | 151 | return $response->withRedirect($defaultPath); |
@@ -173,7 +174,7 @@ abstract class ShaarliVisitorController | |||
173 | } | 174 | } |
174 | } | 175 | } |
175 | 176 | ||
176 | $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; | 177 | $queryString = count($params) > 0 ? '?' . http_build_query($params) : ''; |
177 | $anchor = $anchor ? '#' . $anchor : ''; | 178 | $anchor = $anchor ? '#' . $anchor : ''; |
178 | 179 | ||
179 | 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..a6c03aae 100644 --- a/application/ApplicationUtils.php +++ b/application/helper/ApplicationUtils.php | |||
@@ -1,7 +1,10 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli; | 2 | |
3 | namespace Shaarli\Helper; | ||
3 | 4 | ||
4 | use Exception; | 5 | use Exception; |
6 | use malkusch\lock\exception\LockAcquireException; | ||
7 | use malkusch\lock\mutex\FlockMutex; | ||
5 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
6 | 9 | ||
7 | /** | 10 | /** |
@@ -14,8 +17,9 @@ class ApplicationUtils | |||
14 | */ | 17 | */ |
15 | public static $VERSION_FILE = 'shaarli_version.php'; | 18 | public static $VERSION_FILE = 'shaarli_version.php'; |
16 | 19 | ||
17 | private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; | 20 | public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli'; |
18 | private static $GIT_BRANCHES = array('latest', 'stable'); | 21 | public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; |
22 | public static $GIT_BRANCHES = ['latest', 'stable']; | ||
19 | private static $VERSION_START_TAG = '<?php /* '; | 23 | private static $VERSION_START_TAG = '<?php /* '; |
20 | private static $VERSION_END_TAG = ' */ ?>'; | 24 | private static $VERSION_END_TAG = ' */ ?>'; |
21 | 25 | ||
@@ -63,8 +67,8 @@ class ApplicationUtils | |||
63 | } | 67 | } |
64 | 68 | ||
65 | return str_replace( | 69 | return str_replace( |
66 | array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), | 70 | [self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL], |
67 | array('', '', ''), | 71 | ['', '', ''], |
68 | $data | 72 | $data |
69 | ); | 73 | ); |
70 | } | 74 | } |
@@ -125,7 +129,7 @@ class ApplicationUtils | |||
125 | // Late Static Binding allows overriding within tests | 129 | // Late Static Binding allows overriding within tests |
126 | // See http://php.net/manual/en/language.oop5.late-static-bindings.php | 130 | // See http://php.net/manual/en/language.oop5.late-static-bindings.php |
127 | $latestVersion = static::getVersion( | 131 | $latestVersion = static::getVersion( |
128 | self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE | 132 | self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE |
129 | ); | 133 | ); |
130 | 134 | ||
131 | if (!$latestVersion) { | 135 | if (!$latestVersion) { |
@@ -171,35 +175,47 @@ class ApplicationUtils | |||
171 | /** | 175 | /** |
172 | * Checks Shaarli has the proper access permissions to its resources | 176 | * Checks Shaarli has the proper access permissions to its resources |
173 | * | 177 | * |
174 | * @param ConfigManager $conf Configuration Manager instance. | 178 | * @param ConfigManager $conf Configuration Manager instance. |
179 | * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template. | ||
180 | * Currently we only need to be able to read the theme and write in raintpl cache. | ||
175 | * | 181 | * |
176 | * @return array A list of the detected configuration issues | 182 | * @return array A list of the detected configuration issues |
177 | */ | 183 | */ |
178 | public static function checkResourcePermissions($conf) | 184 | public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array |
179 | { | 185 | { |
180 | $errors = array(); | 186 | $errors = []; |
181 | $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); | 187 | $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); |
182 | 188 | ||
183 | // Check script and template directories are readable | 189 | // Check script and template directories are readable |
184 | foreach (array( | 190 | foreach ( |
185 | 'application', | 191 | [ |
186 | 'inc', | 192 | 'application', |
187 | 'plugins', | 193 | 'inc', |
188 | $rainTplDir, | 194 | 'plugins', |
189 | $rainTplDir . '/' . $conf->get('resource.theme'), | 195 | $rainTplDir, |
190 | ) as $path) { | 196 | $rainTplDir . '/' . $conf->get('resource.theme'), |
197 | ] as $path | ||
198 | ) { | ||
191 | if (!is_readable(realpath($path))) { | 199 | if (!is_readable(realpath($path))) { |
192 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); | 200 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); |
193 | } | 201 | } |
194 | } | 202 | } |
195 | 203 | ||
196 | // Check cache and data directories are readable and writable | 204 | // Check cache and data directories are readable and writable |
197 | foreach (array( | 205 | if ($minimalMode) { |
198 | $conf->get('resource.thumbnails_cache'), | 206 | $folders = [ |
199 | $conf->get('resource.data_dir'), | 207 | $conf->get('resource.raintpl_tmp'), |
200 | $conf->get('resource.page_cache'), | 208 | ]; |
201 | $conf->get('resource.raintpl_tmp'), | 209 | } else { |
202 | ) as $path) { | 210 | $folders = [ |
211 | $conf->get('resource.thumbnails_cache'), | ||
212 | $conf->get('resource.data_dir'), | ||
213 | $conf->get('resource.page_cache'), | ||
214 | $conf->get('resource.raintpl_tmp'), | ||
215 | ]; | ||
216 | } | ||
217 | |||
218 | foreach ($folders as $path) { | ||
203 | if (!is_readable(realpath($path))) { | 219 | if (!is_readable(realpath($path))) { |
204 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); | 220 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); |
205 | } | 221 | } |
@@ -208,14 +224,20 @@ class ApplicationUtils | |||
208 | } | 224 | } |
209 | } | 225 | } |
210 | 226 | ||
227 | if ($minimalMode) { | ||
228 | return $errors; | ||
229 | } | ||
230 | |||
211 | // Check configuration files are readable and writable | 231 | // Check configuration files are readable and writable |
212 | foreach (array( | 232 | foreach ( |
213 | $conf->getConfigFileExt(), | 233 | [ |
214 | $conf->get('resource.datastore'), | 234 | $conf->getConfigFileExt(), |
215 | $conf->get('resource.ban_file'), | 235 | $conf->get('resource.datastore'), |
216 | $conf->get('resource.log'), | 236 | $conf->get('resource.ban_file'), |
217 | $conf->get('resource.update_check'), | 237 | $conf->get('resource.log'), |
218 | ) as $path) { | 238 | $conf->get('resource.update_check'), |
239 | ] as $path | ||
240 | ) { | ||
219 | if (!is_file(realpath($path))) { | 241 | if (!is_file(realpath($path))) { |
220 | # the file may not exist yet | 242 | # the file may not exist yet |
221 | continue; | 243 | continue; |
@@ -232,6 +254,20 @@ class ApplicationUtils | |||
232 | return $errors; | 254 | return $errors; |
233 | } | 255 | } |
234 | 256 | ||
257 | public static function checkDatastoreMutex(): array | ||
258 | { | ||
259 | $mutex = new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2); | ||
260 | try { | ||
261 | $mutex->synchronized(function () { | ||
262 | return true; | ||
263 | }); | ||
264 | } catch (LockAcquireException $e) { | ||
265 | $errors[] = t('Lock can not be acquired on the datastore. You might encounter concurrent access issues.'); | ||
266 | } | ||
267 | |||
268 | return $errors ?? []; | ||
269 | } | ||
270 | |||
235 | /** | 271 | /** |
236 | * Returns a salted hash representing the current Shaarli version. | 272 | * Returns a salted hash representing the current Shaarli version. |
237 | * | 273 | * |
@@ -246,4 +282,54 @@ class ApplicationUtils | |||
246 | { | 282 | { |
247 | return hash_hmac('sha256', $currentVersion, $salt); | 283 | return hash_hmac('sha256', $currentVersion, $salt); |
248 | } | 284 | } |
285 | |||
286 | /** | ||
287 | * Get a list of PHP extensions used by Shaarli. | ||
288 | * | ||
289 | * @return array[] List of extension with following keys: | ||
290 | * - name: extension name | ||
291 | * - required: whether the extension is required to use Shaarli | ||
292 | * - desc: short description of extension usage in Shaarli | ||
293 | * - loaded: whether the extension is properly loaded or not | ||
294 | */ | ||
295 | public static function getPhpExtensionsRequirement(): array | ||
296 | { | ||
297 | $extensions = [ | ||
298 | ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')], | ||
299 | ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')], | ||
300 | ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')], | ||
301 | ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')], | ||
302 | ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')], | ||
303 | ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')], | ||
304 | ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')], | ||
305 | ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')], | ||
306 | ]; | ||
307 | |||
308 | foreach ($extensions as &$extension) { | ||
309 | $extension['loaded'] = extension_loaded($extension['name']); | ||
310 | } | ||
311 | |||
312 | return $extensions; | ||
313 | } | ||
314 | |||
315 | /** | ||
316 | * Return the EOL date of given PHP version. If the version is unknown, | ||
317 | * we return today + 2 years. | ||
318 | * | ||
319 | * @param string $fullVersion PHP version, e.g. 7.4.7 | ||
320 | * | ||
321 | * @return string Date format: YYYY-MM-DD | ||
322 | */ | ||
323 | public static function getPhpEol(string $fullVersion): string | ||
324 | { | ||
325 | preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches); | ||
326 | |||
327 | return [ | ||
328 | '7.1' => '2019-12-01', | ||
329 | '7.2' => '2020-11-30', | ||
330 | '7.3' => '2021-12-06', | ||
331 | '7.4' => '2022-11-28', | ||
332 | '8.0' => '2023-12-01', | ||
333 | ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d'); | ||
334 | } | ||
249 | } | 335 | } |
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php new file mode 100644 index 00000000..05f95812 --- /dev/null +++ b/application/helper/DailyPageHelper.php | |||
@@ -0,0 +1,236 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Helper; | ||
6 | |||
7 | use DatePeriod; | ||
8 | use DateTimeImmutable; | ||
9 | use Exception; | ||
10 | use Shaarli\Bookmark\Bookmark; | ||
11 | use Slim\Http\Request; | ||
12 | |||
13 | class DailyPageHelper | ||
14 | { | ||
15 | public const MONTH = 'month'; | ||
16 | public const WEEK = 'week'; | ||
17 | public const DAY = 'day'; | ||
18 | |||
19 | /** | ||
20 | * Extracts the type of the daily to display from the HTTP request parameters | ||
21 | * | ||
22 | * @param Request $request HTTP request | ||
23 | * | ||
24 | * @return string month/week/day | ||
25 | */ | ||
26 | public static function extractRequestedType(Request $request): string | ||
27 | { | ||
28 | if ($request->getQueryParam(static::MONTH) !== null) { | ||
29 | return static::MONTH; | ||
30 | } elseif ($request->getQueryParam(static::WEEK) !== null) { | ||
31 | return static::WEEK; | ||
32 | } | ||
33 | |||
34 | return static::DAY; | ||
35 | } | ||
36 | |||
37 | /** | ||
38 | * Extracts a DateTimeImmutable from provided HTTP request. | ||
39 | * If no parameter is provided, we rely on the creation date of the latest provided created bookmark. | ||
40 | * If the datastore is empty or no bookmark is provided, we use the current date. | ||
41 | * | ||
42 | * @param string $type month/week/day | ||
43 | * @param string|null $requestedDate Input string extracted from the request | ||
44 | * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date) | ||
45 | * | ||
46 | * @return DateTimeImmutable from input or latest bookmark. | ||
47 | * | ||
48 | * @throws Exception Type not supported. | ||
49 | */ | ||
50 | public static function extractRequestedDateTime( | ||
51 | string $type, | ||
52 | ?string $requestedDate, | ||
53 | Bookmark $latestBookmark = null | ||
54 | ): DateTimeImmutable { | ||
55 | $format = static::getFormatByType($type); | ||
56 | if (empty($requestedDate)) { | ||
57 | return $latestBookmark instanceof Bookmark | ||
58 | ? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) | ||
59 | : new DateTimeImmutable() | ||
60 | ; | ||
61 | } | ||
62 | |||
63 | // W is not supported by createFromFormat... | ||
64 | if ($type === static::WEEK) { | ||
65 | return (new DateTimeImmutable()) | ||
66 | ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2)) | ||
67 | ; | ||
68 | } | ||
69 | |||
70 | return DateTimeImmutable::createFromFormat($format, $requestedDate); | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * Get the DateTime format used by provided type | ||
75 | * Examples: | ||
76 | * - day: 20201016 (<year><month><day>) | ||
77 | * - week: 202041 (<year><week number>) | ||
78 | * - month: 202010 (<year><month>) | ||
79 | * | ||
80 | * @param string $type month/week/day | ||
81 | * | ||
82 | * @return string DateTime compatible format | ||
83 | * | ||
84 | * @see https://www.php.net/manual/en/datetime.format.php | ||
85 | * | ||
86 | * @throws Exception Type not supported. | ||
87 | */ | ||
88 | public static function getFormatByType(string $type): string | ||
89 | { | ||
90 | switch ($type) { | ||
91 | case static::MONTH: | ||
92 | return 'Ym'; | ||
93 | case static::WEEK: | ||
94 | return 'YW'; | ||
95 | case static::DAY: | ||
96 | return 'Ymd'; | ||
97 | default: | ||
98 | throw new Exception('Unsupported daily format type'); | ||
99 | } | ||
100 | } | ||
101 | |||
102 | /** | ||
103 | * Get the first DateTime of the time period depending on given datetime and type. | ||
104 | * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax | ||
105 | * and we don't want to alter original datetime. | ||
106 | * | ||
107 | * @param string $type month/week/day | ||
108 | * @param DateTimeImmutable $requested DateTime extracted from request input | ||
109 | * (should come from extractRequestedDateTime) | ||
110 | * | ||
111 | * @return \DateTimeInterface First DateTime of the time period | ||
112 | * | ||
113 | * @throws Exception Type not supported. | ||
114 | */ | ||
115 | public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface | ||
116 | { | ||
117 | switch ($type) { | ||
118 | case static::MONTH: | ||
119 | return $requested->modify('first day of this month midnight'); | ||
120 | case static::WEEK: | ||
121 | return $requested->modify('Monday this week midnight'); | ||
122 | case static::DAY: | ||
123 | return $requested->modify('Today midnight'); | ||
124 | default: | ||
125 | throw new Exception('Unsupported daily format type'); | ||
126 | } | ||
127 | } | ||
128 | |||
129 | /** | ||
130 | * Get the last DateTime of the time period depending on given datetime and type. | ||
131 | * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax | ||
132 | * and we don't want to alter original datetime. | ||
133 | * | ||
134 | * @param string $type month/week/day | ||
135 | * @param DateTimeImmutable $requested DateTime extracted from request input | ||
136 | * (should come from extractRequestedDateTime) | ||
137 | * | ||
138 | * @return \DateTimeInterface Last DateTime of the time period | ||
139 | * | ||
140 | * @throws Exception Type not supported. | ||
141 | */ | ||
142 | public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface | ||
143 | { | ||
144 | switch ($type) { | ||
145 | case static::MONTH: | ||
146 | return $requested->modify('last day of this month 23:59:59'); | ||
147 | case static::WEEK: | ||
148 | return $requested->modify('Sunday this week 23:59:59'); | ||
149 | case static::DAY: | ||
150 | return $requested->modify('Today 23:59:59'); | ||
151 | default: | ||
152 | throw new Exception('Unsupported daily format type'); | ||
153 | } | ||
154 | } | ||
155 | |||
156 | /** | ||
157 | * Get localized description of the time period depending on given datetime and type. | ||
158 | * Example: for a month period, it returns `October, 2020`. | ||
159 | * | ||
160 | * @param string $type month/week/day | ||
161 | * @param \DateTimeImmutable $requested DateTime extracted from request input | ||
162 | * (should come from extractRequestedDateTime) | ||
163 | * @param bool $includeRelative Include relative date description (today, yesterday, etc.) | ||
164 | * | ||
165 | * @return string Localized time period description | ||
166 | * | ||
167 | * @throws Exception Type not supported. | ||
168 | */ | ||
169 | public static function getDescriptionByType( | ||
170 | string $type, | ||
171 | \DateTimeImmutable $requested, | ||
172 | bool $includeRelative = true | ||
173 | ): string { | ||
174 | switch ($type) { | ||
175 | case static::MONTH: | ||
176 | return $requested->format('F') . ', ' . $requested->format('Y'); | ||
177 | case static::WEEK: | ||
178 | $requested = $requested->modify('Monday this week'); | ||
179 | return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')'; | ||
180 | case static::DAY: | ||
181 | $out = ''; | ||
182 | if ($includeRelative && $requested->format('Ymd') === date('Ymd')) { | ||
183 | $out = t('Today') . ' - '; | ||
184 | } elseif ($includeRelative && $requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) { | ||
185 | $out = t('Yesterday') . ' - '; | ||
186 | } | ||
187 | return $out . format_date($requested, false); | ||
188 | default: | ||
189 | throw new Exception('Unsupported daily format type'); | ||
190 | } | ||
191 | } | ||
192 | |||
193 | /** | ||
194 | * Get the number of items to display in the RSS feed depending on the given type. | ||
195 | * | ||
196 | * @param string $type month/week/day | ||
197 | * | ||
198 | * @return int number of elements | ||
199 | * | ||
200 | * @throws Exception Type not supported. | ||
201 | */ | ||
202 | public static function getRssLengthByType(string $type): int | ||
203 | { | ||
204 | switch ($type) { | ||
205 | case static::MONTH: | ||
206 | return 12; // 1 year | ||
207 | case static::WEEK: | ||
208 | return 26; // ~6 months | ||
209 | case static::DAY: | ||
210 | return 30; // ~1 month | ||
211 | default: | ||
212 | throw new Exception('Unsupported daily format type'); | ||
213 | } | ||
214 | } | ||
215 | |||
216 | /** | ||
217 | * Get the number of items to display in the RSS feed depending on the given type. | ||
218 | * | ||
219 | * @param string $type month/week/day | ||
220 | * @param ?DateTimeImmutable $requested Currently only used for UT | ||
221 | * | ||
222 | * @return DatePeriod number of elements | ||
223 | * | ||
224 | * @throws Exception Type not supported. | ||
225 | */ | ||
226 | public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod | ||
227 | { | ||
228 | $requested = $requested ?? new DateTimeImmutable(); | ||
229 | |||
230 | return new DatePeriod( | ||
231 | static::getStartDateTimeByType($type, $requested), | ||
232 | new \DateInterval('P1D'), | ||
233 | static::getEndDateTimeByType($type, $requested) | ||
234 | ); | ||
235 | } | ||
236 | } | ||
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..cfc72583 --- /dev/null +++ b/application/http/MetadataRetriever.php | |||
@@ -0,0 +1,74 @@ | |||
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 array_map([$this, 'cleanMetadata'], [ | ||
64 | 'title' => $title, | ||
65 | 'description' => $description, | ||
66 | 'tags' => $tags, | ||
67 | ]); | ||
68 | } | ||
69 | |||
70 | protected function cleanMetadata($data): ?string | ||
71 | { | ||
72 | return !is_string($data) || empty(trim($data)) ? null : trim($data); | ||
73 | } | ||
74 | } | ||
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 da66dea3..7fc0cb04 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php | |||
@@ -1,8 +1,10 @@ | |||
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; |
5 | use Shaarli\Plugin\Exception\PluginFileNotFoundException; | 6 | use Shaarli\Plugin\Exception\PluginFileNotFoundException; |
7 | use Shaarli\Plugin\Exception\PluginInvalidRouteException; | ||
6 | 8 | ||
7 | /** | 9 | /** |
8 | * Class PluginManager | 10 | * Class PluginManager |
@@ -23,7 +25,15 @@ class PluginManager | |||
23 | * | 25 | * |
24 | * @var array $loadedPlugins | 26 | * @var array $loadedPlugins |
25 | */ | 27 | */ |
26 | private $loadedPlugins = array(); | 28 | private $loadedPlugins = []; |
29 | |||
30 | /** @var array List of registered routes. Contains keys: | ||
31 | * - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE | ||
32 | * - `route` (path): without prefix, e.g. `/up/{variable}` | ||
33 | * It will be later prefixed by `/plugin/<plugin name>/`. | ||
34 | * - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`. | ||
35 | */ | ||
36 | protected $registeredRoutes = []; | ||
27 | 37 | ||
28 | /** | 38 | /** |
29 | * @var ConfigManager Configuration Manager instance. | 39 | * @var ConfigManager Configuration Manager instance. |
@@ -57,7 +67,7 @@ class PluginManager | |||
57 | public function __construct(&$conf) | 67 | public function __construct(&$conf) |
58 | { | 68 | { |
59 | $this->conf = $conf; | 69 | $this->conf = $conf; |
60 | $this->errors = array(); | 70 | $this->errors = []; |
61 | } | 71 | } |
62 | 72 | ||
63 | /** | 73 | /** |
@@ -85,6 +95,9 @@ class PluginManager | |||
85 | $this->loadPlugin($dirs[$index], $plugin); | 95 | $this->loadPlugin($dirs[$index], $plugin); |
86 | } catch (PluginFileNotFoundException $e) { | 96 | } catch (PluginFileNotFoundException $e) { |
87 | error_log($e->getMessage()); | 97 | error_log($e->getMessage()); |
98 | } catch (\Throwable $e) { | ||
99 | $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage(); | ||
100 | $this->errors = array_unique(array_merge($this->errors, [$error])); | ||
88 | } | 101 | } |
89 | } | 102 | } |
90 | } | 103 | } |
@@ -98,7 +111,7 @@ class PluginManager | |||
98 | * | 111 | * |
99 | * @return void | 112 | * @return void |
100 | */ | 113 | */ |
101 | public function executeHooks($hook, &$data, $params = array()) | 114 | public function executeHooks($hook, &$data, $params = []) |
102 | { | 115 | { |
103 | $metadataParameters = [ | 116 | $metadataParameters = [ |
104 | 'target' => '_PAGE_', | 117 | 'target' => '_PAGE_', |
@@ -165,6 +178,22 @@ class PluginManager | |||
165 | } | 178 | } |
166 | } | 179 | } |
167 | 180 | ||
181 | $registerRouteFunction = $pluginName . '_register_routes'; | ||
182 | $routes = null; | ||
183 | if (function_exists($registerRouteFunction)) { | ||
184 | $routes = call_user_func($registerRouteFunction); | ||
185 | } | ||
186 | |||
187 | if ($routes !== null) { | ||
188 | foreach ($routes as $route) { | ||
189 | if (static::validateRouteRegistration($route)) { | ||
190 | $this->registeredRoutes[$pluginName][] = $route; | ||
191 | } else { | ||
192 | throw new PluginInvalidRouteException($pluginName); | ||
193 | } | ||
194 | } | ||
195 | } | ||
196 | |||
168 | $this->loadedPlugins[] = $pluginName; | 197 | $this->loadedPlugins[] = $pluginName; |
169 | } | 198 | } |
170 | 199 | ||
@@ -196,7 +225,7 @@ class PluginManager | |||
196 | */ | 225 | */ |
197 | public function getPluginsMeta() | 226 | public function getPluginsMeta() |
198 | { | 227 | { |
199 | $metaData = array(); | 228 | $metaData = []; |
200 | $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK); | 229 | $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK); |
201 | 230 | ||
202 | // Browse all plugin directories. | 231 | // Browse all plugin directories. |
@@ -217,9 +246,9 @@ class PluginManager | |||
217 | if (isset($metaData[$plugin]['parameters'])) { | 246 | if (isset($metaData[$plugin]['parameters'])) { |
218 | $params = explode(';', $metaData[$plugin]['parameters']); | 247 | $params = explode(';', $metaData[$plugin]['parameters']); |
219 | } else { | 248 | } else { |
220 | $params = array(); | 249 | $params = []; |
221 | } | 250 | } |
222 | $metaData[$plugin]['parameters'] = array(); | 251 | $metaData[$plugin]['parameters'] = []; |
223 | foreach ($params as $param) { | 252 | foreach ($params as $param) { |
224 | if (empty($param)) { | 253 | if (empty($param)) { |
225 | continue; | 254 | continue; |
@@ -237,6 +266,14 @@ class PluginManager | |||
237 | } | 266 | } |
238 | 267 | ||
239 | /** | 268 | /** |
269 | * @return array List of registered custom routes by plugins. | ||
270 | */ | ||
271 | public function getRegisteredRoutes(): array | ||
272 | { | ||
273 | return $this->registeredRoutes; | ||
274 | } | ||
275 | |||
276 | /** | ||
240 | * Return the list of encountered errors. | 277 | * Return the list of encountered errors. |
241 | * | 278 | * |
242 | * @return array List of errors (empty array if none exists). | 279 | * @return array List of errors (empty array if none exists). |
@@ -245,4 +282,32 @@ class PluginManager | |||
245 | { | 282 | { |
246 | return $this->errors; | 283 | return $this->errors; |
247 | } | 284 | } |
285 | |||
286 | /** | ||
287 | * Checks whether provided input is valid to register a new route. | ||
288 | * It must contain keys `method`, `route`, `callable` (all strings). | ||
289 | * | ||
290 | * @param string[] $input | ||
291 | * | ||
292 | * @return bool | ||
293 | */ | ||
294 | protected static function validateRouteRegistration(array $input): bool | ||
295 | { | ||
296 | if ( | ||
297 | !array_key_exists('method', $input) | ||
298 | || !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) | ||
299 | ) { | ||
300 | return false; | ||
301 | } | ||
302 | |||
303 | if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) { | ||
304 | return false; | ||
305 | } | ||
306 | |||
307 | if (!array_key_exists('callable', $input)) { | ||
308 | return false; | ||
309 | } | ||
310 | |||
311 | return true; | ||
312 | } | ||
248 | } | 313 | } |
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/plugin/exception/PluginInvalidRouteException.php b/application/plugin/exception/PluginInvalidRouteException.php new file mode 100644 index 00000000..6ba9bc43 --- /dev/null +++ b/application/plugin/exception/PluginInvalidRouteException.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Plugin\Exception; | ||
6 | |||
7 | use Exception; | ||
8 | |||
9 | /** | ||
10 | * Class PluginFileNotFoundException | ||
11 | * | ||
12 | * Raise when plugin files can't be found. | ||
13 | */ | ||
14 | class PluginInvalidRouteException extends Exception | ||
15 | { | ||
16 | /** | ||
17 | * Construct exception with plugin name. | ||
18 | * Generate message. | ||
19 | * | ||
20 | * @param string $pluginName name of the plugin not found | ||
21 | */ | ||
22 | public function __construct() | ||
23 | { | ||
24 | $this->message = 'trying to register invalid route.'; | ||
25 | } | ||
26 | } | ||
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 2d6d2dbe..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); |
diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php index 97805c35..fe74bf27 100644 --- a/application/render/PageCacheManager.php +++ b/application/render/PageCacheManager.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Render; | 3 | namespace Shaarli\Render; |
4 | 4 | ||
5 | use DatePeriod; | ||
5 | use Shaarli\Feed\CachedPage; | 6 | use Shaarli\Feed\CachedPage; |
6 | 7 | ||
7 | /** | 8 | /** |
@@ -49,12 +50,21 @@ class PageCacheManager | |||
49 | $this->purgeCachedPages(); | 50 | $this->purgeCachedPages(); |
50 | } | 51 | } |
51 | 52 | ||
52 | public function getCachePage(string $pageUrl): CachedPage | 53 | /** |
54 | * Get CachedPage instance for provided URL. | ||
55 | * | ||
56 | * @param string $pageUrl | ||
57 | * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache | ||
58 | * | ||
59 | * @return CachedPage | ||
60 | */ | ||
61 | public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage | ||
53 | { | 62 | { |
54 | return new CachedPage( | 63 | return new CachedPage( |
55 | $this->pageCacheDir, | 64 | $this->pageCacheDir, |
56 | $pageUrl, | 65 | $pageUrl, |
57 | false === $this->isLoggedIn | 66 | false === $this->isLoggedIn, |
67 | $validityPeriod | ||
58 | ); | 68 | ); |
59 | } | 69 | } |
60 | } | 70 | } |
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 65048f10..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(); |
@@ -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 aadffc13..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-tag').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 2f49bbd2..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 { |
@@ -1023,6 +1033,10 @@ body, | |||
1023 | &.button-red { | 1033 | &.button-red { |
1024 | background: $red; | 1034 | background: $red; |
1025 | } | 1035 | } |
1036 | |||
1037 | &.button-grey { | ||
1038 | background: $light-grey; | ||
1039 | } | ||
1026 | } | 1040 | } |
1027 | 1041 | ||
1028 | .submit-buttons { | 1042 | .submit-buttons { |
@@ -1047,7 +1061,7 @@ body, | |||
1047 | } | 1061 | } |
1048 | 1062 | ||
1049 | table { | 1063 | table { |
1050 | margin: auto; | 1064 | margin: 10px auto 25px auto; |
1051 | width: 90%; | 1065 | width: 90%; |
1052 | 1066 | ||
1053 | .order { | 1067 | .order { |
@@ -1083,6 +1097,11 @@ body, | |||
1083 | position: absolute; | 1097 | position: absolute; |
1084 | right: 5%; | 1098 | right: 5%; |
1085 | } | 1099 | } |
1100 | |||
1101 | &.button-grey { | ||
1102 | position: absolute; | ||
1103 | left: 5%; | ||
1104 | } | ||
1086 | } | 1105 | } |
1087 | } | 1106 | } |
1088 | } | 1107 | } |
@@ -1257,11 +1276,15 @@ form { | |||
1257 | margin: 70px 0 25px; | 1276 | margin: 70px 0 25px; |
1258 | } | 1277 | } |
1259 | 1278 | ||
1279 | a { | ||
1280 | color: var(--main-color); | ||
1281 | } | ||
1282 | |||
1260 | pre { | 1283 | pre { |
1261 | margin: 0 20%; | 1284 | margin: 0 20%; |
1262 | padding: 20px 0; | 1285 | padding: 20px 0; |
1263 | text-align: left; | 1286 | text-align: left; |
1264 | line-height: .7em; | 1287 | line-height: 1em; |
1265 | } | 1288 | } |
1266 | } | 1289 | } |
1267 | 1290 | ||
@@ -1273,6 +1296,57 @@ form { | |||
1273 | } | 1296 | } |
1274 | } | 1297 | } |
1275 | 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 | |||
1276 | // LOGIN | 1350 | // LOGIN |
1277 | .login-form-container { | 1351 | .login-form-container { |
1278 | .remember-me { | 1352 | .remember-me { |
@@ -1645,6 +1719,123 @@ form { | |||
1645 | } | 1719 | } |
1646 | } | 1720 | } |
1647 | 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 | |||
1648 | // Print rules | 1839 | // Print rules |
1649 | @media print { | 1840 | @media print { |
1650 | .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 c0855e47..138319ca 100644 --- a/composer.json +++ b/composer.json | |||
@@ -23,9 +23,10 @@ | |||
23 | "erusev/parsedown": "^1.6", | 23 | "erusev/parsedown": "^1.6", |
24 | "erusev/parsedown-extra": "^0.8.1", | 24 | "erusev/parsedown-extra": "^0.8.1", |
25 | "gettext/gettext": "^4.4", | 25 | "gettext/gettext": "^4.4", |
26 | "katzgrau/klogger": "^1.2", | ||
26 | "malkusch/lock": "^2.1", | 27 | "malkusch/lock": "^2.1", |
27 | "pubsubhubbub/publisher": "dev-master", | 28 | "pubsubhubbub/publisher": "dev-master", |
28 | "shaarli/netscape-bookmark-parser": "^2.1", | 29 | "shaarli/netscape-bookmark-parser": "^3.0", |
29 | "slim/slim": "^3.0" | 30 | "slim/slim": "^3.0" |
30 | }, | 31 | }, |
31 | "require-dev": { | 32 | "require-dev": { |
@@ -58,6 +59,7 @@ | |||
58 | "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin", | 59 | "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin", |
59 | "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor", | 60 | "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor", |
60 | "Shaarli\\Front\\Exception\\": "application/front/exceptions", | 61 | "Shaarli\\Front\\Exception\\": "application/front/exceptions", |
62 | "Shaarli\\Helper\\": "application/helper", | ||
61 | "Shaarli\\Http\\": "application/http", | 63 | "Shaarli\\Http\\": "application/http", |
62 | "Shaarli\\Legacy\\": "application/legacy", | 64 | "Shaarli\\Legacy\\": "application/legacy", |
63 | "Shaarli\\Netscape\\": "application/netscape", | 65 | "Shaarli\\Netscape\\": "application/netscape", |
diff --git a/composer.lock b/composer.lock index c379d8e7..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": "932b191006135ff8be495aa0b4ba7e09", | 7 | "content-hash": "83852dec81e299a117a81206a5091472", |
8 | "packages": [ | 8 | "packages": [ |
9 | { | 9 | { |
10 | "name": "arthurhoaro/web-thumbnailer", | 10 | "name": "arthurhoaro/web-thumbnailer", |
@@ -786,24 +786,25 @@ | |||
786 | }, | 786 | }, |
787 | { | 787 | { |
788 | "name": "shaarli/netscape-bookmark-parser", | 788 | "name": "shaarli/netscape-bookmark-parser", |
789 | "version": "v2.2.0", | 789 | "version": "v3.0.1", |
790 | "source": { | 790 | "source": { |
791 | "type": "git", | 791 | "type": "git", |
792 | "url": "https://github.com/shaarli/netscape-bookmark-parser.git", | 792 | "url": "https://github.com/shaarli/netscape-bookmark-parser.git", |
793 | "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df" | 793 | "reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305" |
794 | }, | 794 | }, |
795 | "dist": { | 795 | "dist": { |
796 | "type": "zip", | 796 | "type": "zip", |
797 | "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", |
798 | "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df", | 798 | "reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305", |
799 | "shasum": "" | 799 | "shasum": "" |
800 | }, | 800 | }, |
801 | "require": { | 801 | "require": { |
802 | "katzgrau/klogger": "~1.0", | 802 | "katzgrau/klogger": "~1.0", |
803 | "php": ">=5.6" | 803 | "php": ">=7.1" |
804 | }, | 804 | }, |
805 | "require-dev": { | 805 | "require-dev": { |
806 | "phpunit/phpunit": "^5.0" | 806 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", |
807 | "squizlabs/php_codesniffer": "^3.5" | ||
807 | }, | 808 | }, |
808 | "type": "library", | 809 | "type": "library", |
809 | "autoload": { | 810 | "autoload": { |
@@ -839,9 +840,9 @@ | |||
839 | ], | 840 | ], |
840 | "support": { | 841 | "support": { |
841 | "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues", | 842 | "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues", |
842 | "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" |
843 | }, | 844 | }, |
844 | "time": "2020-06-06T15:53:53+00:00" | 845 | "time": "2020-11-03T12:27:58+00:00" |
845 | }, | 846 | }, |
846 | { | 847 | { |
847 | "name": "slim/slim", | 848 | "name": "slim/slim", |
@@ -1713,12 +1714,12 @@ | |||
1713 | "source": { | 1714 | "source": { |
1714 | "type": "git", | 1715 | "type": "git", |
1715 | "url": "https://github.com/Roave/SecurityAdvisories.git", | 1716 | "url": "https://github.com/Roave/SecurityAdvisories.git", |
1716 | "reference": "ba5d234b3a1559321b816b64aafc2ce6728799ff" | 1717 | "reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6" |
1717 | }, | 1718 | }, |
1718 | "dist": { | 1719 | "dist": { |
1719 | "type": "zip", | 1720 | "type": "zip", |
1720 | "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ba5d234b3a1559321b816b64aafc2ce6728799ff", | 1721 | "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/065a018d3b5c2c84a53db3347cca4e1b7fa362a6", |
1721 | "reference": "ba5d234b3a1559321b816b64aafc2ce6728799ff", | 1722 | "reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6", |
1722 | "shasum": "" | 1723 | "shasum": "" |
1723 | }, | 1724 | }, |
1724 | "conflict": { | 1725 | "conflict": { |
@@ -1734,7 +1735,7 @@ | |||
1734 | "bagisto/bagisto": "<0.1.5", | 1735 | "bagisto/bagisto": "<0.1.5", |
1735 | "barrelstrength/sprout-base-email": "<1.2.7", | 1736 | "barrelstrength/sprout-base-email": "<1.2.7", |
1736 | "barrelstrength/sprout-forms": "<3.9", | 1737 | "barrelstrength/sprout-forms": "<3.9", |
1737 | "baserproject/basercms": ">=4,<=4.3.6", | 1738 | "baserproject/basercms": ">=4,<=4.3.6|>=4.4,<4.4.1", |
1738 | "bolt/bolt": "<3.7.1", | 1739 | "bolt/bolt": "<3.7.1", |
1739 | "brightlocal/phpwhois": "<=4.2.5", | 1740 | "brightlocal/phpwhois": "<=4.2.5", |
1740 | "buddypress/buddypress": "<5.1.2", | 1741 | "buddypress/buddypress": "<5.1.2", |
@@ -1818,6 +1819,7 @@ | |||
1818 | "magento/magento1ee": ">=1,<1.14.4.3", | 1819 | "magento/magento1ee": ">=1,<1.14.4.3", |
1819 | "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", |
1820 | "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", | ||
1821 | "mittwald/typo3_forum": "<1.2.1", | 1823 | "mittwald/typo3_forum": "<1.2.1", |
1822 | "monolog/monolog": ">=1.8,<1.12", | 1824 | "monolog/monolog": ">=1.8,<1.12", |
1823 | "namshi/jose": "<2.2", | 1825 | "namshi/jose": "<2.2", |
@@ -1832,7 +1834,8 @@ | |||
1832 | "onelogin/php-saml": "<2.10.4", | 1834 | "onelogin/php-saml": "<2.10.4", |
1833 | "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5", | 1835 | "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5", |
1834 | "openid/php-openid": "<2.3", | 1836 | "openid/php-openid": "<2.3", |
1835 | "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", | ||
1836 | "oro/crm": ">=1.7,<1.7.4", | 1839 | "oro/crm": ">=1.7,<1.7.4", |
1837 | "oro/platform": ">=1.7,<1.7.4", | 1840 | "oro/platform": ">=1.7,<1.7.4", |
1838 | "padraic/humbug_get_contents": "<1.1.2", | 1841 | "padraic/humbug_get_contents": "<1.1.2", |
@@ -1867,8 +1870,8 @@ | |||
1867 | "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", | 1870 | "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", |
1868 | "sensiolabs/connect": "<4.2.3", | 1871 | "sensiolabs/connect": "<4.2.3", |
1869 | "serluck/phpwhois": "<=4.2.6", | 1872 | "serluck/phpwhois": "<=4.2.6", |
1870 | "shopware/core": "<=6.3.1", | 1873 | "shopware/core": "<=6.3.2", |
1871 | "shopware/platform": "<=6.3.1", | 1874 | "shopware/platform": "<=6.3.2", |
1872 | "shopware/shopware": "<5.3.7", | 1875 | "shopware/shopware": "<5.3.7", |
1873 | "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", |
1874 | "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2", | 1877 | "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2", |
@@ -1901,7 +1904,7 @@ | |||
1901 | "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", |
1902 | "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", |
1903 | "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", |
1904 | "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", |
1905 | "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", | 1908 | "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", |
1906 | "symbiote/silverstripe-versionedfiles": "<=2.0.3", | 1909 | "symbiote/silverstripe-versionedfiles": "<=2.0.3", |
1907 | "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", |
@@ -2018,7 +2021,7 @@ | |||
2018 | "type": "tidelift" | 2021 | "type": "tidelift" |
2019 | } | 2022 | } |
2020 | ], | 2023 | ], |
2021 | "time": "2020-10-08T21:02:27+00:00" | 2024 | "time": "2020-11-01T20:01:47+00:00" |
2022 | }, | 2025 | }, |
2023 | { | 2026 | { |
2024 | "name": "sebastian/code-unit-reverse-lookup", | 2027 | "name": "sebastian/code-unit-reverse-lookup", |
@@ -2632,16 +2635,16 @@ | |||
2632 | }, | 2635 | }, |
2633 | { | 2636 | { |
2634 | "name": "squizlabs/php_codesniffer", | 2637 | "name": "squizlabs/php_codesniffer", |
2635 | "version": "3.5.6", | 2638 | "version": "3.5.8", |
2636 | "source": { | 2639 | "source": { |
2637 | "type": "git", | 2640 | "type": "git", |
2638 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", | 2641 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", |
2639 | "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" | 2642 | "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" |
2640 | }, | 2643 | }, |
2641 | "dist": { | 2644 | "dist": { |
2642 | "type": "zip", | 2645 | "type": "zip", |
2643 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", | 2646 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", |
2644 | "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", | 2647 | "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", |
2645 | "shasum": "" | 2648 | "shasum": "" |
2646 | }, | 2649 | }, |
2647 | "require": { | 2650 | "require": { |
@@ -2684,24 +2687,24 @@ | |||
2684 | "source": "https://github.com/squizlabs/PHP_CodeSniffer", | 2687 | "source": "https://github.com/squizlabs/PHP_CodeSniffer", |
2685 | "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" | 2688 | "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" |
2686 | }, | 2689 | }, |
2687 | "time": "2020-08-10T04:50:15+00:00" | 2690 | "time": "2020-10-23T02:01:07+00:00" |
2688 | }, | 2691 | }, |
2689 | { | 2692 | { |
2690 | "name": "symfony/polyfill-ctype", | 2693 | "name": "symfony/polyfill-ctype", |
2691 | "version": "v1.18.1", | 2694 | "version": "v1.20.0", |
2692 | "source": { | 2695 | "source": { |
2693 | "type": "git", | 2696 | "type": "git", |
2694 | "url": "https://github.com/symfony/polyfill-ctype.git", | 2697 | "url": "https://github.com/symfony/polyfill-ctype.git", |
2695 | "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" | 2698 | "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" |
2696 | }, | 2699 | }, |
2697 | "dist": { | 2700 | "dist": { |
2698 | "type": "zip", | 2701 | "type": "zip", |
2699 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", | 2702 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", |
2700 | "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", | 2703 | "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", |
2701 | "shasum": "" | 2704 | "shasum": "" |
2702 | }, | 2705 | }, |
2703 | "require": { | 2706 | "require": { |
2704 | "php": ">=5.3.3" | 2707 | "php": ">=7.1" |
2705 | }, | 2708 | }, |
2706 | "suggest": { | 2709 | "suggest": { |
2707 | "ext-ctype": "For best performance" | 2710 | "ext-ctype": "For best performance" |
@@ -2709,7 +2712,7 @@ | |||
2709 | "type": "library", | 2712 | "type": "library", |
2710 | "extra": { | 2713 | "extra": { |
2711 | "branch-alias": { | 2714 | "branch-alias": { |
2712 | "dev-master": "1.18-dev" | 2715 | "dev-main": "1.20-dev" |
2713 | }, | 2716 | }, |
2714 | "thanks": { | 2717 | "thanks": { |
2715 | "name": "symfony/polyfill", | 2718 | "name": "symfony/polyfill", |
@@ -2747,7 +2750,7 @@ | |||
2747 | "portable" | 2750 | "portable" |
2748 | ], | 2751 | ], |
2749 | "support": { | 2752 | "support": { |
2750 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.18.0" | 2753 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" |
2751 | }, | 2754 | }, |
2752 | "funding": [ | 2755 | "funding": [ |
2753 | { | 2756 | { |
@@ -2763,7 +2766,7 @@ | |||
2763 | "type": "tidelift" | 2766 | "type": "tidelift" |
2764 | } | 2767 | } |
2765 | ], | 2768 | ], |
2766 | "time": "2020-07-14T12:35:20+00:00" | 2769 | "time": "2020-10-23T14:02:19+00:00" |
2767 | }, | 2770 | }, |
2768 | { | 2771 | { |
2769 | "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/REST-API.md b/doc/md/REST-API.md index 01071d8e..2a36ea29 100644 --- a/doc/md/REST-API.md +++ b/doc/md/REST-API.md | |||
@@ -73,7 +73,7 @@ var_dump(getInfo($baseUrl, $secret)); | |||
73 | ### Authentication | 73 | ### Authentication |
74 | 74 | ||
75 | - All requests to Shaarli's API must include a **JWT token** to verify their authenticity. | 75 | - All requests to Shaarli's API must include a **JWT token** to verify their authenticity. |
76 | - This token must be included as an HTTP header called `Authentication: Bearer <jwt token>`. | 76 | - This token must be included as an HTTP header called `Authorization: Bearer <jwt token>`. |
77 | - JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64: | 77 | - JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64: |
78 | 78 | ||
79 | ``` | 79 | ``` |
diff --git a/doc/md/Server-configuration.md b/doc/md/Server-configuration.md index 8cb39934..a49b6033 100644 --- a/doc/md/Server-configuration.md +++ b/doc/md/Server-configuration.md | |||
@@ -193,19 +193,24 @@ sudo nano /etc/apache2/sites-available/shaarli.mydomain.org.conf | |||
193 | Require all granted | 193 | Require all granted |
194 | </Directory> | 194 | </Directory> |
195 | 195 | ||
196 | <LocationMatch "/\."> | 196 | # BE CAREFUL: directives order matter! |
197 | # Prevent accessing dotfiles | ||
198 | RedirectMatch 404 ".*" | ||
199 | </LocationMatch> | ||
200 | 197 | ||
201 | <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)$"> | ||
202 | # allow client-side caching of static files | 207 | # allow client-side caching of static files |
203 | 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" |
204 | </LocationMatch> | 209 | </FilesMatch> |
210 | |||
205 | 211 | ||
206 | # serve the Shaarli favicon from its custom location | 212 | # serve the Shaarli favicon from its custom location |
207 | Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico | 213 | Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico |
208 | |||
209 | </VirtualHost> | 214 | </VirtualHost> |
210 | ``` | 215 | ``` |
211 | 216 | ||
@@ -296,7 +301,7 @@ server { | |||
296 | location / { | 301 | location / { |
297 | # default index file when no file URI is requested | 302 | # default index file when no file URI is requested |
298 | index index.php; | 303 | index index.php; |
299 | try_files $uri /index.php$is_args$args; | 304 | try_files _ /index.php$is_args$args; |
300 | } | 305 | } |
301 | 306 | ||
302 | location ~ (index)\.php$ { | 307 | location ~ (index)\.php$ { |
@@ -309,20 +314,9 @@ server { | |||
309 | include fastcgi.conf; | 314 | include fastcgi.conf; |
310 | } | 315 | } |
311 | 316 | ||
312 | location ~ \.php$ { | 317 | location ~ /doc/html/ { |
313 | # deny access to all other PHP scripts | 318 | default_type "text/html"; |
314 | # disable this if you host other PHP applications on the same virtualhost | 319 | try_files $uri $uri/ $uri.html =404; |
315 | deny all; | ||
316 | } | ||
317 | |||
318 | location ~ /\. { | ||
319 | # deny access to dotfiles | ||
320 | deny all; | ||
321 | } | ||
322 | |||
323 | location ~ ~$ { | ||
324 | # deny access to temp editor files, e.g. "script.php~" | ||
325 | deny all; | ||
326 | } | 320 | } |
327 | 321 | ||
328 | location = /favicon.ico { | 322 | location = /favicon.ico { |
@@ -331,13 +325,12 @@ server { | |||
331 | } | 325 | } |
332 | 326 | ||
333 | # allow client-side caching of static files | 327 | # allow client-side caching of static files |
334 | location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { | 328 | location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$ { |
335 | expires max; | 329 | expires max; |
336 | add_header Cache-Control "public, must-revalidate, proxy-revalidate"; | 330 | add_header Cache-Control "public, must-revalidate, proxy-revalidate"; |
337 | # HTTP 1.0 compatibility | 331 | # HTTP 1.0 compatibility |
338 | add_header Pragma public; | 332 | add_header Pragma public; |
339 | } | 333 | } |
340 | |||
341 | } | 334 | } |
342 | ``` | 335 | ``` |
343 | 336 | ||
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 f09fadc2..79654011 100644 --- a/doc/md/dev/Plugin-system.md +++ b/doc/md/dev/Plugin-system.md | |||
@@ -139,6 +139,31 @@ Each file contain two keys: | |||
139 | 139 | ||
140 | > Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file. | 140 | > Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file. |
141 | 141 | ||
142 | ### Register plugin's routes | ||
143 | |||
144 | Shaarli lets you register custom Slim routes for your plugin. | ||
145 | |||
146 | To register a route, the plugin must include a function called `function <plugin_name>_register_routes(): array`. | ||
147 | |||
148 | This method must return an array of routes, each entry must contain the following keys: | ||
149 | |||
150 | - `method`: HTTP method, `GET/POST/PUT/PATCH/DELETE` | ||
151 | - `route` (path): without prefix, e.g. `/up/{variable}` | ||
152 | It will be later prefixed by `/plugin/<plugin name>/`. | ||
153 | - `callable` string, function name or FQN class's method to execute, e.g. `demo_plugin_custom_controller`. | ||
154 | |||
155 | Callable functions or methods must have `Slim\Http\Request` and `Slim\Http\Response` parameters | ||
156 | and return a `Slim\Http\Response`. We recommend creating a dedicated class and extend either | ||
157 | `ShaarliVisitorController` or `ShaarliAdminController` to use helper functions they provide. | ||
158 | |||
159 | A dedicated plugin template is available for rendering content: `pluginscontent.html` using `content` placeholder. | ||
160 | |||
161 | > **Warning**: plugins are not able to use RainTPL template engine for their content due to technical restrictions. | ||
162 | > RainTPL does not allow to register multiple template folders, so all HTML rendering must be done within plugin | ||
163 | > custom controller. | ||
164 | |||
165 | Check out the `demo_plugin` for a live example: `GET <shaarli_url>/plugin/demo_plugin/custom`. | ||
166 | |||
142 | ### Understanding relative paths | 167 | ### Understanding relative paths |
143 | 168 | ||
144 | Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder. | 169 | Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder. |
diff --git a/doc/md/dev/Release-Shaarli.md b/doc/md/dev/Release-Shaarli.md index 2c772406..d79be9ce 100644 --- a/doc/md/dev/Release-Shaarli.md +++ b/doc/md/dev/Release-Shaarli.md | |||
@@ -64,6 +64,14 @@ git pull upstream master | |||
64 | 64 | ||
65 | # If releasing a new minor version, create a release branch | 65 | # If releasing a new minor version, create a release branch |
66 | $ git checkout -b v0.x | 66 | $ git checkout -b v0.x |
67 | # Otherwise just use the existing one | ||
68 | $ git checkout v0.x | ||
69 | |||
70 | # Get the latest changes | ||
71 | $ git merge master | ||
72 | |||
73 | # Check that everything went fine: | ||
74 | $ make test | ||
67 | 75 | ||
68 | # Bump shaarli_version.php from dev to 0.x.0, **without the v** | 76 | # Bump shaarli_version.php from dev to 0.x.0, **without the v** |
69 | $ vim shaarli_version.php | 77 | $ vim shaarli_version.php |
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 f7baedfb..01492af4 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-10-16 20:01+0200\n" | 4 | "POT-Creation-Date: 2020-11-24 13:13+0100\n" |
5 | "PO-Revision-Date: 2020-10-16 20:02+0200\n" | 5 | "PO-Revision-Date: 2020-11-24 13:14+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,58 +20,31 @@ 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:181 |
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:192 |
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 | ||
58 | #: application/Languages.php:181 | 31 | #: application/Languages.php:184 |
59 | msgid "Automatic" | 32 | msgid "Automatic" |
60 | msgstr "Automatique" | 33 | msgstr "Automatique" |
61 | 34 | ||
62 | #: application/Languages.php:182 | 35 | #: application/Languages.php:185 |
63 | msgid "German" | 36 | msgid "German" |
64 | msgstr "Allemand" | 37 | msgstr "Allemand" |
65 | 38 | ||
66 | #: application/Languages.php:183 | 39 | #: application/Languages.php:186 |
67 | msgid "English" | 40 | msgid "English" |
68 | msgstr "Anglais" | 41 | msgstr "Anglais" |
69 | 42 | ||
70 | #: application/Languages.php:184 | 43 | #: application/Languages.php:187 |
71 | msgid "French" | 44 | msgid "French" |
72 | msgstr "Français" | 45 | msgstr "Français" |
73 | 46 | ||
74 | #: application/Languages.php:185 | 47 | #: application/Languages.php:188 |
75 | msgid "Japanese" | 48 | msgid "Japanese" |
76 | msgstr "Japonais" | 49 | msgstr "Japonais" |
77 | 50 | ||
@@ -83,46 +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:405 |
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:412 |
91 | msgid "Unlimited" | 64 | msgid "Unlimited" |
92 | msgstr "Illimité" | 65 | msgstr "Illimité" |
93 | 66 | ||
94 | #: application/Utils.php:393 | 67 | #: application/Utils.php:415 |
95 | msgid "B" | 68 | msgid "B" |
96 | msgstr "o" | 69 | msgstr "o" |
97 | 70 | ||
98 | #: application/Utils.php:393 | 71 | #: application/Utils.php:415 |
99 | msgid "kiB" | 72 | msgid "kiB" |
100 | msgstr "ko" | 73 | msgstr "ko" |
101 | 74 | ||
102 | #: application/Utils.php:393 | 75 | #: application/Utils.php:415 |
103 | msgid "MiB" | 76 | msgid "MiB" |
104 | msgstr "Mo" | 77 | msgstr "Mo" |
105 | 78 | ||
106 | #: application/Utils.php:393 | 79 | #: application/Utils.php:415 |
107 | msgid "GiB" | 80 | msgid "GiB" |
108 | msgstr "Go" | 81 | msgstr "Go" |
109 | 82 | ||
110 | #: application/bookmark/BookmarkFileService.php:180 | 83 | #: application/bookmark/BookmarkFileService.php:185 |
111 | #: application/bookmark/BookmarkFileService.php:202 | 84 | #: application/bookmark/BookmarkFileService.php:207 |
112 | #: application/bookmark/BookmarkFileService.php:224 | 85 | #: application/bookmark/BookmarkFileService.php:229 |
113 | #: application/bookmark/BookmarkFileService.php:238 | 86 | #: application/bookmark/BookmarkFileService.php:243 |
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:205 | 90 | #: application/bookmark/BookmarkFileService.php:210 |
118 | msgid "This bookmarks already exists" | 91 | msgid "This bookmarks already exists" |
119 | msgstr "Ce marque-page existe déjà." | 92 | msgstr "Ce marque-page existe déjà" |
120 | 93 | ||
121 | #: application/bookmark/BookmarkInitializer.php:39 | 94 | #: application/bookmark/BookmarkInitializer.php:42 |
122 | msgid "(private bookmark with thumbnail demo)" | 95 | msgid "(private bookmark with thumbnail demo)" |
123 | msgstr "(marque page privé avec une miniature)" | 96 | msgstr "(marque page privé avec une miniature)" |
124 | 97 | ||
125 | #: application/bookmark/BookmarkInitializer.php:42 | 98 | #: application/bookmark/BookmarkInitializer.php:45 |
126 | msgid "" | 99 | msgid "" |
127 | "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 " |
128 | "websites.\n" | 101 | "websites.\n" |
@@ -145,11 +118,11 @@ msgstr "" | |||
145 | "\n" | 118 | "\n" |
146 | "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" |
147 | 120 | ||
148 | #: application/bookmark/BookmarkInitializer.php:55 | 121 | #: application/bookmark/BookmarkInitializer.php:58 |
149 | msgid "Note: Shaare descriptions" | 122 | msgid "Note: Shaare descriptions" |
150 | msgstr "Note : Description des Shaares" | 123 | msgstr "Note : Description des Shaares" |
151 | 124 | ||
152 | #: application/bookmark/BookmarkInitializer.php:57 | 125 | #: application/bookmark/BookmarkInitializer.php:60 |
153 | msgid "" | 126 | msgid "" |
154 | "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 " |
155 | "such as this one.\n" | 128 | "such as this one.\n" |
@@ -213,19 +186,19 @@ msgstr "" | |||
213 | "| Citron | Fruit | Jaune | 30 |\n" | 186 | "| Citron | Fruit | Jaune | 30 |\n" |
214 | "| Carotte | Légume | Orange | 14 |\n" | 187 | "| Carotte | Légume | Orange | 14 |\n" |
215 | 188 | ||
216 | #: application/bookmark/BookmarkInitializer.php:91 | 189 | #: application/bookmark/BookmarkInitializer.php:94 |
217 | #: application/legacy/LegacyLinkDB.php:246 | 190 | #: application/legacy/LegacyLinkDB.php:246 |
218 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | 191 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 |
219 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | 192 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 |
220 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 | 193 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 |
221 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49 | 194 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 |
222 | msgid "" | 195 | msgid "" |
223 | "The personal, minimalist, super-fast, database free, bookmarking service" | 196 | "The personal, minimalist, super-fast, database free, bookmarking service" |
224 | msgstr "" | 197 | msgstr "" |
225 | "Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " | 198 | "Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " |
226 | "données" | 199 | "données" |
227 | 200 | ||
228 | #: application/bookmark/BookmarkInitializer.php:94 | 201 | #: application/bookmark/BookmarkInitializer.php:97 |
229 | msgid "" | 202 | msgid "" |
230 | "Welcome to Shaarli!\n" | 203 | "Welcome to Shaarli!\n" |
231 | "\n" | 204 | "\n" |
@@ -274,11 +247,11 @@ msgstr "" | |||
274 | "issues) si vous avez une suggestion ou si vous rencontrez un problème.\n" | 247 | "issues) si vous avez une suggestion ou si vous rencontrez un problème.\n" |
275 | " \n" | 248 | " \n" |
276 | 249 | ||
277 | #: application/bookmark/exception/BookmarkNotFoundException.php:13 | 250 | #: application/bookmark/exception/BookmarkNotFoundException.php:14 |
278 | msgid "The link you are trying to reach does not exist or has been deleted." | 251 | msgid "The link you are trying to reach does not exist or has been deleted." |
279 | msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." | 252 | msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." |
280 | 253 | ||
281 | #: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129 | 254 | #: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:131 |
282 | msgid "" | 255 | msgid "" |
283 | "Shaarli could not create the config file. Please make sure Shaarli has the " | 256 | "Shaarli could not create the config file. Please make sure Shaarli has the " |
284 | "right to write in the folder is it installed in." | 257 | "right to write in the folder is it installed in." |
@@ -286,12 +259,12 @@ msgstr "" | |||
286 | "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " | 259 | "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " |
287 | "Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." | 260 | "Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." |
288 | 261 | ||
289 | #: application/config/ConfigManager.php:136 | 262 | #: application/config/ConfigManager.php:137 |
290 | #: application/config/ConfigManager.php:163 | 263 | #: application/config/ConfigManager.php:164 |
291 | msgid "Invalid setting key parameter. String expected, got: " | 264 | msgid "Invalid setting key parameter. String expected, got: " |
292 | msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " | 265 | msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " |
293 | 266 | ||
294 | #: application/config/exception/MissingFieldConfigException.php:21 | 267 | #: application/config/exception/MissingFieldConfigException.php:20 |
295 | #, php-format | 268 | #, php-format |
296 | msgid "Configuration value is required for %s" | 269 | msgid "Configuration value is required for %s" |
297 | msgstr "Le paramètre %s est obligatoire" | 270 | msgstr "Le paramètre %s est obligatoire" |
@@ -301,46 +274,48 @@ msgid "An error occurred while trying to save plugins loading order." | |||
301 | msgstr "" | 274 | msgstr "" |
302 | "Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions." | 275 | "Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions." |
303 | 276 | ||
304 | #: application/config/exception/UnauthorizedConfigException.php:16 | 277 | #: application/config/exception/UnauthorizedConfigException.php:15 |
305 | msgid "You are not authorized to alter config." | 278 | msgid "You are not authorized to alter config." |
306 | msgstr "Vous n'êtes pas autorisé à modifier la configuration." | 279 | msgstr "Vous n'êtes pas autorisé à modifier la configuration." |
307 | 280 | ||
308 | #: application/exceptions/IOException.php:22 | 281 | #: application/exceptions/IOException.php:23 |
309 | msgid "Error accessing" | 282 | msgid "Error accessing" |
310 | msgstr "Une erreur s'est produite en accédant à" | 283 | msgstr "Une erreur s'est produite en accédant à" |
311 | 284 | ||
312 | #: application/feed/FeedBuilder.php:179 | 285 | #: application/feed/FeedBuilder.php:180 |
313 | msgid "Direct link" | 286 | msgid "Direct link" |
314 | msgstr "Liens directs" | 287 | msgstr "Liens directs" |
315 | 288 | ||
316 | #: application/feed/FeedBuilder.php:181 | 289 | #: application/feed/FeedBuilder.php:182 |
317 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 | 290 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 |
291 | #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | ||
318 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 | 292 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 |
319 | msgid "Permalink" | 293 | msgid "Permalink" |
320 | msgstr "Permalien" | 294 | msgstr "Permalien" |
321 | 295 | ||
322 | #: application/front/controller/admin/ConfigureController.php:54 | 296 | #: application/front/controller/admin/ConfigureController.php:56 |
323 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | 297 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 |
324 | msgid "Configure" | 298 | msgid "Configure" |
325 | msgstr "Configurer" | 299 | msgstr "Configurer" |
326 | 300 | ||
327 | #: application/front/controller/admin/ConfigureController.php:102 | 301 | #: application/front/controller/admin/ConfigureController.php:106 |
328 | #: application/legacy/LegacyUpdater.php:537 | 302 | #: application/legacy/LegacyUpdater.php:539 |
329 | msgid "You have enabled or changed thumbnails mode." | 303 | msgid "You have enabled or changed thumbnails mode." |
330 | msgstr "Vous avez activé ou changé le mode de miniatures." | 304 | msgstr "Vous avez activé ou changé le mode de miniatures." |
331 | 305 | ||
332 | #: application/front/controller/admin/ConfigureController.php:103 | 306 | #: application/front/controller/admin/ConfigureController.php:108 |
333 | #: application/legacy/LegacyUpdater.php:538 | 307 | #: application/front/controller/admin/ServerController.php:76 |
308 | #: application/legacy/LegacyUpdater.php:540 | ||
334 | msgid "Please synchronize them." | 309 | msgid "Please synchronize them." |
335 | msgstr "Merci de les synchroniser." | 310 | msgstr "Merci de les synchroniser." |
336 | 311 | ||
337 | #: application/front/controller/admin/ConfigureController.php:113 | 312 | #: application/front/controller/admin/ConfigureController.php:119 |
338 | #: application/front/controller/visitor/InstallController.php:136 | 313 | #: application/front/controller/visitor/InstallController.php:149 |
339 | msgid "Error while writing config file after configuration update." | 314 | msgid "Error while writing config file after configuration update." |
340 | msgstr "" | 315 | msgstr "" |
341 | "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." |
342 | 317 | ||
343 | #: application/front/controller/admin/ConfigureController.php:122 | 318 | #: application/front/controller/admin/ConfigureController.php:128 |
344 | msgid "Configuration was saved." | 319 | msgid "Configuration was saved." |
345 | msgstr "La configuration a été sauvegardée." | 320 | msgstr "La configuration a été sauvegardée." |
346 | 321 | ||
@@ -372,70 +347,47 @@ msgstr "" | |||
372 | "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 " |
373 | "légères." | 348 | "légères." |
374 | 349 | ||
375 | #: application/front/controller/admin/ManageShaareController.php:29 | 350 | #: application/front/controller/admin/ManageTagController.php:30 |
376 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 351 | msgid "whitespace" |
377 | msgid "Shaare a new link" | 352 | msgstr "espace" |
378 | msgstr "Partager un nouveau lien" | ||
379 | 353 | ||
380 | #: application/front/controller/admin/ManageShaareController.php:78 | 354 | #: application/front/controller/admin/ManageTagController.php:35 |
381 | msgid "Note: " | ||
382 | msgstr "Note : " | ||
383 | |||
384 | #: application/front/controller/admin/ManageShaareController.php:109 | ||
385 | #: application/front/controller/admin/ManageShaareController.php:206 | ||
386 | #: application/front/controller/admin/ManageShaareController.php:275 | ||
387 | #: application/front/controller/admin/ManageShaareController.php:315 | ||
388 | #, php-format | ||
389 | msgid "Bookmark with identifier %s could not be found." | ||
390 | msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." | ||
391 | |||
392 | #: application/front/controller/admin/ManageShaareController.php:194 | ||
393 | #: application/front/controller/admin/ManageShaareController.php:252 | ||
394 | msgid "Invalid bookmark ID provided." | ||
395 | msgstr "ID du lien non valide." | ||
396 | |||
397 | #: application/front/controller/admin/ManageShaareController.php:260 | ||
398 | msgid "Invalid visibility provided." | ||
399 | msgstr "Visibilité du lien non valide." | ||
400 | |||
401 | #: application/front/controller/admin/ManageShaareController.php:363 | ||
402 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 | ||
403 | msgid "Edit" | ||
404 | msgstr "Modifier" | ||
405 | |||
406 | #: application/front/controller/admin/ManageShaareController.php:366 | ||
407 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
408 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 | ||
409 | msgid "Shaare" | ||
410 | msgstr "Shaare" | ||
411 | |||
412 | #: application/front/controller/admin/ManageTagController.php:29 | ||
413 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 355 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 |
414 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | 356 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 |
415 | msgid "Manage tags" | 357 | msgid "Manage tags" |
416 | msgstr "Gérer les tags" | 358 | msgstr "Gérer les tags" |
417 | 359 | ||
418 | #: application/front/controller/admin/ManageTagController.php:48 | 360 | #: application/front/controller/admin/ManageTagController.php:54 |
419 | msgid "Invalid tags provided." | 361 | msgid "Invalid tags provided." |
420 | msgstr "Les tags fournis ne sont pas valides." | 362 | msgstr "Les tags fournis ne sont pas valides." |
421 | 363 | ||
422 | #: application/front/controller/admin/ManageTagController.php:72 | 364 | #: application/front/controller/admin/ManageTagController.php:78 |
423 | #, php-format | 365 | #, php-format |
424 | msgid "The tag was removed from %d bookmark." | 366 | msgid "The tag was removed from %d bookmark." |
425 | msgid_plural "The tag was removed from %d bookmarks." | 367 | msgid_plural "The tag was removed from %d bookmarks." |
426 | msgstr[0] "Le tag a été supprimé du %d lien." | 368 | msgstr[0] "Le tag a été supprimé du %d lien." |
427 | msgstr[1] "Le tag a été supprimé de %d liens." | 369 | msgstr[1] "Le tag a été supprimé de %d liens." |
428 | 370 | ||
429 | #: application/front/controller/admin/ManageTagController.php:77 | 371 | #: application/front/controller/admin/ManageTagController.php:83 |
430 | #, php-format | 372 | #, php-format |
431 | msgid "The tag was renamed in %d bookmark." | 373 | msgid "The tag was renamed in %d bookmark." |
432 | msgid_plural "The tag was renamed in %d bookmarks." | 374 | msgid_plural "The tag was renamed in %d bookmarks." |
433 | msgstr[0] "Le tag a été renommé dans %d lien." | 375 | msgstr[0] "Le tag a été renommé dans %d lien." |
434 | msgstr[1] "Le tag a été renommé dans %d liens." | 376 | msgstr[1] "Le tag a été renommé dans %d liens." |
435 | 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 | |||
436 | #: application/front/controller/admin/PasswordController.php:28 | 388 | #: application/front/controller/admin/PasswordController.php:28 |
437 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | 389 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 |
438 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | 390 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 |
439 | msgid "Change password" | 391 | msgid "Change password" |
440 | msgstr "Modifier le mot de passe" | 392 | msgstr "Modifier le mot de passe" |
441 | 393 | ||
@@ -467,6 +419,61 @@ msgstr "" | |||
467 | "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 " |
468 | "plugins : " | 420 | "plugins : " |
469 | 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:85 | ||
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:173 | ||
463 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 | ||
464 | msgid "Edit" | ||
465 | msgstr "Modifier" | ||
466 | |||
467 | #: application/front/controller/admin/ShaarePublishController.php:176 | ||
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:208 | ||
474 | msgid "Note: " | ||
475 | msgstr "Note : " | ||
476 | |||
470 | #: application/front/controller/admin/ThumbnailsController.php:37 | 477 | #: application/front/controller/admin/ThumbnailsController.php:37 |
471 | #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | 478 | #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 |
472 | msgid "Thumbnails update" | 479 | msgid "Thumbnails update" |
@@ -478,33 +485,62 @@ msgstr "Mise à jour des miniatures" | |||
478 | msgid "Tools" | 485 | msgid "Tools" |
479 | msgstr "Outils" | 486 | msgstr "Outils" |
480 | 487 | ||
481 | #: application/front/controller/visitor/BookmarkListController.php:116 | 488 | #: application/front/controller/visitor/BookmarkListController.php:121 |
482 | msgid "Search: " | 489 | msgid "Search: " |
483 | msgstr "Recherche : " | 490 | msgstr "Recherche : " |
484 | 491 | ||
485 | #: application/front/controller/visitor/DailyController.php:45 | 492 | #: application/front/controller/visitor/DailyController.php:200 |
486 | msgid "Today" | 493 | msgid "day" |
487 | msgstr "Aujourd'hui" | 494 | msgstr "jour" |
488 | |||
489 | #: application/front/controller/visitor/DailyController.php:47 | ||
490 | msgid "Yesterday" | ||
491 | msgstr "Hier" | ||
492 | 495 | ||
493 | #: 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 | ||
494 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | 499 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 |
495 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 | 500 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 |
496 | msgid "Daily" | 501 | msgid "Daily" |
497 | msgstr "Quotidien" | 502 | msgstr "Quotidien" |
498 | 503 | ||
499 | #: 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 | ||
500 | msgid "An unexpected error occurred." | 531 | msgid "An unexpected error occurred." |
501 | msgstr "Une erreur inattendue s'est produite." | 532 | msgstr "Une erreur inattendue s'est produite." |
502 | 533 | ||
503 | #: application/front/controller/visitor/ErrorNotFoundController.php:25 | 534 | #: application/front/controller/visitor/ErrorNotFoundController.php:25 |
504 | msgid "Requested page could not be found." | 535 | msgid "Requested page could not be found." |
505 | msgstr "" | 536 | msgstr "La page demandée n'a pas pu être trouvée." |
537 | |||
538 | #: application/front/controller/visitor/InstallController.php:65 | ||
539 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | ||
540 | msgid "Install Shaarli" | ||
541 | msgstr "Installation de Shaarli" | ||
506 | 542 | ||
507 | #: application/front/controller/visitor/InstallController.php:73 | 543 | #: application/front/controller/visitor/InstallController.php:85 |
508 | #, php-format | 544 | #, php-format |
509 | msgid "" | 545 | msgid "" |
510 | "<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 " |
@@ -523,14 +559,14 @@ msgstr "" | |||
523 | "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 " |
524 | "adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" | 560 | "adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" |
525 | 561 | ||
526 | #: application/front/controller/visitor/InstallController.php:144 | 562 | #: application/front/controller/visitor/InstallController.php:157 |
527 | msgid "" | 563 | msgid "" |
528 | "Shaarli is now configured. Please login and start shaaring your bookmarks!" | 564 | "Shaarli is now configured. Please login and start shaaring your bookmarks!" |
529 | msgstr "" | 565 | msgstr "" |
530 | "Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " | 566 | "Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " |
531 | "shaare vos liens !" | 567 | "shaare vos liens !" |
532 | 568 | ||
533 | #: application/front/controller/visitor/InstallController.php:158 | 569 | #: application/front/controller/visitor/InstallController.php:171 |
534 | msgid "Insufficient permissions:" | 570 | msgid "Insufficient permissions:" |
535 | msgstr "Permissions insuffisantes :" | 571 | msgstr "Permissions insuffisantes :" |
536 | 572 | ||
@@ -554,9 +590,9 @@ msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." | |||
554 | msgid "Picture wall" | 590 | msgid "Picture wall" |
555 | msgstr "Mur d'images" | 591 | msgstr "Mur d'images" |
556 | 592 | ||
557 | #: application/front/controller/visitor/TagCloudController.php:88 | 593 | #: application/front/controller/visitor/TagCloudController.php:90 |
558 | msgid "Tag " | 594 | msgid "Tag " |
559 | msgstr "Tag" | 595 | msgstr "Tag " |
560 | 596 | ||
561 | #: application/front/exceptions/AlreadyInstalledException.php:11 | 597 | #: application/front/exceptions/AlreadyInstalledException.php:11 |
562 | msgid "Shaarli has already been installed. Login to edit the configuration." | 598 | msgid "Shaarli has already been installed. Login to edit the configuration." |
@@ -584,6 +620,94 @@ msgstr "" | |||
584 | msgid "Wrong token." | 620 | msgid "Wrong token." |
585 | msgstr "Jeton invalide." | 621 | msgstr "Jeton invalide." |
586 | 622 | ||
623 | #: application/helper/ApplicationUtils.php:165 | ||
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:200 | ||
635 | #: application/helper/ApplicationUtils.php:220 | ||
636 | msgid "directory is not readable" | ||
637 | msgstr "le répertoire n'est pas accessible en lecture" | ||
638 | |||
639 | #: application/helper/ApplicationUtils.php:223 | ||
640 | msgid "directory is not writable" | ||
641 | msgstr "le répertoire n'est pas accessible en écriture" | ||
642 | |||
643 | #: application/helper/ApplicationUtils.php:247 | ||
644 | msgid "file is not readable" | ||
645 | msgstr "le fichier n'est pas accessible en lecture" | ||
646 | |||
647 | #: application/helper/ApplicationUtils.php:250 | ||
648 | msgid "file is not writable" | ||
649 | msgstr "le fichier n'est pas accessible en écriture" | ||
650 | |||
651 | #: application/helper/ApplicationUtils.php:260 | ||
652 | msgid "" | ||
653 | "Lock can not be acquired on the datastore. You might encounter concurrent " | ||
654 | "access issues." | ||
655 | msgstr "" | ||
656 | "Le fichier datastore ne peut pas être verrouillé. Vous pourriez rencontrer " | ||
657 | "des problèmes d'accès concurrents." | ||
658 | |||
659 | #: application/helper/ApplicationUtils.php:293 | ||
660 | msgid "Configuration parsing" | ||
661 | msgstr "Chargement de la configuration" | ||
662 | |||
663 | #: application/helper/ApplicationUtils.php:294 | ||
664 | msgid "Slim Framework (routing, etc.)" | ||
665 | msgstr "Slim Framwork (routage, etc.)" | ||
666 | |||
667 | #: application/helper/ApplicationUtils.php:295 | ||
668 | msgid "Multibyte (Unicode) string support" | ||
669 | msgstr "Support des chaînes de caractère multibytes (Unicode)" | ||
670 | |||
671 | #: application/helper/ApplicationUtils.php:296 | ||
672 | msgid "Required to use thumbnails" | ||
673 | msgstr "Obligatoire pour utiliser les miniatures" | ||
674 | |||
675 | #: application/helper/ApplicationUtils.php:297 | ||
676 | msgid "Localized text sorting (e.g. e->è->f)" | ||
677 | msgstr "Tri des textes traduits (ex : e->è->f)" | ||
678 | |||
679 | #: application/helper/ApplicationUtils.php:298 | ||
680 | msgid "Better retrieval of bookmark metadata and thumbnail" | ||
681 | msgstr "Meilleure récupération des meta-données des marque-pages et minatures" | ||
682 | |||
683 | #: application/helper/ApplicationUtils.php:299 | ||
684 | msgid "Use the translation system in gettext mode" | ||
685 | msgstr "Utiliser le système de traduction en mode gettext" | ||
686 | |||
687 | #: application/helper/ApplicationUtils.php:300 | ||
688 | msgid "Login using LDAP server" | ||
689 | msgstr "Authentification via un serveur LDAP" | ||
690 | |||
691 | #: application/helper/DailyPageHelper.php:172 | ||
692 | msgid "Week" | ||
693 | msgstr "Semaine" | ||
694 | |||
695 | #: application/helper/DailyPageHelper.php:176 | ||
696 | msgid "Today" | ||
697 | msgstr "Aujourd'hui" | ||
698 | |||
699 | #: application/helper/DailyPageHelper.php:178 | ||
700 | msgid "Yesterday" | ||
701 | msgstr "Hier" | ||
702 | |||
703 | #: application/helper/FileUtils.php:100 | ||
704 | msgid "Provided path is not a directory." | ||
705 | msgstr "Le chemin fourni n'est pas un dossier." | ||
706 | |||
707 | #: application/helper/FileUtils.php:104 | ||
708 | msgid "Trying to delete a folder outside of Shaarli path." | ||
709 | msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli." | ||
710 | |||
587 | #: application/legacy/LegacyLinkDB.php:131 | 711 | #: application/legacy/LegacyLinkDB.php:131 |
588 | msgid "You are not authorized to add a link." | 712 | msgid "You are not authorized to add a link." |
589 | msgstr "Vous n'êtes pas autorisé à ajouter un lien." | 713 | msgstr "Vous n'êtes pas autorisé à ajouter un lien." |
@@ -634,7 +758,7 @@ msgstr "" | |||
634 | msgid "Couldn't retrieve updater class methods." | 758 | msgid "Couldn't retrieve updater class methods." |
635 | msgstr "Impossible de récupérer les méthodes de la classe Updater." | 759 | msgstr "Impossible de récupérer les méthodes de la classe Updater." |
636 | 760 | ||
637 | #: application/legacy/LegacyUpdater.php:538 | 761 | #: application/legacy/LegacyUpdater.php:540 |
638 | msgid "<a href=\"./admin/thumbnails\">" | 762 | msgid "<a href=\"./admin/thumbnails\">" |
639 | msgstr "<a href=\"./admin/thumbnails\">" | 763 | msgstr "<a href=\"./admin/thumbnails\">" |
640 | 764 | ||
@@ -660,11 +784,11 @@ msgstr "" | |||
660 | "a été importé avec succès en %d secondes : %d liens importés, %d liens " | 784 | "a été importé avec succès en %d secondes : %d liens importés, %d liens " |
661 | "écrasés, %d liens ignorés." | 785 | "écrasés, %d liens ignorés." |
662 | 786 | ||
663 | #: application/plugin/PluginManager.php:124 | 787 | #: application/plugin/PluginManager.php:125 |
664 | msgid " [plugin incompatibility]: " | 788 | msgid " [plugin incompatibility]: " |
665 | msgstr " [incompatibilité de l'extension] : " | 789 | msgstr " [incompatibilité de l'extension] : " |
666 | 790 | ||
667 | #: application/plugin/exception/PluginFileNotFoundException.php:21 | 791 | #: application/plugin/exception/PluginFileNotFoundException.php:22 |
668 | #, php-format | 792 | #, php-format |
669 | msgid "Plugin \"%s\" files not found." | 793 | msgid "Plugin \"%s\" files not found." |
670 | msgstr "Les fichiers de l'extension \"%s\" sont introuvables." | 794 | msgstr "Les fichiers de l'extension \"%s\" sont introuvables." |
@@ -678,7 +802,7 @@ msgstr "Impossible de purger %s : le répertoire n'existe pas" | |||
678 | msgid "An error occurred while running the update " | 802 | msgid "An error occurred while running the update " |
679 | msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " | 803 | msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " |
680 | 804 | ||
681 | #: index.php:65 | 805 | #: index.php:81 |
682 | msgid "Shared bookmarks on " | 806 | msgid "Shared bookmarks on " |
683 | msgstr "Liens partagés sur " | 807 | msgstr "Liens partagés sur " |
684 | 808 | ||
@@ -695,11 +819,11 @@ msgstr "Shaare" | |||
695 | msgid "Adds the addlink input on the linklist page." | 819 | msgid "Adds the addlink input on the linklist page." |
696 | msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." | 820 | msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." |
697 | 821 | ||
698 | #: plugins/archiveorg/archiveorg.php:28 | 822 | #: plugins/archiveorg/archiveorg.php:29 |
699 | msgid "View on archive.org" | 823 | msgid "View on archive.org" |
700 | msgstr "Voir sur archive.org" | 824 | msgstr "Voir sur archive.org" |
701 | 825 | ||
702 | #: plugins/archiveorg/archiveorg.php:41 | 826 | #: plugins/archiveorg/archiveorg.php:42 |
703 | msgid "For each link, add an Archive.org icon." | 827 | msgid "For each link, add an Archive.org icon." |
704 | msgstr "Pour chaque lien, ajoute une icône pour Archive.org." | 828 | msgstr "Pour chaque lien, ajoute une icône pour Archive.org." |
705 | 829 | ||
@@ -729,7 +853,7 @@ msgstr "Couleur de fond (gris léger)" | |||
729 | msgid "Dark main color (e.g. visited links)" | 853 | msgid "Dark main color (e.g. visited links)" |
730 | msgstr "Couleur principale sombre (ex : les liens visités)" | 854 | msgstr "Couleur principale sombre (ex : les liens visités)" |
731 | 855 | ||
732 | #: plugins/demo_plugin/demo_plugin.php:477 | 856 | #: plugins/demo_plugin/demo_plugin.php:478 |
733 | msgid "" | 857 | msgid "" |
734 | "A demo plugin covering all use cases for template designers and plugin " | 858 | "A demo plugin covering all use cases for template designers and plugin " |
735 | "developers." | 859 | "developers." |
@@ -737,11 +861,11 @@ msgstr "" | |||
737 | "Une extension de démonstration couvrant tous les cas d'utilisation pour les " | 861 | "Une extension de démonstration couvrant tous les cas d'utilisation pour les " |
738 | "designers de thèmes et les développeurs d'extensions." | 862 | "designers de thèmes et les développeurs d'extensions." |
739 | 863 | ||
740 | #: plugins/demo_plugin/demo_plugin.php:478 | 864 | #: plugins/demo_plugin/demo_plugin.php:479 |
741 | msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." | 865 | msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." |
742 | msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé." | 866 | msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé." |
743 | 867 | ||
744 | #: plugins/demo_plugin/demo_plugin.php:479 | 868 | #: plugins/demo_plugin/demo_plugin.php:480 |
745 | msgid "Other demo parameter" | 869 | msgid "Other demo parameter" |
746 | msgstr "Un autre paramètre de démo" | 870 | msgstr "Un autre paramètre de démo" |
747 | 871 | ||
@@ -763,7 +887,7 @@ msgstr "" | |||
763 | msgid "Isso server URL (without 'http://')" | 887 | msgid "Isso server URL (without 'http://')" |
764 | msgstr "URL du serveur Isso (sans 'http://')" | 888 | msgstr "URL du serveur Isso (sans 'http://')" |
765 | 889 | ||
766 | #: plugins/piwik/piwik.php:23 | 890 | #: plugins/piwik/piwik.php:24 |
767 | msgid "" | 891 | msgid "" |
768 | "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " | 892 | "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " |
769 | "administration page." | 893 | "administration page." |
@@ -771,27 +895,27 @@ msgstr "" | |||
771 | "Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et " | 895 | "Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et " |
772 | "PIWIK_SITEID dans la page d'administration des extensions." | 896 | "PIWIK_SITEID dans la page d'administration des extensions." |
773 | 897 | ||
774 | #: plugins/piwik/piwik.php:72 | 898 | #: plugins/piwik/piwik.php:73 |
775 | msgid "A plugin that adds Piwik tracking code to Shaarli pages." | 899 | msgid "A plugin that adds Piwik tracking code to Shaarli pages." |
776 | msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli." | 900 | msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli." |
777 | 901 | ||
778 | #: plugins/piwik/piwik.php:73 | 902 | #: plugins/piwik/piwik.php:74 |
779 | msgid "Piwik URL" | 903 | msgid "Piwik URL" |
780 | msgstr "URL de Piwik" | 904 | msgstr "URL de Piwik" |
781 | 905 | ||
782 | #: plugins/piwik/piwik.php:74 | 906 | #: plugins/piwik/piwik.php:75 |
783 | msgid "Piwik site ID" | 907 | msgid "Piwik site ID" |
784 | msgstr "Site ID de Piwik" | 908 | msgstr "Site ID de Piwik" |
785 | 909 | ||
786 | #: plugins/playvideos/playvideos.php:25 | 910 | #: plugins/playvideos/playvideos.php:26 |
787 | msgid "Video player" | 911 | msgid "Video player" |
788 | msgstr "Lecteur vidéo" | 912 | msgstr "Lecteur vidéo" |
789 | 913 | ||
790 | #: plugins/playvideos/playvideos.php:28 | 914 | #: plugins/playvideos/playvideos.php:29 |
791 | msgid "Play Videos" | 915 | msgid "Play Videos" |
792 | msgstr "Jouer les vidéos" | 916 | msgstr "Jouer les vidéos" |
793 | 917 | ||
794 | #: plugins/playvideos/playvideos.php:59 | 918 | #: plugins/playvideos/playvideos.php:60 |
795 | msgid "Add a button in the toolbar allowing to watch all videos." | 919 | msgid "Add a button in the toolbar allowing to watch all videos." |
796 | msgstr "" | 920 | msgstr "" |
797 | "Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos." | 921 | "Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos." |
@@ -819,11 +943,11 @@ msgstr "Mauvaise réponse du hub %s" | |||
819 | msgid "Enable PubSubHubbub feed publishing." | 943 | msgid "Enable PubSubHubbub feed publishing." |
820 | msgstr "Active la publication de flux vers PubSubHubbub." | 944 | msgstr "Active la publication de flux vers PubSubHubbub." |
821 | 945 | ||
822 | #: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70 | 946 | #: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72 |
823 | msgid "For each link, add a QRCode icon." | 947 | msgid "For each link, add a QRCode icon." |
824 | msgstr "Pour chaque lien, ajouter une icône de QRCode." | 948 | msgstr "Pour chaque lien, ajouter une icône de QRCode." |
825 | 949 | ||
826 | #: plugins/wallabag/wallabag.php:21 | 950 | #: plugins/wallabag/wallabag.php:22 |
827 | msgid "" | 951 | msgid "" |
828 | "Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " | 952 | "Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " |
829 | "plugin administration page." | 953 | "plugin administration page." |
@@ -831,15 +955,15 @@ msgstr "" | |||
831 | "Erreur de l'extension Wallabag : Merci de définir le paramètre « " | 955 | "Erreur de l'extension Wallabag : Merci de définir le paramètre « " |
832 | "WALLABAG_URL » dans la page d'administration des extensions." | 956 | "WALLABAG_URL » dans la page d'administration des extensions." |
833 | 957 | ||
834 | #: plugins/wallabag/wallabag.php:47 | 958 | #: plugins/wallabag/wallabag.php:49 |
835 | msgid "Save to wallabag" | 959 | msgid "Save to wallabag" |
836 | msgstr "Sauvegarder dans Wallabag" | 960 | msgstr "Sauvegarder dans Wallabag" |
837 | 961 | ||
838 | #: plugins/wallabag/wallabag.php:71 | 962 | #: plugins/wallabag/wallabag.php:73 |
839 | msgid "Wallabag API URL" | 963 | msgid "Wallabag API URL" |
840 | msgstr "URL de l'API Wallabag" | 964 | msgstr "URL de l'API Wallabag" |
841 | 965 | ||
842 | #: plugins/wallabag/wallabag.php:72 | 966 | #: plugins/wallabag/wallabag.php:74 |
843 | msgid "Wallabag API version (1 or 2)" | 967 | msgid "Wallabag API version (1 or 2)" |
844 | msgstr "Version de l'API Wallabag (1 ou 2)" | 968 | msgstr "Version de l'API Wallabag (1 ou 2)" |
845 | 969 | ||
@@ -851,6 +975,48 @@ msgstr "Désolé, il y a rien à voir ici." | |||
851 | msgid "URL or leave empty to post a note" | 975 | msgid "URL or leave empty to post a note" |
852 | msgstr "URL ou laisser vide pour créer une note" | 976 | msgstr "URL ou laisser vide pour créer une note" |
853 | 977 | ||
978 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
979 | msgid "BULK CREATION" | ||
980 | msgstr "CRÉATION DE MASSE" | ||
981 | |||
982 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
983 | msgid "Metadata asynchronous retrieval is disabled." | ||
984 | msgstr "La récupération asynchrone des meta-données est désactivée." | ||
985 | |||
986 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
987 | msgid "" | ||
988 | "We recommend that you enable the setting <em>general > " | ||
989 | "enable_async_metadata</em> in your configuration file to use bulk link " | ||
990 | "creation." | ||
991 | msgstr "" | ||
992 | "Nous recommandons d'activer le paramètre <em>general > " | ||
993 | "enable_async_metadata</em> dans votre fichier de configuration pour utiliser " | ||
994 | "la création de masse." | ||
995 | |||
996 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 | ||
997 | msgid "Shaare multiple new links" | ||
998 | msgstr "Partagez plusieurs nouveaux liens" | ||
999 | |||
1000 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 | ||
1001 | msgid "Add one URL per line to create multiple bookmarks." | ||
1002 | msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages." | ||
1003 | |||
1004 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 | ||
1005 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 | ||
1006 | msgid "Tags" | ||
1007 | msgstr "Tags" | ||
1008 | |||
1009 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 | ||
1010 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 | ||
1011 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
1012 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
1013 | msgid "Private" | ||
1014 | msgstr "Privé" | ||
1015 | |||
1016 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 | ||
1017 | msgid "Add links" | ||
1018 | msgstr "Ajouter des liens" | ||
1019 | |||
854 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | 1020 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 |
855 | msgid "Current password" | 1021 | msgid "Current password" |
856 | msgstr "Mot de passe actuel" | 1022 | msgstr "Mot de passe actuel" |
@@ -885,14 +1051,40 @@ msgstr "Renommer le tag" | |||
885 | msgid "Delete tag" | 1051 | msgid "Delete tag" |
886 | msgstr "Supprimer le tag" | 1052 | msgstr "Supprimer le tag" |
887 | 1053 | ||
888 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | 1054 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 |
889 | msgid "You can also edit tags in the" | 1055 | msgid "You can also edit tags in the" |
890 | msgstr "Vous pouvez aussi modifier les tags dans la" | 1056 | msgstr "Vous pouvez aussi modifier les tags dans la" |
891 | 1057 | ||
892 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 | 1058 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 |
893 | msgid "tag list" | 1059 | msgid "tag list" |
894 | msgstr "liste des tags" | 1060 | msgstr "liste des tags" |
895 | 1061 | ||
1062 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | ||
1063 | msgid "Change tags separator" | ||
1064 | msgstr "Changer le séparateur de tags" | ||
1065 | |||
1066 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 | ||
1067 | msgid "Your current tag separator is" | ||
1068 | msgstr "Votre séparateur actuel est" | ||
1069 | |||
1070 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 | ||
1071 | msgid "New separator" | ||
1072 | msgstr "Nouveau séparateur" | ||
1073 | |||
1074 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1075 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 | ||
1076 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1077 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | ||
1078 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 | ||
1079 | msgid "Save" | ||
1080 | msgstr "Enregistrer" | ||
1081 | |||
1082 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 | ||
1083 | msgid "Note that hashtags won't fully work with a non-whitespace separator." | ||
1084 | msgstr "" | ||
1085 | "Notez que les hashtags ne sont pas complètement fonctionnels avec un " | ||
1086 | "séparateur qui n'est pas un espace." | ||
1087 | |||
896 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | 1088 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 |
897 | msgid "title" | 1089 | msgid "title" |
898 | msgstr "titre" | 1090 | msgstr "titre" |
@@ -1016,71 +1208,72 @@ msgstr "" | |||
1016 | "miniatures." | 1208 | "miniatures." |
1017 | 1209 | ||
1018 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 | 1210 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 |
1019 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 | 1211 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 |
1020 | msgid "Synchronize thumbnails" | 1212 | msgid "Synchronize thumbnails" |
1021 | msgstr "Synchroniser les miniatures" | 1213 | msgstr "Synchroniser les miniatures" |
1022 | 1214 | ||
1023 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 | 1215 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 |
1024 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 | 1216 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 |
1217 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
1025 | msgid "All" | 1218 | msgid "All" |
1026 | msgstr "Tous" | 1219 | msgstr "Tous" |
1027 | 1220 | ||
1028 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 | 1221 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 |
1222 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 | ||
1029 | msgid "Only common media hosts" | 1223 | msgid "Only common media hosts" |
1030 | msgstr "Seulement les hébergeurs de média connus" | 1224 | msgstr "Seulement les hébergeurs de média connus" |
1031 | 1225 | ||
1032 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 | 1226 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 |
1227 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 | ||
1033 | msgid "None" | 1228 | msgid "None" |
1034 | msgstr "Aucune" | 1229 | msgstr "Aucune" |
1035 | 1230 | ||
1036 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 | 1231 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 |
1037 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 | 1232 | msgid "1 RSS entry per :type" |
1038 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | 1233 | msgid_plural "" |
1039 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 | 1234 | msgstr[0] "1 entrée RSS par :type" |
1040 | msgid "Save" | 1235 | msgstr[1] "" |
1041 | msgstr "Enregistrer" | 1236 | |
1042 | 1237 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | |
1043 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | 1238 | msgid "Previous :type" |
1044 | msgid "The Daily Shaarli" | 1239 | msgid_plural "" |
1045 | msgstr "Le Quotidien Shaarli" | 1240 | msgstr[0] ":type précédent" |
1046 | 1241 | msgstr[1] "Jour précédent" | |
1047 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 | 1242 | |
1048 | msgid "1 RSS entry per day" | 1243 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 |
1049 | msgstr "1 entrée RSS par jour" | 1244 | #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 |
1050 | 1245 | msgid "All links of one :type in a single page." | |
1051 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 | 1246 | msgid_plural "" |
1052 | msgid "Previous day" | 1247 | msgstr[0] "Tous les liens d'un :type sur une page." |
1053 | msgstr "Jour précédent" | 1248 | msgstr[1] "Tous les liens d'un jour sur une page." |
1054 | 1249 | ||
1055 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | 1250 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 |
1056 | msgid "All links of one day in a single page." | 1251 | msgid "Next :type" |
1057 | msgstr "Tous les liens d'un jour sur une page." | 1252 | msgid_plural "" |
1058 | 1253 | msgstr[0] ":type suivant" | |
1059 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 | 1254 | msgstr[1] "" |
1060 | msgid "Next day" | 1255 | |
1061 | msgstr "Jour suivant" | 1256 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 |
1062 | |||
1063 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | ||
1064 | msgid "Edit Shaare" | 1257 | msgid "Edit Shaare" |
1065 | msgstr "Modifier le Shaare" | 1258 | msgstr "Modifier le Shaare" |
1066 | 1259 | ||
1067 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | 1260 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 |
1068 | msgid "New Shaare" | 1261 | msgid "New Shaare" |
1069 | msgstr "Nouveau Shaare" | 1262 | msgstr "Nouveau Shaare" |
1070 | 1263 | ||
1071 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | 1264 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 |
1072 | msgid "Created:" | 1265 | msgid "Created:" |
1073 | msgstr "Création :" | 1266 | msgstr "Création :" |
1074 | 1267 | ||
1075 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | 1268 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 |
1076 | msgid "URL" | 1269 | msgid "URL" |
1077 | msgstr "URL" | 1270 | msgstr "URL" |
1078 | 1271 | ||
1079 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | 1272 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 |
1080 | msgid "Title" | 1273 | msgid "Title" |
1081 | msgstr "Titre" | 1274 | msgstr "Titre" |
1082 | 1275 | ||
1083 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | 1276 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 |
1084 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | 1277 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 |
1085 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 | 1278 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 |
1086 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 | 1279 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 |
@@ -1088,33 +1281,27 @@ msgstr "Titre" | |||
1088 | msgid "Description" | 1281 | msgid "Description" |
1089 | msgstr "Description" | 1282 | msgstr "Description" |
1090 | 1283 | ||
1091 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | 1284 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 |
1092 | msgid "Tags" | ||
1093 | msgstr "Tags" | ||
1094 | |||
1095 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 | ||
1096 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
1097 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
1098 | msgid "Private" | ||
1099 | msgstr "Privé" | ||
1100 | |||
1101 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 | ||
1102 | msgid "Description will be rendered with" | 1285 | msgid "Description will be rendered with" |
1103 | msgstr "La description sera générée avec" | 1286 | msgstr "La description sera générée avec" |
1104 | 1287 | ||
1105 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 | 1288 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 |
1106 | msgid "Markdown syntax documentation" | 1289 | msgid "Markdown syntax documentation" |
1107 | msgstr "Documentation sur la syntaxe Markdown" | 1290 | msgstr "Documentation sur la syntaxe Markdown" |
1108 | 1291 | ||
1109 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 | 1292 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 |
1110 | msgid "Markdown syntax" | 1293 | msgid "Markdown syntax" |
1111 | msgstr "la syntaxe Markdown" | 1294 | msgstr "la syntaxe Markdown" |
1112 | 1295 | ||
1113 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 | 1296 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115 |
1297 | msgid "Cancel" | ||
1298 | msgstr "Annuler" | ||
1299 | |||
1300 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1114 | msgid "Apply Changes" | 1301 | msgid "Apply Changes" |
1115 | msgstr "Appliquer les changements" | 1302 | msgstr "Appliquer les changements" |
1116 | 1303 | ||
1117 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93 | 1304 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126 |
1118 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 | 1305 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 |
1119 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 | 1306 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 |
1120 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 | 1307 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 |
@@ -1122,6 +1309,11 @@ msgstr "Appliquer les changements" | |||
1122 | msgid "Delete" | 1309 | msgid "Delete" |
1123 | msgstr "Supprimer" | 1310 | msgstr "Supprimer" |
1124 | 1311 | ||
1312 | #: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 | ||
1313 | #: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 | ||
1314 | msgid "Save all" | ||
1315 | msgstr "Tout enregistrer" | ||
1316 | |||
1125 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | 1317 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 |
1126 | msgid "Export Database" | 1318 | msgid "Export Database" |
1127 | msgstr "Exporter les données" | 1319 | msgstr "Exporter les données" |
@@ -1179,10 +1371,6 @@ msgstr "Les doublons s'appuient sur les URL" | |||
1179 | msgid "Add default tags" | 1371 | msgid "Add default tags" |
1180 | msgstr "Ajouter des tags par défaut" | 1372 | msgstr "Ajouter des tags par défaut" |
1181 | 1373 | ||
1182 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | ||
1183 | msgid "Install Shaarli" | ||
1184 | msgstr "Installation de Shaarli" | ||
1185 | |||
1186 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 | 1374 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 |
1187 | msgid "It looks like it's the first time you run Shaarli. Please configure it." | 1375 | msgid "It looks like it's the first time you run Shaarli. Please configure it." |
1188 | msgstr "" | 1376 | msgstr "" |
@@ -1215,6 +1403,10 @@ msgstr "Mes liens" | |||
1215 | msgid "Install" | 1403 | msgid "Install" |
1216 | msgstr "Installer" | 1404 | msgstr "Installer" |
1217 | 1405 | ||
1406 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190 | ||
1407 | msgid "Server requirements" | ||
1408 | msgstr "Pré-requis serveur" | ||
1409 | |||
1218 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | 1410 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 |
1219 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 | 1411 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 |
1220 | msgid "shaare" | 1412 | msgid "shaare" |
@@ -1288,8 +1480,8 @@ msgid "without any tag" | |||
1288 | msgstr "sans tag" | 1480 | msgstr "sans tag" |
1289 | 1481 | ||
1290 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 | 1482 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 |
1291 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | 1483 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 |
1292 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 | 1484 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41 |
1293 | msgid "Fold" | 1485 | msgid "Fold" |
1294 | msgstr "Replier" | 1486 | msgstr "Replier" |
1295 | 1487 | ||
@@ -1313,6 +1505,10 @@ msgstr "Changer statut épinglé" | |||
1313 | msgid "Sticky" | 1505 | msgid "Sticky" |
1314 | msgstr "Épinglé" | 1506 | msgstr "Épinglé" |
1315 | 1507 | ||
1508 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 | ||
1509 | msgid "Share a private link" | ||
1510 | msgstr "Partager un lien privé" | ||
1511 | |||
1316 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 | 1512 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 |
1317 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 | 1513 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 |
1318 | msgid "Filters" | 1514 | msgid "Filters" |
@@ -1331,7 +1527,7 @@ msgstr "Afficher uniquement les liens publics" | |||
1331 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | 1527 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 |
1332 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 | 1528 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 |
1333 | msgid "Filter untagged links" | 1529 | msgid "Filter untagged links" |
1334 | msgstr "Filtrer par liens privés" | 1530 | msgstr "Filtrer par liens sans tag" |
1335 | 1531 | ||
1336 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | 1532 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 |
1337 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 | 1533 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 |
@@ -1342,8 +1538,8 @@ msgstr "Tout sélectionner" | |||
1342 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 | 1538 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 |
1343 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 | 1539 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 |
1344 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 | 1540 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 |
1345 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | 1541 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 |
1346 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 | 1542 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 |
1347 | msgid "Fold all" | 1543 | msgid "Fold all" |
1348 | msgstr "Replier tout" | 1544 | msgstr "Replier tout" |
1349 | 1545 | ||
@@ -1359,9 +1555,9 @@ msgid "Remember me" | |||
1359 | msgstr "Rester connecté" | 1555 | msgstr "Rester connecté" |
1360 | 1556 | ||
1361 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | 1557 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 |
1362 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | 1558 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 |
1363 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 | 1559 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 |
1364 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49 | 1560 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 |
1365 | msgid "by the Shaarli community" | 1561 | msgid "by the Shaarli community" |
1366 | msgstr "par la communauté Shaarli" | 1562 | msgstr "par la communauté Shaarli" |
1367 | 1563 | ||
@@ -1370,18 +1566,23 @@ msgstr "par la communauté Shaarli" | |||
1370 | msgid "Documentation" | 1566 | msgid "Documentation" |
1371 | msgstr "Documentation" | 1567 | msgstr "Documentation" |
1372 | 1568 | ||
1373 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 | 1569 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 |
1374 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 | 1570 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 |
1375 | msgid "Expand" | 1571 | msgid "Expand" |
1376 | msgstr "Déplier" | 1572 | msgstr "Déplier" |
1377 | 1573 | ||
1378 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | 1574 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 |
1379 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 | 1575 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 |
1380 | msgid "Expand all" | 1576 | msgid "Expand all" |
1381 | msgstr "Déplier tout" | 1577 | msgstr "Déplier tout" |
1382 | 1578 | ||
1383 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | 1579 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 |
1384 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:47 | 1580 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 |
1581 | msgid "Are you sure you want to delete this link?" | ||
1582 | msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" | ||
1583 | |||
1584 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
1585 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 | ||
1385 | msgid "Are you sure you want to delete this tag?" | 1586 | msgid "Are you sure you want to delete this tag?" |
1386 | msgstr "Êtes-vous sûr de vouloir supprimer ce tag ?" | 1587 | msgstr "Êtes-vous sûr de vouloir supprimer ce tag ?" |
1387 | 1588 | ||
@@ -1511,6 +1712,100 @@ msgstr "Configuration des extensions" | |||
1511 | msgid "No parameter available." | 1712 | msgid "No parameter available." |
1512 | msgstr "Aucun paramètre disponible." | 1713 | msgstr "Aucun paramètre disponible." |
1513 | 1714 | ||
1715 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1716 | msgid "General" | ||
1717 | msgstr "Général" | ||
1718 | |||
1719 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 | ||
1720 | msgid "Index URL" | ||
1721 | msgstr "URL de l'index" | ||
1722 | |||
1723 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
1724 | msgid "Base path" | ||
1725 | msgstr "Chemin de base" | ||
1726 | |||
1727 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1728 | msgid "Client IP" | ||
1729 | msgstr "IP du client" | ||
1730 | |||
1731 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
1732 | msgid "Trusted reverse proxies" | ||
1733 | msgstr "Reverse proxies de confiance" | ||
1734 | |||
1735 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1736 | msgid "N/A" | ||
1737 | msgstr "N/A" | ||
1738 | |||
1739 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 | ||
1740 | msgid "Visit releases page on Github" | ||
1741 | msgstr "Visiter la page des releases sur Github" | ||
1742 | |||
1743 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1744 | msgid "Synchronize all link thumbnails" | ||
1745 | msgstr "Synchroniser toutes les miniatures" | ||
1746 | |||
1747 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2 | ||
1748 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2 | ||
1749 | msgid "Permissions" | ||
1750 | msgstr "Permissions" | ||
1751 | |||
1752 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8 | ||
1753 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8 | ||
1754 | msgid "There are permissions that need to be fixed." | ||
1755 | msgstr "Il y a des permissions qui doivent être corrigées." | ||
1756 | |||
1757 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
1758 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23 | ||
1759 | msgid "All read/write permissions are properly set." | ||
1760 | msgstr "Toutes les permissions de lecture/écriture sont définies correctement." | ||
1761 | |||
1762 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 | ||
1763 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32 | ||
1764 | msgid "Running PHP" | ||
1765 | msgstr "Fonctionnant avec PHP" | ||
1766 | |||
1767 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1768 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36 | ||
1769 | msgid "End of life: " | ||
1770 | msgstr "Fin de vie : " | ||
1771 | |||
1772 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
1773 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48 | ||
1774 | msgid "Extension" | ||
1775 | msgstr "Extension" | ||
1776 | |||
1777 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | ||
1778 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49 | ||
1779 | msgid "Usage" | ||
1780 | msgstr "Utilisation" | ||
1781 | |||
1782 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 | ||
1783 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50 | ||
1784 | msgid "Status" | ||
1785 | msgstr "Statut" | ||
1786 | |||
1787 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 | ||
1788 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 | ||
1789 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51 | ||
1790 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66 | ||
1791 | msgid "Loaded" | ||
1792 | msgstr "Chargé" | ||
1793 | |||
1794 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 | ||
1795 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 | ||
1796 | msgid "Required" | ||
1797 | msgstr "Obligatoire" | ||
1798 | |||
1799 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 | ||
1800 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 | ||
1801 | msgid "Optional" | ||
1802 | msgstr "Optionnel" | ||
1803 | |||
1804 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 | ||
1805 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70 | ||
1806 | msgid "Not loaded" | ||
1807 | msgstr "Non chargé" | ||
1808 | |||
1514 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | 1809 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 |
1515 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | 1810 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 |
1516 | msgid "tags" | 1811 | msgid "tags" |
@@ -1561,15 +1856,19 @@ msgstr "Configurer Shaarli" | |||
1561 | msgid "Enable, disable and configure plugins" | 1856 | msgid "Enable, disable and configure plugins" |
1562 | msgstr "Activer, désactiver et configurer les extensions" | 1857 | msgstr "Activer, désactiver et configurer les extensions" |
1563 | 1858 | ||
1564 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | 1859 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 |
1860 | msgid "Check instance's server configuration" | ||
1861 | msgstr "Vérifier la configuration serveur de l'instance" | ||
1862 | |||
1863 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
1565 | msgid "Change your password" | 1864 | msgid "Change your password" |
1566 | msgstr "Modifier le mot de passe" | 1865 | msgstr "Modifier le mot de passe" |
1567 | 1866 | ||
1568 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | 1867 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 |
1569 | msgid "Rename or delete a tag in all links" | 1868 | msgid "Rename or delete a tag in all links" |
1570 | msgstr "Renommer ou supprimer un tag dans tous les liens" | 1869 | msgstr "Renommer ou supprimer un tag dans tous les liens" |
1571 | 1870 | ||
1572 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | 1871 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 |
1573 | msgid "" | 1872 | msgid "" |
1574 | "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " | 1873 | "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " |
1575 | "delicious...)" | 1874 | "delicious...)" |
@@ -1577,11 +1876,11 @@ msgstr "" | |||
1577 | "Importer des marques pages au format Netscape HTML (comme exportés depuis " | 1876 | "Importer des marques pages au format Netscape HTML (comme exportés depuis " |
1578 | "Firefox, Chrome, Opera, delicious...)" | 1877 | "Firefox, Chrome, Opera, delicious...)" |
1579 | 1878 | ||
1580 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | 1879 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 |
1581 | msgid "Import links" | 1880 | msgid "Import links" |
1582 | msgstr "Importer des liens" | 1881 | msgstr "Importer des liens" |
1583 | 1882 | ||
1584 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | 1883 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 |
1585 | msgid "" | 1884 | msgid "" |
1586 | "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " | 1885 | "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " |
1587 | "Opera, delicious...)" | 1886 | "Opera, delicious...)" |
@@ -1589,15 +1888,11 @@ msgstr "" | |||
1589 | "Exporter les marques pages au format Netscape HTML (comme exportés depuis " | 1888 | "Exporter les marques pages au format Netscape HTML (comme exportés depuis " |
1590 | "Firefox, Chrome, Opera, delicious...)" | 1889 | "Firefox, Chrome, Opera, delicious...)" |
1591 | 1890 | ||
1592 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | 1891 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54 |
1593 | msgid "Export database" | 1892 | msgid "Export database" |
1594 | msgstr "Exporter les données" | 1893 | msgstr "Exporter les données" |
1595 | 1894 | ||
1596 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55 | 1895 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 |
1597 | msgid "Synchronize all link thumbnails" | ||
1598 | msgstr "Synchroniser toutes les miniatures" | ||
1599 | |||
1600 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81 | ||
1601 | msgid "" | 1896 | msgid "" |
1602 | "Drag one of these button to your bookmarks toolbar or right-click it and " | 1897 | "Drag one of these button to your bookmarks toolbar or right-click it and " |
1603 | "\"Bookmark This Link\"" | 1898 | "\"Bookmark This Link\"" |
@@ -1605,13 +1900,13 @@ msgstr "" | |||
1605 | "Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " | 1900 | "Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " |
1606 | "dessus et « Ajouter aux favoris »" | 1901 | "dessus et « Ajouter aux favoris »" |
1607 | 1902 | ||
1608 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 | 1903 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 |
1609 | msgid "then click on the bookmarklet in any page you want to share." | 1904 | msgid "then click on the bookmarklet in any page you want to share." |
1610 | msgstr "" | 1905 | msgstr "" |
1611 | "puis cliquer sur le marque-page depuis un site que vous souhaitez partager." | 1906 | "puis cliquer sur le marque-page depuis un site que vous souhaitez partager." |
1612 | 1907 | ||
1613 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 | 1908 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 |
1614 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 | 1909 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 |
1615 | msgid "" | 1910 | msgid "" |
1616 | "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " | 1911 | "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " |
1617 | "Link" | 1912 | "Link" |
@@ -1619,40 +1914,40 @@ msgstr "" | |||
1619 | "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " | 1914 | "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " |
1620 | "Ajouter aux favoris »" | 1915 | "Ajouter aux favoris »" |
1621 | 1916 | ||
1622 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 | 1917 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 |
1623 | msgid "then click ✚Shaare link button in any page you want to share" | 1918 | msgid "then click ✚Shaare link button in any page you want to share" |
1624 | msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" | 1919 | msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" |
1625 | 1920 | ||
1626 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 | 1921 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 |
1627 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 | 1922 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 |
1628 | msgid "The selected text is too long, it will be truncated." | 1923 | msgid "The selected text is too long, it will be truncated." |
1629 | msgstr "Le texte sélectionné est trop long, il sera tronqué." | 1924 | msgstr "Le texte sélectionné est trop long, il sera tronqué." |
1630 | 1925 | ||
1631 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 | 1926 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 |
1632 | msgid "Shaare link" | 1927 | msgid "Shaare link" |
1633 | msgstr "Shaare" | 1928 | msgstr "Shaare" |
1634 | 1929 | ||
1635 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 | 1930 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 |
1636 | msgid "" | 1931 | msgid "" |
1637 | "Then click ✚Add Note button anytime to start composing a private Note (text " | 1932 | "Then click ✚Add Note button anytime to start composing a private Note (text " |
1638 | "post) to your Shaarli" | 1933 | "post) to your Shaarli" |
1639 | msgstr "" | 1934 | msgstr "" |
1640 | "Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" | 1935 | "Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" |
1641 | 1936 | ||
1642 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 | 1937 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 |
1643 | msgid "Add Note" | 1938 | msgid "Add Note" |
1644 | msgstr "Ajouter une Note" | 1939 | msgstr "Ajouter une Note" |
1645 | 1940 | ||
1646 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 | 1941 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132 |
1647 | msgid "3rd party" | 1942 | msgid "3rd party" |
1648 | msgstr "Applications tierces" | 1943 | msgstr "Applications tierces" |
1649 | 1944 | ||
1650 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | 1945 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 |
1651 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 | 1946 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140 |
1652 | msgid "plugin" | 1947 | msgid "plugin" |
1653 | msgstr "extension" | 1948 | msgstr "extension" |
1654 | 1949 | ||
1655 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | 1950 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 |
1656 | msgid "" | 1951 | msgid "" |
1657 | "Drag this link to your bookmarks toolbar, or right-click it and choose " | 1952 | "Drag this link to your bookmarks toolbar, or right-click it and choose " |
1658 | "Bookmark This Link" | 1953 | "Bookmark This Link" |
@@ -1660,11 +1955,11 @@ msgstr "" | |||
1660 | "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " | 1955 | "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " |
1661 | "Ajouter aux favoris »" | 1956 | "Ajouter aux favoris »" |
1662 | 1957 | ||
1663 | #~ msgid "Provided data is invalid" | 1958 | #~ msgid "Display:" |
1664 | #~ msgstr "Les informations fournies ne sont pas valides" | 1959 | #~ msgstr "Afficher :" |
1665 | 1960 | ||
1666 | #~ msgid "Rename" | 1961 | #~ msgid "The Daily Shaarli" |
1667 | #~ msgstr "Renommer" | 1962 | #~ msgstr "Le Quotidien Shaarli" |
1668 | 1963 | ||
1669 | #, fuzzy | 1964 | #, fuzzy |
1670 | #~| msgid "Selection" | 1965 | #~| msgid "Selection" |
diff --git a/inc/languages/ru/LC_MESSAGES/shaarli.po b/inc/languages/ru/LC_MESSAGES/shaarli.po new file mode 100644 index 00000000..98e70425 --- /dev/null +++ b/inc/languages/ru/LC_MESSAGES/shaarli.po | |||
@@ -0,0 +1,1944 @@ | |||
1 | msgid "" | ||
2 | msgstr "" | ||
3 | "Project-Id-Version: Shaarli\n" | ||
4 | "POT-Creation-Date: 2020-11-14 07:47+0500\n" | ||
5 | "PO-Revision-Date: 2020-11-15 06:16+0500\n" | ||
6 | "Last-Translator: progit <pash.vld@gmail.com>\n" | ||
7 | "Language-Team: Shaarli\n" | ||
8 | "Language: ru_RU\n" | ||
9 | "MIME-Version: 1.0\n" | ||
10 | "Content-Type: text/plain; charset=UTF-8\n" | ||
11 | "Content-Transfer-Encoding: 8bit\n" | ||
12 | "X-Generator: Poedit 2.0.1\n" | ||
13 | "X-Poedit-Basepath: ../../../..\n" | ||
14 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | ||
15 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | ||
16 | "X-Poedit-SourceCharset: UTF-8\n" | ||
17 | "X-Poedit-KeywordsList: t:1,2;t\n" | ||
18 | "X-Poedit-SearchPath-0: application\n" | ||
19 | "X-Poedit-SearchPath-1: tmp\n" | ||
20 | "X-Poedit-SearchPath-2: index.php\n" | ||
21 | "X-Poedit-SearchPath-3: init.php\n" | ||
22 | "X-Poedit-SearchPath-4: plugins\n" | ||
23 | |||
24 | #: application/History.php:181 | ||
25 | msgid "History file isn't readable or writable" | ||
26 | msgstr "Файл истории не доступен для чтения или записи" | ||
27 | |||
28 | #: application/History.php:192 | ||
29 | msgid "Could not parse history file" | ||
30 | msgstr "Не удалось разобрать файл истории" | ||
31 | |||
32 | #: application/Languages.php:184 | ||
33 | msgid "Automatic" | ||
34 | msgstr "Автоматический" | ||
35 | |||
36 | #: application/Languages.php:185 | ||
37 | msgid "German" | ||
38 | msgstr "Немецкий" | ||
39 | |||
40 | #: application/Languages.php:186 | ||
41 | msgid "English" | ||
42 | msgstr "Английский" | ||
43 | |||
44 | #: application/Languages.php:187 | ||
45 | msgid "French" | ||
46 | msgstr "Французский" | ||
47 | |||
48 | #: application/Languages.php:188 | ||
49 | msgid "Japanese" | ||
50 | msgstr "Японский" | ||
51 | |||
52 | #: application/Languages.php:189 | ||
53 | msgid "Russian" | ||
54 | msgstr "Русский" | ||
55 | |||
56 | #: application/Thumbnailer.php:62 | ||
57 | msgid "" | ||
58 | "php-gd extension must be loaded to use thumbnails. Thumbnails are now " | ||
59 | "disabled. Please reload the page." | ||
60 | msgstr "" | ||
61 | "для использования миниатюр необходимо загрузить расширение php-gd. Миниатюры " | ||
62 | "сейчас отключены. Перезагрузите страницу." | ||
63 | |||
64 | #: application/Utils.php:405 | ||
65 | msgid "Setting not set" | ||
66 | msgstr "Настройка не задана" | ||
67 | |||
68 | #: application/Utils.php:412 | ||
69 | msgid "Unlimited" | ||
70 | msgstr "Неограниченно" | ||
71 | |||
72 | #: application/Utils.php:415 | ||
73 | msgid "B" | ||
74 | msgstr "Б" | ||
75 | |||
76 | #: application/Utils.php:415 | ||
77 | msgid "kiB" | ||
78 | msgstr "КБ" | ||
79 | |||
80 | #: application/Utils.php:415 | ||
81 | msgid "MiB" | ||
82 | msgstr "МБ" | ||
83 | |||
84 | #: application/Utils.php:415 | ||
85 | msgid "GiB" | ||
86 | msgstr "ГБ" | ||
87 | |||
88 | #: application/bookmark/BookmarkFileService.php:185 | ||
89 | #: application/bookmark/BookmarkFileService.php:207 | ||
90 | #: application/bookmark/BookmarkFileService.php:229 | ||
91 | #: application/bookmark/BookmarkFileService.php:243 | ||
92 | msgid "You're not authorized to alter the datastore" | ||
93 | msgstr "У вас нет прав на изменение хранилища данных" | ||
94 | |||
95 | #: application/bookmark/BookmarkFileService.php:210 | ||
96 | msgid "This bookmarks already exists" | ||
97 | msgstr "Эта закладка уже существует" | ||
98 | |||
99 | #: application/bookmark/BookmarkInitializer.php:42 | ||
100 | msgid "(private bookmark with thumbnail demo)" | ||
101 | msgstr "(личная закладка с показом миниатюр)" | ||
102 | |||
103 | #: application/bookmark/BookmarkInitializer.php:45 | ||
104 | msgid "" | ||
105 | "Shaarli will automatically pick up the thumbnail for links to a variety of " | ||
106 | "websites.\n" | ||
107 | "\n" | ||
108 | "Explore your new Shaarli instance by trying out controls and menus.\n" | ||
109 | "Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the " | ||
110 | "documentation](https://shaarli.readthedocs.io/en/master/) to learn more " | ||
111 | "about Shaarli.\n" | ||
112 | "\n" | ||
113 | "Now you can edit or delete the default shaares.\n" | ||
114 | msgstr "" | ||
115 | "Shaarli автоматически подберет миниатюру для ссылок на различные сайты.\n" | ||
116 | "\n" | ||
117 | "Изучите Shaarli, попробовав элементы управления и меню.\n" | ||
118 | "Посетите проект [Github](https://github.com/shaarli/Shaarli) или " | ||
119 | "[документацию](https://shaarli.readthedocs.io/en/master/),чтобы узнать " | ||
120 | "больше о Shaarli.\n" | ||
121 | "\n" | ||
122 | "Теперь вы можете редактировать или удалять шаары по умолчанию.\n" | ||
123 | |||
124 | #: application/bookmark/BookmarkInitializer.php:58 | ||
125 | msgid "Note: Shaare descriptions" | ||
126 | msgstr "Примечание: описания Шаар" | ||
127 | |||
128 | #: application/bookmark/BookmarkInitializer.php:60 | ||
129 | msgid "" | ||
130 | "Adding a shaare without entering a URL creates a text-only \"note\" post " | ||
131 | "such as this one.\n" | ||
132 | "This note is private, so you are the only one able to see it while logged " | ||
133 | "in.\n" | ||
134 | "\n" | ||
135 | "You can use this to keep notes, post articles, code snippets, and much " | ||
136 | "more.\n" | ||
137 | "\n" | ||
138 | "The Markdown formatting setting allows you to format your notes and bookmark " | ||
139 | "description:\n" | ||
140 | "\n" | ||
141 | "### Title headings\n" | ||
142 | "\n" | ||
143 | "#### Multiple headings levels\n" | ||
144 | " * bullet lists\n" | ||
145 | " * _italic_ text\n" | ||
146 | " * **bold** text\n" | ||
147 | " * ~~strike through~~ text\n" | ||
148 | " * `code` blocks\n" | ||
149 | " * images\n" | ||
150 | " * [links](https://en.wikipedia.org/wiki/Markdown)\n" | ||
151 | "\n" | ||
152 | "Markdown also supports tables:\n" | ||
153 | "\n" | ||
154 | "| Name | Type | Color | Qty |\n" | ||
155 | "| ------- | --------- | ------ | ----- |\n" | ||
156 | "| Orange | Fruit | Orange | 126 |\n" | ||
157 | "| Apple | Fruit | Any | 62 |\n" | ||
158 | "| Lemon | Fruit | Yellow | 30 |\n" | ||
159 | "| Carrot | Vegetable | Red | 14 |\n" | ||
160 | msgstr "" | ||
161 | "При добавлении закладки без ввода URL адреса создается текстовая \"заметка" | ||
162 | "\", такая как эта.\n" | ||
163 | "Эта заметка является личной, поэтому вы единственный, кто может ее увидеть, " | ||
164 | "находясь в системе.\n" | ||
165 | "\n" | ||
166 | "Вы можете использовать это для хранения заметок, публикации статей, " | ||
167 | "фрагментов кода и многого другого.\n" | ||
168 | "\n" | ||
169 | "Параметр форматирования Markdown позволяет форматировать заметки и описание " | ||
170 | "закладок:\n" | ||
171 | "\n" | ||
172 | "### Заголовок заголовков\n" | ||
173 | "\n" | ||
174 | "#### Multiple headings levels\n" | ||
175 | " * маркированные списки\n" | ||
176 | " * _наклонный_ текст\n" | ||
177 | " * **жирный** текст\n" | ||
178 | " * ~~зачеркнутый~~ текст\n" | ||
179 | " * блоки `кода`\n" | ||
180 | " * изображения\n" | ||
181 | " * [ссылки](https://en.wikipedia.org/wiki/Markdown)\n" | ||
182 | "\n" | ||
183 | "Markdown также поддерживает таблицы:\n" | ||
184 | "\n" | ||
185 | "| Имя | Тип | Цвет | Количество |\n" | ||
186 | "| ------- | --------- | ------ | ----- |\n" | ||
187 | "| Апельсин | Фрукт | Оранжевый | 126 |\n" | ||
188 | "| Яблоко | Фрукт | Любой | 62 |\n" | ||
189 | "| Лимон | Фрукт | Желтый | 30 |\n" | ||
190 | "| Морковь | Овощ | Красный | 14 |\n" | ||
191 | |||
192 | #: application/bookmark/BookmarkInitializer.php:94 | ||
193 | #: application/legacy/LegacyLinkDB.php:246 | ||
194 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
195 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
196 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 | ||
197 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 | ||
198 | msgid "" | ||
199 | "The personal, minimalist, super-fast, database free, bookmarking service" | ||
200 | msgstr "Личный, минималистичный, сверхбыстрый сервис закладок без баз данных" | ||
201 | |||
202 | #: application/bookmark/BookmarkInitializer.php:97 | ||
203 | msgid "" | ||
204 | "Welcome to Shaarli!\n" | ||
205 | "\n" | ||
206 | "Shaarli allows you to bookmark your favorite pages, and share them with " | ||
207 | "others or store them privately.\n" | ||
208 | "You can add a description to your bookmarks, such as this one, and tag " | ||
209 | "them.\n" | ||
210 | "\n" | ||
211 | "Create a new shaare by clicking the `+Shaare` button, or using any of the " | ||
212 | "recommended tools (browser extension, mobile app, bookmarklet, REST API, " | ||
213 | "etc.).\n" | ||
214 | "\n" | ||
215 | "You can easily retrieve your links, even with thousands of them, using the " | ||
216 | "internal search engine, or search through tags (e.g. this Shaare is tagged " | ||
217 | "with `shaarli` and `help`).\n" | ||
218 | "Hashtags such as #shaarli #help are also supported.\n" | ||
219 | "You can also filter the available [RSS feed](/feed/atom) and picture wall by " | ||
220 | "tag or plaintext search.\n" | ||
221 | "\n" | ||
222 | "We hope that you will enjoy using Shaarli, maintained with ❤️ by the " | ||
223 | "community!\n" | ||
224 | "Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if " | ||
225 | "you have a suggestion or encounter an issue.\n" | ||
226 | msgstr "" | ||
227 | "Добро пожаловать в Shaarli!\n" | ||
228 | "\n" | ||
229 | "Shaarli позволяет добавлять в закладки свои любимые страницы и делиться ими " | ||
230 | "с другими или хранить их в частном порядке.\n" | ||
231 | "Вы можете добавить описание к своим закладкам, например этой, и пометить " | ||
232 | "их.\n" | ||
233 | "\n" | ||
234 | "Создайте новую закладку, нажав кнопку `+Поделиться`, или используя любой из " | ||
235 | "рекомендуемых инструментов (расширение для браузера, мобильное приложение, " | ||
236 | "букмарклет, REST API и т.д.).\n" | ||
237 | "\n" | ||
238 | "Вы можете легко получить свои ссылки, даже если их тысячи, с помощью " | ||
239 | "внутренней поисковой системы или поиска по тегам (например, эта заметка " | ||
240 | "помечена тегами `shaarli` and `help`).\n" | ||
241 | "Также поддерживаются хэштеги, такие как #shaarli #help.\n" | ||
242 | "Вы можете также фильтровать доступный [RSS канал](/feed/atom) и галерею по " | ||
243 | "тегу или по поиску текста.\n" | ||
244 | "\n" | ||
245 | "Мы надеемся, что вам понравится использовать Shaarli, с ❤️ поддерживаемый " | ||
246 | "сообществом!\n" | ||
247 | "Не стесняйтесь открывать [запрос](https://github.com/shaarli/Shaarli/" | ||
248 | "issues), если у вас есть предложение или возникла проблема.\n" | ||
249 | |||
250 | #: application/bookmark/exception/BookmarkNotFoundException.php:14 | ||
251 | msgid "The link you are trying to reach does not exist or has been deleted." | ||
252 | msgstr "" | ||
253 | "Ссылка, по которой вы пытаетесь перейти, не существует или была удалена." | ||
254 | |||
255 | #: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:131 | ||
256 | msgid "" | ||
257 | "Shaarli could not create the config file. Please make sure Shaarli has the " | ||
258 | "right to write in the folder is it installed in." | ||
259 | msgstr "" | ||
260 | "Shaarli не удалось создать файл конфигурации. Убедитесь, что у Shaarli есть " | ||
261 | "право на запись в папку, в которой он установлен." | ||
262 | |||
263 | #: application/config/ConfigManager.php:137 | ||
264 | #: application/config/ConfigManager.php:164 | ||
265 | msgid "Invalid setting key parameter. String expected, got: " | ||
266 | msgstr "Неверная настройка ключевого параметра. Ожидалась строка, получено: " | ||
267 | |||
268 | #: application/config/exception/MissingFieldConfigException.php:20 | ||
269 | #, php-format | ||
270 | msgid "Configuration value is required for %s" | ||
271 | msgstr "Значение конфигурации требуется для %s" | ||
272 | |||
273 | #: application/config/exception/PluginConfigOrderException.php:15 | ||
274 | msgid "An error occurred while trying to save plugins loading order." | ||
275 | msgstr "Произошла ошибка при попытке сохранить порядок загрузки плагинов." | ||
276 | |||
277 | #: application/config/exception/UnauthorizedConfigException.php:15 | ||
278 | msgid "You are not authorized to alter config." | ||
279 | msgstr "Вы не авторизованы для изменения конфигурации." | ||
280 | |||
281 | #: application/exceptions/IOException.php:23 | ||
282 | msgid "Error accessing" | ||
283 | msgstr "Ошибка доступа" | ||
284 | |||
285 | #: application/feed/FeedBuilder.php:180 | ||
286 | msgid "Direct link" | ||
287 | msgstr "Прямая ссылка" | ||
288 | |||
289 | #: application/feed/FeedBuilder.php:182 | ||
290 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 | ||
291 | #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | ||
292 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 | ||
293 | msgid "Permalink" | ||
294 | msgstr "Постоянная ссылка" | ||
295 | |||
296 | #: application/front/controller/admin/ConfigureController.php:56 | ||
297 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
298 | msgid "Configure" | ||
299 | msgstr "Настройка" | ||
300 | |||
301 | #: application/front/controller/admin/ConfigureController.php:106 | ||
302 | #: application/legacy/LegacyUpdater.php:539 | ||
303 | msgid "You have enabled or changed thumbnails mode." | ||
304 | msgstr "Вы включили или изменили режим миниатюр." | ||
305 | |||
306 | #: application/front/controller/admin/ConfigureController.php:108 | ||
307 | #: application/front/controller/admin/ServerController.php:76 | ||
308 | #: application/legacy/LegacyUpdater.php:540 | ||
309 | msgid "Please synchronize them." | ||
310 | msgstr "Пожалуйста, синхронизируйте их." | ||
311 | |||
312 | #: application/front/controller/admin/ConfigureController.php:119 | ||
313 | #: application/front/controller/visitor/InstallController.php:149 | ||
314 | msgid "Error while writing config file after configuration update." | ||
315 | msgstr "Ошибка при записи файла конфигурации после обновления конфигурации." | ||
316 | |||
317 | #: application/front/controller/admin/ConfigureController.php:128 | ||
318 | msgid "Configuration was saved." | ||
319 | msgstr "Конфигурация сохранена." | ||
320 | |||
321 | #: application/front/controller/admin/ExportController.php:26 | ||
322 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 | ||
323 | msgid "Export" | ||
324 | msgstr "Экспорт" | ||
325 | |||
326 | #: application/front/controller/admin/ExportController.php:42 | ||
327 | msgid "Please select an export mode." | ||
328 | msgstr "Выберите режим экспорта." | ||
329 | |||
330 | #: application/front/controller/admin/ImportController.php:41 | ||
331 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 | ||
332 | msgid "Import" | ||
333 | msgstr "Импорт" | ||
334 | |||
335 | #: application/front/controller/admin/ImportController.php:55 | ||
336 | msgid "No import file provided." | ||
337 | msgstr "Файл импорта не предоставлен." | ||
338 | |||
339 | #: application/front/controller/admin/ImportController.php:66 | ||
340 | #, php-format | ||
341 | msgid "" | ||
342 | "The file you are trying to upload is probably bigger than what this " | ||
343 | "webserver can accept (%s). Please upload in smaller chunks." | ||
344 | msgstr "" | ||
345 | "Файл, который вы пытаетесь загрузить, вероятно, больше, чем может принять " | ||
346 | "этот сервер (%s). Пожалуйста, загружайте небольшими частями." | ||
347 | |||
348 | #: application/front/controller/admin/ManageTagController.php:30 | ||
349 | msgid "whitespace" | ||
350 | msgstr "пробел" | ||
351 | |||
352 | #: application/front/controller/admin/ManageTagController.php:35 | ||
353 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
354 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
355 | msgid "Manage tags" | ||
356 | msgstr "Управление тегами" | ||
357 | |||
358 | #: application/front/controller/admin/ManageTagController.php:54 | ||
359 | msgid "Invalid tags provided." | ||
360 | msgstr "Предоставлены недействительные теги." | ||
361 | |||
362 | #: application/front/controller/admin/ManageTagController.php:78 | ||
363 | #, php-format | ||
364 | msgid "The tag was removed from %d bookmark." | ||
365 | msgid_plural "The tag was removed from %d bookmarks." | ||
366 | msgstr[0] "Тег был удален из %d закладки." | ||
367 | msgstr[1] "Тег был удален из %d закладок." | ||
368 | msgstr[2] "Тег был удален из %d закладок." | ||
369 | |||
370 | #: application/front/controller/admin/ManageTagController.php:83 | ||
371 | #, php-format | ||
372 | msgid "The tag was renamed in %d bookmark." | ||
373 | msgid_plural "The tag was renamed in %d bookmarks." | ||
374 | msgstr[0] "Тег был переименован в %d закладке." | ||
375 | msgstr[1] "Тег был переименован в %d закладках." | ||
376 | msgstr[2] "Тег был переименован в %d закладках." | ||
377 | |||
378 | #: application/front/controller/admin/ManageTagController.php:105 | ||
379 | msgid "Tags separator must be a single character." | ||
380 | msgstr "Разделитель тегов должен состоять из одного символа." | ||
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 | "Эти символы зарезервированы и не могут использоваться в качестве разделителя " | ||
386 | "тегов: " | ||
387 | |||
388 | #: application/front/controller/admin/PasswordController.php:28 | ||
389 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
390 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
391 | msgid "Change password" | ||
392 | msgstr "Изменить пароль" | ||
393 | |||
394 | #: application/front/controller/admin/PasswordController.php:55 | ||
395 | msgid "You must provide the current and new password to change it." | ||
396 | msgstr "Вы должны предоставить текущий и новый пароль, чтобы изменить его." | ||
397 | |||
398 | #: application/front/controller/admin/PasswordController.php:71 | ||
399 | msgid "The old password is not correct." | ||
400 | msgstr "Старый пароль неверен." | ||
401 | |||
402 | #: application/front/controller/admin/PasswordController.php:97 | ||
403 | msgid "Your password has been changed" | ||
404 | msgstr "Пароль изменен" | ||
405 | |||
406 | #: application/front/controller/admin/PluginsController.php:45 | ||
407 | msgid "Plugin Administration" | ||
408 | msgstr "Управление плагинами" | ||
409 | |||
410 | #: application/front/controller/admin/PluginsController.php:76 | ||
411 | msgid "Setting successfully saved." | ||
412 | msgstr "Настройка успешно сохранена." | ||
413 | |||
414 | #: application/front/controller/admin/PluginsController.php:79 | ||
415 | msgid "Error while saving plugin configuration: " | ||
416 | msgstr "Ошибка при сохранении конфигурации плагина: " | ||
417 | |||
418 | #: application/front/controller/admin/ServerController.php:35 | ||
419 | msgid "Check disabled" | ||
420 | msgstr "Проверка отключена" | ||
421 | |||
422 | #: application/front/controller/admin/ServerController.php:57 | ||
423 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
424 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
425 | msgid "Server administration" | ||
426 | msgstr "Администрирование сервера" | ||
427 | |||
428 | #: application/front/controller/admin/ServerController.php:74 | ||
429 | msgid "Thumbnails cache has been cleared." | ||
430 | msgstr "Кэш миниатюр очищен." | ||
431 | |||
432 | #: application/front/controller/admin/ServerController.php:85 | ||
433 | msgid "Shaarli's cache folder has been cleared!" | ||
434 | msgstr "Папка с кэшем Shaarli очищена!" | ||
435 | |||
436 | #: application/front/controller/admin/ShaareAddController.php:26 | ||
437 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
438 | msgid "Shaare a new link" | ||
439 | msgstr "Поделиться новой ссылкой" | ||
440 | |||
441 | #: application/front/controller/admin/ShaareManageController.php:35 | ||
442 | #: application/front/controller/admin/ShaareManageController.php:93 | ||
443 | msgid "Invalid bookmark ID provided." | ||
444 | msgstr "Указан неверный идентификатор закладки." | ||
445 | |||
446 | #: application/front/controller/admin/ShaareManageController.php:47 | ||
447 | #: application/front/controller/admin/ShaareManageController.php:116 | ||
448 | #: application/front/controller/admin/ShaareManageController.php:156 | ||
449 | #: application/front/controller/admin/ShaarePublishController.php:82 | ||
450 | #, php-format | ||
451 | msgid "Bookmark with identifier %s could not be found." | ||
452 | msgstr "Закладка с идентификатором %s не найдена." | ||
453 | |||
454 | #: application/front/controller/admin/ShaareManageController.php:101 | ||
455 | msgid "Invalid visibility provided." | ||
456 | msgstr "Предоставлена недопустимая видимость." | ||
457 | |||
458 | #: application/front/controller/admin/ShaarePublishController.php:173 | ||
459 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 | ||
460 | msgid "Edit" | ||
461 | msgstr "Редактировать" | ||
462 | |||
463 | #: application/front/controller/admin/ShaarePublishController.php:176 | ||
464 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
465 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 | ||
466 | msgid "Shaare" | ||
467 | msgstr "Поделиться" | ||
468 | |||
469 | #: application/front/controller/admin/ShaarePublishController.php:208 | ||
470 | msgid "Note: " | ||
471 | msgstr "Заметка: " | ||
472 | |||
473 | #: application/front/controller/admin/ThumbnailsController.php:37 | ||
474 | #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
475 | msgid "Thumbnails update" | ||
476 | msgstr "Обновление миниатюр" | ||
477 | |||
478 | #: application/front/controller/admin/ToolsController.php:31 | ||
479 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 | ||
480 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33 | ||
481 | msgid "Tools" | ||
482 | msgstr "Инструменты" | ||
483 | |||
484 | #: application/front/controller/visitor/BookmarkListController.php:121 | ||
485 | msgid "Search: " | ||
486 | msgstr "Поиск: " | ||
487 | |||
488 | #: application/front/controller/visitor/DailyController.php:200 | ||
489 | msgid "day" | ||
490 | msgstr "день" | ||
491 | |||
492 | #: application/front/controller/visitor/DailyController.php:200 | ||
493 | #: application/front/controller/visitor/DailyController.php:203 | ||
494 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
495 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
496 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 | ||
497 | msgid "Daily" | ||
498 | msgstr "За день" | ||
499 | |||
500 | #: application/front/controller/visitor/DailyController.php:201 | ||
501 | msgid "week" | ||
502 | msgstr "неделя" | ||
503 | |||
504 | #: application/front/controller/visitor/DailyController.php:201 | ||
505 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
506 | msgid "Weekly" | ||
507 | msgstr "За неделю" | ||
508 | |||
509 | #: application/front/controller/visitor/DailyController.php:202 | ||
510 | msgid "month" | ||
511 | msgstr "месяц" | ||
512 | |||
513 | #: application/front/controller/visitor/DailyController.php:202 | ||
514 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
515 | msgid "Monthly" | ||
516 | msgstr "За месяц" | ||
517 | |||
518 | #: application/front/controller/visitor/ErrorController.php:30 | ||
519 | msgid "Error: " | ||
520 | msgstr "Ошибка: " | ||
521 | |||
522 | #: application/front/controller/visitor/ErrorController.php:34 | ||
523 | msgid "Please report it on Github." | ||
524 | msgstr "Пожалуйста, сообщите об этом на Github." | ||
525 | |||
526 | #: application/front/controller/visitor/ErrorController.php:39 | ||
527 | msgid "An unexpected error occurred." | ||
528 | msgstr "Произошла непредвиденная ошибка." | ||
529 | |||
530 | #: application/front/controller/visitor/ErrorNotFoundController.php:25 | ||
531 | msgid "Requested page could not be found." | ||
532 | msgstr "Запрошенная страница не может быть найдена." | ||
533 | |||
534 | #: application/front/controller/visitor/InstallController.php:65 | ||
535 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | ||
536 | msgid "Install Shaarli" | ||
537 | msgstr "Установить Shaarli" | ||
538 | |||
539 | #: application/front/controller/visitor/InstallController.php:85 | ||
540 | #, php-format | ||
541 | msgid "" | ||
542 | "<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " | ||
543 | "variable \"session.save_path\" is set correctly in your PHP config, and that " | ||
544 | "you have write access to it.<br>It currently points to %s.<br>On some " | ||
545 | "browsers, accessing your server via a hostname like 'localhost' or any " | ||
546 | "custom hostname without a dot causes cookie storage to fail. We recommend " | ||
547 | "accessing your server via it's IP address or Fully Qualified Domain Name.<br>" | ||
548 | msgstr "" | ||
549 | "<pre>Сессии на вашем сервере работают некорректно.<br>Убедитесь, что " | ||
550 | "переменная \"session.save_path\" правильно установлена в вашей конфигурации " | ||
551 | "PHP и что у вас есть доступ к ней на запись.<br>В настоящее время она " | ||
552 | "указывает на %s.<br>В некоторых браузерах доступ к вашему серверу через имя " | ||
553 | "хоста, например localhost или любое другое имя хоста без точки, приводит к " | ||
554 | "сбою хранилища файлов cookie. Мы рекомендуем получить доступ к вашему " | ||
555 | "серверу через его IP адрес или полное доменное имя.<br>" | ||
556 | |||
557 | #: application/front/controller/visitor/InstallController.php:157 | ||
558 | msgid "" | ||
559 | "Shaarli is now configured. Please login and start shaaring your bookmarks!" | ||
560 | msgstr "Shaarli настроен. Войдите и начните делиться своими закладками!" | ||
561 | |||
562 | #: application/front/controller/visitor/InstallController.php:171 | ||
563 | msgid "Insufficient permissions:" | ||
564 | msgstr "Недостаточно разрешений:" | ||
565 | |||
566 | #: application/front/controller/visitor/LoginController.php:46 | ||
567 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
568 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
569 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 | ||
570 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 | ||
571 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77 | ||
572 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101 | ||
573 | msgid "Login" | ||
574 | msgstr "Вход" | ||
575 | |||
576 | #: application/front/controller/visitor/LoginController.php:78 | ||
577 | msgid "Wrong login/password." | ||
578 | msgstr "Неверный логин или пароль." | ||
579 | |||
580 | #: application/front/controller/visitor/PictureWallController.php:29 | ||
581 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
582 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43 | ||
583 | msgid "Picture wall" | ||
584 | msgstr "Галерея" | ||
585 | |||
586 | #: application/front/controller/visitor/TagCloudController.php:90 | ||
587 | msgid "Tag " | ||
588 | msgstr "Тег " | ||
589 | |||
590 | #: application/front/exceptions/AlreadyInstalledException.php:11 | ||
591 | msgid "Shaarli has already been installed. Login to edit the configuration." | ||
592 | msgstr "Shaarli уже установлен. Войдите, чтобы изменить конфигурацию." | ||
593 | |||
594 | #: application/front/exceptions/LoginBannedException.php:11 | ||
595 | msgid "" | ||
596 | "You have been banned after too many failed login attempts. Try again later." | ||
597 | msgstr "" | ||
598 | "Вы были заблокированы из-за большого количества неудачных попыток входа в " | ||
599 | "систему. Попробуйте позже." | ||
600 | |||
601 | #: application/front/exceptions/OpenShaarliPasswordException.php:16 | ||
602 | msgid "You are not supposed to change a password on an Open Shaarli." | ||
603 | msgstr "Вы не должны менять пароль на Open Shaarli." | ||
604 | |||
605 | #: application/front/exceptions/ThumbnailsDisabledException.php:11 | ||
606 | msgid "Picture wall unavailable (thumbnails are disabled)." | ||
607 | msgstr "Галерея недоступна (миниатюры отключены)." | ||
608 | |||
609 | #: application/front/exceptions/WrongTokenException.php:16 | ||
610 | msgid "Wrong token." | ||
611 | msgstr "Неправильный токен." | ||
612 | |||
613 | #: application/helper/ApplicationUtils.php:163 | ||
614 | #, php-format | ||
615 | msgid "" | ||
616 | "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " | ||
617 | "cannot run. Your PHP version has known security vulnerabilities and should " | ||
618 | "be updated as soon as possible." | ||
619 | msgstr "" | ||
620 | "Ваша версия PHP устарела! Shaarli требует как минимум PHP %s, и поэтому не " | ||
621 | "может работать. В вашей версии PHP есть известные уязвимости в системе " | ||
622 | "безопасности, и ее следует обновить как можно скорее." | ||
623 | |||
624 | #: application/helper/ApplicationUtils.php:198 | ||
625 | #: application/helper/ApplicationUtils.php:218 | ||
626 | msgid "directory is not readable" | ||
627 | msgstr "папка не доступна для чтения" | ||
628 | |||
629 | #: application/helper/ApplicationUtils.php:221 | ||
630 | msgid "directory is not writable" | ||
631 | msgstr "папка не доступна для записи" | ||
632 | |||
633 | #: application/helper/ApplicationUtils.php:245 | ||
634 | msgid "file is not readable" | ||
635 | msgstr "файл не доступен для чтения" | ||
636 | |||
637 | #: application/helper/ApplicationUtils.php:248 | ||
638 | msgid "file is not writable" | ||
639 | msgstr "файл не доступен для записи" | ||
640 | |||
641 | #: application/helper/ApplicationUtils.php:282 | ||
642 | msgid "Configuration parsing" | ||
643 | msgstr "Разбор конфигурации" | ||
644 | |||
645 | #: application/helper/ApplicationUtils.php:283 | ||
646 | msgid "Slim Framework (routing, etc.)" | ||
647 | msgstr "Slim Framework (маршрутизация и т. д.)" | ||
648 | |||
649 | #: application/helper/ApplicationUtils.php:284 | ||
650 | msgid "Multibyte (Unicode) string support" | ||
651 | msgstr "Поддержка многобайтовых (Unicode) строк" | ||
652 | |||
653 | #: application/helper/ApplicationUtils.php:285 | ||
654 | msgid "Required to use thumbnails" | ||
655 | msgstr "Обязательно использование миниатюр" | ||
656 | |||
657 | #: application/helper/ApplicationUtils.php:286 | ||
658 | msgid "Localized text sorting (e.g. e->è->f)" | ||
659 | msgstr "Локализованная сортировка текста (например, e->è->f)" | ||
660 | |||
661 | #: application/helper/ApplicationUtils.php:287 | ||
662 | msgid "Better retrieval of bookmark metadata and thumbnail" | ||
663 | msgstr "Лучшее получение метаданных закладок и миниатюр" | ||
664 | |||
665 | #: application/helper/ApplicationUtils.php:288 | ||
666 | msgid "Use the translation system in gettext mode" | ||
667 | msgstr "Используйте систему перевода в режиме gettext" | ||
668 | |||
669 | #: application/helper/ApplicationUtils.php:289 | ||
670 | msgid "Login using LDAP server" | ||
671 | msgstr "Вход через LDAP сервер" | ||
672 | |||
673 | #: application/helper/DailyPageHelper.php:172 | ||
674 | msgid "Week" | ||
675 | msgstr "Неделя" | ||
676 | |||
677 | #: application/helper/DailyPageHelper.php:176 | ||
678 | msgid "Today" | ||
679 | msgstr "Сегодня" | ||
680 | |||
681 | #: application/helper/DailyPageHelper.php:178 | ||
682 | msgid "Yesterday" | ||
683 | msgstr "Вчера" | ||
684 | |||
685 | #: application/helper/FileUtils.php:100 | ||
686 | msgid "Provided path is not a directory." | ||
687 | msgstr "Указанный путь не является папкой." | ||
688 | |||
689 | #: application/helper/FileUtils.php:104 | ||
690 | msgid "Trying to delete a folder outside of Shaarli path." | ||
691 | msgstr "Попытка удалить папку за пределами пути Shaarli." | ||
692 | |||
693 | #: application/legacy/LegacyLinkDB.php:131 | ||
694 | msgid "You are not authorized to add a link." | ||
695 | msgstr "Вы не авторизованы для изменения ссылки." | ||
696 | |||
697 | #: application/legacy/LegacyLinkDB.php:134 | ||
698 | msgid "Internal Error: A link should always have an id and URL." | ||
699 | msgstr "Внутренняя ошибка: ссылка всегда должна иметь идентификатор и URL." | ||
700 | |||
701 | #: application/legacy/LegacyLinkDB.php:137 | ||
702 | msgid "You must specify an integer as a key." | ||
703 | msgstr "В качестве ключа необходимо указать целое число." | ||
704 | |||
705 | #: application/legacy/LegacyLinkDB.php:140 | ||
706 | msgid "Array offset and link ID must be equal." | ||
707 | msgstr "Смещение массива и идентификатор ссылки должны быть одинаковыми." | ||
708 | |||
709 | #: application/legacy/LegacyLinkDB.php:249 | ||
710 | msgid "" | ||
711 | "Welcome to Shaarli! This is your first public bookmark. To edit or delete " | ||
712 | "me, you must first login.\n" | ||
713 | "\n" | ||
714 | "To learn how to use Shaarli, consult the link \"Documentation\" at the " | ||
715 | "bottom of this page.\n" | ||
716 | "\n" | ||
717 | "You use the community supported version of the original Shaarli project, by " | ||
718 | "Sebastien Sauvage." | ||
719 | msgstr "" | ||
720 | "Добро пожаловать в Shaarli! Это ваша первая общедоступная закладка. Чтобы " | ||
721 | "отредактировать или удалить меня, вы должны сначала авторизоваться.\n" | ||
722 | "\n" | ||
723 | "Чтобы узнать, как использовать Shaarli, перейдите по ссылке \"Документация\" " | ||
724 | "внизу этой страницы.\n" | ||
725 | "\n" | ||
726 | "Вы используете поддерживаемую сообществом версию оригинального проекта " | ||
727 | "Shaarli от Себастьяна Соваж." | ||
728 | |||
729 | #: application/legacy/LegacyLinkDB.php:266 | ||
730 | msgid "My secret stuff... - Pastebin.com" | ||
731 | msgstr "Мой секрет... - Pastebin.com" | ||
732 | |||
733 | #: application/legacy/LegacyLinkDB.php:268 | ||
734 | msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." | ||
735 | msgstr "" | ||
736 | "Тссс! Это личная ссылка, которую видите только ВЫ. Вы тоже можете удалить " | ||
737 | "меня." | ||
738 | |||
739 | #: application/legacy/LegacyUpdater.php:104 | ||
740 | msgid "Couldn't retrieve updater class methods." | ||
741 | msgstr "Не удалось получить методы класса средства обновления." | ||
742 | |||
743 | #: application/legacy/LegacyUpdater.php:540 | ||
744 | msgid "<a href=\"./admin/thumbnails\">" | ||
745 | msgstr "<a href=\"./admin/thumbnails\">" | ||
746 | |||
747 | #: application/netscape/NetscapeBookmarkUtils.php:63 | ||
748 | msgid "Invalid export selection:" | ||
749 | msgstr "Неверный выбор экспорта:" | ||
750 | |||
751 | #: application/netscape/NetscapeBookmarkUtils.php:215 | ||
752 | #, php-format | ||
753 | msgid "File %s (%d bytes) " | ||
754 | msgstr "Файл %s (%d байт) " | ||
755 | |||
756 | #: application/netscape/NetscapeBookmarkUtils.php:217 | ||
757 | msgid "has an unknown file format. Nothing was imported." | ||
758 | msgstr "имеет неизвестный формат файла. Ничего не импортировано." | ||
759 | |||
760 | #: application/netscape/NetscapeBookmarkUtils.php:221 | ||
761 | #, php-format | ||
762 | msgid "" | ||
763 | "was successfully processed in %d seconds: %d bookmarks imported, %d " | ||
764 | "bookmarks overwritten, %d bookmarks skipped." | ||
765 | msgstr "" | ||
766 | "успешно обработано за %d секунд: %d закладок импортировано, %d закладок " | ||
767 | "перезаписаны, %d закладок пропущено." | ||
768 | |||
769 | #: application/plugin/PluginManager.php:125 | ||
770 | msgid " [plugin incompatibility]: " | ||
771 | msgstr " [несовместимость плагинов]: " | ||
772 | |||
773 | #: application/plugin/exception/PluginFileNotFoundException.php:22 | ||
774 | #, php-format | ||
775 | msgid "Plugin \"%s\" files not found." | ||
776 | msgstr "Файл плагина \"%s\" не найден." | ||
777 | |||
778 | #: application/render/PageCacheManager.php:32 | ||
779 | #, php-format | ||
780 | msgid "Cannot purge %s: no directory" | ||
781 | msgstr "Невозможно очистить%s: нет папки" | ||
782 | |||
783 | #: application/updater/exception/UpdaterException.php:51 | ||
784 | msgid "An error occurred while running the update " | ||
785 | msgstr "Произошла ошибка при запуске обновления " | ||
786 | |||
787 | #: index.php:81 | ||
788 | msgid "Shared bookmarks on " | ||
789 | msgstr "Общие закладки на " | ||
790 | |||
791 | #: plugins/addlink_toolbar/addlink_toolbar.php:31 | ||
792 | msgid "URI" | ||
793 | msgstr "URI" | ||
794 | |||
795 | #: plugins/addlink_toolbar/addlink_toolbar.php:35 | ||
796 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 | ||
797 | msgid "Add link" | ||
798 | msgstr "Добавить ссылку" | ||
799 | |||
800 | #: plugins/addlink_toolbar/addlink_toolbar.php:52 | ||
801 | msgid "Adds the addlink input on the linklist page." | ||
802 | msgstr "" | ||
803 | "Добавляет на страницу списка ссылок поле для добавления новой закладки." | ||
804 | |||
805 | #: plugins/archiveorg/archiveorg.php:29 | ||
806 | msgid "View on archive.org" | ||
807 | msgstr "Посмотреть на archive.org" | ||
808 | |||
809 | #: plugins/archiveorg/archiveorg.php:42 | ||
810 | msgid "For each link, add an Archive.org icon." | ||
811 | msgstr "Для каждой ссылки добавить значок с Archive.org." | ||
812 | |||
813 | #: plugins/default_colors/default_colors.php:38 | ||
814 | msgid "" | ||
815 | "Default colors plugin error: This plugin is active and no custom color is " | ||
816 | "configured." | ||
817 | msgstr "" | ||
818 | "Ошибка плагина цветов по умолчанию: этот плагин активен, и пользовательский " | ||
819 | "цвет не настроен." | ||
820 | |||
821 | #: plugins/default_colors/default_colors.php:113 | ||
822 | msgid "Override default theme colors. Use any CSS valid color." | ||
823 | msgstr "" | ||
824 | "Переопределить цвета темы по умолчанию. Используйте любой допустимый цвет " | ||
825 | "CSS." | ||
826 | |||
827 | #: plugins/default_colors/default_colors.php:114 | ||
828 | msgid "Main color (navbar green)" | ||
829 | msgstr "Основной цвет (зеленый на панели навигации)" | ||
830 | |||
831 | #: plugins/default_colors/default_colors.php:115 | ||
832 | msgid "Background color (light grey)" | ||
833 | msgstr "Цвет фона (светло-серый)" | ||
834 | |||
835 | #: plugins/default_colors/default_colors.php:116 | ||
836 | msgid "Dark main color (e.g. visited links)" | ||
837 | msgstr "Темный основной цвет (например, посещенные ссылки)" | ||
838 | |||
839 | #: plugins/demo_plugin/demo_plugin.php:478 | ||
840 | msgid "" | ||
841 | "A demo plugin covering all use cases for template designers and plugin " | ||
842 | "developers." | ||
843 | msgstr "" | ||
844 | "Демо плагин, охватывающий все варианты использования для дизайнеров шаблонов " | ||
845 | "и разработчиков плагинов." | ||
846 | |||
847 | #: plugins/demo_plugin/demo_plugin.php:479 | ||
848 | msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." | ||
849 | msgstr "" | ||
850 | "Это параметр предназначен для демонстрационного плагина. Это будет суффикс." | ||
851 | |||
852 | #: plugins/demo_plugin/demo_plugin.php:480 | ||
853 | msgid "Other demo parameter" | ||
854 | msgstr "Другой демонстрационный параметр" | ||
855 | |||
856 | #: plugins/isso/isso.php:22 | ||
857 | msgid "" | ||
858 | "Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " | ||
859 | "administration page." | ||
860 | msgstr "" | ||
861 | "Ошибка плагина Isso: определите параметр \"ISSO_SERVER\" на странице " | ||
862 | "настройки плагина." | ||
863 | |||
864 | #: plugins/isso/isso.php:92 | ||
865 | msgid "Let visitor comment your shaares on permalinks with Isso." | ||
866 | msgstr "" | ||
867 | "Позволить посетителю комментировать ваши закладки по постоянным ссылкам с " | ||
868 | "Isso." | ||
869 | |||
870 | #: plugins/isso/isso.php:93 | ||
871 | msgid "Isso server URL (without 'http://')" | ||
872 | msgstr "URL сервера Isso (без 'http: //')" | ||
873 | |||
874 | #: plugins/piwik/piwik.php:24 | ||
875 | msgid "" | ||
876 | "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " | ||
877 | "administration page." | ||
878 | msgstr "" | ||
879 | "Ошибка плагина Piwik: укажите PIWIK_URL и PIWIK_SITEID на странице настройки " | ||
880 | "плагина." | ||
881 | |||
882 | #: plugins/piwik/piwik.php:73 | ||
883 | msgid "A plugin that adds Piwik tracking code to Shaarli pages." | ||
884 | msgstr "Плагин, который добавляет код отслеживания Piwik на страницы Shaarli." | ||
885 | |||
886 | #: plugins/piwik/piwik.php:74 | ||
887 | msgid "Piwik URL" | ||
888 | msgstr "Piwik URL" | ||
889 | |||
890 | #: plugins/piwik/piwik.php:75 | ||
891 | msgid "Piwik site ID" | ||
892 | msgstr "Piwik site ID" | ||
893 | |||
894 | #: plugins/playvideos/playvideos.php:26 | ||
895 | msgid "Video player" | ||
896 | msgstr "Видео плеер" | ||
897 | |||
898 | #: plugins/playvideos/playvideos.php:29 | ||
899 | msgid "Play Videos" | ||
900 | msgstr "Воспроизвести видео" | ||
901 | |||
902 | #: plugins/playvideos/playvideos.php:60 | ||
903 | msgid "Add a button in the toolbar allowing to watch all videos." | ||
904 | msgstr "" | ||
905 | "Добавьте кнопку на панель инструментов, позволяющую смотреть все видео." | ||
906 | |||
907 | #: plugins/playvideos/youtube_playlist.js:214 | ||
908 | msgid "plugins/playvideos/jquery-1.11.2.min.js" | ||
909 | msgstr "plugins/playvideos/jquery-1.11.2.min.js" | ||
910 | |||
911 | #: plugins/pubsubhubbub/pubsubhubbub.php:72 | ||
912 | #, php-format | ||
913 | msgid "Could not publish to PubSubHubbub: %s" | ||
914 | msgstr "Не удалось опубликовать в PubSubHubbub: %s" | ||
915 | |||
916 | #: plugins/pubsubhubbub/pubsubhubbub.php:99 | ||
917 | #, php-format | ||
918 | msgid "Could not post to %s" | ||
919 | msgstr "Не удалось отправить сообщение в %s" | ||
920 | |||
921 | #: plugins/pubsubhubbub/pubsubhubbub.php:103 | ||
922 | #, php-format | ||
923 | msgid "Bad response from the hub %s" | ||
924 | msgstr "Плохой ответ от хаба %s" | ||
925 | |||
926 | #: plugins/pubsubhubbub/pubsubhubbub.php:114 | ||
927 | msgid "Enable PubSubHubbub feed publishing." | ||
928 | msgstr "Включить публикацию канала PubSubHubbub." | ||
929 | |||
930 | #: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72 | ||
931 | msgid "For each link, add a QRCode icon." | ||
932 | msgstr "Для каждой ссылки добавить значок QR кода." | ||
933 | |||
934 | #: plugins/wallabag/wallabag.php:22 | ||
935 | msgid "" | ||
936 | "Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " | ||
937 | "plugin administration page." | ||
938 | msgstr "" | ||
939 | "Ошибка плагина Wallabag: определите параметр \"WALLABAG_URL\" на странице " | ||
940 | "настройки плагина." | ||
941 | |||
942 | #: plugins/wallabag/wallabag.php:49 | ||
943 | msgid "Save to wallabag" | ||
944 | msgstr "Сохранить в wallabag" | ||
945 | |||
946 | #: plugins/wallabag/wallabag.php:73 | ||
947 | msgid "Wallabag API URL" | ||
948 | msgstr "Wallabag API URL" | ||
949 | |||
950 | #: plugins/wallabag/wallabag.php:74 | ||
951 | msgid "Wallabag API version (1 or 2)" | ||
952 | msgstr "Wallabag версия API (1 или 2)" | ||
953 | |||
954 | #: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 | ||
955 | msgid "Sorry, nothing to see here." | ||
956 | msgstr "Извините, тут ничего нет." | ||
957 | |||
958 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
959 | msgid "URL or leave empty to post a note" | ||
960 | msgstr "URL или оставьте пустым, чтобы опубликовать заметку" | ||
961 | |||
962 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
963 | msgid "BULK CREATION" | ||
964 | msgstr "МАССОВОЕ СОЗДАНИЕ" | ||
965 | |||
966 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
967 | msgid "Metadata asynchronous retrieval is disabled." | ||
968 | msgstr "Асинхронное получение метаданных отключено." | ||
969 | |||
970 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
971 | msgid "" | ||
972 | "We recommend that you enable the setting <em>general > " | ||
973 | "enable_async_metadata</em> in your configuration file to use bulk link " | ||
974 | "creation." | ||
975 | msgstr "" | ||
976 | "Мы рекомендуем включить параметр <em>general > enable_async_metadata</em> в " | ||
977 | "вашем файле конфигурации, чтобы использовать массовое создание ссылок." | ||
978 | |||
979 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 | ||
980 | msgid "Shaare multiple new links" | ||
981 | msgstr "Поделиться несколькими новыми ссылками" | ||
982 | |||
983 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 | ||
984 | msgid "Add one URL per line to create multiple bookmarks." | ||
985 | msgstr "Добавьте по одному URL в строке, чтобы создать несколько закладок." | ||
986 | |||
987 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 | ||
988 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 | ||
989 | msgid "Tags" | ||
990 | msgstr "Теги" | ||
991 | |||
992 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 | ||
993 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 | ||
994 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
995 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
996 | msgid "Private" | ||
997 | msgstr "Личный" | ||
998 | |||
999 | #: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 | ||
1000 | msgid "Add links" | ||
1001 | msgstr "Добавить ссылки" | ||
1002 | |||
1003 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1004 | msgid "Current password" | ||
1005 | msgstr "Текущий пароль" | ||
1006 | |||
1007 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1008 | msgid "New password" | ||
1009 | msgstr "Новый пароль" | ||
1010 | |||
1011 | #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
1012 | msgid "Change" | ||
1013 | msgstr "Изменить" | ||
1014 | |||
1015 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1016 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 | ||
1017 | msgid "Tag" | ||
1018 | msgstr "Тег" | ||
1019 | |||
1020 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
1021 | msgid "New name" | ||
1022 | msgstr "Новое имя" | ||
1023 | |||
1024 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 | ||
1025 | msgid "Case sensitive" | ||
1026 | msgstr "С учетом регистра" | ||
1027 | |||
1028 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
1029 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 | ||
1030 | msgid "Rename tag" | ||
1031 | msgstr "Переименовать тег" | ||
1032 | |||
1033 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 | ||
1034 | msgid "Delete tag" | ||
1035 | msgstr "Удалить тег" | ||
1036 | |||
1037 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
1038 | msgid "You can also edit tags in the" | ||
1039 | msgstr "Вы также можете редактировать теги в" | ||
1040 | |||
1041 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
1042 | msgid "tag list" | ||
1043 | msgstr "список тегов" | ||
1044 | |||
1045 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | ||
1046 | msgid "Change tags separator" | ||
1047 | msgstr "Изменить разделитель тегов" | ||
1048 | |||
1049 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 | ||
1050 | msgid "Your current tag separator is" | ||
1051 | msgstr "Текущий разделитель тегов" | ||
1052 | |||
1053 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 | ||
1054 | msgid "New separator" | ||
1055 | msgstr "Новый разделитель" | ||
1056 | |||
1057 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1058 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 | ||
1059 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1060 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | ||
1061 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 | ||
1062 | msgid "Save" | ||
1063 | msgstr "Сохранить" | ||
1064 | |||
1065 | #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 | ||
1066 | msgid "Note that hashtags won't fully work with a non-whitespace separator." | ||
1067 | msgstr "" | ||
1068 | "Обратите внимание, что хэштеги не будут полностью работать с разделителем, " | ||
1069 | "отличным от пробелов." | ||
1070 | |||
1071 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
1072 | msgid "title" | ||
1073 | msgstr "заголовок" | ||
1074 | |||
1075 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
1076 | msgid "Home link" | ||
1077 | msgstr "Домашняя ссылка" | ||
1078 | |||
1079 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
1080 | msgid "Default value" | ||
1081 | msgstr "Значение по умолчанию" | ||
1082 | |||
1083 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1084 | msgid "Theme" | ||
1085 | msgstr "Тема" | ||
1086 | |||
1087 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85 | ||
1088 | msgid "Description formatter" | ||
1089 | msgstr "Средство форматирования описания" | ||
1090 | |||
1091 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 | ||
1092 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 | ||
1093 | msgid "Language" | ||
1094 | msgstr "Язык" | ||
1095 | |||
1096 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 | ||
1097 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 | ||
1098 | msgid "Timezone" | ||
1099 | msgstr "Часовой пояс" | ||
1100 | |||
1101 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 | ||
1102 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
1103 | msgid "Continent" | ||
1104 | msgstr "Континент" | ||
1105 | |||
1106 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 | ||
1107 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
1108 | msgid "City" | ||
1109 | msgstr "Город" | ||
1110 | |||
1111 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191 | ||
1112 | msgid "Disable session cookie hijacking protection" | ||
1113 | msgstr "Отключить защиту от перехвата файлов сеанса cookie" | ||
1114 | |||
1115 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193 | ||
1116 | msgid "Check this if you get disconnected or if your IP address changes often" | ||
1117 | msgstr "Проверьте это, если вы отключаетесь или ваш IP адрес часто меняется" | ||
1118 | |||
1119 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210 | ||
1120 | msgid "Private links by default" | ||
1121 | msgstr "Приватные ссылки по умолчанию" | ||
1122 | |||
1123 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211 | ||
1124 | msgid "All new links are private by default" | ||
1125 | msgstr "Все новые ссылки по умолчанию являются приватными" | ||
1126 | |||
1127 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226 | ||
1128 | msgid "RSS direct links" | ||
1129 | msgstr "RSS прямые ссылки" | ||
1130 | |||
1131 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227 | ||
1132 | msgid "Check this to use direct URL instead of permalink in feeds" | ||
1133 | msgstr "" | ||
1134 | "Установите этот флажок, чтобы использовать прямой URL вместо постоянной " | ||
1135 | "ссылки в фидах" | ||
1136 | |||
1137 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242 | ||
1138 | msgid "Hide public links" | ||
1139 | msgstr "Скрыть общедоступные ссылки" | ||
1140 | |||
1141 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243 | ||
1142 | msgid "Do not show any links if the user is not logged in" | ||
1143 | msgstr "Не показывать ссылки, если пользователь не авторизован" | ||
1144 | |||
1145 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258 | ||
1146 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149 | ||
1147 | msgid "Check updates" | ||
1148 | msgstr "Проверить обновления" | ||
1149 | |||
1150 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259 | ||
1151 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 | ||
1152 | msgid "Notify me when a new release is ready" | ||
1153 | msgstr "Оповестить, когда будет готов новый выпуск" | ||
1154 | |||
1155 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274 | ||
1156 | msgid "Automatically retrieve description for new bookmarks" | ||
1157 | msgstr "Автоматически получать описание для новых закладок" | ||
1158 | |||
1159 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275 | ||
1160 | msgid "Shaarli will try to retrieve the description from meta HTML headers" | ||
1161 | msgstr "Shaarli попытается получить описание из мета заголовков HTML" | ||
1162 | |||
1163 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290 | ||
1164 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 | ||
1165 | msgid "Enable REST API" | ||
1166 | msgstr "Включить REST API" | ||
1167 | |||
1168 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291 | ||
1169 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 | ||
1170 | msgid "Allow third party software to use Shaarli such as mobile application" | ||
1171 | msgstr "" | ||
1172 | "Разрешить стороннему программному обеспечению использовать Shaarli, например " | ||
1173 | "мобильное приложение" | ||
1174 | |||
1175 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306 | ||
1176 | msgid "API secret" | ||
1177 | msgstr "API ключ" | ||
1178 | |||
1179 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320 | ||
1180 | msgid "Enable thumbnails" | ||
1181 | msgstr "Включить миниатюры" | ||
1182 | |||
1183 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324 | ||
1184 | msgid "You need to enable the extension <code>php-gd</code> to use thumbnails." | ||
1185 | msgstr "" | ||
1186 | "Вам необходимо включить расширение <code>php-gd</code> для использования " | ||
1187 | "миниатюр." | ||
1188 | |||
1189 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 | ||
1190 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 | ||
1191 | msgid "Synchronize thumbnails" | ||
1192 | msgstr "Синхронизировать миниатюры" | ||
1193 | |||
1194 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 | ||
1195 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 | ||
1196 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
1197 | msgid "All" | ||
1198 | msgstr "Все" | ||
1199 | |||
1200 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 | ||
1201 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 | ||
1202 | msgid "Only common media hosts" | ||
1203 | msgstr "Только обычные медиа хосты" | ||
1204 | |||
1205 | #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 | ||
1206 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 | ||
1207 | msgid "None" | ||
1208 | msgstr "Ничего" | ||
1209 | |||
1210 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | ||
1211 | msgid "1 RSS entry per :type" | ||
1212 | msgid_plural "" | ||
1213 | msgstr[0] "1 RSS запись для каждого :type" | ||
1214 | msgstr[1] "1 RSS запись для каждого :type" | ||
1215 | msgstr[2] "1 RSS запись для каждого :type" | ||
1216 | |||
1217 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | ||
1218 | msgid "Previous :type" | ||
1219 | msgid_plural "" | ||
1220 | msgstr[0] "Предыдущий :type" | ||
1221 | msgstr[1] "Предыдущих :type" | ||
1222 | msgstr[2] "Предыдущих :type" | ||
1223 | |||
1224 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 | ||
1225 | #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 | ||
1226 | msgid "All links of one :type in a single page." | ||
1227 | msgid_plural "" | ||
1228 | msgstr[0] "Все ссылки одного :type на одной странице." | ||
1229 | msgstr[1] "Все ссылки одного :type на одной странице." | ||
1230 | msgstr[2] "Все ссылки одного :type на одной странице." | ||
1231 | |||
1232 | #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 | ||
1233 | msgid "Next :type" | ||
1234 | msgid_plural "" | ||
1235 | msgstr[0] "Следующий :type" | ||
1236 | msgstr[1] "Следующие :type" | ||
1237 | msgstr[2] "Следующие :type" | ||
1238 | |||
1239 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 | ||
1240 | msgid "Edit Shaare" | ||
1241 | msgstr "Изменить закладку" | ||
1242 | |||
1243 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 | ||
1244 | msgid "New Shaare" | ||
1245 | msgstr "Новая закладка" | ||
1246 | |||
1247 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 | ||
1248 | msgid "Created:" | ||
1249 | msgstr "Создано:" | ||
1250 | |||
1251 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
1252 | msgid "URL" | ||
1253 | msgstr "URL" | ||
1254 | |||
1255 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | ||
1256 | msgid "Title" | ||
1257 | msgstr "Заголовок" | ||
1258 | |||
1259 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1260 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
1261 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 | ||
1262 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 | ||
1263 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 | ||
1264 | msgid "Description" | ||
1265 | msgstr "Описание" | ||
1266 | |||
1267 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 | ||
1268 | msgid "Description will be rendered with" | ||
1269 | msgstr "Описание будет отображаться с" | ||
1270 | |||
1271 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 | ||
1272 | msgid "Markdown syntax documentation" | ||
1273 | msgstr "Документация по синтаксису Markdown" | ||
1274 | |||
1275 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 | ||
1276 | msgid "Markdown syntax" | ||
1277 | msgstr "Синтаксис Markdown" | ||
1278 | |||
1279 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115 | ||
1280 | msgid "Cancel" | ||
1281 | msgstr "Отменить" | ||
1282 | |||
1283 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1284 | msgid "Apply Changes" | ||
1285 | msgstr "Применить изменения" | ||
1286 | |||
1287 | #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126 | ||
1288 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 | ||
1289 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 | ||
1290 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 | ||
1291 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 | ||
1292 | msgid "Delete" | ||
1293 | msgstr "Удалить" | ||
1294 | |||
1295 | #: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 | ||
1296 | #: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 | ||
1297 | msgid "Save all" | ||
1298 | msgstr "Сохранить все" | ||
1299 | |||
1300 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1301 | msgid "Export Database" | ||
1302 | msgstr "Экспорт базы данных" | ||
1303 | |||
1304 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
1305 | msgid "Selection" | ||
1306 | msgstr "Выбор" | ||
1307 | |||
1308 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
1309 | msgid "Public" | ||
1310 | msgstr "Общедоступно" | ||
1311 | |||
1312 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 | ||
1313 | msgid "Prepend note permalinks with this Shaarli instance's URL" | ||
1314 | msgstr "" | ||
1315 | "Добавить постоянные ссылки на заметку с URL адресом этого экземпляра Shaarli" | ||
1316 | |||
1317 | #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 | ||
1318 | msgid "Useful to import bookmarks in a web browser" | ||
1319 | msgstr "Useful to import bookmarks in a web browser" | ||
1320 | |||
1321 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1322 | msgid "Import Database" | ||
1323 | msgstr "Импорт базы данных" | ||
1324 | |||
1325 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
1326 | msgid "Maximum size allowed:" | ||
1327 | msgstr "Максимально допустимый размер:" | ||
1328 | |||
1329 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
1330 | msgid "Visibility" | ||
1331 | msgstr "Видимость" | ||
1332 | |||
1333 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1334 | msgid "Use values from the imported file, default to public" | ||
1335 | msgstr "" | ||
1336 | "Использовать значения из импортированного файла, по умолчанию общедоступные" | ||
1337 | |||
1338 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
1339 | msgid "Import all bookmarks as private" | ||
1340 | msgstr "Импортировать все закладки как личные" | ||
1341 | |||
1342 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
1343 | msgid "Import all bookmarks as public" | ||
1344 | msgstr "Импортировать все закладки как общедоступные" | ||
1345 | |||
1346 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 | ||
1347 | msgid "Overwrite existing bookmarks" | ||
1348 | msgstr "Заменить существующие закладки" | ||
1349 | |||
1350 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1351 | msgid "Duplicates based on URL" | ||
1352 | msgstr "Дубликаты на основе URL" | ||
1353 | |||
1354 | #: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 | ||
1355 | msgid "Add default tags" | ||
1356 | msgstr "Добавить теги по умолчанию" | ||
1357 | |||
1358 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 | ||
1359 | msgid "It looks like it's the first time you run Shaarli. Please configure it." | ||
1360 | msgstr "Похоже, вы впервые запускаете Shaarli. Пожалуйста, настройте его." | ||
1361 | |||
1362 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 | ||
1363 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1364 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167 | ||
1365 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167 | ||
1366 | msgid "Username" | ||
1367 | msgstr "Имя пользователя" | ||
1368 | |||
1369 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | ||
1370 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 | ||
1371 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 | ||
1372 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168 | ||
1373 | msgid "Password" | ||
1374 | msgstr "Пароль" | ||
1375 | |||
1376 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62 | ||
1377 | msgid "Shaarli title" | ||
1378 | msgstr "Заголовок Shaarli" | ||
1379 | |||
1380 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 | ||
1381 | msgid "My links" | ||
1382 | msgstr "Мои ссылки" | ||
1383 | |||
1384 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 | ||
1385 | msgid "Install" | ||
1386 | msgstr "Установка" | ||
1387 | |||
1388 | #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190 | ||
1389 | msgid "Server requirements" | ||
1390 | msgstr "Системные требования" | ||
1391 | |||
1392 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
1393 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 | ||
1394 | msgid "shaare" | ||
1395 | msgid_plural "shaares" | ||
1396 | msgstr[0] "закладка" | ||
1397 | msgstr[1] "закладки" | ||
1398 | msgstr[2] "закладок" | ||
1399 | |||
1400 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | ||
1401 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 | ||
1402 | msgid "private link" | ||
1403 | msgid_plural "private links" | ||
1404 | msgstr[0] "личная ссылка" | ||
1405 | msgstr[1] "личные ссылки" | ||
1406 | msgstr[2] "личных ссылок" | ||
1407 | |||
1408 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 | ||
1409 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 | ||
1410 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123 | ||
1411 | msgid "Search text" | ||
1412 | msgstr "Поиск текста" | ||
1413 | |||
1414 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 | ||
1415 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 | ||
1416 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130 | ||
1417 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1418 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 | ||
1419 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1420 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 | ||
1421 | msgid "Filter by tag" | ||
1422 | msgstr "Фильтровать по тегу" | ||
1423 | |||
1424 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
1425 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 | ||
1426 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 | ||
1427 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:87 | ||
1428 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:139 | ||
1429 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
1430 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 | ||
1431 | msgid "Search" | ||
1432 | msgstr "Поиск" | ||
1433 | |||
1434 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 | ||
1435 | msgid "Nothing found." | ||
1436 | msgstr "Ничего не найдено." | ||
1437 | |||
1438 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 | ||
1439 | #, php-format | ||
1440 | msgid "%s result" | ||
1441 | msgid_plural "%s results" | ||
1442 | msgstr[0] "%s результат" | ||
1443 | msgstr[1] "%s результатов" | ||
1444 | msgstr[2] "%s результатов" | ||
1445 | |||
1446 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 | ||
1447 | msgid "for" | ||
1448 | msgstr "для" | ||
1449 | |||
1450 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 | ||
1451 | msgid "tagged" | ||
1452 | msgstr "отмечено" | ||
1453 | |||
1454 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133 | ||
1455 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 | ||
1456 | msgid "Remove tag" | ||
1457 | msgstr "Удалить тег" | ||
1458 | |||
1459 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 | ||
1460 | msgid "with status" | ||
1461 | msgstr "со статусом" | ||
1462 | |||
1463 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 | ||
1464 | msgid "without any tag" | ||
1465 | msgstr "без тега" | ||
1466 | |||
1467 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 | ||
1468 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
1469 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41 | ||
1470 | msgid "Fold" | ||
1471 | msgstr "Сложить" | ||
1472 | |||
1473 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177 | ||
1474 | msgid "Edited: " | ||
1475 | msgstr "Отредактировано: " | ||
1476 | |||
1477 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 | ||
1478 | msgid "permalink" | ||
1479 | msgstr "постоянная ссылка" | ||
1480 | |||
1481 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 | ||
1482 | msgid "Add tag" | ||
1483 | msgstr "Добавить тег" | ||
1484 | |||
1485 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185 | ||
1486 | msgid "Toggle sticky" | ||
1487 | msgstr "Закрепить / Открепить" | ||
1488 | |||
1489 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187 | ||
1490 | msgid "Sticky" | ||
1491 | msgstr "Закреплено" | ||
1492 | |||
1493 | #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 | ||
1494 | msgid "Share a private link" | ||
1495 | msgstr "Поделиться личной ссылкой" | ||
1496 | |||
1497 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 | ||
1498 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 | ||
1499 | msgid "Filters" | ||
1500 | msgstr "Фильтры" | ||
1501 | |||
1502 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:10 | ||
1503 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:10 | ||
1504 | msgid "Only display private links" | ||
1505 | msgstr "Отображать только личные ссылки" | ||
1506 | |||
1507 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 | ||
1508 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:13 | ||
1509 | msgid "Only display public links" | ||
1510 | msgstr "Отображать только общедоступные ссылки" | ||
1511 | |||
1512 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 | ||
1513 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 | ||
1514 | msgid "Filter untagged links" | ||
1515 | msgstr "Фильтровать неотмеченные ссылки" | ||
1516 | |||
1517 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
1518 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 | ||
1519 | msgid "Select all" | ||
1520 | msgstr "Выбрать все" | ||
1521 | |||
1522 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
1523 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 | ||
1524 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 | ||
1525 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 | ||
1526 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 | ||
1527 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 | ||
1528 | msgid "Fold all" | ||
1529 | msgstr "Сложить все" | ||
1530 | |||
1531 | #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 | ||
1532 | #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76 | ||
1533 | msgid "Links per page" | ||
1534 | msgstr "Ссылок на страницу" | ||
1535 | |||
1536 | #: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 | ||
1537 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 | ||
1538 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171 | ||
1539 | msgid "Remember me" | ||
1540 | msgstr "Запомнить меня" | ||
1541 | |||
1542 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
1543 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
1544 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 | ||
1545 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 | ||
1546 | msgid "by the Shaarli community" | ||
1547 | msgstr "сообществом Shaarli" | ||
1548 | |||
1549 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1550 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:16 | ||
1551 | msgid "Documentation" | ||
1552 | msgstr "Документация" | ||
1553 | |||
1554 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
1555 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 | ||
1556 | msgid "Expand" | ||
1557 | msgstr "Развернуть" | ||
1558 | |||
1559 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
1560 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 | ||
1561 | msgid "Expand all" | ||
1562 | msgstr "Развернуть все" | ||
1563 | |||
1564 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 | ||
1565 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 | ||
1566 | msgid "Are you sure you want to delete this link?" | ||
1567 | msgstr "Вы уверены, что хотите удалить эту ссылку?" | ||
1568 | |||
1569 | #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 | ||
1570 | #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 | ||
1571 | msgid "Are you sure you want to delete this tag?" | ||
1572 | msgstr "Вы уверены, что хотите удалить этот тег?" | ||
1573 | |||
1574 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11 | ||
1575 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11 | ||
1576 | msgid "Menu" | ||
1577 | msgstr "Меню" | ||
1578 | |||
1579 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 | ||
1580 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38 | ||
1581 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1582 | msgid "Tag cloud" | ||
1583 | msgstr "Облако тегов" | ||
1584 | |||
1585 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 | ||
1586 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 | ||
1587 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67 | ||
1588 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92 | ||
1589 | msgid "RSS Feed" | ||
1590 | msgstr "RSS канал" | ||
1591 | |||
1592 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 | ||
1593 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 | ||
1594 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72 | ||
1595 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108 | ||
1596 | msgid "Logout" | ||
1597 | msgstr "Выйти" | ||
1598 | |||
1599 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 | ||
1600 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152 | ||
1601 | msgid "Set public" | ||
1602 | msgstr "Сделать общедоступным" | ||
1603 | |||
1604 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157 | ||
1605 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157 | ||
1606 | msgid "Set private" | ||
1607 | msgstr "Сделать личным" | ||
1608 | |||
1609 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 | ||
1610 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189 | ||
1611 | msgid "is available" | ||
1612 | msgstr "доступно" | ||
1613 | |||
1614 | #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196 | ||
1615 | #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196 | ||
1616 | msgid "Error" | ||
1617 | msgstr "Ошибка" | ||
1618 | |||
1619 | #: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
1620 | msgid "There is no cached thumbnail." | ||
1621 | msgstr "Нет кэшированных миниатюр." | ||
1622 | |||
1623 | #: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 | ||
1624 | msgid "Try to synchronize them." | ||
1625 | msgstr "Попробуйте синхронизировать их." | ||
1626 | |||
1627 | #: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
1628 | msgid "Picture Wall" | ||
1629 | msgstr "Галерея" | ||
1630 | |||
1631 | #: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
1632 | msgid "pics" | ||
1633 | msgstr "изображений" | ||
1634 | |||
1635 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 | ||
1636 | msgid "You need to enable Javascript to change plugin loading order." | ||
1637 | msgstr "" | ||
1638 | "Вам необходимо включить Javascript, чтобы изменить порядок загрузки плагинов." | ||
1639 | |||
1640 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 | ||
1641 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 | ||
1642 | msgid "Plugin administration" | ||
1643 | msgstr "Управление плагинами" | ||
1644 | |||
1645 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 | ||
1646 | msgid "Enabled Plugins" | ||
1647 | msgstr "Включенные плагины" | ||
1648 | |||
1649 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
1650 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 | ||
1651 | msgid "No plugin enabled." | ||
1652 | msgstr "Нет включенных плагинов." | ||
1653 | |||
1654 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 | ||
1655 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 | ||
1656 | msgid "Disable" | ||
1657 | msgstr "Отключить" | ||
1658 | |||
1659 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
1660 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 | ||
1661 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98 | ||
1662 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 | ||
1663 | msgid "Name" | ||
1664 | msgstr "Имя" | ||
1665 | |||
1666 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 | ||
1667 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 | ||
1668 | msgid "Order" | ||
1669 | msgstr "Порядок" | ||
1670 | |||
1671 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 | ||
1672 | msgid "Disabled Plugins" | ||
1673 | msgstr "Отключенные плагины" | ||
1674 | |||
1675 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 | ||
1676 | msgid "No plugin disabled." | ||
1677 | msgstr "Нет отключенных плагинов." | ||
1678 | |||
1679 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97 | ||
1680 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 | ||
1681 | msgid "Enable" | ||
1682 | msgstr "Включить" | ||
1683 | |||
1684 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 | ||
1685 | msgid "More plugins available" | ||
1686 | msgstr "Доступны другие плагины" | ||
1687 | |||
1688 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 | ||
1689 | msgid "in the documentation" | ||
1690 | msgstr "в документации" | ||
1691 | |||
1692 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 | ||
1693 | msgid "Plugin configuration" | ||
1694 | msgstr "Настройка плагинов" | ||
1695 | |||
1696 | #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195 | ||
1697 | msgid "No parameter available." | ||
1698 | msgstr "Нет доступных параметров." | ||
1699 | |||
1700 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1701 | msgid "General" | ||
1702 | msgstr "Общее" | ||
1703 | |||
1704 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 | ||
1705 | msgid "Index URL" | ||
1706 | msgstr "Индексный URL" | ||
1707 | |||
1708 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 | ||
1709 | msgid "Base path" | ||
1710 | msgstr "Базовый путь" | ||
1711 | |||
1712 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1713 | msgid "Client IP" | ||
1714 | msgstr "IP клиента" | ||
1715 | |||
1716 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 | ||
1717 | msgid "Trusted reverse proxies" | ||
1718 | msgstr "Надежные обратные прокси" | ||
1719 | |||
1720 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 | ||
1721 | msgid "N/A" | ||
1722 | msgstr "Нет данных" | ||
1723 | |||
1724 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 | ||
1725 | msgid "Visit releases page on Github" | ||
1726 | msgstr "Посетить страницу релизов на Github" | ||
1727 | |||
1728 | #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 | ||
1729 | msgid "Synchronize all link thumbnails" | ||
1730 | msgstr "Синхронизировать все миниатюры ссылок" | ||
1731 | |||
1732 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2 | ||
1733 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2 | ||
1734 | msgid "Permissions" | ||
1735 | msgstr "Разрешения" | ||
1736 | |||
1737 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8 | ||
1738 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8 | ||
1739 | msgid "There are permissions that need to be fixed." | ||
1740 | msgstr "Есть разрешения, которые нужно исправить." | ||
1741 | |||
1742 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 | ||
1743 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23 | ||
1744 | msgid "All read/write permissions are properly set." | ||
1745 | msgstr "Все разрешения на чтение и запись установлены правильно." | ||
1746 | |||
1747 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 | ||
1748 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32 | ||
1749 | msgid "Running PHP" | ||
1750 | msgstr "Запуск PHP" | ||
1751 | |||
1752 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 | ||
1753 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36 | ||
1754 | msgid "End of life: " | ||
1755 | msgstr "Конец жизни: " | ||
1756 | |||
1757 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
1758 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48 | ||
1759 | msgid "Extension" | ||
1760 | msgstr "Расширение" | ||
1761 | |||
1762 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 | ||
1763 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49 | ||
1764 | msgid "Usage" | ||
1765 | msgstr "Применение" | ||
1766 | |||
1767 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 | ||
1768 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50 | ||
1769 | msgid "Status" | ||
1770 | msgstr "Статус" | ||
1771 | |||
1772 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 | ||
1773 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 | ||
1774 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51 | ||
1775 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66 | ||
1776 | msgid "Loaded" | ||
1777 | msgstr "Загружено" | ||
1778 | |||
1779 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 | ||
1780 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 | ||
1781 | msgid "Required" | ||
1782 | msgstr "Обязательно" | ||
1783 | |||
1784 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 | ||
1785 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 | ||
1786 | msgid "Optional" | ||
1787 | msgstr "Необязательно" | ||
1788 | |||
1789 | #: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 | ||
1790 | #: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70 | ||
1791 | msgid "Not loaded" | ||
1792 | msgstr "Не загружено" | ||
1793 | |||
1794 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1795 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1796 | msgid "tags" | ||
1797 | msgstr "теги" | ||
1798 | |||
1799 | #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
1800 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 | ||
1801 | msgid "List all links with those tags" | ||
1802 | msgstr "Список всех ссылок с этими тегами" | ||
1803 | |||
1804 | #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 | ||
1805 | msgid "Tag list" | ||
1806 | msgstr "Список тегов" | ||
1807 | |||
1808 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 | ||
1809 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 | ||
1810 | msgid "Sort by:" | ||
1811 | msgstr "Сортировать по:" | ||
1812 | |||
1813 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 | ||
1814 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5 | ||
1815 | msgid "Cloud" | ||
1816 | msgstr "Облако" | ||
1817 | |||
1818 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6 | ||
1819 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6 | ||
1820 | msgid "Most used" | ||
1821 | msgstr "Наиболее используемое" | ||
1822 | |||
1823 | #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 | ||
1824 | #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7 | ||
1825 | msgid "Alphabetical" | ||
1826 | msgstr "Алфавит" | ||
1827 | |||
1828 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 | ||
1829 | msgid "Settings" | ||
1830 | msgstr "Настройки" | ||
1831 | |||
1832 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 | ||
1833 | msgid "Change Shaarli settings: title, timezone, etc." | ||
1834 | msgstr "Измените настройки Shaarli: заголовок, часовой пояс и т.д." | ||
1835 | |||
1836 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 | ||
1837 | msgid "Configure your Shaarli" | ||
1838 | msgstr "Настройка Shaarli" | ||
1839 | |||
1840 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 | ||
1841 | msgid "Enable, disable and configure plugins" | ||
1842 | msgstr "Включить, отключить и настроить плагины" | ||
1843 | |||
1844 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 | ||
1845 | msgid "Check instance's server configuration" | ||
1846 | msgstr "Проверка конфигурации экземпляра сервера" | ||
1847 | |||
1848 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 | ||
1849 | msgid "Change your password" | ||
1850 | msgstr "Изменить пароль" | ||
1851 | |||
1852 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 | ||
1853 | msgid "Rename or delete a tag in all links" | ||
1854 | msgstr "Переименовать или удалить тег во всех ссылках" | ||
1855 | |||
1856 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 | ||
1857 | msgid "" | ||
1858 | "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " | ||
1859 | "delicious...)" | ||
1860 | msgstr "" | ||
1861 | "Импорт закладок Netscape HTML (экспортированные из Firefox, Chrome, Opera, " | ||
1862 | "delicious...)" | ||
1863 | |||
1864 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 | ||
1865 | msgid "Import links" | ||
1866 | msgstr "Импорт ссылок" | ||
1867 | |||
1868 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 | ||
1869 | msgid "" | ||
1870 | "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " | ||
1871 | "Opera, delicious...)" | ||
1872 | msgstr "" | ||
1873 | "Экспорт закладок Netscape HTML (которые могут быть импортированы в Firefox, " | ||
1874 | "Chrome, Opera, delicious...)" | ||
1875 | |||
1876 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54 | ||
1877 | msgid "Export database" | ||
1878 | msgstr "Экспорт базы данных" | ||
1879 | |||
1880 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 | ||
1881 | msgid "" | ||
1882 | "Drag one of these button to your bookmarks toolbar or right-click it and " | ||
1883 | "\"Bookmark This Link\"" | ||
1884 | msgstr "" | ||
1885 | "Перетащите одну из этих кнопок на панель закладок или щелкните по ней правой " | ||
1886 | "кнопкой мыши и выберите \"Добавить ссылку в закладки\"" | ||
1887 | |||
1888 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 | ||
1889 | msgid "then click on the bookmarklet in any page you want to share." | ||
1890 | msgstr "" | ||
1891 | "затем щелкните букмарклет на любой странице, которой хотите поделиться." | ||
1892 | |||
1893 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 | ||
1894 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 | ||
1895 | msgid "" | ||
1896 | "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " | ||
1897 | "Link" | ||
1898 | msgstr "" | ||
1899 | "Перетащите эту ссылку на панель закладок или щелкните по ней правой кнопкой " | ||
1900 | "мыши и добавьте эту ссылку в закладки" | ||
1901 | |||
1902 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 | ||
1903 | msgid "then click ✚Shaare link button in any page you want to share" | ||
1904 | msgstr "" | ||
1905 | "затем нажмите кнопку ✚Поделиться ссылкой на любой странице, которой хотите " | ||
1906 | "поделиться" | ||
1907 | |||
1908 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 | ||
1909 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 | ||
1910 | msgid "The selected text is too long, it will be truncated." | ||
1911 | msgstr "Выделенный текст слишком длинный, он будет обрезан." | ||
1912 | |||
1913 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 | ||
1914 | msgid "Shaare link" | ||
1915 | msgstr "Поделиться ссылкой" | ||
1916 | |||
1917 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 | ||
1918 | msgid "" | ||
1919 | "Then click ✚Add Note button anytime to start composing a private Note (text " | ||
1920 | "post) to your Shaarli" | ||
1921 | msgstr "" | ||
1922 | "Затем в любое время нажмите кнопку ✚Добавить заметку, чтобы начать создавать " | ||
1923 | "личную заметку (текстовое сообщение) в своем Shaarli" | ||
1924 | |||
1925 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 | ||
1926 | msgid "Add Note" | ||
1927 | msgstr "Добавить заметку" | ||
1928 | |||
1929 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132 | ||
1930 | msgid "3rd party" | ||
1931 | msgstr "Третья сторона" | ||
1932 | |||
1933 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 | ||
1934 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140 | ||
1935 | msgid "plugin" | ||
1936 | msgstr "плагин" | ||
1937 | |||
1938 | #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 | ||
1939 | msgid "" | ||
1940 | "Drag this link to your bookmarks toolbar, or right-click it and choose " | ||
1941 | "Bookmark This Link" | ||
1942 | msgstr "" | ||
1943 | "Перетащите эту ссылку на панель закладок или щелкните по ней правой кнопкой " | ||
1944 | "мыши и выберите \"Добавить ссылку в закладки\"" | ||
@@ -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,13 @@ 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\Plugin\PluginManager; | ||
35 | use Shaarli\Security\BanManager; | ||
31 | use Shaarli\Security\CookieManager; | 36 | use Shaarli\Security\CookieManager; |
32 | use Shaarli\Security\LoginManager; | 37 | use Shaarli\Security\LoginManager; |
33 | use Shaarli\Security\SessionManager; | 38 | use Shaarli\Security\SessionManager; |
@@ -48,10 +53,22 @@ if ($conf->get('dev.debug', false)) { | |||
48 | }); | 53 | }); |
49 | } | 54 | } |
50 | 55 | ||
56 | $logger = new Logger( | ||
57 | dirname($conf->get('resource.log')), | ||
58 | !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, | ||
59 | ['filename' => basename($conf->get('resource.log'))] | ||
60 | ); | ||
51 | $sessionManager = new SessionManager($_SESSION, $conf, session_save_path()); | 61 | $sessionManager = new SessionManager($_SESSION, $conf, session_save_path()); |
52 | $sessionManager->initialize(); | 62 | $sessionManager->initialize(); |
53 | $cookieManager = new CookieManager($_COOKIE); | 63 | $cookieManager = new CookieManager($_COOKIE); |
54 | $loginManager = new LoginManager($conf, $sessionManager, $cookieManager); | 64 | $banManager = new BanManager( |
65 | $conf->get('security.trusted_proxies', []), | ||
66 | $conf->get('security.ban_after'), | ||
67 | $conf->get('security.ban_duration'), | ||
68 | $conf->get('resource.ban_file', 'data/ipbans.php'), | ||
69 | $logger | ||
70 | ); | ||
71 | $loginManager = new LoginManager($conf, $sessionManager, $cookieManager, $banManager, $logger); | ||
55 | $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); | 72 | $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); |
56 | 73 | ||
57 | // Sniff browser language and set date format accordingly. | 74 | // Sniff browser language and set date format accordingly. |
@@ -62,16 +79,26 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { | |||
62 | new Languages(setlocale(LC_MESSAGES, 0), $conf); | 79 | new Languages(setlocale(LC_MESSAGES, 0), $conf); |
63 | 80 | ||
64 | $conf->setEmpty('general.timezone', date_default_timezone_get()); | 81 | $conf->setEmpty('general.timezone', date_default_timezone_get()); |
65 | $conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER))); | 82 | $conf->setEmpty('general.title', t('Shared bookmarks on ') . escape(index_url($_SERVER))); |
66 | 83 | ||
67 | RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory | 84 | 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 | 85 | RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory |
69 | 86 | ||
70 | date_default_timezone_set($conf->get('general.timezone', 'UTC')); | 87 | date_default_timezone_set($conf->get('general.timezone', 'UTC')); |
71 | 88 | ||
72 | $loginManager->checkLoginState(client_ip_id($_SERVER)); | 89 | $loginManager->checkLoginState(client_ip_id($_SERVER)); |
73 | 90 | ||
74 | $containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager); | 91 | $pluginManager = new PluginManager($conf); |
92 | $pluginManager->load($conf->get('general.enabled_plugins', [])); | ||
93 | |||
94 | $containerBuilder = new ContainerBuilder( | ||
95 | $conf, | ||
96 | $sessionManager, | ||
97 | $cookieManager, | ||
98 | $loginManager, | ||
99 | $pluginManager, | ||
100 | $logger | ||
101 | ); | ||
75 | $container = $containerBuilder->build(); | 102 | $container = $containerBuilder->build(); |
76 | $app = new App($container); | 103 | $app = new App($container); |
77 | 104 | ||
@@ -110,13 +137,16 @@ $app->group('/admin', function () { | |||
110 | $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save'); | 137 | $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save'); |
111 | $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index'); | 138 | $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index'); |
112 | $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save'); | 139 | $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save'); |
113 | $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare'); | 140 | $this->post('/tags/change-separator', '\Shaarli\Front\Controller\Admin\ManageTagController:changeSeparator'); |
114 | $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm'); | 141 | $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare'); |
115 | $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm'); | 142 | $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm'); |
116 | $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); | 143 | $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm'); |
117 | $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); | 144 | $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate'); |
118 | $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); | 145 | $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms'); |
119 | $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); | 146 | $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save'); |
147 | $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark'); | ||
148 | $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility'); | ||
149 | $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark'); | ||
120 | $this->patch( | 150 | $this->patch( |
121 | '/shaare/{id:[0-9]+}/update-thumbnail', | 151 | '/shaare/{id:[0-9]+}/update-thumbnail', |
122 | '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate' | 152 | '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate' |
@@ -128,11 +158,22 @@ $app->group('/admin', function () { | |||
128 | $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index'); | 158 | $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index'); |
129 | $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save'); | 159 | $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save'); |
130 | $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken'); | 160 | $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken'); |
161 | $this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index'); | ||
162 | $this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache'); | ||
131 | $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index'); | 163 | $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index'); |
132 | 164 | $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle'); | |
133 | $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); | 165 | $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); |
134 | })->add('\Shaarli\Front\ShaarliAdminMiddleware'); | 166 | })->add('\Shaarli\Front\ShaarliAdminMiddleware'); |
135 | 167 | ||
168 | $app->group('/plugin', function () use ($pluginManager) { | ||
169 | foreach ($pluginManager->getRegisteredRoutes() as $pluginName => $routes) { | ||
170 | $this->group('/' . $pluginName, function () use ($routes) { | ||
171 | foreach ($routes as $route) { | ||
172 | $this->{strtolower($route['method'])}('/' . ltrim($route['route'], '/'), $route['callable']); | ||
173 | } | ||
174 | }); | ||
175 | } | ||
176 | })->add('\Shaarli\Front\ShaarliMiddleware'); | ||
136 | 177 | ||
137 | // REST API routes | 178 | // REST API routes |
138 | $app->group('/api/v1', function () { | 179 | $app->group('/api/v1', function () { |
@@ -151,6 +192,12 @@ $app->group('/api/v1', function () { | |||
151 | $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); | 192 | $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); |
152 | })->add('\Shaarli\Api\ApiMiddleware'); | 193 | })->add('\Shaarli\Api\ApiMiddleware'); |
153 | 194 | ||
154 | $response = $app->run(true); | 195 | try { |
155 | 196 | $response = $app->run(true); | |
156 | $app->respond($response); | 197 | $app->respond($response); |
198 | } catch (Throwable $e) { | ||
199 | die(nl2br( | ||
200 | 'An unexpected error happened, and the error template could not be displayed.' . PHP_EOL . PHP_EOL . | ||
201 | exception2text($e) | ||
202 | )); | ||
203 | } | ||
@@ -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 |
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,19 @@ | |||
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 | <exclude-pattern>plugins/*</exclude-pattern> | ||
22 | </rule> | ||
17 | </ruleset> | 23 | </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 ed271532..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 | * |
diff --git a/plugins/default_colors/default_colors.php b/plugins/default_colors/default_colors.php index e1fd5cfb..d3e1fa76 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 | } |
@@ -47,6 +47,20 @@ function default_colors_init($conf) | |||
47 | } | 47 | } |
48 | 48 | ||
49 | /** | 49 | /** |
50 | * When plugin parameters are saved, we regenerate the custom CSS file with provided settings. | ||
51 | * | ||
52 | * @param array $data $_POST array | ||
53 | * | ||
54 | * @return array Updated $_POST array | ||
55 | */ | ||
56 | function hook_default_colors_save_plugin_parameters($data) | ||
57 | { | ||
58 | default_colors_generate_css_file($data); | ||
59 | |||
60 | return $data; | ||
61 | } | ||
62 | |||
63 | /** | ||
50 | * When linklist is displayed, include default_colors CSS file. | 64 | * When linklist is displayed, include default_colors CSS file. |
51 | * | 65 | * |
52 | * @param array $data - header data. | 66 | * @param array $data - header data. |
@@ -56,7 +70,7 @@ function default_colors_init($conf) | |||
56 | function hook_default_colors_render_includes($data) | 70 | function hook_default_colors_render_includes($data) |
57 | { | 71 | { |
58 | $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css'; | 72 | $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css'; |
59 | if (file_exists($file )) { | 73 | if (file_exists($file)) { |
60 | $data['css_files'][] = $file ; | 74 | $data['css_files'][] = $file ; |
61 | } | 75 | } |
62 | 76 | ||
@@ -75,7 +89,7 @@ function default_colors_generate_css_file($params): void | |||
75 | $content = ''; | 89 | $content = ''; |
76 | foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) { | 90 | foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) { |
77 | $content .= !empty($params[$rule]) | 91 | $content .= !empty($params[$rule]) |
78 | ? default_colors_format_css_rule($params, $rule) .';'. PHP_EOL | 92 | ? default_colors_format_css_rule($params, $rule) . ';' . PHP_EOL |
79 | : ''; | 93 | : ''; |
80 | } | 94 | } |
81 | 95 | ||
@@ -99,8 +113,8 @@ function default_colors_format_css_rule($data, $parameter) | |||
99 | } | 113 | } |
100 | 114 | ||
101 | $key = str_replace('DEFAULT_COLORS_', '', $parameter); | 115 | $key = str_replace('DEFAULT_COLORS_', '', $parameter); |
102 | $key = str_replace('_', '-', strtolower($key)) .'-color'; | 116 | $key = str_replace('_', '-', strtolower($key)) . '-color'; |
103 | return ' --'. $key .': '. $data[$parameter]; | 117 | return ' --' . $key . ': ' . $data[$parameter]; |
104 | } | 118 | } |
105 | 119 | ||
106 | 120 | ||
diff --git a/plugins/demo_plugin/DemoPluginController.php b/plugins/demo_plugin/DemoPluginController.php new file mode 100644 index 00000000..b8ace9c8 --- /dev/null +++ b/plugins/demo_plugin/DemoPluginController.php | |||
@@ -0,0 +1,24 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\DemoPlugin; | ||
6 | |||
7 | use Shaarli\Front\Controller\Admin\ShaarliAdminController; | ||
8 | use Slim\Http\Request; | ||
9 | use Slim\Http\Response; | ||
10 | |||
11 | class DemoPluginController extends ShaarliAdminController | ||
12 | { | ||
13 | public function index(Request $request, Response $response): Response | ||
14 | { | ||
15 | $this->assignView( | ||
16 | 'content', | ||
17 | '<div class="center">' . | ||
18 | 'This is a demo page. I have access to Shaarli container, so I\'m free to do whatever I want here.' . | ||
19 | '</div>' | ||
20 | ); | ||
21 | |||
22 | return $response->write($this->render('pluginscontent')); | ||
23 | } | ||
24 | } | ||
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php index defb01f7..15cfc2c5 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 | * |
@@ -6,6 +7,8 @@ | |||
6 | * Can be used by plugin developers to make their own plugin. | 7 | * Can be used by plugin developers to make their own plugin. |
7 | */ | 8 | */ |
8 | 9 | ||
10 | require_once __DIR__ . '/DemoPluginController.php'; | ||
11 | |||
9 | /* | 12 | /* |
10 | * RENDER HEADER, INCLUDES, FOOTER | 13 | * RENDER HEADER, INCLUDES, FOOTER |
11 | * | 14 | * |
@@ -59,6 +62,17 @@ function demo_plugin_init($conf) | |||
59 | return $errors; | 62 | return $errors; |
60 | } | 63 | } |
61 | 64 | ||
65 | function demo_plugin_register_routes(): array | ||
66 | { | ||
67 | return [ | ||
68 | [ | ||
69 | 'method' => 'GET', | ||
70 | 'route' => '/custom', | ||
71 | 'callable' => 'Shaarli\DemoPlugin\DemoPluginController:index', | ||
72 | ], | ||
73 | ]; | ||
74 | } | ||
75 | |||
62 | /** | 76 | /** |
63 | * Hook render_header. | 77 | * Hook render_header. |
64 | * Executed on every page render. | 78 | * Executed on every page render. |
@@ -82,14 +96,14 @@ function hook_demo_plugin_render_header($data) | |||
82 | * A link is an array of its attributes (key="value"), | 96 | * A link is an array of its attributes (key="value"), |
83 | * and a mandatory `html` key, which contains its value. | 97 | * and a mandatory `html` key, which contains its value. |
84 | */ | 98 | */ |
85 | $button = array( | 99 | $button = [ |
86 | 'attr' => array ( | 100 | 'attr' => [ |
87 | 'href' => '#', | 101 | 'href' => '#', |
88 | 'class' => 'mybutton', | 102 | 'class' => 'mybutton', |
89 | 'title' => 'hover me', | 103 | 'title' => 'hover me', |
90 | ), | 104 | ], |
91 | 'html' => 'DEMO buttons toolbar', | 105 | 'html' => 'DEMO buttons toolbar', |
92 | ); | 106 | ]; |
93 | $data['buttons_toolbar'][] = $button; | 107 | $data['buttons_toolbar'][] = $button; |
94 | } | 108 | } |
95 | 109 | ||
@@ -115,29 +129,29 @@ function hook_demo_plugin_render_header($data) | |||
115 | * <input input-2-attribute-1="input 2 attribute 1 value"> | 129 | * <input input-2-attribute-1="input 2 attribute 1 value"> |
116 | * </form> | 130 | * </form> |
117 | */ | 131 | */ |
118 | $form = array( | 132 | $form = [ |
119 | 'attr' => array( | 133 | 'attr' => [ |
120 | 'method' => 'GET', | 134 | 'method' => 'GET', |
121 | 'action' => $data['_BASE_PATH_'] . '/', | 135 | 'action' => $data['_BASE_PATH_'] . '/', |
122 | 'class' => 'addform', | 136 | 'class' => 'addform', |
123 | ), | 137 | ], |
124 | 'inputs' => array( | 138 | 'inputs' => [ |
125 | array( | 139 | [ |
126 | 'type' => 'text', | 140 | 'type' => 'text', |
127 | 'name' => 'demo', | 141 | 'name' => 'demo', |
128 | 'placeholder' => 'demo', | 142 | 'placeholder' => 'demo', |
129 | ) | 143 | ] |
130 | ) | 144 | ] |
131 | ); | 145 | ]; |
132 | $data['fields_toolbar'][] = $form; | 146 | $data['fields_toolbar'][] = $form; |
133 | } | 147 | } |
134 | // Another button always displayed | 148 | // Another button always displayed |
135 | $button = array( | 149 | $button = [ |
136 | 'attr' => array( | 150 | 'attr' => [ |
137 | 'href' => '#', | 151 | 'href' => '#', |
138 | ), | 152 | ], |
139 | 'html' => 'Demo', | 153 | 'html' => 'Demo', |
140 | ); | 154 | ]; |
141 | $data['buttons_toolbar'][] = $button; | 155 | $data['buttons_toolbar'][] = $button; |
142 | 156 | ||
143 | return $data; | 157 | return $data; |
@@ -187,7 +201,7 @@ function hook_demo_plugin_render_includes($data) | |||
187 | function hook_demo_plugin_render_footer($data) | 201 | function hook_demo_plugin_render_footer($data) |
188 | { | 202 | { |
189 | // Footer text | 203 | // Footer text |
190 | $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); | 204 | $data['text'][] = '<br>' . demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); |
191 | 205 | ||
192 | // Free elements at the end of the page. | 206 | // Free elements at the end of the page. |
193 | $data['endofpage'][] = '<marquee id="demo_marquee">' . | 207 | $data['endofpage'][] = '<marquee id="demo_marquee">' . |
@@ -229,13 +243,13 @@ function hook_demo_plugin_render_linklist($data) | |||
229 | * and a mandatory `html` key, which contains its value. | 243 | * and a mandatory `html` key, which contains its value. |
230 | * It's also recommended to add key 'on' or 'off' for theme rendering. | 244 | * It's also recommended to add key 'on' or 'off' for theme rendering. |
231 | */ | 245 | */ |
232 | $action = array( | 246 | $action = [ |
233 | 'attr' => array( | 247 | 'attr' => [ |
234 | 'href' => '?up', | 248 | 'href' => '?up', |
235 | 'title' => 'Uppercase!', | 249 | 'title' => 'Uppercase!', |
236 | ), | 250 | ], |
237 | 'html' => '←', | 251 | 'html' => '←', |
238 | ); | 252 | ]; |
239 | 253 | ||
240 | if (isset($_GET['up'])) { | 254 | if (isset($_GET['up'])) { |
241 | // Manipulate link data | 255 | // Manipulate link data |
@@ -275,7 +289,7 @@ function hook_demo_plugin_render_linklist($data) | |||
275 | function hook_demo_plugin_render_editlink($data) | 289 | function hook_demo_plugin_render_editlink($data) |
276 | { | 290 | { |
277 | // Load HTML into a string | 291 | // Load HTML into a string |
278 | $html = file_get_contents(PluginManager::$PLUGINS_PATH .'/demo_plugin/field.html'); | 292 | $html = file_get_contents(PluginManager::$PLUGINS_PATH . '/demo_plugin/field.html'); |
279 | 293 | ||
280 | // Replace value in HTML if it exists in $data | 294 | // Replace value in HTML if it exists in $data |
281 | if (!empty($data['link']['stuff'])) { | 295 | if (!empty($data['link']['stuff'])) { |
@@ -303,7 +317,11 @@ function hook_demo_plugin_render_editlink($data) | |||
303 | function hook_demo_plugin_render_tools($data) | 317 | function hook_demo_plugin_render_tools($data) |
304 | { | 318 | { |
305 | // field_plugin | 319 | // field_plugin |
306 | $data['tools_plugin'][] = 'tools_plugin'; | 320 | $data['tools_plugin'][] = '<div class="tools-item"> |
321 | <a href="' . $data['_BASE_PATH_'] . '/plugin/demo_plugin/custom"> | ||
322 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Demo Plugin Custom Route</span> | ||
323 | </a> | ||
324 | </div>'; | ||
307 | 325 | ||
308 | return $data; | 326 | return $data; |
309 | } | 327 | } |
diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php index d4632163..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="'. $data['_ROOT_PATH_'].'/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 24fd18ba..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. |
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 d0df3501..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 | ||
@@ -51,7 +53,7 @@ function hook_wallabag_render_linklist($data, $conf) | |||
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/PluginManagerTest.php b/tests/PluginManagerTest.php index efef5e87..8947f679 100644 --- a/tests/PluginManagerTest.php +++ b/tests/PluginManagerTest.php | |||
@@ -120,4 +120,43 @@ class PluginManagerTest extends \Shaarli\TestCase | |||
120 | $this->assertEquals('test plugin', $meta[self::$pluginName]['description']); | 120 | $this->assertEquals('test plugin', $meta[self::$pluginName]['description']); |
121 | $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']); | 121 | $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']); |
122 | } | 122 | } |
123 | |||
124 | /** | ||
125 | * Test plugin custom routes - note that there is no check on callable functions | ||
126 | */ | ||
127 | public function testRegisteredRoutes(): void | ||
128 | { | ||
129 | PluginManager::$PLUGINS_PATH = self::$pluginPath; | ||
130 | $this->pluginManager->load([self::$pluginName]); | ||
131 | |||
132 | $expectedParameters = [ | ||
133 | [ | ||
134 | 'method' => 'GET', | ||
135 | 'route' => '/test', | ||
136 | 'callable' => 'getFunction', | ||
137 | ], | ||
138 | [ | ||
139 | 'method' => 'POST', | ||
140 | 'route' => '/custom', | ||
141 | 'callable' => 'postFunction', | ||
142 | ], | ||
143 | ]; | ||
144 | $meta = $this->pluginManager->getRegisteredRoutes(); | ||
145 | static::assertSame($expectedParameters, $meta[self::$pluginName]); | ||
146 | } | ||
147 | |||
148 | /** | ||
149 | * Test plugin custom routes with invalid route | ||
150 | */ | ||
151 | public function testRegisteredRoutesInvalid(): void | ||
152 | { | ||
153 | $plugin = 'test_route_invalid'; | ||
154 | $this->pluginManager->load([$plugin]); | ||
155 | |||
156 | $meta = $this->pluginManager->getRegisteredRoutes(); | ||
157 | static::assertSame([], $meta); | ||
158 | |||
159 | $errors = $this->pluginManager->getErrors(); | ||
160 | static::assertSame(['test_route_invalid [plugin incompatibility]: trying to register invalid route.'], $errors); | ||
161 | } | ||
123 | } | 162 | } |
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/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php index 7ff92f5c..f755e2d2 100644 --- a/tests/api/controllers/links/PostLinkTest.php +++ b/tests/api/controllers/links/PostLinkTest.php | |||
@@ -92,8 +92,8 @@ class PostLinkTest extends TestCase | |||
92 | 92 | ||
93 | $mock = $this->createMock(Router::class); | 93 | $mock = $this->createMock(Router::class); |
94 | $mock->expects($this->any()) | 94 | $mock->expects($this->any()) |
95 | ->method('relativePathFor') | 95 | ->method('pathFor') |
96 | ->willReturn('api/v1/bookmarks/1'); | 96 | ->willReturn('/api/v1/bookmarks/1'); |
97 | 97 | ||
98 | // affect @property-read... seems to work | 98 | // affect @property-read... seems to work |
99 | $this->controller->getCi()->router = $mock; | 99 | $this->controller->getCi()->router = $mock; |
@@ -128,7 +128,7 @@ class PostLinkTest extends TestCase | |||
128 | 128 | ||
129 | $response = $this->controller->postLink($request, new Response()); | 129 | $response = $this->controller->postLink($request, new Response()); |
130 | $this->assertEquals(201, $response->getStatusCode()); | 130 | $this->assertEquals(201, $response->getStatusCode()); |
131 | $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); | 131 | $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); |
132 | $data = json_decode((string) $response->getBody(), true); | 132 | $data = json_decode((string) $response->getBody(), true); |
133 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); | 133 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); |
134 | $this->assertEquals(43, $data['id']); | 134 | $this->assertEquals(43, $data['id']); |
@@ -175,7 +175,7 @@ class PostLinkTest extends TestCase | |||
175 | $response = $this->controller->postLink($request, new Response()); | 175 | $response = $this->controller->postLink($request, new Response()); |
176 | 176 | ||
177 | $this->assertEquals(201, $response->getStatusCode()); | 177 | $this->assertEquals(201, $response->getStatusCode()); |
178 | $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); | 178 | $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); |
179 | $data = json_decode((string) $response->getBody(), true); | 179 | $data = json_decode((string) $response->getBody(), true); |
180 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); | 180 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); |
181 | $this->assertEquals(43, $data['id']); | 181 | $this->assertEquals(43, $data['id']); |
@@ -229,4 +229,52 @@ class PostLinkTest extends TestCase | |||
229 | \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) | 229 | \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) |
230 | ); | 230 | ); |
231 | } | 231 | } |
232 | |||
233 | /** | ||
234 | * Test link creation with a tag string provided | ||
235 | */ | ||
236 | public function testPostLinkWithTagString(): void | ||
237 | { | ||
238 | $link = [ | ||
239 | 'tags' => 'one two', | ||
240 | ]; | ||
241 | $env = Environment::mock([ | ||
242 | 'REQUEST_METHOD' => 'POST', | ||
243 | 'CONTENT_TYPE' => 'application/json' | ||
244 | ]); | ||
245 | |||
246 | $request = Request::createFromEnvironment($env); | ||
247 | $request = $request->withParsedBody($link); | ||
248 | $response = $this->controller->postLink($request, new Response()); | ||
249 | |||
250 | $this->assertEquals(201, $response->getStatusCode()); | ||
251 | $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); | ||
252 | $data = json_decode((string) $response->getBody(), true); | ||
253 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); | ||
254 | $this->assertEquals(['one', 'two'], $data['tags']); | ||
255 | } | ||
256 | |||
257 | /** | ||
258 | * Test link creation with a tag string provided | ||
259 | */ | ||
260 | public function testPostLinkWithTagString2(): void | ||
261 | { | ||
262 | $link = [ | ||
263 | 'tags' => ['one two'], | ||
264 | ]; | ||
265 | $env = Environment::mock([ | ||
266 | 'REQUEST_METHOD' => 'POST', | ||
267 | 'CONTENT_TYPE' => 'application/json' | ||
268 | ]); | ||
269 | |||
270 | $request = Request::createFromEnvironment($env); | ||
271 | $request = $request->withParsedBody($link); | ||
272 | $response = $this->controller->postLink($request, new Response()); | ||
273 | |||
274 | $this->assertEquals(201, $response->getStatusCode()); | ||
275 | $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); | ||
276 | $data = json_decode((string) $response->getBody(), true); | ||
277 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); | ||
278 | $this->assertEquals(['one', 'two'], $data['tags']); | ||
279 | } | ||
232 | } | 280 | } |
diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php index 240ee323..fe24f2eb 100644 --- a/tests/api/controllers/links/PutLinkTest.php +++ b/tests/api/controllers/links/PutLinkTest.php | |||
@@ -233,4 +233,52 @@ class PutLinkTest extends \Shaarli\TestCase | |||
233 | 233 | ||
234 | $this->controller->putLink($request, new Response(), ['id' => -1]); | 234 | $this->controller->putLink($request, new Response(), ['id' => -1]); |
235 | } | 235 | } |
236 | |||
237 | /** | ||
238 | * Test link creation with a tag string provided | ||
239 | */ | ||
240 | public function testPutLinkWithTagString(): void | ||
241 | { | ||
242 | $link = [ | ||
243 | 'tags' => 'one two', | ||
244 | ]; | ||
245 | $id = '41'; | ||
246 | $env = Environment::mock([ | ||
247 | 'REQUEST_METHOD' => 'PUT', | ||
248 | 'CONTENT_TYPE' => 'application/json' | ||
249 | ]); | ||
250 | |||
251 | $request = Request::createFromEnvironment($env); | ||
252 | $request = $request->withParsedBody($link); | ||
253 | $response = $this->controller->putLink($request, new Response(), ['id' => $id]); | ||
254 | |||
255 | $this->assertEquals(200, $response->getStatusCode()); | ||
256 | $data = json_decode((string) $response->getBody(), true); | ||
257 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); | ||
258 | $this->assertEquals(['one', 'two'], $data['tags']); | ||
259 | } | ||
260 | |||
261 | /** | ||
262 | * Test link creation with a tag string provided | ||
263 | */ | ||
264 | public function testPutLinkWithTagString2(): void | ||
265 | { | ||
266 | $link = [ | ||
267 | 'tags' => ['one two'], | ||
268 | ]; | ||
269 | $id = '41'; | ||
270 | $env = Environment::mock([ | ||
271 | 'REQUEST_METHOD' => 'PUT', | ||
272 | 'CONTENT_TYPE' => 'application/json' | ||
273 | ]); | ||
274 | |||
275 | $request = Request::createFromEnvironment($env); | ||
276 | $request = $request->withParsedBody($link); | ||
277 | $response = $this->controller->putLink($request, new Response(), ['id' => $id]); | ||
278 | |||
279 | $this->assertEquals(200, $response->getStatusCode()); | ||
280 | $data = json_decode((string) $response->getBody(), true); | ||
281 | $this->assertEquals(self::NB_FIELDS_LINK, count($data)); | ||
282 | $this->assertEquals(['one', 'two'], $data['tags']); | ||
283 | } | ||
236 | } | 284 | } |
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php index daafd250..f619aff3 100644 --- a/tests/bookmark/BookmarkFileServiceTest.php +++ b/tests/bookmark/BookmarkFileServiceTest.php | |||
@@ -686,22 +686,6 @@ class BookmarkFileServiceTest extends TestCase | |||
686 | } | 686 | } |
687 | 687 | ||
688 | /** | 688 | /** |
689 | * List the days for which bookmarks have been posted | ||
690 | */ | ||
691 | public function testDays() | ||
692 | { | ||
693 | $this->assertSame( | ||
694 | ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'], | ||
695 | $this->publicLinkDB->days() | ||
696 | ); | ||
697 | |||
698 | $this->assertSame( | ||
699 | ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'], | ||
700 | $this->privateLinkDB->days() | ||
701 | ); | ||
702 | } | ||
703 | |||
704 | /** | ||
705 | * The URL corresponds to an existing entry in the DB | 689 | * The URL corresponds to an existing entry in the DB |
706 | */ | 690 | */ |
707 | public function testGetKnownLinkFromURL() | 691 | public function testGetKnownLinkFromURL() |
@@ -898,6 +882,37 @@ class BookmarkFileServiceTest extends TestCase | |||
898 | } | 882 | } |
899 | 883 | ||
900 | /** | 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 | /** | ||
901 | * Test linksCountPerTag all tags without filter. | 916 | * Test linksCountPerTag all tags without filter. |
902 | * Equal occurrences should be sorted alphabetically. | 917 | * Equal occurrences should be sorted alphabetically. |
903 | */ | 918 | */ |
@@ -1043,33 +1058,105 @@ class BookmarkFileServiceTest extends TestCase | |||
1043 | } | 1058 | } |
1044 | 1059 | ||
1045 | /** | 1060 | /** |
1046 | * 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. |
1047 | */ | 1062 | */ |
1048 | public function testFilterDayLoggedIn(): void | 1063 | public function testFilterByDateMidTimePeriodSingleBookmark(): void |
1049 | { | 1064 | { |
1050 | $bookmarks = $this->privateLinkDB->filterDay('20121206'); | 1065 | $bookmarks = $this->privateLinkDB->findByDate( |
1051 | $expectedIds = [4, 9, 1, 0]; | 1066 | DateTime::createFromFormat('Ymd_His', '20121206_150000'), |
1067 | DateTime::createFromFormat('Ymd_His', '20121206_160000'), | ||
1068 | $before, | ||
1069 | $after | ||
1070 | ); | ||
1052 | 1071 | ||
1053 | static::assertCount(4, $bookmarks); | 1072 | static::assertCount(1, $bookmarks); |
1054 | foreach ($bookmarks as $bookmark) { | 1073 | |
1055 | $i = ($i ?? -1) + 1; | 1074 | static::assertSame(9, $bookmarks[0]->getId()); |
1056 | static::assertSame($expectedIds[$i], $bookmark->getId()); | 1075 | static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before); |
1057 | } | 1076 | static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after); |
1058 | } | 1077 | } |
1059 | 1078 | ||
1060 | /** | 1079 | /** |
1061 | * 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. |
1062 | */ | 1081 | */ |
1063 | public function testFilterDayLoggedOut(): void | 1082 | public function testFilterByDateMidTimePeriodMultipleBookmarks(): void |
1064 | { | 1083 | { |
1065 | $bookmarks = $this->publicLinkDB->filterDay('20121206'); | 1084 | $bookmarks = $this->privateLinkDB->findByDate( |
1066 | $expectedIds = [4, 9, 1]; | 1085 | DateTime::createFromFormat('Ymd_His', '20121206_150000'), |
1086 | DateTime::createFromFormat('Ymd_His', '20121206_180000'), | ||
1087 | $before, | ||
1088 | $after | ||
1089 | ); | ||
1067 | 1090 | ||
1068 | static::assertCount(3, $bookmarks); | 1091 | static::assertCount(2, $bookmarks); |
1069 | foreach ($bookmarks as $bookmark) { | 1092 | |
1070 | $i = ($i ?? -1) + 1; | 1093 | static::assertSame(1, $bookmarks[0]->getId()); |
1071 | static::assertSame($expectedIds[$i], $bookmark->getId()); | 1094 | static::assertSame(9, $bookmarks[1]->getId()); |
1072 | } | 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); | ||
1073 | } | 1160 | } |
1074 | 1161 | ||
1075 | /** | 1162 | /** |
diff --git a/tests/bookmark/BookmarkFilterTest.php b/tests/bookmark/BookmarkFilterTest.php index 574d8e3f..835674f2 100644 --- a/tests/bookmark/BookmarkFilterTest.php +++ b/tests/bookmark/BookmarkFilterTest.php | |||
@@ -44,7 +44,7 @@ class BookmarkFilterTest extends TestCase | |||
44 | self::$refDB->write(self::$testDatastore); | 44 | self::$refDB->write(self::$testDatastore); |
45 | $history = new History('sandbox/history.php'); | 45 | $history = new History('sandbox/history.php'); |
46 | self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true); | 46 | self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true); |
47 | self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks()); | 47 | self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks(), $conf); |
48 | } | 48 | } |
49 | 49 | ||
50 | /** | 50 | /** |
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php index 4c7ae4c0..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() |
@@ -252,7 +269,7 @@ class BookmarkTest extends TestCase | |||
252 | { | 269 | { |
253 | $bookmark = new Bookmark(); | 270 | $bookmark = new Bookmark(); |
254 | 271 | ||
255 | $str = 'tag1 tag2 tag3.tag3-2, tag4 , -tag5 '; | 272 | $str = 'tag1 tag2 tag3.tag3-2 tag4 -tag5 '; |
256 | $bookmark->setTagsString($str); | 273 | $bookmark->setTagsString($str); |
257 | $this->assertEquals( | 274 | $this->assertEquals( |
258 | [ | 275 | [ |
@@ -276,9 +293,9 @@ class BookmarkTest extends TestCase | |||
276 | $array = [ | 293 | $array = [ |
277 | 'tag1 ', | 294 | 'tag1 ', |
278 | ' tag2', | 295 | ' tag2', |
279 | 'tag3.tag3-2,', | 296 | 'tag3.tag3-2', |
280 | ', tag4', | 297 | ' tag4', |
281 | ', ', | 298 | ' ', |
282 | '-tag5 ', | 299 | '-tag5 ', |
283 | ]; | 300 | ]; |
284 | $bookmark->setTags($array); | 301 | $bookmark->setTags($array); |
@@ -347,4 +364,48 @@ class BookmarkTest extends TestCase | |||
347 | $bookmark->deleteTag('nope'); | 364 | $bookmark->deleteTag('nope'); |
348 | $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); | 365 | $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); |
349 | } | 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 | } | ||
350 | } | 411 | } |
diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php index 29941c8c..46a7f1fe 100644 --- a/tests/bookmark/LinkUtilsTest.php +++ b/tests/bookmark/LinkUtilsTest.php | |||
@@ -169,6 +169,36 @@ class LinkUtilsTest extends TestCase | |||
169 | } | 169 | } |
170 | 170 | ||
171 | /** | 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)); | ||
199 | } | ||
200 | |||
201 | /** | ||
172 | * Test html_extract_tag() when the tag <meta name= is not found. | 202 | * Test html_extract_tag() when the tag <meta name= is not found. |
173 | */ | 203 | */ |
174 | public function testHtmlExtractNonExistentNameTag() | 204 | public function testHtmlExtractNonExistentNameTag() |
@@ -215,61 +245,104 @@ class LinkUtilsTest extends TestCase | |||
215 | $this->assertFalse(html_extract_tag('description', $html)); | 245 | $this->assertFalse(html_extract_tag('description', $html)); |
216 | } | 246 | } |
217 | 247 | ||
248 | public function testHtmlExtractDescriptionFromGoogleRealCase(): void | ||
249 | { | ||
250 | $html = 'id="gsr"><meta content="Fêtes de fin d\'année" property="twitter:title"><meta '. | ||
251 | 'content="Bonnes fêtes de fin d\'année ! #GoogleDoodle" property="twitter:description">'. | ||
252 | '<meta content="Bonnes fêtes de fin d\'année ! #GoogleDoodle" property="og:description">'. | ||
253 | '<meta content="summary_large_image" property="twitter:card"><meta co' | ||
254 | ; | ||
255 | $this->assertSame('Bonnes fêtes de fin d\'année ! #GoogleDoodle', html_extract_tag('description', $html)); | ||
256 | } | ||
257 | |||
258 | /** | ||
259 | * Test the header callback with valid value | ||
260 | */ | ||
261 | public function testCurlHeaderCallbackOk(): void | ||
262 | { | ||
263 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ok'); | ||
264 | $data = [ | ||
265 | 'HTTP/1.1 200 OK', | ||
266 | 'Server: GitHub.com', | ||
267 | 'Date: Sat, 28 Oct 2017 12:01:33 GMT', | ||
268 | 'Content-Type: text/html; charset=utf-8', | ||
269 | 'Status: 200 OK', | ||
270 | ]; | ||
271 | |||
272 | foreach ($data as $chunk) { | ||
273 | static::assertIsInt($callback(null, $chunk)); | ||
274 | } | ||
275 | |||
276 | static::assertSame('utf-8', $charset); | ||
277 | } | ||
278 | |||
218 | /** | 279 | /** |
219 | * Test the download callback with valid value | 280 | * Test the download callback with valid value |
220 | */ | 281 | */ |
221 | public function testCurlDownloadCallbackOk() | 282 | public function testCurlDownloadCallbackOk(): void |
222 | { | 283 | { |
284 | $charset = 'utf-8'; | ||
223 | $callback = get_curl_download_callback( | 285 | $callback = get_curl_download_callback( |
224 | $charset, | 286 | $charset, |
225 | $title, | 287 | $title, |
226 | $desc, | 288 | $desc, |
227 | $keywords, | 289 | $keywords, |
228 | false, | 290 | false, |
229 | 'ut_curl_getinfo_ok' | 291 | ' ' |
230 | ); | 292 | ); |
293 | |||
231 | $data = [ | 294 | $data = [ |
232 | 'HTTP/1.1 200 OK', | 295 | 'th=device-width">' |
233 | 'Server: GitHub.com', | ||
234 | 'Date: Sat, 28 Oct 2017 12:01:33 GMT', | ||
235 | 'Content-Type: text/html; charset=utf-8', | ||
236 | 'Status: 200 OK', | ||
237 | 'end' => 'th=device-width">' | ||
238 | . '<title>Refactoring · GitHub</title>' | 296 | . '<title>Refactoring · GitHub</title>' |
239 | . '<link rel="search" type="application/opensea', | 297 | . '<link rel="search" type="application/opensea', |
240 | '<title>ignored</title>' | 298 | '<title>ignored</title>' |
241 | . '<meta name="description" content="desc" />' | 299 | . '<meta name="description" content="desc" />' |
242 | . '<meta name="keywords" content="key1,key2" />', | 300 | . '<meta name="keywords" content="key1,key2" />', |
243 | ]; | 301 | ]; |
244 | foreach ($data as $key => $line) { | 302 | |
245 | $ignore = null; | 303 | foreach ($data as $chunk) { |
246 | $expected = $key !== 'end' ? strlen($line) : false; | 304 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
247 | $this->assertEquals($expected, $callback($ignore, $line)); | ||
248 | if ($expected === false) { | ||
249 | break; | ||
250 | } | ||
251 | } | 305 | } |
252 | $this->assertEquals('utf-8', $charset); | 306 | |
253 | $this->assertEquals('Refactoring · GitHub', $title); | 307 | static::assertSame('utf-8', $charset); |
254 | $this->assertEmpty($desc); | 308 | static::assertSame('Refactoring · GitHub', $title); |
255 | $this->assertEmpty($keywords); | 309 | static::assertEmpty($desc); |
310 | static::assertEmpty($keywords); | ||
311 | } | ||
312 | |||
313 | /** | ||
314 | * Test the header callback with valid value | ||
315 | */ | ||
316 | public function testCurlHeaderCallbackNoCharset(): void | ||
317 | { | ||
318 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_no_charset'); | ||
319 | $data = [ | ||
320 | 'HTTP/1.1 200 OK', | ||
321 | ]; | ||
322 | |||
323 | foreach ($data as $chunk) { | ||
324 | static::assertSame(strlen($chunk), $callback(null, $chunk)); | ||
325 | } | ||
326 | |||
327 | static::assertFalse($charset); | ||
256 | } | 328 | } |
257 | 329 | ||
258 | /** | 330 | /** |
259 | * Test the download callback with valid values and no charset | 331 | * Test the download callback with valid values and no charset |
260 | */ | 332 | */ |
261 | public function testCurlDownloadCallbackOkNoCharset() | 333 | public function testCurlDownloadCallbackOkNoCharset(): void |
262 | { | 334 | { |
335 | $charset = null; | ||
263 | $callback = get_curl_download_callback( | 336 | $callback = get_curl_download_callback( |
264 | $charset, | 337 | $charset, |
265 | $title, | 338 | $title, |
266 | $desc, | 339 | $desc, |
267 | $keywords, | 340 | $keywords, |
268 | false, | 341 | false, |
269 | 'ut_curl_getinfo_no_charset' | 342 | ' ' |
270 | ); | 343 | ); |
344 | |||
271 | $data = [ | 345 | $data = [ |
272 | 'HTTP/1.1 200 OK', | ||
273 | 'end' => 'th=device-width">' | 346 | 'end' => 'th=device-width">' |
274 | . '<title>Refactoring · GitHub</title>' | 347 | . '<title>Refactoring · GitHub</title>' |
275 | . '<link rel="search" type="application/opensea', | 348 | . '<link rel="search" type="application/opensea', |
@@ -277,10 +350,11 @@ class LinkUtilsTest extends TestCase | |||
277 | . '<meta name="description" content="desc" />' | 350 | . '<meta name="description" content="desc" />' |
278 | . '<meta name="keywords" content="key1,key2" />', | 351 | . '<meta name="keywords" content="key1,key2" />', |
279 | ]; | 352 | ]; |
280 | foreach ($data as $key => $line) { | 353 | |
281 | $ignore = null; | 354 | foreach ($data as $chunk) { |
282 | $this->assertEquals(strlen($line), $callback($ignore, $line)); | 355 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
283 | } | 356 | } |
357 | |||
284 | $this->assertEmpty($charset); | 358 | $this->assertEmpty($charset); |
285 | $this->assertEquals('Refactoring · GitHub', $title); | 359 | $this->assertEquals('Refactoring · GitHub', $title); |
286 | $this->assertEmpty($desc); | 360 | $this->assertEmpty($desc); |
@@ -290,18 +364,19 @@ class LinkUtilsTest extends TestCase | |||
290 | /** | 364 | /** |
291 | * Test the download callback with valid values and no charset | 365 | * Test the download callback with valid values and no charset |
292 | */ | 366 | */ |
293 | public function testCurlDownloadCallbackOkHtmlCharset() | 367 | public function testCurlDownloadCallbackOkHtmlCharset(): void |
294 | { | 368 | { |
369 | $charset = null; | ||
295 | $callback = get_curl_download_callback( | 370 | $callback = get_curl_download_callback( |
296 | $charset, | 371 | $charset, |
297 | $title, | 372 | $title, |
298 | $desc, | 373 | $desc, |
299 | $keywords, | 374 | $keywords, |
300 | false, | 375 | false, |
301 | 'ut_curl_getinfo_no_charset' | 376 | ' ' |
302 | ); | 377 | ); |
378 | |||
303 | $data = [ | 379 | $data = [ |
304 | 'HTTP/1.1 200 OK', | ||
305 | '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />', | 380 | '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />', |
306 | 'end' => 'th=device-width">' | 381 | 'end' => 'th=device-width">' |
307 | . '<title>Refactoring · GitHub</title>' | 382 | . '<title>Refactoring · GitHub</title>' |
@@ -310,14 +385,10 @@ class LinkUtilsTest extends TestCase | |||
310 | . '<meta name="description" content="desc" />' | 385 | . '<meta name="description" content="desc" />' |
311 | . '<meta name="keywords" content="key1,key2" />', | 386 | . '<meta name="keywords" content="key1,key2" />', |
312 | ]; | 387 | ]; |
313 | foreach ($data as $key => $line) { | 388 | foreach ($data as $chunk) { |
314 | $ignore = null; | 389 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
315 | $expected = $key !== 'end' ? strlen($line) : false; | ||
316 | $this->assertEquals($expected, $callback($ignore, $line)); | ||
317 | if ($expected === false) { | ||
318 | break; | ||
319 | } | ||
320 | } | 390 | } |
391 | |||
321 | $this->assertEquals('utf-8', $charset); | 392 | $this->assertEquals('utf-8', $charset); |
322 | $this->assertEquals('Refactoring · GitHub', $title); | 393 | $this->assertEquals('Refactoring · GitHub', $title); |
323 | $this->assertEmpty($desc); | 394 | $this->assertEmpty($desc); |
@@ -327,25 +398,27 @@ class LinkUtilsTest extends TestCase | |||
327 | /** | 398 | /** |
328 | * Test the download callback with valid values and no title | 399 | * Test the download callback with valid values and no title |
329 | */ | 400 | */ |
330 | public function testCurlDownloadCallbackOkNoTitle() | 401 | public function testCurlDownloadCallbackOkNoTitle(): void |
331 | { | 402 | { |
403 | $charset = 'utf-8'; | ||
332 | $callback = get_curl_download_callback( | 404 | $callback = get_curl_download_callback( |
333 | $charset, | 405 | $charset, |
334 | $title, | 406 | $title, |
335 | $desc, | 407 | $desc, |
336 | $keywords, | 408 | $keywords, |
337 | false, | 409 | false, |
338 | 'ut_curl_getinfo_ok' | 410 | ' ' |
339 | ); | 411 | ); |
412 | |||
340 | $data = [ | 413 | $data = [ |
341 | 'HTTP/1.1 200 OK', | ||
342 | 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea', | 414 | 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea', |
343 | 'ignored', | 415 | 'ignored', |
344 | ]; | 416 | ]; |
345 | foreach ($data as $key => $line) { | 417 | |
346 | $ignore = null; | 418 | foreach ($data as $chunk) { |
347 | $this->assertEquals(strlen($line), $callback($ignore, $line)); | 419 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
348 | } | 420 | } |
421 | |||
349 | $this->assertEquals('utf-8', $charset); | 422 | $this->assertEquals('utf-8', $charset); |
350 | $this->assertEmpty($title); | 423 | $this->assertEmpty($title); |
351 | $this->assertEmpty($desc); | 424 | $this->assertEmpty($desc); |
@@ -353,81 +426,56 @@ class LinkUtilsTest extends TestCase | |||
353 | } | 426 | } |
354 | 427 | ||
355 | /** | 428 | /** |
356 | * Test the download callback with an invalid content type. | 429 | * Test the header callback with an invalid content type. |
357 | */ | 430 | */ |
358 | public function testCurlDownloadCallbackInvalidContentType() | 431 | public function testCurlHeaderCallbackInvalidContentType(): void |
359 | { | 432 | { |
360 | $callback = get_curl_download_callback( | 433 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ct_ko'); |
361 | $charset, | 434 | $data = [ |
362 | $title, | 435 | 'HTTP/1.1 200 OK', |
363 | $desc, | 436 | ]; |
364 | $keywords, | 437 | |
365 | false, | 438 | static::assertFalse($callback(null, $data[0])); |
366 | 'ut_curl_getinfo_ct_ko' | 439 | static::assertNull($charset); |
367 | ); | ||
368 | $ignore = null; | ||
369 | $this->assertFalse($callback($ignore, '')); | ||
370 | $this->assertEmpty($charset); | ||
371 | $this->assertEmpty($title); | ||
372 | } | 440 | } |
373 | 441 | ||
374 | /** | 442 | /** |
375 | * Test the download callback with an invalid response code. | 443 | * Test the header callback with an invalid response code. |
376 | */ | 444 | */ |
377 | public function testCurlDownloadCallbackInvalidResponseCode() | 445 | public function testCurlHeaderCallbackInvalidResponseCode(): void |
378 | { | 446 | { |
379 | $callback = $callback = get_curl_download_callback( | 447 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rc_ko'); |
380 | $charset, | 448 | |
381 | $title, | 449 | static::assertFalse($callback(null, '')); |
382 | $desc, | 450 | static::assertNull($charset); |
383 | $keywords, | ||
384 | false, | ||
385 | 'ut_curl_getinfo_rc_ko' | ||
386 | ); | ||
387 | $ignore = null; | ||
388 | $this->assertFalse($callback($ignore, '')); | ||
389 | $this->assertEmpty($charset); | ||
390 | $this->assertEmpty($title); | ||
391 | } | 451 | } |
392 | 452 | ||
393 | /** | 453 | /** |
394 | * Test the download callback with an invalid content type and response code. | 454 | * Test the header callback with an invalid content type and response code. |
395 | */ | 455 | */ |
396 | public function testCurlDownloadCallbackInvalidContentTypeAndResponseCode() | 456 | public function testCurlHeaderCallbackInvalidContentTypeAndResponseCode(): void |
397 | { | 457 | { |
398 | $callback = $callback = get_curl_download_callback( | 458 | $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rs_ct_ko'); |
399 | $charset, | 459 | |
400 | $title, | 460 | static::assertFalse($callback(null, '')); |
401 | $desc, | 461 | static::assertNull($charset); |
402 | $keywords, | ||
403 | false, | ||
404 | 'ut_curl_getinfo_rs_ct_ko' | ||
405 | ); | ||
406 | $ignore = null; | ||
407 | $this->assertFalse($callback($ignore, '')); | ||
408 | $this->assertEmpty($charset); | ||
409 | $this->assertEmpty($title); | ||
410 | } | 462 | } |
411 | 463 | ||
412 | /** | 464 | /** |
413 | * Test the download callback with valid value, and retrieve_description option enabled. | 465 | * Test the download callback with valid value, and retrieve_description option enabled. |
414 | */ | 466 | */ |
415 | public function testCurlDownloadCallbackOkWithDesc() | 467 | public function testCurlDownloadCallbackOkWithDesc(): void |
416 | { | 468 | { |
469 | $charset = 'utf-8'; | ||
417 | $callback = get_curl_download_callback( | 470 | $callback = get_curl_download_callback( |
418 | $charset, | 471 | $charset, |
419 | $title, | 472 | $title, |
420 | $desc, | 473 | $desc, |
421 | $keywords, | 474 | $keywords, |
422 | true, | 475 | true, |
423 | 'ut_curl_getinfo_ok' | 476 | ' ' |
424 | ); | 477 | ); |
425 | $data = [ | 478 | $data = [ |
426 | 'HTTP/1.1 200 OK', | ||
427 | 'Server: GitHub.com', | ||
428 | 'Date: Sat, 28 Oct 2017 12:01:33 GMT', | ||
429 | 'Content-Type: text/html; charset=utf-8', | ||
430 | 'Status: 200 OK', | ||
431 | 'th=device-width">' | 479 | 'th=device-width">' |
432 | . '<title>Refactoring · GitHub</title>' | 480 | . '<title>Refactoring · GitHub</title>' |
433 | . '<link rel="search" type="application/opensea', | 481 | . '<link rel="search" type="application/opensea', |
@@ -435,14 +483,11 @@ class LinkUtilsTest extends TestCase | |||
435 | . '<meta name="description" content="link desc" />' | 483 | . '<meta name="description" content="link desc" />' |
436 | . '<meta name="keywords" content="key1,key2" />', | 484 | . '<meta name="keywords" content="key1,key2" />', |
437 | ]; | 485 | ]; |
438 | foreach ($data as $key => $line) { | 486 | |
439 | $ignore = null; | 487 | foreach ($data as $chunk) { |
440 | $expected = $key !== 'end' ? strlen($line) : false; | 488 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
441 | $this->assertEquals($expected, $callback($ignore, $line)); | ||
442 | if ($expected === false) { | ||
443 | break; | ||
444 | } | ||
445 | } | 489 | } |
490 | |||
446 | $this->assertEquals('utf-8', $charset); | 491 | $this->assertEquals('utf-8', $charset); |
447 | $this->assertEquals('Refactoring · GitHub', $title); | 492 | $this->assertEquals('Refactoring · GitHub', $title); |
448 | $this->assertEquals('link desc', $desc); | 493 | $this->assertEquals('link desc', $desc); |
@@ -453,8 +498,9 @@ class LinkUtilsTest extends TestCase | |||
453 | * Test the download callback with valid value, and retrieve_description option enabled, | 498 | * Test the download callback with valid value, and retrieve_description option enabled, |
454 | * but no desc or keyword defined in the page. | 499 | * but no desc or keyword defined in the page. |
455 | */ | 500 | */ |
456 | public function testCurlDownloadCallbackOkWithDescNotFound() | 501 | public function testCurlDownloadCallbackOkWithDescNotFound(): void |
457 | { | 502 | { |
503 | $charset = 'utf-8'; | ||
458 | $callback = get_curl_download_callback( | 504 | $callback = get_curl_download_callback( |
459 | $charset, | 505 | $charset, |
460 | $title, | 506 | $title, |
@@ -464,24 +510,16 @@ class LinkUtilsTest extends TestCase | |||
464 | 'ut_curl_getinfo_ok' | 510 | 'ut_curl_getinfo_ok' |
465 | ); | 511 | ); |
466 | $data = [ | 512 | $data = [ |
467 | 'HTTP/1.1 200 OK', | ||
468 | 'Server: GitHub.com', | ||
469 | 'Date: Sat, 28 Oct 2017 12:01:33 GMT', | ||
470 | 'Content-Type: text/html; charset=utf-8', | ||
471 | 'Status: 200 OK', | ||
472 | 'th=device-width">' | 513 | 'th=device-width">' |
473 | . '<title>Refactoring · GitHub</title>' | 514 | . '<title>Refactoring · GitHub</title>' |
474 | . '<link rel="search" type="application/opensea', | 515 | . '<link rel="search" type="application/opensea', |
475 | 'end' => '<title>ignored</title>', | 516 | 'end' => '<title>ignored</title>', |
476 | ]; | 517 | ]; |
477 | foreach ($data as $key => $line) { | 518 | |
478 | $ignore = null; | 519 | foreach ($data as $chunk) { |
479 | $expected = $key !== 'end' ? strlen($line) : false; | 520 | static::assertSame(strlen($chunk), $callback(null, $chunk)); |
480 | $this->assertEquals($expected, $callback($ignore, $line)); | ||
481 | if ($expected === false) { | ||
482 | break; | ||
483 | } | ||
484 | } | 521 | } |
522 | |||
485 | $this->assertEquals('utf-8', $charset); | 523 | $this->assertEquals('utf-8', $charset); |
486 | $this->assertEquals('Refactoring · GitHub', $title); | 524 | $this->assertEquals('Refactoring · GitHub', $title); |
487 | $this->assertEmpty($desc); | 525 | $this->assertEmpty($desc); |
@@ -582,6 +620,115 @@ class LinkUtilsTest extends TestCase | |||
582 | } | 620 | } |
583 | 621 | ||
584 | /** | 622 | /** |
623 | * Test tags_str2array with whitespace separator. | ||
624 | */ | ||
625 | public function testTagsStr2ArrayWithSpaceSeparator(): void | ||
626 | { | ||
627 | $separator = ' '; | ||
628 | |||
629 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator)); | ||
630 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator)); | ||
631 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array(' tag1 tag2 tag3 ', $separator)); | ||
632 | static::assertSame(['tag1@', 'tag2,', '.tag3'], tags_str2array(' tag1@ tag2, .tag3 ', $separator)); | ||
633 | static::assertSame([], tags_str2array('', $separator)); | ||
634 | static::assertSame([], tags_str2array(' ', $separator)); | ||
635 | static::assertSame([], tags_str2array(null, $separator)); | ||
636 | } | ||
637 | |||
638 | /** | ||
639 | * Test tags_str2array with @ separator. | ||
640 | */ | ||
641 | public function testTagsStr2ArrayWithCharSeparator(): void | ||
642 | { | ||
643 | $separator = '@'; | ||
644 | |||
645 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@tag2@tag3', $separator)); | ||
646 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@@@@tag2@@@@tag3', $separator)); | ||
647 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('@@@tag1@@@tag2@@@@tag3@@', $separator)); | ||
648 | static::assertSame( | ||
649 | ['tag1#', 'tag2, and other', '.tag3'], | ||
650 | tags_str2array('@@@ tag1# @@@ tag2, and other @@@@.tag3@@', $separator) | ||
651 | ); | ||
652 | static::assertSame([], tags_str2array('', $separator)); | ||
653 | static::assertSame([], tags_str2array(' ', $separator)); | ||
654 | static::assertSame([], tags_str2array(null, $separator)); | ||
655 | } | ||
656 | |||
657 | /** | ||
658 | * Test tags_array2str with ' ' separator. | ||
659 | */ | ||
660 | public function testTagsArray2StrWithSpaceSeparator(): void | ||
661 | { | ||
662 | $separator = ' '; | ||
663 | |||
664 | static::assertSame('tag1 tag2 tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator)); | ||
665 | static::assertSame('tag1, tag2@ tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator)); | ||
666 | static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', 'tag2', 'tag3 '], $separator)); | ||
667 | static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator)); | ||
668 | static::assertSame('tag1', tags_array2str([' tag1 '], $separator)); | ||
669 | static::assertSame('', tags_array2str([' '], $separator)); | ||
670 | static::assertSame('', tags_array2str([], $separator)); | ||
671 | static::assertSame('', tags_array2str(null, $separator)); | ||
672 | } | ||
673 | |||
674 | /** | ||
675 | * Test tags_array2str with @ separator. | ||
676 | */ | ||
677 | public function testTagsArray2StrWithCharSeparator(): void | ||
678 | { | ||
679 | $separator = '@'; | ||
680 | |||
681 | static::assertSame('tag1@tag2@tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator)); | ||
682 | static::assertSame('tag1,@tag2@tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator)); | ||
683 | static::assertSame( | ||
684 | 'tag1@tag2, and other@tag3', | ||
685 | tags_array2str(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator) | ||
686 | ); | ||
687 | static::assertSame('tag1@tag2@tag3', tags_array2str(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator)); | ||
688 | static::assertSame('tag1', tags_array2str(['@@@@tag1@@@@'], $separator)); | ||
689 | static::assertSame('', tags_array2str(['@@@'], $separator)); | ||
690 | static::assertSame('', tags_array2str([], $separator)); | ||
691 | static::assertSame('', tags_array2str(null, $separator)); | ||
692 | } | ||
693 | |||
694 | /** | ||
695 | * Test tags_array2str with @ separator. | ||
696 | */ | ||
697 | public function testTagsFilterWithSpaceSeparator(): void | ||
698 | { | ||
699 | $separator = ' '; | ||
700 | |||
701 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator)); | ||
702 | static::assertSame(['tag1,', 'tag2@', 'tag3'], tags_filter(['tag1,', 'tag2@', 'tag3'], $separator)); | ||
703 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', 'tag2', 'tag3 '], $separator)); | ||
704 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator)); | ||
705 | static::assertSame(['tag1'], tags_filter([' tag1 '], $separator)); | ||
706 | static::assertSame([], tags_filter([' '], $separator)); | ||
707 | static::assertSame([], tags_filter([], $separator)); | ||
708 | static::assertSame([], tags_filter(null, $separator)); | ||
709 | } | ||
710 | |||
711 | /** | ||
712 | * Test tags_array2str with @ separator. | ||
713 | */ | ||
714 | public function testTagsArrayFilterWithSpaceSeparator(): void | ||
715 | { | ||
716 | $separator = '@'; | ||
717 | |||
718 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator)); | ||
719 | static::assertSame(['tag1,', 'tag2#', 'tag3'], tags_filter(['tag1,', 'tag2#', 'tag3'], $separator)); | ||
720 | static::assertSame( | ||
721 | ['tag1', 'tag2, and other', 'tag3'], | ||
722 | tags_filter(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator) | ||
723 | ); | ||
724 | static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator)); | ||
725 | static::assertSame(['tag1'], tags_filter(['@@@@tag1@@@@'], $separator)); | ||
726 | static::assertSame([], tags_filter(['@@@'], $separator)); | ||
727 | static::assertSame([], tags_filter([], $separator)); | ||
728 | static::assertSame([], tags_filter(null, $separator)); | ||
729 | } | ||
730 | |||
731 | /** | ||
585 | * Util function to build an hashtag link. | 732 | * Util function to build an hashtag link. |
586 | * | 733 | * |
587 | * @param string $hashtag Hashtag name. | 734 | * @param string $hashtag Hashtag name. |
diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php index 5d52daef..04d4ef01 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; |
@@ -41,11 +43,15 @@ class ContainerBuilderTest extends TestCase | |||
41 | /** @var CookieManager */ | 43 | /** @var CookieManager */ |
42 | protected $cookieManager; | 44 | protected $cookieManager; |
43 | 45 | ||
46 | /** @var PluginManager */ | ||
47 | protected $pluginManager; | ||
48 | |||
44 | public function setUp(): void | 49 | public function setUp(): void |
45 | { | 50 | { |
46 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 51 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
47 | $this->sessionManager = $this->createMock(SessionManager::class); | 52 | $this->sessionManager = $this->createMock(SessionManager::class); |
48 | $this->cookieManager = $this->createMock(CookieManager::class); | 53 | $this->cookieManager = $this->createMock(CookieManager::class); |
54 | $this->pluginManager = $this->createMock(PluginManager::class); | ||
49 | 55 | ||
50 | $this->loginManager = $this->createMock(LoginManager::class); | 56 | $this->loginManager = $this->createMock(LoginManager::class); |
51 | $this->loginManager->method('isLoggedIn')->willReturn(true); | 57 | $this->loginManager->method('isLoggedIn')->willReturn(true); |
@@ -54,7 +60,9 @@ class ContainerBuilderTest extends TestCase | |||
54 | $this->conf, | 60 | $this->conf, |
55 | $this->sessionManager, | 61 | $this->sessionManager, |
56 | $this->cookieManager, | 62 | $this->cookieManager, |
57 | $this->loginManager | 63 | $this->loginManager, |
64 | $this->pluginManager, | ||
65 | $this->createMock(LoggerInterface::class) | ||
58 | ); | 66 | ); |
59 | } | 67 | } |
60 | 68 | ||
@@ -72,6 +80,8 @@ class ContainerBuilderTest extends TestCase | |||
72 | static::assertInstanceOf(History::class, $container->history); | 80 | static::assertInstanceOf(History::class, $container->history); |
73 | static::assertInstanceOf(HttpAccess::class, $container->httpAccess); | 81 | static::assertInstanceOf(HttpAccess::class, $container->httpAccess); |
74 | static::assertInstanceOf(LoginManager::class, $container->loginManager); | 82 | static::assertInstanceOf(LoginManager::class, $container->loginManager); |
83 | static::assertInstanceOf(LoggerInterface::class, $container->logger); | ||
84 | static::assertInstanceOf(MetadataRetriever::class, $container->metadataRetriever); | ||
75 | static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils); | 85 | static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils); |
76 | static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); | 86 | static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); |
77 | static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager); | 87 | static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager); |
diff --git a/tests/feed/CachedPageTest.php b/tests/feed/CachedPageTest.php index 904db9dc..1decfaf3 100644 --- a/tests/feed/CachedPageTest.php +++ b/tests/feed/CachedPageTest.php | |||
@@ -40,10 +40,10 @@ class CachedPageTest extends \Shaarli\TestCase | |||
40 | */ | 40 | */ |
41 | public function testConstruct() | 41 | public function testConstruct() |
42 | { | 42 | { |
43 | new CachedPage(self::$testCacheDir, '', true); | 43 | new CachedPage(self::$testCacheDir, '', true, null); |
44 | new CachedPage(self::$testCacheDir, '', false); | 44 | new CachedPage(self::$testCacheDir, '', false, null); |
45 | new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true); | 45 | new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true, null); |
46 | new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false); | 46 | new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false, null); |
47 | $this->addToAssertionCount(1); | 47 | $this->addToAssertionCount(1); |
48 | } | 48 | } |
49 | 49 | ||
@@ -52,7 +52,7 @@ class CachedPageTest extends \Shaarli\TestCase | |||
52 | */ | 52 | */ |
53 | public function testCache() | 53 | public function testCache() |
54 | { | 54 | { |
55 | $page = new CachedPage(self::$testCacheDir, self::$url, true); | 55 | $page = new CachedPage(self::$testCacheDir, self::$url, true, null); |
56 | 56 | ||
57 | $this->assertFileNotExists(self::$filename); | 57 | $this->assertFileNotExists(self::$filename); |
58 | $page->cache('<p>Some content</p>'); | 58 | $page->cache('<p>Some content</p>'); |
@@ -68,7 +68,7 @@ class CachedPageTest extends \Shaarli\TestCase | |||
68 | */ | 68 | */ |
69 | public function testShouldNotCache() | 69 | public function testShouldNotCache() |
70 | { | 70 | { |
71 | $page = new CachedPage(self::$testCacheDir, self::$url, false); | 71 | $page = new CachedPage(self::$testCacheDir, self::$url, false, null); |
72 | 72 | ||
73 | $this->assertFileNotExists(self::$filename); | 73 | $this->assertFileNotExists(self::$filename); |
74 | $page->cache('<p>Some content</p>'); | 74 | $page->cache('<p>Some content</p>'); |
@@ -80,7 +80,7 @@ class CachedPageTest extends \Shaarli\TestCase | |||
80 | */ | 80 | */ |
81 | public function testCachedVersion() | 81 | public function testCachedVersion() |
82 | { | 82 | { |
83 | $page = new CachedPage(self::$testCacheDir, self::$url, true); | 83 | $page = new CachedPage(self::$testCacheDir, self::$url, true, null); |
84 | 84 | ||
85 | $this->assertFileNotExists(self::$filename); | 85 | $this->assertFileNotExists(self::$filename); |
86 | $page->cache('<p>Some content</p>'); | 86 | $page->cache('<p>Some content</p>'); |
@@ -96,7 +96,7 @@ class CachedPageTest extends \Shaarli\TestCase | |||
96 | */ | 96 | */ |
97 | public function testCachedVersionNoFile() | 97 | public function testCachedVersionNoFile() |
98 | { | 98 | { |
99 | $page = new CachedPage(self::$testCacheDir, self::$url, true); | 99 | $page = new CachedPage(self::$testCacheDir, self::$url, true, null); |
100 | 100 | ||
101 | $this->assertFileNotExists(self::$filename); | 101 | $this->assertFileNotExists(self::$filename); |
102 | $this->assertEquals( | 102 | $this->assertEquals( |
@@ -110,7 +110,7 @@ class CachedPageTest extends \Shaarli\TestCase | |||
110 | */ | 110 | */ |
111 | public function testNoCachedVersion() | 111 | public function testNoCachedVersion() |
112 | { | 112 | { |
113 | $page = new CachedPage(self::$testCacheDir, self::$url, false); | 113 | $page = new CachedPage(self::$testCacheDir, self::$url, false, null); |
114 | 114 | ||
115 | $this->assertFileNotExists(self::$filename); | 115 | $this->assertFileNotExists(self::$filename); |
116 | $this->assertEquals( | 116 | $this->assertEquals( |
@@ -118,4 +118,43 @@ class CachedPageTest extends \Shaarli\TestCase | |||
118 | $page->cachedVersion() | 118 | $page->cachedVersion() |
119 | ); | 119 | ); |
120 | } | 120 | } |
121 | |||
122 | /** | ||
123 | * Return a page's cached content within date period | ||
124 | */ | ||
125 | public function testCachedVersionInDatePeriod() | ||
126 | { | ||
127 | $period = new \DatePeriod( | ||
128 | new \DateTime('yesterday'), | ||
129 | new \DateInterval('P1D'), | ||
130 | new \DateTime('tomorrow') | ||
131 | ); | ||
132 | $page = new CachedPage(self::$testCacheDir, self::$url, true, $period); | ||
133 | |||
134 | $this->assertFileNotExists(self::$filename); | ||
135 | $page->cache('<p>Some content</p>'); | ||
136 | $this->assertFileExists(self::$filename); | ||
137 | $this->assertEquals( | ||
138 | '<p>Some content</p>', | ||
139 | $page->cachedVersion() | ||
140 | ); | ||
141 | } | ||
142 | |||
143 | /** | ||
144 | * Return a page's cached content outside of date period | ||
145 | */ | ||
146 | public function testCachedVersionNotInDatePeriod() | ||
147 | { | ||
148 | $period = new \DatePeriod( | ||
149 | new \DateTime('yesterday noon'), | ||
150 | new \DateInterval('P1D'), | ||
151 | new \DateTime('yesterday midnight') | ||
152 | ); | ||
153 | $page = new CachedPage(self::$testCacheDir, self::$url, true, $period); | ||
154 | |||
155 | $this->assertFileNotExists(self::$filename); | ||
156 | $page->cache('<p>Some content</p>'); | ||
157 | $this->assertFileExists(self::$filename); | ||
158 | $this->assertNull($page->cachedVersion()); | ||
159 | } | ||
121 | } | 160 | } |
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php index 3fc6f8dc..4fcc5dd1 100644 --- a/tests/formatter/BookmarkDefaultFormatterTest.php +++ b/tests/formatter/BookmarkDefaultFormatterTest.php | |||
@@ -289,4 +289,24 @@ class BookmarkDefaultFormatterTest extends TestCase | |||
289 | $link['taglist_html'] | 289 | $link['taglist_html'] |
290 | ); | 290 | ); |
291 | } | 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 | } | ||
292 | } | 312 | } |
diff --git a/tests/front/controller/admin/ConfigureControllerTest.php b/tests/front/controller/admin/ConfigureControllerTest.php index d82db0a7..13644df9 100644 --- a/tests/front/controller/admin/ConfigureControllerTest.php +++ b/tests/front/controller/admin/ConfigureControllerTest.php | |||
@@ -62,7 +62,7 @@ class ConfigureControllerTest extends TestCase | |||
62 | static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']); | 62 | static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']); |
63 | static::assertSame('api.enabled', $assignedVariables['api_enabled']); | 63 | static::assertSame('api.enabled', $assignedVariables['api_enabled']); |
64 | static::assertSame('api.secret', $assignedVariables['api_secret']); | 64 | static::assertSame('api.secret', $assignedVariables['api_secret']); |
65 | static::assertCount(5, $assignedVariables['languages']); | 65 | static::assertCount(6, $assignedVariables['languages']); |
66 | static::assertArrayHasKey('gd_enabled', $assignedVariables); | 66 | static::assertArrayHasKey('gd_enabled', $assignedVariables); |
67 | static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']); | 67 | static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']); |
68 | } | 68 | } |
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 83bbee7c..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 | /** |
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 37542c26..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 | /** |
@@ -209,7 +209,7 @@ class SaveBookmarkTest extends TestCase | |||
209 | /** | 209 | /** |
210 | * Test save a bookmark - try to retrieve the thumbnail | 210 | * Test save a bookmark - try to retrieve the thumbnail |
211 | */ | 211 | */ |
212 | public function testSaveBookmarkWithThumbnail(): void | 212 | public function testSaveBookmarkWithThumbnailSync(): void |
213 | { | 213 | { |
214 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; | 214 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; |
215 | 215 | ||
@@ -224,7 +224,13 @@ class SaveBookmarkTest extends TestCase | |||
224 | 224 | ||
225 | $this->container->conf = $this->createMock(ConfigManager::class); | 225 | $this->container->conf = $this->createMock(ConfigManager::class); |
226 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | 226 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { |
227 | 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; | ||
228 | }); | 234 | }); |
229 | 235 | ||
230 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); | 236 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); |
@@ -275,6 +281,51 @@ class SaveBookmarkTest extends TestCase | |||
275 | } | 281 | } |
276 | 282 | ||
277 | /** | 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 | /** | ||
278 | * Change the password with a wrong existing password | 329 | * Change the password with a wrong existing password |
279 | */ | 330 | */ |
280 | public function testSaveBookmarkFromBookmarklet(): void | 331 | public function testSaveBookmarkFromBookmarklet(): void |
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..2d745800 --- /dev/null +++ b/tests/helper/DailyPageHelperTest.php | |||
@@ -0,0 +1,341 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Helper; | ||
6 | |||
7 | use DateTimeImmutable; | ||
8 | use DateTimeInterface; | ||
9 | use Shaarli\Bookmark\Bookmark; | ||
10 | use Shaarli\TestCase; | ||
11 | use Slim\Http\Request; | ||
12 | |||
13 | class DailyPageHelperTest extends TestCase | ||
14 | { | ||
15 | /** | ||
16 | * @dataProvider getRequestedTypes | ||
17 | */ | ||
18 | public function testExtractRequestedType(array $queryParams, string $expectedType): void | ||
19 | { | ||
20 | $request = $this->createMock(Request::class); | ||
21 | $request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string { | ||
22 | return $queryParams[$key] ?? null; | ||
23 | }); | ||
24 | |||
25 | $type = DailyPageHelper::extractRequestedType($request); | ||
26 | |||
27 | static::assertSame($type, $expectedType); | ||
28 | } | ||
29 | |||
30 | /** | ||
31 | * @dataProvider getRequestedDateTimes | ||
32 | */ | ||
33 | public function testExtractRequestedDateTime( | ||
34 | string $type, | ||
35 | string $input, | ||
36 | ?Bookmark $bookmark, | ||
37 | DateTimeInterface $expectedDateTime, | ||
38 | string $compareFormat = 'Ymd' | ||
39 | ): void { | ||
40 | $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark); | ||
41 | |||
42 | static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat)); | ||
43 | } | ||
44 | |||
45 | public function testExtractRequestedDateTimeExceptionUnknownType(): void | ||
46 | { | ||
47 | $this->expectException(\Exception::class); | ||
48 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
49 | |||
50 | DailyPageHelper::extractRequestedDateTime('nope', null, null); | ||
51 | } | ||
52 | |||
53 | /** | ||
54 | * @dataProvider getFormatsByType | ||
55 | */ | ||
56 | public function testGetFormatByType(string $type, string $expectedFormat): void | ||
57 | { | ||
58 | $format = DailyPageHelper::getFormatByType($type); | ||
59 | |||
60 | static::assertSame($expectedFormat, $format); | ||
61 | } | ||
62 | |||
63 | public function testGetFormatByTypeExceptionUnknownType(): void | ||
64 | { | ||
65 | $this->expectException(\Exception::class); | ||
66 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
67 | |||
68 | DailyPageHelper::getFormatByType('nope'); | ||
69 | } | ||
70 | |||
71 | /** | ||
72 | * @dataProvider getStartDatesByType | ||
73 | */ | ||
74 | public function testGetStartDatesByType( | ||
75 | string $type, | ||
76 | DateTimeImmutable $dateTime, | ||
77 | DateTimeInterface $expectedDateTime | ||
78 | ): void { | ||
79 | $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime); | ||
80 | |||
81 | static::assertEquals($expectedDateTime, $startDateTime); | ||
82 | } | ||
83 | |||
84 | public function testGetStartDatesByTypeExceptionUnknownType(): void | ||
85 | { | ||
86 | $this->expectException(\Exception::class); | ||
87 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
88 | |||
89 | DailyPageHelper::getStartDateTimeByType('nope', new DateTimeImmutable()); | ||
90 | } | ||
91 | |||
92 | /** | ||
93 | * @dataProvider getEndDatesByType | ||
94 | */ | ||
95 | public function testGetEndDatesByType( | ||
96 | string $type, | ||
97 | DateTimeImmutable $dateTime, | ||
98 | DateTimeInterface $expectedDateTime | ||
99 | ): void { | ||
100 | $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime); | ||
101 | |||
102 | static::assertEquals($expectedDateTime, $endDateTime); | ||
103 | } | ||
104 | |||
105 | public function testGetEndDatesByTypeExceptionUnknownType(): void | ||
106 | { | ||
107 | $this->expectException(\Exception::class); | ||
108 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
109 | |||
110 | DailyPageHelper::getEndDateTimeByType('nope', new DateTimeImmutable()); | ||
111 | } | ||
112 | |||
113 | /** | ||
114 | * @dataProvider getDescriptionsByType | ||
115 | */ | ||
116 | public function testGeDescriptionsByType( | ||
117 | string $type, | ||
118 | DateTimeImmutable $dateTime, | ||
119 | string $expectedDescription | ||
120 | ): void { | ||
121 | $description = DailyPageHelper::getDescriptionByType($type, $dateTime); | ||
122 | |||
123 | static::assertEquals($expectedDescription, $description); | ||
124 | } | ||
125 | |||
126 | /** | ||
127 | * @dataProvider getDescriptionsByTypeNotIncludeRelative | ||
128 | */ | ||
129 | public function testGeDescriptionsByTypeNotIncludeRelative( | ||
130 | string $type, | ||
131 | \DateTimeImmutable $dateTime, | ||
132 | string $expectedDescription | ||
133 | ): void { | ||
134 | $description = DailyPageHelper::getDescriptionByType($type, $dateTime, false); | ||
135 | |||
136 | static::assertEquals($expectedDescription, $description); | ||
137 | } | ||
138 | |||
139 | public function getDescriptionByTypeExceptionUnknownType(): void | ||
140 | { | ||
141 | $this->expectException(\Exception::class); | ||
142 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
143 | |||
144 | DailyPageHelper::getDescriptionByType('nope', new DateTimeImmutable()); | ||
145 | } | ||
146 | |||
147 | /** | ||
148 | * @dataProvider getRssLengthsByType | ||
149 | */ | ||
150 | public function testGeRssLengthsByType(string $type): void { | ||
151 | $length = DailyPageHelper::getRssLengthByType($type); | ||
152 | |||
153 | static::assertIsInt($length); | ||
154 | } | ||
155 | |||
156 | public function testGeRssLengthsByTypeExceptionUnknownType(): void | ||
157 | { | ||
158 | $this->expectException(\Exception::class); | ||
159 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
160 | |||
161 | DailyPageHelper::getRssLengthByType('nope'); | ||
162 | } | ||
163 | |||
164 | /** | ||
165 | * @dataProvider getCacheDatePeriodByType | ||
166 | */ | ||
167 | public function testGetCacheDatePeriodByType( | ||
168 | string $type, | ||
169 | DateTimeImmutable $requested, | ||
170 | DateTimeInterface $start, | ||
171 | DateTimeInterface $end | ||
172 | ): void { | ||
173 | $period = DailyPageHelper::getCacheDatePeriodByType($type, $requested); | ||
174 | |||
175 | static::assertEquals($start, $period->getStartDate()); | ||
176 | static::assertEquals($end, $period->getEndDate()); | ||
177 | } | ||
178 | |||
179 | public function testGetCacheDatePeriodByTypeExceptionUnknownType(): void | ||
180 | { | ||
181 | $this->expectException(\Exception::class); | ||
182 | $this->expectExceptionMessage('Unsupported daily format type'); | ||
183 | |||
184 | DailyPageHelper::getCacheDatePeriodByType('nope'); | ||
185 | } | ||
186 | |||
187 | /** | ||
188 | * Data provider for testExtractRequestedType() test method. | ||
189 | */ | ||
190 | public function getRequestedTypes(): array | ||
191 | { | ||
192 | return [ | ||
193 | [['month' => null], DailyPageHelper::DAY], | ||
194 | [['month' => ''], DailyPageHelper::MONTH], | ||
195 | [['month' => 'content'], DailyPageHelper::MONTH], | ||
196 | [['week' => null], DailyPageHelper::DAY], | ||
197 | [['week' => ''], DailyPageHelper::WEEK], | ||
198 | [['week' => 'content'], DailyPageHelper::WEEK], | ||
199 | [['day' => null], DailyPageHelper::DAY], | ||
200 | [['day' => ''], DailyPageHelper::DAY], | ||
201 | [['day' => 'content'], DailyPageHelper::DAY], | ||
202 | ]; | ||
203 | } | ||
204 | |||
205 | /** | ||
206 | * Data provider for testExtractRequestedDateTime() test method. | ||
207 | */ | ||
208 | public function getRequestedDateTimes(): array | ||
209 | { | ||
210 | return [ | ||
211 | [DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')], | ||
212 | [ | ||
213 | DailyPageHelper::DAY, | ||
214 | '', | ||
215 | (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), | ||
216 | $date, | ||
217 | ], | ||
218 | [DailyPageHelper::DAY, '', null, new \DateTime()], | ||
219 | [DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')], | ||
220 | [ | ||
221 | DailyPageHelper::WEEK, | ||
222 | '', | ||
223 | (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), | ||
224 | new \DateTime('2020-10-13'), | ||
225 | ], | ||
226 | [DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'], | ||
227 | [DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'], | ||
228 | [ | ||
229 | DailyPageHelper::MONTH, | ||
230 | '', | ||
231 | (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), | ||
232 | new \DateTime('2020-10-13'), | ||
233 | 'Ym' | ||
234 | ], | ||
235 | [DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'], | ||
236 | ]; | ||
237 | } | ||
238 | |||
239 | /** | ||
240 | * Data provider for testGetFormatByType() test method. | ||
241 | */ | ||
242 | public function getFormatsByType(): array | ||
243 | { | ||
244 | return [ | ||
245 | [DailyPageHelper::DAY, 'Ymd'], | ||
246 | [DailyPageHelper::WEEK, 'YW'], | ||
247 | [DailyPageHelper::MONTH, 'Ym'], | ||
248 | ]; | ||
249 | } | ||
250 | |||
251 | /** | ||
252 | * Data provider for testGetStartDatesByType() test method. | ||
253 | */ | ||
254 | public function getStartDatesByType(): array | ||
255 | { | ||
256 | return [ | ||
257 | [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')], | ||
258 | [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')], | ||
259 | [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')], | ||
260 | ]; | ||
261 | } | ||
262 | |||
263 | /** | ||
264 | * Data provider for testGetEndDatesByType() test method. | ||
265 | */ | ||
266 | public function getEndDatesByType(): array | ||
267 | { | ||
268 | return [ | ||
269 | [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')], | ||
270 | [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')], | ||
271 | [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')], | ||
272 | ]; | ||
273 | } | ||
274 | |||
275 | /** | ||
276 | * Data provider for testGetDescriptionsByType() test method. | ||
277 | */ | ||
278 | public function getDescriptionsByType(): array | ||
279 | { | ||
280 | return [ | ||
281 | [DailyPageHelper::DAY, $date = new DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')], | ||
282 | [DailyPageHelper::DAY, $date = new DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')], | ||
283 | [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], | ||
284 | [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], | ||
285 | [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], | ||
286 | ]; | ||
287 | } | ||
288 | |||
289 | /** | ||
290 | * Data provider for testGeDescriptionsByTypeNotIncludeRelative() test method. | ||
291 | */ | ||
292 | public function getDescriptionsByTypeNotIncludeRelative(): array | ||
293 | { | ||
294 | return [ | ||
295 | [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), $date->format('F j, Y')], | ||
296 | [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), $date->format('F j, Y')], | ||
297 | [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], | ||
298 | [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], | ||
299 | [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], | ||
300 | ]; | ||
301 | } | ||
302 | |||
303 | /** | ||
304 | * Data provider for testGetRssLengthsByType() test method. | ||
305 | */ | ||
306 | public function getRssLengthsByType(): array | ||
307 | { | ||
308 | return [ | ||
309 | [DailyPageHelper::DAY], | ||
310 | [DailyPageHelper::WEEK], | ||
311 | [DailyPageHelper::MONTH], | ||
312 | ]; | ||
313 | } | ||
314 | |||
315 | /** | ||
316 | * Data provider for testGetCacheDatePeriodByType() test method. | ||
317 | */ | ||
318 | public function getCacheDatePeriodByType(): array | ||
319 | { | ||
320 | return [ | ||
321 | [ | ||
322 | DailyPageHelper::DAY, | ||
323 | new DateTimeImmutable('2020-10-09 04:05:06'), | ||
324 | new \DateTime('2020-10-09 00:00:00'), | ||
325 | new \DateTime('2020-10-09 23:59:59'), | ||
326 | ], | ||
327 | [ | ||
328 | DailyPageHelper::WEEK, | ||
329 | new DateTimeImmutable('2020-10-09 04:05:06'), | ||
330 | new \DateTime('2020-10-05 00:00:00'), | ||
331 | new \DateTime('2020-10-11 23:59:59'), | ||
332 | ], | ||
333 | [ | ||
334 | DailyPageHelper::MONTH, | ||
335 | new DateTimeImmutable('2020-10-09 04:05:06'), | ||
336 | new \DateTime('2020-10-01 00:00:00'), | ||
337 | new \DateTime('2020-10-31 23:59:59'), | ||
338 | ], | ||
339 | ]; | ||
340 | } | ||
341 | } | ||
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..cae65091 --- /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' => trim($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/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/BookmarkImportTest.php b/tests/netscape/BookmarkImportTest.php index c526d5c8..6856ebca 100644 --- a/tests/netscape/BookmarkImportTest.php +++ b/tests/netscape/BookmarkImportTest.php | |||
@@ -531,7 +531,7 @@ class BookmarkImportTest extends TestCase | |||
531 | { | 531 | { |
532 | $post = array( | 532 | $post = array( |
533 | 'privacy' => 'public', | 533 | 'privacy' => 'public', |
534 | 'default_tags' => 'tag1,tag2 tag3' | 534 | 'default_tags' => 'tag1 tag2 tag3' |
535 | ); | 535 | ); |
536 | $files = file2array('netscape_basic.htm'); | 536 | $files = file2array('netscape_basic.htm'); |
537 | $this->assertStringMatchesFormat( | 537 | $this->assertStringMatchesFormat( |
@@ -552,7 +552,7 @@ class BookmarkImportTest extends TestCase | |||
552 | { | 552 | { |
553 | $post = array( | 553 | $post = array( |
554 | 'privacy' => 'public', | 554 | 'privacy' => 'public', |
555 | 'default_tags' => 'tag1&,tag2 "tag3"' | 555 | 'default_tags' => 'tag1& tag2 "tag3"' |
556 | ); | 556 | ); |
557 | $files = file2array('netscape_basic.htm'); | 557 | $files = file2array('netscape_basic.htm'); |
558 | $this->assertStringMatchesFormat( | 558 | $this->assertStringMatchesFormat( |
@@ -573,6 +573,43 @@ class BookmarkImportTest extends TestCase | |||
573 | } | 573 | } |
574 | 574 | ||
575 | /** | 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 | /** | ||
576 | * Ensure each imported bookmark has a unique id | 613 | * Ensure each imported bookmark has a unique id |
577 | * | 614 | * |
578 | * See https://github.com/shaarli/Shaarli/issues/351 | 615 | * See https://github.com/shaarli/Shaarli/issues/351 |
diff --git a/tests/plugins/PluginDefaultColorsTest.php b/tests/plugins/PluginDefaultColorsTest.php index cc844c60..54e97612 100644 --- a/tests/plugins/PluginDefaultColorsTest.php +++ b/tests/plugins/PluginDefaultColorsTest.php | |||
@@ -193,4 +193,27 @@ class PluginDefaultColorsTest extends TestCase | |||
193 | $result = default_colors_format_css_rule($data, ''); | 193 | $result = default_colors_format_css_rule($data, ''); |
194 | $this->assertEmpty($result); | 194 | $this->assertEmpty($result); |
195 | } | 195 | } |
196 | |||
197 | /** | ||
198 | * Make sure that a new CSS file is generated when save_plugin_parameters hook is triggered. | ||
199 | */ | ||
200 | public function testHookSavePluginParameters(): void | ||
201 | { | ||
202 | $params = [ | ||
203 | 'other1' => true, | ||
204 | 'DEFAULT_COLORS_BACKGROUND' => 'pink', | ||
205 | 'other2' => ['yep'], | ||
206 | 'DEFAULT_COLORS_DARK_MAIN' => '', | ||
207 | ]; | ||
208 | |||
209 | hook_default_colors_save_plugin_parameters($params); | ||
210 | $this->assertFileExists($file = 'sandbox/default_colors/default_colors.css'); | ||
211 | $content = file_get_contents($file); | ||
212 | $expected = ':root { | ||
213 | --background-color: pink; | ||
214 | |||
215 | } | ||
216 | '; | ||
217 | $this->assertEquals($expected, $content); | ||
218 | } | ||
196 | } | 219 | } |
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/plugins/test/test.php b/tests/plugins/test/test.php index 03be4f4e..34cd339e 100644 --- a/tests/plugins/test/test.php +++ b/tests/plugins/test/test.php | |||
@@ -27,3 +27,19 @@ function hook_test_error() | |||
27 | { | 27 | { |
28 | new Unknown(); | 28 | new Unknown(); |
29 | } | 29 | } |
30 | |||
31 | function test_register_routes(): array | ||
32 | { | ||
33 | return [ | ||
34 | [ | ||
35 | 'method' => 'GET', | ||
36 | 'route' => '/test', | ||
37 | 'callable' => 'getFunction', | ||
38 | ], | ||
39 | [ | ||
40 | 'method' => 'POST', | ||
41 | 'route' => '/custom', | ||
42 | 'callable' => 'postFunction', | ||
43 | ], | ||
44 | ]; | ||
45 | } | ||
diff --git a/tests/plugins/test_route_invalid/test_route_invalid.php b/tests/plugins/test_route_invalid/test_route_invalid.php new file mode 100644 index 00000000..0c5a5101 --- /dev/null +++ b/tests/plugins/test_route_invalid/test_route_invalid.php | |||
@@ -0,0 +1,12 @@ | |||
1 | <?php | ||
2 | |||
3 | function test_route_invalid_register_routes(): array | ||
4 | { | ||
5 | return [ | ||
6 | [ | ||
7 | 'method' => 'GET', | ||
8 | 'route' => 'not a route', | ||
9 | 'callable' => 'getFunction', | ||
10 | ], | ||
11 | ]; | ||
12 | } | ||
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 47332544..cadd8265 100644 --- a/tests/updater/UpdaterTest.php +++ b/tests/updater/UpdaterTest.php | |||
@@ -60,10 +60,10 @@ class UpdaterTest extends TestCase | |||
60 | */ | 60 | */ |
61 | public function testReadEmptyUpdatesFile() | 61 | public function testReadEmptyUpdatesFile() |
62 | { | 62 | { |
63 | $this->assertEquals(array(), UpdaterUtils::read_updates_file('')); | 63 | $this->assertEquals(array(), UpdaterUtils::readUpdatesFile('')); |
64 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; | 64 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; |
65 | touch($updatesFile); | 65 | touch($updatesFile); |
66 | $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile)); | 66 | $this->assertEquals(array(), UpdaterUtils::readUpdatesFile($updatesFile)); |
67 | unlink($updatesFile); | 67 | unlink($updatesFile); |
68 | } | 68 | } |
69 | 69 | ||
@@ -75,14 +75,14 @@ class UpdaterTest extends TestCase | |||
75 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; | 75 | $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; |
76 | $updatesMethods = array('m1', 'm2', 'm3'); | 76 | $updatesMethods = array('m1', 'm2', 'm3'); |
77 | 77 | ||
78 | UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); | 78 | UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); |
79 | $readMethods = UpdaterUtils::read_updates_file($updatesFile); | 79 | $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); |
80 | $this->assertEquals($readMethods, $updatesMethods); | 80 | $this->assertEquals($readMethods, $updatesMethods); |
81 | 81 | ||
82 | // Update | 82 | // Update |
83 | $updatesMethods[] = 'm4'; | 83 | $updatesMethods[] = 'm4'; |
84 | UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); | 84 | UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); |
85 | $readMethods = UpdaterUtils::read_updates_file($updatesFile); | 85 | $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); |
86 | $this->assertEquals($readMethods, $updatesMethods); | 86 | $this->assertEquals($readMethods, $updatesMethods); |
87 | unlink($updatesFile); | 87 | unlink($updatesFile); |
88 | } | 88 | } |
@@ -95,7 +95,7 @@ class UpdaterTest extends TestCase | |||
95 | $this->expectException(\Exception::class); | 95 | $this->expectException(\Exception::class); |
96 | $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); | 96 | $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); |
97 | 97 | ||
98 | UpdaterUtils::write_updates_file('', array('test')); | 98 | UpdaterUtils::writeUpdatesFile('', array('test')); |
99 | } | 99 | } |
100 | 100 | ||
101 | /** | 101 | /** |
@@ -110,7 +110,7 @@ class UpdaterTest extends TestCase | |||
110 | touch($updatesFile); | 110 | touch($updatesFile); |
111 | chmod($updatesFile, 0444); | 111 | chmod($updatesFile, 0444); |
112 | try { | 112 | try { |
113 | @UpdaterUtils::write_updates_file($updatesFile, array('test')); | 113 | @UpdaterUtils::writeUpdatesFile($updatesFile, array('test')); |
114 | } catch (Exception $e) { | 114 | } catch (Exception $e) { |
115 | unlink($updatesFile); | 115 | unlink($updatesFile); |
116 | 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/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 89d08e2c..13b7f24a 100644 --- a/tpl/default/changetag.html +++ b/tpl/default/changetag.html | |||
@@ -28,13 +28,37 @@ | |||
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 tag'|t}" name="renametag"> | 30 | <input type="submit" value="{'Rename tag'|t}" name="renametag"> |
31 | <input type="submit" value="{'Delete tag'|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 3749bffb..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"> |
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/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 beab0eac..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,14 +129,19 @@ | |||
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"> |
@@ -158,7 +163,7 @@ | |||
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} |
@@ -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 | · |
@@ -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 c153def0..58ca18c5 100644 --- a/tpl/default/page.footer.html +++ b/tpl/default/page.footer.html | |||
@@ -18,8 +18,6 @@ | |||
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} |
@@ -28,16 +26,20 @@ | |||
28 | <script src="{$root_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 tag?'|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/pluginscontent.html b/tpl/default/pluginscontent.html new file mode 100644 index 00000000..1e4f6b80 --- /dev/null +++ b/tpl/default/pluginscontent.html | |||
@@ -0,0 +1,13 @@ | |||
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 | {$content} | ||
10 | |||
11 | {include="page.footer"} | ||
12 | </body> | ||
13 | </html> | ||
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" |
@@ -3047,9 +3052,9 @@ inherits@2.0.3: | |||
3047 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= | 3052 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= |
3048 | 3053 | ||
3049 | ini@^1.3.4, ini@^1.3.5: | 3054 | ini@^1.3.4, ini@^1.3.5: |
3050 | version "1.3.5" | 3055 | version "1.3.7" |
3051 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" | 3056 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" |
3052 | integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== | 3057 | integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== |
3053 | 3058 | ||
3054 | interpret@^1.4.0: | 3059 | interpret@^1.4.0: |
3055 | version "1.4.0" | 3060 | version "1.4.0" |