aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-11-12 13:02:36 +0100
committerArthurHoaro <arthur@hoa.ro>2020-11-12 13:02:36 +0100
commit1409f1c89a7ca01456ae2dcd6357d296e2b99f5a (patch)
treeffa30a9358e82d27be75d8fc5e57f3c8820dc6d3
parent054e03f37fa29da8066f1a637919f13c7e7dc5d2 (diff)
parenta6935feb22df8d9634189ee87d257da9f03eedbd (diff)
downloadShaarli-0.12.1.tar.gz
Shaarli-0.12.1.tar.zst
Shaarli-0.12.1.zip
Merge branch 'master' into v0.12v0.12.1v0.12
-rw-r--r--.docker/nginx.conf38
-rw-r--r--.dockerignore11
-rw-r--r--.htaccess2
-rw-r--r--.travis.yml5
-rw-r--r--AUTHORS5
-rw-r--r--CHANGELOG.md50
-rw-r--r--Dockerfile1
-rw-r--r--Makefile5
-rw-r--r--README.md6
-rw-r--r--application/History.php12
-rw-r--r--application/Languages.php17
-rw-r--r--application/Thumbnailer.php13
-rw-r--r--application/TimeZone.php7
-rw-r--r--application/Utils.php78
-rw-r--r--application/api/ApiMiddleware.php6
-rw-r--r--application/api/ApiUtils.php21
-rw-r--r--application/api/controllers/ApiController.php3
-rw-r--r--application/api/controllers/HistoryController.php1
-rw-r--r--application/api/controllers/Info.php4
-rw-r--r--application/api/controllers/Links.php31
-rw-r--r--application/api/exceptions/ApiAuthorizationException.php2
-rw-r--r--application/api/exceptions/ApiException.php2
-rw-r--r--application/bookmark/Bookmark.php217
-rw-r--r--application/bookmark/BookmarkArray.php20
-rw-r--r--application/bookmark/BookmarkFileService.php134
-rw-r--r--application/bookmark/BookmarkFilter.php203
-rw-r--r--application/bookmark/BookmarkIO.php43
-rw-r--r--application/bookmark/BookmarkInitializer.php19
-rw-r--r--application/bookmark/BookmarkServiceInterface.php109
-rw-r--r--application/bookmark/LinkUtils.php72
-rw-r--r--application/bookmark/exception/BookmarkNotFoundException.php1
-rw-r--r--application/bookmark/exception/EmptyDataStoreException.php6
-rw-r--r--application/bookmark/exception/InvalidBookmarkException.php14
-rw-r--r--application/bookmark/exception/NotWritableDataStoreException.php4
-rw-r--r--application/config/ConfigIO.php1
-rw-r--r--application/config/ConfigJson.php6
-rw-r--r--application/config/ConfigManager.php21
-rw-r--r--application/config/ConfigPhp.php28
-rw-r--r--application/config/ConfigPlugin.php8
-rw-r--r--application/config/exception/MissingFieldConfigException.php1
-rw-r--r--application/config/exception/UnauthorizedConfigException.php1
-rw-r--r--application/container/ContainerBuilder.php19
-rw-r--r--application/container/ShaarliContainer.php4
-rw-r--r--application/exceptions/IOException.php1
-rw-r--r--application/feed/FeedBuilder.php11
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php140
-rw-r--r--application/formatter/BookmarkFormatter.php82
-rw-r--r--application/formatter/BookmarkMarkdownExtraFormatter.php24
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php20
-rw-r--r--application/formatter/BookmarkRawFormatter.php4
-rw-r--r--application/formatter/FormatterFactory.php2
-rw-r--r--application/front/ShaarliMiddleware.php6
-rw-r--r--application/front/controller/admin/ConfigureController.php14
-rw-r--r--application/front/controller/admin/ExportController.php4
-rw-r--r--application/front/controller/admin/ImportController.php4
-rw-r--r--application/front/controller/admin/ManageShaareController.php371
-rw-r--r--application/front/controller/admin/ManageTagController.php37
-rw-r--r--application/front/controller/admin/MetadataController.php29
-rw-r--r--application/front/controller/admin/PasswordController.php4
-rw-r--r--application/front/controller/admin/PluginsController.php4
-rw-r--r--application/front/controller/admin/ServerController.php96
-rw-r--r--application/front/controller/admin/SessionFilterController.php2
-rw-r--r--application/front/controller/admin/ShaareAddController.php34
-rw-r--r--application/front/controller/admin/ShaareManageController.php202
-rw-r--r--application/front/controller/admin/ShaarePublishController.php274
-rw-r--r--application/front/controller/admin/ThumbnailsController.php4
-rw-r--r--application/front/controller/admin/ToolsController.php2
-rw-r--r--application/front/controller/visitor/BookmarkListController.php53
-rw-r--r--application/front/controller/visitor/DailyController.php105
-rw-r--r--application/front/controller/visitor/ErrorController.php12
-rw-r--r--application/front/controller/visitor/FeedController.php2
-rw-r--r--application/front/controller/visitor/InstallController.php39
-rw-r--r--application/front/controller/visitor/LoginController.php9
-rw-r--r--application/front/controller/visitor/PictureWallController.php2
-rw-r--r--application/front/controller/visitor/ShaarliVisitorController.php6
-rw-r--r--application/front/controller/visitor/TagCloudController.php12
-rw-r--r--application/front/controller/visitor/TagController.php18
-rw-r--r--application/helper/ApplicationUtils.php (renamed from application/ApplicationUtils.php)128
-rw-r--r--application/helper/DailyPageHelper.php208
-rw-r--r--application/helper/FileUtils.php (renamed from application/FileUtils.php)58
-rw-r--r--application/http/HttpAccess.php20
-rw-r--r--application/http/HttpUtils.php198
-rw-r--r--application/http/MetadataRetriever.php69
-rw-r--r--application/http/Url.php10
-rw-r--r--application/http/UrlUtils.php11
-rw-r--r--application/legacy/LegacyController.php2
-rw-r--r--application/legacy/LegacyLinkDB.php22
-rw-r--r--application/legacy/LegacyLinkFilter.php18
-rw-r--r--application/legacy/LegacyUpdater.php16
-rw-r--r--application/netscape/NetscapeBookmarkUtils.php14
-rw-r--r--application/plugin/PluginManager.php14
-rw-r--r--application/plugin/exception/PluginFileNotFoundException.php1
-rw-r--r--application/render/PageBuilder.php38
-rw-r--r--application/render/TemplatePage.php1
-rw-r--r--application/render/ThemeUtils.php4
-rw-r--r--application/security/BanManager.php32
-rw-r--r--application/security/LoginManager.php83
-rw-r--r--application/security/SessionManager.php10
-rw-r--r--application/updater/Updater.php10
-rw-r--r--application/updater/UpdaterUtils.php8
-rw-r--r--assets/common/js/metadata.js107
-rw-r--r--assets/common/js/shaare-batch.js121
-rw-r--r--assets/default/js/base.js73
-rw-r--r--assets/default/scss/shaarli.scss199
-rw-r--r--assets/vintage/css/shaarli.css61
-rw-r--r--assets/vintage/js/base.js45
-rw-r--r--composer.json13
-rw-r--r--composer.lock207
-rw-r--r--doc/md/Docker.md7
-rw-r--r--doc/md/Server-configuration.md63
-rw-r--r--doc/md/Shaarli-configuration.md19
-rw-r--r--doc/md/dev/Development.md12
-rw-r--r--doc/md/dev/Plugin-system.md11
-rw-r--r--docker-compose.yml9
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po753
-rw-r--r--inc/languages/jp/LC_MESSAGES/shaarli.po2038
-rw-r--r--index.php57
-rw-r--r--init.php3
-rw-r--r--package.json1
-rw-r--r--phpcs.xml11
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.php20
-rw-r--r--plugins/archiveorg/archiveorg.php3
-rw-r--r--plugins/default_colors/default_colors.php12
-rw-r--r--plugins/demo_plugin/demo_plugin.php45
-rw-r--r--plugins/isso/isso.php8
-rw-r--r--plugins/piwik/piwik.php3
-rw-r--r--plugins/playvideos/playvideos.php11
-rw-r--r--plugins/pubsubhubbub/pubsubhubbub.php14
-rw-r--r--plugins/qrcode/qrcode.php3
-rw-r--r--plugins/wallabag/WallabagInstance.php9
-rw-r--r--plugins/wallabag/wallabag.php12
-rw-r--r--tests/HistoryTest.php8
-rw-r--r--tests/UtilsTest.php36
-rw-r--r--tests/api/controllers/info/InfoTest.php4
-rw-r--r--tests/api/controllers/links/DeleteLinkTest.php9
-rw-r--r--tests/api/controllers/links/GetLinkIdTest.php4
-rw-r--r--tests/api/controllers/links/GetLinksTest.php6
-rw-r--r--tests/api/controllers/links/PostLinkTest.php20
-rw-r--r--tests/api/controllers/links/PutLinkTest.php4
-rw-r--r--tests/api/controllers/tags/DeleteTagTest.php11
-rw-r--r--tests/api/controllers/tags/GetTagNameTest.php4
-rw-r--r--tests/api/controllers/tags/GetTagsTest.php4
-rw-r--r--tests/api/controllers/tags/PutTagTest.php4
-rw-r--r--tests/bookmark/BookmarkArrayTest.php13
-rw-r--r--tests/bookmark/BookmarkFileServiceTest.php259
-rw-r--r--tests/bookmark/BookmarkFilterTest.php46
-rw-r--r--tests/bookmark/BookmarkInitializerTest.php13
-rw-r--r--tests/bookmark/BookmarkTest.php107
-rw-r--r--tests/bookmark/LinkUtilsTest.php446
-rw-r--r--tests/bootstrap.php4
-rw-r--r--tests/container/ContainerBuilderTest.php7
-rw-r--r--tests/feed/FeedBuilderTest.php4
-rw-r--r--tests/formatter/BookmarkDefaultFormatterTest.php135
-rw-r--r--tests/formatter/BookmarkMarkdownExtraFormatterTest.php162
-rw-r--r--tests/front/controller/admin/ConfigureControllerTest.php2
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php47
-rw-r--r--tests/front/controller/admin/ManageTagControllerTest.php136
-rw-r--r--tests/front/controller/admin/ServerControllerTest.php184
-rw-r--r--tests/front/controller/admin/ShaareAddControllerTest.php97
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php)8
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php)18
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php)8
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php139
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php63
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php)136
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php)10
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php)83
-rw-r--r--tests/front/controller/admin/ThumbnailsControllerTest.php4
-rw-r--r--tests/front/controller/visitor/BookmarkListControllerTest.php94
-rw-r--r--tests/front/controller/visitor/DailyControllerTest.php412
-rw-r--r--tests/front/controller/visitor/ErrorControllerTest.php29
-rw-r--r--tests/front/controller/visitor/FrontControllerMockHelper.php4
-rw-r--r--tests/front/controller/visitor/InstallControllerTest.php9
-rw-r--r--tests/front/controller/visitor/LoginControllerTest.php2
-rw-r--r--tests/front/controller/visitor/TagCloudControllerTest.php12
-rw-r--r--tests/front/controller/visitor/TagControllerTest.php6
-rw-r--r--tests/helper/ApplicationUtilsTest.php (renamed from tests/ApplicationUtilsTest.php)65
-rw-r--r--tests/helper/DailyPageHelperTest.php262
-rw-r--r--tests/helper/FileUtilsTest.php (renamed from tests/FileUtilsTest.php)91
-rw-r--r--tests/http/MetadataRetrieverTest.php154
-rw-r--r--tests/legacy/LegacyLinkDBTest.php12
-rw-r--r--tests/legacy/LegacyUpdaterTest.php16
-rw-r--r--tests/netscape/BookmarkExportTest.php4
-rw-r--r--tests/netscape/BookmarkImportTest.php45
-rw-r--r--tests/plugins/PluginWallabagTest.php35
-rw-r--r--tests/security/BanManagerTest.php5
-rw-r--r--tests/security/LoginManagerTest.php51
-rw-r--r--tests/security/SessionManagerTest.php5
-rw-r--r--tests/updater/UpdaterTest.php20
-rw-r--r--tests/utils/FakeApplicationUtils.php2
-rw-r--r--tests/utils/FakeConfigManager.php10
-rw-r--r--tests/utils/ReferenceHistory.php2
-rw-r--r--tests/utils/ReferenceLinkDB.php2
-rw-r--r--tpl/default/addlink.html56
-rw-r--r--tpl/default/changetag.html28
-rw-r--r--tpl/default/daily.html34
-rw-r--r--tpl/default/dailyrss.html11
-rw-r--r--tpl/default/editlink.batch.html32
-rw-r--r--tpl/default/editlink.html39
-rw-r--r--tpl/default/error.html8
-rw-r--r--tpl/default/includes.html6
-rw-r--r--tpl/default/install.html10
-rw-r--r--tpl/default/linklist.html28
-rw-r--r--tpl/default/page.footer.html12
-rw-r--r--tpl/default/picwall.html2
-rw-r--r--tpl/default/pluginsadmin.html2
-rw-r--r--tpl/default/server.html129
-rw-r--r--tpl/default/server.requirements.html68
-rw-r--r--tpl/default/tag.cloud.html2
-rw-r--r--tpl/default/tools.html14
-rw-r--r--tpl/vintage/daily.html8
-rw-r--r--tpl/vintage/editlink.html32
-rw-r--r--tpl/vintage/includes.html4
-rw-r--r--tpl/vintage/linklist.html9
-rw-r--r--tpl/vintage/page.footer.html6
-rw-r--r--tpl/vintage/page.header.html24
-rw-r--r--webpack.config.js6
-rw-r--r--yarn.lock5
218 files changed, 8665 insertions, 3309 deletions
diff --git a/.docker/nginx.conf b/.docker/nginx.conf
index 07fba33f..30810a87 100644
--- a/.docker/nginx.conf
+++ b/.docker/nginx.conf
@@ -17,27 +17,13 @@ http {
17 index index.html index.php; 17 index index.html index.php;
18 18
19 server { 19 server {
20 listen 80; 20 listen 80;
21 root /var/www/shaarli; 21 root /var/www/shaarli;
22 22
23 access_log /var/log/nginx/shaarli.access.log; 23 access_log /var/log/nginx/shaarli.access.log;
24 error_log /var/log/nginx/shaarli.error.log; 24 error_log /var/log/nginx/shaarli.error.log;
25 25
26 location ~ /\. { 26 location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$ {
27 # deny access to dotfiles
28 access_log off;
29 log_not_found off;
30 deny all;
31 }
32
33 location ~ ~$ {
34 # deny access to temp editor files, e.g. "script.php~"
35 access_log off;
36 log_not_found off;
37 deny all;
38 }
39
40 location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
41 # cache static assets 27 # cache static assets
42 expires max; 28 expires max;
43 add_header Pragma public; 29 add_header Pragma public;
@@ -49,25 +35,25 @@ http {
49 alias /var/www/shaarli/images/favicon.ico; 35 alias /var/www/shaarli/images/favicon.ico;
50 } 36 }
51 37
38 location /doc/html/ {
39 default_type "text/html";
40 try_files $uri $uri/ $uri.html =404;
41 }
42
52 location / { 43 location / {
53 # Slim - rewrite URLs 44 # Slim - rewrite URLs & do NOT serve static files through this location
54 try_files $uri /index.php$is_args$args; 45 try_files _ /index.php$is_args$args;
55 } 46 }
56 47
57 location ~ (index)\.php$ { 48 location ~ index\.php$ {
58 # Slim - split URL path into (script_filename, path_info) 49 # Slim - split URL path into (script_filename, path_info)
59 try_files $uri =404; 50 try_files $uri =404;
60 fastcgi_split_path_info ^(.+\.php)(/.+)$; 51 fastcgi_split_path_info ^(index.php)(/.+)$;
61 52
62 # filter and proxy PHP requests to PHP-FPM 53 # filter and proxy PHP requests to PHP-FPM
63 fastcgi_pass unix:/var/run/php-fpm.sock; 54 fastcgi_pass unix:/var/run/php-fpm.sock;
64 fastcgi_index index.php; 55 fastcgi_index index.php;
65 include fastcgi.conf; 56 include fastcgi.conf;
66 } 57 }
67
68 location ~ \.php$ {
69 # deny access to all other PHP scripts
70 deny all;
71 }
72 } 58 }
73} 59}
diff --git a/.dockerignore b/.dockerignore
index 96fd31c5..19fd87a5 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,8 +2,16 @@
2.dev 2.dev
3.git 3.git
4.github 4.github
5.gitattributes
6.gitignore
7.travis.yml
5tests 8tests
6 9
10# Docker related resources are not needed inside the container
11.dockerignore
12Dockerfile
13Dockerfile.armhf
14
7# Docker Compose resources 15# Docker Compose resources
8docker-compose.yml 16docker-compose.yml
9 17
@@ -13,6 +21,9 @@ data/*
13pagecache/* 21pagecache/*
14tmp/* 22tmp/*
15 23
24# Shaarli's docs are created during the build
25doc/html/
26
16# Eclipse project files 27# Eclipse project files
17.settings 28.settings
18.buildpath 29.buildpath
diff --git a/.htaccess b/.htaccess
index 25fcfb03..9d1522df 100644
--- a/.htaccess
+++ b/.htaccess
@@ -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 /
19RewriteCond %{REQUEST_FILENAME} !-f 19RewriteCond %{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
52before_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
52install: 56install:
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:
60script: 64script:
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
diff --git a/AUTHORS b/AUTHORS
index 0ec52acc..be815364 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -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.
4The format is based on [Keep a Changelog](http://keepachangelog.com/) 4The format is based on [Keep a Changelog](http://keepachangelog.com/)
5and this project adheres to [Semantic Versioning](http://semver.org/). 5and this project adheres to [Semantic Versioning](http://semver.org/).
6 6
7## [v0.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
diff --git a/Dockerfile b/Dockerfile
index e2ff71fd..f6120b71 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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/Makefile b/Makefile
index 0ff6bd3f..181b61c4 100644
--- a/Makefile
+++ b/Makefile
@@ -27,10 +27,6 @@ PHPCS := $(BIN)/phpcs
27code_sniffer: 27code_sniffer:
28 @$(PHPCS) 28 @$(PHPCS)
29 29
30### - errors filtered by coding standard: PEAR, PSR1, PSR2, Zend...
31PHPCS_%:
32 @$(PHPCS) --report-full --report-width=200 --standard=$*
33
34### - errors by Git author 30### - errors by Git author
35code_sniffer_blame: 31code_sniffer_blame:
36 @$(PHPCS) --report-gitblame 32 @$(PHPCS) --report-gitblame
@@ -175,6 +171,7 @@ translate:
175eslint: 171eslint:
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
180sasslint: 177sasslint:
diff --git a/README.md b/README.md
index 4fb0bfe0..46dda8d5 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,13 @@ _Do you want to share the links you discover?_
6_Shaarli is a minimalist link sharing service that you can install on your own server._ 6_Shaarli is a minimalist link sharing service that you can install on your own server._
7_It is designed to be personal (single-user), fast and handy._ 7_It is designed to be personal (single-user), fast and handy._
8 8
9[![](https://img.shields.io/badge/stable-v0.10.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4) 9[![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)
10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) 10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
11&bull; 11&bull;
12[![](https://img.shields.io/badge/latest-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) 12[![](https://img.shields.io/badge/latest-v0.12.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0)
13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) 13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
14&bull; 14&bull;
15[![](https://img.shields.io/badge/master-v0.11.x-blue.svg)](https://github.com/shaarli/Shaarli) 15[![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli)
16[![](https://img.shields.io/travis/shaarli/Shaarli.svg?label=master)](https://travis-ci.org/shaarli/Shaarli) 16[![](https://img.shields.io/travis/shaarli/Shaarli.svg?label=master)](https://travis-ci.org/shaarli/Shaarli)
17 17
18[![Join the chat at https://gitter.im/shaarli/Shaarli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shaarli/Shaarli) 18[![Join the chat at https://gitter.im/shaarli/Shaarli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shaarli/Shaarli)
diff --git a/application/History.php b/application/History.php
index 4fd2f294..d230f39d 100644
--- a/application/History.php
+++ b/application/History.php
@@ -1,9 +1,11 @@
1<?php 1<?php
2
2namespace Shaarli; 3namespace Shaarli;
3 4
4use DateTime; 5use DateTime;
5use Exception; 6use Exception;
6use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Helper\FileUtils;
7 9
8/** 10/**
9 * Class History 11 * Class History
@@ -30,27 +32,27 @@ class History
30 /** 32 /**
31 * @var string Action key: a new link has been created. 33 * @var string Action key: a new link has been created.
32 */ 34 */
33 const CREATED = 'CREATED'; 35 public const CREATED = 'CREATED';
34 36
35 /** 37 /**
36 * @var string Action key: a link has been updated. 38 * @var string Action key: a link has been updated.
37 */ 39 */
38 const UPDATED = 'UPDATED'; 40 public const UPDATED = 'UPDATED';
39 41
40 /** 42 /**
41 * @var string Action key: a link has been deleted. 43 * @var string Action key: a link has been deleted.
42 */ 44 */
43 const DELETED = 'DELETED'; 45 public const DELETED = 'DELETED';
44 46
45 /** 47 /**
46 * @var string Action key: settings have been updated. 48 * @var string Action key: settings have been updated.
47 */ 49 */
48 const SETTINGS = 'SETTINGS'; 50 public const SETTINGS = 'SETTINGS';
49 51
50 /** 52 /**
51 * @var string Action key: a bulk import has been processed. 53 * @var string Action key: a bulk import has been processed.
52 */ 54 */
53 const IMPORT = 'IMPORT'; 55 public const IMPORT = 'IMPORT';
54 56
55 /** 57 /**
56 * @var string History file path. 58 * @var string History file path.
diff --git a/application/Languages.php b/application/Languages.php
index d83e0765..60e91631 100644
--- a/application/Languages.php
+++ b/application/Languages.php
@@ -41,7 +41,7 @@ class Languages
41 /** 41 /**
42 * Core translations domain 42 * Core translations domain
43 */ 43 */
44 const DEFAULT_DOMAIN = 'shaarli'; 44 public const DEFAULT_DOMAIN = 'shaarli';
45 45
46 /** 46 /**
47 * @var TranslatorInterface 47 * @var TranslatorInterface
@@ -76,7 +76,8 @@ class Languages
76 $this->language = $confLanguage; 76 $this->language = $confLanguage;
77 } 77 }
78 78
79 if (! extension_loaded('gettext') 79 if (
80 ! extension_loaded('gettext')
80 || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php']) 81 || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
81 ) { 82 ) {
82 $this->initPhpTranslator(); 83 $this->initPhpTranslator();
@@ -98,7 +99,7 @@ class Languages
98 $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages'); 99 $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
99 100
100 // Default extension translation from the current theme 101 // Default extension translation from the current theme
101 $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language'; 102 $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $this->conf->get('theme') . '/language';
102 if (is_dir($themeTransFolder)) { 103 if (is_dir($themeTransFolder)) {
103 $this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false); 104 $this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false);
104 } 105 }
@@ -121,7 +122,9 @@ class Languages
121 $translations = new Translations(); 122 $translations = new Translations();
122 // Core translations 123 // Core translations
123 try { 124 try {
124 $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po'); 125 $translations = $translations->addFromPoFile(
126 'inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po'
127 );
125 $translations->setDomain('shaarli'); 128 $translations->setDomain('shaarli');
126 $this->translator->loadTranslations($translations); 129 $this->translator->loadTranslations($translations);
127 } catch (\InvalidArgumentException $e) { 130 } catch (\InvalidArgumentException $e) {
@@ -129,11 +132,11 @@ class Languages
129 132
130 // Default extension translation from the current theme 133 // Default extension translation from the current theme
131 $theme = $this->conf->get('theme'); 134 $theme = $this->conf->get('theme');
132 $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language'; 135 $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $theme . '/language';
133 if (is_dir($themeTransFolder)) { 136 if (is_dir($themeTransFolder)) {
134 try { 137 try {
135 $translations = Translations::fromPoFile( 138 $translations = Translations::fromPoFile(
136 $themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po' 139 $themeTransFolder . '/' . $this->language . '/LC_MESSAGES/' . $theme . '.po'
137 ); 140 );
138 $translations->setDomain($theme); 141 $translations->setDomain($theme);
139 $this->translator->loadTranslations($translations); 142 $this->translator->loadTranslations($translations);
@@ -149,7 +152,7 @@ class Languages
149 152
150 try { 153 try {
151 $extension = Translations::fromPoFile( 154 $extension = Translations::fromPoFile(
152 $translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po' 155 $translationPath . $this->language . '/LC_MESSAGES/' . $domain . '.po'
153 ); 156 );
154 $extension->setDomain($domain); 157 $extension->setDomain($domain);
155 $this->translator->loadTranslations($extension); 158 $this->translator->loadTranslations($extension);
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php
index 5aec23c8..c4ff8d7a 100644
--- a/application/Thumbnailer.php
+++ b/application/Thumbnailer.php
@@ -13,7 +13,7 @@ use WebThumbnailer\WebThumbnailer;
13 */ 13 */
14class Thumbnailer 14class 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 = '')
85function isTimeZoneValid($continent, $city) 86function 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 */
15function logm($logFile, $clientIp, $message) 15function 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 */
164function generateLocation($referer, $host, $loopTerms = array()) 167function generateLocation($referer, $host, $loopTerms = [])
165{ 168{
166 $finalReferer = './?'; 169 $finalReferer = './?';
167 170
@@ -194,7 +197,7 @@ function generateLocation($referer, $host, $loopTerms = array())
194function autoLocale($headerLocale) 197function 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 */
337function 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 */
462function t($text, $nText = '', $nb = 1, $domain = 'shaarli') 486function 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 */
498function exception2text(Throwable $e): string
463{ 499{
464 return dn__($domain, $text, $nText, $nb); 500 return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString();
465} 501}
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index f5b53b01..9fb88358 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -1,6 +1,8 @@
1<?php 1<?php
2
2namespace Shaarli\Api; 3namespace Shaarli\Api;
3 4
5use malkusch\lock\mutex\FlockMutex;
4use Shaarli\Api\Exceptions\ApiAuthorizationException; 6use Shaarli\Api\Exceptions\ApiAuthorizationException;
5use Shaarli\Api\Exceptions\ApiException; 7use Shaarli\Api\Exceptions\ApiException;
6use Shaarli\Bookmark\BookmarkFileService; 8use Shaarli\Bookmark\BookmarkFileService;
@@ -107,7 +109,8 @@ class ApiMiddleware
107 */ 109 */
108 protected function checkToken($request) 110 protected function checkToken($request)
109 { 111 {
110 if (!$request->hasHeader('Authorization') 112 if (
113 !$request->hasHeader('Authorization')
111 && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION']) 114 && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])
112 ) { 115 ) {
113 throw new ApiAuthorizationException('JWT token not provided'); 116 throw new ApiAuthorizationException('JWT token not provided');
@@ -143,6 +146,7 @@ class ApiMiddleware
143 $linkDb = new BookmarkFileService( 146 $linkDb = new BookmarkFileService(
144 $conf, 147 $conf,
145 $this->container->get('history'), 148 $this->container->get('history'),
149 new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
146 true 150 true
147 ); 151 );
148 $this->container['db'] = $linkDb; 152 $this->container['db'] = $linkDb;
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index faebb8f5..05a2840a 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Api; 3namespace Shaarli\Api;
3 4
4use Shaarli\Api\Exceptions\ApiAuthorizationException; 5use Shaarli\Api\Exceptions\ApiAuthorizationException;
@@ -27,7 +28,7 @@ class ApiUtils
27 throw new ApiAuthorizationException('Malformed JWT token'); 28 throw new ApiAuthorizationException('Malformed JWT token');
28 } 29 }
29 30
30 $genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] .'.'. $parts[1], $secret, true)); 31 $genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] . '.' . $parts[1], $secret, true));
31 if ($parts[2] != $genSign) { 32 if ($parts[2] != $genSign) {
32 throw new ApiAuthorizationException('Invalid JWT signature'); 33 throw new ApiAuthorizationException('Invalid JWT signature');
33 } 34 }
@@ -42,7 +43,8 @@ class ApiUtils
42 throw new ApiAuthorizationException('Invalid JWT payload'); 43 throw new ApiAuthorizationException('Invalid JWT payload');
43 } 44 }
44 45
45 if (empty($payload->iat) 46 if (
47 empty($payload->iat)
46 || $payload->iat > time() 48 || $payload->iat > time()
47 || time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION 49 || time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION
48 ) { 50 ) {
@@ -89,12 +91,12 @@ class ApiUtils
89 * If no URL is provided, it will generate a local note URL. 91 * If no URL is provided, it will generate a local note URL.
90 * If no title is provided, it will use the URL as title. 92 * If no title is provided, it will use the URL as title.
91 * 93 *
92 * @param array $input Request Link. 94 * @param array|null $input Request Link.
93 * @param bool $defaultPrivate Request Link. 95 * @param bool $defaultPrivate Setting defined if a bookmark is private by default.
94 * 96 *
95 * @return Bookmark instance. 97 * @return Bookmark instance.
96 */ 98 */
97 public static function buildLinkFromRequest($input, $defaultPrivate) 99 public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark
98 { 100 {
99 $bookmark = new Bookmark(); 101 $bookmark = new Bookmark();
100 $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; 102 $url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
@@ -110,6 +112,15 @@ class ApiUtils
110 $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); 112 $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
111 $bookmark->setPrivate($private); 113 $bookmark->setPrivate($private);
112 114
115 $created = \DateTime::createFromFormat(\DateTime::ATOM, $input['created'] ?? '');
116 if ($created instanceof \DateTimeInterface) {
117 $bookmark->setCreated($created);
118 }
119 $updated = \DateTime::createFromFormat(\DateTime::ATOM, $input['updated'] ?? '');
120 if ($updated instanceof \DateTimeInterface) {
121 $bookmark->setUpdated($updated);
122 }
123
113 return $bookmark; 124 return $bookmark;
114 } 125 }
115 126
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php
index c4b3d0c3..88a845eb 100644
--- a/application/api/controllers/ApiController.php
+++ b/application/api/controllers/ApiController.php
@@ -4,6 +4,7 @@ namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\BookmarkServiceInterface; 5use Shaarli\Bookmark\BookmarkServiceInterface;
6use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Shaarli\History;
7use Slim\Container; 8use Slim\Container;
8 9
9/** 10/**
@@ -31,7 +32,7 @@ abstract class ApiController
31 protected $bookmarkService; 32 protected $bookmarkService;
32 33
33 /** 34 /**
34 * @var HistoryController 35 * @var History
35 */ 36 */
36 protected $history; 37 protected $history;
37 38
diff --git a/application/api/controllers/HistoryController.php b/application/api/controllers/HistoryController.php
index 505647a9..d83a3a25 100644
--- a/application/api/controllers/HistoryController.php
+++ b/application/api/controllers/HistoryController.php
@@ -1,6 +1,5 @@
1<?php 1<?php
2 2
3
4namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
5 4
6use Shaarli\Api\Exceptions\ApiBadParametersException; 5use Shaarli\Api\Exceptions\ApiBadParametersException;
diff --git a/application/api/controllers/Info.php b/application/api/controllers/Info.php
index 12f6b2f0..ae7db93e 100644
--- a/application/api/controllers/Info.php
+++ b/application/api/controllers/Info.php
@@ -29,13 +29,13 @@ class Info extends ApiController
29 $info = [ 29 $info = [
30 'global_counter' => $this->bookmarkService->count(), 30 'global_counter' => $this->bookmarkService->count(),
31 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE), 31 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
32 'settings' => array( 32 'settings' => [
33 'title' => $this->conf->get('general.title', 'Shaarli'), 33 'title' => $this->conf->get('general.title', 'Shaarli'),
34 'header_link' => $this->conf->get('general.header_link', '?'), 34 'header_link' => $this->conf->get('general.header_link', '?'),
35 'timezone' => $this->conf->get('general.timezone', 'UTC'), 35 'timezone' => $this->conf->get('general.timezone', 'UTC'),
36 'enabled_plugins' => $this->conf->get('general.enabled_plugins', []), 36 'enabled_plugins' => $this->conf->get('general.enabled_plugins', []),
37 'default_private_links' => $this->conf->get('privacy.default_private_links', false), 37 'default_private_links' => $this->conf->get('privacy.default_private_links', false),
38 ), 38 ],
39 ]; 39 ];
40 40
41 return $response->withJson($info, 200, $this->jsonStyle); 41 return $response->withJson($info, 200, $this->jsonStyle);
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index 29247950..c379b962 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -96,11 +96,12 @@ class Links extends ApiController
96 */ 96 */
97 public function getLink($request, $response, $args) 97 public function getLink($request, $response, $args)
98 { 98 {
99 if (!$this->bookmarkService->exists($args['id'])) { 99 $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
100 if ($id === null || ! $this->bookmarkService->exists($id)) {
100 throw new ApiLinkNotFoundException(); 101 throw new ApiLinkNotFoundException();
101 } 102 }
102 $index = index_url($this->ci['environment']); 103 $index = index_url($this->ci['environment']);
103 $out = ApiUtils::formatLink($this->bookmarkService->get($args['id']), $index); 104 $out = ApiUtils::formatLink($this->bookmarkService->get($id), $index);
104 105
105 return $response->withJson($out, 200, $this->jsonStyle); 106 return $response->withJson($out, 200, $this->jsonStyle);
106 } 107 }
@@ -115,10 +116,11 @@ class Links extends ApiController
115 */ 116 */
116 public function postLink($request, $response) 117 public function postLink($request, $response)
117 { 118 {
118 $data = $request->getParsedBody(); 119 $data = (array) ($request->getParsedBody() ?? []);
119 $bookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); 120 $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links'));
120 // duplicate by URL, return 409 Conflict 121 // duplicate by URL, return 409 Conflict
121 if (! empty($bookmark->getUrl()) 122 if (
123 ! empty($bookmark->getUrl())
122 && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) 124 && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl()))
123 ) { 125 ) {
124 return $response->withJson( 126 return $response->withJson(
@@ -130,7 +132,7 @@ class Links extends ApiController
130 132
131 $this->bookmarkService->add($bookmark); 133 $this->bookmarkService->add($bookmark);
132 $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment'])); 134 $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
133 $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]); 135 $redirect = $this->ci->router->pathFor('getLink', ['id' => $bookmark->getId()]);
134 return $response->withAddedHeader('Location', $redirect) 136 return $response->withAddedHeader('Location', $redirect)
135 ->withJson($out, 201, $this->jsonStyle); 137 ->withJson($out, 201, $this->jsonStyle);
136 } 138 }
@@ -148,18 +150,20 @@ class Links extends ApiController
148 */ 150 */
149 public function putLink($request, $response, $args) 151 public function putLink($request, $response, $args)
150 { 152 {
151 if (! $this->bookmarkService->exists($args['id'])) { 153 $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
154 if ($id === null || !$this->bookmarkService->exists($id)) {
152 throw new ApiLinkNotFoundException(); 155 throw new ApiLinkNotFoundException();
153 } 156 }
154 157
155 $index = index_url($this->ci['environment']); 158 $index = index_url($this->ci['environment']);
156 $data = $request->getParsedBody(); 159 $data = $request->getParsedBody();
157 160
158 $requestBookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); 161 $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links'));
159 // duplicate URL on a different link, return 409 Conflict 162 // duplicate URL on a different link, return 409 Conflict
160 if (! empty($requestBookmark->getUrl()) 163 if (
164 ! empty($requestBookmark->getUrl())
161 && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) 165 && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
162 && $dup->getId() != $args['id'] 166 && $dup->getId() != $id
163 ) { 167 ) {
164 return $response->withJson( 168 return $response->withJson(
165 ApiUtils::formatLink($dup, $index), 169 ApiUtils::formatLink($dup, $index),
@@ -168,7 +172,7 @@ class Links extends ApiController
168 ); 172 );
169 } 173 }
170 174
171 $responseBookmark = $this->bookmarkService->get($args['id']); 175 $responseBookmark = $this->bookmarkService->get($id);
172 $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark); 176 $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark);
173 $this->bookmarkService->set($responseBookmark); 177 $this->bookmarkService->set($responseBookmark);
174 178
@@ -189,10 +193,11 @@ class Links extends ApiController
189 */ 193 */
190 public function deleteLink($request, $response, $args) 194 public function deleteLink($request, $response, $args)
191 { 195 {
192 if (! $this->bookmarkService->exists($args['id'])) { 196 $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
197 if ($id === null || !$this->bookmarkService->exists($id)) {
193 throw new ApiLinkNotFoundException(); 198 throw new ApiLinkNotFoundException();
194 } 199 }
195 $bookmark = $this->bookmarkService->get($args['id']); 200 $bookmark = $this->bookmarkService->get($id);
196 $this->bookmarkService->remove($bookmark); 201 $this->bookmarkService->remove($bookmark);
197 202
198 return $response->withStatus(204); 203 return $response->withStatus(204);
diff --git a/application/api/exceptions/ApiAuthorizationException.php b/application/api/exceptions/ApiAuthorizationException.php
index 0e3f4776..c77e9eea 100644
--- a/application/api/exceptions/ApiAuthorizationException.php
+++ b/application/api/exceptions/ApiAuthorizationException.php
@@ -28,7 +28,7 @@ class ApiAuthorizationException extends ApiException
28 */ 28 */
29 public function setMessage($message) 29 public function setMessage($message)
30 { 30 {
31 $original = $this->debug === true ? ': '. $this->getMessage() : ''; 31 $original = $this->debug === true ? ': ' . $this->getMessage() : '';
32 $this->message = $message . $original; 32 $this->message = $message . $original;
33 } 33 }
34} 34}
diff --git a/application/api/exceptions/ApiException.php b/application/api/exceptions/ApiException.php
index d6b66323..7deafb96 100644
--- a/application/api/exceptions/ApiException.php
+++ b/application/api/exceptions/ApiException.php
@@ -44,7 +44,7 @@ abstract class ApiException extends \Exception
44 } 44 }
45 return [ 45 return [
46 'message' => $this->getMessage(), 46 'message' => $this->getMessage(),
47 'stacktrace' => get_class($this) .': '. $this->getTraceAsString() 47 'stacktrace' => get_class($this) . ': ' . $this->getTraceAsString()
48 ]; 48 ];
49 } 49 }
50 50
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php
index 1beb8be2..4238ef25 100644
--- a/application/bookmark/Bookmark.php
+++ b/application/bookmark/Bookmark.php
@@ -1,5 +1,7 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
5use DateTime; 7use DateTime;
@@ -17,7 +19,7 @@ use Shaarli\Bookmark\Exception\InvalidBookmarkException;
17class Bookmark 19class Bookmark
18{ 20{
19 /** @var string Date format used in string (former ID format) */ 21 /** @var string Date format used in string (former ID format) */
20 const LINK_DATE_FORMAT = 'Ymd_His'; 22 public const LINK_DATE_FORMAT = 'Ymd_His';
21 23
22 /** @var int Bookmark ID */ 24 /** @var int Bookmark ID */
23 protected $id; 25 protected $id;
@@ -52,32 +54,37 @@ class Bookmark
52 /** @var bool True if the bookmark can only be seen while logged in */ 54 /** @var bool True if the bookmark can only be seen while logged in */
53 protected $private; 55 protected $private;
54 56
57 /** @var mixed[] Available to store any additional content for a bookmark. Currently used for search highlight. */
58 protected $additionalContent = [];
59
55 /** 60 /**
56 * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. 61 * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
57 * 62 *
58 * @param array $data 63 * @param array $data
64 * @param string $tagsSeparator Tags separator loaded from the config file.
65 * This is a context data, and it should *never* be stored in the Bookmark object.
59 * 66 *
60 * @return $this 67 * @return $this
61 */ 68 */
62 public function fromArray($data) 69 public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark
63 { 70 {
64 $this->id = $data['id']; 71 $this->id = $data['id'] ?? null;
65 $this->shortUrl = $data['shorturl']; 72 $this->shortUrl = $data['shorturl'] ?? null;
66 $this->url = $data['url']; 73 $this->url = $data['url'] ?? null;
67 $this->title = $data['title']; 74 $this->title = $data['title'] ?? null;
68 $this->description = $data['description']; 75 $this->description = $data['description'] ?? null;
69 $this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null; 76 $this->thumbnail = $data['thumbnail'] ?? null;
70 $this->sticky = isset($data['sticky']) ? $data['sticky'] : false; 77 $this->sticky = $data['sticky'] ?? false;
71 $this->created = $data['created']; 78 $this->created = $data['created'] ?? null;
72 if (is_array($data['tags'])) { 79 if (is_array($data['tags'])) {
73 $this->tags = $data['tags']; 80 $this->tags = $data['tags'];
74 } else { 81 } else {
75 $this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY); 82 $this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator);
76 } 83 }
77 if (! empty($data['updated'])) { 84 if (! empty($data['updated'])) {
78 $this->updated = $data['updated']; 85 $this->updated = $data['updated'];
79 } 86 }
80 $this->private = $data['private'] ? true : false; 87 $this->private = ($data['private'] ?? false) ? true : false;
81 88
82 return $this; 89 return $this;
83 } 90 }
@@ -93,24 +100,29 @@ class Bookmark
93 * - the URL with the permalink 100 * - the URL with the permalink
94 * - the title with the URL 101 * - the title with the URL
95 * 102 *
103 * Also make sure that we do not save search highlights in the datastore.
104 *
96 * @throws InvalidBookmarkException 105 * @throws InvalidBookmarkException
97 */ 106 */
98 public function validate() 107 public function validate(): void
99 { 108 {
100 if ($this->id === null 109 if (
110 $this->id === null
101 || ! is_int($this->id) 111 || ! is_int($this->id)
102 || empty($this->shortUrl) 112 || empty($this->shortUrl)
103 || empty($this->created) 113 || empty($this->created)
104 || ! $this->created instanceof DateTimeInterface
105 ) { 114 ) {
106 throw new InvalidBookmarkException($this); 115 throw new InvalidBookmarkException($this);
107 } 116 }
108 if (empty($this->url)) { 117 if (empty($this->url)) {
109 $this->url = '/shaare/'. $this->shortUrl; 118 $this->url = '/shaare/' . $this->shortUrl;
110 } 119 }
111 if (empty($this->title)) { 120 if (empty($this->title)) {
112 $this->title = $this->url; 121 $this->title = $this->url;
113 } 122 }
123 if (array_key_exists('search_highlight', $this->additionalContent)) {
124 unset($this->additionalContent['search_highlight']);
125 }
114 } 126 }
115 127
116 /** 128 /**
@@ -119,11 +131,11 @@ class Bookmark
119 * - created: with the current datetime 131 * - created: with the current datetime
120 * - shortUrl: with a generated small hash from the date and the given ID 132 * - shortUrl: with a generated small hash from the date and the given ID
121 * 133 *
122 * @param int $id 134 * @param int|null $id
123 * 135 *
124 * @return Bookmark 136 * @return Bookmark
125 */ 137 */
126 public function setId($id) 138 public function setId(?int $id): Bookmark
127 { 139 {
128 $this->id = $id; 140 $this->id = $id;
129 if (empty($this->created)) { 141 if (empty($this->created)) {
@@ -139,9 +151,9 @@ class Bookmark
139 /** 151 /**
140 * Get the Id. 152 * Get the Id.
141 * 153 *
142 * @return int 154 * @return int|null
143 */ 155 */
144 public function getId() 156 public function getId(): ?int
145 { 157 {
146 return $this->id; 158 return $this->id;
147 } 159 }
@@ -149,9 +161,9 @@ class Bookmark
149 /** 161 /**
150 * Get the ShortUrl. 162 * Get the ShortUrl.
151 * 163 *
152 * @return string 164 * @return string|null
153 */ 165 */
154 public function getShortUrl() 166 public function getShortUrl(): ?string
155 { 167 {
156 return $this->shortUrl; 168 return $this->shortUrl;
157 } 169 }
@@ -159,9 +171,9 @@ class Bookmark
159 /** 171 /**
160 * Get the Url. 172 * Get the Url.
161 * 173 *
162 * @return string 174 * @return string|null
163 */ 175 */
164 public function getUrl() 176 public function getUrl(): ?string
165 { 177 {
166 return $this->url; 178 return $this->url;
167 } 179 }
@@ -171,7 +183,7 @@ class Bookmark
171 * 183 *
172 * @return string 184 * @return string
173 */ 185 */
174 public function getTitle() 186 public function getTitle(): ?string
175 { 187 {
176 return $this->title; 188 return $this->title;
177 } 189 }
@@ -181,7 +193,7 @@ class Bookmark
181 * 193 *
182 * @return string 194 * @return string
183 */ 195 */
184 public function getDescription() 196 public function getDescription(): string
185 { 197 {
186 return ! empty($this->description) ? $this->description : ''; 198 return ! empty($this->description) ? $this->description : '';
187 } 199 }
@@ -191,7 +203,7 @@ class Bookmark
191 * 203 *
192 * @return DateTimeInterface 204 * @return DateTimeInterface
193 */ 205 */
194 public function getCreated() 206 public function getCreated(): ?DateTimeInterface
195 { 207 {
196 return $this->created; 208 return $this->created;
197 } 209 }
@@ -201,7 +213,7 @@ class Bookmark
201 * 213 *
202 * @return DateTimeInterface 214 * @return DateTimeInterface
203 */ 215 */
204 public function getUpdated() 216 public function getUpdated(): ?DateTimeInterface
205 { 217 {
206 return $this->updated; 218 return $this->updated;
207 } 219 }
@@ -209,11 +221,11 @@ class Bookmark
209 /** 221 /**
210 * Set the ShortUrl. 222 * Set the ShortUrl.
211 * 223 *
212 * @param string $shortUrl 224 * @param string|null $shortUrl
213 * 225 *
214 * @return Bookmark 226 * @return Bookmark
215 */ 227 */
216 public function setShortUrl($shortUrl) 228 public function setShortUrl(?string $shortUrl): Bookmark
217 { 229 {
218 $this->shortUrl = $shortUrl; 230 $this->shortUrl = $shortUrl;
219 231
@@ -223,14 +235,14 @@ class Bookmark
223 /** 235 /**
224 * Set the Url. 236 * Set the Url.
225 * 237 *
226 * @param string $url 238 * @param string|null $url
227 * @param array $allowedProtocols 239 * @param string[] $allowedProtocols
228 * 240 *
229 * @return Bookmark 241 * @return Bookmark
230 */ 242 */
231 public function setUrl($url, $allowedProtocols = []) 243 public function setUrl(?string $url, array $allowedProtocols = []): Bookmark
232 { 244 {
233 $url = trim($url); 245 $url = $url !== null ? trim($url) : '';
234 if (! empty($url)) { 246 if (! empty($url)) {
235 $url = whitelist_protocols($url, $allowedProtocols); 247 $url = whitelist_protocols($url, $allowedProtocols);
236 } 248 }
@@ -242,13 +254,13 @@ class Bookmark
242 /** 254 /**
243 * Set the Title. 255 * Set the Title.
244 * 256 *
245 * @param string $title 257 * @param string|null $title
246 * 258 *
247 * @return Bookmark 259 * @return Bookmark
248 */ 260 */
249 public function setTitle($title) 261 public function setTitle(?string $title): Bookmark
250 { 262 {
251 $this->title = trim($title); 263 $this->title = $title !== null ? trim($title) : '';
252 264
253 return $this; 265 return $this;
254 } 266 }
@@ -256,11 +268,11 @@ class Bookmark
256 /** 268 /**
257 * Set the Description. 269 * Set the Description.
258 * 270 *
259 * @param string $description 271 * @param string|null $description
260 * 272 *
261 * @return Bookmark 273 * @return Bookmark
262 */ 274 */
263 public function setDescription($description) 275 public function setDescription(?string $description): Bookmark
264 { 276 {
265 $this->description = $description; 277 $this->description = $description;
266 278
@@ -271,11 +283,11 @@ class Bookmark
271 * Set the Created. 283 * Set the Created.
272 * Note: you shouldn't set this manually except for special cases (like bookmark import) 284 * Note: you shouldn't set this manually except for special cases (like bookmark import)
273 * 285 *
274 * @param DateTimeInterface $created 286 * @param DateTimeInterface|null $created
275 * 287 *
276 * @return Bookmark 288 * @return Bookmark
277 */ 289 */
278 public function setCreated($created) 290 public function setCreated(?DateTimeInterface $created): Bookmark
279 { 291 {
280 $this->created = $created; 292 $this->created = $created;
281 293
@@ -285,11 +297,11 @@ class Bookmark
285 /** 297 /**
286 * Set the Updated. 298 * Set the Updated.
287 * 299 *
288 * @param DateTimeInterface $updated 300 * @param DateTimeInterface|null $updated
289 * 301 *
290 * @return Bookmark 302 * @return Bookmark
291 */ 303 */
292 public function setUpdated($updated) 304 public function setUpdated(?DateTimeInterface $updated): Bookmark
293 { 305 {
294 $this->updated = $updated; 306 $this->updated = $updated;
295 307
@@ -301,7 +313,7 @@ class Bookmark
301 * 313 *
302 * @return bool 314 * @return bool
303 */ 315 */
304 public function isPrivate() 316 public function isPrivate(): bool
305 { 317 {
306 return $this->private ? true : false; 318 return $this->private ? true : false;
307 } 319 }
@@ -309,11 +321,11 @@ class Bookmark
309 /** 321 /**
310 * Set the Private. 322 * Set the Private.
311 * 323 *
312 * @param bool $private 324 * @param bool|null $private
313 * 325 *
314 * @return Bookmark 326 * @return Bookmark
315 */ 327 */
316 public function setPrivate($private) 328 public function setPrivate(?bool $private): Bookmark
317 { 329 {
318 $this->private = $private ? true : false; 330 $this->private = $private ? true : false;
319 331
@@ -323,9 +335,9 @@ class Bookmark
323 /** 335 /**
324 * Get the Tags. 336 * Get the Tags.
325 * 337 *
326 * @return array 338 * @return string[]
327 */ 339 */
328 public function getTags() 340 public function getTags(): array
329 { 341 {
330 return is_array($this->tags) ? $this->tags : []; 342 return is_array($this->tags) ? $this->tags : [];
331 } 343 }
@@ -333,13 +345,18 @@ class Bookmark
333 /** 345 /**
334 * Set the Tags. 346 * Set the Tags.
335 * 347 *
336 * @param array $tags 348 * @param string[]|null $tags
337 * 349 *
338 * @return Bookmark 350 * @return Bookmark
339 */ 351 */
340 public function setTags($tags) 352 public function setTags(?array $tags): Bookmark
341 { 353 {
342 $this->setTagsString(implode(' ', $tags)); 354 $this->tags = array_map(
355 function (string $tag): string {
356 return $tag[0] === '-' ? substr($tag, 1) : $tag;
357 },
358 tags_filter($tags, ' ')
359 );
343 360
344 return $this; 361 return $this;
345 } 362 }
@@ -357,11 +374,11 @@ class Bookmark
357 /** 374 /**
358 * Set the Thumbnail. 375 * Set the Thumbnail.
359 * 376 *
360 * @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found 377 * @param string|bool|null $thumbnail Thumbnail's URL - false if no thumbnail could be found
361 * 378 *
362 * @return Bookmark 379 * @return Bookmark
363 */ 380 */
364 public function setThumbnail($thumbnail) 381 public function setThumbnail($thumbnail): Bookmark
365 { 382 {
366 $this->thumbnail = $thumbnail; 383 $this->thumbnail = $thumbnail;
367 384
@@ -369,11 +386,29 @@ class Bookmark
369 } 386 }
370 387
371 /** 388 /**
389 * Return true if:
390 * - the bookmark's thumbnail is not already set to false (= not found)
391 * - it's not a note
392 * - it's an HTTP(S) link
393 * - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore
394 *
395 * @return bool True if the bookmark's thumbnail needs to be retrieved.
396 */
397 public function shouldUpdateThumbnail(): bool
398 {
399 return $this->thumbnail !== false
400 && !$this->isNote()
401 && startsWith(strtolower($this->url), 'http')
402 && (null === $this->thumbnail || !is_file($this->thumbnail))
403 ;
404 }
405
406 /**
372 * Get the Sticky. 407 * Get the Sticky.
373 * 408 *
374 * @return bool 409 * @return bool
375 */ 410 */
376 public function isSticky() 411 public function isSticky(): bool
377 { 412 {
378 return $this->sticky ? true : false; 413 return $this->sticky ? true : false;
379 } 414 }
@@ -381,11 +416,11 @@ class Bookmark
381 /** 416 /**
382 * Set the Sticky. 417 * Set the Sticky.
383 * 418 *
384 * @param bool $sticky 419 * @param bool|null $sticky
385 * 420 *
386 * @return Bookmark 421 * @return Bookmark
387 */ 422 */
388 public function setSticky($sticky) 423 public function setSticky(?bool $sticky): Bookmark
389 { 424 {
390 $this->sticky = $sticky ? true : false; 425 $this->sticky = $sticky ? true : false;
391 426
@@ -393,17 +428,19 @@ class Bookmark
393 } 428 }
394 429
395 /** 430 /**
396 * @return string Bookmark's tags as a string, separated by a space 431 * @param string $separator Tags separator loaded from the config file.
432 *
433 * @return string Bookmark's tags as a string, separated by a separator
397 */ 434 */
398 public function getTagsString() 435 public function getTagsString(string $separator = ' '): string
399 { 436 {
400 return implode(' ', $this->getTags()); 437 return tags_array2str($this->getTags(), $separator);
401 } 438 }
402 439
403 /** 440 /**
404 * @return bool 441 * @return bool
405 */ 442 */
406 public function isNote() 443 public function isNote(): bool
407 { 444 {
408 // We check empty value to get a valid result if the link has not been saved yet 445 // We check empty value to get a valid result if the link has not been saved yet
409 return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?'; 446 return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
@@ -416,33 +453,65 @@ class Bookmark
416 * - multiple spaces will be removed 453 * - multiple spaces will be removed
417 * - trailing dash in tags will be removed 454 * - trailing dash in tags will be removed
418 * 455 *
419 * @param string $tags 456 * @param string|null $tags
457 * @param string $separator Tags separator loaded from the config file.
420 * 458 *
421 * @return $this 459 * @return $this
422 */ 460 */
423 public function setTagsString($tags) 461 public function setTagsString(?string $tags, string $separator = ' '): Bookmark
424 { 462 {
425 // Remove first '-' char in tags. 463 $this->setTags(tags_str2array($tags, $separator));
426 $tags = preg_replace('/(^| )\-/', '$1', $tags);
427 // Explode all tags separted by spaces or commas
428 $tags = preg_split('/[\s,]+/', $tags);
429 // Remove eventual empty values
430 $tags = array_values(array_filter($tags));
431 464
432 $this->tags = $tags; 465 return $this;
466 }
467
468 /**
469 * Get entire additionalContent array.
470 *
471 * @return mixed[]
472 */
473 public function getAdditionalContent(): array
474 {
475 return $this->additionalContent;
476 }
477
478 /**
479 * Set a single entry in additionalContent, by key.
480 *
481 * @param string $key
482 * @param mixed|null $value Any type of value can be set.
483 *
484 * @return $this
485 */
486 public function addAdditionalContentEntry(string $key, $value): self
487 {
488 $this->additionalContent[$key] = $value;
433 489
434 return $this; 490 return $this;
435 } 491 }
436 492
437 /** 493 /**
494 * Get a single entry in additionalContent, by key.
495 *
496 * @param string $key
497 * @param mixed|null $default
498 *
499 * @return mixed|null can be any type or even null.
500 */
501 public function getAdditionalContentEntry(string $key, $default = null)
502 {
503 return array_key_exists($key, $this->additionalContent) ? $this->additionalContent[$key] : $default;
504 }
505
506 /**
438 * Rename a tag in tags list. 507 * Rename a tag in tags list.
439 * 508 *
440 * @param string $fromTag 509 * @param string $fromTag
441 * @param string $toTag 510 * @param string $toTag
442 */ 511 */
443 public function renameTag($fromTag, $toTag) 512 public function renameTag(string $fromTag, string $toTag): void
444 { 513 {
445 if (($pos = array_search($fromTag, $this->tags)) !== false) { 514 if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) {
446 $this->tags[$pos] = trim($toTag); 515 $this->tags[$pos] = trim($toTag);
447 } 516 }
448 } 517 }
@@ -452,9 +521,9 @@ class Bookmark
452 * 521 *
453 * @param string $tag 522 * @param string $tag
454 */ 523 */
455 public function deleteTag($tag) 524 public function deleteTag(string $tag): void
456 { 525 {
457 if (($pos = array_search($tag, $this->tags)) !== false) { 526 if (($pos = array_search($tag, $this->tags ?? [])) !== false) {
458 unset($this->tags[$pos]); 527 unset($this->tags[$pos]);
459 $this->tags = array_values($this->tags); 528 $this->tags = array_values($this->tags);
460 } 529 }
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php
index 3bd5eb20..b9328116 100644
--- a/application/bookmark/BookmarkArray.php
+++ b/application/bookmark/BookmarkArray.php
@@ -1,5 +1,7 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
5use Shaarli\Bookmark\Exception\InvalidBookmarkException; 7use Shaarli\Bookmark\Exception\InvalidBookmarkException;
@@ -70,7 +72,8 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
70 */ 72 */
71 public function offsetSet($offset, $value) 73 public function offsetSet($offset, $value)
72 { 74 {
73 if (! $value instanceof Bookmark 75 if (
76 ! $value instanceof Bookmark
74 || $value->getId() === null || empty($value->getUrl()) 77 || $value->getId() === null || empty($value->getUrl())
75 || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId()) 78 || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId())
76 || $offset !== null && $offset !== $value->getId() 79 || $offset !== null && $offset !== $value->getId()
@@ -187,13 +190,13 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
187 /** 190 /**
188 * Returns a bookmark offset in bookmarks array from its unique ID. 191 * Returns a bookmark offset in bookmarks array from its unique ID.
189 * 192 *
190 * @param int $id Persistent ID of a bookmark. 193 * @param int|null $id Persistent ID of a bookmark.
191 * 194 *
192 * @return int Real offset in local array, or null if doesn't exist. 195 * @return int Real offset in local array, or null if doesn't exist.
193 */ 196 */
194 protected function getBookmarkOffset($id) 197 protected function getBookmarkOffset(?int $id): ?int
195 { 198 {
196 if (isset($this->ids[$id])) { 199 if ($id !== null && isset($this->ids[$id])) {
197 return $this->ids[$id]; 200 return $this->ids[$id];
198 } 201 }
199 return null; 202 return null;
@@ -205,7 +208,7 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
205 * 208 *
206 * @return int next ID. 209 * @return int next ID.
207 */ 210 */
208 public function getNextId() 211 public function getNextId(): int
209 { 212 {
210 if (!empty($this->ids)) { 213 if (!empty($this->ids)) {
211 return max(array_keys($this->ids)) + 1; 214 return max(array_keys($this->ids)) + 1;
@@ -214,13 +217,14 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
214 } 217 }
215 218
216 /** 219 /**
217 * @param $url 220 * @param string $url
218 * 221 *
219 * @return Bookmark|null 222 * @return Bookmark|null
220 */ 223 */
221 public function getByUrl($url) 224 public function getByUrl(string $url): ?Bookmark
222 { 225 {
223 if (! empty($url) 226 if (
227 ! empty($url)
224 && isset($this->urls[$url]) 228 && isset($this->urls[$url])
225 && isset($this->bookmarks[$this->urls[$url]]) 229 && isset($this->bookmarks[$this->urls[$url]])
226 ) { 230 ) {
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
index c9ec2609..6666a251 100644
--- a/application/bookmark/BookmarkFileService.php
+++ b/application/bookmark/BookmarkFileService.php
@@ -1,10 +1,12 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
3 4
4namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
5 6
6 7use DateTime;
7use Exception; 8use Exception;
9use malkusch\lock\mutex\Mutex;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 10use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; 11use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
10use Shaarli\Bookmark\Exception\EmptyDataStoreException; 12use Shaarli\Bookmark\Exception\EmptyDataStoreException;
@@ -47,15 +49,19 @@ class BookmarkFileService implements BookmarkServiceInterface
47 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ 49 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
48 protected $isLoggedIn; 50 protected $isLoggedIn;
49 51
52 /** @var Mutex */
53 protected $mutex;
54
50 /** 55 /**
51 * @inheritDoc 56 * @inheritDoc
52 */ 57 */
53 public function __construct(ConfigManager $conf, History $history, $isLoggedIn) 58 public function __construct(ConfigManager $conf, History $history, Mutex $mutex, bool $isLoggedIn)
54 { 59 {
55 $this->conf = $conf; 60 $this->conf = $conf;
56 $this->history = $history; 61 $this->history = $history;
62 $this->mutex = $mutex;
57 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); 63 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
58 $this->bookmarksIO = new BookmarkIO($this->conf); 64 $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex);
59 $this->isLoggedIn = $isLoggedIn; 65 $this->isLoggedIn = $isLoggedIn;
60 66
61 if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { 67 if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
@@ -63,7 +69,7 @@ class BookmarkFileService implements BookmarkServiceInterface
63 } else { 69 } else {
64 try { 70 try {
65 $this->bookmarks = $this->bookmarksIO->read(); 71 $this->bookmarks = $this->bookmarksIO->read();
66 } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { 72 } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) {
67 $this->bookmarks = new BookmarkArray(); 73 $this->bookmarks = new BookmarkArray();
68 74
69 if ($this->isLoggedIn) { 75 if ($this->isLoggedIn) {
@@ -79,25 +85,29 @@ class BookmarkFileService implements BookmarkServiceInterface
79 if (! $this->bookmarks instanceof BookmarkArray) { 85 if (! $this->bookmarks instanceof BookmarkArray) {
80 $this->migrate(); 86 $this->migrate();
81 exit( 87 exit(
82 'Your data store has been migrated, please reload the page.'. PHP_EOL . 88 'Your data store has been migrated, please reload the page.' . PHP_EOL .
83 'If this message keeps showing up, please delete data/updates.txt file.' 89 'If this message keeps showing up, please delete data/updates.txt file.'
84 ); 90 );
85 } 91 }
86 } 92 }
87 93
88 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks); 94 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf);
89 } 95 }
90 96
91 /** 97 /**
92 * @inheritDoc 98 * @inheritDoc
93 */ 99 */
94 public function findByHash($hash) 100 public function findByHash(string $hash, string $privateKey = null): Bookmark
95 { 101 {
96 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); 102 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
97 // PHP 7.3 introduced array_key_first() to avoid this hack 103 // PHP 7.3 introduced array_key_first() to avoid this hack
98 $first = reset($bookmark); 104 $first = reset($bookmark);
99 if (! $this->isLoggedIn && $first->isPrivate()) { 105 if (
100 throw new Exception('Not authorized'); 106 !$this->isLoggedIn
107 && $first->isPrivate()
108 && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
109 ) {
110 throw new BookmarkNotFoundException();
101 } 111 }
102 112
103 return $first; 113 return $first;
@@ -106,7 +116,7 @@ class BookmarkFileService implements BookmarkServiceInterface
106 /** 116 /**
107 * @inheritDoc 117 * @inheritDoc
108 */ 118 */
109 public function findByUrl($url) 119 public function findByUrl(string $url): ?Bookmark
110 { 120 {
111 return $this->bookmarks->getByUrl($url); 121 return $this->bookmarks->getByUrl($url);
112 } 122 }
@@ -115,10 +125,10 @@ class BookmarkFileService implements BookmarkServiceInterface
115 * @inheritDoc 125 * @inheritDoc
116 */ 126 */
117 public function search( 127 public function search(
118 $request = [], 128 array $request = [],
119 $visibility = null, 129 string $visibility = null,
120 $caseSensitive = false, 130 bool $caseSensitive = false,
121 $untaggedOnly = false, 131 bool $untaggedOnly = false,
122 bool $ignoreSticky = false 132 bool $ignoreSticky = false
123 ) { 133 ) {
124 if ($visibility === null) { 134 if ($visibility === null) {
@@ -126,8 +136,8 @@ class BookmarkFileService implements BookmarkServiceInterface
126 } 136 }
127 137
128 // Filter bookmark database according to parameters. 138 // Filter bookmark database according to parameters.
129 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : ''; 139 $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
130 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : ''; 140 $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
131 141
132 if ($ignoreSticky) { 142 if ($ignoreSticky) {
133 $this->bookmarks->reorder('DESC', true); 143 $this->bookmarks->reorder('DESC', true);
@@ -135,7 +145,7 @@ class BookmarkFileService implements BookmarkServiceInterface
135 145
136 return $this->bookmarkFilter->filter( 146 return $this->bookmarkFilter->filter(
137 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, 147 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
138 [$searchtags, $searchterm], 148 [$searchTags, $searchTerm],
139 $caseSensitive, 149 $caseSensitive,
140 $visibility, 150 $visibility,
141 $untaggedOnly 151 $untaggedOnly
@@ -145,7 +155,7 @@ class BookmarkFileService implements BookmarkServiceInterface
145 /** 155 /**
146 * @inheritDoc 156 * @inheritDoc
147 */ 157 */
148 public function get($id, $visibility = null) 158 public function get(int $id, string $visibility = null): Bookmark
149 { 159 {
150 if (! isset($this->bookmarks[$id])) { 160 if (! isset($this->bookmarks[$id])) {
151 throw new BookmarkNotFoundException(); 161 throw new BookmarkNotFoundException();
@@ -156,7 +166,8 @@ class BookmarkFileService implements BookmarkServiceInterface
156 } 166 }
157 167
158 $bookmark = $this->bookmarks[$id]; 168 $bookmark = $this->bookmarks[$id];
159 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') 169 if (
170 ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
160 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') 171 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
161 ) { 172 ) {
162 throw new Exception('Unauthorized'); 173 throw new Exception('Unauthorized');
@@ -168,20 +179,17 @@ class BookmarkFileService implements BookmarkServiceInterface
168 /** 179 /**
169 * @inheritDoc 180 * @inheritDoc
170 */ 181 */
171 public function set($bookmark, $save = true) 182 public function set(Bookmark $bookmark, bool $save = true): Bookmark
172 { 183 {
173 if (true !== $this->isLoggedIn) { 184 if (true !== $this->isLoggedIn) {
174 throw new Exception(t('You\'re not authorized to alter the datastore')); 185 throw new Exception(t('You\'re not authorized to alter the datastore'));
175 } 186 }
176 if (! $bookmark instanceof Bookmark) {
177 throw new Exception(t('Provided data is invalid'));
178 }
179 if (! isset($this->bookmarks[$bookmark->getId()])) { 187 if (! isset($this->bookmarks[$bookmark->getId()])) {
180 throw new BookmarkNotFoundException(); 188 throw new BookmarkNotFoundException();
181 } 189 }
182 $bookmark->validate(); 190 $bookmark->validate();
183 191
184 $bookmark->setUpdated(new \DateTime()); 192 $bookmark->setUpdated(new DateTime());
185 $this->bookmarks[$bookmark->getId()] = $bookmark; 193 $this->bookmarks[$bookmark->getId()] = $bookmark;
186 if ($save === true) { 194 if ($save === true) {
187 $this->save(); 195 $this->save();
@@ -193,15 +201,12 @@ class BookmarkFileService implements BookmarkServiceInterface
193 /** 201 /**
194 * @inheritDoc 202 * @inheritDoc
195 */ 203 */
196 public function add($bookmark, $save = true) 204 public function add(Bookmark $bookmark, bool $save = true): Bookmark
197 { 205 {
198 if (true !== $this->isLoggedIn) { 206 if (true !== $this->isLoggedIn) {
199 throw new Exception(t('You\'re not authorized to alter the datastore')); 207 throw new Exception(t('You\'re not authorized to alter the datastore'));
200 } 208 }
201 if (! $bookmark instanceof Bookmark) { 209 if (!empty($bookmark->getId())) {
202 throw new Exception(t('Provided data is invalid'));
203 }
204 if (! empty($bookmark->getId())) {
205 throw new Exception(t('This bookmarks already exists')); 210 throw new Exception(t('This bookmarks already exists'));
206 } 211 }
207 $bookmark->setId($this->bookmarks->getNextId()); 212 $bookmark->setId($this->bookmarks->getNextId());
@@ -218,14 +223,11 @@ class BookmarkFileService implements BookmarkServiceInterface
218 /** 223 /**
219 * @inheritDoc 224 * @inheritDoc
220 */ 225 */
221 public function addOrSet($bookmark, $save = true) 226 public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark
222 { 227 {
223 if (true !== $this->isLoggedIn) { 228 if (true !== $this->isLoggedIn) {
224 throw new Exception(t('You\'re not authorized to alter the datastore')); 229 throw new Exception(t('You\'re not authorized to alter the datastore'));
225 } 230 }
226 if (! $bookmark instanceof Bookmark) {
227 throw new Exception('Provided data is invalid');
228 }
229 if ($bookmark->getId() === null) { 231 if ($bookmark->getId() === null) {
230 return $this->add($bookmark, $save); 232 return $this->add($bookmark, $save);
231 } 233 }
@@ -235,14 +237,11 @@ class BookmarkFileService implements BookmarkServiceInterface
235 /** 237 /**
236 * @inheritDoc 238 * @inheritDoc
237 */ 239 */
238 public function remove($bookmark, $save = true) 240 public function remove(Bookmark $bookmark, bool $save = true): void
239 { 241 {
240 if (true !== $this->isLoggedIn) { 242 if (true !== $this->isLoggedIn) {
241 throw new Exception(t('You\'re not authorized to alter the datastore')); 243 throw new Exception(t('You\'re not authorized to alter the datastore'));
242 } 244 }
243 if (! $bookmark instanceof Bookmark) {
244 throw new Exception(t('Provided data is invalid'));
245 }
246 if (! isset($this->bookmarks[$bookmark->getId()])) { 245 if (! isset($this->bookmarks[$bookmark->getId()])) {
247 throw new BookmarkNotFoundException(); 246 throw new BookmarkNotFoundException();
248 } 247 }
@@ -257,7 +256,7 @@ class BookmarkFileService implements BookmarkServiceInterface
257 /** 256 /**
258 * @inheritDoc 257 * @inheritDoc
259 */ 258 */
260 public function exists($id, $visibility = null) 259 public function exists(int $id, string $visibility = null): bool
261 { 260 {
262 if (! isset($this->bookmarks[$id])) { 261 if (! isset($this->bookmarks[$id])) {
263 return false; 262 return false;
@@ -268,7 +267,8 @@ class BookmarkFileService implements BookmarkServiceInterface
268 } 267 }
269 268
270 $bookmark = $this->bookmarks[$id]; 269 $bookmark = $this->bookmarks[$id];
271 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') 270 if (
271 ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
272 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') 272 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
273 ) { 273 ) {
274 return false; 274 return false;
@@ -280,7 +280,7 @@ class BookmarkFileService implements BookmarkServiceInterface
280 /** 280 /**
281 * @inheritDoc 281 * @inheritDoc
282 */ 282 */
283 public function count($visibility = null) 283 public function count(string $visibility = null): int
284 { 284 {
285 return count($this->search([], $visibility)); 285 return count($this->search([], $visibility));
286 } 286 }
@@ -288,7 +288,7 @@ class BookmarkFileService implements BookmarkServiceInterface
288 /** 288 /**
289 * @inheritDoc 289 * @inheritDoc
290 */ 290 */
291 public function save() 291 public function save(): void
292 { 292 {
293 if (true !== $this->isLoggedIn) { 293 if (true !== $this->isLoggedIn) {
294 // TODO: raise an Exception instead 294 // TODO: raise an Exception instead
@@ -303,14 +303,15 @@ class BookmarkFileService implements BookmarkServiceInterface
303 /** 303 /**
304 * @inheritDoc 304 * @inheritDoc
305 */ 305 */
306 public function bookmarksCountPerTag($filteringTags = [], $visibility = null) 306 public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
307 { 307 {
308 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); 308 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
309 $tags = []; 309 $tags = [];
310 $caseMapping = []; 310 $caseMapping = [];
311 foreach ($bookmarks as $bookmark) { 311 foreach ($bookmarks as $bookmark) {
312 foreach ($bookmark->getTags() as $tag) { 312 foreach ($bookmark->getTags() as $tag) {
313 if (empty($tag) 313 if (
314 empty($tag)
314 || (! $this->isLoggedIn && startsWith($tag, '.')) 315 || (! $this->isLoggedIn && startsWith($tag, '.'))
315 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG 316 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
316 || in_array($tag, $filteringTags, true) 317 || in_array($tag, $filteringTags, true)
@@ -339,38 +340,55 @@ class BookmarkFileService implements BookmarkServiceInterface
339 $keys = array_keys($tags); 340 $keys = array_keys($tags);
340 $tmpTags = array_combine($keys, $keys); 341 $tmpTags = array_combine($keys, $keys);
341 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); 342 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
343
342 return $tags; 344 return $tags;
343 } 345 }
344 346
345 /** 347 /**
346 * @inheritDoc 348 * @inheritDoc
347 */ 349 */
348 public function days() 350 public function findByDate(
349 { 351 \DateTimeInterface $from,
350 $bookmarkDays = []; 352 \DateTimeInterface $to,
351 foreach ($this->search() as $bookmark) { 353 ?\DateTimeInterface &$previous,
352 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; 354 ?\DateTimeInterface &$next
355 ): array {
356 $out = [];
357 $previous = null;
358 $next = null;
359
360 foreach ($this->search([], null, false, false, true) as $bookmark) {
361 if ($to < $bookmark->getCreated()) {
362 $next = $bookmark->getCreated();
363 } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
364 $out[] = $bookmark;
365 } else {
366 if ($previous !== null) {
367 break;
368 }
369 $previous = $bookmark->getCreated();
370 }
353 } 371 }
354 $bookmarkDays = array_keys($bookmarkDays);
355 sort($bookmarkDays);
356 372
357 return $bookmarkDays; 373 return $out;
358 } 374 }
359 375
360 /** 376 /**
361 * @inheritDoc 377 * @inheritDoc
362 */ 378 */
363 public function filterDay($request) 379 public function getLatest(): ?Bookmark
364 { 380 {
365 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; 381 foreach ($this->search([], null, false, false, true) as $bookmark) {
382 return $bookmark;
383 }
366 384
367 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); 385 return null;
368 } 386 }
369 387
370 /** 388 /**
371 * @inheritDoc 389 * @inheritDoc
372 */ 390 */
373 public function initialize() 391 public function initialize(): void
374 { 392 {
375 $initializer = new BookmarkInitializer($this); 393 $initializer = new BookmarkInitializer($this);
376 $initializer->initialize(); 394 $initializer->initialize();
@@ -383,7 +401,7 @@ class BookmarkFileService implements BookmarkServiceInterface
383 /** 401 /**
384 * Handles migration to the new database format (BookmarksArray). 402 * Handles migration to the new database format (BookmarksArray).
385 */ 403 */
386 protected function migrate() 404 protected function migrate(): void
387 { 405 {
388 $bookmarkDb = new LegacyLinkDB( 406 $bookmarkDb = new LegacyLinkDB(
389 $this->conf->get('resource.datastore'), 407 $this->conf->get('resource.datastore'),
@@ -391,14 +409,14 @@ class BookmarkFileService implements BookmarkServiceInterface
391 false 409 false
392 ); 410 );
393 $updater = new LegacyUpdater( 411 $updater = new LegacyUpdater(
394 UpdaterUtils::read_updates_file($this->conf->get('resource.updates')), 412 UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')),
395 $bookmarkDb, 413 $bookmarkDb,
396 $this->conf, 414 $this->conf,
397 true 415 true
398 ); 416 );
399 $newUpdates = $updater->update(); 417 $newUpdates = $updater->update();
400 if (! empty($newUpdates)) { 418 if (! empty($newUpdates)) {
401 UpdaterUtils::write_updates_file( 419 UpdaterUtils::writeUpdatesFile(
402 $this->conf->get('resource.updates'), 420 $this->conf->get('resource.updates'),
403 $updater->getDoneUpdates() 421 $updater->getDoneUpdates()
404 ); 422 );
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php
index 6636bbfe..db83c51c 100644
--- a/application/bookmark/BookmarkFilter.php
+++ b/application/bookmark/BookmarkFilter.php
@@ -1,9 +1,12 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
5use Exception; 7use Exception;
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Config\ConfigManager;
7 10
8/** 11/**
9 * Class LinkFilter. 12 * Class LinkFilter.
@@ -56,12 +59,16 @@ class BookmarkFilter
56 */ 59 */
57 private $bookmarks; 60 private $bookmarks;
58 61
62 /** @var ConfigManager */
63 protected $conf;
64
59 /** 65 /**
60 * @param Bookmark[] $bookmarks initialization. 66 * @param Bookmark[] $bookmarks initialization.
61 */ 67 */
62 public function __construct($bookmarks) 68 public function __construct($bookmarks, ConfigManager $conf)
63 { 69 {
64 $this->bookmarks = $bookmarks; 70 $this->bookmarks = $bookmarks;
71 $this->conf = $conf;
65 } 72 }
66 73
67 /** 74 /**
@@ -77,8 +84,13 @@ class BookmarkFilter
77 * 84 *
78 * @throws BookmarkNotFoundException 85 * @throws BookmarkNotFoundException
79 */ 86 */
80 public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false) 87 public function filter(
81 { 88 string $type,
89 $request,
90 bool $casesensitive = false,
91 string $visibility = 'all',
92 bool $untaggedonly = false
93 ) {
82 if (!in_array($visibility, ['all', 'public', 'private'])) { 94 if (!in_array($visibility, ['all', 'public', 'private'])) {
83 $visibility = 'all'; 95 $visibility = 'all';
84 } 96 }
@@ -100,10 +112,14 @@ class BookmarkFilter
100 $filtered = $this->bookmarks; 112 $filtered = $this->bookmarks;
101 } 113 }
102 if (!empty($request[0])) { 114 if (!empty($request[0])) {
103 $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); 115 $filtered = (new BookmarkFilter($filtered, $this->conf))
116 ->filterTags($request[0], $casesensitive, $visibility)
117 ;
104 } 118 }
105 if (!empty($request[1])) { 119 if (!empty($request[1])) {
106 $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility); 120 $filtered = (new BookmarkFilter($filtered, $this->conf))
121 ->filterFulltext($request[1], $visibility)
122 ;
107 } 123 }
108 return $filtered; 124 return $filtered;
109 case self::$FILTER_TEXT: 125 case self::$FILTER_TEXT:
@@ -128,13 +144,13 @@ class BookmarkFilter
128 * 144 *
129 * @return Bookmark[] filtered bookmarks. 145 * @return Bookmark[] filtered bookmarks.
130 */ 146 */
131 private function noFilter($visibility = 'all') 147 private function noFilter(string $visibility = 'all')
132 { 148 {
133 if ($visibility === 'all') { 149 if ($visibility === 'all') {
134 return $this->bookmarks; 150 return $this->bookmarks;
135 } 151 }
136 152
137 $out = array(); 153 $out = [];
138 foreach ($this->bookmarks as $key => $value) { 154 foreach ($this->bookmarks as $key => $value) {
139 if ($value->isPrivate() && $visibility === 'private') { 155 if ($value->isPrivate() && $visibility === 'private') {
140 $out[$key] = $value; 156 $out[$key] = $value;
@@ -151,11 +167,11 @@ class BookmarkFilter
151 * 167 *
152 * @param string $smallHash permalink hash. 168 * @param string $smallHash permalink hash.
153 * 169 *
154 * @return array $filtered array containing permalink data. 170 * @return Bookmark[] $filtered array containing permalink data.
155 * 171 *
156 * @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link. 172 * @throws BookmarkNotFoundException if the smallhash doesn't match any link.
157 */ 173 */
158 private function filterSmallHash($smallHash) 174 private function filterSmallHash(string $smallHash)
159 { 175 {
160 foreach ($this->bookmarks as $key => $l) { 176 foreach ($this->bookmarks as $key => $l) {
161 if ($smallHash == $l->getShortUrl()) { 177 if ($smallHash == $l->getShortUrl()) {
@@ -186,15 +202,15 @@ class BookmarkFilter
186 * @param string $searchterms search query. 202 * @param string $searchterms search query.
187 * @param string $visibility Optional: return only all/private/public bookmarks. 203 * @param string $visibility Optional: return only all/private/public bookmarks.
188 * 204 *
189 * @return array search results. 205 * @return Bookmark[] search results.
190 */ 206 */
191 private function filterFulltext($searchterms, $visibility = 'all') 207 private function filterFulltext(string $searchterms, string $visibility = 'all')
192 { 208 {
193 if (empty($searchterms)) { 209 if (empty($searchterms)) {
194 return $this->noFilter($visibility); 210 return $this->noFilter($visibility);
195 } 211 }
196 212
197 $filtered = array(); 213 $filtered = [];
198 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); 214 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
199 $exactRegex = '/"([^"]+)"/'; 215 $exactRegex = '/"([^"]+)"/';
200 // Retrieve exact search terms. 216 // Retrieve exact search terms.
@@ -206,8 +222,8 @@ class BookmarkFilter
206 $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); 222 $explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
207 223
208 // Filter excluding terms and update andSearch. 224 // Filter excluding terms and update andSearch.
209 $excludeSearch = array(); 225 $excludeSearch = [];
210 $andSearch = array(); 226 $andSearch = [];
211 foreach ($explodedSearchAnd as $needle) { 227 foreach ($explodedSearchAnd as $needle) {
212 if ($needle[0] == '-' && strlen($needle) > 1) { 228 if ($needle[0] == '-' && strlen($needle) > 1) {
213 $excludeSearch[] = substr($needle, 1); 229 $excludeSearch[] = substr($needle, 1);
@@ -227,33 +243,38 @@ class BookmarkFilter
227 } 243 }
228 } 244 }
229 245
230 // Concatenate link fields to search across fields. 246 $lengths = [];
231 // Adds a '\' separator for exact search terms. 247 $content = $this->buildFullTextSearchableLink($link, $lengths);
232 $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
233 $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
234 $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
235 $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\';
236 248
237 // Be optimistic 249 // Be optimistic
238 $found = true; 250 $found = true;
251 $foundPositions = [];
239 252
240 // First, we look for exact term search 253 // First, we look for exact term search
241 for ($i = 0; $i < count($exactSearch) && $found; $i++) { 254 // Then iterate over keywords, if keyword is not found,
242 $found = strpos($content, $exactSearch[$i]) !== false;
243 }
244
245 // Iterate over keywords, if keyword is not found,
246 // no need to check for the others. We want all or nothing. 255 // no need to check for the others. We want all or nothing.
247 for ($i = 0; $i < count($andSearch) && $found; $i++) { 256 foreach ([$exactSearch, $andSearch] as $search) {
248 $found = strpos($content, $andSearch[$i]) !== false; 257 for ($i = 0; $i < count($search) && $found !== false; $i++) {
258 $found = mb_strpos($content, $search[$i]);
259 if ($found === false) {
260 break;
261 }
262
263 $foundPositions[] = ['start' => $found, 'end' => $found + mb_strlen($search[$i])];
264 }
249 } 265 }
250 266
251 // Exclude terms. 267 // Exclude terms.
252 for ($i = 0; $i < count($excludeSearch) && $found; $i++) { 268 for ($i = 0; $i < count($excludeSearch) && $found !== false; $i++) {
253 $found = strpos($content, $excludeSearch[$i]) === false; 269 $found = strpos($content, $excludeSearch[$i]) === false;
254 } 270 }
255 271
256 if ($found) { 272 if ($found !== false) {
273 $link->addAdditionalContentEntry(
274 'search_highlight',
275 $this->postProcessFoundPositions($lengths, $foundPositions)
276 );
277
257 $filtered[$id] = $link; 278 $filtered[$id] = $link;
258 } 279 }
259 } 280 }
@@ -268,8 +289,9 @@ class BookmarkFilter
268 * 289 *
269 * @return string generated regex fragment 290 * @return string generated regex fragment
270 */ 291 */
271 private static function tag2regex($tag) 292 protected function tag2regex(string $tag): string
272 { 293 {
294 $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
273 $len = strlen($tag); 295 $len = strlen($tag);
274 if (!$len || $tag === "-" || $tag === "*") { 296 if (!$len || $tag === "-" || $tag === "*") {
275 // nothing to search, return empty regex 297 // nothing to search, return empty regex
@@ -283,12 +305,13 @@ class BookmarkFilter
283 $i = 0; // start at first character 305 $i = 0; // start at first character
284 $regex = '(?='; // use positive lookahead 306 $regex = '(?='; // use positive lookahead
285 } 307 }
286 $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning 308 // before tag may only be the separator or the beginning
309 $regex .= '.*(?:^|' . $tagsSeparator . ')';
287 // iterate over string, separating it into placeholder and content 310 // iterate over string, separating it into placeholder and content
288 for (; $i < $len; $i++) { 311 for (; $i < $len; $i++) {
289 if ($tag[$i] === '*') { 312 if ($tag[$i] === '*') {
290 // placeholder found 313 // placeholder found
291 $regex .= '[^ ]*?'; 314 $regex .= '[^' . $tagsSeparator . ']*?';
292 } else { 315 } else {
293 // regular characters 316 // regular characters
294 $offset = strpos($tag, '*', $i); 317 $offset = strpos($tag, '*', $i);
@@ -304,7 +327,8 @@ class BookmarkFilter
304 $i = $offset; 327 $i = $offset;
305 } 328 }
306 } 329 }
307 $regex .= '(?:$| ))'; // after the tag may only be a space or the end 330 // after the tag may only be the separator or the end
331 $regex .= '(?:$|' . $tagsSeparator . '))';
308 return $regex; 332 return $regex;
309 } 333 }
310 334
@@ -314,22 +338,23 @@ class BookmarkFilter
314 * You can specify one or more tags, separated by space or a comma, e.g. 338 * You can specify one or more tags, separated by space or a comma, e.g.
315 * print_r($mydb->filterTags('linux programming')); 339 * print_r($mydb->filterTags('linux programming'));
316 * 340 *
317 * @param string $tags list of tags separated by commas or blank spaces. 341 * @param string|array $tags list of tags, separated by commas or blank spaces if passed as string.
318 * @param bool $casesensitive ignore case if false. 342 * @param bool $casesensitive ignore case if false.
319 * @param string $visibility Optional: return only all/private/public bookmarks. 343 * @param string $visibility Optional: return only all/private/public bookmarks.
320 * 344 *
321 * @return array filtered bookmarks. 345 * @return Bookmark[] filtered bookmarks.
322 */ 346 */
323 public function filterTags($tags, $casesensitive = false, $visibility = 'all') 347 public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
324 { 348 {
349 $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
325 // get single tags (we may get passed an array, even though the docs say different) 350 // get single tags (we may get passed an array, even though the docs say different)
326 $inputTags = $tags; 351 $inputTags = $tags;
327 if (!is_array($tags)) { 352 if (!is_array($tags)) {
328 // we got an input string, split tags 353 // we got an input string, split tags
329 $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); 354 $inputTags = tags_str2array($inputTags, $tagsSeparator);
330 } 355 }
331 356
332 if (!count($inputTags)) { 357 if (count($inputTags) === 0) {
333 // no input tags 358 // no input tags
334 return $this->noFilter($visibility); 359 return $this->noFilter($visibility);
335 } 360 }
@@ -346,7 +371,7 @@ class BookmarkFilter
346 } 371 }
347 372
348 // build regex from all tags 373 // build regex from all tags
349 $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; 374 $re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/';
350 if (!$casesensitive) { 375 if (!$casesensitive) {
351 // make regex case insensitive 376 // make regex case insensitive
352 $re .= 'i'; 377 $re .= 'i';
@@ -366,10 +391,11 @@ class BookmarkFilter
366 continue; 391 continue;
367 } 392 }
368 } 393 }
369 $search = $link->getTagsString(); // build search string, start with tags of current link 394 // build search string, start with tags of current link
395 $search = $link->getTagsString($tagsSeparator);
370 if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { 396 if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
371 // description given and at least one possible tag found 397 // description given and at least one possible tag found
372 $descTags = array(); 398 $descTags = [];
373 // find all tags in the form of #tag in the description 399 // find all tags in the form of #tag in the description
374 preg_match_all( 400 preg_match_all(
375 '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', 401 '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
@@ -378,9 +404,9 @@ class BookmarkFilter
378 ); 404 );
379 if (count($descTags[1])) { 405 if (count($descTags[1])) {
380 // there were some tags in the description, add them to the search string 406 // there were some tags in the description, add them to the search string
381 $search .= ' ' . implode(' ', $descTags[1]); 407 $search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator);
382 } 408 }
383 }; 409 }
384 // match regular expression with search string 410 // match regular expression with search string
385 if (!preg_match($re, $search)) { 411 if (!preg_match($re, $search)) {
386 // this entry does _not_ match our regex 412 // this entry does _not_ match our regex
@@ -396,9 +422,9 @@ class BookmarkFilter
396 * 422 *
397 * @param string $visibility return only all/private/public bookmarks. 423 * @param string $visibility return only all/private/public bookmarks.
398 * 424 *
399 * @return array filtered bookmarks. 425 * @return Bookmark[] filtered bookmarks.
400 */ 426 */
401 public function filterUntagged($visibility) 427 public function filterUntagged(string $visibility)
402 { 428 {
403 $filtered = []; 429 $filtered = [];
404 foreach ($this->bookmarks as $key => $link) { 430 foreach ($this->bookmarks as $key => $link) {
@@ -410,7 +436,7 @@ class BookmarkFilter
410 } 436 }
411 } 437 }
412 438
413 if (empty(trim($link->getTagsString()))) { 439 if (empty($link->getTags())) {
414 $filtered[$key] = $link; 440 $filtered[$key] = $link;
415 } 441 }
416 } 442 }
@@ -427,11 +453,11 @@ class BookmarkFilter
427 * @param string $day day to filter. 453 * @param string $day day to filter.
428 * @param string $visibility return only all/private/public bookmarks. 454 * @param string $visibility return only all/private/public bookmarks.
429 455
430 * @return array all link matching given day. 456 * @return Bookmark[] all link matching given day.
431 * 457 *
432 * @throws Exception if date format is invalid. 458 * @throws Exception if date format is invalid.
433 */ 459 */
434 public function filterDay($day, $visibility) 460 public function filterDay(string $day, string $visibility)
435 { 461 {
436 if (!checkDateFormat('Ymd', $day)) { 462 if (!checkDateFormat('Ymd', $day)) {
437 throw new Exception('Invalid date format'); 463 throw new Exception('Invalid date format');
@@ -460,9 +486,9 @@ class BookmarkFilter
460 * @param string $tags string containing a list of tags. 486 * @param string $tags string containing a list of tags.
461 * @param bool $casesensitive will convert everything to lowercase if false. 487 * @param bool $casesensitive will convert everything to lowercase if false.
462 * 488 *
463 * @return array filtered tags string. 489 * @return string[] filtered tags string.
464 */ 490 */
465 public static function tagsStrToArray($tags, $casesensitive) 491 public static function tagsStrToArray(string $tags, bool $casesensitive): array
466 { 492 {
467 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) 493 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
468 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); 494 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
@@ -470,4 +496,75 @@ class BookmarkFilter
470 496
471 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); 497 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
472 } 498 }
499
500 /**
501 * This method finalize the content of the foundPositions array,
502 * by associated all search results to their associated bookmark field,
503 * making sure that there is no overlapping results, etc.
504 *
505 * @param array $fieldLengths Start and end positions of every bookmark fields in the aggregated bookmark content.
506 * @param array $foundPositions Positions where the search results were found in the aggregated content.
507 *
508 * @return array Updated $foundPositions, by bookmark field.
509 */
510 protected function postProcessFoundPositions(array $fieldLengths, array $foundPositions): array
511 {
512 // Sort results by starting position ASC.
513 usort($foundPositions, function (array $entryA, array $entryB): int {
514 return $entryA['start'] > $entryB['start'] ? 1 : -1;
515 });
516
517 $out = [];
518 $currentMax = -1;
519 foreach ($foundPositions as $foundPosition) {
520 // we do not allow overlapping highlights
521 if ($foundPosition['start'] < $currentMax) {
522 continue;
523 }
524
525 $currentMax = $foundPosition['end'];
526 foreach ($fieldLengths as $part => $length) {
527 if ($foundPosition['start'] < $length['start'] || $foundPosition['start'] > $length['end']) {
528 continue;
529 }
530
531 $out[$part][] = [
532 'start' => $foundPosition['start'] - $length['start'],
533 'end' => $foundPosition['end'] - $length['start'],
534 ];
535 break;
536 }
537 }
538
539 return $out;
540 }
541
542 /**
543 * Concatenate link fields to search across fields. Adds a '\' separator for exact search terms.
544 * Also populate $length array with starting and ending positions of every bookmark field
545 * inside concatenated content.
546 *
547 * @param Bookmark $link
548 * @param array $lengths (by reference)
549 *
550 * @return string Lowercase concatenated fields content.
551 */
552 protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
553 {
554 $tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' '));
555 $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') . '\\';
556 $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') . '\\';
557 $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') . '\\';
558 $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') . '\\';
559
560 $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
561 $nextField = $lengths['title']['end'] + 1;
562 $lengths['description'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getDescription())];
563 $nextField = $lengths['description']['end'] + 1;
564 $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
565 $nextField = $lengths['url']['end'] + 1;
566 $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)];
567
568 return $content;
569 }
473} 570}
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php
index 6bf7f365..c78dbe41 100644
--- a/application/bookmark/BookmarkIO.php
+++ b/application/bookmark/BookmarkIO.php
@@ -1,7 +1,11 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
7use malkusch\lock\mutex\Mutex;
8use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; 9use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
6use Shaarli\Bookmark\Exception\EmptyDataStoreException; 10use Shaarli\Bookmark\Exception\EmptyDataStoreException;
7use Shaarli\Bookmark\Exception\NotWritableDataStoreException; 11use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
@@ -27,11 +31,14 @@ class BookmarkIO
27 */ 31 */
28 protected $conf; 32 protected $conf;
29 33
34
35 /** @var Mutex */
36 protected $mutex;
37
30 /** 38 /**
31 * string Datastore PHP prefix 39 * string Datastore PHP prefix
32 */ 40 */
33 protected static $phpPrefix = '<?php /* '; 41 protected static $phpPrefix = '<?php /* ';
34
35 /** 42 /**
36 * string Datastore PHP suffix 43 * string Datastore PHP suffix
37 */ 44 */
@@ -42,16 +49,21 @@ class BookmarkIO
42 * 49 *
43 * @param ConfigManager $conf instance 50 * @param ConfigManager $conf instance
44 */ 51 */
45 public function __construct($conf) 52 public function __construct(ConfigManager $conf, Mutex $mutex = null)
46 { 53 {
54 if ($mutex === null) {
55 // This should only happen with legacy classes
56 $mutex = new NoMutex();
57 }
47 $this->conf = $conf; 58 $this->conf = $conf;
48 $this->datastore = $conf->get('resource.datastore'); 59 $this->datastore = $conf->get('resource.datastore');
60 $this->mutex = $mutex;
49 } 61 }
50 62
51 /** 63 /**
52 * Reads database from disk to memory 64 * Reads database from disk to memory
53 * 65 *
54 * @return BookmarkArray instance 66 * @return Bookmark[]
55 * 67 *
56 * @throws NotWritableDataStoreException Data couldn't be loaded 68 * @throws NotWritableDataStoreException Data couldn't be loaded
57 * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark 69 * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark
@@ -67,11 +79,16 @@ class BookmarkIO
67 throw new NotWritableDataStoreException($this->datastore); 79 throw new NotWritableDataStoreException($this->datastore);
68 } 80 }
69 81
82 $content = null;
83 $this->mutex->synchronized(function () use (&$content) {
84 $content = file_get_contents($this->datastore);
85 });
86
70 // Note that gzinflate is faster than gzuncompress. 87 // Note that gzinflate is faster than gzuncompress.
71 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 88 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
72 $links = unserialize(gzinflate(base64_decode( 89 $links = unserialize(gzinflate(base64_decode(
73 substr(file_get_contents($this->datastore), 90 substr($content, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
74 strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); 91 )));
75 92
76 if (empty($links)) { 93 if (empty($links)) {
77 if (filesize($this->datastore) > 100) { 94 if (filesize($this->datastore) > 100) {
@@ -86,7 +103,7 @@ class BookmarkIO
86 /** 103 /**
87 * Saves the database from memory to disk 104 * Saves the database from memory to disk
88 * 105 *
89 * @param BookmarkArray $links instance. 106 * @param Bookmark[] $links
90 * 107 *
91 * @throws NotWritableDataStoreException the datastore is not writable 108 * @throws NotWritableDataStoreException the datastore is not writable
92 */ 109 */
@@ -95,14 +112,18 @@ class BookmarkIO
95 if (is_file($this->datastore) && !is_writeable($this->datastore)) { 112 if (is_file($this->datastore) && !is_writeable($this->datastore)) {
96 // The datastore exists but is not writeable 113 // The datastore exists but is not writeable
97 throw new NotWritableDataStoreException($this->datastore); 114 throw new NotWritableDataStoreException($this->datastore);
98 } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { 115 } elseif (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
99 // The datastore does not exist and its parent directory is not writeable 116 // The datastore does not exist and its parent directory is not writeable
100 throw new NotWritableDataStoreException(dirname($this->datastore)); 117 throw new NotWritableDataStoreException(dirname($this->datastore));
101 } 118 }
102 119
103 file_put_contents( 120 $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix;
104 $this->datastore, 121
105 self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix 122 $this->mutex->synchronized(function () use ($data) {
106 ); 123 file_put_contents(
124 $this->datastore,
125 $data
126 );
127 });
107 } 128 }
108} 129}
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php
index 815047e3..8ab5c441 100644
--- a/application/bookmark/BookmarkInitializer.php
+++ b/application/bookmark/BookmarkInitializer.php
@@ -1,5 +1,7 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
5/** 7/**
@@ -11,6 +13,9 @@ namespace Shaarli\Bookmark;
11 * To prevent data corruption, it does not overwrite existing bookmarks, 13 * To prevent data corruption, it does not overwrite existing bookmarks,
12 * even though there should not be any. 14 * even though there should not be any.
13 * 15 *
16 * We disable this because otherwise it creates indentation issues, and heredoc is not supported by PHP gettext.
17 * @phpcs:disable Generic.Files.LineLength.TooLong
18 *
14 * @package Shaarli\Bookmark 19 * @package Shaarli\Bookmark
15 */ 20 */
16class BookmarkInitializer 21class BookmarkInitializer
@@ -23,7 +28,7 @@ class BookmarkInitializer
23 * 28 *
24 * @param BookmarkServiceInterface $bookmarkService 29 * @param BookmarkServiceInterface $bookmarkService
25 */ 30 */
26 public function __construct($bookmarkService) 31 public function __construct(BookmarkServiceInterface $bookmarkService)
27 { 32 {
28 $this->bookmarkService = $bookmarkService; 33 $this->bookmarkService = $bookmarkService;
29 } 34 }
@@ -31,13 +36,13 @@ class BookmarkInitializer
31 /** 36 /**
32 * Initialize the data store with default bookmarks 37 * Initialize the data store with default bookmarks
33 */ 38 */
34 public function initialize() 39 public function initialize(): void
35 { 40 {
36 $bookmark = new Bookmark(); 41 $bookmark = new Bookmark();
37 $bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)')); 42 $bookmark->setTitle('Calm Jazz Music - YouTube ' . t('(private bookmark with thumbnail demo)'));
38 $bookmark->setUrl('https://vimeo.com/153493904'); 43 $bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c');
39 $bookmark->setDescription(t( 44 $bookmark->setDescription(t(
40'Shaarli will automatically pick up the thumbnail for links to a variety of websites. 45 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.
41 46
42Explore your new Shaarli instance by trying out controls and menus. 47Explore your new Shaarli instance by trying out controls and menus.
43Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli. 48Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli.
@@ -52,7 +57,7 @@ Now you can edit or delete the default shaares.
52 $bookmark = new Bookmark(); 57 $bookmark = new Bookmark();
53 $bookmark->setTitle(t('Note: Shaare descriptions')); 58 $bookmark->setTitle(t('Note: Shaare descriptions'));
54 $bookmark->setDescription(t( 59 $bookmark->setDescription(t(
55'Adding a shaare without entering a URL creates a text-only "note" post such as this one. 60 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.
56This note is private, so you are the only one able to see it while logged in. 61This note is private, so you are the only one able to see it while logged in.
57 62
58You can use this to keep notes, post articles, code snippets, and much more. 63You can use this to keep notes, post articles, code snippets, and much more.
@@ -89,7 +94,7 @@ Markdown also supports tables:
89 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service') 94 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service')
90 ); 95 );
91 $bookmark->setDescription(t( 96 $bookmark->setDescription(t(
92'Welcome to Shaarli! 97 'Welcome to Shaarli!
93 98
94Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately. 99Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately.
95You can add a description to your bookmarks, such as this one, and tag them. 100You can add a description to your bookmarks, such as this one, and tag them.
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php
index b9b483eb..08cdbb4e 100644
--- a/application/bookmark/BookmarkServiceInterface.php
+++ b/application/bookmark/BookmarkServiceInterface.php
@@ -1,79 +1,73 @@
1<?php 1<?php
2 2
3namespace Shaarli\Bookmark; 3declare(strict_types=1);
4 4
5namespace Shaarli\Bookmark;
5 6
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 7use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7use Shaarli\Bookmark\Exception\NotWritableDataStoreException; 8use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
8use Shaarli\Config\ConfigManager;
9use Shaarli\History;
10 9
11/** 10/**
12 * Class BookmarksService 11 * Class BookmarksService
13 * 12 *
14 * This is the entry point to manipulate the bookmark DB. 13 * This is the entry point to manipulate the bookmark DB.
14 *
15 * Regarding return types of a list of bookmarks, it can either be an array or an ArrayAccess implementation,
16 * so until PHP 8.0 is the minimal supported version with union return types it cannot be explicitly added.
15 */ 17 */
16interface BookmarkServiceInterface 18interface BookmarkServiceInterface
17{ 19{
18 /** 20 /**
19 * BookmarksService constructor.
20 *
21 * @param ConfigManager $conf instance
22 * @param History $history instance
23 * @param bool $isLoggedIn true if the current user is logged in
24 */
25 public function __construct(ConfigManager $conf, History $history, $isLoggedIn);
26
27 /**
28 * Find a bookmark by hash 21 * Find a bookmark by hash
29 * 22 *
30 * @param string $hash 23 * @param string $hash Bookmark's hash
24 * @param string|null $privateKey Optional key used to access private links while logged out
31 * 25 *
32 * @return mixed 26 * @return Bookmark
33 * 27 *
34 * @throws \Exception 28 * @throws \Exception
35 */ 29 */
36 public function findByHash($hash); 30 public function findByHash(string $hash, string $privateKey = null);
37 31
38 /** 32 /**
39 * @param $url 33 * @param $url
40 * 34 *
41 * @return Bookmark|null 35 * @return Bookmark|null
42 */ 36 */
43 public function findByUrl($url); 37 public function findByUrl(string $url): ?Bookmark;
44 38
45 /** 39 /**
46 * Search bookmarks 40 * Search bookmarks
47 * 41 *
48 * @param mixed $request 42 * @param array $request
49 * @param string $visibility 43 * @param ?string $visibility
50 * @param bool $caseSensitive 44 * @param bool $caseSensitive
51 * @param bool $untaggedOnly 45 * @param bool $untaggedOnly
52 * @param bool $ignoreSticky 46 * @param bool $ignoreSticky
53 * 47 *
54 * @return Bookmark[] 48 * @return Bookmark[]
55 */ 49 */
56 public function search( 50 public function search(
57 $request = [], 51 array $request = [],
58 $visibility = null, 52 string $visibility = null,
59 $caseSensitive = false, 53 bool $caseSensitive = false,
60 $untaggedOnly = false, 54 bool $untaggedOnly = false,
61 bool $ignoreSticky = false 55 bool $ignoreSticky = false
62 ); 56 );
63 57
64 /** 58 /**
65 * Get a single bookmark by its ID. 59 * Get a single bookmark by its ID.
66 * 60 *
67 * @param int $id Bookmark ID 61 * @param int $id Bookmark ID
68 * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an 62 * @param ?string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
69 * exception 63 * exception
70 * 64 *
71 * @return Bookmark 65 * @return Bookmark
72 * 66 *
73 * @throws BookmarkNotFoundException 67 * @throws BookmarkNotFoundException
74 * @throws \Exception 68 * @throws \Exception
75 */ 69 */
76 public function get($id, $visibility = null); 70 public function get(int $id, string $visibility = null);
77 71
78 /** 72 /**
79 * Updates an existing bookmark (depending on its ID). 73 * Updates an existing bookmark (depending on its ID).
@@ -86,7 +80,7 @@ interface BookmarkServiceInterface
86 * @throws BookmarkNotFoundException 80 * @throws BookmarkNotFoundException
87 * @throws \Exception 81 * @throws \Exception
88 */ 82 */
89 public function set($bookmark, $save = true); 83 public function set(Bookmark $bookmark, bool $save = true): Bookmark;
90 84
91 /** 85 /**
92 * Adds a new bookmark (the ID must be empty). 86 * Adds a new bookmark (the ID must be empty).
@@ -98,7 +92,7 @@ interface BookmarkServiceInterface
98 * 92 *
99 * @throws \Exception 93 * @throws \Exception
100 */ 94 */
101 public function add($bookmark, $save = true); 95 public function add(Bookmark $bookmark, bool $save = true): Bookmark;
102 96
103 /** 97 /**
104 * Adds or updates a bookmark depending on its ID: 98 * Adds or updates a bookmark depending on its ID:
@@ -112,7 +106,7 @@ interface BookmarkServiceInterface
112 * 106 *
113 * @throws \Exception 107 * @throws \Exception
114 */ 108 */
115 public function addOrSet($bookmark, $save = true); 109 public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark;
116 110
117 /** 111 /**
118 * Deletes a bookmark. 112 * Deletes a bookmark.
@@ -122,65 +116,72 @@ interface BookmarkServiceInterface
122 * 116 *
123 * @throws \Exception 117 * @throws \Exception
124 */ 118 */
125 public function remove($bookmark, $save = true); 119 public function remove(Bookmark $bookmark, bool $save = true): void;
126 120
127 /** 121 /**
128 * Get a single bookmark by its ID. 122 * Get a single bookmark by its ID.
129 * 123 *
130 * @param int $id Bookmark ID 124 * @param int $id Bookmark ID
131 * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an 125 * @param ?string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
132 * exception 126 * exception
133 * 127 *
134 * @return bool 128 * @return bool
135 */ 129 */
136 public function exists($id, $visibility = null); 130 public function exists(int $id, string $visibility = null): bool;
137 131
138 /** 132 /**
139 * Return the number of available bookmarks for given visibility. 133 * Return the number of available bookmarks for given visibility.
140 * 134 *
141 * @param string $visibility public|private|all 135 * @param ?string $visibility public|private|all
142 * 136 *
143 * @return int Number of bookmarks 137 * @return int Number of bookmarks
144 */ 138 */
145 public function count($visibility = null); 139 public function count(string $visibility = null): int;
146 140
147 /** 141 /**
148 * Write the datastore. 142 * Write the datastore.
149 * 143 *
150 * @throws NotWritableDataStoreException 144 * @throws NotWritableDataStoreException
151 */ 145 */
152 public function save(); 146 public function save(): void;
153 147
154 /** 148 /**
155 * Returns the list tags appearing in the bookmarks with the given tags 149 * Returns the list tags appearing in the bookmarks with the given tags
156 * 150 *
157 * @param array $filteringTags tags selecting the bookmarks to consider 151 * @param array|null $filteringTags tags selecting the bookmarks to consider
158 * @param string $visibility process only all/private/public bookmarks 152 * @param string|null $visibility process only all/private/public bookmarks
159 * 153 *
160 * @return array tag => bookmarksCount 154 * @return array tag => bookmarksCount
161 */ 155 */
162 public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all'); 156 public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
163 157
164 /** 158 /**
165 * Returns the list of days containing articles (oldest first) 159 * Return a list of bookmark matching provided period of time.
160 * It also update directly previous and next date outside of given period found in the datastore.
161 *
162 * @param \DateTimeInterface $from Starting date.
163 * @param \DateTimeInterface $to Ending date.
164 * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from.
165 * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to.
166 * 166 *
167 * @return array containing days (in format YYYYMMDD). 167 * @return array List of bookmarks matching provided period of time.
168 */ 168 */
169 public function days(); 169 public function findByDate(
170 \DateTimeInterface $from,
171 \DateTimeInterface $to,
172 ?\DateTimeInterface &$previous,
173 ?\DateTimeInterface &$next
174 ): array;
170 175
171 /** 176 /**
172 * Returns the list of articles for a given day. 177 * Returns the latest bookmark by creation date.
173 * 178 *
174 * @param string $request day to filter. Format: YYYYMMDD. 179 * @return Bookmark|null Found Bookmark or null if the datastore is empty.
175 *
176 * @return Bookmark[] list of shaare found.
177 *
178 * @throws BookmarkNotFoundException
179 */ 180 */
180 public function filterDay($request); 181 public function getLatest(): ?Bookmark;
181 182
182 /** 183 /**
183 * Creates the default database after a fresh install. 184 * Creates the default database after a fresh install.
184 */ 185 */
185 public function initialize(); 186 public function initialize(): void;
186} 187}
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index e7af4d55..d65e97ed 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -66,16 +66,19 @@ function html_extract_tag($tag, $html)
66{ 66{
67 $propertiesKey = ['property', 'name', 'itemprop']; 67 $propertiesKey = ['property', 'name', 'itemprop'];
68 $properties = implode('|', $propertiesKey); 68 $properties = implode('|', $propertiesKey);
69 // Try to retrieve OpenGraph image. 69 // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
70 $ogRegex = '#<meta[^>]+(?:'. $properties .')=["\']?(?:og:)?'. $tag .'["\'\s][^>]*content=["\']?(.*?)["\'/>]#'; 70 $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
71 // Try to retrieve OpenGraph tag.
72 $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#';
71 // If the attributes are not in the order property => content (e.g. Github) 73 // If the attributes are not in the order property => content (e.g. Github)
72 // New regex to keep this readable... more or less. 74 // New regex to keep this readable... more or less.
73 $ogRegexReverse = '#<meta[^>]+content=["\']([^"\']+)[^>]+(?:'. $properties .')=["\']?(?:og)?:'. $tag .'["\'\s/>]#'; 75 $ogRegexReverse = '#<meta[^>]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
74 76
75 if (preg_match($ogRegex, $html, $matches) > 0 77 if (
78 preg_match($ogRegex, $html, $matches) > 0
76 || preg_match($ogRegexReverse, $html, $matches) > 0 79 || preg_match($ogRegexReverse, $html, $matches) > 0
77 ) { 80 ) {
78 return $matches[1]; 81 return $matches[2];
79 } 82 }
80 83
81 return false; 84 return false;
@@ -114,7 +117,7 @@ function hashtag_autolink($description, $indexUrl = '')
114 * \p{Mn} - any non marking space (accents, umlauts, etc) 117 * \p{Mn} - any non marking space (accents, umlauts, etc)
115 */ 118 */
116 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 119 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
117 $replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>'; 120 $replacement = '$1<a href="' . $indexUrl . './add-tag/$2" title="Hashtag $2">#$2</a>';
118 return preg_replace($regex, $replacement, $description); 121 return preg_replace($regex, $replacement, $description);
119} 122}
120 123
@@ -136,12 +139,17 @@ function space2nbsp($text)
136 * 139 *
137 * @param string $description shaare's description. 140 * @param string $description shaare's description.
138 * @param string $indexUrl URL to Shaarli's index. 141 * @param string $indexUrl URL to Shaarli's index.
139 142 * @param bool $autolink Turn on/off automatic linkifications of URLs and hashtags
143 *
140 * @return string formatted description. 144 * @return string formatted description.
141 */ 145 */
142function format_description($description, $indexUrl = '') 146function format_description($description, $indexUrl = '', $autolink = true)
143{ 147{
144 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl))); 148 if ($autolink) {
149 $description = hashtag_autolink(text2clickable($description), $indexUrl);
150 }
151
152 return nl2br(space2nbsp($description));
145} 153}
146 154
147/** 155/**
@@ -169,3 +177,49 @@ function is_note($linkUrl)
169{ 177{
170 return isset($linkUrl[0]) && $linkUrl[0] === '?'; 178 return isset($linkUrl[0]) && $linkUrl[0] === '?';
171} 179}
180
181/**
182 * Extract an array of tags from a given tag string, with provided separator.
183 *
184 * @param string|null $tags String containing a list of tags separated by $separator.
185 * @param string $separator Shaarli's default: ' ' (whitespace)
186 *
187 * @return array List of tags
188 */
189function tags_str2array(?string $tags, string $separator): array
190{
191 // For whitespaces, we use the special \s regex character
192 $separator = $separator === ' ' ? '\s' : $separator;
193
194 return preg_split('/\s*' . $separator . '+\s*/', trim($tags) ?? '', -1, PREG_SPLIT_NO_EMPTY);
195}
196
197/**
198 * Return a tag string with provided separator from a list of tags.
199 * Note that given array is clean up by tags_filter().
200 *
201 * @param array|null $tags List of tags
202 * @param string $separator
203 *
204 * @return string
205 */
206function tags_array2str(?array $tags, string $separator): string
207{
208 return implode($separator, tags_filter($tags, $separator));
209}
210
211/**
212 * Clean an array of tags: trim + remove empty entries
213 *
214 * @param array|null $tags List of tags
215 * @param string $separator
216 *
217 * @return array
218 */
219function tags_filter(?array $tags, string $separator): array
220{
221 $trimDefault = " \t\n\r\0\x0B";
222 return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string {
223 return trim($entry, $trimDefault . $separator);
224 }, $tags ?? [])));
225}
diff --git a/application/bookmark/exception/BookmarkNotFoundException.php b/application/bookmark/exception/BookmarkNotFoundException.php
index 827a3d35..a91d1efa 100644
--- a/application/bookmark/exception/BookmarkNotFoundException.php
+++ b/application/bookmark/exception/BookmarkNotFoundException.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Bookmark\Exception; 3namespace Shaarli\Bookmark\Exception;
3 4
4use Exception; 5use 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
4namespace Shaarli\Bookmark\Exception; 3namespace Shaarli\Bookmark\Exception;
5 4
6 5class EmptyDataStoreException extends \Exception
7class 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
4namespace Shaarli\Bookmark\Exception; 3namespace Shaarli\Bookmark\Exception;
5 4
6
7class NotWritableDataStoreException extends \Exception 5class 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
2namespace Shaarli\Config; 3namespace 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
2namespace Shaarli\Config; 3namespace Shaarli\Config;
3 4
4use Shaarli\Config\Exception\MissingFieldConfigException; 5use 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
2namespace Shaarli\Config; 3namespace 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 */
82function validate_plugin_order($formData) 82function 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
4namespace Shaarli\Config\Exception; 3namespace 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
4namespace Shaarli\Config\Exception; 3namespace Shaarli\Config\Exception;
5 4
6/** 5/**
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php
index 55bb51b5..f0234eca 100644
--- a/application/container/ContainerBuilder.php
+++ b/application/container/ContainerBuilder.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Container; 5namespace Shaarli\Container;
6 6
7use malkusch\lock\mutex\FlockMutex;
8use Psr\Log\LoggerInterface;
7use Shaarli\Bookmark\BookmarkFileService; 9use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\BookmarkServiceInterface; 10use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 11use Shaarli\Config\ConfigManager;
@@ -13,6 +15,7 @@ use Shaarli\Front\Controller\Visitor\ErrorController;
13use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; 15use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
14use Shaarli\History; 16use Shaarli\History;
15use Shaarli\Http\HttpAccess; 17use Shaarli\Http\HttpAccess;
18use Shaarli\Http\MetadataRetriever;
16use Shaarli\Netscape\NetscapeBookmarkUtils; 19use Shaarli\Netscape\NetscapeBookmarkUtils;
17use Shaarli\Plugin\PluginManager; 20use Shaarli\Plugin\PluginManager;
18use Shaarli\Render\PageBuilder; 21use Shaarli\Render\PageBuilder;
@@ -47,6 +50,9 @@ class ContainerBuilder
47 /** @var LoginManager */ 50 /** @var LoginManager */
48 protected $login; 51 protected $login;
49 52
53 /** @var LoggerInterface */
54 protected $logger;
55
50 /** @var string|null */ 56 /** @var string|null */
51 protected $basePath = null; 57 protected $basePath = null;
52 58
@@ -54,12 +60,14 @@ class ContainerBuilder
54 ConfigManager $conf, 60 ConfigManager $conf,
55 SessionManager $session, 61 SessionManager $session,
56 CookieManager $cookieManager, 62 CookieManager $cookieManager,
57 LoginManager $login 63 LoginManager $login,
64 LoggerInterface $logger
58 ) { 65 ) {
59 $this->conf = $conf; 66 $this->conf = $conf;
60 $this->session = $session; 67 $this->session = $session;
61 $this->login = $login; 68 $this->login = $login;
62 $this->cookieManager = $cookieManager; 69 $this->cookieManager = $cookieManager;
70 $this->logger = $logger;
63 } 71 }
64 72
65 public function build(): ShaarliContainer 73 public function build(): ShaarliContainer
@@ -70,6 +78,7 @@ class ContainerBuilder
70 $container['sessionManager'] = $this->session; 78 $container['sessionManager'] = $this->session;
71 $container['cookieManager'] = $this->cookieManager; 79 $container['cookieManager'] = $this->cookieManager;
72 $container['loginManager'] = $this->login; 80 $container['loginManager'] = $this->login;
81 $container['logger'] = $this->logger;
73 $container['basePath'] = $this->basePath; 82 $container['basePath'] = $this->basePath;
74 83
75 $container['plugins'] = function (ShaarliContainer $container): PluginManager { 84 $container['plugins'] = function (ShaarliContainer $container): PluginManager {
@@ -84,14 +93,20 @@ class ContainerBuilder
84 return new BookmarkFileService( 93 return new BookmarkFileService(
85 $container->conf, 94 $container->conf,
86 $container->history, 95 $container->history,
96 new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
87 $container->loginManager->isLoggedIn() 97 $container->loginManager->isLoggedIn()
88 ); 98 );
89 }; 99 };
90 100
101 $container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever {
102 return new MetadataRetriever($container->conf, $container->httpAccess);
103 };
104
91 $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder { 105 $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
92 return new PageBuilder( 106 return new PageBuilder(
93 $container->conf, 107 $container->conf,
94 $container->sessionManager->getSession(), 108 $container->sessionManager->getSession(),
109 $container->logger,
95 $container->bookmarkService, 110 $container->bookmarkService,
96 $container->sessionManager->generateToken(), 111 $container->sessionManager->generateToken(),
97 $container->loginManager->isLoggedIn() 112 $container->loginManager->isLoggedIn()
@@ -143,7 +158,7 @@ class ContainerBuilder
143 158
144 $container['updater'] = function (ShaarliContainer $container): Updater { 159 $container['updater'] = function (ShaarliContainer $container): Updater {
145 return new Updater( 160 return new Updater(
146 UpdaterUtils::read_updates_file($container->conf->get('resource.updates')), 161 UpdaterUtils::readUpdatesFile($container->conf->get('resource.updates')),
147 $container->bookmarkService, 162 $container->bookmarkService,
148 $container->conf, 163 $container->conf,
149 $container->loginManager->isLoggedIn() 164 $container->loginManager->isLoggedIn()
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php
index 66e669aa..3e5bd252 100644
--- a/application/container/ShaarliContainer.php
+++ b/application/container/ShaarliContainer.php
@@ -4,12 +4,14 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Container; 5namespace Shaarli\Container;
6 6
7use Psr\Log\LoggerInterface;
7use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
9use Shaarli\Feed\FeedBuilder; 10use Shaarli\Feed\FeedBuilder;
10use Shaarli\Formatter\FormatterFactory; 11use Shaarli\Formatter\FormatterFactory;
11use Shaarli\History; 12use Shaarli\History;
12use Shaarli\Http\HttpAccess; 13use Shaarli\Http\HttpAccess;
14use Shaarli\Http\MetadataRetriever;
13use Shaarli\Netscape\NetscapeBookmarkUtils; 15use Shaarli\Netscape\NetscapeBookmarkUtils;
14use Shaarli\Plugin\PluginManager; 16use Shaarli\Plugin\PluginManager;
15use Shaarli\Render\PageBuilder; 17use 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
2namespace Shaarli\Exceptions; 3namespace Shaarli\Exceptions;
3 4
4use Exception; 5use Exception;
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php
index f6def630..ed62af26 100644
--- a/application/feed/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Feed; 3namespace Shaarli\Feed;
3 4
4use DateTime; 5use DateTime;
@@ -102,19 +103,19 @@ class FeedBuilder
102 } 103 }
103 104
104 // Optionally filter the results: 105 // Optionally filter the results:
105 $linksToDisplay = $this->linkDB->search($userInput, null, false, false, true); 106 $linksToDisplay = $this->linkDB->search($userInput ?? [], null, false, false, true);
106 107
107 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput); 108 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
108 109
109 // Can't use array_keys() because $link is a LinkDB instance and not a real array. 110 // Can't use array_keys() because $link is a LinkDB instance and not a real array.
110 $keys = array(); 111 $keys = [];
111 foreach ($linksToDisplay as $key => $value) { 112 foreach ($linksToDisplay as $key => $value) {
112 $keys[] = $key; 113 $keys[] = $key;
113 } 114 }
114 115
115 $pageaddr = escape(index_url($this->serverInfo)); 116 $pageaddr = escape(index_url($this->serverInfo));
116 $this->formatter->addContextData('index_url', $pageaddr); 117 $this->formatter->addContextData('index_url', $pageaddr);
117 $linkDisplayed = array(); 118 $linkDisplayed = [];
118 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { 119 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
119 $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr); 120 $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
120 } 121 }
@@ -176,9 +177,9 @@ class FeedBuilder
176 $data = $this->formatter->format($link); 177 $data = $this->formatter->format($link);
177 $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl']; 178 $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl'];
178 if ($this->usePermalinks === true) { 179 if ($this->usePermalinks === true) {
179 $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>'; 180 $permalink = '<a href="' . $data['url'] . '" title="' . t('Direct link') . '">' . t('Direct link') . '</a>';
180 } else { 181 } else {
181 $permalink = '<a href="'. $data['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>'; 182 $permalink = '<a href="' . $data['guid'] . '" title="' . t('Permalink') . '">' . t('Permalink') . '</a>';
182 } 183 }
183 $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink; 184 $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
184 185
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
index 9d4a0fa0..7e0afafc 100644
--- a/application/formatter/BookmarkDefaultFormatter.php
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -12,10 +12,13 @@ namespace Shaarli\Formatter;
12 */ 12 */
13class BookmarkDefaultFormatter extends BookmarkFormatter 13class BookmarkDefaultFormatter extends BookmarkFormatter
14{ 14{
15 protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT';
16 protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|';
17
15 /** 18 /**
16 * @inheritdoc 19 * @inheritdoc
17 */ 20 */
18 public function formatTitle($bookmark) 21 protected function formatTitle($bookmark)
19 { 22 {
20 return escape($bookmark->getTitle()); 23 return escape($bookmark->getTitle());
21 } 24 }
@@ -23,10 +26,33 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
23 /** 26 /**
24 * @inheritdoc 27 * @inheritdoc
25 */ 28 */
26 public function formatDescription($bookmark) 29 protected function formatTitleHtml($bookmark)
30 {
31 $title = $this->tokenizeSearchHighlightField(
32 $bookmark->getTitle() ?? '',
33 $bookmark->getAdditionalContentEntry('search_highlight')['title'] ?? []
34 );
35
36 return $this->replaceTokens(escape($title));
37 }
38
39 /**
40 * @inheritdoc
41 */
42 protected function formatDescription($bookmark)
27 { 43 {
28 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; 44 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
29 return format_description(escape($bookmark->getDescription()), $indexUrl); 45 $description = $this->tokenizeSearchHighlightField(
46 $bookmark->getDescription() ?? '',
47 $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
48 );
49 $description = format_description(
50 escape($description),
51 $indexUrl,
52 $this->conf->get('formatter_settings.autolink', true)
53 );
54
55 return $this->replaceTokens($description);
30 } 56 }
31 57
32 /** 58 /**
@@ -40,15 +66,36 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
40 /** 66 /**
41 * @inheritdoc 67 * @inheritdoc
42 */ 68 */
43 public function formatTagString($bookmark) 69 protected function formatTagListHtml($bookmark)
44 { 70 {
45 return implode(' ', $this->formatTagList($bookmark)); 71 $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
72 if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) {
73 return $this->formatTagList($bookmark);
74 }
75
76 $tags = $this->tokenizeSearchHighlightField(
77 $bookmark->getTagsString($tagsSeparator),
78 $bookmark->getAdditionalContentEntry('search_highlight')['tags']
79 );
80 $tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator));
81 $tags = escape($tags);
82 $tags = $this->replaceTokensArray($tags);
83
84 return $tags;
46 } 85 }
47 86
48 /** 87 /**
49 * @inheritdoc 88 * @inheritdoc
50 */ 89 */
51 public function formatUrl($bookmark) 90 protected function formatTagString($bookmark)
91 {
92 return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark));
93 }
94
95 /**
96 * @inheritdoc
97 */
98 protected function formatUrl($bookmark)
52 { 99 {
53 if ($bookmark->isNote() && isset($this->contextData['index_url'])) { 100 if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
54 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); 101 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
@@ -80,8 +127,89 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
80 /** 127 /**
81 * @inheritdoc 128 * @inheritdoc
82 */ 129 */
130 protected function formatUrlHtml($bookmark)
131 {
132 $url = $this->tokenizeSearchHighlightField(
133 $bookmark->getUrl() ?? '',
134 $bookmark->getAdditionalContentEntry('search_highlight')['url'] ?? []
135 );
136
137 return $this->replaceTokens(escape($url));
138 }
139
140 /**
141 * @inheritdoc
142 */
83 protected function formatThumbnail($bookmark) 143 protected function formatThumbnail($bookmark)
84 { 144 {
85 return escape($bookmark->getThumbnail()); 145 return escape($bookmark->getThumbnail());
86 } 146 }
147
148 /**
149 * Insert search highlight token in provided field content based on a list of search result positions
150 *
151 * @param string $fieldContent
152 * @param array|null $positions List of of search results with 'start' and 'end' positions.
153 *
154 * @return string Updated $fieldContent.
155 */
156 protected function tokenizeSearchHighlightField(string $fieldContent, ?array $positions): string
157 {
158 if (empty($positions)) {
159 return $fieldContent;
160 }
161
162 $insertedTokens = 0;
163 $tokenLength = strlen(static::SEARCH_HIGHLIGHT_OPEN);
164 foreach ($positions as $position) {
165 $position = [
166 'start' => $position['start'] + ($insertedTokens * $tokenLength),
167 'end' => $position['end'] + ($insertedTokens * $tokenLength),
168 ];
169
170 $content = mb_substr($fieldContent, 0, $position['start']);
171 $content .= static::SEARCH_HIGHLIGHT_OPEN;
172 $content .= mb_substr($fieldContent, $position['start'], $position['end'] - $position['start']);
173 $content .= static::SEARCH_HIGHLIGHT_CLOSE;
174 $content .= mb_substr($fieldContent, $position['end']);
175
176 $fieldContent = $content;
177
178 $insertedTokens += 2;
179 }
180
181 return $fieldContent;
182 }
183
184 /**
185 * Replace search highlight tokens with HTML highlighted span.
186 *
187 * @param string $fieldContent
188 *
189 * @return string updated content.
190 */
191 protected function replaceTokens(string $fieldContent): string
192 {
193 return str_replace(
194 [static::SEARCH_HIGHLIGHT_OPEN, static::SEARCH_HIGHLIGHT_CLOSE],
195 ['<span class="search-highlight">', '</span>'],
196 $fieldContent
197 );
198 }
199
200 /**
201 * Apply replaceTokens to an array of content strings.
202 *
203 * @param string[] $fieldContents
204 *
205 * @return array
206 */
207 protected function replaceTokensArray(array $fieldContents): array
208 {
209 foreach ($fieldContents as &$entry) {
210 $entry = $this->replaceTokens($entry);
211 }
212
213 return $fieldContents;
214 }
87} 215}
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php
index 0042dafe..124ce78b 100644
--- a/application/formatter/BookmarkFormatter.php
+++ b/application/formatter/BookmarkFormatter.php
@@ -2,7 +2,7 @@
2 2
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use DateTime; 5use DateTimeInterface;
6use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8 8
@@ -11,6 +11,29 @@ use Shaarli\Config\ConfigManager;
11 * 11 *
12 * Abstract class processing all bookmark attributes through methods designed to be overridden. 12 * Abstract class processing all bookmark attributes through methods designed to be overridden.
13 * 13 *
14 * List of available formatted fields:
15 * - id ID
16 * - shorturl Unique identifier, used in permalinks
17 * - url URL, can be altered in some way, e.g. passing through an HTTP reverse proxy
18 * - real_url (legacy) same as `url`
19 * - url_html URL to be displayed in HTML content (it can contain HTML tags)
20 * - title Title
21 * - title_html Title to be displayed in HTML content (it can contain HTML tags)
22 * - description Description content. It most likely contains HTML tags
23 * - thumbnail Thumbnail: path to local cache file, false if there is none, null if hasn't been retrieved
24 * - taglist List of tags (array)
25 * - taglist_urlencoded List of tags (array) URL encoded: it must be used to create a link to a URL containing a tag
26 * - taglist_html List of tags (array) to be displayed in HTML content (it can contain HTML tags)
27 * - tags Tags separated by a single whitespace
28 * - tags_urlencoded Tags separated by a single whitespace, URL encoded: must be used to create a link
29 * - sticky Is sticky (bool)
30 * - private Is private (bool)
31 * - class Additional CSS class
32 * - created Creation DateTime
33 * - updated Last edit DateTime
34 * - timestamp Creation timestamp
35 * - updated_timestamp Last edit timestamp
36 *
14 * @package Shaarli\Formatter 37 * @package Shaarli\Formatter
15 */ 38 */
16abstract class BookmarkFormatter 39abstract class BookmarkFormatter
@@ -55,13 +78,16 @@ abstract class BookmarkFormatter
55 $out['shorturl'] = $this->formatShortUrl($bookmark); 78 $out['shorturl'] = $this->formatShortUrl($bookmark);
56 $out['url'] = $this->formatUrl($bookmark); 79 $out['url'] = $this->formatUrl($bookmark);
57 $out['real_url'] = $this->formatRealUrl($bookmark); 80 $out['real_url'] = $this->formatRealUrl($bookmark);
81 $out['url_html'] = $this->formatUrlHtml($bookmark);
58 $out['title'] = $this->formatTitle($bookmark); 82 $out['title'] = $this->formatTitle($bookmark);
83 $out['title_html'] = $this->formatTitleHtml($bookmark);
59 $out['description'] = $this->formatDescription($bookmark); 84 $out['description'] = $this->formatDescription($bookmark);
60 $out['thumbnail'] = $this->formatThumbnail($bookmark); 85 $out['thumbnail'] = $this->formatThumbnail($bookmark);
61 $out['urlencoded_taglist'] = $this->formatUrlEncodedTagList($bookmark);
62 $out['taglist'] = $this->formatTagList($bookmark); 86 $out['taglist'] = $this->formatTagList($bookmark);
63 $out['urlencoded_tags'] = $this->formatUrlEncodedTagString($bookmark); 87 $out['taglist_urlencoded'] = $this->formatTagListUrlEncoded($bookmark);
88 $out['taglist_html'] = $this->formatTagListHtml($bookmark);
64 $out['tags'] = $this->formatTagString($bookmark); 89 $out['tags'] = $this->formatTagString($bookmark);
90 $out['tags_urlencoded'] = $this->formatTagStringUrlEncoded($bookmark);
65 $out['sticky'] = $bookmark->isSticky(); 91 $out['sticky'] = $bookmark->isSticky();
66 $out['private'] = $bookmark->isPrivate(); 92 $out['private'] = $bookmark->isPrivate();
67 $out['class'] = $this->formatClass($bookmark); 93 $out['class'] = $this->formatClass($bookmark);
@@ -69,6 +95,7 @@ abstract class BookmarkFormatter
69 $out['updated'] = $this->formatUpdated($bookmark); 95 $out['updated'] = $this->formatUpdated($bookmark);
70 $out['timestamp'] = $this->formatCreatedTimestamp($bookmark); 96 $out['timestamp'] = $this->formatCreatedTimestamp($bookmark);
71 $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark); 97 $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark);
98
72 return $out; 99 return $out;
73 } 100 }
74 101
@@ -136,6 +163,18 @@ abstract class BookmarkFormatter
136 } 163 }
137 164
138 /** 165 /**
166 * Format Url Html: to be displayed in HTML content, it can contains HTML tags.
167 *
168 * @param Bookmark $bookmark instance
169 *
170 * @return string formatted Url HTML
171 */
172 protected function formatUrlHtml($bookmark)
173 {
174 return $this->formatUrl($bookmark);
175 }
176
177 /**
139 * Format Title 178 * Format Title
140 * 179 *
141 * @param Bookmark $bookmark instance 180 * @param Bookmark $bookmark instance
@@ -148,6 +187,18 @@ abstract class BookmarkFormatter
148 } 187 }
149 188
150 /** 189 /**
190 * Format Title HTML: to be displayed in HTML content, it can contains HTML tags.
191 *
192 * @param Bookmark $bookmark instance
193 *
194 * @return string formatted Title
195 */
196 protected function formatTitleHtml($bookmark)
197 {
198 return $bookmark->getTitle();
199 }
200
201 /**
151 * Format Description 202 * Format Description
152 * 203 *
153 * @param Bookmark $bookmark instance 204 * @param Bookmark $bookmark instance
@@ -190,12 +241,24 @@ abstract class BookmarkFormatter
190 * 241 *
191 * @return array formatted Tags 242 * @return array formatted Tags
192 */ 243 */
193 protected function formatUrlEncodedTagList($bookmark) 244 protected function formatTagListUrlEncoded($bookmark)
194 { 245 {
195 return array_map('urlencode', $this->filterTagList($bookmark->getTags())); 246 return array_map('urlencode', $this->filterTagList($bookmark->getTags()));
196 } 247 }
197 248
198 /** 249 /**
250 * Format Tags HTML: to be displayed in HTML content, it can contains HTML tags.
251 *
252 * @param Bookmark $bookmark instance
253 *
254 * @return array formatted Tags
255 */
256 protected function formatTagListHtml($bookmark)
257 {
258 return $this->formatTagList($bookmark);
259 }
260
261 /**
199 * Format TagString 262 * Format TagString
200 * 263 *
201 * @param Bookmark $bookmark instance 264 * @param Bookmark $bookmark instance
@@ -204,7 +267,7 @@ abstract class BookmarkFormatter
204 */ 267 */
205 protected function formatTagString($bookmark) 268 protected function formatTagString($bookmark)
206 { 269 {
207 return implode(' ', $this->formatTagList($bookmark)); 270 return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark));
208 } 271 }
209 272
210 /** 273 /**
@@ -214,9 +277,9 @@ abstract class BookmarkFormatter
214 * 277 *
215 * @return string formatted TagString 278 * @return string formatted TagString
216 */ 279 */
217 protected function formatUrlEncodedTagString($bookmark) 280 protected function formatTagStringUrlEncoded($bookmark)
218 { 281 {
219 return implode(' ', $this->formatUrlEncodedTagList($bookmark)); 282 return implode(' ', $this->formatTagListUrlEncoded($bookmark));
220 } 283 }
221 284
222 /** 285 /**
@@ -237,7 +300,7 @@ abstract class BookmarkFormatter
237 * 300 *
238 * @param Bookmark $bookmark instance 301 * @param Bookmark $bookmark instance
239 * 302 *
240 * @return DateTime instance 303 * @return DateTimeInterface instance
241 */ 304 */
242 protected function formatCreated(Bookmark $bookmark) 305 protected function formatCreated(Bookmark $bookmark)
243 { 306 {
@@ -249,7 +312,7 @@ abstract class BookmarkFormatter
249 * 312 *
250 * @param Bookmark $bookmark instance 313 * @param Bookmark $bookmark instance
251 * 314 *
252 * @return DateTime instance 315 * @return DateTimeInterface instance
253 */ 316 */
254 protected function formatUpdated(Bookmark $bookmark) 317 protected function formatUpdated(Bookmark $bookmark)
255 { 318 {
@@ -288,6 +351,7 @@ abstract class BookmarkFormatter
288 351
289 /** 352 /**
290 * Format tag list, e.g. remove private tags if the user is not logged in. 353 * Format tag list, e.g. remove private tags if the user is not logged in.
354 * TODO: this method is called multiple time to format tags, the result should be cached.
291 * 355 *
292 * @param array $tags 356 * @param array $tags
293 * 357 *
diff --git a/application/formatter/BookmarkMarkdownExtraFormatter.php b/application/formatter/BookmarkMarkdownExtraFormatter.php
new file mode 100644
index 00000000..0694b23f
--- /dev/null
+++ b/application/formatter/BookmarkMarkdownExtraFormatter.php
@@ -0,0 +1,24 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class BookmarkMarkdownExtraFormatter
9 *
10 * Format bookmark description into MarkdownExtra format.
11 *
12 * @see https://michelf.ca/projects/php-markdown/extra/
13 *
14 * @package Shaarli\Formatter
15 */
16class BookmarkMarkdownExtraFormatter extends BookmarkMarkdownFormatter
17{
18 public function __construct(ConfigManager $conf, bool $isLoggedIn)
19 {
20 parent::__construct($conf, $isLoggedIn);
21
22 $this->parsedown = new \ParsedownExtra();
23 }
24}
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
index 5d244d4c..ee4e8dca 100644
--- a/application/formatter/BookmarkMarkdownFormatter.php
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -16,7 +16,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
16 /** 16 /**
17 * When this tag is present in a bookmark, its description should not be processed with Markdown 17 * When this tag is present in a bookmark, its description should not be processed with Markdown
18 */ 18 */
19 const NO_MD_TAG = 'nomarkdown'; 19 public const NO_MD_TAG = 'nomarkdown';
20 20
21 /** @var \Parsedown instance */ 21 /** @var \Parsedown instance */
22 protected $parsedown; 22 protected $parsedown;
@@ -56,7 +56,10 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
56 return parent::formatDescription($bookmark); 56 return parent::formatDescription($bookmark);
57 } 57 }
58 58
59 $processedDescription = $bookmark->getDescription(); 59 $processedDescription = $this->tokenizeSearchHighlightField(
60 $bookmark->getDescription() ?? '',
61 $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
62 );
60 $processedDescription = $this->filterProtocols($processedDescription); 63 $processedDescription = $this->filterProtocols($processedDescription);
61 $processedDescription = $this->formatHashTags($processedDescription); 64 $processedDescription = $this->formatHashTags($processedDescription);
62 $processedDescription = $this->reverseEscapedHtml($processedDescription); 65 $processedDescription = $this->reverseEscapedHtml($processedDescription);
@@ -65,9 +68,10 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
65 ->setBreaksEnabled(true) 68 ->setBreaksEnabled(true)
66 ->text($processedDescription); 69 ->text($processedDescription);
67 $processedDescription = $this->sanitizeHtml($processedDescription); 70 $processedDescription = $this->sanitizeHtml($processedDescription);
71 $processedDescription = $this->replaceTokens($processedDescription);
68 72
69 if (!empty($processedDescription)) { 73 if (!empty($processedDescription)) {
70 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; 74 $processedDescription = '<div class="markdown">' . $processedDescription . '</div>';
71 } 75 }
72 76
73 return $processedDescription; 77 return $processedDescription;
@@ -106,7 +110,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
106 function ($match) use ($allowedProtocols, $indexUrl) { 110 function ($match) use ($allowedProtocols, $indexUrl) {
107 $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; 111 $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
108 $link .= whitelist_protocols($match[1], $allowedProtocols); 112 $link .= whitelist_protocols($match[1], $allowedProtocols);
109 return ']('. $link.')'; 113 return '](' . $link . ')';
110 }, 114 },
111 $description 115 $description
112 ); 116 );
@@ -133,7 +137,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
133 * \p{Mn} - any non marking space (accents, umlauts, etc) 137 * \p{Mn} - any non marking space (accents, umlauts, etc)
134 */ 138 */
135 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 139 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
136 $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)'; 140 $replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)';
137 141
138 $descriptionLines = explode(PHP_EOL, $description); 142 $descriptionLines = explode(PHP_EOL, $description);
139 $descriptionOut = ''; 143 $descriptionOut = '';
@@ -174,17 +178,17 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
174 */ 178 */
175 protected function sanitizeHtml($description) 179 protected function sanitizeHtml($description)
176 { 180 {
177 $escapeTags = array( 181 $escapeTags = [
178 'script', 182 'script',
179 'style', 183 'style',
180 'link', 184 'link',
181 'iframe', 185 'iframe',
182 'frameset', 186 'frameset',
183 'frame', 187 'frame',
184 ); 188 ];
185 foreach ($escapeTags as $tag) { 189 foreach ($escapeTags as $tag) {
186 $description = preg_replace_callback( 190 $description = preg_replace_callback(
187 '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is', 191 '#<\s*' . $tag . '[^>]*>(.*</\s*' . $tag . '[^>]*>)?#is',
188 function ($match) { 192 function ($match) {
189 return escape($match[0]); 193 return escape($match[0]);
190 }, 194 },
diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php
index bc372273..4ff07cdf 100644
--- a/application/formatter/BookmarkRawFormatter.php
+++ b/application/formatter/BookmarkRawFormatter.php
@@ -10,4 +10,6 @@ namespace Shaarli\Formatter;
10 * 10 *
11 * @package Shaarli\Formatter 11 * @package Shaarli\Formatter
12 */ 12 */
13class BookmarkRawFormatter extends BookmarkFormatter {} 13class BookmarkRawFormatter extends BookmarkFormatter
14{
15}
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php
index a029579f..bb865aed 100644
--- a/application/formatter/FormatterFactory.php
+++ b/application/formatter/FormatterFactory.php
@@ -41,7 +41,7 @@ class FormatterFactory
41 public function getFormatter(string $type = null): BookmarkFormatter 41 public function getFormatter(string $type = null): BookmarkFormatter
42 { 42 {
43 $type = $type ? $type : $this->conf->get('formatter', 'default'); 43 $type = $type ? $type : $this->conf->get('formatter', 'default');
44 $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; 44 $className = '\\Shaarli\\Formatter\\Bookmark' . ucfirst($type) . 'Formatter';
45 if (!class_exists($className)) { 45 if (!class_exists($className)) {
46 $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter'; 46 $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
47 } 47 }
diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php
index d1aa1399..164217f4 100644
--- a/application/front/ShaarliMiddleware.php
+++ b/application/front/ShaarliMiddleware.php
@@ -42,7 +42,8 @@ class ShaarliMiddleware
42 $this->initBasePath($request); 42 $this->initBasePath($request);
43 43
44 try { 44 try {
45 if (!is_file($this->container->conf->getConfigFileExt()) 45 if (
46 !is_file($this->container->conf->getConfigFileExt())
46 && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true) 47 && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
47 ) { 48 ) {
48 return $response->withRedirect($this->container->basePath . '/install'); 49 return $response->withRedirect($this->container->basePath . '/install');
@@ -86,7 +87,8 @@ class ShaarliMiddleware
86 */ 87 */
87 protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool 88 protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool
88 { 89 {
89 if (// if the user isn't logged in 90 if (
91// if the user isn't logged in
90 !$this->container->loginManager->isLoggedIn() 92 !$this->container->loginManager->isLoggedIn()
91 // and Shaarli doesn't have public content... 93 // and Shaarli doesn't have public content...
92 && $this->container->conf->get('privacy.hide_public_links') 94 && $this->container->conf->get('privacy.hide_public_links')
diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php
index e675fcca..dc421661 100644
--- a/application/front/controller/admin/ConfigureController.php
+++ b/application/front/controller/admin/ConfigureController.php
@@ -30,7 +30,7 @@ class ConfigureController extends ShaarliAdminController
30 'theme_available', 30 'theme_available',
31 ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl')) 31 ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl'))
32 ); 32 );
33 $this->assignView('formatter_available', ['default', 'markdown']); 33 $this->assignView('formatter_available', ['default', 'markdown', 'markdownExtra']);
34 list($continents, $cities) = generateTimeZoneData( 34 list($continents, $cities) = generateTimeZoneData(
35 timezone_identifiers_list(), 35 timezone_identifiers_list(),
36 $this->container->conf->get('general.timezone') 36 $this->container->conf->get('general.timezone')
@@ -51,7 +51,10 @@ class ConfigureController extends ShaarliAdminController
51 $this->assignView('languages', Languages::getAvailableLanguages()); 51 $this->assignView('languages', Languages::getAvailableLanguages());
52 $this->assignView('gd_enabled', extension_loaded('gd')); 52 $this->assignView('gd_enabled', extension_loaded('gd'));
53 $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); 53 $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
54 $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); 54 $this->assignView(
55 'pagetitle',
56 t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
57 );
55 58
56 return $response->write($this->render(TemplatePage::CONFIGURE)); 59 return $response->write($this->render(TemplatePage::CONFIGURE));
57 } 60 }
@@ -95,12 +98,15 @@ class ConfigureController extends ShaarliAdminController
95 } 98 }
96 99
97 $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; 100 $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE;
98 if ($thumbnailsMode !== Thumbnailer::MODE_NONE 101 if (
102 $thumbnailsMode !== Thumbnailer::MODE_NONE
99 && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) 103 && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
100 ) { 104 ) {
101 $this->saveWarningMessage( 105 $this->saveWarningMessage(
102 t('You have enabled or changed thumbnails mode.') . 106 t('You have enabled or changed thumbnails mode.') .
103 '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>' 107 '<a href="' . $this->container->basePath . '/admin/thumbnails">' .
108 t('Please synchronize them.') .
109 '</a>'
104 ); 110 );
105 } 111 }
106 $this->container->conf->set('thumbnails.mode', $thumbnailsMode); 112 $this->container->conf->set('thumbnails.mode', $thumbnailsMode);
diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php
index 2be957fa..f01d7e9b 100644
--- a/application/front/controller/admin/ExportController.php
+++ b/application/front/controller/admin/ExportController.php
@@ -23,7 +23,7 @@ class ExportController extends ShaarliAdminController
23 */ 23 */
24 public function index(Request $request, Response $response): Response 24 public function index(Request $request, Response $response): Response
25 { 25 {
26 $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); 26 $this->assignView('pagetitle', t('Export') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
27 27
28 return $response->write($this->render(TemplatePage::EXPORT)); 28 return $response->write($this->render(TemplatePage::EXPORT));
29 } 29 }
@@ -68,7 +68,7 @@ class ExportController extends ShaarliAdminController
68 $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); 68 $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
69 $response = $response->withHeader( 69 $response = $response->withHeader(
70 'Content-disposition', 70 'Content-disposition',
71 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' 71 'attachment; filename=bookmarks_' . $selection . '_' . $now->format(Bookmark::LINK_DATE_FORMAT) . '.html'
72 ); 72 );
73 73
74 $this->assignView('date', $now->format(DateTime::RFC822)); 74 $this->assignView('date', $now->format(DateTime::RFC822));
diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php
index 758d5ef9..c2ad6a09 100644
--- a/application/front/controller/admin/ImportController.php
+++ b/application/front/controller/admin/ImportController.php
@@ -38,7 +38,7 @@ class ImportController extends ShaarliAdminController
38 true 38 true
39 ) 39 )
40 ); 40 );
41 $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); 41 $this->assignView('pagetitle', t('Import') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
42 42
43 return $response->write($this->render(TemplatePage::IMPORT)); 43 return $response->write($this->render(TemplatePage::IMPORT));
44 } 44 }
@@ -64,7 +64,7 @@ class ImportController extends ShaarliAdminController
64 $msg = sprintf( 64 $msg = sprintf(
65 t( 65 t(
66 'The file you are trying to upload is probably bigger than what this webserver can accept' 66 'The file you are trying to upload is probably bigger than what this webserver can accept'
67 .' (%s). Please upload in smaller chunks.' 67 . ' (%s). Please upload in smaller chunks.'
68 ), 68 ),
69 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) 69 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
70 ); 70 );
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php
deleted file mode 100644
index bb083486..00000000
--- a/application/front/controller/admin/ManageShaareController.php
+++ /dev/null
@@ -1,371 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkMarkdownFormatter;
10use Shaarli\Render\TemplatePage;
11use Shaarli\Thumbnailer;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15/**
16 * Class PostBookmarkController
17 *
18 * Slim controller used to handle Shaarli create or edit bookmarks.
19 */
20class 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 = '&nbsp;';
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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Controller used to retrieve/update bookmark's metadata.
12 */
13class MetadataController extends ShaarliAdminController
14{
15 /**
16 * GET /admin/metadata/{url} - Attempt to retrieve the bookmark title from provided URL.
17 */
18 public function ajaxRetrieveTitle(Request $request, Response $response): Response
19 {
20 $url = $request->getParam('url');
21
22 // Only try to extract metadata from URL with HTTP(s) scheme
23 if (!empty($url) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
24 return $response->withJson($this->container->metadataRetriever->retrieve($url));
25 }
26
27 return $response->withJson([]);
28 }
29}
diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php
index 5ec0d24b..4aaf1f82 100644
--- a/application/front/controller/admin/PasswordController.php
+++ b/application/front/controller/admin/PasswordController.php
@@ -25,7 +25,7 @@ class PasswordController extends ShaarliAdminController
25 25
26 $this->assignView( 26 $this->assignView(
27 'pagetitle', 27 'pagetitle',
28 t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli') 28 t('Change password') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
29 ); 29 );
30 } 30 }
31 31
@@ -78,7 +78,7 @@ class PasswordController extends ShaarliAdminController
78 78
79 // Save new password 79 // Save new password
80 // Salt renders rainbow-tables attacks useless. 80 // Salt renders rainbow-tables attacks useless.
81 $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); 81 $this->container->conf->set('credentials.salt', sha1(uniqid('', true) . '_' . mt_rand()));
82 $this->container->conf->set( 82 $this->container->conf->set(
83 'credentials.hash', 83 'credentials.hash',
84 sha1( 84 sha1(
diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php
index 8e059681..ae47c1af 100644
--- a/application/front/controller/admin/PluginsController.php
+++ b/application/front/controller/admin/PluginsController.php
@@ -42,7 +42,7 @@ class PluginsController extends ShaarliAdminController
42 $this->assignView('disabledPlugins', $disabledPlugins); 42 $this->assignView('disabledPlugins', $disabledPlugins);
43 $this->assignView( 43 $this->assignView(
44 'pagetitle', 44 'pagetitle',
45 t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') 45 t('Plugin Administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
46 ); 46 );
47 47
48 return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); 48 return $response->write($this->render(TemplatePage::PLUGINS_ADMIN));
@@ -64,7 +64,7 @@ class PluginsController extends ShaarliAdminController
64 unset($parameters['parameters_form']); 64 unset($parameters['parameters_form']);
65 unset($parameters['token']); 65 unset($parameters['token']);
66 foreach ($parameters as $param => $value) { 66 foreach ($parameters as $param => $value) {
67 $this->container->conf->set('plugins.'. $param, escape($value)); 67 $this->container->conf->set('plugins.' . $param, escape($value));
68 } 68 }
69 } else { 69 } else {
70 $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); 70 $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters));
diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php
new file mode 100644
index 00000000..fabeaf2f
--- /dev/null
+++ b/application/front/controller/admin/ServerController.php
@@ -0,0 +1,96 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Helper\ApplicationUtils;
8use Shaarli\Helper\FileUtils;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Slim controller used to handle Server administration page, and actions.
14 */
15class ServerController extends ShaarliAdminController
16{
17 /** @var string Cache type - main - by default pagecache/ and tmp/ */
18 protected const CACHE_MAIN = 'main';
19
20 /** @var string Cache type - thumbnails - by default cache/ */
21 protected const CACHE_THUMB = 'thumbnails';
22
23 /**
24 * GET /admin/server - Display page Server administration
25 */
26 public function index(Request $request, Response $response): Response
27 {
28 $releaseUrl = ApplicationUtils::$GITHUB_URL . '/releases/';
29 if ($this->container->conf->get('updates.check_updates', true)) {
30 $latestVersion = 'v' . ApplicationUtils::getVersion(
31 ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE
32 );
33 $releaseUrl .= 'tag/' . $latestVersion;
34 } else {
35 $latestVersion = t('Check disabled');
36 }
37
38 $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php');
39 $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion;
40 $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
41
42 $this->assignView('php_version', PHP_VERSION);
43 $this->assignView('php_eol', format_date($phpEol, false));
44 $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
45 $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
46 $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
47 $this->assignView('release_url', $releaseUrl);
48 $this->assignView('latest_version', $latestVersion);
49 $this->assignView('current_version', $currentVersion);
50 $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode'));
51 $this->assignView('index_url', index_url($this->container->environment));
52 $this->assignView('client_ip', client_ip_id($this->container->environment));
53 $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', []));
54
55 $this->assignView(
56 'pagetitle',
57 t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
58 );
59
60 return $response->write($this->render('server'));
61 }
62
63 /**
64 * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails).
65 */
66 public function clearCache(Request $request, Response $response): Response
67 {
68 $exclude = ['.htaccess'];
69
70 if ($request->getQueryParam('type') === static::CACHE_THUMB) {
71 $folders = [$this->container->conf->get('resource.thumbnails_cache')];
72
73 $this->saveWarningMessage(
74 t('Thumbnails cache has been cleared.') . ' ' .
75 '<a href="' . $this->container->basePath . '/admin/thumbnails">' .
76 t('Please synchronize them.') .
77 '</a>'
78 );
79 } else {
80 $folders = [
81 $this->container->conf->get('resource.page_cache'),
82 $this->container->conf->get('resource.raintpl_tmp'),
83 ];
84
85 $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!'));
86 }
87
88 // Make sure that we don't delete root cache folder
89 $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders))));
90 foreach ($folders as $folder) {
91 FileUtils::clearFolder($folder, false, $exclude);
92 }
93
94 return $this->redirect($response, '/admin/server');
95 }
96}
diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php
index d9a7a2e0..0917b6d2 100644
--- a/application/front/controller/admin/SessionFilterController.php
+++ b/application/front/controller/admin/SessionFilterController.php
@@ -45,6 +45,4 @@ class SessionFilterController extends ShaarliAdminController
45 45
46 return $this->redirectFromReferer($request, $response, ['visibility']); 46 return $this->redirectFromReferer($request, $response, ['visibility']);
47 } 47 }
48
49
50} 48}
diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php
new file mode 100644
index 00000000..ab8e7f40
--- /dev/null
+++ b/application/front/controller/admin/ShaareAddController.php
@@ -0,0 +1,34 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Formatter\BookmarkMarkdownFormatter;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class PostBookmarkController
13 *
14 * Slim controller used to handle Shaarli create or edit bookmarks.
15 */
16class ShaareManageController extends ShaarliAdminController
17{
18 /**
19 * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter).
20 */
21 public function deleteBookmark(Request $request, Response $response): Response
22 {
23 $this->checkToken($request);
24
25 $ids = escape(trim($request->getParam('id') ?? ''));
26 if (empty($ids) || strpos($ids, ' ') !== false) {
27 // multiple, space-separated ids provided
28 $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
29 } else {
30 $ids = [$ids];
31 }
32
33 // assert at least one id is given
34 if (0 === count($ids)) {
35 $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
36
37 return $this->redirectFromReferer($request, $response, [], ['delete-shaare']);
38 }
39
40 $formatter = $this->container->formatterFactory->getFormatter('raw');
41 $count = 0;
42 foreach ($ids as $id) {
43 try {
44 $bookmark = $this->container->bookmarkService->get((int) $id);
45 } catch (BookmarkNotFoundException $e) {
46 $this->saveErrorMessage(sprintf(
47 t('Bookmark with identifier %s could not be found.'),
48 $id
49 ));
50
51 continue;
52 }
53
54 $data = $formatter->format($bookmark);
55 $this->executePageHooks('delete_link', $data);
56 $this->container->bookmarkService->remove($bookmark, false);
57 ++$count;
58 }
59
60 if ($count > 0) {
61 $this->container->bookmarkService->save();
62 }
63
64 // If we are called from the bookmarklet, we must close the popup:
65 if ($request->getParam('source') === 'bookmarklet') {
66 return $response->write('<script>self.close();</script>');
67 }
68
69 // Don't redirect to permalink after deletion.
70 return $this->redirectFromReferer($request, $response, ['shaare/']);
71 }
72
73 /**
74 * GET /admin/shaare/visibility
75 *
76 * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter).
77 */
78 public function changeVisibility(Request $request, Response $response): Response
79 {
80 $this->checkToken($request);
81
82 $ids = trim(escape($request->getParam('id') ?? ''));
83 if (empty($ids) || strpos($ids, ' ') !== false) {
84 // multiple, space-separated ids provided
85 $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
86 } else {
87 // only a single id provided
88 $ids = [$ids];
89 }
90
91 // assert at least one id is given
92 if (0 === count($ids)) {
93 $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
94
95 return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
96 }
97
98 // assert that the visibility is valid
99 $visibility = $request->getParam('newVisibility');
100 if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) {
101 $this->saveErrorMessage(t('Invalid visibility provided.'));
102
103 return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
104 } else {
105 $isPrivate = $visibility === 'private';
106 }
107
108 $formatter = $this->container->formatterFactory->getFormatter('raw');
109 $count = 0;
110
111 foreach ($ids as $id) {
112 try {
113 $bookmark = $this->container->bookmarkService->get((int) $id);
114 } catch (BookmarkNotFoundException $e) {
115 $this->saveErrorMessage(sprintf(
116 t('Bookmark with identifier %s could not be found.'),
117 $id
118 ));
119
120 continue;
121 }
122
123 $bookmark->setPrivate($isPrivate);
124
125 // To preserve backward compatibility with 3rd parties, plugins still use arrays
126 $data = $formatter->format($bookmark);
127 $this->executePageHooks('save_link', $data);
128 $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
129
130 $this->container->bookmarkService->set($bookmark, false);
131 ++$count;
132 }
133
134 if ($count > 0) {
135 $this->container->bookmarkService->save();
136 }
137
138 return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']);
139 }
140
141 /**
142 * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark.
143 */
144 public function pinBookmark(Request $request, Response $response, array $args): Response
145 {
146 $this->checkToken($request);
147
148 $id = $args['id'] ?? '';
149 try {
150 if (false === ctype_digit($id)) {
151 throw new BookmarkNotFoundException();
152 }
153 $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
154 } catch (BookmarkNotFoundException $e) {
155 $this->saveErrorMessage(sprintf(
156 t('Bookmark with identifier %s could not be found.'),
157 $id
158 ));
159
160 return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
161 }
162
163 $formatter = $this->container->formatterFactory->getFormatter('raw');
164
165 $bookmark->setSticky(!$bookmark->isSticky());
166
167 // To preserve backward compatibility with 3rd parties, plugins still use arrays
168 $data = $formatter->format($bookmark);
169 $this->executePageHooks('save_link', $data);
170 $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
171
172 $this->container->bookmarkService->set($bookmark);
173
174 return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
175 }
176
177 /**
178 * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL.
179 */
180 public function sharePrivate(Request $request, Response $response, array $args): Response
181 {
182 $this->checkToken($request);
183
184 $hash = $args['hash'] ?? '';
185 $bookmark = $this->container->bookmarkService->findByHash($hash);
186
187 if ($bookmark->isPrivate() !== true) {
188 return $this->redirect($response, '/shaare/' . $hash);
189 }
190
191 if (empty($bookmark->getAdditionalContentEntry('private_key'))) {
192 $privateKey = bin2hex(random_bytes(16));
193 $bookmark->addAdditionalContentEntry('private_key', $privateKey);
194 $this->container->bookmarkService->set($bookmark);
195 }
196
197 return $this->redirect(
198 $response,
199 '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key')
200 );
201 }
202}
diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php
new file mode 100644
index 00000000..4cbfcdc5
--- /dev/null
+++ b/application/front/controller/admin/ShaarePublishController.php
@@ -0,0 +1,274 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\BookmarkMarkdownFormatter;
11use Shaarli\Render\TemplatePage;
12use Shaarli\Thumbnailer;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class ShaarePublishController extends ShaarliAdminController
17{
18 /**
19 * @var BookmarkFormatter[] Statically cached instances of formatters
20 */
21 protected $formatters = [];
22
23 /**
24 * @var array Statically cached bookmark's tags counts
25 */
26 protected $tags;
27
28 /**
29 * GET /admin/shaare - Displays the bookmark form for creation.
30 * Note that if the URL is found in existing bookmarks, then it will be in edit mode.
31 */
32 public function displayCreateForm(Request $request, Response $response): Response
33 {
34 $url = cleanup_url($request->getParam('post'));
35 $link = $this->buildLinkDataFromUrl($request, $url);
36
37 return $this->displayForm($link, $link['linkIsNew'], $request, $response);
38 }
39
40 /**
41 * POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page.
42 */
43 public function displayCreateBatchForms(Request $request, Response $response): Response
44 {
45 $urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls')));
46
47 $links = [];
48 foreach ($urls as $url) {
49 if (empty($url)) {
50 continue;
51 }
52 $link = $this->buildLinkDataFromUrl($request, $url);
53 $data = $this->buildFormData($link, $link['linkIsNew'], $request);
54 $data['token'] = $this->container->sessionManager->generateToken();
55 $data['source'] = 'batch';
56
57 $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
58
59 $links[] = $data;
60 }
61
62 $this->assignView('links', $links);
63 $this->assignView('batch_mode', true);
64 $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
65
66 return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH));
67 }
68
69 /**
70 * GET /admin/shaare/{id} - Displays the bookmark form in edition mode.
71 */
72 public function displayEditForm(Request $request, Response $response, array $args): Response
73 {
74 $id = $args['id'] ?? '';
75 try {
76 if (false === ctype_digit($id)) {
77 throw new BookmarkNotFoundException();
78 }
79 $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
80 } catch (BookmarkNotFoundException $e) {
81 $this->saveErrorMessage(sprintf(
82 t('Bookmark with identifier %s could not be found.'),
83 $id
84 ));
85
86 return $this->redirect($response, '/');
87 }
88
89 $formatter = $this->getFormatter('raw');
90 $link = $formatter->format($bookmark);
91
92 return $this->displayForm($link, false, $request, $response);
93 }
94
95 /**
96 * POST /admin/shaare
97 */
98 public function save(Request $request, Response $response): Response
99 {
100 $this->checkToken($request);
101
102 // lf_id should only be present if the link exists.
103 $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null;
104 if (null !== $id && true === $this->container->bookmarkService->exists($id)) {
105 // Edit
106 $bookmark = $this->container->bookmarkService->get($id);
107 } else {
108 // New link
109 $bookmark = new Bookmark();
110 }
111
112 $bookmark->setTitle($request->getParam('lf_title'));
113 $bookmark->setDescription($request->getParam('lf_description'));
114 $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
115 $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
116 $bookmark->setTagsString(
117 $request->getParam('lf_tags'),
118 $this->container->conf->get('general.tags_separator', ' ')
119 );
120
121 if (
122 $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
123 && true !== $this->container->conf->get('general.enable_async_metadata', true)
124 && $bookmark->shouldUpdateThumbnail()
125 ) {
126 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
127 }
128 $this->container->bookmarkService->addOrSet($bookmark, false);
129
130 // To preserve backward compatibility with 3rd parties, plugins still use arrays
131 $formatter = $this->getFormatter('raw');
132 $data = $formatter->format($bookmark);
133 $this->executePageHooks('save_link', $data);
134
135 $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
136 $this->container->bookmarkService->set($bookmark);
137
138 // If we are called from the bookmarklet, we must close the popup:
139 if ($request->getParam('source') === 'bookmarklet') {
140 return $response->write('<script>self.close();</script>');
141 } elseif ($request->getParam('source') === 'batch') {
142 return $response;
143 }
144
145 if (!empty($request->getParam('returnurl'))) {
146 $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
147 }
148
149 return $this->redirectFromReferer(
150 $request,
151 $response,
152 ['/admin/add-shaare', '/admin/shaare'],
153 ['addlink', 'post', 'edit_link'],
154 $bookmark->getShortUrl()
155 );
156 }
157
158 /**
159 * Helper function used to display the shaare form whether it's a new or existing bookmark.
160 *
161 * @param array $link data used in template, either from parameters or from the data store
162 */
163 protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
164 {
165 $data = $this->buildFormData($link, $isNew, $request);
166
167 $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
168
169 foreach ($data as $key => $value) {
170 $this->assignView($key, $value);
171 }
172
173 $editLabel = false === $isNew ? t('Edit') . ' ' : '';
174 $this->assignView(
175 'pagetitle',
176 $editLabel . t('Shaare') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
177 );
178
179 return $response->write($this->render(TemplatePage::EDIT_LINK));
180 }
181
182 protected function buildLinkDataFromUrl(Request $request, string $url): array
183 {
184 // Check if URL is not already in database (in this case, we will edit the existing link)
185 $bookmark = $this->container->bookmarkService->findByUrl($url);
186 if (null === $bookmark) {
187 // Get shaare data if it was provided in URL (e.g.: by the bookmarklet).
188 $title = $request->getParam('title');
189 $description = $request->getParam('description');
190 $tags = $request->getParam('tags');
191 if ($request->getParam('private') !== null) {
192 $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
193 } else {
194 $private = $this->container->conf->get('privacy.default_private_links', false);
195 }
196
197 // If this is an HTTP(S) link, we try go get the page to extract
198 // the title (otherwise we will to straight to the edit form.)
199 if (
200 true !== $this->container->conf->get('general.enable_async_metadata', true)
201 && empty($title)
202 && strpos(get_url_scheme($url) ?: '', 'http') !== false
203 ) {
204 $metadata = $this->container->metadataRetriever->retrieve($url);
205 }
206
207 if (empty($url)) {
208 $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: '));
209 }
210
211 return [
212 'title' => $title ?? $metadata['title'] ?? '',
213 'url' => $url ?? '',
214 'description' => $description ?? $metadata['description'] ?? '',
215 'tags' => $tags ?? $metadata['tags'] ?? '',
216 'private' => $private,
217 'linkIsNew' => true,
218 ];
219 }
220
221 $formatter = $this->getFormatter('raw');
222 $link = $formatter->format($bookmark);
223 $link['linkIsNew'] = false;
224
225 return $link;
226 }
227
228 protected function buildFormData(array $link, bool $isNew, Request $request): array
229 {
230 $link['tags'] = strlen($link['tags']) > 0
231 ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ')
232 : $link['tags']
233 ;
234
235 return escape([
236 'link' => $link,
237 'link_is_new' => $isNew,
238 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
239 'source' => $request->getParam('source') ?? '',
240 'tags' => $this->getTags(),
241 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
242 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true),
243 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false),
244 ]);
245 }
246
247 /**
248 * Memoize formatterFactory->getFormatter() calls.
249 */
250 protected function getFormatter(string $type): BookmarkFormatter
251 {
252 if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) {
253 $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type);
254 }
255
256 return $this->formatters[$type];
257 }
258
259 /**
260 * Memoize bookmarkService->bookmarksCountPerTag() calls.
261 */
262 protected function getTags(): array
263 {
264 if ($this->tags === null) {
265 $this->tags = $this->container->bookmarkService->bookmarksCountPerTag();
266
267 if ($this->container->conf->get('formatter') === 'markdown') {
268 $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
269 }
270 }
271
272 return $this->tags;
273 }
274}
diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php
index 81c87ed0..94d97d4b 100644
--- a/application/front/controller/admin/ThumbnailsController.php
+++ b/application/front/controller/admin/ThumbnailsController.php
@@ -34,7 +34,7 @@ class ThumbnailsController extends ShaarliAdminController
34 $this->assignView('ids', $ids); 34 $this->assignView('ids', $ids);
35 $this->assignView( 35 $this->assignView(
36 'pagetitle', 36 'pagetitle',
37 t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') 37 t('Thumbnails update') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
38 ); 38 );
39 39
40 return $response->write($this->render(TemplatePage::THUMBNAILS)); 40 return $response->write($this->render(TemplatePage::THUMBNAILS));
@@ -52,7 +52,7 @@ class ThumbnailsController extends ShaarliAdminController
52 } 52 }
53 53
54 try { 54 try {
55 $bookmark = $this->container->bookmarkService->get($id); 55 $bookmark = $this->container->bookmarkService->get((int) $id);
56 } catch (BookmarkNotFoundException $e) { 56 } catch (BookmarkNotFoundException $e) {
57 return $response->withStatus(404); 57 return $response->withStatus(404);
58 } 58 }
diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php
index a87f20d2..560e5e3e 100644
--- a/application/front/controller/admin/ToolsController.php
+++ b/application/front/controller/admin/ToolsController.php
@@ -28,7 +28,7 @@ class ToolsController extends ShaarliAdminController
28 $this->assignView($key, $value); 28 $this->assignView($key, $value);
29 } 29 }
30 30
31 $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); 31 $this->assignView('pagetitle', t('Tools') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
32 32
33 return $response->write($this->render(TemplatePage::TOOLS)); 33 return $response->write($this->render(TemplatePage::TOOLS));
34 } 34 }
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php
index 18368751..fe8231be 100644
--- a/application/front/controller/visitor/BookmarkListController.php
+++ b/application/front/controller/visitor/BookmarkListController.php
@@ -35,7 +35,8 @@ class BookmarkListController extends ShaarliVisitorController
35 $formatter->addContextData('base_path', $this->container->basePath); 35 $formatter->addContextData('base_path', $this->container->basePath);
36 36
37 $searchTags = normalize_spaces($request->getParam('searchtags') ?? ''); 37 $searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
38 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));; 38 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));
39 ;
39 40
40 // Filter bookmarks according search parameters. 41 // Filter bookmarks according search parameters.
41 $visibility = $this->container->sessionManager->getSessionParameter('visibility'); 42 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
@@ -95,6 +96,10 @@ class BookmarkListController extends ShaarliVisitorController
95 $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl; 96 $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
96 } 97 }
97 98
99 $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
100 $searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator));
101 $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
102
98 // Fill all template fields. 103 // Fill all template fields.
99 $data = array_merge( 104 $data = array_merge(
100 $this->initializeTemplateVars(), 105 $this->initializeTemplateVars(),
@@ -106,7 +111,7 @@ class BookmarkListController extends ShaarliVisitorController
106 'result_count' => count($linksToDisplay), 111 'result_count' => count($linksToDisplay),
107 'search_term' => escape($searchTerm), 112 'search_term' => escape($searchTerm),
108 'search_tags' => escape($searchTags), 113 'search_tags' => escape($searchTags),
109 'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)), 114 'search_tags_url' => $searchTagsUrlEncoded,
110 'visibility' => $visibility, 115 'visibility' => $visibility,
111 'links' => $linkDisp, 116 'links' => $linkDisp,
112 ] 117 ]
@@ -119,8 +124,9 @@ class BookmarkListController extends ShaarliVisitorController
119 return '[' . $tag . ']'; 124 return '[' . $tag . ']';
120 }; 125 };
121 $data['pagetitle'] .= ! empty($searchTags) 126 $data['pagetitle'] .= ! empty($searchTags)
122 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' ' 127 ? implode(' ', array_map($bracketWrap, tags_str2array($searchTags, $tagsSeparator))) . ' '
123 : ''; 128 : ''
129 ;
124 $data['pagetitle'] .= '- '; 130 $data['pagetitle'] .= '- ';
125 } 131 }
126 132
@@ -137,8 +143,10 @@ class BookmarkListController extends ShaarliVisitorController
137 */ 143 */
138 public function permalink(Request $request, Response $response, array $args): Response 144 public function permalink(Request $request, Response $response, array $args): Response
139 { 145 {
146 $privateKey = $request->getParam('key');
147
140 try { 148 try {
141 $bookmark = $this->container->bookmarkService->findByHash($args['hash']); 149 $bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey);
142 } catch (BookmarkNotFoundException $e) { 150 } catch (BookmarkNotFoundException $e) {
143 $this->assignView('error_message', $e->getMessage()); 151 $this->assignView('error_message', $e->getMessage());
144 152
@@ -153,7 +161,7 @@ class BookmarkListController extends ShaarliVisitorController
153 $data = array_merge( 161 $data = array_merge(
154 $this->initializeTemplateVars(), 162 $this->initializeTemplateVars(),
155 [ 163 [
156 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'), 164 'pagetitle' => $bookmark->getTitle() . ' - ' . $this->container->conf->get('general.title', 'Shaarli'),
157 'links' => [$formatter->format($bookmark)], 165 'links' => [$formatter->format($bookmark)],
158 ] 166 ]
159 ); 167 );
@@ -169,19 +177,25 @@ class BookmarkListController extends ShaarliVisitorController
169 */ 177 */
170 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool 178 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
171 { 179 {
172 // Logged in, thumbnails enabled, not a note, is HTTP 180 if (false === $this->container->loginManager->isLoggedIn()) {
173 // and (never retrieved yet or no valid cache file) 181 return false;
174 if ($this->container->loginManager->isLoggedIn() 182 }
175 && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE 183
176 && false !== $bookmark->getThumbnail() 184 // If thumbnail should be updated, we reset it to null
177 && !$bookmark->isNote() 185 if ($bookmark->shouldUpdateThumbnail()) {
178 && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail())) 186 $bookmark->setThumbnail(null);
179 && startsWith(strtolower($bookmark->getUrl()), 'http') 187
180 ) { 188 // Requires an update, not async retrieval, thumbnails enabled
181 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); 189 if (
182 $this->container->bookmarkService->set($bookmark, $writeDatastore); 190 $bookmark->shouldUpdateThumbnail()
183 191 && true !== $this->container->conf->get('general.enable_async_metadata', true)
184 return true; 192 && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
193 ) {
194 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
195 $this->container->bookmarkService->set($bookmark, $writeDatastore);
196
197 return true;
198 }
185 } 199 }
186 200
187 return false; 201 return false;
@@ -198,6 +212,7 @@ class BookmarkListController extends ShaarliVisitorController
198 'page_max' => '', 212 'page_max' => '',
199 'search_tags' => '', 213 'search_tags' => '',
200 'result_count' => '', 214 'result_count' => '',
215 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true)
201 ]; 216 ];
202 } 217 }
203 218
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php
index 07617cf1..846cfe22 100644
--- a/application/front/controller/visitor/DailyController.php
+++ b/application/front/controller/visitor/DailyController.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
5namespace Shaarli\Front\Controller\Visitor; 5namespace Shaarli\Front\Controller\Visitor;
6 6
7use DateTime; 7use DateTime;
8use DateTimeImmutable;
9use Shaarli\Bookmark\Bookmark; 8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Helper\DailyPageHelper;
10use Shaarli\Render\TemplatePage; 10use Shaarli\Render\TemplatePage;
11use Slim\Http\Request; 11use Slim\Http\Request;
12use Slim\Http\Response; 12use Slim\Http\Response;
@@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController
26 */ 26 */
27 public function index(Request $request, Response $response): Response 27 public function index(Request $request, Response $response): Response
28 { 28 {
29 $day = $request->getQueryParam('day') ?? date('Ymd'); 29 $type = DailyPageHelper::extractRequestedType($request);
30 30 $format = DailyPageHelper::getFormatByType($type);
31 $availableDates = $this->container->bookmarkService->days(); 31 $latestBookmark = $this->container->bookmarkService->getLatest();
32 $nbAvailableDates = count($availableDates); 32 $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark);
33 $index = array_search($day, $availableDates); 33 $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
34 34 $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
35 if ($index === false) { 35 $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime);
36 // no bookmarks for day, but at least one day with bookmarks 36
37 $day = $availableDates[$nbAvailableDates - 1] ?? $day; 37 $linksToDisplay = $this->container->bookmarkService->findByDate(
38 $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; 38 $start,
39 } else { 39 $end,
40 $previousDay = $availableDates[$index - 1] ?? ''; 40 $previousDay,
41 $nextDay = $availableDates[$index + 1] ?? ''; 41 $nextDay
42 } 42 );
43
44 if ($day === date('Ymd')) {
45 $this->assignView('dayDesc', t('Today'));
46 } elseif ($day === date('Ymd', strtotime('-1 days'))) {
47 $this->assignView('dayDesc', t('Yesterday'));
48 }
49
50 try {
51 $linksToDisplay = $this->container->bookmarkService->filterDay($day);
52 } catch (\Exception $exc) {
53 $linksToDisplay = [];
54 }
55 43
56 $formatter = $this->container->formatterFactory->getFormatter(); 44 $formatter = $this->container->formatterFactory->getFormatter();
57 $formatter->addContextData('base_path', $this->container->basePath); 45 $formatter->addContextData('base_path', $this->container->basePath);
@@ -63,13 +51,15 @@ class DailyController extends ShaarliVisitorController
63 $linksToDisplay[$key]['description'] = $bookmark->getDescription(); 51 $linksToDisplay[$key]['description'] = $bookmark->getDescription();
64 } 52 }
65 53
66 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
67 $data = [ 54 $data = [
68 'linksToDisplay' => $linksToDisplay, 55 'linksToDisplay' => $linksToDisplay,
69 'day' => $dayDate->getTimestamp(), 56 'dayDate' => $start,
70 'dayDate' => $dayDate, 57 'day' => $start->getTimestamp(),
71 'previousday' => $previousDay ?? '', 58 'previousday' => $previousDay ? $previousDay->format($format) : '',
72 'nextday' => $nextDay ?? '', 59 'nextday' => $nextDay ? $nextDay->format($format) : '',
60 'dayDesc' => $dailyDesc,
61 'type' => $type,
62 'localizedType' => $this->translateType($type),
73 ]; 63 ];
74 64
75 // Hooks are called before column construction so that plugins don't have to deal with columns. 65 // Hooks are called before column construction so that plugins don't have to deal with columns.
@@ -82,7 +72,7 @@ class DailyController extends ShaarliVisitorController
82 $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); 72 $mainTitle = $this->container->conf->get('general.title', 'Shaarli');
83 $this->assignView( 73 $this->assignView(
84 'pagetitle', 74 'pagetitle',
85 t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle 75 $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle
86 ); 76 );
87 77
88 return $response->write($this->render(TemplatePage::DAILY)); 78 return $response->write($this->render(TemplatePage::DAILY));
@@ -106,11 +96,14 @@ class DailyController extends ShaarliVisitorController
106 } 96 }
107 97
108 $days = []; 98 $days = [];
99 $type = DailyPageHelper::extractRequestedType($request);
100 $format = DailyPageHelper::getFormatByType($type);
101 $length = DailyPageHelper::getRssLengthByType($type);
109 foreach ($this->container->bookmarkService->search() as $bookmark) { 102 foreach ($this->container->bookmarkService->search() as $bookmark) {
110 $day = $bookmark->getCreated()->format('Ymd'); 103 $day = $bookmark->getCreated()->format($format);
111 104
112 // Stop iterating after DAILY_RSS_NB_DAYS entries 105 // Stop iterating after DAILY_RSS_NB_DAYS entries
113 if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { 106 if (count($days) === $length && !isset($days[$day])) {
114 break; 107 break;
115 } 108 }
116 109
@@ -127,12 +120,19 @@ class DailyController extends ShaarliVisitorController
127 120
128 /** @var Bookmark[] $bookmarks */ 121 /** @var Bookmark[] $bookmarks */
129 foreach ($days as $day => $bookmarks) { 122 foreach ($days as $day => $bookmarks) {
130 $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); 123 $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day);
124 $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime);
125
126 // We only want the RSS entry to be published when the period is over.
127 if (new DateTime() < $endDateTime) {
128 continue;
129 }
130
131 $dataPerDay[$day] = [ 131 $dataPerDay[$day] = [
132 'date' => $dayDatetime, 132 'date' => $endDateTime,
133 'date_rss' => $dayDatetime->format(DateTime::RSS), 133 'date_rss' => $endDateTime->format(DateTime::RSS),
134 'date_human' => format_date($dayDatetime, false, true), 134 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime),
135 'absolute_url' => $indexUrl . 'daily?day=' . $day, 135 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day,
136 'links' => [], 136 'links' => [],
137 ]; 137 ];
138 138
@@ -141,16 +141,20 @@ class DailyController extends ShaarliVisitorController
141 141
142 // Make permalink URL absolute 142 // Make permalink URL absolute
143 if ($bookmark->isNote()) { 143 if ($bookmark->isNote()) {
144 $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); 144 $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl();
145 } 145 }
146 } 146 }
147 } 147 }
148 148
149 $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); 149 $this->assignAllView([
150 $this->assignView('index_url', $indexUrl); 150 'title' => $this->container->conf->get('general.title', 'Shaarli'),
151 $this->assignView('page_url', $pageUrl); 151 'index_url' => $indexUrl,
152 $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); 152 'page_url' => $pageUrl,
153 $this->assignView('days', $dataPerDay); 153 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false),
154 'days' => $dataPerDay,
155 'type' => $type,
156 'localizedType' => $this->translateType($type),
157 ]);
154 158
155 $rssContent = $this->render(TemplatePage::DAILY_RSS); 159 $rssContent = $this->render(TemplatePage::DAILY_RSS);
156 160
@@ -189,4 +193,13 @@ class DailyController extends ShaarliVisitorController
189 193
190 return $columns; 194 return $columns;
191 } 195 }
196
197 protected function translateType($type): string
198 {
199 return [
200 t('day') => t('Daily'),
201 t('week') => t('Weekly'),
202 t('month') => t('Monthly'),
203 ][t($type)] ?? t('Daily');
204 }
192} 205}
diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php
index 10aa84c8..428e8254 100644
--- a/application/front/controller/visitor/ErrorController.php
+++ b/application/front/controller/visitor/ErrorController.php
@@ -26,12 +26,15 @@ class ErrorController extends ShaarliVisitorController
26 $response = $response->withStatus($throwable->getCode()); 26 $response = $response->withStatus($throwable->getCode());
27 } else { 27 } else {
28 // Internal error (any other Throwable) 28 // Internal error (any other Throwable)
29 if ($this->container->conf->get('dev.debug', false)) { 29 if ($this->container->conf->get('dev.debug', false) || $this->container->loginManager->isLoggedIn()) {
30 $this->assignView('message', $throwable->getMessage()); 30 $this->assignView('message', t('Error: ') . $throwable->getMessage());
31 $this->assignView( 31 $this->assignView(
32 'stacktrace', 32 'text',
33 nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString()) 33 '<a href="https://github.com/shaarli/Shaarli/issues/new">'
34 . t('Please report it on Github.')
35 . '</a>'
34 ); 36 );
37 $this->assignView('stacktrace', exception2text($throwable));
35 } else { 38 } else {
36 $this->assignView('message', t('An unexpected error occurred.')); 39 $this->assignView('message', t('An unexpected error occurred.'));
37 } 40 }
@@ -39,7 +42,6 @@ class ErrorController extends ShaarliVisitorController
39 $response = $response->withStatus(500); 42 $response = $response->withStatus(500);
40 } 43 }
41 44
42
43 return $response->write($this->render('error')); 45 return $response->write($this->render('error'));
44 } 46 }
45} 47}
diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php
index 8d8b546a..edc7ef43 100644
--- a/application/front/controller/visitor/FeedController.php
+++ b/application/front/controller/visitor/FeedController.php
@@ -27,7 +27,7 @@ class FeedController extends ShaarliVisitorController
27 27
28 protected function processRequest(string $feedType, Request $request, Response $response): Response 28 protected function processRequest(string $feedType, Request $request, Response $response): Response
29 { 29 {
30 $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8'); 30 $response = $response->withHeader('Content-Type', 'application/' . $feedType . '+xml; charset=utf-8');
31 31
32 $pageUrl = page_url($this->container->environment); 32 $pageUrl = page_url($this->container->environment);
33 $cache = $this->container->pageCacheManager->getCachePage($pageUrl); 33 $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php
index 7cb32777..bf965929 100644
--- a/application/front/controller/visitor/InstallController.php
+++ b/application/front/controller/visitor/InstallController.php
@@ -4,10 +4,10 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Visitor; 5namespace Shaarli\Front\Controller\Visitor;
6 6
7use Shaarli\ApplicationUtils;
8use Shaarli\Container\ShaarliContainer; 7use Shaarli\Container\ShaarliContainer;
9use Shaarli\Front\Exception\AlreadyInstalledException; 8use Shaarli\Front\Exception\AlreadyInstalledException;
10use Shaarli\Front\Exception\ResourcePermissionException; 9use Shaarli\Front\Exception\ResourcePermissionException;
10use Shaarli\Helper\ApplicationUtils;
11use Shaarli\Languages; 11use Shaarli\Languages;
12use Shaarli\Security\SessionManager; 12use Shaarli\Security\SessionManager;
13use Slim\Http\Request; 13use Slim\Http\Request;
@@ -39,7 +39,8 @@ class InstallController extends ShaarliVisitorController
39 // Before installation, we'll make sure that permissions are set properly, and sessions are working. 39 // Before installation, we'll make sure that permissions are set properly, and sessions are working.
40 $this->checkPermissions(); 40 $this->checkPermissions();
41 41
42 if (static::SESSION_TEST_VALUE 42 if (
43 static::SESSION_TEST_VALUE
43 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) 44 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
44 ) { 45 ) {
45 $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); 46 $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE);
@@ -53,6 +54,16 @@ class InstallController extends ShaarliVisitorController
53 $this->assignView('cities', $cities); 54 $this->assignView('cities', $cities);
54 $this->assignView('languages', Languages::getAvailableLanguages()); 55 $this->assignView('languages', Languages::getAvailableLanguages());
55 56
57 $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
58
59 $this->assignView('php_version', PHP_VERSION);
60 $this->assignView('php_eol', format_date($phpEol, false));
61 $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
62 $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
63 $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
64
65 $this->assignView('pagetitle', t('Install Shaarli'));
66
56 return $response->write($this->render('install')); 67 return $response->write($this->render('install'));
57 } 68 }
58 69
@@ -65,17 +76,18 @@ class InstallController extends ShaarliVisitorController
65 // This part makes sure sessions works correctly. 76 // This part makes sure sessions works correctly.
66 // (Because on some hosts, session.save_path may not be set correctly, 77 // (Because on some hosts, session.save_path may not be set correctly,
67 // or we may not have write access to it.) 78 // or we may not have write access to it.)
68 if (static::SESSION_TEST_VALUE 79 if (
80 static::SESSION_TEST_VALUE
69 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) 81 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
70 ) { 82 ) {
71 // Step 2: Check if data in session is correct. 83 // Step 2: Check if data in session is correct.
72 $msg = t( 84 $msg = t(
73 '<pre>Sessions do not seem to work correctly on your server.<br>'. 85 '<pre>Sessions do not seem to work correctly on your server.<br>' .
74 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. 86 'Make sure the variable "session.save_path" is set correctly in your PHP config, ' .
75 'and that you have write access to it.<br>'. 87 'and that you have write access to it.<br>' .
76 'It currently points to %s.<br>'. 88 'It currently points to %s.<br>' .
77 'On some browsers, accessing your server via a hostname like \'localhost\' '. 89 'On some browsers, accessing your server via a hostname like \'localhost\' ' .
78 'or any custom hostname without a dot causes cookie storage to fail. '. 90 'or any custom hostname without a dot causes cookie storage to fail. ' .
79 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>' 91 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
80 ); 92 );
81 $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); 93 $msg = sprintf($msg, $this->container->sessionManager->getSavePath());
@@ -94,7 +106,8 @@ class InstallController extends ShaarliVisitorController
94 public function save(Request $request, Response $response): Response 106 public function save(Request $request, Response $response): Response
95 { 107 {
96 $timezone = 'UTC'; 108 $timezone = 'UTC';
97 if (!empty($request->getParam('continent')) 109 if (
110 !empty($request->getParam('continent'))
98 && !empty($request->getParam('city')) 111 && !empty($request->getParam('city'))
99 && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) 112 && isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
100 ) { 113 ) {
@@ -104,7 +117,7 @@ class InstallController extends ShaarliVisitorController
104 117
105 $login = $request->getParam('setlogin'); 118 $login = $request->getParam('setlogin');
106 $this->container->conf->set('credentials.login', $login); 119 $this->container->conf->set('credentials.login', $login);
107 $salt = sha1(uniqid('', true) .'_'. mt_rand()); 120 $salt = sha1(uniqid('', true) . '_' . mt_rand());
108 $this->container->conf->set('credentials.salt', $salt); 121 $this->container->conf->set('credentials.salt', $salt);
109 $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); 122 $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
110 123
@@ -113,7 +126,7 @@ class InstallController extends ShaarliVisitorController
113 } else { 126 } else {
114 $this->container->conf->set( 127 $this->container->conf->set(
115 'general.title', 128 'general.title',
116 'Shared bookmarks on '.escape(index_url($this->container->environment)) 129 'Shared bookmarks on ' . escape(index_url($this->container->environment))
117 ); 130 );
118 } 131 }
119 132
@@ -150,7 +163,7 @@ class InstallController extends ShaarliVisitorController
150 protected function checkPermissions(): bool 163 protected function checkPermissions(): bool
151 { 164 {
152 // Ensure Shaarli has proper access to its resources 165 // Ensure Shaarli has proper access to its resources
153 $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); 166 $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true);
154 if (empty($errors)) { 167 if (empty($errors)) {
155 return true; 168 return true;
156 } 169 }
diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php
index 121ba40b..4b881535 100644
--- a/application/front/controller/visitor/LoginController.php
+++ b/application/front/controller/visitor/LoginController.php
@@ -43,7 +43,7 @@ class LoginController extends ShaarliVisitorController
43 $this 43 $this
44 ->assignView('returnurl', escape($returnUrl)) 44 ->assignView('returnurl', escape($returnUrl))
45 ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) 45 ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
46 ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) 46 ->assignView('pagetitle', t('Login') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'))
47 ; 47 ;
48 48
49 return $response->write($this->render(TemplatePage::LOGIN)); 49 return $response->write($this->render(TemplatePage::LOGIN));
@@ -64,8 +64,8 @@ class LoginController extends ShaarliVisitorController
64 return $this->redirect($response, '/'); 64 return $this->redirect($response, '/');
65 } 65 }
66 66
67 if (!$this->container->loginManager->checkCredentials( 67 if (
68 $this->container->environment['REMOTE_ADDR'], 68 !$this->container->loginManager->checkCredentials(
69 client_ip_id($this->container->environment), 69 client_ip_id($this->container->environment),
70 $request->getParam('login'), 70 $request->getParam('login'),
71 $request->getParam('password') 71 $request->getParam('password')
@@ -102,7 +102,8 @@ class LoginController extends ShaarliVisitorController
102 */ 102 */
103 protected function checkLoginState(): bool 103 protected function checkLoginState(): bool
104 { 104 {
105 if ($this->container->loginManager->isLoggedIn() 105 if (
106 $this->container->loginManager->isLoggedIn()
106 || $this->container->conf->get('security.open_shaarli', false) 107 || $this->container->conf->get('security.open_shaarli', false)
107 ) { 108 ) {
108 throw new CantLoginException(); 109 throw new CantLoginException();
diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php
index 3c57f8dd..23553ee6 100644
--- a/application/front/controller/visitor/PictureWallController.php
+++ b/application/front/controller/visitor/PictureWallController.php
@@ -26,7 +26,7 @@ class PictureWallController extends ShaarliVisitorController
26 26
27 $this->assignView( 27 $this->assignView(
28 'pagetitle', 28 'pagetitle',
29 t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli') 29 t('Picture wall') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
30 ); 30 );
31 31
32 // Optionally filter the results: 32 // Optionally filter the results:
diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php
index 55c075a2..ae946c59 100644
--- a/application/front/controller/visitor/ShaarliVisitorController.php
+++ b/application/front/controller/visitor/ShaarliVisitorController.php
@@ -106,6 +106,7 @@ abstract class ShaarliVisitorController
106 'target' => $template, 106 'target' => $template,
107 'loggedin' => $this->container->loginManager->isLoggedIn(), 107 'loggedin' => $this->container->loginManager->isLoggedIn(),
108 'basePath' => $this->container->basePath, 108 'basePath' => $this->container->basePath,
109 'rootPath' => preg_replace('#/index\.php$#', '', $this->container->basePath),
109 'bookmarkService' => $this->container->bookmarkService 110 'bookmarkService' => $this->container->bookmarkService
110 ]; 111 ];
111 } 112 }
@@ -143,7 +144,8 @@ abstract class ShaarliVisitorController
143 if (null !== $referer) { 144 if (null !== $referer) {
144 $currentUrl = parse_url($referer); 145 $currentUrl = parse_url($referer);
145 // If the referer is not related to Shaarli instance, redirect to default 146 // If the referer is not related to Shaarli instance, redirect to default
146 if (isset($currentUrl['host']) 147 if (
148 isset($currentUrl['host'])
147 && strpos(index_url($this->container->environment), $currentUrl['host']) === false 149 && strpos(index_url($this->container->environment), $currentUrl['host']) === false
148 ) { 150 ) {
149 return $response->withRedirect($defaultPath); 151 return $response->withRedirect($defaultPath);
@@ -172,7 +174,7 @@ abstract class ShaarliVisitorController
172 } 174 }
173 } 175 }
174 176
175 $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; 177 $queryString = count($params) > 0 ? '?' . http_build_query($params) : '';
176 $anchor = $anchor ? '#' . $anchor : ''; 178 $anchor = $anchor ? '#' . $anchor : '';
177 179
178 return $response->withRedirect($path . $queryString . $anchor); 180 return $response->withRedirect($path . $queryString . $anchor);
diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php
index 76ed7690..46d62779 100644
--- a/application/front/controller/visitor/TagCloudController.php
+++ b/application/front/controller/visitor/TagCloudController.php
@@ -47,13 +47,14 @@ class TagCloudController extends ShaarliVisitorController
47 */ 47 */
48 protected function processRequest(string $type, Request $request, Response $response): Response 48 protected function processRequest(string $type, Request $request, Response $response): Response
49 { 49 {
50 $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
50 if ($this->container->loginManager->isLoggedIn() === true) { 51 if ($this->container->loginManager->isLoggedIn() === true) {
51 $visibility = $this->container->sessionManager->getSessionParameter('visibility'); 52 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
52 } 53 }
53 54
54 $sort = $request->getQueryParam('sort'); 55 $sort = $request->getQueryParam('sort');
55 $searchTags = $request->getQueryParam('searchtags'); 56 $searchTags = $request->getQueryParam('searchtags');
56 $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; 57 $filteringTags = $searchTags !== null ? explode($tagsSeparator, $searchTags) : [];
57 58
58 $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); 59 $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
59 60
@@ -71,8 +72,9 @@ class TagCloudController extends ShaarliVisitorController
71 $tagsUrl[escape($tag)] = urlencode((string) $tag); 72 $tagsUrl[escape($tag)] = urlencode((string) $tag);
72 } 73 }
73 74
74 $searchTags = implode(' ', escape($filteringTags)); 75 $searchTags = tags_array2str($filteringTags, $tagsSeparator);
75 $searchTagsUrl = urlencode(implode(' ', $filteringTags)); 76 $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
77 $searchTagsUrl = urlencode($searchTags);
76 $data = [ 78 $data = [
77 'search_tags' => escape($searchTags), 79 'search_tags' => escape($searchTags),
78 'search_tags_url' => $searchTagsUrl, 80 'search_tags_url' => $searchTagsUrl,
@@ -82,10 +84,10 @@ class TagCloudController extends ShaarliVisitorController
82 $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type); 84 $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
83 $this->assignAllView($data); 85 $this->assignAllView($data);
84 86
85 $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; 87 $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) . ' - ' : '';
86 $this->assignView( 88 $this->assignView(
87 'pagetitle', 89 'pagetitle',
88 $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') 90 $searchTags . t('Tag ' . $type) . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
89 ); 91 );
90 92
91 return $response->write($this->render('tag.' . $type)); 93 return $response->write($this->render('tag.' . $type));
diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php
index de4e7ea2..3aa58542 100644
--- a/application/front/controller/visitor/TagController.php
+++ b/application/front/controller/visitor/TagController.php
@@ -27,7 +27,7 @@ class TagController extends ShaarliVisitorController
27 // In case browser does not send HTTP_REFERER, we search a single tag 27 // In case browser does not send HTTP_REFERER, we search a single tag
28 if (null === $referer) { 28 if (null === $referer) {
29 if (null !== $newTag) { 29 if (null !== $newTag) {
30 return $this->redirect($response, '/?searchtags='. urlencode($newTag)); 30 return $this->redirect($response, '/?searchtags=' . urlencode($newTag));
31 } 31 }
32 32
33 return $this->redirect($response, '/'); 33 return $this->redirect($response, '/');
@@ -37,7 +37,7 @@ class TagController extends ShaarliVisitorController
37 parse_str($currentUrl['query'] ?? '', $params); 37 parse_str($currentUrl['query'] ?? '', $params);
38 38
39 if (null === $newTag) { 39 if (null === $newTag) {
40 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); 40 return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
41 } 41 }
42 42
43 // Prevent redirection loop 43 // Prevent redirection loop
@@ -45,9 +45,10 @@ class TagController extends ShaarliVisitorController
45 unset($params['addtag']); 45 unset($params['addtag']);
46 } 46 }
47 47
48 $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
48 // Check if this tag is already in the search query and ignore it if it is. 49 // Check if this tag is already in the search query and ignore it if it is.
49 // Each tag is always separated by a space 50 // Each tag is always separated by a space
50 $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : []; 51 $currentTags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
51 52
52 $addtag = true; 53 $addtag = true;
53 foreach ($currentTags as $value) { 54 foreach ($currentTags as $value) {
@@ -62,12 +63,12 @@ class TagController extends ShaarliVisitorController
62 $currentTags[] = trim($newTag); 63 $currentTags[] = trim($newTag);
63 } 64 }
64 65
65 $params['searchtags'] = trim(implode(' ', $currentTags)); 66 $params['searchtags'] = tags_array2str($currentTags, $tagsSeparator);
66 67
67 // We also remove page (keeping the same page has no sense, since the results are different) 68 // We also remove page (keeping the same page has no sense, since the results are different)
68 unset($params['page']); 69 unset($params['page']);
69 70
70 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); 71 return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
71 } 72 }
72 73
73 /** 74 /**
@@ -89,7 +90,7 @@ class TagController extends ShaarliVisitorController
89 parse_str($currentUrl['query'] ?? '', $params); 90 parse_str($currentUrl['query'] ?? '', $params);
90 91
91 if (null === $tagToRemove) { 92 if (null === $tagToRemove) {
92 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); 93 return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
93 } 94 }
94 95
95 // Prevent redirection loop 96 // Prevent redirection loop
@@ -98,10 +99,11 @@ class TagController extends ShaarliVisitorController
98 } 99 }
99 100
100 if (isset($params['searchtags'])) { 101 if (isset($params['searchtags'])) {
101 $tags = explode(' ', $params['searchtags']); 102 $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
103 $tags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
102 // Remove value from array $tags. 104 // Remove value from array $tags.
103 $tags = array_diff($tags, [$tagToRemove]); 105 $tags = array_diff($tags, [$tagToRemove]);
104 $params['searchtags'] = implode(' ', $tags); 106 $params['searchtags'] = tags_array2str($tags, $tagsSeparator);
105 107
106 if (empty($params['searchtags'])) { 108 if (empty($params['searchtags'])) {
107 unset($params['searchtags']); 109 unset($params['searchtags']);
diff --git a/application/ApplicationUtils.php b/application/helper/ApplicationUtils.php
index 3aa21829..212dd8e2 100644
--- a/application/ApplicationUtils.php
+++ b/application/helper/ApplicationUtils.php
@@ -1,5 +1,6 @@
1<?php 1<?php
2namespace Shaarli; 2
3namespace Shaarli\Helper;
3 4
4use Exception; 5use Exception;
5use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
@@ -14,8 +15,9 @@ class ApplicationUtils
14 */ 15 */
15 public static $VERSION_FILE = 'shaarli_version.php'; 16 public static $VERSION_FILE = 'shaarli_version.php';
16 17
17 private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; 18 public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
18 private static $GIT_BRANCHES = array('latest', 'stable'); 19 public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
20 public static $GIT_BRANCHES = ['latest', 'stable'];
19 private static $VERSION_START_TAG = '<?php /* '; 21 private static $VERSION_START_TAG = '<?php /* ';
20 private static $VERSION_END_TAG = ' */ ?>'; 22 private static $VERSION_END_TAG = ' */ ?>';
21 23
@@ -63,8 +65,8 @@ class ApplicationUtils
63 } 65 }
64 66
65 return str_replace( 67 return str_replace(
66 array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), 68 [self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL],
67 array('', '', ''), 69 ['', '', ''],
68 $data 70 $data
69 ); 71 );
70 } 72 }
@@ -125,7 +127,7 @@ class ApplicationUtils
125 // Late Static Binding allows overriding within tests 127 // Late Static Binding allows overriding within tests
126 // See http://php.net/manual/en/language.oop5.late-static-bindings.php 128 // See http://php.net/manual/en/language.oop5.late-static-bindings.php
127 $latestVersion = static::getVersion( 129 $latestVersion = static::getVersion(
128 self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE 130 self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
129 ); 131 );
130 132
131 if (!$latestVersion) { 133 if (!$latestVersion) {
@@ -171,35 +173,47 @@ class ApplicationUtils
171 /** 173 /**
172 * Checks Shaarli has the proper access permissions to its resources 174 * Checks Shaarli has the proper access permissions to its resources
173 * 175 *
174 * @param ConfigManager $conf Configuration Manager instance. 176 * @param ConfigManager $conf Configuration Manager instance.
177 * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template.
178 * Currently we only need to be able to read the theme and write in raintpl cache.
175 * 179 *
176 * @return array A list of the detected configuration issues 180 * @return array A list of the detected configuration issues
177 */ 181 */
178 public static function checkResourcePermissions($conf) 182 public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
179 { 183 {
180 $errors = array(); 184 $errors = [];
181 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); 185 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
182 186
183 // Check script and template directories are readable 187 // Check script and template directories are readable
184 foreach (array( 188 foreach (
185 'application', 189 [
186 'inc', 190 'application',
187 'plugins', 191 'inc',
188 $rainTplDir, 192 'plugins',
189 $rainTplDir . '/' . $conf->get('resource.theme'), 193 $rainTplDir,
190 ) as $path) { 194 $rainTplDir . '/' . $conf->get('resource.theme'),
195 ] as $path
196 ) {
191 if (!is_readable(realpath($path))) { 197 if (!is_readable(realpath($path))) {
192 $errors[] = '"' . $path . '" ' . t('directory is not readable'); 198 $errors[] = '"' . $path . '" ' . t('directory is not readable');
193 } 199 }
194 } 200 }
195 201
196 // Check cache and data directories are readable and writable 202 // Check cache and data directories are readable and writable
197 foreach (array( 203 if ($minimalMode) {
198 $conf->get('resource.thumbnails_cache'), 204 $folders = [
199 $conf->get('resource.data_dir'), 205 $conf->get('resource.raintpl_tmp'),
200 $conf->get('resource.page_cache'), 206 ];
201 $conf->get('resource.raintpl_tmp'), 207 } else {
202 ) as $path) { 208 $folders = [
209 $conf->get('resource.thumbnails_cache'),
210 $conf->get('resource.data_dir'),
211 $conf->get('resource.page_cache'),
212 $conf->get('resource.raintpl_tmp'),
213 ];
214 }
215
216 foreach ($folders as $path) {
203 if (!is_readable(realpath($path))) { 217 if (!is_readable(realpath($path))) {
204 $errors[] = '"' . $path . '" ' . t('directory is not readable'); 218 $errors[] = '"' . $path . '" ' . t('directory is not readable');
205 } 219 }
@@ -208,14 +222,20 @@ class ApplicationUtils
208 } 222 }
209 } 223 }
210 224
225 if ($minimalMode) {
226 return $errors;
227 }
228
211 // Check configuration files are readable and writable 229 // Check configuration files are readable and writable
212 foreach (array( 230 foreach (
213 $conf->getConfigFileExt(), 231 [
214 $conf->get('resource.datastore'), 232 $conf->getConfigFileExt(),
215 $conf->get('resource.ban_file'), 233 $conf->get('resource.datastore'),
216 $conf->get('resource.log'), 234 $conf->get('resource.ban_file'),
217 $conf->get('resource.update_check'), 235 $conf->get('resource.log'),
218 ) as $path) { 236 $conf->get('resource.update_check'),
237 ] as $path
238 ) {
219 if (!is_file(realpath($path))) { 239 if (!is_file(realpath($path))) {
220 # the file may not exist yet 240 # the file may not exist yet
221 continue; 241 continue;
@@ -246,4 +266,54 @@ class ApplicationUtils
246 { 266 {
247 return hash_hmac('sha256', $currentVersion, $salt); 267 return hash_hmac('sha256', $currentVersion, $salt);
248 } 268 }
269
270 /**
271 * Get a list of PHP extensions used by Shaarli.
272 *
273 * @return array[] List of extension with following keys:
274 * - name: extension name
275 * - required: whether the extension is required to use Shaarli
276 * - desc: short description of extension usage in Shaarli
277 * - loaded: whether the extension is properly loaded or not
278 */
279 public static function getPhpExtensionsRequirement(): array
280 {
281 $extensions = [
282 ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')],
283 ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')],
284 ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')],
285 ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')],
286 ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')],
287 ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')],
288 ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')],
289 ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')],
290 ];
291
292 foreach ($extensions as &$extension) {
293 $extension['loaded'] = extension_loaded($extension['name']);
294 }
295
296 return $extensions;
297 }
298
299 /**
300 * Return the EOL date of given PHP version. If the version is unknown,
301 * we return today + 2 years.
302 *
303 * @param string $fullVersion PHP version, e.g. 7.4.7
304 *
305 * @return string Date format: YYYY-MM-DD
306 */
307 public static function getPhpEol(string $fullVersion): string
308 {
309 preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches);
310
311 return [
312 '7.1' => '2019-12-01',
313 '7.2' => '2020-11-30',
314 '7.3' => '2021-12-06',
315 '7.4' => '2022-11-28',
316 '8.0' => '2023-12-01',
317 ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d');
318 }
249} 319}
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php
new file mode 100644
index 00000000..5fabc907
--- /dev/null
+++ b/application/helper/DailyPageHelper.php
@@ -0,0 +1,208 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Helper;
6
7use Shaarli\Bookmark\Bookmark;
8use Slim\Http\Request;
9
10class DailyPageHelper
11{
12 public const MONTH = 'month';
13 public const WEEK = 'week';
14 public const DAY = 'day';
15
16 /**
17 * Extracts the type of the daily to display from the HTTP request parameters
18 *
19 * @param Request $request HTTP request
20 *
21 * @return string month/week/day
22 */
23 public static function extractRequestedType(Request $request): string
24 {
25 if ($request->getQueryParam(static::MONTH) !== null) {
26 return static::MONTH;
27 } elseif ($request->getQueryParam(static::WEEK) !== null) {
28 return static::WEEK;
29 }
30
31 return static::DAY;
32 }
33
34 /**
35 * Extracts a DateTimeImmutable from provided HTTP request.
36 * If no parameter is provided, we rely on the creation date of the latest provided created bookmark.
37 * If the datastore is empty or no bookmark is provided, we use the current date.
38 *
39 * @param string $type month/week/day
40 * @param string|null $requestedDate Input string extracted from the request
41 * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
42 *
43 * @return \DateTimeImmutable from input or latest bookmark.
44 *
45 * @throws \Exception Type not supported.
46 */
47 public static function extractRequestedDateTime(
48 string $type,
49 ?string $requestedDate,
50 Bookmark $latestBookmark = null
51 ): \DateTimeImmutable {
52 $format = static::getFormatByType($type);
53 if (empty($requestedDate)) {
54 return $latestBookmark instanceof Bookmark
55 ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
56 : new \DateTimeImmutable()
57 ;
58 }
59
60 // W is not supported by createFromFormat...
61 if ($type === static::WEEK) {
62 return (new \DateTimeImmutable())
63 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
64 ;
65 }
66
67 return \DateTimeImmutable::createFromFormat($format, $requestedDate);
68 }
69
70 /**
71 * Get the DateTime format used by provided type
72 * Examples:
73 * - day: 20201016 (<year><month><day>)
74 * - week: 202041 (<year><week number>)
75 * - month: 202010 (<year><month>)
76 *
77 * @param string $type month/week/day
78 *
79 * @return string DateTime compatible format
80 *
81 * @see https://www.php.net/manual/en/datetime.format.php
82 *
83 * @throws \Exception Type not supported.
84 */
85 public static function getFormatByType(string $type): string
86 {
87 switch ($type) {
88 case static::MONTH:
89 return 'Ym';
90 case static::WEEK:
91 return 'YW';
92 case static::DAY:
93 return 'Ymd';
94 default:
95 throw new \Exception('Unsupported daily format type');
96 }
97 }
98
99 /**
100 * Get the first DateTime of the time period depending on given datetime and type.
101 * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
102 * and we don't want to alter original datetime.
103 *
104 * @param string $type month/week/day
105 * @param \DateTimeImmutable $requested DateTime extracted from request input
106 * (should come from extractRequestedDateTime)
107 *
108 * @return \DateTimeInterface First DateTime of the time period
109 *
110 * @throws \Exception Type not supported.
111 */
112 public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
113 {
114 switch ($type) {
115 case static::MONTH:
116 return $requested->modify('first day of this month midnight');
117 case static::WEEK:
118 return $requested->modify('Monday this week midnight');
119 case static::DAY:
120 return $requested->modify('Today midnight');
121 default:
122 throw new \Exception('Unsupported daily format type');
123 }
124 }
125
126 /**
127 * Get the last DateTime of the time period depending on given datetime and type.
128 * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
129 * and we don't want to alter original datetime.
130 *
131 * @param string $type month/week/day
132 * @param \DateTimeImmutable $requested DateTime extracted from request input
133 * (should come from extractRequestedDateTime)
134 *
135 * @return \DateTimeInterface Last DateTime of the time period
136 *
137 * @throws \Exception Type not supported.
138 */
139 public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
140 {
141 switch ($type) {
142 case static::MONTH:
143 return $requested->modify('last day of this month 23:59:59');
144 case static::WEEK:
145 return $requested->modify('Sunday this week 23:59:59');
146 case static::DAY:
147 return $requested->modify('Today 23:59:59');
148 default:
149 throw new \Exception('Unsupported daily format type');
150 }
151 }
152
153 /**
154 * Get localized description of the time period depending on given datetime and type.
155 * Example: for a month period, it returns `October, 2020`.
156 *
157 * @param string $type month/week/day
158 * @param \DateTimeImmutable $requested DateTime extracted from request input
159 * (should come from extractRequestedDateTime)
160 *
161 * @return string Localized time period description
162 *
163 * @throws \Exception Type not supported.
164 */
165 public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string
166 {
167 switch ($type) {
168 case static::MONTH:
169 return $requested->format('F') . ', ' . $requested->format('Y');
170 case static::WEEK:
171 $requested = $requested->modify('Monday this week');
172 return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
173 case static::DAY:
174 $out = '';
175 if ($requested->format('Ymd') === date('Ymd')) {
176 $out = t('Today') . ' - ';
177 } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
178 $out = t('Yesterday') . ' - ';
179 }
180 return $out . format_date($requested, false);
181 default:
182 throw new \Exception('Unsupported daily format type');
183 }
184 }
185
186 /**
187 * Get the number of items to display in the RSS feed depending on the given type.
188 *
189 * @param string $type month/week/day
190 *
191 * @return int number of elements
192 *
193 * @throws \Exception Type not supported.
194 */
195 public static function getRssLengthByType(string $type): int
196 {
197 switch ($type) {
198 case static::MONTH:
199 return 12; // 1 year
200 case static::WEEK:
201 return 26; // ~6 months
202 case static::DAY:
203 return 30; // ~1 month
204 default:
205 throw new \Exception('Unsupported daily format type');
206 }
207 }
208}
diff --git a/application/FileUtils.php b/application/helper/FileUtils.php
index 30560bfc..e8a2168c 100644
--- a/application/FileUtils.php
+++ b/application/helper/FileUtils.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2 2
3namespace Shaarli; 3namespace Shaarli\Helper;
4 4
5use Shaarli\Exceptions\IOException; 5use 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 */
15class HttpAccess 15class 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 */
38function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null) 40function 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 */
511function get_curl_header_callback(
512 &$charset,
513 $curlGetInfo = 'curl_getinfo'
514) {
515 $isRedirected = false;
516
517 return function ($ch, $data) use ($curlGetInfo, &$charset, &$isRedirected) {
518 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
519 $chunkLength = strlen($data);
520 if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
521 $isRedirected = true;
522 return $chunkLength;
523 }
524 if (!empty($responseCode) && $responseCode !== 200) {
525 return false;
526 }
527 // After a redirection, the content type will keep the previous request value
528 // until it finds the next content-type header.
529 if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
530 $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
531 }
532 if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
533 return false;
534 }
535 if (!empty($contentType) && empty($charset)) {
536 $charset = header_extract_charset($contentType);
537 }
538
539 return $chunkLength;
540 };
541}
542
543/**
544 * Get cURL callback function for CURLOPT_WRITEFUNCTION
545 *
546 * @param string $charset to extract from the downloaded page (reference)
496 * @param string $title to extract from the downloaded page (reference) 547 * @param string $title to extract from the downloaded page (reference)
497 * @param string $description to extract from the downloaded page (reference) 548 * @param string $description to extract from the downloaded page (reference)
498 * @param string $keywords to extract from the downloaded page (reference) 549 * @param string $keywords to extract from the downloaded page (reference)
@@ -507,9 +558,8 @@ function get_curl_download_callback(
507 &$description, 558 &$description,
508 &$keywords, 559 &$keywords,
509 $retrieveDescription, 560 $retrieveDescription,
510 $curlGetInfo = 'curl_getinfo' 561 $tagsSeparator
511) { 562) {
512 $isRedirected = false;
513 $currentChunk = 0; 563 $currentChunk = 0;
514 $foundChunk = null; 564 $foundChunk = null;
515 565
@@ -524,37 +574,22 @@ function get_curl_download_callback(
524 * 574 *
525 * @return int|bool length of $data or false if we need to stop the download 575 * @return int|bool length of $data or false if we need to stop the download
526 */ 576 */
527 return function (&$ch, $data) use ( 577 return function (
578 $ch,
579 $data
580 ) use (
528 $retrieveDescription, 581 $retrieveDescription,
529 $curlGetInfo, 582 $tagsSeparator,
530 &$charset, 583 &$charset,
531 &$title, 584 &$title,
532 &$description, 585 &$description,
533 &$keywords, 586 &$keywords,
534 &$isRedirected,
535 &$currentChunk, 587 &$currentChunk,
536 &$foundChunk 588 &$foundChunk
537 ) { 589 ) {
590 $chunkLength = strlen($data);
538 $currentChunk++; 591 $currentChunk++;
539 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); 592
540 if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
541 $isRedirected = true;
542 return strlen($data);
543 }
544 if (!empty($responseCode) && $responseCode !== 200) {
545 return false;
546 }
547 // After a redirection, the content type will keep the previous request value
548 // until it finds the next content-type header.
549 if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
550 $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
551 }
552 if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
553 return false;
554 }
555 if (!empty($contentType) && empty($charset)) {
556 $charset = header_extract_charset($contentType);
557 }
558 if (empty($charset)) { 593 if (empty($charset)) {
559 $charset = html_extract_charset($data); 594 $charset = html_extract_charset($data);
560 } 595 }
@@ -562,6 +597,10 @@ function get_curl_download_callback(
562 $title = html_extract_title($data); 597 $title = html_extract_title($data);
563 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; 598 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
564 } 599 }
600 if (empty($title)) {
601 $title = html_extract_tag('title', $data);
602 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
603 }
565 if ($retrieveDescription && empty($description)) { 604 if ($retrieveDescription && empty($description)) {
566 $description = html_extract_tag('description', $data); 605 $description = html_extract_tag('description', $data);
567 $foundChunk = ! empty($description) ? $currentChunk : $foundChunk; 606 $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
@@ -571,10 +610,10 @@ function get_curl_download_callback(
571 if (! empty($keywords)) { 610 if (! empty($keywords)) {
572 $foundChunk = $currentChunk; 611 $foundChunk = $currentChunk;
573 // Keywords use the format tag1, tag2 multiple words, tag 612 // Keywords use the format tag1, tag2 multiple words, tag
574 // So we format them to match Shaarli's separator and glue multiple words with '-' 613 // So we split the result with `,`, then if a tag contains the separator we replace it by `-`.
575 $keywords = implode(' ', array_map(function($keyword) { 614 $keywords = tags_array2str(array_map(function (string $keyword) use ($tagsSeparator): string {
576 return implode('-', preg_split('/\s+/', trim($keyword))); 615 return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-');
577 }, explode(',', $keywords))); 616 }, tags_str2array($keywords, ',')), $tagsSeparator);
578 } 617 }
579 } 618 }
580 619
@@ -582,7 +621,8 @@ function get_curl_download_callback(
582 // If we already found either the title, description or keywords, 621 // If we already found either the title, description or keywords,
583 // it's highly unlikely that we'll found the other metas further than 622 // it's highly unlikely that we'll found the other metas further than
584 // in the same chunk of data or the next one. So we also stop the download after that. 623 // in the same chunk of data or the next one. So we also stop the download after that.
585 if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null 624 if (
625 (!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
586 && (! $retrieveDescription 626 && (! $retrieveDescription
587 || $foundChunk < $currentChunk 627 || $foundChunk < $currentChunk
588 || (!empty($title) && !empty($description) && !empty($keywords)) 628 || (!empty($title) && !empty($description) && !empty($keywords))
@@ -591,6 +631,6 @@ function get_curl_download_callback(
591 return false; 631 return false;
592 } 632 }
593 633
594 return strlen($data); 634 return $chunkLength;
595 }; 635 };
596} 636}
diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php
new file mode 100644
index 00000000..2e1401ec
--- /dev/null
+++ b/application/http/MetadataRetriever.php
@@ -0,0 +1,69 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Http;
6
7use Shaarli\Config\ConfigManager;
8
9/**
10 * HTTP Tool used to extract metadata from external URL (title, description, etc.).
11 */
12class MetadataRetriever
13{
14 /** @var ConfigManager */
15 protected $conf;
16
17 /** @var HttpAccess */
18 protected $httpAccess;
19
20 public function __construct(ConfigManager $conf, HttpAccess $httpAccess)
21 {
22 $this->conf = $conf;
23 $this->httpAccess = $httpAccess;
24 }
25
26 /**
27 * Retrieve metadata for given URL.
28 *
29 * @return array [
30 * 'title' => <remote title>,
31 * 'description' => <remote description>,
32 * 'tags' => <remote keywords>,
33 * ]
34 */
35 public function retrieve(string $url): array
36 {
37 $charset = null;
38 $title = null;
39 $description = null;
40 $tags = null;
41
42 // Short timeout to keep the application responsive
43 // The callback will fill $charset and $title with data from the downloaded page.
44 $this->httpAccess->getHttpResponse(
45 $url,
46 $this->conf->get('general.download_timeout', 30),
47 $this->conf->get('general.download_max_size', 4194304),
48 $this->httpAccess->getCurlHeaderCallback($charset),
49 $this->httpAccess->getCurlDownloadCallback(
50 $charset,
51 $title,
52 $description,
53 $tags,
54 $this->conf->get('general.retrieve_description'),
55 $this->conf->get('general.tags_separator', ' ')
56 )
57 );
58
59 if (!empty($title) && strtolower($charset) !== 'utf-8') {
60 $title = mb_convert_encoding($title, 'utf-8', $charset);
61 }
62
63 return [
64 'title' => $title,
65 'description' => $description,
66 'tags' => $tags,
67 ];
68 }
69}
diff --git a/application/http/Url.php b/application/http/Url.php
index 90444a2f..fe87088f 100644
--- a/application/http/Url.php
+++ b/application/http/Url.php
@@ -17,7 +17,7 @@ namespace Shaarli\Http;
17 */ 17 */
18class Url 18class Url
19{ 19{
20 private static $annoyingQueryParams = array( 20 private static $annoyingQueryParams = [
21 // Facebook 21 // Facebook
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 */
13function unparse_url($parsedUrl) 14function 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;
8use Iterator; 8use Iterator;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Exceptions\IOException; 10use Shaarli\Exceptions\IOException;
11use Shaarli\FileUtils; 11use Shaarli\Helper\FileUtils;
12use Shaarli\Render\PageCacheManager; 12use 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;
7use ReflectionClass; 7use ReflectionClass;
8use ReflectionException; 8use ReflectionException;
9use ReflectionMethod; 9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\Bookmark; 10use Shaarli\Bookmark\Bookmark;
12use Shaarli\Bookmark\BookmarkArray; 11use Shaarli\Bookmark\BookmarkArray;
13use Shaarli\Bookmark\BookmarkFilter; 12use Shaarli\Bookmark\BookmarkFilter;
@@ -17,6 +16,7 @@ use Shaarli\Config\ConfigJson;
17use Shaarli\Config\ConfigManager; 16use Shaarli\Config\ConfigManager;
18use Shaarli\Config\ConfigPhp; 17use Shaarli\Config\ConfigPhp;
19use Shaarli\Exceptions\IOException; 18use Shaarli\Exceptions\IOException;
19use Shaarli\Helper\ApplicationUtils;
20use Shaarli\Thumbnailer; 20use Shaarli\Thumbnailer;
21use Shaarli\Updater\Exception\UpdaterException; 21use Shaarli\Updater\Exception\UpdaterException;
22 22
@@ -93,7 +93,7 @@ class LegacyUpdater
93 */ 93 */
94 public function update() 94 public function update()
95 { 95 {
96 $updatesRan = array(); 96 $updatesRan = [];
97 97
98 // If the user isn't logged in, exit without updating. 98 // If the user isn't logged in, exit without updating.
99 if ($this->isLoggedIn !== true) { 99 if ($this->isLoggedIn !== true) {
@@ -106,7 +106,8 @@ class LegacyUpdater
106 106
107 foreach ($this->methods as $method) { 107 foreach ($this->methods as $method) {
108 // Not an update method or already done, pass. 108 // Not an update method or already done, pass.
109 if (!startsWith($method->getName(), 'updateMethod') 109 if (
110 !startsWith($method->getName(), 'updateMethod')
110 || in_array($method->getName(), $this->doneUpdates) 111 || in_array($method->getName(), $this->doneUpdates)
111 ) { 112 ) {
112 continue; 113 continue;
@@ -189,7 +190,7 @@ class LegacyUpdater
189 } 190 }
190 191
191 // Set sub config keys (config and plugins) 192 // Set sub config keys (config and plugins)
192 $subConfig = array('config', 'plugins'); 193 $subConfig = ['config', 'plugins'];
193 foreach ($subConfig as $sub) { 194 foreach ($subConfig as $sub) {
194 foreach ($oldConfig[$sub] as $key => $value) { 195 foreach ($oldConfig[$sub] as $key => $value) {
195 if (isset($legacyMap[$sub . '.' . $key])) { 196 if (isset($legacyMap[$sub . '.' . $key])) {
@@ -259,7 +260,7 @@ class LegacyUpdater
259 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php'; 260 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
260 copy($this->conf->get('resource.datastore'), $save); 261 copy($this->conf->get('resource.datastore'), $save);
261 262
262 $links = array(); 263 $links = [];
263 foreach ($this->linkDB as $offset => $value) { 264 foreach ($this->linkDB as $offset => $value) {
264 $links[] = $value; 265 $links[] = $value;
265 unset($this->linkDB[$offset]); 266 unset($this->linkDB[$offset]);
@@ -498,7 +499,8 @@ class LegacyUpdater
498 */ 499 */
499 public function updateMethodDownloadSizeAndTimeoutConf() 500 public function updateMethodDownloadSizeAndTimeoutConf()
500 { 501 {
501 if ($this->conf->exists('general.download_max_size') 502 if (
503 $this->conf->exists('general.download_max_size')
502 && $this->conf->exists('general.download_timeout') 504 && $this->conf->exists('general.download_timeout')
503 ) { 505 ) {
504 return true; 506 return true;
@@ -585,7 +587,7 @@ class LegacyUpdater
585 587
586 $linksArray = new BookmarkArray(); 588 $linksArray = new BookmarkArray();
587 foreach ($this->linkDB as $key => $link) { 589 foreach ($this->linkDB as $key => $link) {
588 $linksArray[$key] = (new Bookmark())->fromArray($link); 590 $linksArray[$key] = (new Bookmark())->fromArray($link, $this->conf->get('general.tags_separator', ' '));
589 } 591 }
590 $linksIo = new BookmarkIO($this->conf); 592 $linksIo = new BookmarkIO($this->conf);
591 $linksIo->write($linksArray); 593 $linksIo->write($linksArray);
diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php
index b83f16f8..2d97b4c8 100644
--- a/application/netscape/NetscapeBookmarkUtils.php
+++ b/application/netscape/NetscapeBookmarkUtils.php
@@ -59,11 +59,11 @@ class NetscapeBookmarkUtils
59 $indexUrl 59 $indexUrl
60 ) { 60 ) {
61 // see tpl/export.html for possible values 61 // see tpl/export.html for possible values
62 if (!in_array($selection, array('all', 'public', 'private'))) { 62 if (!in_array($selection, ['all', 'public', 'private'])) {
63 throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"'); 63 throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
64 } 64 }
65 65
66 $bookmarkLinks = array(); 66 $bookmarkLinks = [];
67 foreach ($this->bookmarkService->search([], $selection) as $bookmark) { 67 foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
68 $link = $formatter->format($bookmark); 68 $link = $formatter->format($bookmark);
69 $link['taglist'] = implode(',', $bookmark->getTags()); 69 $link['taglist'] = implode(',', $bookmark->getTags());
@@ -101,11 +101,11 @@ class NetscapeBookmarkUtils
101 101
102 // Add tags to all imported bookmarks? 102 // Add tags to all imported bookmarks?
103 if (empty($post['default_tags'])) { 103 if (empty($post['default_tags'])) {
104 $defaultTags = array(); 104 $defaultTags = [];
105 } else { 105 } else {
106 $defaultTags = preg_split( 106 $defaultTags = tags_str2array(
107 '/[\s,]+/', 107 escape($post['default_tags']),
108 escape($post['default_tags']) 108 $this->conf->get('general.tags_separator', ' ')
109 ); 109 );
110 } 110 }
111 111
@@ -171,7 +171,7 @@ class NetscapeBookmarkUtils
171 $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols')); 171 $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
172 $link->setDescription($bkm['note']); 172 $link->setDescription($bkm['note']);
173 $link->setPrivate($private); 173 $link->setPrivate($private);
174 $link->setTagsString($bkm['tags']); 174 $link->setTags($bkm['tags']);
175 175
176 $this->bookmarkService->addOrSet($link, false); 176 $this->bookmarkService->addOrSet($link, false);
177 $importCount++; 177 $importCount++;
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php
index 1b2197c9..3ea55728 100644
--- a/application/plugin/PluginManager.php
+++ b/application/plugin/PluginManager.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Plugin; 3namespace Shaarli\Plugin;
3 4
4use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
@@ -23,7 +24,7 @@ class PluginManager
23 * 24 *
24 * @var array $loadedPlugins 25 * @var array $loadedPlugins
25 */ 26 */
26 private $loadedPlugins = array(); 27 private $loadedPlugins = [];
27 28
28 /** 29 /**
29 * @var ConfigManager Configuration Manager instance. 30 * @var ConfigManager Configuration Manager instance.
@@ -57,7 +58,7 @@ class PluginManager
57 public function __construct(&$conf) 58 public function __construct(&$conf)
58 { 59 {
59 $this->conf = $conf; 60 $this->conf = $conf;
60 $this->errors = array(); 61 $this->errors = [];
61 } 62 }
62 63
63 /** 64 /**
@@ -98,12 +99,13 @@ class PluginManager
98 * 99 *
99 * @return void 100 * @return void
100 */ 101 */
101 public function executeHooks($hook, &$data, $params = array()) 102 public function executeHooks($hook, &$data, $params = [])
102 { 103 {
103 $metadataParameters = [ 104 $metadataParameters = [
104 'target' => '_PAGE_', 105 'target' => '_PAGE_',
105 'loggedin' => '_LOGGEDIN_', 106 'loggedin' => '_LOGGEDIN_',
106 'basePath' => '_BASE_PATH_', 107 'basePath' => '_BASE_PATH_',
108 'rootPath' => '_ROOT_PATH_',
107 'bookmarkService' => '_BOOKMARK_SERVICE_', 109 'bookmarkService' => '_BOOKMARK_SERVICE_',
108 ]; 110 ];
109 111
@@ -195,7 +197,7 @@ class PluginManager
195 */ 197 */
196 public function getPluginsMeta() 198 public function getPluginsMeta()
197 { 199 {
198 $metaData = array(); 200 $metaData = [];
199 $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK); 201 $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
200 202
201 // Browse all plugin directories. 203 // Browse all plugin directories.
@@ -216,9 +218,9 @@ class PluginManager
216 if (isset($metaData[$plugin]['parameters'])) { 218 if (isset($metaData[$plugin]['parameters'])) {
217 $params = explode(';', $metaData[$plugin]['parameters']); 219 $params = explode(';', $metaData[$plugin]['parameters']);
218 } else { 220 } else {
219 $params = array(); 221 $params = [];
220 } 222 }
221 $metaData[$plugin]['parameters'] = array(); 223 $metaData[$plugin]['parameters'] = [];
222 foreach ($params as $param) { 224 foreach ($params as $param) {
223 if (empty($param)) { 225 if (empty($param)) {
224 continue; 226 continue;
diff --git a/application/plugin/exception/PluginFileNotFoundException.php b/application/plugin/exception/PluginFileNotFoundException.php
index e5386f02..21ac6604 100644
--- a/application/plugin/exception/PluginFileNotFoundException.php
+++ b/application/plugin/exception/PluginFileNotFoundException.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Plugin\Exception; 3namespace Shaarli\Plugin\Exception;
3 4
4use Exception; 5use Exception;
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index 41b357dd..bf0ae326 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -3,11 +3,11 @@
3namespace Shaarli\Render; 3namespace Shaarli\Render;
4 4
5use Exception; 5use Exception;
6use exceptions\MissingBasePathException; 6use Psr\Log\LoggerInterface;
7use RainTPL; 7use RainTPL;
8use Shaarli\ApplicationUtils;
9use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
10use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Helper\ApplicationUtils;
11use Shaarli\Security\SessionManager; 11use Shaarli\Security\SessionManager;
12use Shaarli\Thumbnailer; 12use Shaarli\Thumbnailer;
13 13
@@ -35,6 +35,9 @@ class PageBuilder
35 */ 35 */
36 protected $session; 36 protected $session;
37 37
38 /** @var LoggerInterface */
39 protected $logger;
40
38 /** 41 /**
39 * @var BookmarkServiceInterface $bookmarkService instance. 42 * @var BookmarkServiceInterface $bookmarkService instance.
40 */ 43 */
@@ -54,17 +57,25 @@ class PageBuilder
54 * PageBuilder constructor. 57 * PageBuilder constructor.
55 * $tpl is initialized at false for lazy loading. 58 * $tpl is initialized at false for lazy loading.
56 * 59 *
57 * @param ConfigManager $conf Configuration Manager instance (reference). 60 * @param ConfigManager $conf Configuration Manager instance (reference).
58 * @param array $session $_SESSION array 61 * @param array $session $_SESSION array
59 * @param BookmarkServiceInterface $linkDB instance. 62 * @param LoggerInterface $logger
60 * @param string $token Session token 63 * @param null $linkDB instance.
61 * @param bool $isLoggedIn 64 * @param null $token Session token
65 * @param bool $isLoggedIn
62 */ 66 */
63 public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) 67 public function __construct(
64 { 68 ConfigManager &$conf,
69 array $session,
70 LoggerInterface $logger,
71 $linkDB = null,
72 $token = null,
73 $isLoggedIn = false
74 ) {
65 $this->tpl = false; 75 $this->tpl = false;
66 $this->conf = $conf; 76 $this->conf = $conf;
67 $this->session = $session; 77 $this->session = $session;
78 $this->logger = $logger;
68 $this->bookmarkService = $linkDB; 79 $this->bookmarkService = $linkDB;
69 $this->token = $token; 80 $this->token = $token;
70 $this->isLoggedIn = $isLoggedIn; 81 $this->isLoggedIn = $isLoggedIn;
@@ -98,7 +109,7 @@ class PageBuilder
98 $this->tpl->assign('newVersion', escape($version)); 109 $this->tpl->assign('newVersion', escape($version));
99 $this->tpl->assign('versionError', ''); 110 $this->tpl->assign('versionError', '');
100 } catch (Exception $exc) { 111 } catch (Exception $exc) {
101 logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); 112 $this->logger->error(format_log('Error: ' . $exc->getMessage(), client_ip_id($_SERVER)));
102 $this->tpl->assign('newVersion', ''); 113 $this->tpl->assign('newVersion', '');
103 $this->tpl->assign('versionError', escape($exc->getMessage())); 114 $this->tpl->assign('versionError', escape($exc->getMessage()));
104 } 115 }
@@ -149,7 +160,8 @@ class PageBuilder
149 160
150 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); 161 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
151 162
152 $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE']); 163 $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20);
164 $this->tpl->assign('tags_separator', $this->conf->get('general.tags_separator', ' '));
153 165
154 // To be removed with a proper theme configuration. 166 // To be removed with a proper theme configuration.
155 $this->tpl->assign('conf', $this->conf); 167 $this->tpl->assign('conf', $this->conf);
@@ -174,10 +186,12 @@ class PageBuilder
174 } 186 }
175 } 187 }
176 188
189 $rootPath = preg_replace('#/index\.php$#', '', $basePath);
177 $this->assign('base_path', $basePath); 190 $this->assign('base_path', $basePath);
191 $this->assign('root_path', $rootPath);
178 $this->assign( 192 $this->assign(
179 'asset_path', 193 'asset_path',
180 $basePath . '/' . 194 $rootPath . '/' .
181 rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . 195 rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' .
182 $this->conf->get('resource.theme', 'default') 196 $this->conf->get('resource.theme', 'default')
183 ); 197 );
diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php
index 8af8228a..03b424f3 100644
--- a/application/render/TemplatePage.php
+++ b/application/render/TemplatePage.php
@@ -14,6 +14,7 @@ interface TemplatePage
14 public const DAILY = 'daily'; 14 public const DAILY = 'daily';
15 public const DAILY_RSS = 'dailyrss'; 15 public const DAILY_RSS = 'dailyrss';
16 public const EDIT_LINK = 'editlink'; 16 public const EDIT_LINK = 'editlink';
17 public const EDIT_LINK_BATCH = 'editlink.batch';
17 public const ERROR = 'error'; 18 public const ERROR = 'error';
18 public const EXPORT = 'export'; 19 public const EXPORT = 'export';
19 public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks'; 20 public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
diff --git a/application/render/ThemeUtils.php b/application/render/ThemeUtils.php
index 86096c64..18471f0a 100644
--- a/application/render/ThemeUtils.php
+++ b/application/render/ThemeUtils.php
@@ -23,10 +23,10 @@ class ThemeUtils
23 public static function getThemes($tplDir) 23 public static function getThemes($tplDir)
24 { 24 {
25 $tplDir = rtrim($tplDir, '/'); 25 $tplDir = rtrim($tplDir, '/');
26 $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR); 26 $allTheme = glob($tplDir . '/*', GLOB_ONLYDIR);
27 $themes = []; 27 $themes = [];
28 foreach ($allTheme as $value) { 28 foreach ($allTheme as $value) {
29 $themes[] = str_replace($tplDir.'/', '', $value); 29 $themes[] = str_replace($tplDir . '/', '', $value);
30 } 30 }
31 31
32 return $themes; 32 return $themes;
diff --git a/application/security/BanManager.php b/application/security/BanManager.php
index 68190c54..7077af5b 100644
--- a/application/security/BanManager.php
+++ b/application/security/BanManager.php
@@ -1,9 +1,9 @@
1<?php 1<?php
2 2
3
4namespace Shaarli\Security; 3namespace Shaarli\Security;
5 4
6use Shaarli\FileUtils; 5use Psr\Log\LoggerInterface;
6use Shaarli\Helper\FileUtils;
7 7
8/** 8/**
9 * Class BanManager 9 * Class BanManager
@@ -28,8 +28,8 @@ class BanManager
28 /** @var string Path to the file containing IP bans and failures */ 28 /** @var string Path to the file containing IP bans and failures */
29 protected $banFile; 29 protected $banFile;
30 30
31 /** @var string Path to the log file, used to log bans */ 31 /** @var LoggerInterface Path to the log file, used to log bans */
32 protected $logFile; 32 protected $logger;
33 33
34 /** @var array List of IP with their associated number of failed attempts */ 34 /** @var array List of IP with their associated number of failed attempts */
35 protected $failures = []; 35 protected $failures = [];
@@ -40,18 +40,20 @@ class BanManager
40 /** 40 /**
41 * BanManager constructor. 41 * BanManager constructor.
42 * 42 *
43 * @param array $trustedProxies List of allowed proxies IP 43 * @param array $trustedProxies List of allowed proxies IP
44 * @param int $nbAttempts Number of allowed failed attempt before the ban 44 * @param int $nbAttempts Number of allowed failed attempt before the ban
45 * @param int $banDuration Ban duration in seconds 45 * @param int $banDuration Ban duration in seconds
46 * @param string $banFile Path to the file containing IP bans and failures 46 * @param string $banFile Path to the file containing IP bans and failures
47 * @param string $logFile Path to the log file, used to log bans 47 * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory
48 */ 48 */
49 public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) { 49 public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger)
50 {
50 $this->trustedProxies = $trustedProxies; 51 $this->trustedProxies = $trustedProxies;
51 $this->nbAttempts = $nbAttempts; 52 $this->nbAttempts = $nbAttempts;
52 $this->banDuration = $banDuration; 53 $this->banDuration = $banDuration;
53 $this->banFile = $banFile; 54 $this->banFile = $banFile;
54 $this->logFile = $logFile; 55 $this->logger = $logger;
56
55 $this->readBanFile(); 57 $this->readBanFile();
56 } 58 }
57 59
@@ -78,11 +80,7 @@ class BanManager
78 80
79 if ($this->failures[$ip] >= $this->nbAttempts) { 81 if ($this->failures[$ip] >= $this->nbAttempts) {
80 $this->bans[$ip] = time() + $this->banDuration; 82 $this->bans[$ip] = time() + $this->banDuration;
81 logm( 83 $this->logger->info(format_log('IP address banned from login: ' . $ip, $ip));
82 $this->logFile,
83 $server['REMOTE_ADDR'],
84 'IP address banned from login: '. $ip
85 );
86 } 84 }
87 $this->writeBanFile(); 85 $this->writeBanFile();
88 } 86 }
@@ -138,7 +136,7 @@ class BanManager
138 unset($this->failures[$ip]); 136 unset($this->failures[$ip]);
139 } 137 }
140 unset($this->bans[$ip]); 138 unset($this->bans[$ip]);
141 logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); 139 $this->logger->info(format_log('Ban lifted for: ' . $ip, $ip));
142 140
143 $this->writeBanFile(); 141 $this->writeBanFile();
144 return false; 142 return false;
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
index d74c3118..b795b80e 100644
--- a/application/security/LoginManager.php
+++ b/application/security/LoginManager.php
@@ -1,7 +1,9 @@
1<?php 1<?php
2
2namespace Shaarli\Security; 3namespace Shaarli\Security;
3 4
4use Exception; 5use Exception;
6use Psr\Log\LoggerInterface;
5use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
6 8
7/** 9/**
@@ -31,26 +33,30 @@ class LoginManager
31 protected $staySignedInToken = ''; 33 protected $staySignedInToken = '';
32 /** @var CookieManager */ 34 /** @var CookieManager */
33 protected $cookieManager; 35 protected $cookieManager;
36 /** @var LoggerInterface */
37 protected $logger;
34 38
35 /** 39 /**
36 * Constructor 40 * Constructor
37 * 41 *
38 * @param ConfigManager $configManager Configuration Manager instance 42 * @param ConfigManager $configManager Configuration Manager instance
39 * @param SessionManager $sessionManager SessionManager instance 43 * @param SessionManager $sessionManager SessionManager instance
40 * @param CookieManager $cookieManager CookieManager instance 44 * @param CookieManager $cookieManager CookieManager instance
45 * @param BanManager $banManager
46 * @param LoggerInterface $logger Used to log login attempts
41 */ 47 */
42 public function __construct($configManager, $sessionManager, $cookieManager) 48 public function __construct(
43 { 49 ConfigManager $configManager,
50 SessionManager $sessionManager,
51 CookieManager $cookieManager,
52 BanManager $banManager,
53 LoggerInterface $logger
54 ) {
44 $this->configManager = $configManager; 55 $this->configManager = $configManager;
45 $this->sessionManager = $sessionManager; 56 $this->sessionManager = $sessionManager;
46 $this->cookieManager = $cookieManager; 57 $this->cookieManager = $cookieManager;
47 $this->banManager = new BanManager( 58 $this->banManager = $banManager;
48 $this->configManager->get('security.trusted_proxies', []), 59 $this->logger = $logger;
49 $this->configManager->get('security.ban_after'),
50 $this->configManager->get('security.ban_duration'),
51 $this->configManager->get('resource.ban_file', 'data/ipbans.php'),
52 $this->configManager->get('resource.log')
53 );
54 60
55 if ($this->configManager->get('security.open_shaarli') === true) { 61 if ($this->configManager->get('security.open_shaarli') === true) {
56 $this->openShaarli = true; 62 $this->openShaarli = true;
@@ -101,7 +107,8 @@ class LoginManager
101 // The user client has a valid stay-signed-in cookie 107 // The user client has a valid stay-signed-in cookie
102 // Session information is updated with the current client information 108 // Session information is updated with the current client information
103 $this->sessionManager->storeLoginInfo($clientIpId); 109 $this->sessionManager->storeLoginInfo($clientIpId);
104 } elseif ($this->sessionManager->hasSessionExpired() 110 } elseif (
111 $this->sessionManager->hasSessionExpired()
105 || $this->sessionManager->hasClientIpChanged($clientIpId) 112 || $this->sessionManager->hasClientIpChanged($clientIpId)
106 ) { 113 ) {
107 $this->sessionManager->logout(); 114 $this->sessionManager->logout();
@@ -118,7 +125,7 @@ class LoginManager
118 * 125 *
119 * @return true when the user is logged in, false otherwise 126 * @return true when the user is logged in, false otherwise
120 */ 127 */
121 public function isLoggedIn() 128 public function isLoggedIn(): bool
122 { 129 {
123 if ($this->openShaarli) { 130 if ($this->openShaarli) {
124 return true; 131 return true;
@@ -129,48 +136,35 @@ class LoginManager
129 /** 136 /**
130 * Check user credentials are valid 137 * Check user credentials are valid
131 * 138 *
132 * @param string $remoteIp Remote client IP address
133 * @param string $clientIpId Client IP address identifier 139 * @param string $clientIpId Client IP address identifier
134 * @param string $login Username 140 * @param string $login Username
135 * @param string $password Password 141 * @param string $password Password
136 * 142 *
137 * @return bool true if the provided credentials are valid, false otherwise 143 * @return bool true if the provided credentials are valid, false otherwise
138 */ 144 */
139 public function checkCredentials($remoteIp, $clientIpId, $login, $password) 145 public function checkCredentials($clientIpId, $login, $password)
140 { 146 {
141 // Check login matches config
142 if ($login !== $this->configManager->get('credentials.login')) {
143 return false;
144 }
145
146 // Check credentials 147 // Check credentials
147 try { 148 try {
148 $useLdapLogin = !empty($this->configManager->get('ldap.host')); 149 $useLdapLogin = !empty($this->configManager->get('ldap.host'));
149 if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) 150 if (
150 || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) 151 $login === $this->configManager->get('credentials.login')
152 && (
153 (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password))
154 || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password))
155 )
151 ) { 156 ) {
152 $this->sessionManager->storeLoginInfo($clientIpId); 157 $this->sessionManager->storeLoginInfo($clientIpId);
153 logm( 158 $this->logger->info(format_log('Login successful', $clientIpId));
154 $this->configManager->get('resource.log'), 159
155 $remoteIp, 160 return true;
156 'Login successful'
157 );
158 return true;
159 } 161 }
160 } 162 } catch (Exception $exception) {
161 catch(Exception $exception) { 163 $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId));
162 logm(
163 $this->configManager->get('resource.log'),
164 $remoteIp,
165 'Exception while checking credentials: ' . $exception
166 );
167 } 164 }
168 165
169 logm( 166 $this->logger->info(format_log('Login failed for user ' . $login, $clientIpId));
170 $this->configManager->get('resource.log'), 167
171 $remoteIp,
172 'Login failed for user ' . $login
173 );
174 return false; 168 return false;
175 } 169 }
176 170
@@ -183,7 +177,8 @@ class LoginManager
183 * 177 *
184 * @return bool true if the provided credentials are valid, false otherwise 178 * @return bool true if the provided credentials are valid, false otherwise
185 */ 179 */
186 public function checkCredentialsFromLocalConfig($login, $password) { 180 public function checkCredentialsFromLocalConfig($login, $password)
181 {
187 $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); 182 $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
188 183
189 return $login == $this->configManager->get('credentials.login') 184 return $login == $this->configManager->get('credentials.login')
@@ -202,14 +197,14 @@ class LoginManager
202 */ 197 */
203 public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null) 198 public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null)
204 { 199 {
205 $connect = $connect ?? function($host) { 200 $connect = $connect ?? function ($host) {
206 $resource = ldap_connect($host); 201 $resource = ldap_connect($host);
207 202
208 ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3); 203 ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3);
209 204
210 return $resource; 205 return $resource;
211 }; 206 };
212 $bind = $bind ?? function($handle, $dn, $password) { 207 $bind = $bind ?? function ($handle, $dn, $password) {
213 return ldap_bind($handle, $dn, $password); 208 return ldap_bind($handle, $dn, $password);
214 }; 209 };
215 210
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
index 36df8c1c..f957b91a 100644
--- a/application/security/SessionManager.php
+++ b/application/security/SessionManager.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Security; 3namespace Shaarli\Security;
3 4
4use Shaarli\Config\ConfigManager; 5use 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 @@
1import 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 */
17function 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 */
33function 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 @@
1const 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
26const 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
43const redirectIfEmptyBatch = (basePath, formElements, path) => {
44 if (formElements == null || formElements.length === 0) {
45 window.location.href = `${basePath}${path}`;
46 }
47};
48
49(() => {
50 const basePath = document.querySelector('input[name="js_base_path"]').value;
51 const getForms = () => document.querySelectorAll('form[name="linkform"]');
52
53 const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]');
54 if (cancelButtons != null) {
55 [...cancelButtons].forEach((cancelButton) => {
56 cancelButton.addEventListener('click', (e) => {
57 e.preventDefault();
58 e.target.closest('form[name="linkform"]').remove();
59 redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare');
60 });
61 });
62 }
63
64 const saveButtons = document.querySelectorAll('[name="save_edit"]');
65 if (saveButtons != null) {
66 [...saveButtons].forEach((saveButton) => {
67 saveButton.addEventListener('click', (e) => {
68 e.preventDefault();
69
70 const formElement = e.target.closest('form[name="linkform"]');
71 sendBookmarkForm(basePath, formElement)
72 .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
73 });
74 });
75 }
76
77 const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]');
78 if (saveAllButtons != null) {
79 [...saveAllButtons].forEach((saveAllButton) => {
80 saveAllButton.addEventListener('click', (e) => {
81 e.preventDefault();
82
83 const forms = [...getForms()];
84 const nbForm = forms.length;
85 let current = 0;
86 const progressBar = document.querySelector('.progressbar > div');
87 const progressBarCurrent = document.querySelector('.progressbar-current');
88
89 document.querySelector('.dark-layer').style.display = 'block';
90 document.querySelector('.progressbar-max').innerHTML = nbForm;
91 progressBarCurrent.innerHTML = current;
92
93 const promises = [];
94 forms.forEach((formElement) => {
95 promises.push(sendBookmarkForm(basePath, formElement).then(() => {
96 current += 1;
97 progressBar.style.width = `${(current * 100) / nbForm}%`;
98 progressBarCurrent.innerHTML = current;
99 }));
100 });
101
102 Promise.all(promises).then(() => {
103 window.location.href = basePath || '/';
104 });
105 });
106 });
107 }
108
109 const deleteButtons = document.querySelectorAll('[name="delete_link"]');
110 if (deleteButtons != null) {
111 [...deleteButtons].forEach((deleteButton) => {
112 deleteButton.addEventListener('click', (e) => {
113 e.preventDefault();
114
115 const formElement = e.target.closest('form[name="linkform"]');
116 sendBookmarkDelete(e.target, formElement)
117 .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
118 });
119 });
120 }
121})();
diff --git a/assets/default/js/base.js b/assets/default/js/base.js
index be986ae0..dd532bb7 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -1,4 +1,5 @@
1import Awesomplete from 'awesomplete'; 1import Awesomplete from 'awesomplete';
2import 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
44function createAwesompleteInstance(element, tags = []) { 45function 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 */
81function updateAwesompleteList(selector, tags, instances) { 85function 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 */
103function htmlEntities(str) {
104 return str.replace(/[\u00A0-\u9999<>&]/gim, (i) => `&#${i.charCodeAt(0)};`);
105}
106
107/**
108 * Add the class 'hidden' to city options not attached to the current selected continent. 103 * Add the class 'hidden' to city options not attached to the current selected continent.
109 * 104 *
110 * @param cities List of <option> elements 105 * @param cities List of <option> elements
@@ -222,6 +217,8 @@ function init(description) {
222 217
223(() => { 218(() => {
224 const basePath = document.querySelector('input[name="js_base_path"]').value; 219 const basePath = document.querySelector('input[name="js_base_path"]').value;
220 const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
221 const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
225 222
226 /** 223 /**
227 * Handle responsive menu. 224 * Handle responsive menu.
@@ -302,7 +299,8 @@ function init(description) {
302 const deleteLinks = document.querySelectorAll('.confirm-delete'); 299 const deleteLinks = document.querySelectorAll('.confirm-delete');
303 [...deleteLinks].forEach((deleteLink) => { 300 [...deleteLinks].forEach((deleteLink) => {
304 deleteLink.addEventListener('click', (event) => { 301 deleteLink.addEventListener('click', (event) => {
305 if (!confirm(document.getElementById('translation-delete-link').innerHTML)) { 302 const type = event.currentTarget.getAttribute('data-type') || 'link';
303 if (!confirm(document.getElementById(`translation-delete-${type}`).innerHTML)) {
306 event.preventDefault(); 304 event.preventDefault();
307 } 305 }
308 }); 306 });
@@ -569,7 +567,7 @@ function init(description) {
569 input.setAttribute('name', totag); 567 input.setAttribute('name', totag);
570 input.setAttribute('value', totag); 568 input.setAttribute('value', totag);
571 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; 569 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
572 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag); 570 block.querySelector('a.tag-link').innerHTML = he.encode(totag);
573 block 571 block
574 .querySelector('a.tag-link') 572 .querySelector('a.tag-link')
575 .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`); 573 .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
@@ -582,7 +580,7 @@ function init(description) {
582 580
583 // Refresh awesomplete values 581 // Refresh awesomplete values
584 existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag)); 582 existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
585 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 583 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
586 } 584 }
587 }; 585 };
588 xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`); 586 xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
@@ -622,14 +620,14 @@ function init(description) {
622 refreshToken(basePath); 620 refreshToken(basePath);
623 621
624 existingTags = existingTags.filter((tagItem) => tagItem !== tag); 622 existingTags = existingTags.filter((tagItem) => tagItem !== tag);
625 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 623 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
626 } 624 }
627 }); 625 });
628 }); 626 });
629 627
630 const autocompleteFields = document.querySelectorAll('input[data-multiple]'); 628 const autocompleteFields = document.querySelectorAll('input[data-multiple]');
631 [...autocompleteFields].forEach((autocompleteField) => { 629 [...autocompleteFields].forEach((autocompleteField) => {
632 awesomepletes.push(createAwesompleteInstance(autocompleteField)); 630 awesomepletes.push(createAwesompleteInstance(autocompleteField, tagsSeparator));
633 }); 631 });
634 632
635 const exportForm = document.querySelector('#exportform'); 633 const exportForm = document.querySelector('#exportform');
@@ -642,4 +640,33 @@ function init(description) {
642 }); 640 });
643 }); 641 });
644 } 642 }
643
644 const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block');
645 if (bulkCreationButton != null) {
646 const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => {
647 if (bulkCreationButton.classList.contains('pure-u-0')) {
648 showMoreBlockElement.classList.remove('pure-u-0');
649 formElement.classList.add('pure-u-0');
650 } else {
651 showMoreBlockElement.classList.add('pure-u-0');
652 formElement.classList.remove('pure-u-0');
653 }
654 };
655
656 const bulkCreationForm = document.querySelector('.addlink-batch-form-block');
657
658 toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
659 bulkCreationButton.querySelector('a').addEventListener('click', (e) => {
660 e.preventDefault();
661 toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
662 });
663
664 // Force to send falsy value if the checkbox is not checked.
665 const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]');
666 const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]');
667 privateButton.addEventListener('click', () => {
668 privateHiddenButton.disabled = !privateHiddenButton.disabled;
669 });
670 privateHiddenButton.disabled = privateButton.checked;
671 }
645})(); 672})();
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index a528adb0..cc8ccc1e 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -139,6 +139,16 @@ body,
139 } 139 }
140} 140}
141 141
142.page-form,
143.pure-alert {
144 code {
145 display: inline-block;
146 padding: 0 2px;
147 color: $dark-grey;
148 background-color: var(--background-color);
149 }
150}
151
142// Make pure-extras alert closable. 152// Make pure-extras alert closable.
143.pure-alert-closable { 153.pure-alert-closable {
144 .fa-times { 154 .fa-times {
@@ -671,6 +681,10 @@ body,
671 content: ''; 681 content: '';
672 } 682 }
673 } 683 }
684
685 .search-highlight {
686 background-color: yellow;
687 }
674} 688}
675 689
676.linklist-item-buttons { 690.linklist-item-buttons {
@@ -1019,6 +1033,10 @@ body,
1019 &.button-red { 1033 &.button-red {
1020 background: $red; 1034 background: $red;
1021 } 1035 }
1036
1037 &.button-grey {
1038 background: $light-grey;
1039 }
1022 } 1040 }
1023 1041
1024 .submit-buttons { 1042 .submit-buttons {
@@ -1043,7 +1061,7 @@ body,
1043 } 1061 }
1044 1062
1045 table { 1063 table {
1046 margin: auto; 1064 margin: 10px auto 25px auto;
1047 width: 90%; 1065 width: 90%;
1048 1066
1049 .order { 1067 .order {
@@ -1079,6 +1097,11 @@ body,
1079 position: absolute; 1097 position: absolute;
1080 right: 5%; 1098 right: 5%;
1081 } 1099 }
1100
1101 &.button-grey {
1102 position: absolute;
1103 left: 5%;
1104 }
1082 } 1105 }
1083 } 1106 }
1084 } 1107 }
@@ -1253,11 +1276,15 @@ form {
1253 margin: 70px 0 25px; 1276 margin: 70px 0 25px;
1254 } 1277 }
1255 1278
1279 a {
1280 color: var(--main-color);
1281 }
1282
1256 pre { 1283 pre {
1257 margin: 0 20%; 1284 margin: 0 20%;
1258 padding: 20px 0; 1285 padding: 20px 0;
1259 text-align: left; 1286 text-align: left;
1260 line-height: .7em; 1287 line-height: 1em;
1261 } 1288 }
1262} 1289}
1263 1290
@@ -1269,6 +1296,57 @@ form {
1269 } 1296 }
1270} 1297}
1271 1298
1299.loading-input {
1300 position: relative;
1301
1302 @keyframes around {
1303 0% {
1304 transform: rotate(0deg);
1305 }
1306
1307 100% {
1308 transform: rotate(360deg);
1309 }
1310 }
1311
1312 .icon-container {
1313 position: absolute;
1314 right: 60px;
1315 top: calc(50% - 10px);
1316 }
1317
1318 .loader {
1319 position: relative;
1320 height: 20px;
1321 width: 20px;
1322 display: inline-block;
1323 animation: around 5.4s infinite;
1324
1325 &::after,
1326 &::before {
1327 content: "";
1328 background: $form-input-background;
1329 position: absolute;
1330 display: inline-block;
1331 width: 100%;
1332 height: 100%;
1333 border-width: 2px;
1334 border-color: #333 #333 transparent transparent;
1335 border-style: solid;
1336 border-radius: 20px;
1337 box-sizing: border-box;
1338 top: 0;
1339 left: 0;
1340 animation: around 0.7s ease-in-out infinite;
1341 }
1342
1343 &::after {
1344 animation: around 0.7s ease-in-out 0.1s infinite;
1345 background: transparent;
1346 }
1347 }
1348}
1349
1272// LOGIN 1350// LOGIN
1273.login-form-container { 1351.login-form-container {
1274 .remember-me { 1352 .remember-me {
@@ -1641,6 +1719,123 @@ form {
1641 } 1719 }
1642} 1720}
1643 1721
1722// SERVER PAGE
1723
1724.server-tables-page,
1725.server-tables {
1726 .window-subtitle {
1727 &::before {
1728 display: block;
1729 margin: 8px auto;
1730 background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color));
1731 width: 50%;
1732 height: 1px;
1733 content: '';
1734 }
1735 }
1736
1737 .server-row {
1738 p {
1739 height: 25px;
1740 padding: 0 10px;
1741 }
1742 }
1743
1744 .server-label {
1745 text-align: right;
1746 font-weight: bold;
1747 }
1748
1749 i {
1750 &.fa-color-green {
1751 color: $main-green;
1752 }
1753
1754 &.fa-color-orange {
1755 color: $orange;
1756 }
1757
1758 &.fa-color-red {
1759 color: $red;
1760 }
1761 }
1762
1763 @media screen and (max-width: 64em) {
1764 .server-label {
1765 text-align: center;
1766 }
1767
1768 .server-row {
1769 p {
1770 text-align: center;
1771 }
1772 }
1773 }
1774}
1775
1776// Batch creation
1777input[name='save_edit_batch'] {
1778 @extend %page-form-button;
1779}
1780
1781.addlink-batch-show-more {
1782 display: flex;
1783 align-items: center;
1784 margin: 20px 0 8px;
1785
1786 a {
1787 color: var(--main-color);
1788 text-decoration: none;
1789 }
1790
1791 &::before,
1792 &::after {
1793 content: "";
1794 flex-grow: 1;
1795 background: rgba(0, 0, 0, 0.35);
1796 height: 1px;
1797 font-size: 0;
1798 line-height: 0;
1799 }
1800
1801 &::before {
1802 margin: 0 16px 0 0;
1803 }
1804
1805 &::after {
1806 margin: 0 0 0 16px;
1807 }
1808}
1809
1810.dark-layer {
1811 display: none;
1812 position: fixed;
1813 height: 100%;
1814 width: 100%;
1815 z-index: 998;
1816 background-color: rgba(0, 0, 0, .75);
1817 color: #fff;
1818
1819 .screen-center {
1820 display: flex;
1821 flex-direction: column;
1822 justify-content: center;
1823 align-items: center;
1824 text-align: center;
1825 min-height: 100vh;
1826 }
1827
1828 .progressbar {
1829 width: 33%;
1830 }
1831}
1832
1833.addlink-batch-form-block {
1834 .pure-alert {
1835 margin: 25px 0 0 0;
1836 }
1837}
1838
1644// Print rules 1839// Print rules
1645@media print { 1840@media print {
1646 .shaarli-menu { 1841 .shaarli-menu {
diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css
index 1688dce0..33e178af 100644
--- a/assets/vintage/css/shaarli.css
+++ b/assets/vintage/css/shaarli.css
@@ -1122,6 +1122,16 @@ ul.errors {
1122 float: left; 1122 float: left;
1123} 1123}
1124 1124
1125ul.warnings {
1126 color: orange;
1127 float: left;
1128}
1129
1130ul.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';
2import 'awesomplete/awesomplete.css'; 2import 'awesomplete/awesomplete.css';
3 3
4(() => { 4(() => {
5 const awp = Awesomplete.$;
6 const autocompleteFields = document.querySelectorAll('input[data-multiple]'); 5 const autocompleteFields = document.querySelectorAll('input[data-multiple]');
6 const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
7 const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
8
7 [...autocompleteFields].forEach((autocompleteField) => { 9 [...autocompleteFields].forEach((autocompleteField) => {
8 const awesomplete = new Awesomplete(awp(autocompleteField)); 10 const awesome = new Awesomplete(Awesomplete.$(autocompleteField));
9 awesomplete.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]); 11
10 awesomplete.replace = (text) => { 12 // Tags are separated by separator
11 const before = awesomplete.input.value.match(/^.+ \s*|/)[0]; 13 awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(
12 awesomplete.input.value = `${before}${text} `; 14 text,
15 input.match(new RegExp(`[^${tagsSeparator}]*$`))[0],
16 );
17 // Insert new selected tag in the input
18 awesome.replace = (text) => {
19 const before = awesome.input.value.match(new RegExp(`^.+${tagsSeparator}+|`))[0];
20 awesome.input.value = `${before}${text}${tagsSeparator}`;
13 }; 21 };
14 awesomplete.minChars = 1; 22 // Highlight found items
23 awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]);
15 24
16 autocompleteField.addEventListener('input', () => { 25 // Don't display already selected items
17 const proposedTags = autocompleteField.getAttribute('data-list').replace(/,/g, '').split(' '); 26 // WARNING: pseudo classes does not seem to work with string litterals...
18 const reg = /(\w+) /g; 27 const reg = new RegExp(`([^${tagsSeparator}]+)${tagsSeparator}`, 'g');
19 let match; 28 let match;
20 while ((match = reg.exec(autocompleteField.value)) !== null) { 29 awesome.data = (item, input) => {
21 const id = proposedTags.indexOf(match[1]); 30 while ((match = reg.exec(input))) {
22 if (id !== -1) { 31 if (item === match[1]) {
23 proposedTags.splice(id, 1); 32 return '';
24 } 33 }
25 } 34 }
26 35 return item;
27 awesomplete.list = proposedTags; 36 };
28 }); 37 awesome.minChars = 1;
29 }); 38 });
30})(); 39})();
diff --git a/composer.json b/composer.json
index cd9fcf5b..138319ca 100644
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,7 @@
10 }, 10 },
11 "keywords": ["bookmark", "link", "share", "web"], 11 "keywords": ["bookmark", "link", "share", "web"],
12 "config": { 12 "config": {
13 "sort-packages": true,
13 "platform": { 14 "platform": {
14 "php": "7.1.29" 15 "php": "7.1.29"
15 } 16 }
@@ -18,12 +19,15 @@
18 "php": ">=7.1", 19 "php": ">=7.1",
19 "ext-json": "*", 20 "ext-json": "*",
20 "ext-zlib": "*", 21 "ext-zlib": "*",
21 "shaarli/netscape-bookmark-parser": "^2.1",
22 "erusev/parsedown": "^1.6",
23 "slim/slim": "^3.0",
24 "arthurhoaro/web-thumbnailer": "^2.0", 22 "arthurhoaro/web-thumbnailer": "^2.0",
23 "erusev/parsedown": "^1.6",
24 "erusev/parsedown-extra": "^0.8.1",
25 "gettext/gettext": "^4.4",
26 "katzgrau/klogger": "^1.2",
27 "malkusch/lock": "^2.1",
25 "pubsubhubbub/publisher": "dev-master", 28 "pubsubhubbub/publisher": "dev-master",
26 "gettext/gettext": "^4.4" 29 "shaarli/netscape-bookmark-parser": "^3.0",
30 "slim/slim": "^3.0"
27 }, 31 },
28 "require-dev": { 32 "require-dev": {
29 "roave/security-advisories": "dev-master", 33 "roave/security-advisories": "dev-master",
@@ -55,6 +59,7 @@
55 "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin", 59 "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
56 "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor", 60 "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
57 "Shaarli\\Front\\Exception\\": "application/front/exceptions", 61 "Shaarli\\Front\\Exception\\": "application/front/exceptions",
62 "Shaarli\\Helper\\": "application/helper",
58 "Shaarli\\Http\\": "application/http", 63 "Shaarli\\Http\\": "application/http",
59 "Shaarli\\Legacy\\": "application/legacy", 64 "Shaarli\\Legacy\\": "application/legacy",
60 "Shaarli\\Netscape\\": "application/netscape", 65 "Shaarli\\Netscape\\": "application/netscape",
diff --git a/composer.lock b/composer.lock
index 2c8b0ea7..0023df88 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 "This file is @generated automatically" 5 "This file is @generated automatically"
6 ], 6 ],
7 "content-hash": "98520a05a7185503ee13d05ffaa535f6", 7 "content-hash": "83852dec81e299a117a81206a5091472",
8 "packages": [ 8 "packages": [
9 { 9 {
10 "name": "arthurhoaro/web-thumbnailer", 10 "name": "arthurhoaro/web-thumbnailer",
@@ -108,6 +108,57 @@
108 "time": "2019-12-30T22:54:17+00:00" 108 "time": "2019-12-30T22:54:17+00:00"
109 }, 109 },
110 { 110 {
111 "name": "erusev/parsedown-extra",
112 "version": "0.8.1",
113 "source": {
114 "type": "git",
115 "url": "https://github.com/erusev/parsedown-extra.git",
116 "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef"
117 },
118 "dist": {
119 "type": "zip",
120 "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/91ac3ff98f0cea243bdccc688df43810f044dcef",
121 "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef",
122 "shasum": ""
123 },
124 "require": {
125 "erusev/parsedown": "^1.7.4"
126 },
127 "require-dev": {
128 "phpunit/phpunit": "^4.8.35"
129 },
130 "type": "library",
131 "autoload": {
132 "psr-0": {
133 "ParsedownExtra": ""
134 }
135 },
136 "notification-url": "https://packagist.org/downloads/",
137 "license": [
138 "MIT"
139 ],
140 "authors": [
141 {
142 "name": "Emanuil Rusev",
143 "email": "hello@erusev.com",
144 "homepage": "http://erusev.com"
145 }
146 ],
147 "description": "An extension of Parsedown that adds support for Markdown Extra.",
148 "homepage": "https://github.com/erusev/parsedown-extra",
149 "keywords": [
150 "markdown",
151 "markdown extra",
152 "parsedown",
153 "parser"
154 ],
155 "support": {
156 "issues": "https://github.com/erusev/parsedown-extra/issues",
157 "source": "https://github.com/erusev/parsedown-extra/tree/0.8.x"
158 },
159 "time": "2019-12-30T23:20:37+00:00"
160 },
161 {
111 "name": "gettext/gettext", 162 "name": "gettext/gettext",
112 "version": "v4.8.2", 163 "version": "v4.8.2",
113 "source": { 164 "source": {
@@ -294,6 +345,91 @@
294 "time": "2016-11-07T19:29:14+00:00" 345 "time": "2016-11-07T19:29:14+00:00"
295 }, 346 },
296 { 347 {
348 "name": "malkusch/lock",
349 "version": "v2.1",
350 "source": {
351 "type": "git",
352 "url": "https://github.com/php-lock/lock.git",
353 "reference": "093f389ec2f38fc8686d2f70e23378182fce7714"
354 },
355 "dist": {
356 "type": "zip",
357 "url": "https://api.github.com/repos/php-lock/lock/zipball/093f389ec2f38fc8686d2f70e23378182fce7714",
358 "reference": "093f389ec2f38fc8686d2f70e23378182fce7714",
359 "shasum": ""
360 },
361 "require": {
362 "php": ">=7.1",
363 "psr/log": "^1"
364 },
365 "require-dev": {
366 "eloquent/liberator": "^2.0",
367 "ext-memcached": "*",
368 "ext-pcntl": "*",
369 "ext-pdo_mysql": "*",
370 "ext-pdo_sqlite": "*",
371 "ext-redis": "*",
372 "ext-sysvsem": "*",
373 "johnkary/phpunit-speedtrap": "^3.0",
374 "kriswallsmith/spork": "^0.3",
375 "mikey179/vfsstream": "^1.6",
376 "php-mock/php-mock-phpunit": "^2.1",
377 "phpunit/phpunit": "^7.4",
378 "predis/predis": "^1.1",
379 "squizlabs/php_codesniffer": "^3.3"
380 },
381 "suggest": {
382 "ext-pnctl": "Enables locking with flock without busy waiting in CLI scripts.",
383 "ext-redis": "To use this library with the PHP Redis extension.",
384 "ext-sysvsem": "Enables locking using semaphores.",
385 "predis/predis": "To use this library with predis."
386 },
387 "type": "library",
388 "autoload": {
389 "psr-4": {
390 "malkusch\\lock\\": "classes/"
391 }
392 },
393 "notification-url": "https://packagist.org/downloads/",
394 "license": [
395 "WTFPL"
396 ],
397 "authors": [
398 {
399 "name": "Markus Malkusch",
400 "email": "markus@malkusch.de",
401 "homepage": "http://markus.malkusch.de",
402 "role": "Developer"
403 },
404 {
405 "name": "Willem Stuursma-Ruwen",
406 "email": "willem@stuursma.name",
407 "role": "Developer"
408 }
409 ],
410 "description": "Mutex library for exclusive code execution.",
411 "homepage": "https://github.com/malkusch/lock",
412 "keywords": [
413 "advisory-locks",
414 "cas",
415 "flock",
416 "lock",
417 "locking",
418 "memcache",
419 "mutex",
420 "mysql",
421 "postgresql",
422 "redis",
423 "redlock",
424 "semaphore"
425 ],
426 "support": {
427 "issues": "https://github.com/php-lock/lock/issues",
428 "source": "https://github.com/php-lock/lock/tree/v2.1"
429 },
430 "time": "2018-12-12T19:53:29+00:00"
431 },
432 {
297 "name": "nikic/fast-route", 433 "name": "nikic/fast-route",
298 "version": "v1.3.0", 434 "version": "v1.3.0",
299 "source": { 435 "source": {
@@ -650,24 +786,25 @@
650 }, 786 },
651 { 787 {
652 "name": "shaarli/netscape-bookmark-parser", 788 "name": "shaarli/netscape-bookmark-parser",
653 "version": "v2.2.0", 789 "version": "v3.0.1",
654 "source": { 790 "source": {
655 "type": "git", 791 "type": "git",
656 "url": "https://github.com/shaarli/netscape-bookmark-parser.git", 792 "url": "https://github.com/shaarli/netscape-bookmark-parser.git",
657 "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df" 793 "reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305"
658 }, 794 },
659 "dist": { 795 "dist": {
660 "type": "zip", 796 "type": "zip",
661 "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/432a010af2bb1832d6fbc4763e6b0100b980a1df", 797 "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/d2321f30413944b2d0a9844bf8cc588c71ae6305",
662 "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df", 798 "reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305",
663 "shasum": "" 799 "shasum": ""
664 }, 800 },
665 "require": { 801 "require": {
666 "katzgrau/klogger": "~1.0", 802 "katzgrau/klogger": "~1.0",
667 "php": ">=5.6" 803 "php": ">=7.1"
668 }, 804 },
669 "require-dev": { 805 "require-dev": {
670 "phpunit/phpunit": "^5.0" 806 "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
807 "squizlabs/php_codesniffer": "^3.5"
671 }, 808 },
672 "type": "library", 809 "type": "library",
673 "autoload": { 810 "autoload": {
@@ -703,9 +840,9 @@
703 ], 840 ],
704 "support": { 841 "support": {
705 "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues", 842 "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues",
706 "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v2.2.0" 843 "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v3.0.1"
707 }, 844 },
708 "time": "2020-06-06T15:53:53+00:00" 845 "time": "2020-11-03T12:27:58+00:00"
709 }, 846 },
710 { 847 {
711 "name": "slim/slim", 848 "name": "slim/slim",
@@ -1577,12 +1714,12 @@
1577 "source": { 1714 "source": {
1578 "type": "git", 1715 "type": "git",
1579 "url": "https://github.com/Roave/SecurityAdvisories.git", 1716 "url": "https://github.com/Roave/SecurityAdvisories.git",
1580 "reference": "0749ceaf15c136d085b722a5bb88141398a54142" 1717 "reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6"
1581 }, 1718 },
1582 "dist": { 1719 "dist": {
1583 "type": "zip", 1720 "type": "zip",
1584 "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0749ceaf15c136d085b722a5bb88141398a54142", 1721 "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/065a018d3b5c2c84a53db3347cca4e1b7fa362a6",
1585 "reference": "0749ceaf15c136d085b722a5bb88141398a54142", 1722 "reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6",
1586 "shasum": "" 1723 "shasum": ""
1587 }, 1724 },
1588 "conflict": { 1725 "conflict": {
@@ -1598,7 +1735,7 @@
1598 "bagisto/bagisto": "<0.1.5", 1735 "bagisto/bagisto": "<0.1.5",
1599 "barrelstrength/sprout-base-email": "<1.2.7", 1736 "barrelstrength/sprout-base-email": "<1.2.7",
1600 "barrelstrength/sprout-forms": "<3.9", 1737 "barrelstrength/sprout-forms": "<3.9",
1601 "baserproject/basercms": ">=4,<=4.3.6", 1738 "baserproject/basercms": ">=4,<=4.3.6|>=4.4,<4.4.1",
1602 "bolt/bolt": "<3.7.1", 1739 "bolt/bolt": "<3.7.1",
1603 "brightlocal/phpwhois": "<=4.2.5", 1740 "brightlocal/phpwhois": "<=4.2.5",
1604 "buddypress/buddypress": "<5.1.2", 1741 "buddypress/buddypress": "<5.1.2",
@@ -1642,7 +1779,7 @@
1642 "ezsystems/ezplatform-kernel": ">=1,<1.0.2.1", 1779 "ezsystems/ezplatform-kernel": ">=1,<1.0.2.1",
1643 "ezsystems/ezplatform-user": ">=1,<1.0.1", 1780 "ezsystems/ezplatform-user": ">=1,<1.0.1",
1644 "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.14.2|>=6,<6.7.9.1|>=6.8,<6.13.6.3|>=7,<7.2.4.1|>=7.3,<7.3.2.1|>=7.5,<7.5.7.1", 1781 "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.14.2|>=6,<6.7.9.1|>=6.8,<6.13.6.3|>=7,<7.2.4.1|>=7.3,<7.3.2.1|>=7.5,<7.5.7.1",
1645 "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.1|>=2011,<2017.12.7.2|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.4.2", 1782 "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.2|>=2011,<2017.12.7.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.5.1",
1646 "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", 1783 "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
1647 "ezsystems/repository-forms": ">=2.3,<2.3.2.1", 1784 "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
1648 "ezyang/htmlpurifier": "<4.1.1", 1785 "ezyang/htmlpurifier": "<4.1.1",
@@ -1682,9 +1819,12 @@
1682 "magento/magento1ee": ">=1,<1.14.4.3", 1819 "magento/magento1ee": ">=1,<1.14.4.3",
1683 "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", 1820 "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
1684 "marcwillmann/turn": "<0.3.3", 1821 "marcwillmann/turn": "<0.3.3",
1822 "mediawiki/core": ">=1.31,<1.31.9|>=1.32,<1.32.4|>=1.33,<1.33.3|>=1.34,<1.34.3|>=1.34.99,<1.35",
1685 "mittwald/typo3_forum": "<1.2.1", 1823 "mittwald/typo3_forum": "<1.2.1",
1686 "monolog/monolog": ">=1.8,<1.12", 1824 "monolog/monolog": ">=1.8,<1.12",
1687 "namshi/jose": "<2.2", 1825 "namshi/jose": "<2.2",
1826 "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6",
1827 "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13",
1688 "nystudio107/craft-seomatic": "<3.3", 1828 "nystudio107/craft-seomatic": "<3.3",
1689 "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", 1829 "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
1690 "october/backend": ">=1.0.319,<1.0.467", 1830 "october/backend": ">=1.0.319,<1.0.467",
@@ -1694,7 +1834,8 @@
1694 "onelogin/php-saml": "<2.10.4", 1834 "onelogin/php-saml": "<2.10.4",
1695 "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5", 1835 "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
1696 "openid/php-openid": "<2.3", 1836 "openid/php-openid": "<2.3",
1697 "openmage/magento-lts": "<19.4.6|>=20,<20.0.2", 1837 "openmage/magento-lts": "<19.4.8|>=20,<20.0.4",
1838 "orchid/platform": ">=9,<9.4.4",
1698 "oro/crm": ">=1.7,<1.7.4", 1839 "oro/crm": ">=1.7,<1.7.4",
1699 "oro/platform": ">=1.7,<1.7.4", 1840 "oro/platform": ">=1.7,<1.7.4",
1700 "padraic/humbug_get_contents": "<1.1.2", 1841 "padraic/humbug_get_contents": "<1.1.2",
@@ -1720,6 +1861,7 @@
1720 "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2", 1861 "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2",
1721 "propel/propel": ">=2-alpha.1,<=2-alpha.7", 1862 "propel/propel": ">=2-alpha.1,<=2-alpha.7",
1722 "propel/propel1": ">=1,<=1.7.1", 1863 "propel/propel1": ">=1,<=1.7.1",
1864 "pterodactyl/panel": "<0.7.19|>=1-rc.0,<=1-rc.6",
1723 "pusher/pusher-php-server": "<2.2.1", 1865 "pusher/pusher-php-server": "<2.2.1",
1724 "rainlab/debugbar-plugin": "<3.1", 1866 "rainlab/debugbar-plugin": "<3.1",
1725 "robrichards/xmlseclibs": "<3.0.4", 1867 "robrichards/xmlseclibs": "<3.0.4",
@@ -1728,8 +1870,8 @@
1728 "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", 1870 "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
1729 "sensiolabs/connect": "<4.2.3", 1871 "sensiolabs/connect": "<4.2.3",
1730 "serluck/phpwhois": "<=4.2.6", 1872 "serluck/phpwhois": "<=4.2.6",
1731 "shopware/core": "<=6.3.1", 1873 "shopware/core": "<=6.3.2",
1732 "shopware/platform": "<=6.3.1", 1874 "shopware/platform": "<=6.3.2",
1733 "shopware/shopware": "<5.3.7", 1875 "shopware/shopware": "<5.3.7",
1734 "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1", 1876 "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
1735 "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2", 1877 "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
@@ -1762,7 +1904,7 @@
1762 "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", 1904 "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
1763 "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", 1905 "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
1764 "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", 1906 "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4",
1765 "sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5", 1907 "sylius/sylius": "<1.6.9|>=1.7,<1.7.9|>=1.8,<1.8.3",
1766 "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", 1908 "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
1767 "symbiote/silverstripe-versionedfiles": "<=2.0.3", 1909 "symbiote/silverstripe-versionedfiles": "<=2.0.3",
1768 "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1910 "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
@@ -1805,6 +1947,7 @@
1805 "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5", 1947 "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5",
1806 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4", 1948 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4",
1807 "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", 1949 "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
1950 "typo3fluid/fluid": ">=2,<2.0.5|>=2.1,<2.1.4|>=2.2,<2.2.1|>=2.3,<2.3.5|>=2.4,<2.4.1|>=2.5,<2.5.5|>=2.6,<2.6.1",
1808 "ua-parser/uap-php": "<3.8", 1951 "ua-parser/uap-php": "<3.8",
1809 "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", 1952 "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
1810 "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4", 1953 "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
@@ -1878,7 +2021,7 @@
1878 "type": "tidelift" 2021 "type": "tidelift"
1879 } 2022 }
1880 ], 2023 ],
1881 "time": "2020-09-24T17:02:11+00:00" 2024 "time": "2020-11-01T20:01:47+00:00"
1882 }, 2025 },
1883 { 2026 {
1884 "name": "sebastian/code-unit-reverse-lookup", 2027 "name": "sebastian/code-unit-reverse-lookup",
@@ -2492,16 +2635,16 @@
2492 }, 2635 },
2493 { 2636 {
2494 "name": "squizlabs/php_codesniffer", 2637 "name": "squizlabs/php_codesniffer",
2495 "version": "3.5.6", 2638 "version": "3.5.8",
2496 "source": { 2639 "source": {
2497 "type": "git", 2640 "type": "git",
2498 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 2641 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
2499 "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" 2642 "reference": "9d583721a7157ee997f235f327de038e7ea6dac4"
2500 }, 2643 },
2501 "dist": { 2644 "dist": {
2502 "type": "zip", 2645 "type": "zip",
2503 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", 2646 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4",
2504 "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", 2647 "reference": "9d583721a7157ee997f235f327de038e7ea6dac4",
2505 "shasum": "" 2648 "shasum": ""
2506 }, 2649 },
2507 "require": { 2650 "require": {
@@ -2544,24 +2687,24 @@
2544 "source": "https://github.com/squizlabs/PHP_CodeSniffer", 2687 "source": "https://github.com/squizlabs/PHP_CodeSniffer",
2545 "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" 2688 "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
2546 }, 2689 },
2547 "time": "2020-08-10T04:50:15+00:00" 2690 "time": "2020-10-23T02:01:07+00:00"
2548 }, 2691 },
2549 { 2692 {
2550 "name": "symfony/polyfill-ctype", 2693 "name": "symfony/polyfill-ctype",
2551 "version": "v1.18.1", 2694 "version": "v1.20.0",
2552 "source": { 2695 "source": {
2553 "type": "git", 2696 "type": "git",
2554 "url": "https://github.com/symfony/polyfill-ctype.git", 2697 "url": "https://github.com/symfony/polyfill-ctype.git",
2555 "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" 2698 "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41"
2556 }, 2699 },
2557 "dist": { 2700 "dist": {
2558 "type": "zip", 2701 "type": "zip",
2559 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", 2702 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41",
2560 "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", 2703 "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41",
2561 "shasum": "" 2704 "shasum": ""
2562 }, 2705 },
2563 "require": { 2706 "require": {
2564 "php": ">=5.3.3" 2707 "php": ">=7.1"
2565 }, 2708 },
2566 "suggest": { 2709 "suggest": {
2567 "ext-ctype": "For best performance" 2710 "ext-ctype": "For best performance"
@@ -2569,7 +2712,7 @@
2569 "type": "library", 2712 "type": "library",
2570 "extra": { 2713 "extra": {
2571 "branch-alias": { 2714 "branch-alias": {
2572 "dev-master": "1.18-dev" 2715 "dev-main": "1.20-dev"
2573 }, 2716 },
2574 "thanks": { 2717 "thanks": {
2575 "name": "symfony/polyfill", 2718 "name": "symfony/polyfill",
@@ -2607,7 +2750,7 @@
2607 "portable" 2750 "portable"
2608 ], 2751 ],
2609 "support": { 2752 "support": {
2610 "source": "https://github.com/symfony/polyfill-ctype/tree/v1.18.0" 2753 "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0"
2611 }, 2754 },
2612 "funding": [ 2755 "funding": [
2613 { 2756 {
@@ -2623,7 +2766,7 @@
2623 "type": "tidelift" 2766 "type": "tidelift"
2624 } 2767 }
2625 ], 2768 ],
2626 "time": "2020-07-14T12:35:20+00:00" 2769 "time": "2020-10-23T14:02:19+00:00"
2627 }, 2770 },
2628 { 2771 {
2629 "name": "theseer/tokenizer", 2772 "name": "theseer/tokenizer",
diff --git a/doc/md/Docker.md b/doc/md/Docker.md
index c152fe92..fc406c00 100644
--- a/doc/md/Docker.md
+++ b/doc/md/Docker.md
@@ -1,3 +1,4 @@
1
1# Docker 2# Docker
2 3
3[Docker](https://docs.docker.com/get-started/overview/) is an open platform for developing, shipping, and running applications 4[Docker](https://docs.docker.com/get-started/overview/) is an open platform for developing, shipping, and running applications
@@ -113,9 +114,11 @@ $ mkdir shaarli && cd shaarli
113# Download the latest version of Shaarli's docker-compose.yml 114# Download the latest version of Shaarli's docker-compose.yml
114$ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/latest/docker-compose.yml -o docker-compose.yml 115$ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/latest/docker-compose.yml -o docker-compose.yml
115# Create the .env file and fill in your VPS and domain information 116# Create the .env file and fill in your VPS and domain information
116# (replace <MY_SHAARLI_DOMAIN> and <MY_CONTACT_EMAIL> with your actual information) 117# (replace <shaarli.mydomain.org>, <admin@mydomain.org> and <latest> with your actual information)
117$ echo 'SHAARLI_VIRTUAL_HOST=shaarli.mydomain.org' > .env 118$ echo 'SHAARLI_VIRTUAL_HOST=shaarli.mydomain.org' > .env
118$ echo 'SHAARLI_LETSENCRYPT_EMAIL=admin@mydomain.org' >> .env 119$ echo 'SHAARLI_LETSENCRYPT_EMAIL=admin@mydomain.org' >> .env
120# Available Docker tags can be found at https://hub.docker.com/r/shaarli/shaarli/tags
121$ echo 'SHAARLI_DOCKER_TAG=latest' >> .env
119# Pull the Docker images 122# Pull the Docker images
120$ docker-compose pull 123$ docker-compose pull
121# Run! 124# Run!
@@ -224,4 +227,4 @@ $ docker system prune
224- [docker pull](https://docs.docker.com/engine/reference/commandline/pull/) 227- [docker pull](https://docs.docker.com/engine/reference/commandline/pull/)
225- [docker run](https://docs.docker.com/engine/reference/commandline/run/) 228- [docker run](https://docs.docker.com/engine/reference/commandline/run/)
226- [docker-compose logs](https://docs.docker.com/compose/reference/logs/) 229- [docker-compose logs](https://docs.docker.com/compose/reference/logs/)
227- Træfik: [Getting Started](https://docs.traefik.io/), [Docker backend](https://docs.traefik.io/configuration/backends/docker/), [Let's Encrypt](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/), [Docker image](https://hub.docker.com/_/traefik/) \ No newline at end of file 230- Træfik: [Getting Started](https://docs.traefik.io/), [Docker backend](https://docs.traefik.io/configuration/backends/docker/), [Let's Encrypt](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/), [Docker image](https://hub.docker.com/_/traefik/)
diff --git a/doc/md/Server-configuration.md b/doc/md/Server-configuration.md
index 297d7c29..a49b6033 100644
--- a/doc/md/Server-configuration.md
+++ b/doc/md/Server-configuration.md
@@ -40,6 +40,8 @@ Supported PHP versions:
40 40
41Version | Status | Shaarli compatibility 41Version | Status | Shaarli compatibility
42:---:|:---:|:---: 42:---:|:---:|:---:
438.0 | Supported | Yes
447.4 | Supported | Yes
437.3 | Supported | Yes 457.3 | Supported | Yes
447.2 | Supported | Yes 467.2 | Supported | Yes
457.1 | Supported | Yes 477.1 | Supported | Yes
@@ -53,7 +55,7 @@ Required PHP extensions:
53 55
54Extension | Required? | Usage 56Extension | Required? | Usage
55---|:---:|--- 57---|:---:|---
56[`openssl`](http://php.net/manual/en/book.openssl.php) | requires | OpenSSL, HTTPS 58[`openssl`](http://php.net/manual/en/book.openssl.php) | required | OpenSSL, HTTPS
57[`php-json`](http://php.net/manual/en/book.json.php) | required | configuration parsing 59[`php-json`](http://php.net/manual/en/book.json.php) | required | configuration parsing
58[`php-simplexml`](https://www.php.net/manual/en/book.simplexml.php) | required | REST API (Slim framework) 60[`php-simplexml`](https://www.php.net/manual/en/book.simplexml.php) | required | REST API (Slim framework)
59[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support 61[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
@@ -191,19 +193,24 @@ sudo nano /etc/apache2/sites-available/shaarli.mydomain.org.conf
191 Require all granted 193 Require all granted
192 </Directory> 194 </Directory>
193 195
194 <LocationMatch "/\."> 196 # BE CAREFUL: directives order matter!
195 # Prevent accessing dotfiles
196 RedirectMatch 404 ".*"
197 </LocationMatch>
198 197
199 <LocationMatch "\.(?:ico|css|js|gif|jpe?g|png)$"> 198 <FilesMatch ".*\.(?!(ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$)[^\.]*$">
199 Require all denied
200 </FilesMatch>
201
202 <Files "index.php">
203 Require all granted
204 </Files>
205
206 <FilesMatch "\.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2)$">
200 # allow client-side caching of static files 207 # allow client-side caching of static files
201 Header set Cache-Control "max-age=2628000, public, must-revalidate, proxy-revalidate" 208 Header set Cache-Control "max-age=2628000, public, must-revalidate, proxy-revalidate"
202 </LocationMatch> 209 </FilesMatch>
210
203 211
204 # serve the Shaarli favicon from its custom location 212 # serve the Shaarli favicon from its custom location
205 Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico 213 Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico
206
207</VirtualHost> 214</VirtualHost>
208``` 215```
209 216
@@ -294,7 +301,7 @@ server {
294 location / { 301 location / {
295 # default index file when no file URI is requested 302 # default index file when no file URI is requested
296 index index.php; 303 index index.php;
297 try_files $uri /index.php$is_args$args; 304 try_files _ /index.php$is_args$args;
298 } 305 }
299 306
300 location ~ (index)\.php$ { 307 location ~ (index)\.php$ {
@@ -307,20 +314,9 @@ server {
307 include fastcgi.conf; 314 include fastcgi.conf;
308 } 315 }
309 316
310 location ~ \.php$ { 317 location ~ /doc/html/ {
311 # deny access to all other PHP scripts 318 default_type "text/html";
312 # disable this if you host other PHP applications on the same virtualhost 319 try_files $uri $uri/ $uri.html =404;
313 deny all;
314 }
315
316 location ~ /\. {
317 # deny access to dotfiles
318 deny all;
319 }
320
321 location ~ ~$ {
322 # deny access to temp editor files, e.g. "script.php~"
323 deny all;
324 } 320 }
325 321
326 location = /favicon.ico { 322 location = /favicon.ico {
@@ -329,13 +325,12 @@ server {
329 } 325 }
330 326
331 # allow client-side caching of static files 327 # allow client-side caching of static files
332 location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { 328 location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$ {
333 expires max; 329 expires max;
334 add_header Cache-Control "public, must-revalidate, proxy-revalidate"; 330 add_header Cache-Control "public, must-revalidate, proxy-revalidate";
335 # HTTP 1.0 compatibility 331 # HTTP 1.0 compatibility
336 add_header Pragma public; 332 add_header Pragma public;
337 } 333 }
338
339} 334}
340``` 335```
341 336
@@ -360,7 +355,23 @@ sudo systemctl reload nginx
360 355
361If Shaarli is hosted on a server behind a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) (i.e. there is a proxy server between clients and the web server hosting Shaarli), configure it accordingly. See [Reverse proxy](Reverse-proxy.md) configuration. 356If Shaarli is hosted on a server behind a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) (i.e. there is a proxy server between clients and the web server hosting Shaarli), configure it accordingly. See [Reverse proxy](Reverse-proxy.md) configuration.
362 357
358## Using Shaarli without URL rewriting
359
360By default, Shaarli uses Slim framework's URL, which requires
361URL rewriting.
362
363If you can't use URL rewriting for any reason (not supported by
364your web server, shared hosting, etc.), you *can* use Shaarli
365without URL rewriting.
366
367You just need to prefix your URL by `/index.php/`.
368Example: instead of accessing `https://shaarli.mydomain.org/`,
369use `https://shaarli.mydomain.org/index.php/`.
363 370
371**Recommended:**
372 * after installation, in the configuration page, set your header link to `/index.php/`.
373 * in your configuration file `config.json.php` set `general.root_url` to
374 `https://shaarli.mydomain.org/index.php/`.
364 375
365## Allow import of large browser bookmarks export 376## Allow import of large browser bookmarks export
366 377
@@ -421,7 +432,7 @@ By default Shaarli already disallows indexing of your local copy of the document
421before = common.conf 432before = common.conf
422[Definition] 433[Definition]
423failregex = \s-\s<HOST>\s-\sLogin failed for user.*$ 434failregex = \s-\s<HOST>\s-\sLogin failed for user.*$
424ignoreregex = 435ignoreregex =
425``` 436```
426 437
427```ini 438```ini
diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md
index 263fb761..b1326cce 100644
--- a/doc/md/Shaarli-configuration.md
+++ b/doc/md/Shaarli-configuration.md
@@ -74,6 +74,7 @@ Some settings can be configured directly from a web browser by accesing the `Too
74 "timezone": "Europe\/Paris", 74 "timezone": "Europe\/Paris",
75 "title": "My Shaarli", 75 "title": "My Shaarli",
76 "header_link": "?" 76 "header_link": "?"
77 "tags_separator": " "
77 }, 78 },
78 "dev": { 79 "dev": {
79 "debug": false, 80 "debug": false,
@@ -150,8 +151,10 @@ _These settings should not be edited_
150- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php). 151- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).
151- **enabled_plugins**: List of enabled plugins. 152- **enabled_plugins**: List of enabled plugins.
152- **default_note_title**: Default title of a new note. 153- **default_note_title**: Default title of a new note.
154- **enable_async_metadata** (boolean): Retrieve external bookmark metadata asynchronously to prevent bookmark creation slowdown.
153- **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags. 155- **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags.
154- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`. 156- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
157- **tags_separator**: Defines your tags separator (default: whitespace).
155 158
156### Security 159### Security
157 160
@@ -163,6 +166,22 @@ _These settings should not be edited_
163- **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy. 166- **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy.
164- **allowed_protocols**: List of allowed protocols in shaare URLs or markdown-rendered descriptions. Useful if you want to store `javascript:` links (bookmarklets) in Shaarli (default: `["ftp", "ftps", "magnet"]`). 167- **allowed_protocols**: List of allowed protocols in shaare URLs or markdown-rendered descriptions. Useful if you want to store `javascript:` links (bookmarklets) in Shaarli (default: `["ftp", "ftps", "magnet"]`).
165 168
169### Formatter
170
171Single 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
179Additional 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).
10Run `make eslint` to check JS style. 10Run `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
54Every link available through the `LinkDB` object is represented as an array 54Every link available through the `LinkDB` object is represented as an array
55containing the following fields: 55containing 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
70Small 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 `@`. 70Small 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
166Patches should try to stick to the [PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially: 166Patches 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
172These are enforced on pull requests using our Continuous Integration tools.
171 173
172**Work in progress:** Static analysis is currently being discussed here: in [#95 - Fix coding style (static analysis)](https://github.com/shaarli/Shaarli/issues/95), [#130 - Continuous Integration tools & features](https://github.com/shaarli/Shaarli/issues/130) 174**Work in progress:** Static analysis is currently being discussed here: in [#95 - Fix coding style (static analysis)](https://github.com/shaarli/Shaarli/issues/95), [#130 - Continuous Integration tools & features](https://github.com/shaarli/Shaarli/issues/130)
173 175
diff --git a/doc/md/dev/Plugin-system.md b/doc/md/dev/Plugin-system.md
index c29774de..f09fadc2 100644
--- a/doc/md/dev/Plugin-system.md
+++ b/doc/md/dev/Plugin-system.md
@@ -148,11 +148,16 @@ If a file needs to be included in server end, use simple relative path:
148`PluginManager::$PLUGINS_PATH . '/mything/template.html'`. 148`PluginManager::$PLUGINS_PATH . '/mything/template.html'`.
149 149
150If it needs to be included in front end side (e.g. an image), 150If it needs to be included in front end side (e.g. an image),
151the relative path must be prefixed with special data `_BASE_PATH_`: 151the relative path must be prefixed with special data:
152`($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH . '/mything/picture.png`. 152
153 * if it's a link that will need to be processed by Shaarli, use `_BASE_PATH_`:
154 for e.g. `$data['_BASE_PATH_'] . '/admin/tools`.
155 * if you want to include an asset, you need to add the root URL (base path without `/index.php`, for people using Shaarli without URL rewriting), then use `_ROOT_PATH_`:
156 for e.g
157`$['_ROOT_PATH_'] . '/' . PluginManager::$PLUGINS_PATH . '/mything/picture.png`.
153 158
154Note that special placeholders for CSS and JS files (respectively `css_files` and `js_files`) are already prefixed 159Note that special placeholders for CSS and JS files (respectively `css_files` and `js_files`) are already prefixed
155with the base path in template files. 160with the root path in template files.
156 161
157### It's not working! 162### It's not working!
158 163
diff --git a/docker-compose.yml b/docker-compose.yml
index a3de4b1c..4ebae447 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,12 +2,13 @@
2# Shaarli - Docker Compose example configuration 2# Shaarli - Docker Compose example configuration
3# 3#
4# See: 4# See:
5# - https://shaarli.readthedocs.io/en/master/docker/shaarli-images/ 5# - https://shaarli.readthedocs.io/en/master/Docker/#docker-compose
6# - https://shaarli.readthedocs.io/en/master/guides/install-shaarli-with-debian9-and-docker/
7# 6#
8# Environment variables: 7# Environment variables:
9# - SHAARLI_VIRTUAL_HOST Fully Qualified Domain Name for the Shaarli instance 8# - SHAARLI_VIRTUAL_HOST Fully Qualified Domain Name for the Shaarli instance
10# - SHAARLI_LETSENCRYPT_EMAIL Contact email for certificate renewal 9# - SHAARLI_LETSENCRYPT_EMAIL Contact email for certificate renewal
10# - SHAARLI_DOCKER_TAG Shaarli docker tag to use
11# See: https://hub.docker.com/r/shaarli/shaarli/tags
11version: '3' 12version: '3'
12 13
13networks: 14networks:
@@ -20,7 +21,7 @@ volumes:
20 21
21services: 22services:
22 shaarli: 23 shaarli:
23 image: shaarli/shaarli:master 24 image: shaarli/shaarli:${SHAARLI_DOCKER_TAG}
24 build: ./ 25 build: ./
25 networks: 26 networks:
26 - http-proxy 27 - http-proxy
@@ -40,7 +41,7 @@ services:
40 - "--entrypoints=Name:https Address::443 TLS" 41 - "--entrypoints=Name:https Address::443 TLS"
41 - "--retry" 42 - "--retry"
42 - "--docker" 43 - "--docker"
43 - "--docker.domain=docker.localhost" 44 - "--docker.domain=${SHAARLI_VIRTUAL_HOST}"
44 - "--docker.exposedbydefault=true" 45 - "--docker.exposedbydefault=true"
45 - "--docker.watch=true" 46 - "--docker.watch=true"
46 - "--acme" 47 - "--acme"
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
index 9a6e3958..26dede4e 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -1,8 +1,8 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: Shaarli\n" 3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2020-09-10 16:06+0200\n" 4"POT-Creation-Date: 2020-11-09 14:39+0100\n"
5"PO-Revision-Date: 2020-09-10 16:07+0200\n" 5"PO-Revision-Date: 2020-11-09 14:42+0100\n"
6"Last-Translator: \n" 6"Last-Translator: \n"
7"Language-Team: Shaarli\n" 7"Language-Team: Shaarli\n"
8"Language: fr_FR\n" 8"Language: fr_FR\n"
@@ -20,38 +20,11 @@ msgstr ""
20"X-Poedit-SearchPath-3: init.php\n" 20"X-Poedit-SearchPath-3: init.php\n"
21"X-Poedit-SearchPath-4: plugins\n" 21"X-Poedit-SearchPath-4: plugins\n"
22 22
23#: application/ApplicationUtils.php:161 23#: application/History.php:180
24#, php-format
25msgid ""
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."
29msgstr ""
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
35msgid "directory is not readable"
36msgstr "le répertoire n'est pas accessible en lecture"
37
38#: application/ApplicationUtils.php:207
39msgid "directory is not writable"
40msgstr "le répertoire n'est pas accessible en écriture"
41
42#: application/ApplicationUtils.php:225
43msgid "file is not readable"
44msgstr "le fichier n'est pas accessible en lecture"
45
46#: application/ApplicationUtils.php:228
47msgid "file is not writable"
48msgstr "le fichier n'est pas accessible en écriture"
49
50#: application/History.php:179
51msgid "History file isn't readable or writable" 24msgid "History file isn't readable or writable"
52msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" 25msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
53 26
54#: application/History.php:190 27#: application/History.php:191
55msgid "Could not parse history file" 28msgid "Could not parse history file"
56msgstr "Format incorrect pour le fichier d'historique" 29msgstr "Format incorrect pour le fichier d'historique"
57 30
@@ -83,52 +56,46 @@ msgstr ""
83"l'extension php-gd doit être chargée pour utiliser les miniatures. Les " 56"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
84"miniatures sont désormais désactivées. Rechargez la page." 57"miniatures sont désormais désactivées. Rechargez la page."
85 58
86#: application/Utils.php:383 59#: application/Utils.php:402
87msgid "Setting not set" 60msgid "Setting not set"
88msgstr "Paramètre non défini" 61msgstr "Paramètre non défini"
89 62
90#: application/Utils.php:390 63#: application/Utils.php:409
91msgid "Unlimited" 64msgid "Unlimited"
92msgstr "Illimité" 65msgstr "Illimité"
93 66
94#: application/Utils.php:393 67#: application/Utils.php:412
95msgid "B" 68msgid "B"
96msgstr "o" 69msgstr "o"
97 70
98#: application/Utils.php:393 71#: application/Utils.php:412
99msgid "kiB" 72msgid "kiB"
100msgstr "ko" 73msgstr "ko"
101 74
102#: application/Utils.php:393 75#: application/Utils.php:412
103msgid "MiB" 76msgid "MiB"
104msgstr "Mo" 77msgstr "Mo"
105 78
106#: application/Utils.php:393 79#: application/Utils.php:412
107msgid "GiB" 80msgid "GiB"
108msgstr "Go" 81msgstr "Go"
109 82
110#: application/bookmark/BookmarkFileService.php:174 83#: application/bookmark/BookmarkFileService.php:183
111#: application/bookmark/BookmarkFileService.php:199 84#: application/bookmark/BookmarkFileService.php:205
112#: application/bookmark/BookmarkFileService.php:224 85#: application/bookmark/BookmarkFileService.php:227
113#: application/bookmark/BookmarkFileService.php:241 86#: application/bookmark/BookmarkFileService.php:241
114msgid "You're not authorized to alter the datastore" 87msgid "You're not authorized to alter the datastore"
115msgstr "Vous n'êtes pas autorisé à modifier les données" 88msgstr "Vous n'êtes pas autorisé à modifier les données"
116 89
117#: application/bookmark/BookmarkFileService.php:177 90#: application/bookmark/BookmarkFileService.php:208
118#: application/bookmark/BookmarkFileService.php:202
119#: application/bookmark/BookmarkFileService.php:244
120msgid "Provided data is invalid"
121msgstr "Les informations fournies ne sont pas valides"
122
123#: application/bookmark/BookmarkFileService.php:205
124msgid "This bookmarks already exists" 91msgid "This bookmarks already exists"
125msgstr "Ce marque-page existe déjà." 92msgstr "Ce marque-page existe déjà"
126 93
127#: application/bookmark/BookmarkInitializer.php:37 94#: application/bookmark/BookmarkInitializer.php:39
128msgid "(private bookmark with thumbnail demo)" 95msgid "(private bookmark with thumbnail demo)"
129msgstr "(marque page privé avec une miniature)" 96msgstr "(marque page privé avec une miniature)"
130 97
131#: application/bookmark/BookmarkInitializer.php:40 98#: application/bookmark/BookmarkInitializer.php:42
132msgid "" 99msgid ""
133"Shaarli will automatically pick up the thumbnail for links to a variety of " 100"Shaarli will automatically pick up the thumbnail for links to a variety of "
134"websites.\n" 101"websites.\n"
@@ -151,11 +118,11 @@ msgstr ""
151"\n" 118"\n"
152"Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n" 119"Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n"
153 120
154#: application/bookmark/BookmarkInitializer.php:53 121#: application/bookmark/BookmarkInitializer.php:55
155msgid "Note: Shaare descriptions" 122msgid "Note: Shaare descriptions"
156msgstr "Note : Description des Shaares" 123msgstr "Note : Description des Shaares"
157 124
158#: application/bookmark/BookmarkInitializer.php:55 125#: application/bookmark/BookmarkInitializer.php:57
159msgid "" 126msgid ""
160"Adding a shaare without entering a URL creates a text-only \"note\" post " 127"Adding a shaare without entering a URL creates a text-only \"note\" post "
161"such as this one.\n" 128"such as this one.\n"
@@ -219,19 +186,19 @@ msgstr ""
219"| Citron | Fruit | Jaune | 30 |\n" 186"| Citron | Fruit | Jaune | 30 |\n"
220"| Carotte | Légume | Orange | 14 |\n" 187"| Carotte | Légume | Orange | 14 |\n"
221 188
222#: application/bookmark/BookmarkInitializer.php:89 189#: application/bookmark/BookmarkInitializer.php:91
223#: application/legacy/LegacyLinkDB.php:246 190#: application/legacy/LegacyLinkDB.php:246
224#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 191#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
225#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 192#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
226#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 193#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
227#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49 194#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
228msgid "" 195msgid ""
229"The personal, minimalist, super-fast, database free, bookmarking service" 196"The personal, minimalist, super-fast, database free, bookmarking service"
230msgstr "" 197msgstr ""
231"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " 198"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
232"données" 199"données"
233 200
234#: application/bookmark/BookmarkInitializer.php:92 201#: application/bookmark/BookmarkInitializer.php:94
235msgid "" 202msgid ""
236"Welcome to Shaarli!\n" 203"Welcome to Shaarli!\n"
237"\n" 204"\n"
@@ -320,7 +287,8 @@ msgid "Direct link"
320msgstr "Liens directs" 287msgstr "Liens directs"
321 288
322#: application/feed/FeedBuilder.php:181 289#: application/feed/FeedBuilder.php:181
323#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 290#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
291#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
324#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 292#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
325msgid "Permalink" 293msgid "Permalink"
326msgstr "Permalien" 294msgstr "Permalien"
@@ -336,12 +304,13 @@ msgid "You have enabled or changed thumbnails mode."
336msgstr "Vous avez activé ou changé le mode de miniatures." 304msgstr "Vous avez activé ou changé le mode de miniatures."
337 305
338#: application/front/controller/admin/ConfigureController.php:103 306#: application/front/controller/admin/ConfigureController.php:103
307#: application/front/controller/admin/ServerController.php:75
339#: application/legacy/LegacyUpdater.php:538 308#: application/legacy/LegacyUpdater.php:538
340msgid "Please synchronize them." 309msgid "Please synchronize them."
341msgstr "Merci de les synchroniser." 310msgstr "Merci de les synchroniser."
342 311
343#: application/front/controller/admin/ConfigureController.php:113 312#: application/front/controller/admin/ConfigureController.php:113
344#: application/front/controller/visitor/InstallController.php:136 313#: application/front/controller/visitor/InstallController.php:146
345msgid "Error while writing config file after configuration update." 314msgid "Error while writing config file after configuration update."
346msgstr "" 315msgstr ""
347"Une erreur s'est produite lors de la sauvegarde du fichier de configuration." 316"Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
@@ -378,70 +347,47 @@ msgstr ""
378"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " 347"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
379"légères." 348"légères."
380 349
381#: application/front/controller/admin/ManageShaareController.php:29 350#: application/front/controller/admin/ManageTagController.php:30
382#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 351msgid "whitespace"
383msgid "Shaare a new link" 352msgstr "espace"
384msgstr "Partager un nouveau lien"
385
386#: application/front/controller/admin/ManageShaareController.php:78
387msgid "Note: "
388msgstr "Note : "
389
390#: application/front/controller/admin/ManageShaareController.php:109
391#: application/front/controller/admin/ManageShaareController.php:206
392#: application/front/controller/admin/ManageShaareController.php:275
393#: application/front/controller/admin/ManageShaareController.php:315
394#, php-format
395msgid "Bookmark with identifier %s could not be found."
396msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
397
398#: application/front/controller/admin/ManageShaareController.php:194
399#: application/front/controller/admin/ManageShaareController.php:252
400msgid "Invalid bookmark ID provided."
401msgstr "ID du lien non valide."
402 353
403#: application/front/controller/admin/ManageShaareController.php:260 354#: application/front/controller/admin/ManageTagController.php:35
404msgid "Invalid visibility provided."
405msgstr "Visibilité du lien non valide."
406
407#: application/front/controller/admin/ManageShaareController.php:363
408#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
409msgid "Edit"
410msgstr "Modifier"
411
412#: application/front/controller/admin/ManageShaareController.php:366
413#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
414#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
415msgid "Shaare"
416msgstr "Shaare"
417
418#: application/front/controller/admin/ManageTagController.php:29
419#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 355#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
420#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 356#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
421msgid "Manage tags" 357msgid "Manage tags"
422msgstr "Gérer les tags" 358msgstr "Gérer les tags"
423 359
424#: application/front/controller/admin/ManageTagController.php:48 360#: application/front/controller/admin/ManageTagController.php:54
425msgid "Invalid tags provided." 361msgid "Invalid tags provided."
426msgstr "Les tags fournis ne sont pas valides." 362msgstr "Les tags fournis ne sont pas valides."
427 363
428#: application/front/controller/admin/ManageTagController.php:72 364#: application/front/controller/admin/ManageTagController.php:78
429#, php-format 365#, php-format
430msgid "The tag was removed from %d bookmark." 366msgid "The tag was removed from %d bookmark."
431msgid_plural "The tag was removed from %d bookmarks." 367msgid_plural "The tag was removed from %d bookmarks."
432msgstr[0] "Le tag a été supprimé du %d lien." 368msgstr[0] "Le tag a été supprimé du %d lien."
433msgstr[1] "Le tag a été supprimé de %d liens." 369msgstr[1] "Le tag a été supprimé de %d liens."
434 370
435#: application/front/controller/admin/ManageTagController.php:77 371#: application/front/controller/admin/ManageTagController.php:83
436#, php-format 372#, php-format
437msgid "The tag was renamed in %d bookmark." 373msgid "The tag was renamed in %d bookmark."
438msgid_plural "The tag was renamed in %d bookmarks." 374msgid_plural "The tag was renamed in %d bookmarks."
439msgstr[0] "Le tag a été renommé dans %d lien." 375msgstr[0] "Le tag a été renommé dans %d lien."
440msgstr[1] "Le tag a été renommé dans %d liens." 376msgstr[1] "Le tag a été renommé dans %d liens."
441 377
378#: application/front/controller/admin/ManageTagController.php:105
379msgid "Tags separator must be a single character."
380msgstr "Un séparateur de tags doit contenir un seul caractère."
381
382#: application/front/controller/admin/ManageTagController.php:111
383msgid "These characters are reserved and can't be used as tags separator: "
384msgstr ""
385"Ces caractères sont réservés et ne peuvent être utilisés comme des "
386"séparateurs de tags : "
387
442#: application/front/controller/admin/PasswordController.php:28 388#: application/front/controller/admin/PasswordController.php:28
443#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 389#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
444#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 390#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
445msgid "Change password" 391msgid "Change password"
446msgstr "Modifier le mot de passe" 392msgstr "Modifier le mot de passe"
447 393
@@ -463,16 +409,71 @@ msgstr "Votre mot de passe a été modifié"
463msgid "Plugin Administration" 409msgid "Plugin Administration"
464msgstr "Administration des plugins" 410msgstr "Administration des plugins"
465 411
466#: application/front/controller/admin/PluginsController.php:75 412#: application/front/controller/admin/PluginsController.php:76
467msgid "Setting successfully saved." 413msgid "Setting successfully saved."
468msgstr "Les paramètres ont été sauvegardés avec succès." 414msgstr "Les paramètres ont été sauvegardés avec succès."
469 415
470#: application/front/controller/admin/PluginsController.php:78 416#: application/front/controller/admin/PluginsController.php:79
471msgid "Error while saving plugin configuration: " 417msgid "Error while saving plugin configuration: "
472msgstr "" 418msgstr ""
473"Une erreur s'est produite lors de la sauvegarde de la configuration des " 419"Une erreur s'est produite lors de la sauvegarde de la configuration des "
474"plugins : " 420"plugins : "
475 421
422#: application/front/controller/admin/ServerController.php:35
423msgid "Check disabled"
424msgstr "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
429msgid "Server administration"
430msgstr "Administration serveur"
431
432#: application/front/controller/admin/ServerController.php:74
433msgid "Thumbnails cache has been cleared."
434msgstr "Le cache des miniatures a été vidé."
435
436#: application/front/controller/admin/ServerController.php:83
437msgid "Shaarli's cache folder has been cleared!"
438msgstr "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
442msgid "Shaare a new link"
443msgstr "Partagez un nouveau lien"
444
445#: application/front/controller/admin/ShaareManageController.php:35
446#: application/front/controller/admin/ShaareManageController.php:93
447msgid "Invalid bookmark ID provided."
448msgstr "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
455msgid "Bookmark with identifier %s could not be found."
456msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
457
458#: application/front/controller/admin/ShaareManageController.php:101
459msgid "Invalid visibility provided."
460msgstr "Visibilité du lien non valide."
461
462#: application/front/controller/admin/ShaarePublishController.php:171
463#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
464msgid "Edit"
465msgstr "Modifier"
466
467#: application/front/controller/admin/ShaarePublishController.php:174
468#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
469#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
470msgid "Shaare"
471msgstr "Shaare"
472
473#: application/front/controller/admin/ShaarePublishController.php:205
474msgid "Note: "
475msgstr "Note : "
476
476#: application/front/controller/admin/ThumbnailsController.php:37 477#: application/front/controller/admin/ThumbnailsController.php:37
477#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 478#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
478msgid "Thumbnails update" 479msgid "Thumbnails update"
@@ -484,29 +485,62 @@ msgstr "Mise à jour des miniatures"
484msgid "Tools" 485msgid "Tools"
485msgstr "Outils" 486msgstr "Outils"
486 487
487#: application/front/controller/visitor/BookmarkListController.php:115 488#: application/front/controller/visitor/BookmarkListController.php:120
488msgid "Search: " 489msgid "Search: "
489msgstr "Recherche : " 490msgstr "Recherche : "
490 491
491#: application/front/controller/visitor/DailyController.php:45 492#: application/front/controller/visitor/DailyController.php:200
492msgid "Today" 493msgid "day"
493msgstr "Aujourd'hui" 494msgstr "jour"
494
495#: application/front/controller/visitor/DailyController.php:47
496msgid "Yesterday"
497msgstr "Hier"
498 495
499#: application/front/controller/visitor/DailyController.php:85 496#: application/front/controller/visitor/DailyController.php:200
497#: application/front/controller/visitor/DailyController.php:203
498#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
500#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 499#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
501#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 500#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
502msgid "Daily" 501msgid "Daily"
503msgstr "Quotidien" 502msgstr "Quotidien"
504 503
505#: application/front/controller/visitor/ErrorController.php:36 504#: application/front/controller/visitor/DailyController.php:201
505msgid "week"
506msgstr "semaine"
507
508#: application/front/controller/visitor/DailyController.php:201
509#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
510msgid "Weekly"
511msgstr "Hebdomadaire"
512
513#: application/front/controller/visitor/DailyController.php:202
514msgid "month"
515msgstr "mois"
516
517#: application/front/controller/visitor/DailyController.php:202
518#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
519msgid "Monthly"
520msgstr "Mensuel"
521
522#: application/front/controller/visitor/ErrorController.php:30
523msgid "Error: "
524msgstr "Erreur : "
525
526#: application/front/controller/visitor/ErrorController.php:34
527msgid "Please report it on Github."
528msgstr "Merci de la rapporter sur Github."
529
530#: application/front/controller/visitor/ErrorController.php:39
506msgid "An unexpected error occurred." 531msgid "An unexpected error occurred."
507msgstr "Une erreur inattendue s'est produite." 532msgstr "Une erreur inattendue s'est produite."
508 533
509#: application/front/controller/visitor/InstallController.php:73 534#: application/front/controller/visitor/ErrorNotFoundController.php:25
535msgid "Requested page could not be found."
536msgstr "La page demandée n'a pas pu être trouvée."
537
538#: application/front/controller/visitor/InstallController.php:64
539#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
540msgid "Install Shaarli"
541msgstr "Installation de Shaarli"
542
543#: application/front/controller/visitor/InstallController.php:83
510#, php-format 544#, php-format
511msgid "" 545msgid ""
512"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " 546"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@@ -525,14 +559,14 @@ msgstr ""
525"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " 559"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
526"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" 560"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
527 561
528#: application/front/controller/visitor/InstallController.php:144 562#: application/front/controller/visitor/InstallController.php:154
529msgid "" 563msgid ""
530"Shaarli is now configured. Please login and start shaaring your bookmarks!" 564"Shaarli is now configured. Please login and start shaaring your bookmarks!"
531msgstr "" 565msgstr ""
532"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " 566"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
533"shaare vos liens !" 567"shaare vos liens !"
534 568
535#: application/front/controller/visitor/InstallController.php:158 569#: application/front/controller/visitor/InstallController.php:168
536msgid "Insufficient permissions:" 570msgid "Insufficient permissions:"
537msgstr "Permissions insuffisantes :" 571msgstr "Permissions insuffisantes :"
538 572
@@ -546,7 +580,7 @@ msgstr "Permissions insuffisantes :"
546msgid "Login" 580msgid "Login"
547msgstr "Connexion" 581msgstr "Connexion"
548 582
549#: application/front/controller/visitor/LoginController.php:78 583#: application/front/controller/visitor/LoginController.php:77
550msgid "Wrong login/password." 584msgid "Wrong login/password."
551msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." 585msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
552 586
@@ -556,11 +590,9 @@ msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
556msgid "Picture wall" 590msgid "Picture wall"
557msgstr "Mur d'images" 591msgstr "Mur d'images"
558 592
559#: application/front/controller/visitor/TagCloudController.php:80 593#: application/front/controller/visitor/TagCloudController.php:90
560#, fuzzy
561#| msgid "Tag list"
562msgid "Tag " 594msgid "Tag "
563msgstr "Liste des tags" 595msgstr "Tag "
564 596
565#: application/front/exceptions/AlreadyInstalledException.php:11 597#: application/front/exceptions/AlreadyInstalledException.php:11
566msgid "Shaarli has already been installed. Login to edit the configuration." 598msgid "Shaarli has already been installed. Login to edit the configuration."
@@ -588,6 +620,86 @@ msgstr ""
588msgid "Wrong token." 620msgid "Wrong token."
589msgstr "Jeton invalide." 621msgstr "Jeton invalide."
590 622
623#: application/helper/ApplicationUtils.php:162
624#, php-format
625msgid ""
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."
629msgstr ""
630"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
631"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
632"connues et devrait être mise à jour au plus tôt."
633
634#: application/helper/ApplicationUtils.php:195
635#: application/helper/ApplicationUtils.php:215
636msgid "directory is not readable"
637msgstr "le répertoire n'est pas accessible en lecture"
638
639#: application/helper/ApplicationUtils.php:218
640msgid "directory is not writable"
641msgstr "le répertoire n'est pas accessible en écriture"
642
643#: application/helper/ApplicationUtils.php:240
644msgid "file is not readable"
645msgstr "le fichier n'est pas accessible en lecture"
646
647#: application/helper/ApplicationUtils.php:243
648msgid "file is not writable"
649msgstr "le fichier n'est pas accessible en écriture"
650
651#: application/helper/ApplicationUtils.php:277
652msgid "Configuration parsing"
653msgstr "Chargement de la configuration"
654
655#: application/helper/ApplicationUtils.php:278
656msgid "Slim Framework (routing, etc.)"
657msgstr "Slim Framwork (routage, etc.)"
658
659#: application/helper/ApplicationUtils.php:279
660msgid "Multibyte (Unicode) string support"
661msgstr "Support des chaînes de caractère multibytes (Unicode)"
662
663#: application/helper/ApplicationUtils.php:280
664msgid "Required to use thumbnails"
665msgstr "Obligatoire pour utiliser les miniatures"
666
667#: application/helper/ApplicationUtils.php:281
668msgid "Localized text sorting (e.g. e->è->f)"
669msgstr "Tri des textes traduits (ex : e->è->f)"
670
671#: application/helper/ApplicationUtils.php:282
672msgid "Better retrieval of bookmark metadata and thumbnail"
673msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
674
675#: application/helper/ApplicationUtils.php:283
676msgid "Use the translation system in gettext mode"
677msgstr "Utiliser le système de traduction en mode gettext"
678
679#: application/helper/ApplicationUtils.php:284
680msgid "Login using LDAP server"
681msgstr "Authentification via un serveur LDAP"
682
683#: application/helper/DailyPageHelper.php:172
684msgid "Week"
685msgstr "Semaine"
686
687#: application/helper/DailyPageHelper.php:176
688msgid "Today"
689msgstr "Aujourd'hui"
690
691#: application/helper/DailyPageHelper.php:178
692msgid "Yesterday"
693msgstr "Hier"
694
695#: application/helper/FileUtils.php:100
696msgid "Provided path is not a directory."
697msgstr "Le chemin fourni n'est pas un dossier."
698
699#: application/helper/FileUtils.php:104
700msgid "Trying to delete a folder outside of Shaarli path."
701msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli."
702
591#: application/legacy/LegacyLinkDB.php:131 703#: application/legacy/LegacyLinkDB.php:131
592msgid "You are not authorized to add a link." 704msgid "You are not authorized to add a link."
593msgstr "Vous n'êtes pas autorisé à ajouter un lien." 705msgstr "Vous n'êtes pas autorisé à ajouter un lien."
@@ -664,7 +776,7 @@ msgstr ""
664"a été importé avec succès en %d secondes : %d liens importés, %d liens " 776"a été importé avec succès en %d secondes : %d liens importés, %d liens "
665"écrasés, %d liens ignorés." 777"écrasés, %d liens ignorés."
666 778
667#: application/plugin/PluginManager.php:122 779#: application/plugin/PluginManager.php:124
668msgid " [plugin incompatibility]: " 780msgid " [plugin incompatibility]: "
669msgstr " [incompatibilité de l'extension] : " 781msgstr " [incompatibilité de l'extension] : "
670 782
@@ -682,7 +794,7 @@ msgstr "Impossible de purger %s : le répertoire n'existe pas"
682msgid "An error occurred while running the update " 794msgid "An error occurred while running the update "
683msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " 795msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
684 796
685#: index.php:62 797#: index.php:80
686msgid "Shared bookmarks on " 798msgid "Shared bookmarks on "
687msgstr "Liens partagés sur " 799msgstr "Liens partagés sur "
688 800
@@ -699,11 +811,11 @@ msgstr "Shaare"
699msgid "Adds the addlink input on the linklist page." 811msgid "Adds the addlink input on the linklist page."
700msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." 812msgstr "Ajoute le formulaire d'ajout de liens sur la page principale."
701 813
702#: plugins/archiveorg/archiveorg.php:26 814#: plugins/archiveorg/archiveorg.php:28
703msgid "View on archive.org" 815msgid "View on archive.org"
704msgstr "Voir sur archive.org" 816msgstr "Voir sur archive.org"
705 817
706#: plugins/archiveorg/archiveorg.php:39 818#: plugins/archiveorg/archiveorg.php:41
707msgid "For each link, add an Archive.org icon." 819msgid "For each link, add an Archive.org icon."
708msgstr "Pour chaque lien, ajoute une icône pour Archive.org." 820msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
709 821
@@ -823,7 +935,7 @@ msgstr "Mauvaise réponse du hub %s"
823msgid "Enable PubSubHubbub feed publishing." 935msgid "Enable PubSubHubbub feed publishing."
824msgstr "Active la publication de flux vers PubSubHubbub." 936msgstr "Active la publication de flux vers PubSubHubbub."
825 937
826#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70 938#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:71
827msgid "For each link, add a QRCode icon." 939msgid "For each link, add a QRCode icon."
828msgstr "Pour chaque lien, ajouter une icône de QRCode." 940msgstr "Pour chaque lien, ajouter une icône de QRCode."
829 941
@@ -835,15 +947,15 @@ msgstr ""
835"Erreur de l'extension Wallabag : Merci de définir le paramètre « " 947"Erreur de l'extension Wallabag : Merci de définir le paramètre « "
836"WALLABAG_URL » dans la page d'administration des extensions." 948"WALLABAG_URL » dans la page d'administration des extensions."
837 949
838#: plugins/wallabag/wallabag.php:47 950#: plugins/wallabag/wallabag.php:48
839msgid "Save to wallabag" 951msgid "Save to wallabag"
840msgstr "Sauvegarder dans Wallabag" 952msgstr "Sauvegarder dans Wallabag"
841 953
842#: plugins/wallabag/wallabag.php:71 954#: plugins/wallabag/wallabag.php:72
843msgid "Wallabag API URL" 955msgid "Wallabag API URL"
844msgstr "URL de l'API Wallabag" 956msgstr "URL de l'API Wallabag"
845 957
846#: plugins/wallabag/wallabag.php:72 958#: plugins/wallabag/wallabag.php:73
847msgid "Wallabag API version (1 or 2)" 959msgid "Wallabag API version (1 or 2)"
848msgstr "Version de l'API Wallabag (1 ou 2)" 960msgstr "Version de l'API Wallabag (1 ou 2)"
849 961
@@ -855,6 +967,48 @@ msgstr "Désolé, il y a rien à voir ici."
855msgid "URL or leave empty to post a note" 967msgid "URL or leave empty to post a note"
856msgstr "URL ou laisser vide pour créer une note" 968msgstr "URL ou laisser vide pour créer une note"
857 969
970#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
971msgid "BULK CREATION"
972msgstr "CRÉATION DE MASSE"
973
974#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
975msgid "Metadata asynchronous retrieval is disabled."
976msgstr "La récupération asynchrone des meta-données est désactivée."
977
978#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
979msgid ""
980"We recommend that you enable the setting <em>general > "
981"enable_async_metadata</em> in your configuration file to use bulk link "
982"creation."
983msgstr ""
984"Nous recommandons d'activer le paramètre <em>general > "
985"enable_async_metadata</em> dans votre fichier de configuration pour utiliser "
986"la création de masse."
987
988#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
989msgid "Shaare multiple new links"
990msgstr "Partagez plusieurs nouveaux liens"
991
992#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
993msgid "Add one URL per line to create multiple bookmarks."
994msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages."
995
996#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
997#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
998msgid "Tags"
999msgstr "Tags"
1000
1001#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
1002#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
1003#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
1004#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1005msgid "Private"
1006msgstr "Privé"
1007
1008#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
1009msgid "Add links"
1010msgstr "Ajouter des liens"
1011
858#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 1012#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
859msgid "Current password" 1013msgid "Current password"
860msgstr "Mot de passe actuel" 1014msgstr "Mot de passe actuel"
@@ -881,26 +1035,48 @@ msgid "Case sensitive"
881msgstr "Sensible à la casse" 1035msgstr "Sensible à la casse"
882 1036
883#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 1037#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
884msgid "Rename" 1038#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
885msgstr "Renommer" 1039msgid "Rename tag"
1040msgstr "Renommer le tag"
886 1041
887#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 1042#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
888#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93 1043msgid "Delete tag"
889#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 1044msgstr "Supprimer le tag"
890#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
891#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
892#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
893msgid "Delete"
894msgstr "Supprimer"
895 1045
896#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 1046#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
897msgid "You can also edit tags in the" 1047msgid "You can also edit tags in the"
898msgstr "Vous pouvez aussi modifier les tags dans la" 1048msgstr "Vous pouvez aussi modifier les tags dans la"
899 1049
900#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 1050#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
901msgid "tag list" 1051msgid "tag list"
902msgstr "liste des tags" 1052msgstr "liste des tags"
903 1053
1054#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1055msgid "Change tags separator"
1056msgstr "Changer le séparateur de tags"
1057
1058#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
1059msgid "Your current tag separator is"
1060msgstr "Votre séparateur actuel est"
1061
1062#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
1063msgid "New separator"
1064msgstr "Nouveau séparateur"
1065
1066#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
1067#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
1068#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
1069#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
1070#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
1071msgid "Save"
1072msgstr "Enregistrer"
1073
1074#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
1075msgid "Note that hashtags won't fully work with a non-whitespace separator."
1076msgstr ""
1077"Notez que les hashtags ne sont pas complètement fonctionnels avec un "
1078"séparateur qui n'est pas un espace."
1079
904#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 1080#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
905msgid "title" 1081msgid "title"
906msgstr "titre" 1082msgstr "titre"
@@ -1024,71 +1200,72 @@ msgstr ""
1024"miniatures." 1200"miniatures."
1025 1201
1026#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 1202#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
1027#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 1203#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
1028msgid "Synchronize thumbnails" 1204msgid "Synchronize thumbnails"
1029msgstr "Synchroniser les miniatures" 1205msgstr "Synchroniser les miniatures"
1030 1206
1031#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 1207#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
1032#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 1208#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1209#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1033msgid "All" 1210msgid "All"
1034msgstr "Tous" 1211msgstr "Tous"
1035 1212
1036#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 1213#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
1214#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
1037msgid "Only common media hosts" 1215msgid "Only common media hosts"
1038msgstr "Seulement les hébergeurs de média connus" 1216msgstr "Seulement les hébergeurs de média connus"
1039 1217
1040#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 1218#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
1219#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
1041msgid "None" 1220msgid "None"
1042msgstr "Aucune" 1221msgstr "Aucune"
1043 1222
1044#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 1223#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
1045#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 1224msgid "1 RSS entry per :type"
1046#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 1225msgid_plural ""
1047#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 1226msgstr[0] "1 entrée RSS par :type"
1048msgid "Save" 1227msgstr[1] ""
1049msgstr "Enregistrer" 1228
1050 1229#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
1051#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1230msgid "Previous :type"
1052msgid "The Daily Shaarli" 1231msgid_plural ""
1053msgstr "Le Quotidien Shaarli" 1232msgstr[0] ":type précédent"
1054 1233msgstr[1] "Jour précédent"
1055#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 1234
1056msgid "1 RSS entry per day" 1235#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
1057msgstr "1 entrée RSS par jour" 1236#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
1058 1237msgid "All links of one :type in a single page."
1059#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 1238msgid_plural ""
1060msgid "Previous day" 1239msgstr[0] "Tous les liens d'un :type sur une page."
1061msgstr "Jour précédent" 1240msgstr[1] "Tous les liens d'un jour sur une page."
1062 1241
1063#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 1242#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
1064msgid "All links of one day in a single page." 1243msgid "Next :type"
1065msgstr "Tous les liens d'un jour sur une page." 1244msgid_plural ""
1066 1245msgstr[0] ":type suivant"
1067#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 1246msgstr[1] ""
1068msgid "Next day" 1247
1069msgstr "Jour suivant" 1248#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1070
1071#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
1072msgid "Edit Shaare" 1249msgid "Edit Shaare"
1073msgstr "Modifier le Shaare" 1250msgstr "Modifier le Shaare"
1074 1251
1075#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 1252#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1076msgid "New Shaare" 1253msgid "New Shaare"
1077msgstr "Nouveau Shaare" 1254msgstr "Nouveau Shaare"
1078 1255
1079#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 1256#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
1080msgid "Created:" 1257msgid "Created:"
1081msgstr "Création :" 1258msgstr "Création :"
1082 1259
1083#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 1260#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1084msgid "URL" 1261msgid "URL"
1085msgstr "URL" 1262msgstr "URL"
1086 1263
1087#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 1264#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1088msgid "Title" 1265msgid "Title"
1089msgstr "Titre" 1266msgstr "Titre"
1090 1267
1091#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1268#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
1092#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1269#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1093#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 1270#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
1094#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 1271#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@@ -1096,32 +1273,39 @@ msgstr "Titre"
1096msgid "Description" 1273msgid "Description"
1097msgstr "Description" 1274msgstr "Description"
1098 1275
1099#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 1276#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89
1100msgid "Tags"
1101msgstr "Tags"
1102
1103#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
1104#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
1105#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1106msgid "Private"
1107msgstr "Privé"
1108
1109#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1110msgid "Description will be rendered with" 1277msgid "Description will be rendered with"
1111msgstr "La description sera générée avec" 1278msgstr "La description sera générée avec"
1112 1279
1113#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 1280#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
1114msgid "Markdown syntax documentation" 1281msgid "Markdown syntax documentation"
1115msgstr "Documentation sur la syntaxe Markdown" 1282msgstr "Documentation sur la syntaxe Markdown"
1116 1283
1117#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 1284#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
1118msgid "Markdown syntax" 1285msgid "Markdown syntax"
1119msgstr "la syntaxe Markdown" 1286msgstr "la syntaxe Markdown"
1120 1287
1121#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 1288#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115
1289msgid "Cancel"
1290msgstr "Annuler"
1291
1292#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
1122msgid "Apply Changes" 1293msgid "Apply Changes"
1123msgstr "Appliquer les changements" 1294msgstr "Appliquer les changements"
1124 1295
1296#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126
1297#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
1298#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
1299#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
1300#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
1301msgid "Delete"
1302msgstr "Supprimer"
1303
1304#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
1305#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
1306msgid "Save all"
1307msgstr "Tout enregistrer"
1308
1125#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 1309#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1126msgid "Export Database" 1310msgid "Export Database"
1127msgstr "Exporter les données" 1311msgstr "Exporter les données"
@@ -1179,10 +1363,6 @@ msgstr "Les doublons s'appuient sur les URL"
1179msgid "Add default tags" 1363msgid "Add default tags"
1180msgstr "Ajouter des tags par défaut" 1364msgstr "Ajouter des tags par défaut"
1181 1365
1182#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
1183msgid "Install Shaarli"
1184msgstr "Installation de Shaarli"
1185
1186#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 1366#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
1187msgid "It looks like it's the first time you run Shaarli. Please configure it." 1367msgid "It looks like it's the first time you run Shaarli. Please configure it."
1188msgstr "" 1368msgstr ""
@@ -1215,6 +1395,10 @@ msgstr "Mes liens"
1215msgid "Install" 1395msgid "Install"
1216msgstr "Installer" 1396msgstr "Installer"
1217 1397
1398#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190
1399msgid "Server requirements"
1400msgstr "Pré-requis serveur"
1401
1218#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1402#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1219#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 1403#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
1220msgid "shaare" 1404msgid "shaare"
@@ -1288,8 +1472,8 @@ msgid "without any tag"
1288msgstr "sans tag" 1472msgstr "sans tag"
1289 1473
1290#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 1474#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
1291#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 1475#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1292#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 1476#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41
1293msgid "Fold" 1477msgid "Fold"
1294msgstr "Replier" 1478msgstr "Replier"
1295 1479
@@ -1313,6 +1497,10 @@ msgstr "Changer statut épinglé"
1313msgid "Sticky" 1497msgid "Sticky"
1314msgstr "Épinglé" 1498msgstr "Épinglé"
1315 1499
1500#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
1501msgid "Share a private link"
1502msgstr "Partager un lien privé"
1503
1316#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 1504#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1317#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 1505#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5
1318msgid "Filters" 1506msgid "Filters"
@@ -1331,7 +1519,7 @@ msgstr "Afficher uniquement les liens publics"
1331#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 1519#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
1332#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 1520#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
1333msgid "Filter untagged links" 1521msgid "Filter untagged links"
1334msgstr "Filtrer par liens privés" 1522msgstr "Filtrer par liens sans tag"
1335 1523
1336#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 1524#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
1337#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 1525#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24
@@ -1342,8 +1530,8 @@ msgstr "Tout sélectionner"
1342#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 1530#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89
1343#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 1531#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29
1344#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 1532#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89
1345#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 1533#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1346#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 1534#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
1347msgid "Fold all" 1535msgid "Fold all"
1348msgstr "Replier tout" 1536msgstr "Replier tout"
1349 1537
@@ -1359,9 +1547,9 @@ msgid "Remember me"
1359msgstr "Rester connecté" 1547msgstr "Rester connecté"
1360 1548
1361#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1549#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1362#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 1550#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1363#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 1551#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
1364#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49 1552#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
1365msgid "by the Shaarli community" 1553msgid "by the Shaarli community"
1366msgstr "par la communauté Shaarli" 1554msgstr "par la communauté Shaarli"
1367 1555
@@ -1370,21 +1558,26 @@ msgstr "par la communauté Shaarli"
1370msgid "Documentation" 1558msgid "Documentation"
1371msgstr "Documentation" 1559msgstr "Documentation"
1372 1560
1373#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 1561#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1374#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 1562#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
1375msgid "Expand" 1563msgid "Expand"
1376msgstr "Déplier" 1564msgstr "Déplier"
1377 1565
1378#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 1566#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1379#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 1567#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
1380msgid "Expand all" 1568msgid "Expand all"
1381msgstr "Déplier tout" 1569msgstr "Déplier tout"
1382 1570
1383#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 1571#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
1384#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:47 1572#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
1385msgid "Are you sure you want to delete this link?" 1573msgid "Are you sure you want to delete this link?"
1386msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" 1574msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
1387 1575
1576#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1577#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
1578msgid "Are you sure you want to delete this tag?"
1579msgstr "Êtes-vous sûr de vouloir supprimer ce tag ?"
1580
1388#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11 1581#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11
1389#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11 1582#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11
1390msgid "Menu" 1583msgid "Menu"
@@ -1511,6 +1704,100 @@ msgstr "Configuration des extensions"
1511msgid "No parameter available." 1704msgid "No parameter available."
1512msgstr "Aucun paramètre disponible." 1705msgstr "Aucun paramètre disponible."
1513 1706
1707#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1708msgid "General"
1709msgstr "Général"
1710
1711#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
1712msgid "Index URL"
1713msgstr "URL de l'index"
1714
1715#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1716msgid "Base path"
1717msgstr "Chemin de base"
1718
1719#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1720msgid "Client IP"
1721msgstr "IP du client"
1722
1723#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1724msgid "Trusted reverse proxies"
1725msgstr "Reverse proxies de confiance"
1726
1727#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
1728msgid "N/A"
1729msgstr "N/A"
1730
1731#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
1732msgid "Visit releases page on Github"
1733msgstr "Visiter la page des releases sur Github"
1734
1735#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
1736msgid "Synchronize all link thumbnails"
1737msgstr "Synchroniser toutes les miniatures"
1738
1739#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2
1740#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2
1741msgid "Permissions"
1742msgstr "Permissions"
1743
1744#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8
1745#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8
1746msgid "There are permissions that need to be fixed."
1747msgstr "Il y a des permissions qui doivent être corrigées."
1748
1749#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1750#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23
1751msgid "All read/write permissions are properly set."
1752msgstr "Toutes les permissions de lecture/écriture sont définies correctement."
1753
1754#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
1755#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32
1756msgid "Running PHP"
1757msgstr "Fonctionnant avec PHP"
1758
1759#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1760#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36
1761msgid "End of life: "
1762msgstr "Fin de vie : "
1763
1764#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1765#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48
1766msgid "Extension"
1767msgstr "Extension"
1768
1769#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
1770#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49
1771msgid "Usage"
1772msgstr "Utilisation"
1773
1774#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
1775#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50
1776msgid "Status"
1777msgstr "Statut"
1778
1779#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
1780#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1781#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51
1782#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66
1783msgid "Loaded"
1784msgstr "Chargé"
1785
1786#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
1787#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
1788msgid "Required"
1789msgstr "Obligatoire"
1790
1791#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
1792#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
1793msgid "Optional"
1794msgstr "Optionnel"
1795
1796#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
1797#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70
1798msgid "Not loaded"
1799msgstr "Non chargé"
1800
1514#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 1801#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1515#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 1802#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1516msgid "tags" 1803msgid "tags"
@@ -1525,10 +1812,6 @@ msgstr "Lister tous les liens avec ces tags"
1525msgid "Tag list" 1812msgid "Tag list"
1526msgstr "Liste des tags" 1813msgstr "Liste des tags"
1527 1814
1528#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
1529msgid "Rename tag"
1530msgstr "Renommer le tag"
1531
1532#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 1815#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1533#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 1816#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1534msgid "Sort by:" 1817msgid "Sort by:"
@@ -1565,15 +1848,19 @@ msgstr "Configurer Shaarli"
1565msgid "Enable, disable and configure plugins" 1848msgid "Enable, disable and configure plugins"
1566msgstr "Activer, désactiver et configurer les extensions" 1849msgstr "Activer, désactiver et configurer les extensions"
1567 1850
1568#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 1851#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
1852msgid "Check instance's server configuration"
1853msgstr "Vérifier la configuration serveur de l'instance"
1854
1855#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
1569msgid "Change your password" 1856msgid "Change your password"
1570msgstr "Modifier le mot de passe" 1857msgstr "Modifier le mot de passe"
1571 1858
1572#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 1859#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1573msgid "Rename or delete a tag in all links" 1860msgid "Rename or delete a tag in all links"
1574msgstr "Renommer ou supprimer un tag dans tous les liens" 1861msgstr "Renommer ou supprimer un tag dans tous les liens"
1575 1862
1576#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1863#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1577msgid "" 1864msgid ""
1578"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " 1865"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1579"delicious...)" 1866"delicious...)"
@@ -1581,11 +1868,11 @@ msgstr ""
1581"Importer des marques pages au format Netscape HTML (comme exportés depuis " 1868"Importer des marques pages au format Netscape HTML (comme exportés depuis "
1582"Firefox, Chrome, Opera, delicious...)" 1869"Firefox, Chrome, Opera, delicious...)"
1583 1870
1584#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1871#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1585msgid "Import links" 1872msgid "Import links"
1586msgstr "Importer des liens" 1873msgstr "Importer des liens"
1587 1874
1588#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 1875#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
1589msgid "" 1876msgid ""
1590"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " 1877"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1591"Opera, delicious...)" 1878"Opera, delicious...)"
@@ -1593,15 +1880,11 @@ msgstr ""
1593"Exporter les marques pages au format Netscape HTML (comme exportés depuis " 1880"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
1594"Firefox, Chrome, Opera, delicious...)" 1881"Firefox, Chrome, Opera, delicious...)"
1595 1882
1596#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 1883#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
1597msgid "Export database" 1884msgid "Export database"
1598msgstr "Exporter les données" 1885msgstr "Exporter les données"
1599 1886
1600#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55 1887#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
1601msgid "Synchronize all link thumbnails"
1602msgstr "Synchroniser toutes les miniatures"
1603
1604#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
1605msgid "" 1888msgid ""
1606"Drag one of these button to your bookmarks toolbar or right-click it and " 1889"Drag one of these button to your bookmarks toolbar or right-click it and "
1607"\"Bookmark This Link\"" 1890"\"Bookmark This Link\""
@@ -1609,13 +1892,13 @@ msgstr ""
1609"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " 1892"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit "
1610"dessus et « Ajouter aux favoris »" 1893"dessus et « Ajouter aux favoris »"
1611 1894
1612#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 1895#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
1613msgid "then click on the bookmarklet in any page you want to share." 1896msgid "then click on the bookmarklet in any page you want to share."
1614msgstr "" 1897msgstr ""
1615"puis cliquer sur le marque-page depuis un site que vous souhaitez partager." 1898"puis cliquer sur le marque-page depuis un site que vous souhaitez partager."
1616 1899
1617#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 1900#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
1618#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 1901#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
1619msgid "" 1902msgid ""
1620"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " 1903"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1621"Link" 1904"Link"
@@ -1623,40 +1906,40 @@ msgstr ""
1623"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1906"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1624"Ajouter aux favoris »" 1907"Ajouter aux favoris »"
1625 1908
1626#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 1909#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
1627msgid "then click ✚Shaare link button in any page you want to share" 1910msgid "then click ✚Shaare link button in any page you want to share"
1628msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" 1911msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
1629 1912
1630#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 1913#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
1631#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 1914#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
1632msgid "The selected text is too long, it will be truncated." 1915msgid "The selected text is too long, it will be truncated."
1633msgstr "Le texte sélectionné est trop long, il sera tronqué." 1916msgstr "Le texte sélectionné est trop long, il sera tronqué."
1634 1917
1635#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 1918#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1636msgid "Shaare link" 1919msgid "Shaare link"
1637msgstr "Shaare" 1920msgstr "Shaare"
1638 1921
1639#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 1922#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
1640msgid "" 1923msgid ""
1641"Then click ✚Add Note button anytime to start composing a private Note (text " 1924"Then click ✚Add Note button anytime to start composing a private Note (text "
1642"post) to your Shaarli" 1925"post) to your Shaarli"
1643msgstr "" 1926msgstr ""
1644"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" 1927"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
1645 1928
1646#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 1929#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1647msgid "Add Note" 1930msgid "Add Note"
1648msgstr "Ajouter une Note" 1931msgstr "Ajouter une Note"
1649 1932
1650#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 1933#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
1651msgid "3rd party" 1934msgid "3rd party"
1652msgstr "Applications tierces" 1935msgstr "Applications tierces"
1653 1936
1654#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 1937#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
1655#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 1938#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
1656msgid "plugin" 1939msgid "plugin"
1657msgstr "extension" 1940msgstr "extension"
1658 1941
1659#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 1942#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
1660msgid "" 1943msgid ""
1661"Drag this link to your bookmarks toolbar, or right-click it and choose " 1944"Drag this link to your bookmarks toolbar, or right-click it and choose "
1662"Bookmark This Link" 1945"Bookmark This Link"
@@ -1664,6 +1947,12 @@ msgstr ""
1664"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1947"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1665"Ajouter aux favoris »" 1948"Ajouter aux favoris »"
1666 1949
1950#~ msgid "Display:"
1951#~ msgstr "Afficher :"
1952
1953#~ msgid "The Daily Shaarli"
1954#~ msgstr "Le Quotidien Shaarli"
1955
1667#, fuzzy 1956#, fuzzy
1668#~| msgid "Selection" 1957#~| msgid "Selection"
1669#~ msgid ".ui-selecting" 1958#~ msgid ".ui-selecting"
diff --git a/inc/languages/jp/LC_MESSAGES/shaarli.po b/inc/languages/jp/LC_MESSAGES/shaarli.po
index b420bb51..57f42fc2 100644
--- a/inc/languages/jp/LC_MESSAGES/shaarli.po
+++ b/inc/languages/jp/LC_MESSAGES/shaarli.po
@@ -2,15 +2,15 @@ msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: Shaarli\n" 3"Project-Id-Version: Shaarli\n"
4"Report-Msgid-Bugs-To: \n" 4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2020-02-11 09:31+0900\n" 5"POT-Creation-Date: 2020-10-19 10:19+0900\n"
6"PO-Revision-Date: 2020-02-11 10:54+0900\n" 6"PO-Revision-Date: 2020-10-19 10:25+0900\n"
7"Last-Translator: yude <yudesleepy@gmail.com>\n" 7"Last-Translator: yude <yudesleepy@gmail.com>\n"
8"Language-Team: Shaarli\n" 8"Language-Team: Shaarli\n"
9"Language: ja\n" 9"Language: ja\n"
10"MIME-Version: 1.0\n" 10"MIME-Version: 1.0\n"
11"Content-Type: text/plain; charset=UTF-8\n" 11"Content-Type: text/plain; charset=UTF-8\n"
12"Content-Transfer-Encoding: 8bit\n" 12"Content-Transfer-Encoding: 8bit\n"
13"X-Generator: Poedit 2.3\n" 13"X-Generator: Poedit 2.2.3\n"
14"X-Poedit-Basepath: ../../../..\n" 14"X-Poedit-Basepath: ../../../..\n"
15"Plural-Forms: nplurals=2; plural=(n != 1);\n" 15"Plural-Forms: nplurals=2; plural=(n != 1);\n"
16"X-Poedit-SourceCharset: UTF-8\n" 16"X-Poedit-SourceCharset: UTF-8\n"
@@ -19,7 +19,7 @@ msgstr ""
19"X-Poedit-SearchPathExcluded-0: node_modules\n" 19"X-Poedit-SearchPathExcluded-0: node_modules\n"
20"X-Poedit-SearchPathExcluded-1: vendor\n" 20"X-Poedit-SearchPathExcluded-1: vendor\n"
21 21
22#: application/ApplicationUtils.php:153 22#: application/ApplicationUtils.php:161
23#, php-format 23#, php-format
24msgid "" 24msgid ""
25"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " 25"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
@@ -30,200 +30,250 @@ msgstr ""
30"ãŒå¿…è¦ã§ã™ã€‚ ç¾åœ¨ä½¿ç”¨ã—ã¦ã„ã‚‹ PHP ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã¯è„†å¼±æ€§ãŒã‚ã‚Šã€ã§ãã‚‹ã ã‘速" 30"ãŒå¿…è¦ã§ã™ã€‚ ç¾åœ¨ä½¿ç”¨ã—ã¦ã„ã‚‹ PHP ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã¯è„†å¼±æ€§ãŒã‚ã‚Šã€ã§ãã‚‹ã ã‘速"
31"ã‚„ã‹ã«ã‚¢ãƒƒãƒ—デートã™ã‚‹ã¹ãã§ã™ã€‚" 31"ã‚„ã‹ã«ã‚¢ãƒƒãƒ—デートã™ã‚‹ã¹ãã§ã™ã€‚"
32 32
33#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195 33#: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204
34msgid "directory is not readable" 34msgid "directory is not readable"
35msgstr "ディレクトリを読ã¿è¾¼ã‚ã¾ã›ã‚“" 35msgstr "ディレクトリを読ã¿è¾¼ã‚ã¾ã›ã‚“"
36 36
37#: application/ApplicationUtils.php:198 37#: application/ApplicationUtils.php:207
38msgid "directory is not writable" 38msgid "directory is not writable"
39msgstr "ディレクトリã«æ›¸ãè¾¼ã‚ã¾ã›ã‚“" 39msgstr "ディレクトリã«æ›¸ãè¾¼ã‚ã¾ã›ã‚“"
40 40
41#: application/ApplicationUtils.php:216 41#: application/ApplicationUtils.php:225
42msgid "file is not readable" 42msgid "file is not readable"
43msgstr "ファイルを読ã¿å–る権é™ãŒã‚ã‚Šã¾ã›ã‚“" 43msgstr "ファイルを読ã¿å–る権é™ãŒã‚ã‚Šã¾ã›ã‚“"
44 44
45#: application/ApplicationUtils.php:219 45#: application/ApplicationUtils.php:228
46msgid "file is not writable" 46msgid "file is not writable"
47msgstr "ファイルを書ã込む権é™ãŒã‚ã‚Šã¾ã›ã‚“" 47msgstr "ファイルを書ã込む権é™ãŒã‚ã‚Šã¾ã›ã‚“"
48 48
49#: application/Cache.php:16 49#: application/History.php:179
50#, php-format
51msgid "Cannot purge %s: no directory"
52msgstr "%s を削除ã§ãã¾ã›ã‚“: ディレクトリãŒå­˜åœ¨ã—ã¾ã›ã‚“"
53
54#: application/FeedBuilder.php:151
55msgid "Direct link"
56msgstr "ダイレクトリンク"
57
58#: application/FeedBuilder.php:153
59#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
60#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178
61msgid "Permalink"
62msgstr "パーマリンク"
63
64#: application/History.php:174
65msgid "History file isn't readable or writable" 50msgid "History file isn't readable or writable"
66msgstr "履歴ファイルを読ã¿è¾¼ã‚€ã€ã¾ãŸã¯æ›¸ã込むãŸã‚ã®æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“" 51msgstr "履歴ファイルを読ã¿è¾¼ã‚€ã€ã¾ãŸã¯æ›¸ã込むãŸã‚ã®æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“"
67 52
68#: application/History.php:185 53#: application/History.php:190
69msgid "Could not parse history file" 54msgid "Could not parse history file"
70msgstr "履歴ファイルを正常ã«å¾©å…ƒã§ãã¾ã›ã‚“ã§ã—ãŸ" 55msgstr "履歴ファイルを正常ã«å¾©å…ƒã§ãã¾ã›ã‚“ã§ã—ãŸ"
71 56
72#: application/Languages.php:177 57#: application/Languages.php:181
73msgid "Automatic" 58msgid "Automatic"
74msgstr "自動" 59msgstr "自動"
75 60
76#: application/Languages.php:178 61#: application/Languages.php:182
62msgid "German"
63msgstr "ドイツ語"
64
65#: application/Languages.php:183
77msgid "English" 66msgid "English"
78msgstr "英語" 67msgstr "英語"
79 68
80#: application/Languages.php:179 69#: application/Languages.php:184
81msgid "French" 70msgid "French"
82msgstr "フランス語" 71msgstr "フランス語"
83 72
84#: application/Languages.php:180 73#: application/Languages.php:185
85msgid "German" 74msgid "Japanese"
86msgstr "ドイツ語" 75msgstr "日本語"
87
88#: application/LinkDB.php:136
89msgid "You are not authorized to add a link."
90msgstr "リンクを追加ã™ã‚‹ã«ã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
91
92#: application/LinkDB.php:139
93msgid "Internal Error: A link should always have an id and URL."
94msgstr "エラー: リンクã«ã¯IDã¨URLを登録ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
95
96#: application/LinkDB.php:142
97msgid "You must specify an integer as a key."
98msgstr "正常ãªã‚­ãƒ¼ã®å€¤ã§ã¯ã‚ã‚Šã¾ã›ã‚“。"
99
100#: application/LinkDB.php:145
101msgid "Array offset and link ID must be equal."
102msgstr "Array オフセットã¨ãƒªãƒ³ã‚¯ã®IDã¯åŒã˜ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
103
104#: application/LinkDB.php:251
105#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
106#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
107#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
108#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
109msgid ""
110"The personal, minimalist, super-fast, database free, bookmarking service"
111msgstr ""
112"個人å‘ã‘ã®ã€ãƒŸãƒ‹ãƒžãƒ ã§é«˜é€Ÿã§ã‹ã¤ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã®ã„らãªã„ブックマークサービス"
113
114#: application/LinkDB.php:253
115msgid ""
116"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
117"me, you must first login.\n"
118"\n"
119"To learn how to use Shaarli, consult the link \"Documentation\" at the "
120"bottom of this page.\n"
121"\n"
122"You use the community supported version of the original Shaarli project, by "
123"Sebastien Sauvage."
124msgstr ""
125"Shaarli ã¸ã‚ˆã†ã“ãï¼ ã“ã‚Œã¯ã‚ãªãŸã®æœ€åˆã®å…¬é–‹ãƒ–ックマークã§ã™ã€‚ã“れを編集ã—ãŸ"
126"り削除ã—ãŸã‚Šã™ã‚‹ã«ã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚\n"
127"\n"
128"Shaarli ã®ä½¿ã„方を知るã«ã¯ã€ã“ã®ãƒšãƒ¼ã‚¸ã®ä¸‹ã«ã‚る「ドキュメントã€ã®ãƒªãƒ³ã‚¯ã‚’é–‹"
129"ã„ã¦ãã ã•ã„。\n"
130"\n"
131"ã‚ãªãŸã¯ Sebastien Sauvage ã«ã‚ˆã‚‹ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ãƒ¼ã‚µãƒãƒ¼ãƒˆã®ã‚ã‚‹ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ã‚ª"
132"リジナルã®Shaarli プロジェクトを使用ã—ã¦ã„ã¾ã™ã€‚"
133
134#: application/LinkDB.php:267
135msgid "My secret stuff... - Pastebin.com"
136msgstr "ã‚ãŸã—ã®ã²ðŸ’—ã¿ðŸ’—ã¤ðŸ’— - Pastebin.com"
137
138#: application/LinkDB.php:269
139msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
140msgstr ""
141"ã‚·ãƒ¼ãƒƒï¼ ã“ã‚Œã¯ã‚ãªãŸã—ã‹è¦‹ã‚‰ã‚Œãªã„プライベートリンクã§ã™ã€‚消ã™ã“ã¨ã‚‚ã§ãã¾"
142"ã™ã€‚"
143
144#: application/LinkFilter.php:452
145msgid "The link you are trying to reach does not exist or has been deleted."
146msgstr "é–‹ã“ã†ã¨ã—ãŸãƒªãƒ³ã‚¯ã¯å­˜åœ¨ã—ãªã„ã‹ã€å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚"
147
148#: application/NetscapeBookmarkUtils.php:35
149msgid "Invalid export selection:"
150msgstr "ä¸æ­£ãªã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã®é¸æŠž:"
151
152#: application/NetscapeBookmarkUtils.php:81
153#, php-format
154msgid "File %s (%d bytes) "
155msgstr "ファイル %s (%d ãƒã‚¤ãƒˆ) "
156
157#: application/NetscapeBookmarkUtils.php:83
158msgid "has an unknown file format. Nothing was imported."
159msgstr "ã¯ä¸æ˜Žãªãƒ•ã‚¡ã‚¤ãƒ«å½¢å¼ã§ã™ã€‚インãƒãƒ¼ãƒˆã¯ä¸­æ­¢ã•ã‚Œã¾ã—ãŸã€‚"
160 76
161#: application/NetscapeBookmarkUtils.php:86 77#: application/Thumbnailer.php:62
162#, php-format
163msgid "" 78msgid ""
164"was successfully processed in %d seconds: %d links imported, %d links " 79"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
165"overwritten, %d links skipped." 80"disabled. Please reload the page."
166msgstr "" 81msgstr ""
167"㌠%d 秒ã§å‡¦ç†ã•ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒã‚¤ãƒ³ãƒãƒ¼ãƒˆã•ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒä¸Šæ›¸ãã•" 82"サムãƒã‚¤ãƒ«ã‚’使用ã™ã‚‹ã«ã¯ã€php-gd エクステンションãŒèª­ã¿è¾¼ã¾ã‚Œã¦ã„ã‚‹å¿…è¦ãŒã‚ã‚Š"
168"ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒã‚¹ã‚­ãƒƒãƒ—ã•ã‚Œã¾ã—ãŸã€‚" 83"ã¾ã™ã€‚サムãƒã‚¤ãƒ«ã¯ç„¡åŠ¹åŒ–ã•ã‚Œã¾ã—ãŸã€‚ページをå†èª­è¾¼ã—ã¦ãã ã•ã„。"
169
170#: application/PageBuilder.php:168
171msgid "The page you are trying to reach does not exist or has been deleted."
172msgstr "ã‚ãªãŸãŒé–‹ã“ã†ã¨ã—ãŸãƒšãƒ¼ã‚¸ã¯å­˜åœ¨ã—ãªã„ã‹ã€å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚"
173
174#: application/PageBuilder.php:170
175msgid "404 Not Found"
176msgstr "404 ページãŒå­˜åœ¨ã—ã¾ã›ã‚“"
177
178#: application/PluginManager.php:243
179#, php-format
180msgid "Plugin \"%s\" files not found."
181msgstr "プラグイン「%sã€ã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒå­˜åœ¨ã—ã¾ã›ã‚“。"
182
183#: application/Updater.php:76
184msgid "Couldn't retrieve Updater class methods."
185msgstr "アップデーターã®ã‚¯ãƒ©ã‚¹ãƒ¡ã‚¾ãƒƒãƒˆã‚’å—ä¿¡ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
186
187#: application/Updater.php:532
188msgid "An error occurred while running the update "
189msgstr "更新中ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—㟠"
190
191#: application/Updater.php:572
192msgid "Updates file path is not set, can't write updates."
193msgstr "æ›´æ–°ã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã®ãƒ‘スãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ãŸã‚ã€æ›´æ–°ã‚’書ãè¾¼ã‚ã¾ã›ã‚“。"
194 84
195#: application/Updater.php:577 85#: application/Utils.php:383 tests/UtilsTest.php:343
196msgid "Unable to write updates in "
197msgstr "更新を次ã®é …ç›®ã«æ›¸ãè¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸ: "
198
199#: application/Utils.php:376 tests/UtilsTest.php:340
200msgid "Setting not set" 86msgid "Setting not set"
201msgstr "未設定" 87msgstr "未設定"
202 88
203#: application/Utils.php:383 tests/UtilsTest.php:338 tests/UtilsTest.php:339 89#: application/Utils.php:390 tests/UtilsTest.php:341 tests/UtilsTest.php:342
204msgid "Unlimited" 90msgid "Unlimited"
205msgstr "無制é™" 91msgstr "無制é™"
206 92
207#: application/Utils.php:386 tests/UtilsTest.php:335 tests/UtilsTest.php:336 93#: application/Utils.php:393 tests/UtilsTest.php:338 tests/UtilsTest.php:339
208#: tests/UtilsTest.php:350 94#: tests/UtilsTest.php:353
209msgid "B" 95msgid "B"
210msgstr "B" 96msgstr "B"
211 97
212#: application/Utils.php:386 tests/UtilsTest.php:329 tests/UtilsTest.php:330 98#: application/Utils.php:393 tests/UtilsTest.php:332 tests/UtilsTest.php:333
213#: tests/UtilsTest.php:337 99#: tests/UtilsTest.php:340
214msgid "kiB" 100msgid "kiB"
215msgstr "kiB" 101msgstr "kiB"
216 102
217#: application/Utils.php:386 tests/UtilsTest.php:331 tests/UtilsTest.php:332 103#: application/Utils.php:393 tests/UtilsTest.php:334 tests/UtilsTest.php:335
218#: tests/UtilsTest.php:348 tests/UtilsTest.php:349 104#: tests/UtilsTest.php:351 tests/UtilsTest.php:352
219msgid "MiB" 105msgid "MiB"
220msgstr "MiB" 106msgstr "MiB"
221 107
222#: application/Utils.php:386 tests/UtilsTest.php:333 tests/UtilsTest.php:334 108#: application/Utils.php:393 tests/UtilsTest.php:336 tests/UtilsTest.php:337
223msgid "GiB" 109msgid "GiB"
224msgstr "GiB" 110msgstr "GiB"
225 111
226#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121 112#: application/bookmark/BookmarkFileService.php:180
113#: application/bookmark/BookmarkFileService.php:202
114#: application/bookmark/BookmarkFileService.php:224
115#: application/bookmark/BookmarkFileService.php:238
116msgid "You're not authorized to alter the datastore"
117msgstr "設定を変更ã™ã‚‹æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“"
118
119#: application/bookmark/BookmarkFileService.php:205
120msgid "This bookmarks already exists"
121msgstr "ã“ã®ãƒ–ックマークã¯æ—¢ã«å­˜åœ¨ã—ã¾ã™ã€‚"
122
123#: application/bookmark/BookmarkInitializer.php:39
124msgid "(private bookmark with thumbnail demo)"
125msgstr "(サムãƒã‚¤ãƒ«ãƒ‡ãƒ¢ãŒä»˜å±žã—ã¦ã„るプライベートブックマーク)"
126
127#: application/bookmark/BookmarkInitializer.php:42
128msgid ""
129"Shaarli will automatically pick up the thumbnail for links to a variety of "
130"websites.\n"
131"\n"
132"Explore your new Shaarli instance by trying out controls and menus.\n"
133"Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the "
134"documentation](https://shaarli.readthedocs.io/en/master/) to learn more "
135"about Shaarli.\n"
136"\n"
137"Now you can edit or delete the default shaares.\n"
138msgstr ""
139"Shaarli ã¯è‡ªå‹•çš„ã«å¤šæ§˜ãªã‚¦ã‚§ãƒ–サイトã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’å–å¾—ã—ã¾ã™ã€‚\n"
140"\n"
141"ã‚ãªãŸã®æ–°ã—ã„ Shaarli インスタンスをコントロールやメニューを試ã—ãŸã‚Šã—ã¦ã€æŽ¢"
142"検ã—ã¦ãã ã•ã„。\n"
143" [Github](https://github.com/shaarli/Shaarli) ã¾ãŸã¯ [the documentation]"
144"(https://shaarli.readthedocs.io/en/master/) ã§ãƒ—ロジェクトを訪å•ã—ã¦ã€"
145"Shaarli ã‚’ã‚‚ã£ã¨ã‚ˆã知るã“ã¨ãŒã§ãã¾ã™ã€‚\n"
146"\n"
147"今ã‹ã‚‰ã€æ—¢å®šã® shaares を編集ã—ãŸã‚Šã€å‰Šé™¤ã—ãŸã‚Šã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚\n"
148
149#: application/bookmark/BookmarkInitializer.php:55
150msgid "Note: Shaare descriptions"
151msgstr "説明: Shaare ã®æ¦‚è¦"
152
153#: application/bookmark/BookmarkInitializer.php:57
154msgid ""
155"Adding a shaare without entering a URL creates a text-only \"note\" post "
156"such as this one.\n"
157"This note is private, so you are the only one able to see it while logged "
158"in.\n"
159"\n"
160"You can use this to keep notes, post articles, code snippets, and much "
161"more.\n"
162"\n"
163"The Markdown formatting setting allows you to format your notes and bookmark "
164"description:\n"
165"\n"
166"### Title headings\n"
167"\n"
168"#### Multiple headings levels\n"
169" * bullet lists\n"
170" * _italic_ text\n"
171" * **bold** text\n"
172" * ~~strike through~~ text\n"
173" * `code` blocks\n"
174" * images\n"
175" * [links](https://en.wikipedia.org/wiki/Markdown)\n"
176"\n"
177"Markdown also supports tables:\n"
178"\n"
179"| Name | Type | Color | Qty |\n"
180"| ------- | --------- | ------ | ----- |\n"
181"| Orange | Fruit | Orange | 126 |\n"
182"| Apple | Fruit | Any | 62 |\n"
183"| Lemon | Fruit | Yellow | 30 |\n"
184"| Carrot | Vegetable | Red | 14 |\n"
185msgstr ""
186"URL を追加ã›ãšã« shaare を作æˆã™ã‚‹ã¨ã€ãƒ†ã‚­ã‚¹ãƒˆã®ã¿ã®ã“ã®ã‚ˆã†ãª \"ノート\" ãŒ"
187"作æˆã•ã‚Œã¾ã™ã€‚\n"
188"ã“ã®ãƒŽãƒ¼ãƒˆã¯ãƒ—ライベートãªã®ã§ã€ãƒ­ã‚°ã‚¤ãƒ³ä¸­ã®ã‚ãªãŸã—ã‹è¦‹ã‚‹ã“ã¨ã¯ã§ãã¾ã›"
189"ん。\n"
190"\n"
191"ã‚ãªãŸã¯ã“れをメモ帳ã¨ã—ã¦ä½¿ã£ãŸã‚Šã€è¨˜äº‹ã‚’投稿ã—ãŸã‚Šã€ã‚³ãƒ¼ãƒ‰ スニペットã¨ã—ãŸ"
192"ã‚Šã™ã‚‹ãªã©ã¨ã„ã£ãŸã“ã¨ã«ä½¿ãˆã¾ã™ã€‚\n"
193"\n"
194"Markdown フォーマットã®è¨­å®šã«ã‚ˆã‚Šã€ãƒŽãƒ¼ãƒˆã‚„ブックマークã®æ¦‚è¦ã‚’以下ã®ã‚ˆã†ã«"
195"フォーマットã§ãã¾ã™:\n"
196"\n"
197"### タイトル ヘッダー\n"
198"\n"
199"#### 複数ã®è¦‹å‡ºã—\n"
200" * 箇æ¡æ›¸ãリスト\n"
201" * _イタリック_ 文字\n"
202" * **ボールド** 文字\n"
203" * ~~打ã¡æ¶ˆã—~~ 文字\n"
204" * `コード` ブロック\n"
205" * ç”»åƒ\n"
206" * [リンク](https://en.wikipedia.org/wiki/Markdown)\n"
207"\n"
208"Markdown ã¯è¡¨ã‚‚サãƒãƒ¼ãƒˆã—ã¾ã™:\n"
209"\n"
210"| åå‰ | 種類 | 色 | æ•°é‡ |\n"
211"| ------- | --------- | ------ | ----- |\n"
212"| オレンジ | 果物 | 橙 | 126 |\n"
213"| リンゴ | 果物 | ä»»æ„ | 62 |\n"
214"| レモン | 果物 | 黄 | 30 |\n"
215"| äººå‚ | é‡Žèœ | 赤 | 14 |\n"
216
217#: application/bookmark/BookmarkInitializer.php:91
218#: application/legacy/LegacyLinkDB.php:246
219msgid ""
220"The personal, minimalist, super-fast, database free, bookmarking service"
221msgstr ""
222"個人å‘ã‘ã®ã€ãƒŸãƒ‹ãƒžãƒ ã§é«˜é€Ÿã§ã‹ã¤ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã®ã„らãªã„ブックマークサービス"
223
224#: application/bookmark/BookmarkInitializer.php:94
225msgid ""
226"Welcome to Shaarli!\n"
227"\n"
228"Shaarli allows you to bookmark your favorite pages, and share them with "
229"others or store them privately.\n"
230"You can add a description to your bookmarks, such as this one, and tag "
231"them.\n"
232"\n"
233"Create a new shaare by clicking the `+Shaare` button, or using any of the "
234"recommended tools (browser extension, mobile app, bookmarklet, REST API, "
235"etc.).\n"
236"\n"
237"You can easily retrieve your links, even with thousands of them, using the "
238"internal search engine, or search through tags (e.g. this Shaare is tagged "
239"with `shaarli` and `help`).\n"
240"Hashtags such as #shaarli #help are also supported.\n"
241"You can also filter the available [RSS feed](/feed/atom) and picture wall by "
242"tag or plaintext search.\n"
243"\n"
244"We hope that you will enjoy using Shaarli, maintained with â¤ï¸ by the "
245"community!\n"
246"Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if "
247"you have a suggestion or encounter an issue.\n"
248msgstr ""
249"Shaarli ã¸ã‚ˆã†ã“ãï¼\n"
250"\n"
251"Shaarli ã§ã¯ã€ã‚ãªãŸã®ãŠæ°—ã«å…¥ã‚Šã®ãƒšãƒ¼ã‚¸ã‚’ブックマークã—ãŸã‚Šã€ãれを他ã®äººã¨"
252"共有ã™ã‚‹ã‹ã€ã¾ãŸã¯ãƒ—ライベートãªã‚‚ã®ã¨ã—ã¦ä¿ç®¡ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚\n"
253"加ãˆã¦ã€ã‚ãªãŸã®ãƒ–ックマークã«ã“ã®é …ç›®ã®ã‚ˆã†ã«æ¦‚è¦ã‚’追加ã—ãŸã‚Šã€ã‚¿ã‚°ä»˜ã‘ã—ãŸ"
254"ã‚Šã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚\n"
255"\n"
256"`+Shaare` ボタンをクリックã™ã‚‹ã“ã¨ã§æ–°ã—ã„ shaare を作æˆã§ãã¾ã™ã€‚ã¾ãŸã€æŽ¨å¥¨"
257"ã•ã‚ŒãŸãƒ„ールを使ã†ã“ã¨ã‚‚ã§ãã¾ã™ (ブラウザー 拡張機能ã€ãƒ¢ãƒã‚¤ãƒ« アプリã€ãƒ–ッ"
258"クマークレットã€REST API ãªã©...)。\n"
259"\n"
260"ã¾ãŸã€ç°¡å˜ã«ã‚ãªãŸã®ãƒªãƒ³ã‚¯ã‚’å–å¾—ã§ãã¾ã™ã€‚ãã‚ŒãŒä½•åƒã¨ç™»ã‚‹æ•°ã§ã‚ã£ã¦ã‚‚ã€å†…部"
261"ã®æ¤œç´¢ã‚¨ãƒ³ã‚¸ãƒ³ã‚„ã€ã‚¿ã‚°ã‚’使ã£ã¦æ¤œç´¢ã§ãã¾ã™ (例ãˆã°ã€ã“ã® Shaare 㯠`shaarli` "
262"㨠`help` ã¨ã„ã†ã‚¿ã‚°ãŒä»˜ã„ã¦ã„ã¾ã™)。\n"
263"#shaarli ã‚„ #help ã¨ã„ã£ãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚‚サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã™ã€‚\n"
264"タグやテキスト検索ã«ã‚ˆã‚‹ [RSS フィード](/feed/atom) ã‚„ ピクãƒãƒ£ãƒ¼ ウォール ã§"
265"項目を絞るã“ã¨ã‚‚ã§ãã¾ã™ã€‚\n"
266"\n"
267"ç§ãŸã¡ã¯ã‚ãªãŸãŒ Shaarli を楽ã—ã‚“ã§ãれるã“ã¨ã‚’願ã£ã¦ã„ã¾ã™ã€‚Shaarli ã¯ã‚³ãƒŸãƒ¥"
268"ニティーã«ã‚ˆã£ã¦ ♡ ã¨å…±ã«ãƒ¡ãƒ³ãƒ†ãƒŠãƒ³ã‚¹ã•ã‚Œã¦ã„ã¾ã™ï¼\n"
269"何ã‹å•é¡Œã«é­é‡ã—ãŸã‚Šã€æ案ãŒã‚ã‚Œã°ã€æ°—軽㫠[Issue](https://github.com/"
270"shaarli/Shaarli/issues) ã‚’é–‹ã„ã¦ãã ã•ã„。\n"
271
272#: application/bookmark/exception/BookmarkNotFoundException.php:13
273msgid "The link you are trying to reach does not exist or has been deleted."
274msgstr "é–‹ã“ã†ã¨ã—ãŸãƒªãƒ³ã‚¯ã¯å­˜åœ¨ã—ãªã„ã‹ã€å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚"
275
276#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129
227msgid "" 277msgid ""
228"Shaarli could not create the config file. Please make sure Shaarli has the " 278"Shaarli could not create the config file. Please make sure Shaarli has the "
229"right to write in the folder is it installed in." 279"right to write in the folder is it installed in."
@@ -232,7 +282,8 @@ msgstr ""
232"ã¦ã„ã¦ã€ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„るディレクトリã«æ›¸ãè¾¼ã¿ã§ãã‚‹ã“ã¨ã‚’確èªã—ã¦ãã " 282"ã¦ã„ã¦ã€ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„るディレクトリã«æ›¸ãè¾¼ã¿ã§ãã‚‹ã“ã¨ã‚’確èªã—ã¦ãã "
233"ã•ã„。" 283"ã•ã„。"
234 284
235#: application/config/ConfigManager.php:135 285#: application/config/ConfigManager.php:136
286#: application/config/ConfigManager.php:163
236msgid "Invalid setting key parameter. String expected, got: " 287msgid "Invalid setting key parameter. String expected, got: "
237msgstr "" 288msgstr ""
238"ä¸æ­£ãªã‚­ãƒ¼ã®å€¤ã§ã™ã€‚文字列ãŒæƒ³å®šã•ã‚Œã¦ã„ã¾ã™ãŒã€æ¬¡ã®ã‚ˆã†ã«å…¥åŠ›ã•ã‚Œã¾ã—ãŸ: " 289"ä¸æ­£ãªã‚­ãƒ¼ã®å€¤ã§ã™ã€‚文字列ãŒæƒ³å®šã•ã‚Œã¦ã„ã¾ã™ãŒã€æ¬¡ã®ã‚ˆã†ã«å…¥åŠ›ã•ã‚Œã¾ã—ãŸ: "
@@ -250,159 +301,185 @@ msgstr "プラグインã®èª­è¾¼é †ã‚’変更ã™ã‚‹éš›ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾
250msgid "You are not authorized to alter config." 301msgid "You are not authorized to alter config."
251msgstr "設定を変更ã™ã‚‹æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“。" 302msgstr "設定を変更ã™ã‚‹æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“。"
252 303
253#: application/exceptions/IOException.php:19 304#: application/exceptions/IOException.php:22
254msgid "Error accessing" 305msgid "Error accessing"
255msgstr "読込中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ" 306msgstr "読込中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
256 307
257#: index.php:142 308#: application/feed/FeedBuilder.php:179
258msgid "Shared links on " 309msgid "Direct link"
259msgstr "次ã«ãŠãã¦å…±æœ‰ã•ãŒãŸãƒªãƒ³ã‚¯:" 310msgstr "ダイレクトリンク"
260 311
261#: index.php:164 312#: application/feed/FeedBuilder.php:181
262msgid "Insufficient permissions:" 313msgid "Permalink"
263msgstr "権é™ãŒãã‚Šã¾ãã‚“:" 314msgstr "パーマリンク"
264 315
265#: index.php:303 316#: application/front/controller/admin/ConfigureController.php:54
266msgid "I said: NO. You are banned for the moment. Go away." 317msgid "Configure"
267msgstr "ã‚ãªãŸã¯ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰BANã•ã‚Œã¦ã„ã¾ã™ã€‚" 318msgstr "設定"
268 319
269#: index.php:368 320#: application/front/controller/admin/ConfigureController.php:102
270msgid "Wrong login/password." 321#: application/legacy/LegacyUpdater.php:537
271msgstr "ä¸æ­£ãªãƒ¦ãƒ¼ã‚¶ãƒ¼åã€ã¾ãŸã¯ãƒ‘スワードã§ã™ã€‚" 322msgid "You have enabled or changed thumbnails mode."
323msgstr "サムãƒã‚¤ãƒ«ã®ãƒ¢ãƒ¼ãƒ‰ã‚’有効化ã€ã¾ãŸã¯å¤‰æ›´ã—ã¾ã—ãŸã€‚"
272 324
273#: index.php:576 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 325#: application/front/controller/admin/ConfigureController.php:103
274#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42 326#: application/legacy/LegacyUpdater.php:538
275msgid "Daily" 327msgid "Please synchronize them."
276msgstr "デイリー" 328msgstr "ãれらをåŒæœŸã—ã¦ãã ã•ã„。"
277 329
278#: index.php:681 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 330#: application/front/controller/admin/ConfigureController.php:113
279#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 331#: application/front/controller/visitor/InstallController.php:136
280#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 332msgid "Error while writing config file after configuration update."
281#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95 333msgstr "設定ファイルを更新ã—ãŸå¾Œã®æ›¸ãè¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
282#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71
283#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95
284msgid "Login"
285msgstr "ログイン"
286 334
287#: index.php:722 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 335#: application/front/controller/admin/ConfigureController.php:122
288#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39 336msgid "Configuration was saved."
289msgid "Picture wall" 337msgstr "設定ã¯ä¿å­˜ã•ã‚Œã¾ã—ãŸã€‚"
290msgstr "ピクãƒãƒ£ã‚¦ã‚©ãƒ¼ãƒ«"
291 338
292#: index.php:770 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 339#: application/front/controller/admin/ExportController.php:26
293#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 340msgid "Export"
294#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 341msgstr "エクスãƒãƒ¼ãƒˆ"
295msgid "Tag cloud"
296msgstr "タグクラウド"
297 342
298#: index.php:803 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 343#: application/front/controller/admin/ExportController.php:42
299msgid "Tag list" 344msgid "Please select an export mode."
300msgstr "タグ一覧" 345msgstr "エクスãƒãƒ¼ãƒˆ モードを指定ã—ã¦ãã ã•ã„。"
301 346
302#: index.php:1028 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 347#: application/front/controller/admin/ImportController.php:41
303#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 348msgid "Import"
304msgid "Tools" 349msgstr "インãƒãƒ¼ãƒˆ"
305msgstr "ツール"
306 350
307#: index.php:1037 351#: application/front/controller/admin/ImportController.php:55
308msgid "You are not supposed to change a password on an Open Shaarli." 352msgid "No import file provided."
353msgstr "何ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆå…ƒãƒ•ã‚¡ã‚¤ãƒ«ã‚‚指定ã•ã‚Œã¾ã›ã‚“ã§ã—ãŸã€‚"
354
355#: application/front/controller/admin/ImportController.php:66
356#, php-format
357msgid ""
358"The file you are trying to upload is probably bigger than what this "
359"webserver can accept (%s). Please upload in smaller chunks."
309msgstr "" 360msgstr ""
310"公開ã•ã‚Œã¦ã„ã‚‹ Shaarli ã«ãŠã„ã¦ã€ãƒ‘スワードを変更ã™ã‚‹ã“ã¨ã¯æƒ³å®šã•ã‚Œã¦ã„ã¾ã›" 361"ã‚ãªãŸãŒã‚¢ãƒƒãƒ—ロードã—よã†ã¨ã—ã¦ã„るファイルã¯ã€ã‚µãƒ¼ãƒãƒ¼ãŒè¨±å¯ã—ã¦ã„るファイ"
311"ん。" 362"ルサイズ (%s) よりも大ãã„ã§ã™ã€‚ã‚‚ã†å°‘ã—å°ã•ã„ã‚‚ã®ã‚’アップロードã—ã¦ãã ã•"
363"ã„。"
312 364
313#: index.php:1042 index.php:1084 index.php:1160 index.php:1191 index.php:1291 365#: application/front/controller/admin/ManageShaareController.php:29
314msgid "Wrong token." 366msgid "Shaare a new link"
315msgstr "ä¸æ­£ãªãƒˆãƒ¼ã‚¯ãƒ³ã§ã™ã€‚" 367msgstr "æ–°ããリンクを追加"
316 368
317#: index.php:1047 369#: application/front/controller/admin/ManageShaareController.php:78
318msgid "The old password is not correct." 370msgid "Note: "
319msgstr "å…ƒã®ãƒ‘スワードãŒæ­£ã—ãã‚ã‚Šã¾ã›ã‚“。" 371msgstr "注: "
320 372
321#: index.php:1067 373#: application/front/controller/admin/ManageShaareController.php:109
322msgid "Your password has been changed" 374#: application/front/controller/admin/ManageShaareController.php:206
323msgstr "ã‚ãªãŸã®ãƒ‘スワードã¯å¤‰æ›´ã•ã‚Œã¾ã—ãŸ" 375#: application/front/controller/admin/ManageShaareController.php:275
376#: application/front/controller/admin/ManageShaareController.php:315
377#, php-format
378msgid "Bookmark with identifier %s could not be found."
379msgstr "%s ã¨ã„ã†è­˜åˆ¥å­ã‚’æŒã£ãŸãƒ–ックマークã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚"
324 380
325#: index.php:1072 381#: application/front/controller/admin/ManageShaareController.php:194
326#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 382#: application/front/controller/admin/ManageShaareController.php:252
327#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 383msgid "Invalid bookmark ID provided."
328msgid "Change password" 384msgstr "ä¸æ­£ãªãƒ–ックマーク ID ãŒå…¥åŠ›ã•ã‚Œã¾ã—ãŸã€‚"
329msgstr "パスワードを変更"
330 385
331#: index.php:1120 386#: application/front/controller/admin/ManageShaareController.php:260
332msgid "Configuration was saved." 387msgid "Invalid visibility provided."
333msgstr "設定ã¯ä¿å­˜ã•ã‚Œã¾ã—ãŸã€‚" 388msgstr "ä¸æ­£ãªå…¬é–‹è¨­å®šãŒå…¥åŠ›ã•ã‚Œã¾ã—ãŸã€‚"
334 389
335#: index.php:1143 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 390#: application/front/controller/admin/ManageShaareController.php:363
336msgid "Configure" 391msgid "Edit"
337msgstr "設定" 392msgstr "共有"
393
394#: application/front/controller/admin/ManageShaareController.php:366
395msgid "Shaare"
396msgstr "Shaare"
338 397
339#: index.php:1154 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 398#: application/front/controller/admin/ManageTagController.php:29
340#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
341msgid "Manage tags" 399msgid "Manage tags"
342msgstr "タグを設定" 400msgstr "タグを設定"
343 401
344#: index.php:1172 402#: application/front/controller/admin/ManageTagController.php:48
403msgid "Invalid tags provided."
404msgstr "ä¸æ­£ãªã‚¿ã‚°ãŒå…¥åŠ›ã•ã‚Œã¾ã—ãŸã€‚"
405
406#: application/front/controller/admin/ManageTagController.php:72
345#, php-format 407#, php-format
346msgid "The tag was removed from %d link." 408msgid "The tag was removed from %d bookmark."
347msgid_plural "The tag was removed from %d links." 409msgid_plural "The tag was removed from %d bookmarks."
348msgstr[0] "%d 件ã®ãƒªãƒ³ã‚¯ã‹ã‚‰ã‚¿ã‚°ãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚" 410msgstr[0] "%d 件ã®ãƒªãƒ³ã‚¯ã‹ã‚‰ã‚¿ã‚°ãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚"
349msgstr[1] "The tag was removed from %d links." 411msgstr[1] "%d 件ã®ãƒªãƒ³ã‚¯ã‹ã‚‰ã‚¿ã‚°ãŒå‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚"
350 412
351#: index.php:1173 413#: application/front/controller/admin/ManageTagController.php:77
352#, php-format 414#, php-format
353msgid "The tag was renamed in %d link." 415msgid "The tag was renamed in %d bookmark."
354msgid_plural "The tag was renamed in %d links." 416msgid_plural "The tag was renamed in %d bookmarks."
355msgstr[0] "タグ㌠%d 件ã®ãƒªãƒ³ã‚¯ã«ãŠã„ã¦ã€åå‰ãŒå¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚" 417msgstr[0] "ã“ã®ã‚¿ã‚°ã‚’æŒã¤ %d 件ã®ãƒªãƒ³ã‚¯ã«ãŠã„ã¦ã€åå‰ãŒå¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚"
356msgstr[1] "タグ㌠%d 件ã®ãƒªãƒ³ã‚¯ã«ãŠã„ã¦ã€åå‰ãŒå¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚" 418msgstr[1] "ã“ã®ã‚¿ã‚°ã‚’æŒã¤ %d 件ã®ãƒªãƒ³ã‚¯ã«ãŠã„ã¦ã€åå‰ãŒå¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚"
357 419
358#: index.php:1181 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 420#: application/front/controller/admin/PasswordController.php:28
359msgid "Shaare a new link" 421msgid "Change password"
360msgstr "æ–°ããリンクを追加" 422msgstr "パスワードを変更"
361 423
362#: index.php:1351 tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 424#: application/front/controller/admin/PasswordController.php:55
363#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 425msgid "You must provide the current and new password to change it."
364msgid "Edit" 426msgstr ""
365msgstr "共有" 427"パスワードを変更ã™ã‚‹ã«ã¯ã€ç¾åœ¨ã®ãƒ‘スワードã¨ã€æ–°ã—ã„パスワードを入力ã™ã‚‹å¿…è¦"
428"ãŒã‚ã‚Šã¾ã™ã€‚"
366 429
367#: index.php:1351 index.php:1421 430#: application/front/controller/admin/PasswordController.php:71
368#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 431msgid "The old password is not correct."
369#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 432msgstr "å…ƒã®ãƒ‘スワードãŒæ­£ã—ãã‚ã‚Šã¾ã›ã‚“。"
370#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
371msgid "Shaare"
372msgstr "Shaare"
373 433
374#: index.php:1390 434#: application/front/controller/admin/PasswordController.php:97
375msgid "Note: " 435msgid "Your password has been changed"
376msgstr "注: " 436msgstr "ã‚ãªãŸã®ãƒ‘スワードã¯å¤‰æ›´ã•ã‚Œã¾ã—ãŸ"
377 437
378#: index.php:1430 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 438#: application/front/controller/admin/PluginsController.php:45
379msgid "Export" 439msgid "Plugin Administration"
380msgstr "エクスãƒãƒ¼ãƒˆ" 440msgstr "プラグイン管ç†"
381 441
382#: index.php:1492 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 442#: application/front/controller/admin/PluginsController.php:76
383msgid "Import" 443msgid "Setting successfully saved."
384msgstr "インãƒãƒ¼ãƒˆ" 444msgstr "設定ãŒæ­£å¸¸ã«ä¿å­˜ã•ãŒã¾ããŸã€‚"
385 445
386#: index.php:1502 446#: application/front/controller/admin/PluginsController.php:79
387#, php-format 447msgid "Error while saving plugin configuration: "
388msgid "" 448msgstr "プラグインã®è¨­å®šãƒ•ã‚¡ã‚¤ãƒ«ã‚’ä¿å­˜ã™ã‚‹ã¨ãã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ: "
389"The file you are trying to upload is probably bigger than what this "
390"webserver can accept (%s). Please upload in smaller chunks."
391msgstr ""
392"ã‚ãªãŸãŒã‚¢ãƒƒãƒ—ロードã—よã†ã¨ã—ã¦ã„るファイルã¯ã€ã‚µãƒ¼ãƒãƒ¼ãŒè¨±å¯ã—ã¦ã„るファイ"
393"ルサイズ (%s) よりも大ãã„ã§ã™ã€‚ã‚‚ã†å°‘ã—å°ã•ã„ã‚‚ã®ã‚’アップロードã—ã¦ãã ã•"
394"ã„。"
395 449
396#: index.php:1541 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 450#: application/front/controller/admin/ThumbnailsController.php:37
397#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 451msgid "Thumbnails update"
398msgid "Plugin administration" 452msgstr "サムãƒã‚¤ãƒ«ã®æ›´æ–°"
399msgstr "プラグイン管ç†" 453
454#: application/front/controller/admin/ToolsController.php:31
455msgid "Tools"
456msgstr "ツール"
400 457
401#: index.php:1706 458#: application/front/controller/visitor/BookmarkListController.php:116
402msgid "Search: " 459msgid "Search: "
403msgstr "検索: " 460msgstr "検索: "
404 461
405#: index.php:1933 462#: application/front/controller/visitor/DailyController.php:45
463msgid "Today"
464msgstr "今日"
465
466#: application/front/controller/visitor/DailyController.php:47
467msgid "Yesterday"
468msgstr "昨日"
469
470#: application/front/controller/visitor/DailyController.php:85
471msgid "Daily"
472msgstr "デイリー"
473
474#: application/front/controller/visitor/ErrorController.php:36
475msgid "An unexpected error occurred."
476msgstr "予期ã—ãªã„エラーãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
477
478#: application/front/controller/visitor/ErrorNotFoundController.php:25
479msgid "Requested page could not be found."
480msgstr "リクエストã•ã‚ŒãŸãƒšãƒ¼ã‚¸ã¯å­˜åœ¨ã—ã¾ã›ã‚“。"
481
482#: application/front/controller/visitor/InstallController.php:73
406#, php-format 483#, php-format
407msgid "" 484msgid ""
408"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " 485"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@@ -420,32 +497,205 @@ msgstr ""
420"ã‚Šã¾ã™ã€‚IP アドレスや完全ãªãƒ‰ãƒ¡ã‚¤ãƒ³åã§ã‚µãƒ¼ãƒãƒ¼ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—" 497"ã‚Šã¾ã™ã€‚IP アドレスや完全ãªãƒ‰ãƒ¡ã‚¤ãƒ³åã§ã‚µãƒ¼ãƒãƒ¼ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—"
421"ã¾ã™ã€‚<br>" 498"ã¾ã™ã€‚<br>"
422 499
423#: index.php:1943 500#: application/front/controller/visitor/InstallController.php:144
424msgid "Click to try again." 501msgid ""
425msgstr "クリックã—ã¦å†åº¦è©¦ã—ã¾ã™ã€‚" 502"Shaarli is now configured. Please login and start shaaring your bookmarks!"
503msgstr ""
504"Shaarli ã®è¨­å®šãŒå®Œäº†ã—ã¾ã—ãŸã€‚ログインã—ã¦ã€ã‚ãªãŸã®ãƒ–ックマークを登録ã—ã¾"
505"ã—ょã†ï¼"
506
507#: application/front/controller/visitor/InstallController.php:158
508msgid "Insufficient permissions:"
509msgstr "権é™ãŒã‚ã‚Šã¾ã›ã‚“:"
510
511#: application/front/controller/visitor/LoginController.php:46
512msgid "Login"
513msgstr "ログイン"
514
515#: application/front/controller/visitor/LoginController.php:78
516msgid "Wrong login/password."
517msgstr "ä¸æ­£ãªãƒ¦ãƒ¼ã‚¶ãƒ¼åã€ã¾ãŸã¯ãƒ‘スワードã§ã™ã€‚"
518
519#: application/front/controller/visitor/PictureWallController.php:29
520msgid "Picture wall"
521msgstr "ピクãƒãƒ£ã‚¦ã‚©ãƒ¼ãƒ«"
522
523#: application/front/controller/visitor/TagCloudController.php:88
524msgid "Tag "
525msgstr "ã‚¿ã‚° "
526
527#: application/front/exceptions/AlreadyInstalledException.php:11
528msgid "Shaarli has already been installed. Login to edit the configuration."
529msgstr "Shaarli ãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¾ã—ãŸã€‚ログインã—ã¦è¨­å®šã‚’変更ã§ãã¾ã™ã€‚"
530
531#: application/front/exceptions/LoginBannedException.php:11
532msgid ""
533"You have been banned after too many failed login attempts. Try again later."
534msgstr "複数回ã«æ¸¡ã‚‹ãƒ­ã‚°ã‚¤ãƒ³ã¸ã®å¤±æ•—を検出ã—ã¾ã—ãŸã€‚後ã§ã¾ãŸè©¦ã—ã¦ãã ã•ã„。"
535
536#: application/front/exceptions/OpenShaarliPasswordException.php:16
537msgid "You are not supposed to change a password on an Open Shaarli."
538msgstr ""
539"公開ã•ã‚Œã¦ã„ã‚‹ Shaarli ã«ãŠã„ã¦ã€ãƒ‘スワードを変更ã™ã‚‹ã“ã¨ã¯æƒ³å®šã•ã‚Œã¦ã„ã¾ã›"
540"ん。"
541
542#: application/front/exceptions/ThumbnailsDisabledException.php:11
543msgid "Picture wall unavailable (thumbnails are disabled)."
544msgstr "ピクãƒãƒ£ ウォールã¯åˆ©ç”¨ã§ãã¾ã›ã‚“ (サムãƒã‚¤ãƒ«ãŒç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™)。"
545
546#: application/front/exceptions/WrongTokenException.php:16
547msgid "Wrong token."
548msgstr "ä¸æ­£ãªãƒˆãƒ¼ã‚¯ãƒ³ã§ã™ã€‚"
549
550#: application/legacy/LegacyLinkDB.php:131
551msgid "You are not authorized to add a link."
552msgstr "リンクを追加ã™ã‚‹ã«ã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
553
554#: application/legacy/LegacyLinkDB.php:134
555msgid "Internal Error: A link should always have an id and URL."
556msgstr "エラー: リンクã«ã¯IDã¨URLを登録ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
557
558#: application/legacy/LegacyLinkDB.php:137
559msgid "You must specify an integer as a key."
560msgstr "正常ãªã‚­ãƒ¼ã®å€¤ã§ã¯ã‚ã‚Šã¾ã›ã‚“。"
561
562#: application/legacy/LegacyLinkDB.php:140
563msgid "Array offset and link ID must be equal."
564msgstr "Array オフセットã¨ãƒªãƒ³ã‚¯ã®IDã¯åŒã˜ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
565
566#: application/legacy/LegacyLinkDB.php:249
567msgid ""
568"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
569"me, you must first login.\n"
570"\n"
571"To learn how to use Shaarli, consult the link \"Documentation\" at the "
572"bottom of this page.\n"
573"\n"
574"You use the community supported version of the original Shaarli project, by "
575"Sebastien Sauvage."
576msgstr ""
577"Shaarli ã¸ã‚ˆã†ã“ãï¼ ã“ã‚Œã¯ã‚ãªãŸã®æœ€åˆã®å…¬é–‹ãƒ–ックマークã§ã™ã€‚ã“れを編集ã—ãŸ"
578"り削除ã—ãŸã‚Šã™ã‚‹ã«ã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚\n"
579"\n"
580"Shaarli ã®ä½¿ã„方を知るã«ã¯ã€ã“ã®ãƒšãƒ¼ã‚¸ã®ä¸‹ã«ã‚る「ドキュメントã€ã®ãƒªãƒ³ã‚¯ã‚’é–‹"
581"ã„ã¦ãã ã•ã„。\n"
582"\n"
583"ã‚ãªãŸã¯ Sebastien Sauvage ã«ã‚ˆã‚‹ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ãƒ¼ã‚µãƒãƒ¼ãƒˆã®ã‚ã‚‹ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ã‚ª"
584"リジナルã®Shaarli プロジェクトを使用ã—ã¦ã„ã¾ã™ã€‚"
585
586#: application/legacy/LegacyLinkDB.php:266
587msgid "My secret stuff... - Pastebin.com"
588msgstr "ã‚ãŸã—ã®ã²ðŸ’—ã¿ðŸ’—ã¤ðŸ’— - Pastebin.com"
589
590#: application/legacy/LegacyLinkDB.php:268
591msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
592msgstr ""
593"ã‚·ãƒ¼ãƒƒï¼ ã“ã‚Œã¯ã‚ãªãŸã—ã‹è¦‹ã‚‰ã‚Œãªã„プライベートリンクã§ã™ã€‚消ã™ã“ã¨ã‚‚ã§ãã¾"
594"ã™ã€‚"
595
596#: application/legacy/LegacyUpdater.php:104
597#, fuzzy
598#| msgid "Couldn't retrieve Updater class methods."
599msgid "Couldn't retrieve updater class methods."
600msgstr "アップデーターã®ã‚¯ãƒ©ã‚¹ãƒ¡ã‚¾ãƒƒãƒˆã‚’å—ä¿¡ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
601
602#: application/legacy/LegacyUpdater.php:538
603msgid "<a href=\"./admin/thumbnails\">"
604msgstr "<a href=\"./admin/thumbnails\">"
605
606#: application/netscape/NetscapeBookmarkUtils.php:63
607msgid "Invalid export selection:"
608msgstr "ä¸æ­£ãªã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã®é¸æŠž:"
609
610#: application/netscape/NetscapeBookmarkUtils.php:215
611#, php-format
612msgid "File %s (%d bytes) "
613msgstr "ファイル %s (%d ãƒã‚¤ãƒˆ) "
614
615#: application/netscape/NetscapeBookmarkUtils.php:217
616msgid "has an unknown file format. Nothing was imported."
617msgstr "ã¯ä¸æ˜Žãªãƒ•ã‚¡ã‚¤ãƒ«å½¢å¼ã§ã™ã€‚インãƒãƒ¼ãƒˆã¯ä¸­æ­¢ã•ã‚Œã¾ã—ãŸã€‚"
618
619#: application/netscape/NetscapeBookmarkUtils.php:221
620#, fuzzy, php-format
621#| msgid ""
622#| "was successfully processed in %d seconds: %d links imported, %d links "
623#| "overwritten, %d links skipped."
624msgid ""
625"was successfully processed in %d seconds: %d bookmarks imported, %d "
626"bookmarks overwritten, %d bookmarks skipped."
627msgstr ""
628"㌠%d 秒ã§å‡¦ç†ã•ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒã‚¤ãƒ³ãƒãƒ¼ãƒˆã•ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒä¸Šæ›¸ãã•"
629"ã‚Œã€%d 件ã®ãƒªãƒ³ã‚¯ãŒã‚¹ã‚­ãƒƒãƒ—ã•ã‚Œã¾ã—ãŸã€‚"
630
631#: application/plugin/PluginManager.php:124
632msgid " [plugin incompatibility]: "
633msgstr "[éžå¯¾å¿œã®ãƒ—ラグイン]: "
634
635#: application/plugin/exception/PluginFileNotFoundException.php:21
636#, php-format
637msgid "Plugin \"%s\" files not found."
638msgstr "プラグイン「%sã€ã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒå­˜åœ¨ã—ã¾ã›ã‚“。"
639
640#: application/render/PageCacheManager.php:32
641#, php-format
642msgid "Cannot purge %s: no directory"
643msgstr "%s を削除ã§ãã¾ã›ã‚“: ディレクトリãŒå­˜åœ¨ã—ã¾ã›ã‚“"
644
645#: application/updater/exception/UpdaterException.php:51
646msgid "An error occurred while running the update "
647msgstr "更新中ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—㟠"
648
649#: index.php:65
650msgid "Shared bookmarks on "
651msgstr "次ã«ãŠã„ã¦å…±æœ‰ã•ã‚ŒãŸãƒªãƒ³ã‚¯ "
426 652
427#: plugins/addlink_toolbar/addlink_toolbar.php:29 653#: plugins/addlink_toolbar/addlink_toolbar.php:31
428msgid "URI" 654msgid "URI"
429msgstr "URI" 655msgstr "URI"
430 656
431#: plugins/addlink_toolbar/addlink_toolbar.php:33 657#: plugins/addlink_toolbar/addlink_toolbar.php:35
432#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
433msgid "Add link" 658msgid "Add link"
434msgstr "リンクを追加" 659msgstr "リンクを追加"
435 660
436#: plugins/addlink_toolbar/addlink_toolbar.php:50 661#: plugins/addlink_toolbar/addlink_toolbar.php:52
437msgid "Adds the addlink input on the linklist page." 662msgid "Adds the addlink input on the linklist page."
438msgstr "リンク一覧ã®ãƒšãƒ¼ã‚¸ã«ã€ãƒªãƒ³ã‚¯ã‚’追加ã™ã‚‹ãŸã‚ã®ãƒ•ã‚©ãƒ¼ãƒ ã‚’表示ã™ã‚‹ã€‚" 663msgstr "リンク一覧ã®ãƒšãƒ¼ã‚¸ã«ã€ãƒªãƒ³ã‚¯ã‚’追加ã™ã‚‹ãŸã‚ã®ãƒ•ã‚©ãƒ¼ãƒ ã‚’表示ã™ã‚‹ã€‚"
439 664
440#: plugins/archiveorg/archiveorg.php:23 665#: plugins/archiveorg/archiveorg.php:28
441msgid "View on archive.org" 666msgid "View on archive.org"
442msgstr "archive.org 上ã§è¡¨ç¤ºã™ã‚‹" 667msgstr "archive.org 上ã§è¡¨ç¤ºã™ã‚‹"
443 668
444#: plugins/archiveorg/archiveorg.php:36 669#: plugins/archiveorg/archiveorg.php:41
445msgid "For each link, add an Archive.org icon." 670msgid "For each link, add an Archive.org icon."
446msgstr "ãã‚Œãžã‚Œã®ãƒªãƒ³ã‚¯ã«ã€Archive.org ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’追加ã™ã‚‹ã€‚" 671msgstr "ãã‚Œãžã‚Œã®ãƒªãƒ³ã‚¯ã«ã€Archive.org ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’追加ã™ã‚‹ã€‚"
447 672
448#: plugins/demo_plugin/demo_plugin.php:465 673#: plugins/default_colors/default_colors.php:38
674msgid ""
675"Default colors plugin error: This plugin is active and no custom color is "
676"configured."
677msgstr ""
678"既定ã®è‰²ã®ãƒ—ラグインã«ãŠã‘るエラー: ã“ã®ãƒ—ラグインã¯æœ‰åŠ¹ãªã®ã§ã€ã‚«ã‚¹ã‚¿ãƒ  ã‚«"
679"ラーã¯é©ç”¨ã•ã‚Œã¾ã›ã‚“。"
680
681#: plugins/default_colors/default_colors.php:113
682msgid "Override default theme colors. Use any CSS valid color."
683msgstr ""
684"既定ã®ãƒ†ãƒ¼ãƒžã®è‰²ã‚’上書ãã—ã¾ã™ã€‚ã©ã®ã‚ˆã†ãª CSS カラーコードã§ã‚‚使ãˆã¾ã™ã€‚"
685
686#: plugins/default_colors/default_colors.php:114
687msgid "Main color (navbar green)"
688msgstr "メイン カラー (ナビãƒãƒ¼ã®ç·‘)"
689
690#: plugins/default_colors/default_colors.php:115
691msgid "Background color (light grey)"
692msgstr "背景色 (ç°è‰²)"
693
694#: plugins/default_colors/default_colors.php:116
695msgid "Dark main color (e.g. visited links)"
696msgstr "æš—ã„方㮠メイン カラー (例: 閲覧済ã¿ãƒªãƒ³ã‚¯)"
697
698#: plugins/demo_plugin/demo_plugin.php:477
449msgid "" 699msgid ""
450"A demo plugin covering all use cases for template designers and plugin " 700"A demo plugin covering all use cases for template designers and plugin "
451"developers." 701"developers."
@@ -453,7 +703,15 @@ msgstr ""
453"テンプレートã®ãƒ‡ã‚¶ã‚¤ãƒŠãƒ¼ã‚„ã€ãƒ—ラグインã®é–‹ç™ºè€…ã®ãŸã‚ã®ã™ã¹ã¦ã®çŠ¶æ³ã«å¯¾å¿œã§ã" 703"テンプレートã®ãƒ‡ã‚¶ã‚¤ãƒŠãƒ¼ã‚„ã€ãƒ—ラグインã®é–‹ç™ºè€…ã®ãŸã‚ã®ã™ã¹ã¦ã®çŠ¶æ³ã«å¯¾å¿œã§ã"
454"るデモプラグインã§ã™ã€‚" 704"るデモプラグインã§ã™ã€‚"
455 705
456#: plugins/isso/isso.php:20 706#: plugins/demo_plugin/demo_plugin.php:478
707msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
708msgstr "ã“ã‚Œã¯ãƒ‡ãƒ¢ãƒ—ラグイン専用ã®ãƒ‘ラメーターã§ã™ã€‚末尾ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚"
709
710#: plugins/demo_plugin/demo_plugin.php:479
711msgid "Other demo parameter"
712msgstr "ä»–ã®ãƒ‡ãƒ¢ パラメーター"
713
714#: plugins/isso/isso.php:22
457msgid "" 715msgid ""
458"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " 716"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
459"administration page." 717"administration page."
@@ -461,45 +719,17 @@ msgstr ""
461"Isso プラグインエラー: \"ISSO_SERVER\" ã®å€¤ã‚’プラグイン管ç†ãƒšãƒ¼ã‚¸ã«ã¦æŒ‡å®šã—ã¦" 719"Isso プラグインエラー: \"ISSO_SERVER\" ã®å€¤ã‚’プラグイン管ç†ãƒšãƒ¼ã‚¸ã«ã¦æŒ‡å®šã—ã¦"
462"ãã ã•ã„。" 720"ãã ã•ã„。"
463 721
464#: plugins/isso/isso.php:63 722#: plugins/isso/isso.php:92
465msgid "Let visitor comment your shaares on permalinks with Isso." 723msgid "Let visitor comment your shaares on permalinks with Isso."
466msgstr "" 724msgstr ""
467"Isso を使ã£ã¦ã€ã‚ãªãŸã®ãƒ‘ーマリンク上ã®ãƒªãƒ³ã‚¯ã«ç¬¬ä¸‰è€…ãŒã‚³ãƒ¡ãƒ³ãƒˆã‚’残ã™ã“ã¨ãŒã§" 725"Isso を使ã£ã¦ã€ã‚ãªãŸã®ãƒ‘ーマリンク上ã®ãƒªãƒ³ã‚¯ã«ç¬¬ä¸‰è€…ãŒã‚³ãƒ¡ãƒ³ãƒˆã‚’残ã™ã“ã¨ãŒã§"
468"ãã¾ã™ã€‚" 726"ãã¾ã™ã€‚"
469 727
470#: plugins/isso/isso.php:64 728#: plugins/isso/isso.php:93
471msgid "Isso server URL (without 'http://')" 729msgid "Isso server URL (without 'http://')"
472msgstr "Isso server URL ('http://' 抜ã)" 730msgstr "Isso server URL ('http://' 抜ã)"
473 731
474#: plugins/markdown/markdown.php:158 732#: plugins/piwik/piwik.php:23
475msgid "Description will be rendered with"
476msgstr "説明ã¯æ¬¡ã®æ–¹æ³•ã§æç”»ã•ã‚Œã¾ã™:"
477
478#: plugins/markdown/markdown.php:159
479msgid "Markdown syntax documentation"
480msgstr "マークダウン形å¼ã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆ"
481
482#: plugins/markdown/markdown.php:160
483msgid "Markdown syntax"
484msgstr "マークダウン形å¼"
485
486#: plugins/markdown/markdown.php:339
487msgid ""
488"Render shaare description with Markdown syntax.<br><strong>Warning</"
489"strong>:\n"
490"If your shaared descriptions contained HTML tags before enabling the "
491"markdown plugin,\n"
492"enabling it might break your page.\n"
493"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
494"markdown#html-rendering\">README</a>."
495msgstr ""
496"リンクã®èª¬æ˜Žã‚’マークダウン形å¼ã§è¡¨ç¤ºã—ã¾ã™ã€‚<br><strong>警告</strong>:\n"
497"リンクã®èª¬æ˜Žã«HTMLã‚¿ã‚°ãŒã“ã®ãƒ—ラグインを有効ã«ã™ã‚‹å‰ã«å«ã¾ã‚Œã¦ã„ãŸå ´åˆã€\n"
498"正常ã«ãƒšãƒ¼ã‚¸ã‚’表示ã§ããªããªã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。\n"
499"詳ã—ã㯠<a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
500"markdown#html-rendering\">README</a> ã‚’ã”覧ãã ã•ã„。"
501
502#: plugins/piwik/piwik.php:21
503msgid "" 733msgid ""
504"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " 734"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
505"administration page." 735"administration page."
@@ -507,27 +737,27 @@ msgstr ""
507"Piwik プラグインエラー: PIWIK_URL 㨠PIWIK_SITEID ã®å€¤ã‚’プラグイン管ç†ãƒšãƒ¼ã‚¸" 737"Piwik プラグインエラー: PIWIK_URL 㨠PIWIK_SITEID ã®å€¤ã‚’プラグイン管ç†ãƒšãƒ¼ã‚¸"
508"ã§æŒ‡å®šã—ã¦ãã ã•ã„。" 738"ã§æŒ‡å®šã—ã¦ãã ã•ã„。"
509 739
510#: plugins/piwik/piwik.php:70 740#: plugins/piwik/piwik.php:72
511msgid "A plugin that adds Piwik tracking code to Shaarli pages." 741msgid "A plugin that adds Piwik tracking code to Shaarli pages."
512msgstr "Piwik ã®ãƒˆãƒ©ãƒƒã‚­ãƒ³ã‚°ã‚³ãƒ¼ãƒ‰ã‚’Shaarliã«è¿½åŠ ã™ã‚‹ãƒ—ラグインã§ã™ã€‚" 742msgstr "Piwik ã®ãƒˆãƒ©ãƒƒã‚­ãƒ³ã‚°ã‚³ãƒ¼ãƒ‰ã‚’Shaarliã«è¿½åŠ ã™ã‚‹ãƒ—ラグインã§ã™ã€‚"
513 743
514#: plugins/piwik/piwik.php:71 744#: plugins/piwik/piwik.php:73
515msgid "Piwik URL" 745msgid "Piwik URL"
516msgstr "Piwik URL" 746msgstr "Piwik URL"
517 747
518#: plugins/piwik/piwik.php:72 748#: plugins/piwik/piwik.php:74
519msgid "Piwik site ID" 749msgid "Piwik site ID"
520msgstr "Piwik サイトID" 750msgstr "Piwik サイトID"
521 751
522#: plugins/playvideos/playvideos.php:22 752#: plugins/playvideos/playvideos.php:25
523msgid "Video player" 753msgid "Video player"
524msgstr "動画プレイヤー" 754msgstr "動画プレイヤー"
525 755
526#: plugins/playvideos/playvideos.php:25 756#: plugins/playvideos/playvideos.php:28
527msgid "Play Videos" 757msgid "Play Videos"
528msgstr "動画をå†ç”Ÿ" 758msgstr "動画をå†ç”Ÿ"
529 759
530#: plugins/playvideos/playvideos.php:56 760#: plugins/playvideos/playvideos.php:59
531msgid "Add a button in the toolbar allowing to watch all videos." 761msgid "Add a button in the toolbar allowing to watch all videos."
532msgstr "ã™ã¹ã¦ã®å‹•ç”»ã‚’閲覧ã™ã‚‹ãƒœã‚¿ãƒ³ã‚’ツールãƒãƒ¼ã«è¿½åŠ ã—ã¾ã™ã€‚" 762msgstr "ã™ã¹ã¦ã®å‹•ç”»ã‚’閲覧ã™ã‚‹ãƒœã‚¿ãƒ³ã‚’ツールãƒãƒ¼ã«è¿½åŠ ã—ã¾ã™ã€‚"
533 763
@@ -535,26 +765,26 @@ msgstr "ã™ã¹ã¦ã®å‹•ç”»ã‚’閲覧ã™ã‚‹ãƒœã‚¿ãƒ³ã‚’ツールãƒãƒ¼ã«è¿½åŠ ã—
535msgid "plugins/playvideos/jquery-1.11.2.min.js" 765msgid "plugins/playvideos/jquery-1.11.2.min.js"
536msgstr "plugins/playvideos/jquery-1.11.2.min.js" 766msgstr "plugins/playvideos/jquery-1.11.2.min.js"
537 767
538#: plugins/pubsubhubbub/pubsubhubbub.php:69 768#: plugins/pubsubhubbub/pubsubhubbub.php:72
539#, php-format 769#, php-format
540msgid "Could not publish to PubSubHubbub: %s" 770msgid "Could not publish to PubSubHubbub: %s"
541msgstr "PubSubHubbub ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ: %s" 771msgstr "PubSubHubbub ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ: %s"
542 772
543#: plugins/pubsubhubbub/pubsubhubbub.php:95 773#: plugins/pubsubhubbub/pubsubhubbub.php:99
544#, php-format 774#, php-format
545msgid "Could not post to %s" 775msgid "Could not post to %s"
546msgstr "%s ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ" 776msgstr "%s ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ"
547 777
548#: plugins/pubsubhubbub/pubsubhubbub.php:99 778#: plugins/pubsubhubbub/pubsubhubbub.php:103
549#, php-format 779#, php-format
550msgid "Bad response from the hub %s" 780msgid "Bad response from the hub %s"
551msgstr "ãƒãƒ– %s ã‹ã‚‰ã®ä¸æ­£ãªãƒ¬ã‚¹ãƒãƒ³ã‚¹" 781msgstr "ãƒãƒ– %s ã‹ã‚‰ã®ä¸æ­£ãªãƒ¬ã‚¹ãƒãƒ³ã‚¹"
552 782
553#: plugins/pubsubhubbub/pubsubhubbub.php:110 783#: plugins/pubsubhubbub/pubsubhubbub.php:114
554msgid "Enable PubSubHubbub feed publishing." 784msgid "Enable PubSubHubbub feed publishing."
555msgstr "PubSubHubbub ã¸ã®ãƒ•ã‚£ãƒ¼ãƒ‰ã‚’公開ã™ã‚‹ã€‚" 785msgstr "PubSubHubbub ã¸ã®ãƒ•ã‚£ãƒ¼ãƒ‰ã‚’公開ã™ã‚‹ã€‚"
556 786
557#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68 787#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70
558msgid "For each link, add a QRCode icon." 788msgid "For each link, add a QRCode icon."
559msgstr "ãã‚Œãžã‚Œã®ãƒªãƒ³ã‚¯ã«ã¤ã„ã¦ã€QRコードã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’追加ã™ã‚‹ã€‚" 789msgstr "ãã‚Œãžã‚Œã®ãƒªãƒ³ã‚¯ã«ã¤ã„ã¦ã€QRコードã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’追加ã™ã‚‹ã€‚"
560 790
@@ -570,724 +800,534 @@ msgstr ""
570msgid "Save to wallabag" 800msgid "Save to wallabag"
571msgstr "Wallabag ã«ä¿å­˜" 801msgstr "Wallabag ã«ä¿å­˜"
572 802
573#: plugins/wallabag/wallabag.php:69 803#: plugins/wallabag/wallabag.php:71
574msgid "Wallabag API URL" 804msgid "Wallabag API URL"
575msgstr "Wallabag ã®APIã®URL" 805msgstr "Wallabag ã®APIã®URL"
576 806
577#: plugins/wallabag/wallabag.php:70 807#: plugins/wallabag/wallabag.php:72
578msgid "Wallabag API version (1 or 2)" 808msgid "Wallabag API version (1 or 2)"
579msgstr "Wallabag ã®APIã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ (1 ã¾ãŸã¯ 2)" 809msgstr "Wallabag ã®APIã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ (1 ã¾ãŸã¯ 2)"
580 810
581#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227 811#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
582#: tests/languages/fr/LanguagesFrTest.php:160 812#: tests/languages/fr/LanguagesFrTest.php:159
583#: tests/languages/fr/LanguagesFrTest.php:173 813#: tests/languages/fr/LanguagesFrTest.php:172
584#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
585#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81
586msgid "Search" 814msgid "Search"
587msgid_plural "Search" 815msgid_plural "Search"
588msgstr[0] "検索" 816msgstr[0] "検索"
589msgstr[1] "検索" 817msgstr[1] "検索"
590 818
591#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 819#~ msgid "The page you are trying to reach does not exist or has been deleted."
592msgid "Sorry, nothing to see here." 820#~ msgstr "ã‚ãªãŸãŒé–‹ã“ã†ã¨ã—ãŸãƒšãƒ¼ã‚¸ã¯å­˜åœ¨ã—ãªã„ã‹ã€å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚"
593msgstr "ã™ã¿ã¾ã›ã‚“ãŒã€ã“ã“ã«ã¯ä½•ã‚‚ã‚ã‚Šã¾ã›ã‚“。"
594
595#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
596msgid "URL or leave empty to post a note"
597msgstr "URL を入力ã™ã‚‹ã‹ã€ç©ºæ¬„ã«ã™ã‚‹ã¨ãƒŽãƒ¼ãƒˆã‚’投稿ã—ã¾ã™"
598
599#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
600msgid "Current password"
601msgstr "ç¾åœ¨ã®ãƒ‘スワード"
602
603#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
604msgid "New password"
605msgstr "æ–°ã—ã„パスワード"
606
607#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
608msgid "Change"
609msgstr "変更"
610
611#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
612#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
613msgid "Tag"
614msgstr "ã‚¿ã‚°"
615
616#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
617msgid "New name"
618msgstr "変更先ã®åå‰"
619
620#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
621msgid "Case sensitive"
622msgstr "大文字ã¨å°æ–‡å­—を区別"
623
624#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
625msgid "Rename"
626msgstr "åå‰ã‚’変更"
627
628#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
629#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
630#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172
631msgid "Delete"
632msgstr "削除"
633
634#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
635msgid "You can also edit tags in the"
636msgstr "次ã«å«ã¾ã‚Œã‚‹ã‚¿ã‚°ã‚’編集ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™:"
637
638#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
639msgid "tag list"
640msgstr "タグ一覧"
641
642#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
643msgid "title"
644msgstr "タイトル"
645
646#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
647msgid "Home link"
648msgstr "ホームã®ãƒªãƒ³ã‚¯å…ˆ"
649
650#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
651msgid "Default value"
652msgstr "既定ã®å€¤"
653
654#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
655msgid "Theme"
656msgstr "テーマ"
657
658#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
659#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
660msgid "Language"
661msgstr "言語"
662
663#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116
664#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
665msgid "Timezone"
666msgstr "タイムゾーン"
667
668#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
669#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
670msgid "Continent"
671msgstr "大陸"
672
673#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
674#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
675msgid "City"
676msgstr "町"
677
678#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
679msgid "Disable session cookie hijacking protection"
680msgstr "ä¸æ­£ãƒ­ã‚°ã‚¤ãƒ³é˜²æ­¢ã®ãŸã‚ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¯ãƒƒã‚­ãƒ¼ã‚’無効化"
681
682#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166
683msgid "Check this if you get disconnected or if your IP address changes often"
684msgstr ""
685"ã‚ãªãŸãŒåˆ‡æ–­ã•ã‚ŒãŸã‚Šã€IPアドレスãŒé »ç¹ã«å¤‰ã‚る環境下ã§ã‚ã‚‹ãªã‚‰ãƒã‚§ãƒƒã‚¯ã‚’入れ"
686"ã¦ãã ã•ã„"
687
688#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
689msgid "Private links by default"
690msgstr "既定ã§ãƒ—ライベートリンク"
691
692#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184
693msgid "All new links are private by default"
694msgstr "ã™ã¹ã¦ã®æ–°è¦ãƒªãƒ³ã‚¯ã‚’プライベートã§ä½œæˆ"
695
696#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
697msgid "RSS direct links"
698msgstr "RSS 直リンク"
699
700#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200
701msgid "Check this to use direct URL instead of permalink in feeds"
702msgstr "フィードã§ãƒ‘ーマリンクã®ä»£ã‚ã‚Šã«ç›´ãƒªãƒ³ã‚¯ã‚’使ã†"
703
704#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
705msgid "Hide public links"
706msgstr "公開リンクを隠ã™"
707
708#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216
709msgid "Do not show any links if the user is not logged in"
710msgstr "ログインã—ã¦ã„ãªã„ユーザーã«ã¯ä½•ã®ãƒªãƒ³ã‚¯ã‚‚表示ã—ãªã„"
711
712#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231
713#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
714msgid "Check updates"
715msgstr "更新を確èª"
716
717#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232
718#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
719msgid "Notify me when a new release is ready"
720msgstr "æ–°ã—ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒãƒªãƒªãƒ¼ã‚¹ã•ã‚ŒãŸã¨ãã«é€šçŸ¥"
721
722#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
723#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
724msgid "Enable REST API"
725msgstr "REST API を有効化"
726
727#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248
728#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
729msgid "Allow third party software to use Shaarli such as mobile application"
730msgstr ""
731"モãƒã‚¤ãƒ«ã‚¢ãƒ—リã¨ã„ã£ãŸã‚µãƒ¼ãƒ‰ãƒ‘ーティーã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã«Shaarliを使用ã™ã‚‹ã“ã¨ã‚’"
732"許å¯"
733
734#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
735msgid "API secret"
736msgstr "API シークレット"
737
738#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
739#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
740#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
741#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
742msgid "Save"
743msgstr "ä¿å­˜"
744
745#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
746msgid "The Daily Shaarli"
747msgstr "デイリーSharli"
748
749#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
750msgid "1 RSS entry per day"
751msgstr "å„æ—¥1ã¤ãšã¤ã®RSSé …ç›®"
752
753#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
754msgid "Previous day"
755msgstr "å‰æ—¥"
756
757#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
758msgid "All links of one day in a single page."
759msgstr "1æ—¥ã«ä½œæˆã•ã‚ŒãŸã™ã¹ã¦ã®ãƒªãƒ³ã‚¯ã§ã™ã€‚"
760
761#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
762msgid "Next day"
763msgstr "翌日"
764
765#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
766msgid "Created:"
767msgstr "作æˆ:"
768
769#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
770msgid "URL"
771msgstr "URL"
772
773#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
774msgid "Title"
775msgstr "タイトル"
776
777#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
778#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
779#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
780#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
781#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
782msgid "Description"
783msgstr "説明"
784
785#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
786msgid "Tags"
787msgstr "ã‚¿ã‚°"
788
789#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
790#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
791#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
792msgid "Private"
793msgstr "プライベート"
794
795#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
796msgid "Apply Changes"
797msgstr "変更をé©ç”¨"
798
799#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
800msgid "Export Database"
801msgstr "データベースをエクスãƒãƒ¼ãƒˆ"
802
803#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
804msgid "Selection"
805msgstr "é¸æŠžæ¸ˆã¿"
806
807#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
808msgid "All"
809msgstr "ã™ã¹ã¦"
810
811#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
812msgid "Public"
813msgstr "公開"
814
815#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
816msgid "Prepend note permalinks with this Shaarli instance's URL"
817msgstr "ã“ã® Shaarli ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®URL ã«ãƒŽãƒ¼ãƒˆã¸ã®ãƒ‘ーマリンクを付ã‘加ãˆã‚‹"
818
819#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
820msgid "Useful to import bookmarks in a web browser"
821msgstr "ウェブブラウザーã®ãƒªãƒ³ã‚¯ã‚’インãƒãƒ¼ãƒˆã™ã‚‹ã®ã«æœ‰åŠ¹ã§ã™"
822
823#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
824msgid "Import Database"
825msgstr "データベースをインãƒãƒ¼ãƒˆ"
826
827#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
828msgid "Maximum size allowed:"
829msgstr "最大サイズ:"
830
831#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
832msgid "Visibility"
833msgstr "å¯è¦–性"
834
835#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
836msgid "Use values from the imported file, default to public"
837msgstr "インãƒãƒ¼ãƒˆå…ƒã®ãƒ•ã‚¡ã‚¤ãƒ«ã®å€¤ã‚’使用 (既定ã¯å…¬é–‹ãƒªãƒ³ã‚¯ã¨ãªã‚Šã¾ã™)"
838
839#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
840msgid "Import all bookmarks as private"
841msgstr "ã™ã¹ã¦ã®ãƒ–ックマーク項目をプライベートリンクã¨ã—ã¦ã‚¤ãƒ³ãƒãƒ¼ãƒˆ"
842
843#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
844msgid "Import all bookmarks as public"
845msgstr "ã™ã¹ã¦ã®ãƒ–ックマーク項目を公開リンクã¨ã—ã¦ã‚¤ãƒ³ãƒãƒ¼ãƒˆ"
846
847#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
848msgid "Overwrite existing bookmarks"
849msgstr "æ—¢ã«å­˜åœ¨ã—ã¦ã„るブックマークを上書ã"
850
851#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
852msgid "Duplicates based on URL"
853msgstr "URL ã«ã‚ˆã‚‹é‡è¤‡"
854
855#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
856msgid "Add default tags"
857msgstr "既定ã®ã‚¿ã‚°ã‚’追加"
858
859#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
860msgid "Install Shaarli"
861msgstr "Shaarli をインストール"
862
863#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
864msgid "It looks like it's the first time you run Shaarli. Please configure it."
865msgstr "ã©ã†ã‚„ら Shaarli ã‚’åˆã‚ã¦èµ·å‹•ã—ã¦ã„るよã†ã§ã™ã€‚設定ã—ã¦ãã ã•ã„。"
866
867#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
868#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
869#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
870#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
871msgid "Username"
872msgstr "ユーザーå"
873
874#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
875#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
876#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
877#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148
878msgid "Password"
879msgstr "パスワード"
880
881#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
882msgid "Shaarli title"
883msgstr "Shaarli ã®ã‚¿ã‚¤ãƒˆãƒ«"
884
885#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
886msgid "My links"
887msgstr "自分ã®ãƒªãƒ³ã‚¯"
888
889#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
890msgid "Install"
891msgstr "インストール"
892
893#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
894#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
895msgid "shaare"
896msgid_plural "shaares"
897msgstr[0] "共有"
898msgstr[1] "共有"
899
900#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
901#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
902msgid "private link"
903msgid_plural "private links"
904msgstr[0] "プライベートリンク"
905msgstr[1] "プライベートリンク"
906
907#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
908#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
909#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117
910msgid "Search text"
911msgstr "文字列ã§æ¤œç´¢"
912
913#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
914#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
915#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124
916#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
917#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
918#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
919#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
920msgid "Filter by tag"
921msgstr "ã‚¿ã‚°ã«ã‚ˆã£ã¦åˆ†é¡ž"
922
923#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
924msgid "Nothing found."
925msgstr "何も見ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚"
926
927#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119
928#, php-format
929msgid "%s result"
930msgid_plural "%s results"
931msgstr[0] "%s 件ã®çµæžœ"
932msgstr[1] "%s 件ã®çµæžœ"
933
934#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
935msgid "for"
936msgstr "for"
937
938#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
939msgid "tagged"
940msgstr "タグ付ã‘ã•ã‚ŒãŸ"
941
942#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
943msgid "Remove tag"
944msgstr "タグを削除"
945
946#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
947msgid "with status"
948msgstr "with status"
949
950#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
951msgid "without any tag"
952msgstr "ã‚¿ã‚°ãªã—"
953
954#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174
955#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
956#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
957msgid "Fold"
958msgstr "畳む"
959
960#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
961msgid "Edited: "
962msgstr "編集済ã¿: "
963
964#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
965msgid "permalink"
966msgstr "パーマリンク"
967 821
968#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 822#~ msgid "404 Not Found"
969msgid "Add tag" 823#~ msgstr "404 ページãŒå­˜åœ¨ã—ã¾ã›ã‚“"
970msgstr "タグを追加"
971
972#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
973#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7
974msgid "Filters"
975msgstr "分類"
976
977#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
978#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12
979msgid "Only display private links"
980msgstr "プライベートリンクã®ã¿ã‚’表示"
981
982#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
983#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:15
984msgid "Only display public links"
985msgstr "公開リンクã®ã¿ã‚’表示"
986
987#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
988#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:20
989msgid "Filter untagged links"
990msgstr "タグ付ã‘ã•ã‚Œã¦ã„ãªã„リンクã§åˆ†é¡ž"
991
992#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
993#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
994#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24
995#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76
996#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
997#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
998msgid "Fold all"
999msgstr "ã™ã¹ã¦ç•³ã‚€"
1000
1001#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
1002#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:69
1003msgid "Links per page"
1004msgstr "å„ページをリンク"
1005
1006#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1007msgid ""
1008"You have been banned after too many failed login attempts. Try again later."
1009msgstr "複数回ã«æ¸¡ã‚‹ãƒ­ã‚°ã‚¤ãƒ³ã¸ã®å¤±æ•—を検出ã—ã¾ã—ãŸã€‚後ã§ã¾ãŸè©¦ã—ã¦ãã ã•ã„。"
1010 824
1011#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 825#~ msgid "Updates file path is not set, can't write updates."
1012#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 826#~ msgstr ""
1013#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151 827#~ "æ›´æ–°ã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã®ãƒ‘スãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ãŸã‚ã€æ›´æ–°ã‚’書ãè¾¼ã‚ã¾ã›ã‚“。"
1014msgid "Remember me"
1015msgstr "パスワードをä¿å­˜"
1016
1017#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1018#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1019#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
1020#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
1021msgid "by the Shaarli community"
1022msgstr "by Shaarli コミュニティ"
1023
1024#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1025#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
1026msgid "Documentation"
1027msgstr "ドキュメント"
1028
1029#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1030#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
1031msgid "Expand"
1032msgstr "展開ã™ã‚‹"
1033
1034#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
1035#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
1036msgid "Expand all"
1037msgstr "ã™ã¹ã¦å±•é–‹ã™ã‚‹"
1038
1039#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1040#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
1041msgid "Are you sure you want to delete this link?"
1042msgstr "本当ã«ã“ã®ãƒªãƒ³ã‚¯ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ"
1043
1044#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
1045#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1046#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61
1047#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86
1048msgid "RSS Feed"
1049msgstr "RSS フィード"
1050
1051#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1052#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1053#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66
1054#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102
1055msgid "Logout"
1056msgstr "ログアウト"
1057
1058#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1059#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
1060msgid "is available"
1061msgstr "ãŒåˆ©ç”¨å¯èƒ½"
1062
1063#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
1064#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176
1065msgid "Error"
1066msgstr "エラー"
1067
1068#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1069msgid "Picture Wall"
1070msgstr "ピクãƒãƒ£ãƒ¼ã‚¦ã‚©ãƒ¼ãƒ«"
1071
1072#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1073msgid "pics"
1074msgstr "ç”»åƒ"
1075
1076#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1077msgid "You need to enable Javascript to change plugin loading order."
1078msgstr ""
1079"プラグインを読ã¿è¾¼ã‚€é †ç•ªã‚’変更ã™ã‚‹ã«ã¯ã€Javascriptを有効ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾"
1080"ã™ã€‚"
1081 828
1082#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 829#~ msgid "Unable to write updates in "
1083msgid "Enabled Plugins" 830#~ msgstr "更新を次ã®é …ç›®ã«æ›¸ãè¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸ: "
1084msgstr "有効ãªãƒ—ラグイン"
1085
1086#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
1087#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1088msgid "No plugin enabled."
1089msgstr "有効ãªãƒ—ラグインã¯ã‚ã‚Šã¾ã›ã‚“。"
1090
1091#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
1092#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
1093msgid "Disable"
1094msgstr "無効化"
1095
1096#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1097#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1098#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98
1099#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1100msgid "Name"
1101msgstr "åå‰"
1102
1103#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1104#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1105msgid "Order"
1106msgstr "é †åº"
1107
1108#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1109msgid "Disabled Plugins"
1110msgstr "無効ãªãƒ—ラグイン"
1111
1112#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
1113msgid "No plugin disabled."
1114msgstr "無効ãªãƒ—ラグインã¯ã‚ã‚Šã¾ã›ã‚“。"
1115
1116#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
1117#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
1118msgid "Enable"
1119msgstr "有効化"
1120
1121#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1122msgid "More plugins available"
1123msgstr "ã•ã‚‰ã«åˆ©ç”¨ã§ãるプラグインãŒã‚ã‚Šã¾ã™"
1124
1125#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
1126msgid "in the documentation"
1127msgstr "ドキュメント内"
1128
1129#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
1130msgid "Plugin configuration"
1131msgstr "プラグイン設定"
1132
1133#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195
1134msgid "No parameter available."
1135msgstr "利用å¯èƒ½ãªè¨­å®šé …ç›®ã¯ã‚ã‚Šã¾ã›ã‚“。"
1136
1137#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1138#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1139msgid "tags"
1140msgstr "ã‚¿ã‚°"
1141
1142#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
1143#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
1144msgid "List all links with those tags"
1145msgstr "ã“ã®ã‚¿ã‚°ãŒä»˜ã„ã¦ã„るリンクをリスト化ã™ã‚‹"
1146
1147#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1148#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1149msgid "Sort by:"
1150msgstr "分類:"
1151
1152#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1153#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
1154msgid "Cloud"
1155msgstr "クラウド"
1156
1157#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6
1158#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
1159msgid "Most used"
1160msgstr "ã‚‚ã£ã¨ã‚‚使ã‚ã‚ŒãŸ"
1161
1162#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
1163#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
1164msgid "Alphabetical"
1165msgstr "アルファベット順"
1166
1167#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1168msgid "Settings"
1169msgstr "設定"
1170 831
1171#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 832#~ msgid "I said: NO. You are banned for the moment. Go away."
1172msgid "Change Shaarli settings: title, timezone, etc." 833#~ msgstr "ã‚ãªãŸã¯ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰BANã•ã‚Œã¦ã„ã¾ã™ã€‚"
1173msgstr "Shaarli ã®è¨­å®šã‚’変更: タイトルã€ã‚¿ã‚¤ãƒ ã‚¾ãƒ¼ãƒ³ãªã©ã€‚"
1174 834
1175#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 835#~ msgid "Tag cloud"
1176msgid "Configure your Shaarli" 836#~ msgstr "タグクラウド"
1177msgstr "ã‚ãªãŸã® Shaarli を設定"
1178 837
1179#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 838#~ msgid "Click to try again."
1180msgid "Enable, disable and configure plugins" 839#~ msgstr "クリックã—ã¦å†åº¦è©¦ã—ã¾ã™ã€‚"
1181msgstr "プラグインを有効化ã€ç„¡åŠ¹åŒ–ã€è¨­å®šã™ã‚‹"
1182 840
1183#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 841#~ msgid "Description will be rendered with"
1184msgid "Change your password" 842#~ msgstr "説明ã¯æ¬¡ã®æ–¹æ³•ã§æç”»ã•ã‚Œã¾ã™:"
1185msgstr "パスワードを変更"
1186 843
1187#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 844#~ msgid "Markdown syntax documentation"
1188msgid "Rename or delete a tag in all links" 845#~ msgstr "マークダウン形å¼ã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆ"
1189msgstr "ã™ã¹ã¦ã®ãƒªãƒ³ã‚¯ã®ã‚¿ã‚°ã®åå‰ã‚’変更ã™ã‚‹ã€ã¾ãŸã¯å‰Šé™¤ã™ã‚‹"
1190 846
1191#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 847#~ msgid "Markdown syntax"
1192msgid "" 848#~ msgstr "マークダウン形å¼"
1193"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1194"delicious...)"
1195msgstr ""
1196"Netscape HTML å½¢å¼ã®ãƒ–ックマークをインãƒãƒ¼ãƒˆã™ã‚‹ (Firefoxã€Chromeã€Operaã¨"
1197"ã„ã£ãŸãƒ–ラウザーãŒå«ã¾ã‚Œã¾ã™)"
1198 849
1199#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 850#~ msgid ""
1200msgid "Import links" 851#~ "Render shaare description with Markdown syntax.<br><strong>Warning</"
1201msgstr "リンクをインãƒãƒ¼ãƒˆ" 852#~ "strong>:\n"
853#~ "If your shaared descriptions contained HTML tags before enabling the "
854#~ "markdown plugin,\n"
855#~ "enabling it might break your page.\n"
856#~ "See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
857#~ "markdown#html-rendering\">README</a>."
858#~ msgstr ""
859#~ "リンクã®èª¬æ˜Žã‚’マークダウン形å¼ã§è¡¨ç¤ºã—ã¾ã™ã€‚<br><strong>警告</strong>:\n"
860#~ "リンクã®èª¬æ˜Žã«HTMLã‚¿ã‚°ãŒã“ã®ãƒ—ラグインを有効ã«ã™ã‚‹å‰ã«å«ã¾ã‚Œã¦ã„ãŸå ´åˆã€\n"
861#~ "正常ã«ãƒšãƒ¼ã‚¸ã‚’表示ã§ããªããªã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。\n"
862#~ "詳ã—ã㯠<a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
863#~ "markdown#html-rendering\">README</a> ã‚’ã”覧ãã ã•ã„。"
1202 864
1203#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 865#~ msgid "Sorry, nothing to see here."
1204msgid "" 866#~ msgstr "ã™ã¿ã¾ã›ã‚“ãŒã€ã“ã“ã«ã¯ä½•ã‚‚ã‚ã‚Šã¾ã›ã‚“。"
1205"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1206"Opera, delicious...)"
1207msgstr ""
1208"Netscape HTML å½¢å¼ã®ãƒ–ックマークをエクスãƒãƒ¼ãƒˆã™ã‚‹ (Firefoxã€Chromeã€Operaã¨"
1209"ã„ã£ãŸãƒ–ラウザーãŒå«ã¾ã‚Œã¾ã™)"
1210 867
1211#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 868#~ msgid "URL or leave empty to post a note"
1212msgid "Export database" 869#~ msgstr "URL を入力ã™ã‚‹ã‹ã€ç©ºæ¬„ã«ã™ã‚‹ã¨ãƒŽãƒ¼ãƒˆã‚’投稿ã—ã¾ã™"
1213msgstr "リンクをエクスãƒãƒ¼ãƒˆ"
1214 870
1215#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 871#~ msgid "Current password"
1216msgid "" 872#~ msgstr "ç¾åœ¨ã®ãƒ‘スワード"
1217"Drag one of these button to your bookmarks toolbar or right-click it and "
1218"\"Bookmark This Link\""
1219msgstr ""
1220"ã“れらã®ãƒœã‚¿ãƒ³ã®ã†ã¡1ã¤ã‚’をブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦"
1221"「ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„"
1222 873
1223#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 874#~ msgid "New password"
1224msgid "then click on the bookmarklet in any page you want to share." 875#~ msgstr "æ–°ã—ã„パスワード"
1225msgstr "共有ã—ãŸã„ページã§ãƒ–ックマークレットをクリックã—ã¦ãã ã•ã„。"
1226 876
1227#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 877#~ msgid "Change"
1228#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100 878#~ msgstr "変更"
1229msgid ""
1230"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1231"Link"
1232msgstr ""
1233"ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’"
1234"ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„"
1235 879
1236#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 880#~ msgid "Tag"
1237msgid "then click ✚Shaare link button in any page you want to share" 881#~ msgstr "タグ"
1238msgstr "✚リンクを共有 ボタンをクリックã™ã‚‹ã“ã¨ã§ã€ã©ã“ã§ã‚‚リンクを共有ã§ãã¾ã™"
1239 882
1240#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 883#~ msgid "New name"
1241#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 884#~ msgstr "変更先ã®åå‰"
1242msgid "The selected text is too long, it will be truncated."
1243msgstr "é¸æŠžã•ã‚ŒãŸæ–‡å­—列ã¯é•·ã™ãŽã‚‹ã®ã§ã€ä¸€éƒ¨ãŒåˆ‡ã‚Šæ¨ã¦ã‚‰ã‚Œã¾ã™ã€‚"
1244 885
1245#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 886#~ msgid "Case sensitive"
1246msgid "Shaare link" 887#~ msgstr "大文字ã¨å°æ–‡å­—を区別"
1247msgstr "共有リンク"
1248 888
1249#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 889#~ msgid "Rename"
1250msgid "" 890#~ msgstr "åå‰ã‚’変更"
1251"Then click ✚Add Note button anytime to start composing a private Note (text "
1252"post) to your Shaarli"
1253msgstr ""
1254"✚ノートを追加 ボタンをクリックã™ã‚‹ã“ã¨ã§ã€ã„ã¤ã§ã‚‚プライベートノート(テキスト"
1255"å½¢å¼)ã‚’Shaarli上ã«ä½œæˆã§ãã¾ã™"
1256 891
1257#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 892#~ msgid "Delete"
1258msgid "Add Note" 893#~ msgstr "削除"
1259msgstr "ノートを追加"
1260 894
1261#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 895#~ msgid "You can also edit tags in the"
1262msgid "" 896#~ msgstr "次ã«å«ã¾ã‚Œã‚‹ã‚¿ã‚°ã‚’編集ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™:"
1263"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1264"functionality."
1265msgstr ""
1266"ã“ã®æ©Ÿèƒ½ã‚’使用ã™ã‚‹ã«ã¯ã€<strong>HTTPS</strong> 経由ã§Shaarliã«æŽ¥ç¶šã—ã¦ãã ã•"
1267"ã„。"
1268 897
1269#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 898#~ msgid "tag list"
1270msgid "Add to" 899#~ msgstr "タグ一覧"
1271msgstr "次ã«è¿½åŠ :"
1272 900
1273#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145 901#~ msgid "title"
1274msgid "3rd party" 902#~ msgstr "タイトル"
1275msgstr "サードパーティー"
1276 903
1277#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 904#~ msgid "Home link"
1278#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 905#~ msgstr "ホームã®ãƒªãƒ³ã‚¯å…ˆ"
1279msgid "Plugin"
1280msgstr "プラグイン"
1281 906
1282#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 907#~ msgid "Default value"
1283#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 908#~ msgstr "既定ã®å€¤"
1284msgid "plugin"
1285msgstr "プラグイン"
1286 909
1287#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 910#~ msgid "Theme"
1288msgid "" 911#~ msgstr "テーマ"
1289"Drag this link to your bookmarks toolbar, or right-click it and choose " 912
1290"Bookmark This Link" 913#~ msgid "Language"
1291msgstr "" 914#~ msgstr "言語"
1292"ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’" 915
1293"ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„" 916#~ msgid "Timezone"
917#~ msgstr "タイムゾーン"
918
919#~ msgid "Continent"
920#~ msgstr "大陸"
921
922#~ msgid "City"
923#~ msgstr "町"
924
925#~ msgid "Disable session cookie hijacking protection"
926#~ msgstr "ä¸æ­£ãƒ­ã‚°ã‚¤ãƒ³é˜²æ­¢ã®ãŸã‚ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¯ãƒƒã‚­ãƒ¼ã‚’無効化"
927
928#~ msgid ""
929#~ "Check this if you get disconnected or if your IP address changes often"
930#~ msgstr ""
931#~ "ã‚ãªãŸãŒåˆ‡æ–­ã•ã‚ŒãŸã‚Šã€IPアドレスãŒé »ç¹ã«å¤‰ã‚る環境下ã§ã‚ã‚‹ãªã‚‰ãƒã‚§ãƒƒã‚¯ã‚’å…¥"
932#~ "ã‚Œã¦ãã ã•ã„"
933
934#~ msgid "Private links by default"
935#~ msgstr "既定ã§ãƒ—ライベートリンク"
936
937#~ msgid "All new links are private by default"
938#~ msgstr "ã™ã¹ã¦ã®æ–°è¦ãƒªãƒ³ã‚¯ã‚’プライベートã§ä½œæˆ"
939
940#~ msgid "RSS direct links"
941#~ msgstr "RSS 直リンク"
942
943#~ msgid "Check this to use direct URL instead of permalink in feeds"
944#~ msgstr "フィードã§ãƒ‘ーマリンクã®ä»£ã‚ã‚Šã«ç›´ãƒªãƒ³ã‚¯ã‚’使ã†"
945
946#~ msgid "Hide public links"
947#~ msgstr "公開リンクを隠ã™"
948
949#~ msgid "Do not show any links if the user is not logged in"
950#~ msgstr "ログインã—ã¦ã„ãªã„ユーザーã«ã¯ä½•ã®ãƒªãƒ³ã‚¯ã‚‚表示ã—ãªã„"
951
952#~ msgid "Check updates"
953#~ msgstr "更新を確èª"
954
955#~ msgid "Notify me when a new release is ready"
956#~ msgstr "æ–°ã—ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒãƒªãƒªãƒ¼ã‚¹ã•ã‚ŒãŸã¨ãã«é€šçŸ¥"
957
958#~ msgid "Enable REST API"
959#~ msgstr "REST API を有効化"
960
961#~ msgid "Allow third party software to use Shaarli such as mobile application"
962#~ msgstr ""
963#~ "モãƒã‚¤ãƒ«ã‚¢ãƒ—リã¨ã„ã£ãŸã‚µãƒ¼ãƒ‰ãƒ‘ーティーã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã«Shaarliを使用ã™ã‚‹ã“"
964#~ "ã¨ã‚’許å¯"
965
966#~ msgid "API secret"
967#~ msgstr "API シークレット"
968
969#~ msgid "Save"
970#~ msgstr "ä¿å­˜"
971
972#~ msgid "The Daily Shaarli"
973#~ msgstr "デイリーSharli"
974
975#~ msgid "1 RSS entry per day"
976#~ msgstr "å„æ—¥1ã¤ãšã¤ã®RSSé …ç›®"
977
978#~ msgid "Previous day"
979#~ msgstr "å‰æ—¥"
980
981#~ msgid "All links of one day in a single page."
982#~ msgstr "1æ—¥ã«ä½œæˆã•ã‚ŒãŸã™ã¹ã¦ã®ãƒªãƒ³ã‚¯ã§ã™ã€‚"
983
984#~ msgid "Next day"
985#~ msgstr "翌日"
986
987#~ msgid "Created:"
988#~ msgstr "作æˆ:"
989
990#~ msgid "URL"
991#~ msgstr "URL"
992
993#~ msgid "Title"
994#~ msgstr "タイトル"
995
996#~ msgid "Description"
997#~ msgstr "説明"
998
999#~ msgid "Tags"
1000#~ msgstr "ã‚¿ã‚°"
1001
1002#~ msgid "Private"
1003#~ msgstr "プライベート"
1004
1005#~ msgid "Apply Changes"
1006#~ msgstr "変更をé©ç”¨"
1007
1008#~ msgid "Export Database"
1009#~ msgstr "データベースをエクスãƒãƒ¼ãƒˆ"
1010
1011#~ msgid "Selection"
1012#~ msgstr "é¸æŠžæ¸ˆã¿"
1013
1014#~ msgid "All"
1015#~ msgstr "ã™ã¹ã¦"
1016
1017#~ msgid "Public"
1018#~ msgstr "公開"
1019
1020#~ msgid "Prepend note permalinks with this Shaarli instance's URL"
1021#~ msgstr ""
1022#~ "ã“ã® Shaarli ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®URL ã«ãƒŽãƒ¼ãƒˆã¸ã®ãƒ‘ーマリンクを付ã‘加ãˆã‚‹"
1023
1024#~ msgid "Useful to import bookmarks in a web browser"
1025#~ msgstr "ウェブブラウザーã®ãƒªãƒ³ã‚¯ã‚’インãƒãƒ¼ãƒˆã™ã‚‹ã®ã«æœ‰åŠ¹ã§ã™"
1026
1027#~ msgid "Import Database"
1028#~ msgstr "データベースをインãƒãƒ¼ãƒˆ"
1029
1030#~ msgid "Maximum size allowed:"
1031#~ msgstr "最大サイズ:"
1032
1033#~ msgid "Visibility"
1034#~ msgstr "å¯è¦–性"
1035
1036#~ msgid "Use values from the imported file, default to public"
1037#~ msgstr "インãƒãƒ¼ãƒˆå…ƒã®ãƒ•ã‚¡ã‚¤ãƒ«ã®å€¤ã‚’使用 (既定ã¯å…¬é–‹ãƒªãƒ³ã‚¯ã¨ãªã‚Šã¾ã™)"
1038
1039#~ msgid "Import all bookmarks as public"
1040#~ msgstr "ã™ã¹ã¦ã®ãƒ–ックマーク項目を公開リンクã¨ã—ã¦ã‚¤ãƒ³ãƒãƒ¼ãƒˆ"
1041
1042#~ msgid "Overwrite existing bookmarks"
1043#~ msgstr "æ—¢ã«å­˜åœ¨ã—ã¦ã„るブックマークを上書ã"
1044
1045#~ msgid "Duplicates based on URL"
1046#~ msgstr "URL ã«ã‚ˆã‚‹é‡è¤‡"
1047
1048#~ msgid "Add default tags"
1049#~ msgstr "既定ã®ã‚¿ã‚°ã‚’追加"
1050
1051#~ msgid "Install Shaarli"
1052#~ msgstr "Shaarli をインストール"
1053
1054#~ msgid ""
1055#~ "It looks like it's the first time you run Shaarli. Please configure it."
1056#~ msgstr "ã©ã†ã‚„ら Shaarli ã‚’åˆã‚ã¦èµ·å‹•ã—ã¦ã„るよã†ã§ã™ã€‚設定ã—ã¦ãã ã•ã„。"
1057
1058#~ msgid "Username"
1059#~ msgstr "ユーザーå"
1060
1061#~ msgid "Password"
1062#~ msgstr "パスワード"
1063
1064#~ msgid "Shaarli title"
1065#~ msgstr "Shaarli ã®ã‚¿ã‚¤ãƒˆãƒ«"
1066
1067#~ msgid "My links"
1068#~ msgstr "自分ã®ãƒªãƒ³ã‚¯"
1069
1070#~ msgid "Install"
1071#~ msgstr "インストール"
1072
1073#~ msgid "shaare"
1074#~ msgid_plural "shaares"
1075#~ msgstr[0] "共有"
1076#~ msgstr[1] "共有"
1077
1078#~ msgid "private link"
1079#~ msgid_plural "private links"
1080#~ msgstr[0] "プライベートリンク"
1081#~ msgstr[1] "プライベートリンク"
1082
1083#~ msgid "Search text"
1084#~ msgstr "文字列ã§æ¤œç´¢"
1085
1086#~ msgid "Filter by tag"
1087#~ msgstr "ã‚¿ã‚°ã«ã‚ˆã£ã¦åˆ†é¡ž"
1088
1089#~ msgid "Nothing found."
1090#~ msgstr "何も見ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚"
1091
1092#~ msgid "%s result"
1093#~ msgid_plural "%s results"
1094#~ msgstr[0] "%s 件ã®çµæžœ"
1095#~ msgstr[1] "%s 件ã®çµæžœ"
1096
1097#~ msgid "for"
1098#~ msgstr "for"
1099
1100#~ msgid "tagged"
1101#~ msgstr "タグ付ã‘ã•ã‚ŒãŸ"
1102
1103#~ msgid "Remove tag"
1104#~ msgstr "タグを削除"
1105
1106#~ msgid "with status"
1107#~ msgstr "with status"
1108
1109#~ msgid "without any tag"
1110#~ msgstr "ã‚¿ã‚°ãªã—"
1111
1112#~ msgid "Fold"
1113#~ msgstr "畳む"
1114
1115#~ msgid "Edited: "
1116#~ msgstr "編集済ã¿: "
1117
1118#~ msgid "permalink"
1119#~ msgstr "パーマリンク"
1120
1121#~ msgid "Add tag"
1122#~ msgstr "タグを追加"
1123
1124#~ msgid "Filters"
1125#~ msgstr "分類"
1126
1127#~ msgid "Only display private links"
1128#~ msgstr "プライベートリンクã®ã¿ã‚’表示"
1129
1130#~ msgid "Only display public links"
1131#~ msgstr "公開リンクã®ã¿ã‚’表示"
1132
1133#~ msgid "Filter untagged links"
1134#~ msgstr "タグ付ã‘ã•ã‚Œã¦ã„ãªã„リンクã§åˆ†é¡ž"
1135
1136#~ msgid "Fold all"
1137#~ msgstr "ã™ã¹ã¦ç•³ã‚€"
1138
1139#~ msgid "Links per page"
1140#~ msgstr "å„ページをリンク"
1141
1142#~ msgid "Remember me"
1143#~ msgstr "パスワードをä¿å­˜"
1144
1145#~ msgid "by the Shaarli community"
1146#~ msgstr "by Shaarli コミュニティ"
1147
1148#~ msgid "Documentation"
1149#~ msgstr "ドキュメント"
1150
1151#~ msgid "Expand"
1152#~ msgstr "展開ã™ã‚‹"
1153
1154#~ msgid "Expand all"
1155#~ msgstr "ã™ã¹ã¦å±•é–‹ã™ã‚‹"
1156
1157#~ msgid "Are you sure you want to delete this link?"
1158#~ msgstr "本当ã«ã“ã®ãƒªãƒ³ã‚¯ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ"
1159
1160#~ msgid "RSS Feed"
1161#~ msgstr "RSS フィード"
1162
1163#~ msgid "Logout"
1164#~ msgstr "ログアウト"
1165
1166#~ msgid "is available"
1167#~ msgstr "ãŒåˆ©ç”¨å¯èƒ½"
1168
1169#~ msgid "Error"
1170#~ msgstr "エラー"
1171
1172#~ msgid "Picture Wall"
1173#~ msgstr "ピクãƒãƒ£ãƒ¼ã‚¦ã‚©ãƒ¼ãƒ«"
1174
1175#~ msgid "pics"
1176#~ msgstr "ç”»åƒ"
1177
1178#~ msgid "You need to enable Javascript to change plugin loading order."
1179#~ msgstr ""
1180#~ "プラグインを読ã¿è¾¼ã‚€é †ç•ªã‚’変更ã™ã‚‹ã«ã¯ã€Javascriptを有効ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾"
1181#~ "ã™ã€‚"
1182
1183#~ msgid "Enabled Plugins"
1184#~ msgstr "有効ãªãƒ—ラグイン"
1185
1186#~ msgid "No plugin enabled."
1187#~ msgstr "有効ãªãƒ—ラグインã¯ã‚ã‚Šã¾ã›ã‚“。"
1188
1189#~ msgid "Disable"
1190#~ msgstr "無効化"
1191
1192#~ msgid "Name"
1193#~ msgstr "åå‰"
1194
1195#~ msgid "Order"
1196#~ msgstr "é †åº"
1197
1198#~ msgid "Disabled Plugins"
1199#~ msgstr "無効ãªãƒ—ラグイン"
1200
1201#~ msgid "No plugin disabled."
1202#~ msgstr "無効ãªãƒ—ラグインã¯ã‚ã‚Šã¾ã›ã‚“。"
1203
1204#~ msgid "Enable"
1205#~ msgstr "有効化"
1206
1207#~ msgid "More plugins available"
1208#~ msgstr "ã•ã‚‰ã«åˆ©ç”¨ã§ãるプラグインãŒã‚ã‚Šã¾ã™"
1209
1210#~ msgid "in the documentation"
1211#~ msgstr "ドキュメント内"
1212
1213#~ msgid "No parameter available."
1214#~ msgstr "利用å¯èƒ½ãªè¨­å®šé …ç›®ã¯ã‚ã‚Šã¾ã›ã‚“。"
1215
1216#~ msgid "tags"
1217#~ msgstr "ã‚¿ã‚°"
1218
1219#~ msgid "List all links with those tags"
1220#~ msgstr "ã“ã®ã‚¿ã‚°ãŒä»˜ã„ã¦ã„るリンクをリスト化ã™ã‚‹"
1221
1222#~ msgid "Sort by:"
1223#~ msgstr "分類:"
1224
1225#~ msgid "Cloud"
1226#~ msgstr "クラウド"
1227
1228#~ msgid "Most used"
1229#~ msgstr "ã‚‚ã£ã¨ã‚‚使ã‚ã‚ŒãŸ"
1230
1231#~ msgid "Alphabetical"
1232#~ msgstr "アルファベット順"
1233
1234#~ msgid "Settings"
1235#~ msgstr "設定"
1236
1237#~ msgid "Change Shaarli settings: title, timezone, etc."
1238#~ msgstr "Shaarli ã®è¨­å®šã‚’変更: タイトルã€ã‚¿ã‚¤ãƒ ã‚¾ãƒ¼ãƒ³ãªã©ã€‚"
1239
1240#~ msgid "Configure your Shaarli"
1241#~ msgstr "ã‚ãªãŸã® Shaarli を設定"
1242
1243#~ msgid "Enable, disable and configure plugins"
1244#~ msgstr "プラグインを有効化ã€ç„¡åŠ¹åŒ–ã€è¨­å®šã™ã‚‹"
1245
1246#~ msgid "Change your password"
1247#~ msgstr "パスワードを変更"
1248
1249#~ msgid "Rename or delete a tag in all links"
1250#~ msgstr "ã™ã¹ã¦ã®ãƒªãƒ³ã‚¯ã®ã‚¿ã‚°ã®åå‰ã‚’変更ã™ã‚‹ã€ã¾ãŸã¯å‰Šé™¤ã™ã‚‹"
1251
1252#~ msgid ""
1253#~ "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1254#~ "delicious...)"
1255#~ msgstr ""
1256#~ "Netscape HTML å½¢å¼ã®ãƒ–ックマークをインãƒãƒ¼ãƒˆã™ã‚‹ (Firefoxã€Chromeã€Operaã¨"
1257#~ "ã„ã£ãŸãƒ–ラウザーãŒå«ã¾ã‚Œã¾ã™)"
1258
1259#~ msgid "Import links"
1260#~ msgstr "リンクをインãƒãƒ¼ãƒˆ"
1261
1262#~ msgid ""
1263#~ "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1264#~ "Opera, delicious...)"
1265#~ msgstr ""
1266#~ "Netscape HTML å½¢å¼ã®ãƒ–ックマークをエクスãƒãƒ¼ãƒˆã™ã‚‹ (Firefoxã€Chromeã€Opera"
1267#~ "ã¨ã„ã£ãŸãƒ–ラウザーãŒå«ã¾ã‚Œã¾ã™)"
1268
1269#~ msgid "Export database"
1270#~ msgstr "リンクをエクスãƒãƒ¼ãƒˆ"
1271
1272#~ msgid ""
1273#~ "Drag one of these button to your bookmarks toolbar or right-click it and "
1274#~ "\"Bookmark This Link\""
1275#~ msgstr ""
1276#~ "ã“れらã®ãƒœã‚¿ãƒ³ã®ã†ã¡1ã¤ã‚’をブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—"
1277#~ "ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„"
1278
1279#~ msgid "then click on the bookmarklet in any page you want to share."
1280#~ msgstr "共有ã—ãŸã„ページã§ãƒ–ックマークレットをクリックã—ã¦ãã ã•ã„。"
1281
1282#~ msgid ""
1283#~ "Drag this link to your bookmarks toolbar or right-click it and Bookmark "
1284#~ "This Link"
1285#~ msgstr ""
1286#~ "ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’"
1287#~ "ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„"
1288
1289#~ msgid "then click ✚Shaare link button in any page you want to share"
1290#~ msgstr ""
1291#~ "✚リンクを共有 ボタンをクリックã™ã‚‹ã“ã¨ã§ã€ã©ã“ã§ã‚‚リンクを共有ã§ãã¾ã™"
1292
1293#~ msgid "The selected text is too long, it will be truncated."
1294#~ msgstr "é¸æŠžã•ã‚ŒãŸæ–‡å­—列ã¯é•·ã™ãŽã‚‹ã®ã§ã€ä¸€éƒ¨ãŒåˆ‡ã‚Šæ¨ã¦ã‚‰ã‚Œã¾ã™ã€‚"
1295
1296#~ msgid "Shaare link"
1297#~ msgstr "共有リンク"
1298
1299#~ msgid ""
1300#~ "Then click ✚Add Note button anytime to start composing a private Note "
1301#~ "(text post) to your Shaarli"
1302#~ msgstr ""
1303#~ "✚ノートを追加 ボタンをクリックã™ã‚‹ã“ã¨ã§ã€ã„ã¤ã§ã‚‚プライベートノート(テキ"
1304#~ "スト形å¼)ã‚’Shaarli上ã«ä½œæˆã§ãã¾ã™"
1305
1306#~ msgid "Add Note"
1307#~ msgstr "ノートを追加"
1308
1309#~ msgid ""
1310#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1311#~ "functionality."
1312#~ msgstr ""
1313#~ "ã“ã®æ©Ÿèƒ½ã‚’使用ã™ã‚‹ã«ã¯ã€<strong>HTTPS</strong> 経由ã§Shaarliã«æŽ¥ç¶šã—ã¦ãã "
1314#~ "ã•ã„。"
1315
1316#~ msgid "Add to"
1317#~ msgstr "次ã«è¿½åŠ :"
1318
1319#~ msgid "3rd party"
1320#~ msgstr "サードパーティー"
1321
1322#~ msgid "Plugin"
1323#~ msgstr "プラグイン"
1324
1325#~ msgid "plugin"
1326#~ msgstr "プラグイン"
1327
1328#~ msgid ""
1329#~ "Drag this link to your bookmarks toolbar, or right-click it and choose "
1330#~ "Bookmark This Link"
1331#~ msgstr ""
1332#~ "ã“ã®ãƒªãƒ³ã‚¯ã‚’ブックマークãƒãƒ¼ã«ãƒ‰ãƒ©ãƒƒã‚°ã™ã‚‹ã‹ã€å³ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã€Œã“ã®ãƒªãƒ³ã‚¯ã‚’"
1333#~ "ブックマークã«è¿½åŠ ã€ã—ã¦ãã ã•ã„"
diff --git a/index.php b/index.php
index b10397dd..1eb7659a 100644
--- a/index.php
+++ b/index.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2/** 3/**
3 * Shaarli - The personal, minimalist, super-fast, database free, bookmarking service. 4 * Shaarli - The personal, minimalist, super-fast, database free, bookmarking service.
4 * 5 *
@@ -25,9 +26,12 @@ require_once 'application/Utils.php';
25 26
26require_once __DIR__ . '/init.php'; 27require_once __DIR__ . '/init.php';
27 28
29use Katzgrau\KLogger\Logger;
30use Psr\Log\LogLevel;
28use Shaarli\Config\ConfigManager; 31use Shaarli\Config\ConfigManager;
29use Shaarli\Container\ContainerBuilder; 32use Shaarli\Container\ContainerBuilder;
30use Shaarli\Languages; 33use Shaarli\Languages;
34use Shaarli\Security\BanManager;
31use Shaarli\Security\CookieManager; 35use Shaarli\Security\CookieManager;
32use Shaarli\Security\LoginManager; 36use Shaarli\Security\LoginManager;
33use Shaarli\Security\SessionManager; 37use Shaarli\Security\SessionManager;
@@ -48,10 +52,22 @@ if ($conf->get('dev.debug', false)) {
48 }); 52 });
49} 53}
50 54
55$logger = new Logger(
56 dirname($conf->get('resource.log')),
57 !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
58 ['filename' => basename($conf->get('resource.log'))]
59);
51$sessionManager = new SessionManager($_SESSION, $conf, session_save_path()); 60$sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
52$sessionManager->initialize(); 61$sessionManager->initialize();
53$cookieManager = new CookieManager($_COOKIE); 62$cookieManager = new CookieManager($_COOKIE);
54$loginManager = new LoginManager($conf, $sessionManager, $cookieManager); 63$banManager = new BanManager(
64 $conf->get('security.trusted_proxies', []),
65 $conf->get('security.ban_after'),
66 $conf->get('security.ban_duration'),
67 $conf->get('resource.ban_file', 'data/ipbans.php'),
68 $logger
69);
70$loginManager = new LoginManager($conf, $sessionManager, $cookieManager, $banManager, $logger);
55$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); 71$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
56 72
57// Sniff browser language and set date format accordingly. 73// Sniff browser language and set date format accordingly.
@@ -62,16 +78,16 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
62new Languages(setlocale(LC_MESSAGES, 0), $conf); 78new Languages(setlocale(LC_MESSAGES, 0), $conf);
63 79
64$conf->setEmpty('general.timezone', date_default_timezone_get()); 80$conf->setEmpty('general.timezone', date_default_timezone_get());
65$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER))); 81$conf->setEmpty('general.title', t('Shared bookmarks on ') . escape(index_url($_SERVER)));
66 82
67RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory 83RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl') . '/' . $conf->get('resource.theme') . '/'; // template directory
68RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 84RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
69 85
70date_default_timezone_set($conf->get('general.timezone', 'UTC')); 86date_default_timezone_set($conf->get('general.timezone', 'UTC'));
71 87
72$loginManager->checkLoginState(client_ip_id($_SERVER)); 88$loginManager->checkLoginState(client_ip_id($_SERVER));
73 89
74$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager); 90$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger);
75$container = $containerBuilder->build(); 91$container = $containerBuilder->build();
76$app = new App($container); 92$app = new App($container);
77 93
@@ -110,13 +126,16 @@ $app->group('/admin', function () {
110 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save'); 126 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
111 $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index'); 127 $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
112 $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save'); 128 $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
113 $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare'); 129 $this->post('/tags/change-separator', '\Shaarli\Front\Controller\Admin\ManageTagController:changeSeparator');
114 $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm'); 130 $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare');
115 $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm'); 131 $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm');
116 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); 132 $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm');
117 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); 133 $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate');
118 $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); 134 $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms');
119 $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); 135 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save');
136 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark');
137 $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility');
138 $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark');
120 $this->patch( 139 $this->patch(
121 '/shaare/{id:[0-9]+}/update-thumbnail', 140 '/shaare/{id:[0-9]+}/update-thumbnail',
122 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate' 141 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
@@ -128,8 +147,10 @@ $app->group('/admin', function () {
128 $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index'); 147 $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
129 $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save'); 148 $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
130 $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken'); 149 $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
150 $this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index');
151 $this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache');
131 $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index'); 152 $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
132 153 $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle');
133 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); 154 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
134})->add('\Shaarli\Front\ShaarliAdminMiddleware'); 155})->add('\Shaarli\Front\ShaarliAdminMiddleware');
135 156
@@ -151,6 +172,12 @@ $app->group('/api/v1', function () {
151 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); 172 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
152})->add('\Shaarli\Api\ApiMiddleware'); 173})->add('\Shaarli\Api\ApiMiddleware');
153 174
154$response = $app->run(true); 175try {
155 176 $response = $app->run(true);
156$app->respond($response); 177 $app->respond($response);
178} catch (Throwable $e) {
179 die(nl2br(
180 'An unexpected error happened, and the error template could not be displayed.' . PHP_EOL . PHP_EOL .
181 exception2text($e)
182 ));
183}
diff --git a/init.php b/init.php
index f0b84368..d8462712 100644
--- a/init.php
+++ b/init.php
@@ -2,7 +2,7 @@
2 2
3require_once __DIR__ . '/vendor/autoload.php'; 3require_once __DIR__ . '/vendor/autoload.php';
4 4
5use Shaarli\ApplicationUtils; 5use Shaarli\Helper\ApplicationUtils;
6use Shaarli\Security\SessionManager; 6use Shaarli\Security\SessionManager;
7 7
8// Set 'UTC' as the default timezone if it is not defined in php.ini 8// Set 'UTC' as the default timezone if it is not defined in php.ini
@@ -60,6 +60,7 @@ ini_set('session.use_only_cookies', 1);
60ini_set('session.use_trans_sid', false); 60ini_set('session.use_trans_sid', false);
61 61
62define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); 62define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
63define('SHAARLI_MUTEX_FILE', __FILE__);
63 64
64session_name('shaarli'); 65session_name('shaarli');
65// Start session if needed (Some server auto-start sessions). 66// Start session if needed (Some server auto-start sessions).
diff --git a/package.json b/package.json
index 8a24512a..b879b223 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
7 "awesomplete": "^1.1.2", 7 "awesomplete": "^1.1.2",
8 "blazy": "^1.8.2", 8 "blazy": "^1.8.2",
9 "fork-awesome": "^1.1.7", 9 "fork-awesome": "^1.1.7",
10 "he": "^1.2.0",
10 "pure-extras": "^1.0.0", 11 "pure-extras": "^1.0.0",
11 "purecss": "^1.0.0" 12 "purecss": "^1.0.0"
12 }, 13 },
diff --git a/phpcs.xml b/phpcs.xml
index 29b95d56..c559e35d 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -5,13 +5,18 @@
5 <file>index.php</file> 5 <file>index.php</file>
6 <file>application</file> 6 <file>application</file>
7 <file>plugins</file> 7 <file>plugins</file>
8 <file>tests</file> 8<!-- <file>tests</file>-->
9 9
10 <exclude-pattern>*/*.css</exclude-pattern> 10 <exclude-pattern>*/*.css</exclude-pattern>
11 <exclude-pattern>*/*.js</exclude-pattern> 11 <exclude-pattern>*/*.js</exclude-pattern>
12 12
13 <arg name="colors"/> 13 <arg name="colors"/>
14 14
15 <rule ref="PSR1"/> 15 <rule ref="PSR12"/>
16 <rule ref="PSR2"/> 16 <rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
17
18 <rule ref="PSR1.Files.SideEffects.FoundWithSymbols">
19 <!-- index.php bootstraps everything, so yes mixed symbols with side effects -->
20 <exclude-pattern>index.php</exclude-pattern>
21 </rule>
17</ruleset> 22</ruleset>
diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php
index ab6ed6de..80b1dd95 100644
--- a/plugins/addlink_toolbar/addlink_toolbar.php
+++ b/plugins/addlink_toolbar/addlink_toolbar.php
@@ -17,26 +17,26 @@ use Shaarli\Render\TemplatePage;
17function hook_addlink_toolbar_render_header($data) 17function hook_addlink_toolbar_render_header($data)
18{ 18{
19 if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) { 19 if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) {
20 $form = array( 20 $form = [
21 'attr' => array( 21 'attr' => [
22 'method' => 'GET', 22 'method' => 'GET',
23 'action' => $data['_BASE_PATH_'] . '/admin/shaare', 23 'action' => $data['_BASE_PATH_'] . '/admin/shaare',
24 'name' => 'addform', 24 'name' => 'addform',
25 'class' => 'addform', 25 'class' => 'addform',
26 ), 26 ],
27 'inputs' => array( 27 'inputs' => [
28 array( 28 [
29 'type' => 'text', 29 'type' => 'text',
30 'name' => 'post', 30 'name' => 'post',
31 'placeholder' => t('URI'), 31 'placeholder' => t('URI'),
32 ), 32 ],
33 array( 33 [
34 'type' => 'submit', 34 'type' => 'submit',
35 'value' => t('Add link'), 35 'value' => t('Add link'),
36 'class' => 'bigbutton', 36 'class' => 'bigbutton',
37 ), 37 ],
38 ), 38 ],
39 ); 39 ];
40 $data['fields_toolbar'][] = $form; 40 $data['fields_toolbar'][] = $form;
41 } 41 }
42 42
diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php
index 922b5966..88f2b653 100644
--- a/plugins/archiveorg/archiveorg.php
+++ b/plugins/archiveorg/archiveorg.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2/** 3/**
3 * Plugin Archive.org. 4 * Plugin Archive.org.
4 * 5 *
@@ -17,7 +18,7 @@ use Shaarli\Plugin\PluginManager;
17function hook_archiveorg_render_linklist($data) 18function hook_archiveorg_render_linklist($data)
18{ 19{
19 $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html'); 20 $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html');
20 $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; 21 $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
21 22
22 foreach ($data['links'] as &$value) { 23 foreach ($data['links'] as &$value) {
23 $isNote = startsWith($value['real_url'], '/shaare/'); 24 $isNote = startsWith($value['real_url'], '/shaare/');
diff --git a/plugins/default_colors/default_colors.php b/plugins/default_colors/default_colors.php
index e1fd5cfb..574a0bd4 100644
--- a/plugins/default_colors/default_colors.php
+++ b/plugins/default_colors/default_colors.php
@@ -28,14 +28,14 @@ function default_colors_init($conf)
28{ 28{
29 $params = []; 29 $params = [];
30 foreach (DEFAULT_COLORS_PLACEHOLDERS as $placeholder) { 30 foreach (DEFAULT_COLORS_PLACEHOLDERS as $placeholder) {
31 $value = trim($conf->get('plugins.'. $placeholder, '')); 31 $value = trim($conf->get('plugins.' . $placeholder, ''));
32 if (strlen($value) > 0) { 32 if (strlen($value) > 0) {
33 $params[$placeholder] = $value; 33 $params[$placeholder] = $value;
34 } 34 }
35 } 35 }
36 36
37 if (empty($params)) { 37 if (empty($params)) {
38 $error = t('Default colors plugin error: '. 38 $error = t('Default colors plugin error: ' .
39 'This plugin is active and no custom color is configured.'); 39 'This plugin is active and no custom color is configured.');
40 return [$error]; 40 return [$error];
41 } 41 }
@@ -56,7 +56,7 @@ function default_colors_init($conf)
56function hook_default_colors_render_includes($data) 56function hook_default_colors_render_includes($data)
57{ 57{
58 $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css'; 58 $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css';
59 if (file_exists($file )) { 59 if (file_exists($file)) {
60 $data['css_files'][] = $file ; 60 $data['css_files'][] = $file ;
61 } 61 }
62 62
@@ -75,7 +75,7 @@ function default_colors_generate_css_file($params): void
75 $content = ''; 75 $content = '';
76 foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) { 76 foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) {
77 $content .= !empty($params[$rule]) 77 $content .= !empty($params[$rule])
78 ? default_colors_format_css_rule($params, $rule) .';'. PHP_EOL 78 ? default_colors_format_css_rule($params, $rule) . ';' . PHP_EOL
79 : ''; 79 : '';
80 } 80 }
81 81
@@ -99,8 +99,8 @@ function default_colors_format_css_rule($data, $parameter)
99 } 99 }
100 100
101 $key = str_replace('DEFAULT_COLORS_', '', $parameter); 101 $key = str_replace('DEFAULT_COLORS_', '', $parameter);
102 $key = str_replace('_', '-', strtolower($key)) .'-color'; 102 $key = str_replace('_', '-', strtolower($key)) . '-color';
103 return ' --'. $key .': '. $data[$parameter]; 103 return ' --' . $key . ': ' . $data[$parameter];
104} 104}
105 105
106 106
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php
index defb01f7..22d27b68 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2/** 3/**
3 * Demo Plugin. 4 * Demo Plugin.
4 * 5 *
@@ -82,14 +83,14 @@ function hook_demo_plugin_render_header($data)
82 * A link is an array of its attributes (key="value"), 83 * A link is an array of its attributes (key="value"),
83 * and a mandatory `html` key, which contains its value. 84 * and a mandatory `html` key, which contains its value.
84 */ 85 */
85 $button = array( 86 $button = [
86 'attr' => array ( 87 'attr' => [
87 'href' => '#', 88 'href' => '#',
88 'class' => 'mybutton', 89 'class' => 'mybutton',
89 'title' => 'hover me', 90 'title' => 'hover me',
90 ), 91 ],
91 'html' => 'DEMO buttons toolbar', 92 'html' => 'DEMO buttons toolbar',
92 ); 93 ];
93 $data['buttons_toolbar'][] = $button; 94 $data['buttons_toolbar'][] = $button;
94 } 95 }
95 96
@@ -115,29 +116,29 @@ function hook_demo_plugin_render_header($data)
115 * <input input-2-attribute-1="input 2 attribute 1 value"> 116 * <input input-2-attribute-1="input 2 attribute 1 value">
116 * </form> 117 * </form>
117 */ 118 */
118 $form = array( 119 $form = [
119 'attr' => array( 120 'attr' => [
120 'method' => 'GET', 121 'method' => 'GET',
121 'action' => $data['_BASE_PATH_'] . '/', 122 'action' => $data['_BASE_PATH_'] . '/',
122 'class' => 'addform', 123 'class' => 'addform',
123 ), 124 ],
124 'inputs' => array( 125 'inputs' => [
125 array( 126 [
126 'type' => 'text', 127 'type' => 'text',
127 'name' => 'demo', 128 'name' => 'demo',
128 'placeholder' => 'demo', 129 'placeholder' => 'demo',
129 ) 130 ]
130 ) 131 ]
131 ); 132 ];
132 $data['fields_toolbar'][] = $form; 133 $data['fields_toolbar'][] = $form;
133 } 134 }
134 // Another button always displayed 135 // Another button always displayed
135 $button = array( 136 $button = [
136 'attr' => array( 137 'attr' => [
137 'href' => '#', 138 'href' => '#',
138 ), 139 ],
139 'html' => 'Demo', 140 'html' => 'Demo',
140 ); 141 ];
141 $data['buttons_toolbar'][] = $button; 142 $data['buttons_toolbar'][] = $button;
142 143
143 return $data; 144 return $data;
@@ -187,7 +188,7 @@ function hook_demo_plugin_render_includes($data)
187function hook_demo_plugin_render_footer($data) 188function hook_demo_plugin_render_footer($data)
188{ 189{
189 // Footer text 190 // Footer text
190 $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); 191 $data['text'][] = '<br>' . demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
191 192
192 // Free elements at the end of the page. 193 // Free elements at the end of the page.
193 $data['endofpage'][] = '<marquee id="demo_marquee">' . 194 $data['endofpage'][] = '<marquee id="demo_marquee">' .
@@ -229,13 +230,13 @@ function hook_demo_plugin_render_linklist($data)
229 * and a mandatory `html` key, which contains its value. 230 * and a mandatory `html` key, which contains its value.
230 * It's also recommended to add key 'on' or 'off' for theme rendering. 231 * It's also recommended to add key 'on' or 'off' for theme rendering.
231 */ 232 */
232 $action = array( 233 $action = [
233 'attr' => array( 234 'attr' => [
234 'href' => '?up', 235 'href' => '?up',
235 'title' => 'Uppercase!', 236 'title' => 'Uppercase!',
236 ), 237 ],
237 'html' => 'â†', 238 'html' => 'â†',
238 ); 239 ];
239 240
240 if (isset($_GET['up'])) { 241 if (isset($_GET['up'])) {
241 // Manipulate link data 242 // Manipulate link data
@@ -275,7 +276,7 @@ function hook_demo_plugin_render_linklist($data)
275function hook_demo_plugin_render_editlink($data) 276function hook_demo_plugin_render_editlink($data)
276{ 277{
277 // Load HTML into a string 278 // Load HTML into a string
278 $html = file_get_contents(PluginManager::$PLUGINS_PATH .'/demo_plugin/field.html'); 279 $html = file_get_contents(PluginManager::$PLUGINS_PATH . '/demo_plugin/field.html');
279 280
280 // Replace value in HTML if it exists in $data 281 // Replace value in HTML if it exists in $data
281 if (!empty($data['link']['stuff'])) { 282 if (!empty($data['link']['stuff'])) {
diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php
index 79e7380b..a5450989 100644
--- a/plugins/isso/isso.php
+++ b/plugins/isso/isso.php
@@ -19,9 +19,9 @@ function isso_init($conf)
19{ 19{
20 $issoUrl = $conf->get('plugins.ISSO_SERVER'); 20 $issoUrl = $conf->get('plugins.ISSO_SERVER');
21 if (empty($issoUrl)) { 21 if (empty($issoUrl)) {
22 $error = t('Isso plugin error: '. 22 $error = t('Isso plugin error: ' .
23 'Please define the "ISSO_SERVER" setting in the plugin administration page.'); 23 'Please define the "ISSO_SERVER" setting in the plugin administration page.');
24 return array($error); 24 return [$error];
25 } 25 }
26} 26}
27 27
@@ -49,12 +49,12 @@ function hook_isso_render_linklist($data, $conf)
49 $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']); 49 $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']);
50 $data['plugin_end_zone'][] = $isso; 50 $data['plugin_end_zone'][] = $isso;
51 } else { 51 } else {
52 $button = '<span><a href="'. ($data['_BASE_PATH_'] ?? '') . '/shaare/%s#isso-thread">'; 52 $button = '<span><a href="' . ($data['_BASE_PATH_'] ?? '') . '/shaare/%s#isso-thread">';
53 // For the default theme we use a FontAwesome icon which is better than an image 53 // For the default theme we use a FontAwesome icon which is better than an image
54 if ($conf->get('resource.theme') === 'default') { 54 if ($conf->get('resource.theme') === 'default') {
55 $button .= '<i class="linklist-plugin-icon fa fa-comment"></i>'; 55 $button .= '<i class="linklist-plugin-icon fa fa-comment"></i>';
56 } else { 56 } else {
57 $button .= '<img class="linklist-plugin-icon" src="plugins/isso/comment.png" '; 57 $button .= '<img class="linklist-plugin-icon" src="' . $data['_ROOT_PATH_'] . '/plugins/isso/comment.png" ';
58 $button .= 'title="Comment on this shaare" alt="Comments" />'; 58 $button .= 'title="Comment on this shaare" alt="Comments" />';
59 } 59 }
60 $button .= '</a></span>'; 60 $button .= '</a></span>';
diff --git a/plugins/piwik/piwik.php b/plugins/piwik/piwik.php
index 17b1aecc..efea8610 100644
--- a/plugins/piwik/piwik.php
+++ b/plugins/piwik/piwik.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2/** 3/**
3 * Piwik plugin. 4 * Piwik plugin.
4 * Adds tracking code on each page. 5 * Adds tracking code on each page.
@@ -22,7 +23,7 @@ function piwik_init($conf)
22 if (empty($piwikUrl) || empty($piwikSiteid)) { 23 if (empty($piwikUrl) || empty($piwikSiteid)) {
23 $error = t('Piwik plugin error: ' . 24 $error = t('Piwik plugin error: ' .
24 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'); 25 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.');
25 return array($error); 26 return [$error];
26 } 27 }
27} 28}
28 29
diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php
index 91a9c1e5..4f874f92 100644
--- a/plugins/playvideos/playvideos.php
+++ b/plugins/playvideos/playvideos.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2/** 3/**
3 * Plugin PlayVideos 4 * Plugin PlayVideos
4 * 5 *
@@ -19,14 +20,14 @@ use Shaarli\Render\TemplatePage;
19function hook_playvideos_render_header($data) 20function 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)
42function hook_pubsubhubbub_render_feed($data, $conf) 42function 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 */
60function hook_pubsubhubbub_save_link($data, $conf) 60function 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 */
88function nocurl_http_post($url, $postString) 88function nocurl_http_post($url, $postString)
89{ 89{
90 $params = array('http' => array( 90 $params = ['http' => [
91 'method' => 'POST', 91 'method' => 'POST',
92 'content' => $postString, 92 'content' => $postString,
93 'user_agent' => 'PubSubHubbub-Publisher-PHP/1.0', 93 'user_agent' => 'PubSubHubbub-Publisher-PHP/1.0',
94 )); 94 ]];
95 95
96 $context = stream_context_create($params); 96 $context = stream_context_create($params);
97 $fp = @fopen($url, 'rb', false, $context); 97 $fp = @fopen($url, 'rb', false, $context);
diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php
index 95499e39..2ae10476 100644
--- a/plugins/qrcode/qrcode.php
+++ b/plugins/qrcode/qrcode.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2/** 3/**
3 * Plugin qrcode 4 * Plugin qrcode
4 * Add QRCode containing URL for each links. 5 * Add QRCode containing URL for each links.
@@ -19,7 +20,7 @@ function hook_qrcode_render_linklist($data)
19{ 20{
20 $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); 21 $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html');
21 22
22 $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; 23 $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
23 foreach ($data['links'] as &$value) { 24 foreach ($data['links'] as &$value) {
24 $qrcode = sprintf( 25 $qrcode = sprintf(
25 $qrcode_html, 26 $qrcode_html,
diff --git a/plugins/wallabag/WallabagInstance.php b/plugins/wallabag/WallabagInstance.php
index f4a0a92b..88f84ae3 100644
--- a/plugins/wallabag/WallabagInstance.php
+++ b/plugins/wallabag/WallabagInstance.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Plugin\Wallabag; 3namespace Shaarli\Plugin\Wallabag;
3 4
4/** 5/**
@@ -11,20 +12,20 @@ class WallabagInstance
11 * - key: version ID, must match plugin settings. 12 * - key: version ID, must match plugin settings.
12 * - value: version name. 13 * - value: version name.
13 */ 14 */
14 private static $wallabagVersions = array( 15 private static $wallabagVersions = [
15 1 => '1.x', 16 1 => '1.x',
16 2 => '2.x', 17 2 => '2.x',
17 ); 18 ];
18 19
19 /** 20 /**
20 * @var array Static reference to WB endpoint according to the API version. 21 * @var array Static reference to WB endpoint according to the API version.
21 * - key: version name. 22 * - key: version name.
22 * - value: endpoint. 23 * - value: endpoint.
23 */ 24 */
24 private static $wallabagEndpoints = array( 25 private static $wallabagEndpoints = [
25 '1.x' => '?plainurl=', 26 '1.x' => '?plainurl=',
26 '2.x' => 'bookmarklet?url=', 27 '2.x' => 'bookmarklet?url=',
27 ); 28 ];
28 29
29 /** 30 /**
30 * @var string Wallabag user instance URL. 31 * @var string Wallabag user instance URL.
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php
index 805c1ad9..f2003cb9 100644
--- a/plugins/wallabag/wallabag.php
+++ b/plugins/wallabag/wallabag.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2/** 3/**
3 * Wallabag plugin 4 * Wallabag plugin
4 */ 5 */
@@ -18,10 +19,11 @@ function wallabag_init($conf)
18{ 19{
19 $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); 20 $wallabagUrl = $conf->get('plugins.WALLABAG_URL');
20 if (empty($wallabagUrl)) { 21 if (empty($wallabagUrl)) {
21 $error = t('Wallabag plugin error: '. 22 $error = t('Wallabag plugin error: ' .
22 'Please define the "WALLABAG_URL" setting in the plugin administration page.'); 23 'Please define the "WALLABAG_URL" setting in the plugin administration page.');
23 return array($error); 24 return [$error];
24 } 25 }
26 $conf->setEmpty('plugins.WALLABAG_URL', '2');
25} 27}
26 28
27/** 29/**
@@ -35,7 +37,7 @@ function wallabag_init($conf)
35function hook_wallabag_render_linklist($data, $conf) 37function hook_wallabag_render_linklist($data, $conf)
36{ 38{
37 $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); 39 $wallabagUrl = $conf->get('plugins.WALLABAG_URL');
38 if (empty($wallabagUrl)) { 40 if (empty($wallabagUrl) || !$data['_LOGGEDIN_']) {
39 return $data; 41 return $data;
40 } 42 }
41 43
@@ -45,13 +47,13 @@ function hook_wallabag_render_linklist($data, $conf)
45 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); 47 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
46 48
47 $linkTitle = t('Save to wallabag'); 49 $linkTitle = t('Save to wallabag');
48 $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH; 50 $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
49 51
50 foreach ($data['links'] as &$value) { 52 foreach ($data['links'] as &$value) {
51 $wallabag = sprintf( 53 $wallabag = sprintf(
52 $wallabagHtml, 54 $wallabagHtml,
53 $wallabagInstance->getWallabagUrl(), 55 $wallabagInstance->getWallabagUrl(),
54 urlencode($value['url']), 56 urlencode(unescape($value['url'])),
55 $path, 57 $path,
56 $linkTitle 58 $linkTitle
57 ); 59 );
diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php
index 6dc0e5b7..e810104e 100644
--- a/tests/HistoryTest.php
+++ b/tests/HistoryTest.php
@@ -89,14 +89,6 @@ class HistoryTest extends \Shaarli\TestCase
89 $this->assertEquals(History::CREATED, $actual['event']); 89 $this->assertEquals(History::CREATED, $actual['event']);
90 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 90 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
91 $this->assertEquals(1, $actual['id']); 91 $this->assertEquals(1, $actual['id']);
92
93 $history = new History(self::$historyFilePath);
94 $bookmark = (new Bookmark())->setId('str');
95 $history->addLink($bookmark);
96 $actual = $history->getHistory()[0];
97 $this->assertEquals(History::CREATED, $actual['event']);
98 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
99 $this->assertEquals('str', $actual['id']);
100 } 92 }
101 93
102// /** 94// /**
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index 6e787d7f..59dca75f 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -63,41 +63,25 @@ class UtilsTest extends \Shaarli\TestCase
63 } 63 }
64 64
65 /** 65 /**
66 * Log a message to a file - IPv4 client address 66 * Format a log a message - IPv4 client address
67 */ 67 */
68 public function testLogmIp4() 68 public function testFormatLogIp4()
69 { 69 {
70 $logMessage = 'IPv4 client connected'; 70 $message = 'IPv4 client connected';
71 logm(self::$testLogFile, '127.0.0.1', $logMessage); 71 $log = format_log($message, '127.0.0.1');
72 list($date, $ip, $message) = $this->getLastLogEntry();
73 72
74 $this->assertInstanceOf( 73 static::assertSame('- 127.0.0.1 - IPv4 client connected', $log);
75 'DateTime',
76 DateTime::createFromFormat(self::$dateFormat, $date)
77 );
78 $this->assertTrue(
79 filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false
80 );
81 $this->assertEquals($logMessage, $message);
82 } 74 }
83 75
84 /** 76 /**
85 * Log a message to a file - IPv6 client address 77 * Format a log a message - IPv6 client address
86 */ 78 */
87 public function testLogmIp6() 79 public function testFormatLogIp6()
88 { 80 {
89 $logMessage = 'IPv6 client connected'; 81 $message = 'IPv6 client connected';
90 logm(self::$testLogFile, '2001:db8::ff00:42:8329', $logMessage); 82 $log = format_log($message, '2001:db8::ff00:42:8329');
91 list($date, $ip, $message) = $this->getLastLogEntry();
92 83
93 $this->assertInstanceOf( 84 static::assertSame('- 2001:db8::ff00:42:8329 - IPv6 client connected', $log);
94 'DateTime',
95 DateTime::createFromFormat(self::$dateFormat, $date)
96 );
97 $this->assertTrue(
98 filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false
99 );
100 $this->assertEquals($logMessage, $message);
101 } 85 }
102 86
103 /** 87 /**
diff --git a/tests/api/controllers/info/InfoTest.php b/tests/api/controllers/info/InfoTest.php
index 1598e1e8..10b29ab2 100644
--- a/tests/api/controllers/info/InfoTest.php
+++ b/tests/api/controllers/info/InfoTest.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use malkusch\lock\mutex\NoMutex;
4use Shaarli\Bookmark\BookmarkFileService; 5use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
6use Shaarli\History; 7use Shaarli\History;
@@ -49,6 +50,7 @@ class InfoTest extends TestCase
49 */ 50 */
50 protected function setUp(): void 51 protected function setUp(): void
51 { 52 {
53 $mutex = new NoMutex();
52 $this->conf = new ConfigManager('tests/utils/config/configJson'); 54 $this->conf = new ConfigManager('tests/utils/config/configJson');
53 $this->conf->set('resource.datastore', self::$testDatastore); 55 $this->conf->set('resource.datastore', self::$testDatastore);
54 $this->refDB = new \ReferenceLinkDB(); 56 $this->refDB = new \ReferenceLinkDB();
@@ -58,7 +60,7 @@ class InfoTest extends TestCase
58 60
59 $this->container = new Container(); 61 $this->container = new Container();
60 $this->container['conf'] = $this->conf; 62 $this->container['conf'] = $this->conf;
61 $this->container['db'] = new BookmarkFileService($this->conf, $history, true); 63 $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
62 $this->container['history'] = null; 64 $this->container['history'] = null;
63 65
64 $this->controller = new Info($this->container); 66 $this->controller = new Info($this->container);
diff --git a/tests/api/controllers/links/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php
index cf9464f0..805c9be3 100644
--- a/tests/api/controllers/links/DeleteLinkTest.php
+++ b/tests/api/controllers/links/DeleteLinkTest.php
@@ -3,6 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use malkusch\lock\mutex\NoMutex;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
8use Shaarli\History; 9use Shaarli\History;
@@ -53,11 +54,15 @@ class DeleteLinkTest extends \Shaarli\TestCase
53 */ 54 */
54 protected $controller; 55 protected $controller;
55 56
57 /** @var NoMutex */
58 protected $mutex;
59
56 /** 60 /**
57 * Before each test, instantiate a new Api with its config, plugins and bookmarks. 61 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
58 */ 62 */
59 protected function setUp(): void 63 protected function setUp(): void
60 { 64 {
65 $this->mutex = new NoMutex();
61 $this->conf = new ConfigManager('tests/utils/config/configJson'); 66 $this->conf = new ConfigManager('tests/utils/config/configJson');
62 $this->conf->set('resource.datastore', self::$testDatastore); 67 $this->conf->set('resource.datastore', self::$testDatastore);
63 $this->refDB = new \ReferenceLinkDB(); 68 $this->refDB = new \ReferenceLinkDB();
@@ -65,7 +70,7 @@ class DeleteLinkTest extends \Shaarli\TestCase
65 $refHistory = new \ReferenceHistory(); 70 $refHistory = new \ReferenceHistory();
66 $refHistory->write(self::$testHistory); 71 $refHistory->write(self::$testHistory);
67 $this->history = new History(self::$testHistory); 72 $this->history = new History(self::$testHistory);
68 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 73 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
69 74
70 $this->container = new Container(); 75 $this->container = new Container();
71 $this->container['conf'] = $this->conf; 76 $this->container['conf'] = $this->conf;
@@ -100,7 +105,7 @@ class DeleteLinkTest extends \Shaarli\TestCase
100 $this->assertEquals(204, $response->getStatusCode()); 105 $this->assertEquals(204, $response->getStatusCode());
101 $this->assertEmpty((string) $response->getBody()); 106 $this->assertEmpty((string) $response->getBody());
102 107
103 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 108 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
104 $this->assertFalse($this->bookmarkService->exists($id)); 109 $this->assertFalse($this->bookmarkService->exists($id));
105 110
106 $historyEntry = $this->history->getHistory()[0]; 111 $historyEntry = $this->history->getHistory()[0];
diff --git a/tests/api/controllers/links/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php
index 99dc606f..1ec56ef3 100644
--- a/tests/api/controllers/links/GetLinkIdTest.php
+++ b/tests/api/controllers/links/GetLinkIdTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
@@ -57,6 +58,7 @@ class GetLinkIdTest extends \Shaarli\TestCase
57 */ 58 */
58 protected function setUp(): void 59 protected function setUp(): void
59 { 60 {
61 $mutex = new NoMutex();
60 $this->conf = new ConfigManager('tests/utils/config/configJson'); 62 $this->conf = new ConfigManager('tests/utils/config/configJson');
61 $this->conf->set('resource.datastore', self::$testDatastore); 63 $this->conf->set('resource.datastore', self::$testDatastore);
62 $this->refDB = new \ReferenceLinkDB(); 64 $this->refDB = new \ReferenceLinkDB();
@@ -65,7 +67,7 @@ class GetLinkIdTest extends \Shaarli\TestCase
65 67
66 $this->container = new Container(); 68 $this->container = new Container();
67 $this->container['conf'] = $this->conf; 69 $this->container['conf'] = $this->conf;
68 $this->container['db'] = new BookmarkFileService($this->conf, $history, true); 70 $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
69 $this->container['history'] = null; 71 $this->container['history'] = null;
70 72
71 $this->controller = new Links($this->container); 73 $this->controller = new Links($this->container);
diff --git a/tests/api/controllers/links/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php
index ca1bfc63..b1c46ee2 100644
--- a/tests/api/controllers/links/GetLinksTest.php
+++ b/tests/api/controllers/links/GetLinksTest.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use malkusch\lock\mutex\NoMutex;
4use Shaarli\Bookmark\Bookmark; 5use Shaarli\Bookmark\Bookmark;
5use Shaarli\Bookmark\BookmarkFileService; 6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\LinkDB; 7use Shaarli\Bookmark\LinkDB;
@@ -57,6 +58,7 @@ class GetLinksTest extends \Shaarli\TestCase
57 */ 58 */
58 protected function setUp(): void 59 protected function setUp(): void
59 { 60 {
61 $mutex = new NoMutex();
60 $this->conf = new ConfigManager('tests/utils/config/configJson'); 62 $this->conf = new ConfigManager('tests/utils/config/configJson');
61 $this->conf->set('resource.datastore', self::$testDatastore); 63 $this->conf->set('resource.datastore', self::$testDatastore);
62 $this->refDB = new \ReferenceLinkDB(); 64 $this->refDB = new \ReferenceLinkDB();
@@ -65,7 +67,7 @@ class GetLinksTest extends \Shaarli\TestCase
65 67
66 $this->container = new Container(); 68 $this->container = new Container();
67 $this->container['conf'] = $this->conf; 69 $this->container['conf'] = $this->conf;
68 $this->container['db'] = new BookmarkFileService($this->conf, $history, true); 70 $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
69 $this->container['history'] = null; 71 $this->container['history'] = null;
70 72
71 $this->controller = new Links($this->container); 73 $this->controller = new Links($this->container);
@@ -396,7 +398,7 @@ class GetLinksTest extends \Shaarli\TestCase
396 $response = $this->controller->getLinks($request, new Response()); 398 $response = $this->controller->getLinks($request, new Response());
397 $this->assertEquals(200, $response->getStatusCode()); 399 $this->assertEquals(200, $response->getStatusCode());
398 $data = json_decode((string) $response->getBody(), true); 400 $data = json_decode((string) $response->getBody(), true);
399 $this->assertEquals(4, count($data)); 401 $this->assertEquals(5, count($data));
400 $this->assertEquals(6, $data[0]['id']); 402 $this->assertEquals(6, $data[0]['id']);
401 403
402 // wildcard: placeholder at the middle 404 // wildcard: placeholder at the middle
diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php
index fe3de66f..e12f803b 100644
--- a/tests/api/controllers/links/PostLinkTest.php
+++ b/tests/api/controllers/links/PostLinkTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
@@ -72,6 +73,7 @@ class PostLinkTest extends TestCase
72 */ 73 */
73 protected function setUp(): void 74 protected function setUp(): void
74 { 75 {
76 $mutex = new NoMutex();
75 $this->conf = new ConfigManager('tests/utils/config/configJson'); 77 $this->conf = new ConfigManager('tests/utils/config/configJson');
76 $this->conf->set('resource.datastore', self::$testDatastore); 78 $this->conf->set('resource.datastore', self::$testDatastore);
77 $this->refDB = new \ReferenceLinkDB(); 79 $this->refDB = new \ReferenceLinkDB();
@@ -79,7 +81,7 @@ class PostLinkTest extends TestCase
79 $refHistory = new \ReferenceHistory(); 81 $refHistory = new \ReferenceHistory();
80 $refHistory->write(self::$testHistory); 82 $refHistory->write(self::$testHistory);
81 $this->history = new History(self::$testHistory); 83 $this->history = new History(self::$testHistory);
82 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 84 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true);
83 85
84 $this->container = new Container(); 86 $this->container = new Container();
85 $this->container['conf'] = $this->conf; 87 $this->container['conf'] = $this->conf;
@@ -90,8 +92,8 @@ class PostLinkTest extends TestCase
90 92
91 $mock = $this->createMock(Router::class); 93 $mock = $this->createMock(Router::class);
92 $mock->expects($this->any()) 94 $mock->expects($this->any())
93 ->method('relativePathFor') 95 ->method('pathFor')
94 ->willReturn('api/v1/bookmarks/1'); 96 ->willReturn('/api/v1/bookmarks/1');
95 97
96 // affect @property-read... seems to work 98 // affect @property-read... seems to work
97 $this->controller->getCi()->router = $mock; 99 $this->controller->getCi()->router = $mock;
@@ -126,7 +128,7 @@ class PostLinkTest extends TestCase
126 128
127 $response = $this->controller->postLink($request, new Response()); 129 $response = $this->controller->postLink($request, new Response());
128 $this->assertEquals(201, $response->getStatusCode()); 130 $this->assertEquals(201, $response->getStatusCode());
129 $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); 131 $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
130 $data = json_decode((string) $response->getBody(), true); 132 $data = json_decode((string) $response->getBody(), true);
131 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 133 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
132 $this->assertEquals(43, $data['id']); 134 $this->assertEquals(43, $data['id']);
@@ -160,6 +162,8 @@ class PostLinkTest extends TestCase
160 'description' => 'shaare description', 162 'description' => 'shaare description',
161 'tags' => ['one', 'two'], 163 'tags' => ['one', 'two'],
162 'private' => true, 164 'private' => true,
165 'created' => '2015-05-05T12:30:00+03:00',
166 'updated' => '2016-06-05T14:32:10+03:00',
163 ]; 167 ];
164 $env = Environment::mock([ 168 $env = Environment::mock([
165 'REQUEST_METHOD' => 'POST', 169 'REQUEST_METHOD' => 'POST',
@@ -171,7 +175,7 @@ class PostLinkTest extends TestCase
171 $response = $this->controller->postLink($request, new Response()); 175 $response = $this->controller->postLink($request, new Response());
172 176
173 $this->assertEquals(201, $response->getStatusCode()); 177 $this->assertEquals(201, $response->getStatusCode());
174 $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); 178 $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
175 $data = json_decode((string) $response->getBody(), true); 179 $data = json_decode((string) $response->getBody(), true);
176 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 180 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
177 $this->assertEquals(43, $data['id']); 181 $this->assertEquals(43, $data['id']);
@@ -181,10 +185,8 @@ class PostLinkTest extends TestCase
181 $this->assertEquals($link['description'], $data['description']); 185 $this->assertEquals($link['description'], $data['description']);
182 $this->assertEquals($link['tags'], $data['tags']); 186 $this->assertEquals($link['tags'], $data['tags']);
183 $this->assertEquals(true, $data['private']); 187 $this->assertEquals(true, $data['private']);
184 $this->assertTrue( 188 $this->assertSame($link['created'], $data['created']);
185 new \DateTime('2 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 189 $this->assertSame($link['updated'], $data['updated']);
186 );
187 $this->assertEquals('', $data['updated']);
188 } 190 }
189 191
190 /** 192 /**
diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php
index a2e87c59..240ee323 100644
--- a/tests/api/controllers/links/PutLinkTest.php
+++ b/tests/api/controllers/links/PutLinkTest.php
@@ -3,6 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use malkusch\lock\mutex\NoMutex;
6use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
7use Shaarli\Bookmark\BookmarkFileService; 8use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
@@ -64,6 +65,7 @@ class PutLinkTest extends \Shaarli\TestCase
64 */ 65 */
65 protected function setUp(): void 66 protected function setUp(): void
66 { 67 {
68 $mutex = new NoMutex();
67 $this->conf = new ConfigManager('tests/utils/config/configJson'); 69 $this->conf = new ConfigManager('tests/utils/config/configJson');
68 $this->conf->set('resource.datastore', self::$testDatastore); 70 $this->conf->set('resource.datastore', self::$testDatastore);
69 $this->refDB = new \ReferenceLinkDB(); 71 $this->refDB = new \ReferenceLinkDB();
@@ -71,7 +73,7 @@ class PutLinkTest extends \Shaarli\TestCase
71 $refHistory = new \ReferenceHistory(); 73 $refHistory = new \ReferenceHistory();
72 $refHistory->write(self::$testHistory); 74 $refHistory->write(self::$testHistory);
73 $this->history = new History(self::$testHistory); 75 $this->history = new History(self::$testHistory);
74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 76 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true);
75 77
76 $this->container = new Container(); 78 $this->container = new Container();
77 $this->container['conf'] = $this->conf; 79 $this->container['conf'] = $this->conf;
diff --git a/tests/api/controllers/tags/DeleteTagTest.php b/tests/api/controllers/tags/DeleteTagTest.php
index 1326eb47..37f07229 100644
--- a/tests/api/controllers/tags/DeleteTagTest.php
+++ b/tests/api/controllers/tags/DeleteTagTest.php
@@ -3,6 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use malkusch\lock\mutex\NoMutex;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\LinkDB; 8use Shaarli\Bookmark\LinkDB;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
@@ -54,11 +55,15 @@ class DeleteTagTest extends \Shaarli\TestCase
54 */ 55 */
55 protected $controller; 56 protected $controller;
56 57
58 /** @var NoMutex */
59 protected $mutex;
60
57 /** 61 /**
58 * Before each test, instantiate a new Api with its config, plugins and bookmarks. 62 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
59 */ 63 */
60 protected function setUp(): void 64 protected function setUp(): void
61 { 65 {
66 $this->mutex = new NoMutex();
62 $this->conf = new ConfigManager('tests/utils/config/configJson'); 67 $this->conf = new ConfigManager('tests/utils/config/configJson');
63 $this->conf->set('resource.datastore', self::$testDatastore); 68 $this->conf->set('resource.datastore', self::$testDatastore);
64 $this->refDB = new \ReferenceLinkDB(); 69 $this->refDB = new \ReferenceLinkDB();
@@ -66,7 +71,7 @@ class DeleteTagTest extends \Shaarli\TestCase
66 $refHistory = new \ReferenceHistory(); 71 $refHistory = new \ReferenceHistory();
67 $refHistory->write(self::$testHistory); 72 $refHistory->write(self::$testHistory);
68 $this->history = new History(self::$testHistory); 73 $this->history = new History(self::$testHistory);
69 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
70 75
71 $this->container = new Container(); 76 $this->container = new Container();
72 $this->container['conf'] = $this->conf; 77 $this->container['conf'] = $this->conf;
@@ -102,7 +107,7 @@ class DeleteTagTest extends \Shaarli\TestCase
102 $this->assertEquals(204, $response->getStatusCode()); 107 $this->assertEquals(204, $response->getStatusCode());
103 $this->assertEmpty((string) $response->getBody()); 108 $this->assertEmpty((string) $response->getBody());
104 109
105 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 110 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
106 $tags = $this->bookmarkService->bookmarksCountPerTag(); 111 $tags = $this->bookmarkService->bookmarksCountPerTag();
107 $this->assertFalse(isset($tags[$tagName])); 112 $this->assertFalse(isset($tags[$tagName]));
108 113
@@ -136,7 +141,7 @@ class DeleteTagTest extends \Shaarli\TestCase
136 $this->assertEquals(204, $response->getStatusCode()); 141 $this->assertEquals(204, $response->getStatusCode());
137 $this->assertEmpty((string) $response->getBody()); 142 $this->assertEmpty((string) $response->getBody());
138 143
139 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 144 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
140 $tags = $this->bookmarkService->bookmarksCountPerTag(); 145 $tags = $this->bookmarkService->bookmarksCountPerTag();
141 $this->assertFalse(isset($tags[$tagName])); 146 $this->assertFalse(isset($tags[$tagName]));
142 $this->assertTrue($tags[strtolower($tagName)] > 0); 147 $this->assertTrue($tags[strtolower($tagName)] > 0);
diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php
index 9c05954b..878de5a4 100644
--- a/tests/api/controllers/tags/GetTagNameTest.php
+++ b/tests/api/controllers/tags/GetTagNameTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\BookmarkFileService; 6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\LinkDB; 7use Shaarli\Bookmark\LinkDB;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
@@ -55,6 +56,7 @@ class GetTagNameTest extends \Shaarli\TestCase
55 */ 56 */
56 protected function setUp(): void 57 protected function setUp(): void
57 { 58 {
59 $mutex = new NoMutex();
58 $this->conf = new ConfigManager('tests/utils/config/configJson'); 60 $this->conf = new ConfigManager('tests/utils/config/configJson');
59 $this->conf->set('resource.datastore', self::$testDatastore); 61 $this->conf->set('resource.datastore', self::$testDatastore);
60 $this->refDB = new \ReferenceLinkDB(); 62 $this->refDB = new \ReferenceLinkDB();
@@ -63,7 +65,7 @@ class GetTagNameTest extends \Shaarli\TestCase
63 65
64 $this->container = new Container(); 66 $this->container = new Container();
65 $this->container['conf'] = $this->conf; 67 $this->container['conf'] = $this->conf;
66 $this->container['db'] = new BookmarkFileService($this->conf, $history, true); 68 $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
67 $this->container['history'] = null; 69 $this->container['history'] = null;
68 70
69 $this->controller = new Tags($this->container); 71 $this->controller = new Tags($this->container);
diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php
index 3459fdfa..b565a8c4 100644
--- a/tests/api/controllers/tags/GetTagsTest.php
+++ b/tests/api/controllers/tags/GetTagsTest.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use malkusch\lock\mutex\NoMutex;
4use Shaarli\Bookmark\BookmarkFileService; 5use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Bookmark\LinkDB; 6use Shaarli\Bookmark\LinkDB;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
@@ -59,13 +60,14 @@ class GetTagsTest extends \Shaarli\TestCase
59 */ 60 */
60 protected function setUp(): void 61 protected function setUp(): void
61 { 62 {
63 $mutex = new NoMutex();
62 $this->conf = new ConfigManager('tests/utils/config/configJson'); 64 $this->conf = new ConfigManager('tests/utils/config/configJson');
63 $this->conf->set('resource.datastore', self::$testDatastore); 65 $this->conf->set('resource.datastore', self::$testDatastore);
64 $this->refDB = new \ReferenceLinkDB(); 66 $this->refDB = new \ReferenceLinkDB();
65 $this->refDB->write(self::$testDatastore); 67 $this->refDB->write(self::$testDatastore);
66 $history = new History('sandbox/history.php'); 68 $history = new History('sandbox/history.php');
67 69
68 $this->bookmarkService = new BookmarkFileService($this->conf, $history, true); 70 $this->bookmarkService = new BookmarkFileService($this->conf, $history, $mutex, true);
69 71
70 $this->container = new Container(); 72 $this->container = new Container();
71 $this->container['conf'] = $this->conf; 73 $this->container['conf'] = $this->conf;
diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php
index 74edde78..c73f6d3b 100644
--- a/tests/api/controllers/tags/PutTagTest.php
+++ b/tests/api/controllers/tags/PutTagTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Api\Exceptions\ApiBadParametersException; 6use Shaarli\Api\Exceptions\ApiBadParametersException;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\LinkDB; 8use Shaarli\Bookmark\LinkDB;
@@ -64,6 +65,7 @@ class PutTagTest extends \Shaarli\TestCase
64 */ 65 */
65 protected function setUp(): void 66 protected function setUp(): void
66 { 67 {
68 $mutex = new NoMutex();
67 $this->conf = new ConfigManager('tests/utils/config/configJson'); 69 $this->conf = new ConfigManager('tests/utils/config/configJson');
68 $this->conf->set('resource.datastore', self::$testDatastore); 70 $this->conf->set('resource.datastore', self::$testDatastore);
69 $this->refDB = new \ReferenceLinkDB(); 71 $this->refDB = new \ReferenceLinkDB();
@@ -71,7 +73,7 @@ class PutTagTest extends \Shaarli\TestCase
71 $refHistory = new \ReferenceHistory(); 73 $refHistory = new \ReferenceHistory();
72 $refHistory->write(self::$testHistory); 74 $refHistory->write(self::$testHistory);
73 $this->history = new History(self::$testHistory); 75 $this->history = new History(self::$testHistory);
74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 76 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true);
75 77
76 $this->container = new Container(); 78 $this->container = new Container();
77 $this->container['conf'] = $this->conf; 79 $this->container['conf'] = $this->conf;
diff --git a/tests/bookmark/BookmarkArrayTest.php b/tests/bookmark/BookmarkArrayTest.php
index ebed9bfc..1953078c 100644
--- a/tests/bookmark/BookmarkArrayTest.php
+++ b/tests/bookmark/BookmarkArrayTest.php
@@ -91,19 +91,6 @@ class BookmarkArrayTest extends TestCase
91 } 91 }
92 92
93 /** 93 /**
94 * Test adding a bad entry: invalid ID type
95 */
96 public function testArrayAccessAddBadEntryIdType()
97 {
98 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
99
100 $array = new BookmarkArray();
101 $bookmark = (new Bookmark())->setId('nope');
102 $bookmark->validate();
103 $array[] = $bookmark;
104 }
105
106 /**
107 * Test adding a bad entry: ID/offset not consistent 94 * Test adding a bad entry: ID/offset not consistent
108 */ 95 */
109 public function testArrayAccessAddBadEntryIdOffset() 96 public function testArrayAccessAddBadEntryIdOffset()
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php
index c399822b..f619aff3 100644
--- a/tests/bookmark/BookmarkFileServiceTest.php
+++ b/tests/bookmark/BookmarkFileServiceTest.php
@@ -6,6 +6,7 @@
6namespace Shaarli\Bookmark; 6namespace Shaarli\Bookmark;
7 7
8use DateTime; 8use DateTime;
9use malkusch\lock\mutex\NoMutex;
9use ReferenceLinkDB; 10use ReferenceLinkDB;
10use ReflectionClass; 11use ReflectionClass;
11use Shaarli; 12use Shaarli;
@@ -52,6 +53,9 @@ class BookmarkFileServiceTest extends TestCase
52 */ 53 */
53 protected $privateLinkDB = null; 54 protected $privateLinkDB = null;
54 55
56 /** @var NoMutex */
57 protected $mutex;
58
55 /** 59 /**
56 * Instantiates public and private LinkDBs with test data 60 * Instantiates public and private LinkDBs with test data
57 * 61 *
@@ -68,6 +72,8 @@ class BookmarkFileServiceTest extends TestCase
68 */ 72 */
69 protected function setUp(): void 73 protected function setUp(): void
70 { 74 {
75 $this->mutex = new NoMutex();
76
71 if (file_exists(self::$testDatastore)) { 77 if (file_exists(self::$testDatastore)) {
72 unlink(self::$testDatastore); 78 unlink(self::$testDatastore);
73 } 79 }
@@ -87,8 +93,8 @@ class BookmarkFileServiceTest extends TestCase
87 $this->refDB = new \ReferenceLinkDB(); 93 $this->refDB = new \ReferenceLinkDB();
88 $this->refDB->write(self::$testDatastore); 94 $this->refDB->write(self::$testDatastore);
89 $this->history = new History('sandbox/history.php'); 95 $this->history = new History('sandbox/history.php');
90 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, false); 96 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
91 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 97 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
92 } 98 }
93 99
94 /** 100 /**
@@ -105,7 +111,7 @@ class BookmarkFileServiceTest extends TestCase
105 $db = self::getMethod('migrate'); 111 $db = self::getMethod('migrate');
106 $db->invokeArgs($this->privateLinkDB, []); 112 $db->invokeArgs($this->privateLinkDB, []);
107 113
108 $db = new \FakeBookmarkService($this->conf, $this->history, true); 114 $db = new \FakeBookmarkService($this->conf, $this->history, $this->mutex, true);
109 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); 115 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
110 $this->assertEquals($this->refDB->countLinks(), $db->count()); 116 $this->assertEquals($this->refDB->countLinks(), $db->count());
111 } 117 }
@@ -174,7 +180,7 @@ class BookmarkFileServiceTest extends TestCase
174 $this->assertEquals($updated, $bookmark->getUpdated()); 180 $this->assertEquals($updated, $bookmark->getUpdated());
175 181
176 // reload from file 182 // reload from file
177 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 183 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
178 184
179 $bookmark = $this->privateLinkDB->get(43); 185 $bookmark = $this->privateLinkDB->get(43);
180 $this->assertEquals(43, $bookmark->getId()); 186 $this->assertEquals(43, $bookmark->getId());
@@ -212,7 +218,7 @@ class BookmarkFileServiceTest extends TestCase
212 $this->assertNull($bookmark->getUpdated()); 218 $this->assertNull($bookmark->getUpdated());
213 219
214 // reload from file 220 // reload from file
215 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 221 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
216 222
217 $bookmark = $this->privateLinkDB->get(43); 223 $bookmark = $this->privateLinkDB->get(43);
218 $this->assertEquals(43, $bookmark->getId()); 224 $this->assertEquals(43, $bookmark->getId());
@@ -242,7 +248,7 @@ class BookmarkFileServiceTest extends TestCase
242 $this->assertEquals(43, $bookmark->getId()); 248 $this->assertEquals(43, $bookmark->getId());
243 249
244 // reload from file 250 // reload from file
245 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 251 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
246 252
247 $this->privateLinkDB->get(43); 253 $this->privateLinkDB->get(43);
248 } 254 }
@@ -259,17 +265,6 @@ class BookmarkFileServiceTest extends TestCase
259 } 265 }
260 266
261 /** 267 /**
262 * Test add() method with an entry which is not a bookmark instance
263 */
264 public function testAddNotABookmark()
265 {
266 $this->expectException(\Exception::class);
267 $this->expectExceptionMessage('Provided data is invalid');
268
269 $this->privateLinkDB->add(['title' => 'hi!']);
270 }
271
272 /**
273 * Test add() method with a Bookmark already containing an ID 268 * Test add() method with a Bookmark already containing an ID
274 */ 269 */
275 public function testAddWithId() 270 public function testAddWithId()
@@ -314,7 +309,7 @@ class BookmarkFileServiceTest extends TestCase
314 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); 309 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
315 310
316 // reload from file 311 // reload from file
317 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 312 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
318 313
319 $bookmark = $this->privateLinkDB->get(42); 314 $bookmark = $this->privateLinkDB->get(42);
320 $this->assertEquals(42, $bookmark->getId()); 315 $this->assertEquals(42, $bookmark->getId());
@@ -355,7 +350,7 @@ class BookmarkFileServiceTest extends TestCase
355 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); 350 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
356 351
357 // reload from file 352 // reload from file
358 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 353 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
359 354
360 $bookmark = $this->privateLinkDB->get(42); 355 $bookmark = $this->privateLinkDB->get(42);
361 $this->assertEquals(42, $bookmark->getId()); 356 $this->assertEquals(42, $bookmark->getId());
@@ -388,7 +383,7 @@ class BookmarkFileServiceTest extends TestCase
388 $this->assertEquals($title, $bookmark->getTitle()); 383 $this->assertEquals($title, $bookmark->getTitle());
389 384
390 // reload from file 385 // reload from file
391 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 386 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
392 387
393 $bookmark = $this->privateLinkDB->get(42); 388 $bookmark = $this->privateLinkDB->get(42);
394 $this->assertEquals(42, $bookmark->getId()); 389 $this->assertEquals(42, $bookmark->getId());
@@ -407,17 +402,6 @@ class BookmarkFileServiceTest extends TestCase
407 } 402 }
408 403
409 /** 404 /**
410 * Test set() method with an entry which is not a bookmark instance
411 */
412 public function testSetNotABookmark()
413 {
414 $this->expectException(\Exception::class);
415 $this->expectExceptionMessage('Provided data is invalid');
416
417 $this->privateLinkDB->set(['title' => 'hi!']);
418 }
419
420 /**
421 * Test set() method with a Bookmark without an ID defined. 405 * Test set() method with a Bookmark without an ID defined.
422 */ 406 */
423 public function testSetWithoutId() 407 public function testSetWithoutId()
@@ -452,7 +436,7 @@ class BookmarkFileServiceTest extends TestCase
452 $this->assertEquals(43, $bookmark->getId()); 436 $this->assertEquals(43, $bookmark->getId());
453 437
454 // reload from file 438 // reload from file
455 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 439 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
456 440
457 $bookmark = $this->privateLinkDB->get(43); 441 $bookmark = $this->privateLinkDB->get(43);
458 $this->assertEquals(43, $bookmark->getId()); 442 $this->assertEquals(43, $bookmark->getId());
@@ -472,7 +456,7 @@ class BookmarkFileServiceTest extends TestCase
472 $this->assertEquals($title, $bookmark->getTitle()); 456 $this->assertEquals($title, $bookmark->getTitle());
473 457
474 // reload from file 458 // reload from file
475 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 459 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
476 460
477 $bookmark = $this->privateLinkDB->get(42); 461 $bookmark = $this->privateLinkDB->get(42);
478 $this->assertEquals(42, $bookmark->getId()); 462 $this->assertEquals(42, $bookmark->getId());
@@ -491,17 +475,6 @@ class BookmarkFileServiceTest extends TestCase
491 } 475 }
492 476
493 /** 477 /**
494 * Test addOrSet() method with an entry which is not a bookmark instance
495 */
496 public function testAddOrSetNotABookmark()
497 {
498 $this->expectException(\Exception::class);
499 $this->expectExceptionMessage('Provided data is invalid');
500
501 $this->privateLinkDB->addOrSet(['title' => 'hi!']);
502 }
503
504 /**
505 * Test addOrSet() method for a bookmark without any field set and without writing the data store 478 * Test addOrSet() method for a bookmark without any field set and without writing the data store
506 */ 479 */
507 public function testAddOrSetMinimalNoWrite() 480 public function testAddOrSetMinimalNoWrite()
@@ -515,7 +488,7 @@ class BookmarkFileServiceTest extends TestCase
515 $this->assertEquals($title, $bookmark->getTitle()); 488 $this->assertEquals($title, $bookmark->getTitle());
516 489
517 // reload from file 490 // reload from file
518 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 491 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
519 492
520 $bookmark = $this->privateLinkDB->get(42); 493 $bookmark = $this->privateLinkDB->get(42);
521 $this->assertEquals(42, $bookmark->getId()); 494 $this->assertEquals(42, $bookmark->getId());
@@ -541,7 +514,7 @@ class BookmarkFileServiceTest extends TestCase
541 $this->assertInstanceOf(BookmarkNotFoundException::class, $exception); 514 $this->assertInstanceOf(BookmarkNotFoundException::class, $exception);
542 515
543 // reload from file 516 // reload from file
544 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 517 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
545 518
546 $this->privateLinkDB->get(42); 519 $this->privateLinkDB->get(42);
547 } 520 }
@@ -559,17 +532,6 @@ class BookmarkFileServiceTest extends TestCase
559 } 532 }
560 533
561 /** 534 /**
562 * Test remove() method with an entry which is not a bookmark instance
563 */
564 public function testRemoveNotABookmark()
565 {
566 $this->expectException(\Exception::class);
567 $this->expectExceptionMessage('Provided data is invalid');
568
569 $this->privateLinkDB->remove(['title' => 'hi!']);
570 }
571
572 /**
573 * Test remove() method with a Bookmark with an unknown ID 535 * Test remove() method with a Bookmark with an unknown ID
574 */ 536 */
575 public function testRemoveWithUnknownId() 537 public function testRemoveWithUnknownId()
@@ -645,7 +607,7 @@ class BookmarkFileServiceTest extends TestCase
645 607
646 $conf = new ConfigManager('tests/utils/config/configJson'); 608 $conf = new ConfigManager('tests/utils/config/configJson');
647 $conf->set('resource.datastore', 'null/store.db'); 609 $conf->set('resource.datastore', 'null/store.db');
648 new BookmarkFileService($conf, $this->history, true); 610 new BookmarkFileService($conf, $this->history, $this->mutex, true);
649 } 611 }
650 612
651 /** 613 /**
@@ -655,7 +617,7 @@ class BookmarkFileServiceTest extends TestCase
655 { 617 {
656 unlink(self::$testDatastore); 618 unlink(self::$testDatastore);
657 $this->assertFileNotExists(self::$testDatastore); 619 $this->assertFileNotExists(self::$testDatastore);
658 new BookmarkFileService($this->conf, $this->history, true); 620 new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
659 $this->assertFileExists(self::$testDatastore); 621 $this->assertFileExists(self::$testDatastore);
660 622
661 // ensure the correct data has been written 623 // ensure the correct data has been written
@@ -669,7 +631,7 @@ class BookmarkFileServiceTest extends TestCase
669 { 631 {
670 unlink(self::$testDatastore); 632 unlink(self::$testDatastore);
671 $this->assertFileNotExists(self::$testDatastore); 633 $this->assertFileNotExists(self::$testDatastore);
672 $db = new \FakeBookmarkService($this->conf, $this->history, false); 634 $db = new \FakeBookmarkService($this->conf, $this->history, $this->mutex, false);
673 $this->assertFileNotExists(self::$testDatastore); 635 $this->assertFileNotExists(self::$testDatastore);
674 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); 636 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
675 $this->assertCount(0, $db->getBookmarks()); 637 $this->assertCount(0, $db->getBookmarks());
@@ -702,13 +664,13 @@ class BookmarkFileServiceTest extends TestCase
702 */ 664 */
703 public function testSave() 665 public function testSave()
704 { 666 {
705 $testDB = new BookmarkFileService($this->conf, $this->history, true); 667 $testDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
706 $dbSize = $testDB->count(); 668 $dbSize = $testDB->count();
707 669
708 $bookmark = new Bookmark(); 670 $bookmark = new Bookmark();
709 $testDB->add($bookmark); 671 $testDB->add($bookmark);
710 672
711 $testDB = new BookmarkFileService($this->conf, $this->history, true); 673 $testDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
712 $this->assertEquals($dbSize + 1, $testDB->count()); 674 $this->assertEquals($dbSize + 1, $testDB->count());
713 } 675 }
714 676
@@ -718,28 +680,12 @@ class BookmarkFileServiceTest extends TestCase
718 public function testCountHiddenPublic() 680 public function testCountHiddenPublic()
719 { 681 {
720 $this->conf->set('privacy.hide_public_links', true); 682 $this->conf->set('privacy.hide_public_links', true);
721 $linkDB = new BookmarkFileService($this->conf, $this->history, false); 683 $linkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
722 684
723 $this->assertEquals(0, $linkDB->count()); 685 $this->assertEquals(0, $linkDB->count());
724 } 686 }
725 687
726 /** 688 /**
727 * List the days for which bookmarks have been posted
728 */
729 public function testDays()
730 {
731 $this->assertEquals(
732 ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'],
733 $this->publicLinkDB->days()
734 );
735
736 $this->assertEquals(
737 ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'],
738 $this->privateLinkDB->days()
739 );
740 }
741
742 /**
743 * The URL corresponds to an existing entry in the DB 689 * The URL corresponds to an existing entry in the DB
744 */ 690 */
745 public function testGetKnownLinkFromURL() 691 public function testGetKnownLinkFromURL()
@@ -786,6 +732,10 @@ class BookmarkFileServiceTest extends TestCase
786 // They need to be grouped with the first case found - order by date DESC: `sTuff`. 732 // They need to be grouped with the first case found - order by date DESC: `sTuff`.
787 'sTuff' => 2, 733 'sTuff' => 2,
788 'ut' => 1, 734 'ut' => 1,
735 'assurance' => 1,
736 'coding-style' => 1,
737 'quality' => 1,
738 'standards' => 1,
789 ], 739 ],
790 $this->publicLinkDB->bookmarksCountPerTag() 740 $this->publicLinkDB->bookmarksCountPerTag()
791 ); 741 );
@@ -814,6 +764,10 @@ class BookmarkFileServiceTest extends TestCase
814 'tag3' => 1, 764 'tag3' => 1,
815 'tag4' => 1, 765 'tag4' => 1,
816 'ut' => 1, 766 'ut' => 1,
767 'assurance' => 1,
768 'coding-style' => 1,
769 'quality' => 1,
770 'standards' => 1,
817 ], 771 ],
818 $this->privateLinkDB->bookmarksCountPerTag() 772 $this->privateLinkDB->bookmarksCountPerTag()
819 ); 773 );
@@ -928,6 +882,37 @@ class BookmarkFileServiceTest extends TestCase
928 } 882 }
929 883
930 /** 884 /**
885 * Test filterHash() on a private bookmark while logged out.
886 */
887 public function testFilterHashPrivateWhileLoggedOut()
888 {
889 $this->expectException(BookmarkNotFoundException::class);
890 $this->expectExceptionMessage('The link you are trying to reach does not exist or has been deleted');
891
892 $hash = smallHash('20141125_084734' . 6);
893
894 $this->publicLinkDB->findByHash($hash);
895 }
896
897 /**
898 * Test filterHash() with private key.
899 */
900 public function testFilterHashWithPrivateKey()
901 {
902 $hash = smallHash('20141125_084734' . 6);
903 $privateKey = 'this is usually auto generated';
904
905 $bookmark = $this->privateLinkDB->findByHash($hash);
906 $bookmark->addAdditionalContentEntry('private_key', $privateKey);
907 $this->privateLinkDB->save();
908
909 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
910 $bookmark = $this->privateLinkDB->findByHash($hash, $privateKey);
911
912 static::assertSame(6, $bookmark->getId());
913 }
914
915 /**
931 * Test linksCountPerTag all tags without filter. 916 * Test linksCountPerTag all tags without filter.
932 * Equal occurrences should be sorted alphabetically. 917 * Equal occurrences should be sorted alphabetically.
933 */ 918 */
@@ -956,6 +941,10 @@ class BookmarkFileServiceTest extends TestCase
956 'tag4' => 1, 941 'tag4' => 1,
957 'ut' => 1, 942 'ut' => 1,
958 'w3c' => 1, 943 'w3c' => 1,
944 'assurance' => 1,
945 'coding-style' => 1,
946 'quality' => 1,
947 'standards' => 1,
959 ]; 948 ];
960 $tags = $this->privateLinkDB->bookmarksCountPerTag(); 949 $tags = $this->privateLinkDB->bookmarksCountPerTag();
961 950
@@ -1054,6 +1043,10 @@ class BookmarkFileServiceTest extends TestCase
1054 'stallman' => 1, 1043 'stallman' => 1,
1055 'ut' => 1, 1044 'ut' => 1,
1056 'w3c' => 1, 1045 'w3c' => 1,
1046 'assurance' => 1,
1047 'coding-style' => 1,
1048 'quality' => 1,
1049 'standards' => 1,
1057 ]; 1050 ];
1058 $bookmark = new Bookmark(); 1051 $bookmark = new Bookmark();
1059 $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]); 1052 $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]);
@@ -1065,33 +1058,105 @@ class BookmarkFileServiceTest extends TestCase
1065 } 1058 }
1066 1059
1067 /** 1060 /**
1068 * Test filterDay while logged in 1061 * Test find by dates in the middle of the datastore (sorted by dates) with a single bookmark as a result.
1069 */ 1062 */
1070 public function testFilterDayLoggedIn(): void 1063 public function testFilterByDateMidTimePeriodSingleBookmark(): void
1071 { 1064 {
1072 $bookmarks = $this->privateLinkDB->filterDay('20121206'); 1065 $bookmarks = $this->privateLinkDB->findByDate(
1073 $expectedIds = [4, 9, 1, 0]; 1066 DateTime::createFromFormat('Ymd_His', '20121206_150000'),
1067 DateTime::createFromFormat('Ymd_His', '20121206_160000'),
1068 $before,
1069 $after
1070 );
1074 1071
1075 static::assertCount(4, $bookmarks); 1072 static::assertCount(1, $bookmarks);
1076 foreach ($bookmarks as $bookmark) { 1073
1077 $i = ($i ?? -1) + 1; 1074 static::assertSame(9, $bookmarks[0]->getId());
1078 static::assertSame($expectedIds[$i], $bookmark->getId()); 1075 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
1079 } 1076 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after);
1080 } 1077 }
1081 1078
1082 /** 1079 /**
1083 * Test filterDay while logged out 1080 * Test find by dates in the middle of the datastore (sorted by dates) with a multiple bookmarks as a result.
1084 */ 1081 */
1085 public function testFilterDayLoggedOut(): void 1082 public function testFilterByDateMidTimePeriodMultipleBookmarks(): void
1086 { 1083 {
1087 $bookmarks = $this->publicLinkDB->filterDay('20121206'); 1084 $bookmarks = $this->privateLinkDB->findByDate(
1088 $expectedIds = [4, 9, 1]; 1085 DateTime::createFromFormat('Ymd_His', '20121206_150000'),
1086 DateTime::createFromFormat('Ymd_His', '20121206_180000'),
1087 $before,
1088 $after
1089 );
1089 1090
1090 static::assertCount(3, $bookmarks); 1091 static::assertCount(2, $bookmarks);
1091 foreach ($bookmarks as $bookmark) { 1092
1092 $i = ($i ?? -1) + 1; 1093 static::assertSame(1, $bookmarks[0]->getId());
1093 static::assertSame($expectedIds[$i], $bookmark->getId()); 1094 static::assertSame(9, $bookmarks[1]->getId());
1094 } 1095 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
1096 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_182539'), $after);
1097 }
1098
1099 /**
1100 * Test find by dates at the end of the datastore (sorted by dates).
1101 */
1102 public function testFilterByDateLastTimePeriod(): void
1103 {
1104 $after = new DateTime();
1105 $bookmarks = $this->privateLinkDB->findByDate(
1106 DateTime::createFromFormat('Ymd_His', '20150310_114640'),
1107 DateTime::createFromFormat('Ymd_His', '20450101_010101'),
1108 $before,
1109 $after
1110 );
1111
1112 static::assertCount(1, $bookmarks);
1113
1114 static::assertSame(41, $bookmarks[0]->getId());
1115 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20150310_114633'), $before);
1116 static::assertNull($after);
1117 }
1118
1119 /**
1120 * Test find by dates at the beginning of the datastore (sorted by dates).
1121 */
1122 public function testFilterByDateFirstTimePeriod(): void
1123 {
1124 $before = new DateTime();
1125 $bookmarks = $this->privateLinkDB->findByDate(
1126 DateTime::createFromFormat('Ymd_His', '20000101_101010'),
1127 DateTime::createFromFormat('Ymd_His', '20100309_110000'),
1128 $before,
1129 $after
1130 );
1131
1132 static::assertCount(1, $bookmarks);
1133
1134 static::assertSame(11, $bookmarks[0]->getId());
1135 static::assertNull($before);
1136 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20100310_101010'), $after);
1137 }
1138
1139 /**
1140 * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
1141 */
1142 public function testGetLatestWithSticky(): void
1143 {
1144 $bookmark = $this->publicLinkDB->getLatest();
1145
1146 static::assertSame(41, $bookmark->getId());
1147 }
1148
1149 /**
1150 * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
1151 */
1152 public function testGetLatestEmptyDatastore(): void
1153 {
1154 unlink($this->conf->get('resource.datastore'));
1155 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
1156
1157 $bookmark = $this->publicLinkDB->getLatest();
1158
1159 static::assertNull($bookmark);
1095 } 1160 }
1096 1161
1097 /** 1162 /**
diff --git a/tests/bookmark/BookmarkFilterTest.php b/tests/bookmark/BookmarkFilterTest.php
index 48c7f824..835674f2 100644
--- a/tests/bookmark/BookmarkFilterTest.php
+++ b/tests/bookmark/BookmarkFilterTest.php
@@ -2,7 +2,7 @@
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use Exception; 5use malkusch\lock\mutex\NoMutex;
6use ReferenceLinkDB; 6use ReferenceLinkDB;
7use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History; 8use Shaarli\History;
@@ -37,13 +37,14 @@ class BookmarkFilterTest extends TestCase
37 */ 37 */
38 public static function setUpBeforeClass(): void 38 public static function setUpBeforeClass(): void
39 { 39 {
40 $mutex = new NoMutex();
40 $conf = new ConfigManager('tests/utils/config/configJson'); 41 $conf = new ConfigManager('tests/utils/config/configJson');
41 $conf->set('resource.datastore', self::$testDatastore); 42 $conf->set('resource.datastore', self::$testDatastore);
42 self::$refDB = new \ReferenceLinkDB(); 43 self::$refDB = new \ReferenceLinkDB();
43 self::$refDB->write(self::$testDatastore); 44 self::$refDB->write(self::$testDatastore);
44 $history = new History('sandbox/history.php'); 45 $history = new History('sandbox/history.php');
45 self::$bookmarkService = new \FakeBookmarkService($conf, $history, true); 46 self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true);
46 self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks()); 47 self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks(), $conf);
47 } 48 }
48 49
49 /** 50 /**
@@ -523,4 +524,43 @@ class BookmarkFilterTest extends TestCase
523 )) 524 ))
524 ); 525 );
525 } 526 }
527
528 /**
529 * Test search result highlights in every field of bookmark reference #9.
530 */
531 public function testFullTextSearchHighlight(): void
532 {
533 $bookmarks = self::$linkFilter->filter(
534 BookmarkFilter::$FILTER_TEXT,
535 '"psr-2" coding guide http fig "psr-2/" "This guide" basic standard. coding-style quality assurance'
536 );
537
538 static::assertCount(1, $bookmarks);
539 static::assertArrayHasKey(9, $bookmarks);
540
541 $bookmark = $bookmarks[9];
542 $expectedHighlights = [
543 'title' => [
544 ['start' => 0, 'end' => 5], // "psr-2"
545 ['start' => 7, 'end' => 13], // coding
546 ['start' => 20, 'end' => 25], // guide
547 ],
548 'description' => [
549 ['start' => 0, 'end' => 10], // "This guide"
550 ['start' => 45, 'end' => 50], // basic
551 ['start' => 58, 'end' => 67], // standard.
552 ],
553 'url' => [
554 ['start' => 0, 'end' => 4], // http
555 ['start' => 15, 'end' => 18], // fig
556 ['start' => 27, 'end' => 33], // "psr-2/"
557 ],
558 'tags' => [
559 ['start' => 0, 'end' => 12], // coding-style
560 ['start' => 23, 'end' => 30], // quality
561 ['start' => 31, 'end' => 40], // assurance
562 ],
563 ];
564 static::assertSame($expectedHighlights, $bookmark->getAdditionalContentEntry('search_highlight'));
565 }
526} 566}
diff --git a/tests/bookmark/BookmarkInitializerTest.php b/tests/bookmark/BookmarkInitializerTest.php
index 25704004..0c8420ce 100644
--- a/tests/bookmark/BookmarkInitializerTest.php
+++ b/tests/bookmark/BookmarkInitializerTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
6use Shaarli\History; 7use Shaarli\History;
7use Shaarli\TestCase; 8use Shaarli\TestCase;
@@ -34,11 +35,15 @@ class BookmarkInitializerTest extends TestCase
34 /** @var BookmarkInitializer instance */ 35 /** @var BookmarkInitializer instance */
35 protected $initializer; 36 protected $initializer;
36 37
38 /** @var NoMutex */
39 protected $mutex;
40
37 /** 41 /**
38 * Initialize an empty BookmarkFileService 42 * Initialize an empty BookmarkFileService
39 */ 43 */
40 public function setUp(): void 44 public function setUp(): void
41 { 45 {
46 $this->mutex = new NoMutex();
42 if (file_exists(self::$testDatastore)) { 47 if (file_exists(self::$testDatastore)) {
43 unlink(self::$testDatastore); 48 unlink(self::$testDatastore);
44 } 49 }
@@ -47,7 +52,7 @@ class BookmarkInitializerTest extends TestCase
47 $this->conf = new ConfigManager(self::$testConf); 52 $this->conf = new ConfigManager(self::$testConf);
48 $this->conf->set('resource.datastore', self::$testDatastore); 53 $this->conf->set('resource.datastore', self::$testDatastore);
49 $this->history = new History('sandbox/history.php'); 54 $this->history = new History('sandbox/history.php');
50 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 55 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
51 56
52 $this->initializer = new BookmarkInitializer($this->bookmarkService); 57 $this->initializer = new BookmarkInitializer($this->bookmarkService);
53 } 58 }
@@ -59,7 +64,7 @@ class BookmarkInitializerTest extends TestCase
59 { 64 {
60 $refDB = new \ReferenceLinkDB(); 65 $refDB = new \ReferenceLinkDB();
61 $refDB->write(self::$testDatastore); 66 $refDB->write(self::$testDatastore);
62 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 67 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
63 $this->initializer = new BookmarkInitializer($this->bookmarkService); 68 $this->initializer = new BookmarkInitializer($this->bookmarkService);
64 69
65 $this->initializer->initialize(); 70 $this->initializer->initialize();
@@ -90,7 +95,7 @@ class BookmarkInitializerTest extends TestCase
90 $this->bookmarkService->save(); 95 $this->bookmarkService->save();
91 96
92 // Reload from file 97 // Reload from file
93 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 98 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
94 $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count()); 99 $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count());
95 100
96 $bookmark = $this->bookmarkService->get(43); 101 $bookmark = $this->bookmarkService->get(43);
@@ -121,7 +126,7 @@ class BookmarkInitializerTest extends TestCase
121 public function testInitializeNonExistentDataStore(): void 126 public function testInitializeNonExistentDataStore(): void
122 { 127 {
123 $this->conf->set('resource.datastore', static::$testDatastore . '_empty'); 128 $this->conf->set('resource.datastore', static::$testDatastore . '_empty');
124 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 129 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
125 130
126 $this->initializer->initialize(); 131 $this->initializer->initialize();
127 132
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php
index afec2440..cb91b26b 100644
--- a/tests/bookmark/BookmarkTest.php
+++ b/tests/bookmark/BookmarkTest.php
@@ -79,6 +79,23 @@ class BookmarkTest extends TestCase
79 } 79 }
80 80
81 /** 81 /**
82 * Test fromArray() with a link with a custom tags separator
83 */
84 public function testFromArrayCustomTagsSeparator()
85 {
86 $data = [
87 'id' => 1,
88 'tags' => ['tag1', 'tag2', 'chair'],
89 ];
90
91 $bookmark = (new Bookmark())->fromArray($data, '@');
92 $this->assertEquals($data['id'], $bookmark->getId());
93 $this->assertEquals($data['tags'], $bookmark->getTags());
94 $this->assertEquals('tag1@tag2@chair', $bookmark->getTagsString('@'));
95 }
96
97
98 /**
82 * Test validate() with a valid minimal bookmark 99 * Test validate() with a valid minimal bookmark
83 */ 100 */
84 public function testValidateValidFullBookmark() 101 public function testValidateValidFullBookmark()
@@ -154,25 +171,6 @@ class BookmarkTest extends TestCase
154 } 171 }
155 172
156 /** 173 /**
157 * Test validate() with a a bookmark with a non integer ID.
158 */
159 public function testValidateNotValidStringId()
160 {
161 $bookmark = new Bookmark();
162 $bookmark->setId('str');
163 $bookmark->setShortUrl('abc');
164 $bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
165 $exception = null;
166 try {
167 $bookmark->validate();
168 } catch (InvalidBookmarkException $e) {
169 $exception = $e;
170 }
171 $this->assertNotNull($exception);
172 $this->assertContainsPolyfill('- ID: str'. PHP_EOL, $exception->getMessage());
173 }
174
175 /**
176 * Test validate() with a a bookmark without short url. 174 * Test validate() with a a bookmark without short url.
177 */ 175 */
178 public function testValidateNotValidNoShortUrl() 176 public function testValidateNotValidNoShortUrl()
@@ -211,25 +209,6 @@ class BookmarkTest extends TestCase
211 } 209 }
212 210
213 /** 211 /**
214 * Test validate() with a a bookmark with a bad created datetime.
215 */
216 public function testValidateNotValidBadCreated()
217 {
218 $bookmark = new Bookmark();
219 $bookmark->setId(1);
220 $bookmark->setShortUrl('abc');
221 $bookmark->setCreated('hi!');
222 $exception = null;
223 try {
224 $bookmark->validate();
225 } catch (InvalidBookmarkException $e) {
226 $exception = $e;
227 }
228 $this->assertNotNull($exception);
229 $this->assertContainsPolyfill('- Created: Not a DateTime object'. PHP_EOL, $exception->getMessage());
230 }
231
232 /**
233 * Test setId() and make sure that default fields are generated. 212 * Test setId() and make sure that default fields are generated.
234 */ 213 */
235 public function testSetIdEmptyGeneratedFields() 214 public function testSetIdEmptyGeneratedFields()
@@ -290,7 +269,7 @@ class BookmarkTest extends TestCase
290 { 269 {
291 $bookmark = new Bookmark(); 270 $bookmark = new Bookmark();
292 271
293 $str = 'tag1 tag2 tag3.tag3-2, tag4 , -tag5 '; 272 $str = 'tag1 tag2 tag3.tag3-2 tag4 -tag5 ';
294 $bookmark->setTagsString($str); 273 $bookmark->setTagsString($str);
295 $this->assertEquals( 274 $this->assertEquals(
296 [ 275 [
@@ -314,9 +293,9 @@ class BookmarkTest extends TestCase
314 $array = [ 293 $array = [
315 'tag1 ', 294 'tag1 ',
316 ' tag2', 295 ' tag2',
317 'tag3.tag3-2,', 296 'tag3.tag3-2',
318 ', tag4', 297 ' tag4',
319 ', ', 298 ' ',
320 '-tag5 ', 299 '-tag5 ',
321 ]; 300 ];
322 $bookmark->setTags($array); 301 $bookmark->setTags($array);
@@ -385,4 +364,48 @@ class BookmarkTest extends TestCase
385 $bookmark->deleteTag('nope'); 364 $bookmark->deleteTag('nope');
386 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); 365 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
387 } 366 }
367
368 /**
369 * Test shouldUpdateThumbnail() with bookmarks needing an update.
370 */
371 public function testShouldUpdateThumbnail(): void
372 {
373 $bookmark = (new Bookmark())->setUrl('http://domain.tld/with-image');
374
375 static::assertTrue($bookmark->shouldUpdateThumbnail());
376
377 $bookmark = (new Bookmark())
378 ->setUrl('http://domain.tld/with-image')
379 ->setThumbnail('unknown file')
380 ;
381
382 static::assertTrue($bookmark->shouldUpdateThumbnail());
383 }
384
385 /**
386 * Test shouldUpdateThumbnail() with bookmarks that should not update.
387 */
388 public function testShouldNotUpdateThumbnail(): void
389 {
390 $bookmark = (new Bookmark());
391
392 static::assertFalse($bookmark->shouldUpdateThumbnail());
393
394 $bookmark = (new Bookmark())
395 ->setUrl('ftp://domain.tld/other-protocol', ['ftp'])
396 ;
397
398 static::assertFalse($bookmark->shouldUpdateThumbnail());
399
400 $bookmark = (new Bookmark())
401 ->setUrl('http://domain.tld/with-image')
402 ->setThumbnail(__FILE__)
403 ;
404
405 static::assertFalse($bookmark->shouldUpdateThumbnail());
406
407 $bookmark = (new Bookmark())->setUrl('/shaare/abcdef');
408
409 static::assertFalse($bookmark->shouldUpdateThumbnail());
410 }
388} 411}
diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php
index ef00b92f..ddab4e3c 100644
--- a/tests/bookmark/LinkUtilsTest.php
+++ b/tests/bookmark/LinkUtilsTest.php
@@ -94,8 +94,108 @@ class LinkUtilsTest extends TestCase
94 public function testHtmlExtractExistentNameTag() 94 public function testHtmlExtractExistentNameTag()
95 { 95 {
96 $description = 'Bob and Alice share cookies.'; 96 $description = 'Bob and Alice share cookies.';
97
98 // Simple one line
97 $html = '<html><meta>stuff2</meta><meta name="description" content="' . $description . '"/></html>'; 99 $html = '<html><meta>stuff2</meta><meta name="description" content="' . $description . '"/></html>';
98 $this->assertEquals($description, html_extract_tag('description', $html)); 100 $this->assertEquals($description, html_extract_tag('description', $html));
101
102 // Simple OpenGraph
103 $html = '<meta property="og:description" content="' . $description . '">';
104 $this->assertEquals($description, html_extract_tag('description', $html));
105
106 // Simple reversed OpenGraph
107 $html = '<meta content="' . $description . '" property="og:description">';
108 $this->assertEquals($description, html_extract_tag('description', $html));
109
110 // ItemProp OpenGraph
111 $html = '<meta itemprop="og:description" content="' . $description . '">';
112 $this->assertEquals($description, html_extract_tag('description', $html));
113
114 // OpenGraph without quotes
115 $html = '<meta property=og:description content="' . $description . '">';
116 $this->assertEquals($description, html_extract_tag('description', $html));
117
118 // OpenGraph reversed without quotes
119 $html = '<meta content="' . $description . '" property=og:description>';
120 $this->assertEquals($description, html_extract_tag('description', $html));
121
122 // OpenGraph with noise
123 $html = '<meta tag1="content1" property="og:description" tag2="content2" content="' .
124 $description . '" tag3="content3">';
125 $this->assertEquals($description, html_extract_tag('description', $html));
126
127 // OpenGraph reversed with noise
128 $html = '<meta tag1="content1" content="' . $description . '" ' .
129 'tag3="content3" tag2="content2" property="og:description">';
130 $this->assertEquals($description, html_extract_tag('description', $html));
131
132 // OpenGraph multiple properties start
133 $html = '<meta property="unrelated og:description" content="' . $description . '">';
134 $this->assertEquals($description, html_extract_tag('description', $html));
135
136 // OpenGraph multiple properties end
137 $html = '<meta property="og:description unrelated" content="' . $description . '">';
138 $this->assertEquals($description, html_extract_tag('description', $html));
139
140 // OpenGraph multiple properties both end
141 $html = '<meta property="og:unrelated1 og:description og:unrelated2" content="' . $description . '">';
142 $this->assertEquals($description, html_extract_tag('description', $html));
143
144 // OpenGraph multiple properties both end with noise
145 $html = '<meta tag1="content1" property="og:unrelated1 og:description og:unrelated2" '.
146 'tag2="content2" content="' . $description . '" tag3="content3">';
147 $this->assertEquals($description, html_extract_tag('description', $html));
148
149 // OpenGraph reversed multiple properties start
150 $html = '<meta content="' . $description . '" property="unrelated og:description">';
151 $this->assertEquals($description, html_extract_tag('description', $html));
152
153 // OpenGraph reversed multiple properties end
154 $html = '<meta content="' . $description . '" property="og:description unrelated">';
155 $this->assertEquals($description, html_extract_tag('description', $html));
156
157 // OpenGraph reversed multiple properties both end
158 $html = '<meta content="' . $description . '" property="og:unrelated1 og:description og:unrelated2">';
159 $this->assertEquals($description, html_extract_tag('description', $html));
160
161 // OpenGraph reversed multiple properties both end with noise
162 $html = '<meta tag1="content1" content="' . $description . '" tag2="content2" '.
163 'property="og:unrelated1 og:description og:unrelated2" tag3="content3">';
164 $this->assertEquals($description, html_extract_tag('description', $html));
165
166 // Suggestion from #1375
167 $html = '<meta property="og:description" name="description" content="' . $description . '">';
168 $this->assertEquals($description, html_extract_tag('description', $html));
169 }
170
171 /**
172 * Test html_extract_tag() with double quoted content containing single quote, and the opposite.
173 */
174 public function testHtmlExtractExistentNameTagWithMixedQuotes(): void
175 {
176 $description = 'Bob and Alice share M&M\'s.';
177
178 $html = '<meta property="og:description" content="' . $description . '">';
179 $this->assertEquals($description, html_extract_tag('description', $html));
180
181 $html = '<meta tag1="content1" property="og:unrelated1 og:description og:unrelated2" '.
182 'tag2="content2" content="' . $description . '" tag3="content3">';
183 $this->assertEquals($description, html_extract_tag('description', $html));
184
185 $html = '<meta property="og:description" name="description" content="' . $description . '">';
186 $this->assertEquals($description, html_extract_tag('description', $html));
187
188 $description = 'Bob and Alice share "cookies".';
189
190 $html = '<meta property="og:description" content=\'' . $description . '\'>';
191 $this->assertEquals($description, html_extract_tag('description', $html));
192
193 $html = '<meta tag1="content1" property="og:unrelated1 og:description og:unrelated2" '.
194 'tag2="content2" content=\'' . $description . '\' tag3="content3">';
195 $this->assertEquals($description, html_extract_tag('description', $html));
196
197 $html = '<meta property="og:description" name="description" content=\'' . $description . '\'>';
198 $this->assertEquals($description, html_extract_tag('description', $html));
99 } 199 }
100 200
101 /** 201 /**
@@ -105,6 +205,25 @@ class LinkUtilsTest extends TestCase
105 { 205 {
106 $html = '<html><meta>stuff2</meta><meta name="image" content="img"/></html>'; 206 $html = '<html><meta>stuff2</meta><meta name="image" content="img"/></html>';
107 $this->assertFalse(html_extract_tag('description', $html)); 207 $this->assertFalse(html_extract_tag('description', $html));
208
209 // Partial meta tag
210 $html = '<meta content="Brief description">';
211 $this->assertFalse(html_extract_tag('description', $html));
212
213 $html = '<meta property="og:description">';
214 $this->assertFalse(html_extract_tag('description', $html));
215
216 $html = '<meta tag1="content1" property="og:description">';
217 $this->assertFalse(html_extract_tag('description', $html));
218
219 $html = '<meta property="og:description" tag1="content1">';
220 $this->assertFalse(html_extract_tag('description', $html));
221
222 $html = '<meta tag1="content1" content="Brief description">';
223 $this->assertFalse(html_extract_tag('description', $html));
224
225 $html = '<meta content="Brief description" tag1="content1">';
226 $this->assertFalse(html_extract_tag('description', $html));
108 } 227 }
109 228
110 /** 229 /**
@@ -127,60 +246,93 @@ class LinkUtilsTest extends TestCase
127 } 246 }
128 247
129 /** 248 /**
249 * Test the header callback with valid value
250 */
251 public function testCurlHeaderCallbackOk(): void
252 {
253 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ok');
254 $data = [
255 'HTTP/1.1 200 OK',
256 'Server: GitHub.com',
257 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
258 'Content-Type: text/html; charset=utf-8',
259 'Status: 200 OK',
260 ];
261
262 foreach ($data as $chunk) {
263 static::assertIsInt($callback(null, $chunk));
264 }
265
266 static::assertSame('utf-8', $charset);
267 }
268
269 /**
130 * Test the download callback with valid value 270 * Test the download callback with valid value
131 */ 271 */
132 public function testCurlDownloadCallbackOk() 272 public function testCurlDownloadCallbackOk(): void
133 { 273 {
274 $charset = 'utf-8';
134 $callback = get_curl_download_callback( 275 $callback = get_curl_download_callback(
135 $charset, 276 $charset,
136 $title, 277 $title,
137 $desc, 278 $desc,
138 $keywords, 279 $keywords,
139 false, 280 false,
140 'ut_curl_getinfo_ok' 281 ' '
141 ); 282 );
283
142 $data = [ 284 $data = [
143 'HTTP/1.1 200 OK', 285 'th=device-width">'
144 'Server: GitHub.com',
145 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
146 'Content-Type: text/html; charset=utf-8',
147 'Status: 200 OK',
148 'end' => 'th=device-width">'
149 . '<title>Refactoring · GitHub</title>' 286 . '<title>Refactoring · GitHub</title>'
150 . '<link rel="search" type="application/opensea', 287 . '<link rel="search" type="application/opensea',
151 '<title>ignored</title>' 288 '<title>ignored</title>'
152 . '<meta name="description" content="desc" />' 289 . '<meta name="description" content="desc" />'
153 . '<meta name="keywords" content="key1,key2" />', 290 . '<meta name="keywords" content="key1,key2" />',
154 ]; 291 ];
155 foreach ($data as $key => $line) { 292
156 $ignore = null; 293 foreach ($data as $chunk) {
157 $expected = $key !== 'end' ? strlen($line) : false; 294 static::assertSame(strlen($chunk), $callback(null, $chunk));
158 $this->assertEquals($expected, $callback($ignore, $line));
159 if ($expected === false) {
160 break;
161 }
162 } 295 }
163 $this->assertEquals('utf-8', $charset); 296
164 $this->assertEquals('Refactoring · GitHub', $title); 297 static::assertSame('utf-8', $charset);
165 $this->assertEmpty($desc); 298 static::assertSame('Refactoring · GitHub', $title);
166 $this->assertEmpty($keywords); 299 static::assertEmpty($desc);
300 static::assertEmpty($keywords);
301 }
302
303 /**
304 * Test the header callback with valid value
305 */
306 public function testCurlHeaderCallbackNoCharset(): void
307 {
308 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_no_charset');
309 $data = [
310 'HTTP/1.1 200 OK',
311 ];
312
313 foreach ($data as $chunk) {
314 static::assertSame(strlen($chunk), $callback(null, $chunk));
315 }
316
317 static::assertFalse($charset);
167 } 318 }
168 319
169 /** 320 /**
170 * Test the download callback with valid values and no charset 321 * Test the download callback with valid values and no charset
171 */ 322 */
172 public function testCurlDownloadCallbackOkNoCharset() 323 public function testCurlDownloadCallbackOkNoCharset(): void
173 { 324 {
325 $charset = null;
174 $callback = get_curl_download_callback( 326 $callback = get_curl_download_callback(
175 $charset, 327 $charset,
176 $title, 328 $title,
177 $desc, 329 $desc,
178 $keywords, 330 $keywords,
179 false, 331 false,
180 'ut_curl_getinfo_no_charset' 332 ' '
181 ); 333 );
334
182 $data = [ 335 $data = [
183 'HTTP/1.1 200 OK',
184 'end' => 'th=device-width">' 336 'end' => 'th=device-width">'
185 . '<title>Refactoring · GitHub</title>' 337 . '<title>Refactoring · GitHub</title>'
186 . '<link rel="search" type="application/opensea', 338 . '<link rel="search" type="application/opensea',
@@ -188,10 +340,11 @@ class LinkUtilsTest extends TestCase
188 . '<meta name="description" content="desc" />' 340 . '<meta name="description" content="desc" />'
189 . '<meta name="keywords" content="key1,key2" />', 341 . '<meta name="keywords" content="key1,key2" />',
190 ]; 342 ];
191 foreach ($data as $key => $line) { 343
192 $ignore = null; 344 foreach ($data as $chunk) {
193 $this->assertEquals(strlen($line), $callback($ignore, $line)); 345 static::assertSame(strlen($chunk), $callback(null, $chunk));
194 } 346 }
347
195 $this->assertEmpty($charset); 348 $this->assertEmpty($charset);
196 $this->assertEquals('Refactoring · GitHub', $title); 349 $this->assertEquals('Refactoring · GitHub', $title);
197 $this->assertEmpty($desc); 350 $this->assertEmpty($desc);
@@ -201,18 +354,19 @@ class LinkUtilsTest extends TestCase
201 /** 354 /**
202 * Test the download callback with valid values and no charset 355 * Test the download callback with valid values and no charset
203 */ 356 */
204 public function testCurlDownloadCallbackOkHtmlCharset() 357 public function testCurlDownloadCallbackOkHtmlCharset(): void
205 { 358 {
359 $charset = null;
206 $callback = get_curl_download_callback( 360 $callback = get_curl_download_callback(
207 $charset, 361 $charset,
208 $title, 362 $title,
209 $desc, 363 $desc,
210 $keywords, 364 $keywords,
211 false, 365 false,
212 'ut_curl_getinfo_no_charset' 366 ' '
213 ); 367 );
368
214 $data = [ 369 $data = [
215 'HTTP/1.1 200 OK',
216 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />', 370 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
217 'end' => 'th=device-width">' 371 'end' => 'th=device-width">'
218 . '<title>Refactoring · GitHub</title>' 372 . '<title>Refactoring · GitHub</title>'
@@ -221,14 +375,10 @@ class LinkUtilsTest extends TestCase
221 . '<meta name="description" content="desc" />' 375 . '<meta name="description" content="desc" />'
222 . '<meta name="keywords" content="key1,key2" />', 376 . '<meta name="keywords" content="key1,key2" />',
223 ]; 377 ];
224 foreach ($data as $key => $line) { 378 foreach ($data as $chunk) {
225 $ignore = null; 379 static::assertSame(strlen($chunk), $callback(null, $chunk));
226 $expected = $key !== 'end' ? strlen($line) : false;
227 $this->assertEquals($expected, $callback($ignore, $line));
228 if ($expected === false) {
229 break;
230 }
231 } 380 }
381
232 $this->assertEquals('utf-8', $charset); 382 $this->assertEquals('utf-8', $charset);
233 $this->assertEquals('Refactoring · GitHub', $title); 383 $this->assertEquals('Refactoring · GitHub', $title);
234 $this->assertEmpty($desc); 384 $this->assertEmpty($desc);
@@ -238,25 +388,27 @@ class LinkUtilsTest extends TestCase
238 /** 388 /**
239 * Test the download callback with valid values and no title 389 * Test the download callback with valid values and no title
240 */ 390 */
241 public function testCurlDownloadCallbackOkNoTitle() 391 public function testCurlDownloadCallbackOkNoTitle(): void
242 { 392 {
393 $charset = 'utf-8';
243 $callback = get_curl_download_callback( 394 $callback = get_curl_download_callback(
244 $charset, 395 $charset,
245 $title, 396 $title,
246 $desc, 397 $desc,
247 $keywords, 398 $keywords,
248 false, 399 false,
249 'ut_curl_getinfo_ok' 400 ' '
250 ); 401 );
402
251 $data = [ 403 $data = [
252 'HTTP/1.1 200 OK',
253 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea', 404 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea',
254 'ignored', 405 'ignored',
255 ]; 406 ];
256 foreach ($data as $key => $line) { 407
257 $ignore = null; 408 foreach ($data as $chunk) {
258 $this->assertEquals(strlen($line), $callback($ignore, $line)); 409 static::assertSame(strlen($chunk), $callback(null, $chunk));
259 } 410 }
411
260 $this->assertEquals('utf-8', $charset); 412 $this->assertEquals('utf-8', $charset);
261 $this->assertEmpty($title); 413 $this->assertEmpty($title);
262 $this->assertEmpty($desc); 414 $this->assertEmpty($desc);
@@ -264,81 +416,56 @@ class LinkUtilsTest extends TestCase
264 } 416 }
265 417
266 /** 418 /**
267 * Test the download callback with an invalid content type. 419 * Test the header callback with an invalid content type.
268 */ 420 */
269 public function testCurlDownloadCallbackInvalidContentType() 421 public function testCurlHeaderCallbackInvalidContentType(): void
270 { 422 {
271 $callback = get_curl_download_callback( 423 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ct_ko');
272 $charset, 424 $data = [
273 $title, 425 'HTTP/1.1 200 OK',
274 $desc, 426 ];
275 $keywords, 427
276 false, 428 static::assertFalse($callback(null, $data[0]));
277 'ut_curl_getinfo_ct_ko' 429 static::assertNull($charset);
278 );
279 $ignore = null;
280 $this->assertFalse($callback($ignore, ''));
281 $this->assertEmpty($charset);
282 $this->assertEmpty($title);
283 } 430 }
284 431
285 /** 432 /**
286 * Test the download callback with an invalid response code. 433 * Test the header callback with an invalid response code.
287 */ 434 */
288 public function testCurlDownloadCallbackInvalidResponseCode() 435 public function testCurlHeaderCallbackInvalidResponseCode(): void
289 { 436 {
290 $callback = $callback = get_curl_download_callback( 437 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rc_ko');
291 $charset, 438
292 $title, 439 static::assertFalse($callback(null, ''));
293 $desc, 440 static::assertNull($charset);
294 $keywords,
295 false,
296 'ut_curl_getinfo_rc_ko'
297 );
298 $ignore = null;
299 $this->assertFalse($callback($ignore, ''));
300 $this->assertEmpty($charset);
301 $this->assertEmpty($title);
302 } 441 }
303 442
304 /** 443 /**
305 * Test the download callback with an invalid content type and response code. 444 * Test the header callback with an invalid content type and response code.
306 */ 445 */
307 public function testCurlDownloadCallbackInvalidContentTypeAndResponseCode() 446 public function testCurlHeaderCallbackInvalidContentTypeAndResponseCode(): void
308 { 447 {
309 $callback = $callback = get_curl_download_callback( 448 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rs_ct_ko');
310 $charset, 449
311 $title, 450 static::assertFalse($callback(null, ''));
312 $desc, 451 static::assertNull($charset);
313 $keywords,
314 false,
315 'ut_curl_getinfo_rs_ct_ko'
316 );
317 $ignore = null;
318 $this->assertFalse($callback($ignore, ''));
319 $this->assertEmpty($charset);
320 $this->assertEmpty($title);
321 } 452 }
322 453
323 /** 454 /**
324 * Test the download callback with valid value, and retrieve_description option enabled. 455 * Test the download callback with valid value, and retrieve_description option enabled.
325 */ 456 */
326 public function testCurlDownloadCallbackOkWithDesc() 457 public function testCurlDownloadCallbackOkWithDesc(): void
327 { 458 {
459 $charset = 'utf-8';
328 $callback = get_curl_download_callback( 460 $callback = get_curl_download_callback(
329 $charset, 461 $charset,
330 $title, 462 $title,
331 $desc, 463 $desc,
332 $keywords, 464 $keywords,
333 true, 465 true,
334 'ut_curl_getinfo_ok' 466 ' '
335 ); 467 );
336 $data = [ 468 $data = [
337 'HTTP/1.1 200 OK',
338 'Server: GitHub.com',
339 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
340 'Content-Type: text/html; charset=utf-8',
341 'Status: 200 OK',
342 'th=device-width">' 469 'th=device-width">'
343 . '<title>Refactoring · GitHub</title>' 470 . '<title>Refactoring · GitHub</title>'
344 . '<link rel="search" type="application/opensea', 471 . '<link rel="search" type="application/opensea',
@@ -346,14 +473,11 @@ class LinkUtilsTest extends TestCase
346 . '<meta name="description" content="link desc" />' 473 . '<meta name="description" content="link desc" />'
347 . '<meta name="keywords" content="key1,key2" />', 474 . '<meta name="keywords" content="key1,key2" />',
348 ]; 475 ];
349 foreach ($data as $key => $line) { 476
350 $ignore = null; 477 foreach ($data as $chunk) {
351 $expected = $key !== 'end' ? strlen($line) : false; 478 static::assertSame(strlen($chunk), $callback(null, $chunk));
352 $this->assertEquals($expected, $callback($ignore, $line));
353 if ($expected === false) {
354 break;
355 }
356 } 479 }
480
357 $this->assertEquals('utf-8', $charset); 481 $this->assertEquals('utf-8', $charset);
358 $this->assertEquals('Refactoring · GitHub', $title); 482 $this->assertEquals('Refactoring · GitHub', $title);
359 $this->assertEquals('link desc', $desc); 483 $this->assertEquals('link desc', $desc);
@@ -364,8 +488,9 @@ class LinkUtilsTest extends TestCase
364 * Test the download callback with valid value, and retrieve_description option enabled, 488 * Test the download callback with valid value, and retrieve_description option enabled,
365 * but no desc or keyword defined in the page. 489 * but no desc or keyword defined in the page.
366 */ 490 */
367 public function testCurlDownloadCallbackOkWithDescNotFound() 491 public function testCurlDownloadCallbackOkWithDescNotFound(): void
368 { 492 {
493 $charset = 'utf-8';
369 $callback = get_curl_download_callback( 494 $callback = get_curl_download_callback(
370 $charset, 495 $charset,
371 $title, 496 $title,
@@ -375,24 +500,16 @@ class LinkUtilsTest extends TestCase
375 'ut_curl_getinfo_ok' 500 'ut_curl_getinfo_ok'
376 ); 501 );
377 $data = [ 502 $data = [
378 'HTTP/1.1 200 OK',
379 'Server: GitHub.com',
380 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
381 'Content-Type: text/html; charset=utf-8',
382 'Status: 200 OK',
383 'th=device-width">' 503 'th=device-width">'
384 . '<title>Refactoring · GitHub</title>' 504 . '<title>Refactoring · GitHub</title>'
385 . '<link rel="search" type="application/opensea', 505 . '<link rel="search" type="application/opensea',
386 'end' => '<title>ignored</title>', 506 'end' => '<title>ignored</title>',
387 ]; 507 ];
388 foreach ($data as $key => $line) { 508
389 $ignore = null; 509 foreach ($data as $chunk) {
390 $expected = $key !== 'end' ? strlen($line) : false; 510 static::assertSame(strlen($chunk), $callback(null, $chunk));
391 $this->assertEquals($expected, $callback($ignore, $line));
392 if ($expected === false) {
393 break;
394 }
395 } 511 }
512
396 $this->assertEquals('utf-8', $charset); 513 $this->assertEquals('utf-8', $charset);
397 $this->assertEquals('Refactoring · GitHub', $title); 514 $this->assertEquals('Refactoring · GitHub', $title);
398 $this->assertEmpty($desc); 515 $this->assertEmpty($desc);
@@ -493,6 +610,115 @@ class LinkUtilsTest extends TestCase
493 } 610 }
494 611
495 /** 612 /**
613 * Test tags_str2array with whitespace separator.
614 */
615 public function testTagsStr2ArrayWithSpaceSeparator(): void
616 {
617 $separator = ' ';
618
619 static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator));
620 static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator));
621 static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array(' tag1 tag2 tag3 ', $separator));
622 static::assertSame(['tag1@', 'tag2,', '.tag3'], tags_str2array(' tag1@ tag2, .tag3 ', $separator));
623 static::assertSame([], tags_str2array('', $separator));
624 static::assertSame([], tags_str2array(' ', $separator));
625 static::assertSame([], tags_str2array(null, $separator));
626 }
627
628 /**
629 * Test tags_str2array with @ separator.
630 */
631 public function testTagsStr2ArrayWithCharSeparator(): void
632 {
633 $separator = '@';
634
635 static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@tag2@tag3', $separator));
636 static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@@@@tag2@@@@tag3', $separator));
637 static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('@@@tag1@@@tag2@@@@tag3@@', $separator));
638 static::assertSame(
639 ['tag1#', 'tag2, and other', '.tag3'],
640 tags_str2array('@@@ tag1# @@@ tag2, and other @@@@.tag3@@', $separator)
641 );
642 static::assertSame([], tags_str2array('', $separator));
643 static::assertSame([], tags_str2array(' ', $separator));
644 static::assertSame([], tags_str2array(null, $separator));
645 }
646
647 /**
648 * Test tags_array2str with ' ' separator.
649 */
650 public function testTagsArray2StrWithSpaceSeparator(): void
651 {
652 $separator = ' ';
653
654 static::assertSame('tag1 tag2 tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator));
655 static::assertSame('tag1, tag2@ tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator));
656 static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', 'tag2', 'tag3 '], $separator));
657 static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator));
658 static::assertSame('tag1', tags_array2str([' tag1 '], $separator));
659 static::assertSame('', tags_array2str([' '], $separator));
660 static::assertSame('', tags_array2str([], $separator));
661 static::assertSame('', tags_array2str(null, $separator));
662 }
663
664 /**
665 * Test tags_array2str with @ separator.
666 */
667 public function testTagsArray2StrWithCharSeparator(): void
668 {
669 $separator = '@';
670
671 static::assertSame('tag1@tag2@tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator));
672 static::assertSame('tag1,@tag2@tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator));
673 static::assertSame(
674 'tag1@tag2, and other@tag3',
675 tags_array2str(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator)
676 );
677 static::assertSame('tag1@tag2@tag3', tags_array2str(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator));
678 static::assertSame('tag1', tags_array2str(['@@@@tag1@@@@'], $separator));
679 static::assertSame('', tags_array2str(['@@@'], $separator));
680 static::assertSame('', tags_array2str([], $separator));
681 static::assertSame('', tags_array2str(null, $separator));
682 }
683
684 /**
685 * Test tags_array2str with @ separator.
686 */
687 public function testTagsFilterWithSpaceSeparator(): void
688 {
689 $separator = ' ';
690
691 static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator));
692 static::assertSame(['tag1,', 'tag2@', 'tag3'], tags_filter(['tag1,', 'tag2@', 'tag3'], $separator));
693 static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', 'tag2', 'tag3 '], $separator));
694 static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator));
695 static::assertSame(['tag1'], tags_filter([' tag1 '], $separator));
696 static::assertSame([], tags_filter([' '], $separator));
697 static::assertSame([], tags_filter([], $separator));
698 static::assertSame([], tags_filter(null, $separator));
699 }
700
701 /**
702 * Test tags_array2str with @ separator.
703 */
704 public function testTagsArrayFilterWithSpaceSeparator(): void
705 {
706 $separator = '@';
707
708 static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator));
709 static::assertSame(['tag1,', 'tag2#', 'tag3'], tags_filter(['tag1,', 'tag2#', 'tag3'], $separator));
710 static::assertSame(
711 ['tag1', 'tag2, and other', 'tag3'],
712 tags_filter(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator)
713 );
714 static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator));
715 static::assertSame(['tag1'], tags_filter(['@@@@tag1@@@@'], $separator));
716 static::assertSame([], tags_filter(['@@@'], $separator));
717 static::assertSame([], tags_filter([], $separator));
718 static::assertSame([], tags_filter(null, $separator));
719 }
720
721 /**
496 * Util function to build an hashtag link. 722 * Util function to build an hashtag link.
497 * 723 *
498 * @param string $hashtag Hashtag name. 724 * @param string $hashtag Hashtag name.
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 2d675c9a..3508a7b1 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -30,3 +30,7 @@ require_once 'tests/utils/ReferenceLinkDB.php';
30require_once 'tests/utils/ReferenceSessionIdHashes.php'; 30require_once 'tests/utils/ReferenceSessionIdHashes.php';
31 31
32\ReferenceSessionIdHashes::genAllHashes(); 32\ReferenceSessionIdHashes::genAllHashes();
33
34if (!defined('SHAARLI_MUTEX_FILE')) {
35 define('SHAARLI_MUTEX_FILE', __FILE__);
36}
diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php
index 5d52daef..3d43c344 100644
--- a/tests/container/ContainerBuilderTest.php
+++ b/tests/container/ContainerBuilderTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Container; 5namespace Shaarli\Container;
6 6
7use Psr\Log\LoggerInterface;
7use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
9use Shaarli\Feed\FeedBuilder; 10use Shaarli\Feed\FeedBuilder;
@@ -12,6 +13,7 @@ use Shaarli\Front\Controller\Visitor\ErrorController;
12use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; 13use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
13use Shaarli\History; 14use Shaarli\History;
14use Shaarli\Http\HttpAccess; 15use Shaarli\Http\HttpAccess;
16use Shaarli\Http\MetadataRetriever;
15use Shaarli\Netscape\NetscapeBookmarkUtils; 17use Shaarli\Netscape\NetscapeBookmarkUtils;
16use Shaarli\Plugin\PluginManager; 18use Shaarli\Plugin\PluginManager;
17use Shaarli\Render\PageBuilder; 19use Shaarli\Render\PageBuilder;
@@ -54,7 +56,8 @@ class ContainerBuilderTest extends TestCase
54 $this->conf, 56 $this->conf,
55 $this->sessionManager, 57 $this->sessionManager,
56 $this->cookieManager, 58 $this->cookieManager,
57 $this->loginManager 59 $this->loginManager,
60 $this->createMock(LoggerInterface::class)
58 ); 61 );
59 } 62 }
60 63
@@ -72,6 +75,8 @@ class ContainerBuilderTest extends TestCase
72 static::assertInstanceOf(History::class, $container->history); 75 static::assertInstanceOf(History::class, $container->history);
73 static::assertInstanceOf(HttpAccess::class, $container->httpAccess); 76 static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
74 static::assertInstanceOf(LoginManager::class, $container->loginManager); 77 static::assertInstanceOf(LoginManager::class, $container->loginManager);
78 static::assertInstanceOf(LoggerInterface::class, $container->logger);
79 static::assertInstanceOf(MetadataRetriever::class, $container->metadataRetriever);
75 static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils); 80 static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
76 static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); 81 static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
77 static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager); 82 static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager);
diff --git a/tests/feed/FeedBuilderTest.php b/tests/feed/FeedBuilderTest.php
index c29e8ef3..6b9204eb 100644
--- a/tests/feed/FeedBuilderTest.php
+++ b/tests/feed/FeedBuilderTest.php
@@ -3,6 +3,7 @@
3namespace Shaarli\Feed; 3namespace Shaarli\Feed;
4 4
5use DateTime; 5use DateTime;
6use malkusch\lock\mutex\NoMutex;
6use ReferenceLinkDB; 7use ReferenceLinkDB;
7use Shaarli\Bookmark\Bookmark; 8use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\BookmarkFileService; 9use Shaarli\Bookmark\BookmarkFileService;
@@ -47,6 +48,7 @@ class FeedBuilderTest extends TestCase
47 */ 48 */
48 public static function setUpBeforeClass(): void 49 public static function setUpBeforeClass(): void
49 { 50 {
51 $mutex = new NoMutex();
50 $conf = new ConfigManager('tests/utils/config/configJson'); 52 $conf = new ConfigManager('tests/utils/config/configJson');
51 $conf->set('resource.datastore', self::$testDatastore); 53 $conf->set('resource.datastore', self::$testDatastore);
52 $refLinkDB = new \ReferenceLinkDB(); 54 $refLinkDB = new \ReferenceLinkDB();
@@ -54,7 +56,7 @@ class FeedBuilderTest extends TestCase
54 $history = new History('sandbox/history.php'); 56 $history = new History('sandbox/history.php');
55 $factory = new FormatterFactory($conf, true); 57 $factory = new FormatterFactory($conf, true);
56 self::$formatter = $factory->getFormatter(); 58 self::$formatter = $factory->getFormatter();
57 self::$bookmarkService = new BookmarkFileService($conf, $history, true); 59 self::$bookmarkService = new BookmarkFileService($conf, $history, $mutex, true);
58 60
59 self::$serverInfo = array( 61 self::$serverInfo = array(
60 'HTTPS' => 'Off', 62 'HTTPS' => 'Off',
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php
index 9534436e..4fcc5dd1 100644
--- a/tests/formatter/BookmarkDefaultFormatterTest.php
+++ b/tests/formatter/BookmarkDefaultFormatterTest.php
@@ -174,4 +174,139 @@ class BookmarkDefaultFormatterTest extends TestCase
174 $this->assertSame($tags, $link['taglist']); 174 $this->assertSame($tags, $link['taglist']);
175 $this->assertSame(implode(' ', $tags), $link['tags']); 175 $this->assertSame(implode(' ', $tags), $link['tags']);
176 } 176 }
177
178 /**
179 * Test formatTitleHtml with search result highlight.
180 */
181 public function testFormatTitleHtmlWithSearchHighlight(): void
182 {
183 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
184
185 $bookmark = new Bookmark();
186 $bookmark->setTitle('PSR-2: Coding Style Guide');
187 $bookmark->addAdditionalContentEntry(
188 'search_highlight',
189 ['title' => [
190 ['start' => 0, 'end' => 5], // "psr-2"
191 ['start' => 7, 'end' => 13], // coding
192 ['start' => 20, 'end' => 25], // guide
193 ]]
194 );
195
196 $link = $this->formatter->format($bookmark);
197
198 $this->assertSame(
199 '<span class="search-highlight">PSR-2</span>: ' .
200 '<span class="search-highlight">Coding</span> Style ' .
201 '<span class="search-highlight">Guide</span>',
202 $link['title_html']
203 );
204 }
205
206 /**
207 * Test formatDescription with search result highlight.
208 */
209 public function testFormatDescriptionWithSearchHighlight(): void
210 {
211 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
212
213 $bookmark = new Bookmark();
214 $bookmark->setDescription('This guide extends and expands on PSR-1, the basic coding standard.');
215 $bookmark->addAdditionalContentEntry(
216 'search_highlight',
217 ['description' => [
218 ['start' => 0, 'end' => 10], // "This guide"
219 ['start' => 45, 'end' => 50], // basic
220 ['start' => 58, 'end' => 67], // standard.
221 ]]
222 );
223
224 $link = $this->formatter->format($bookmark);
225
226 $this->assertSame(
227 '<span class="search-highlight">This guide</span> extends and expands on PSR-1, the ' .
228 '<span class="search-highlight">basic</span> coding ' .
229 '<span class="search-highlight">standard.</span>',
230 $link['description']
231 );
232 }
233
234 /**
235 * Test formatUrlHtml with search result highlight.
236 */
237 public function testFormatUrlHtmlWithSearchHighlight(): void
238 {
239 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
240
241 $bookmark = new Bookmark();
242 $bookmark->setUrl('http://www.php-fig.org/psr/psr-2/');
243 $bookmark->addAdditionalContentEntry(
244 'search_highlight',
245 ['url' => [
246 ['start' => 0, 'end' => 4], // http
247 ['start' => 15, 'end' => 18], // fig
248 ['start' => 27, 'end' => 33], // "psr-2/"
249 ]]
250 );
251
252 $link = $this->formatter->format($bookmark);
253
254 $this->assertSame(
255 '<span class="search-highlight">http</span>://www.php-' .
256 '<span class="search-highlight">fig</span>.org/psr/' .
257 '<span class="search-highlight">psr-2/</span>',
258 $link['url_html']
259 );
260 }
261
262 /**
263 * Test formatTagListHtml with search result highlight.
264 */
265 public function testFormatTagListHtmlWithSearchHighlight(): void
266 {
267 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
268
269 $bookmark = new Bookmark();
270 $bookmark->setTagsString('coding-style standards quality assurance');
271 $bookmark->addAdditionalContentEntry(
272 'search_highlight',
273 ['tags' => [
274 ['start' => 0, 'end' => 12], // coding-style
275 ['start' => 23, 'end' => 30], // quality
276 ['start' => 31, 'end' => 40], // assurance
277 ],]
278 );
279
280 $link = $this->formatter->format($bookmark);
281
282 $this->assertSame(
283 [
284 '<span class="search-highlight">coding-style</span>',
285 'standards',
286 '<span class="search-highlight">quality</span>',
287 '<span class="search-highlight">assurance</span>',
288 ],
289 $link['taglist_html']
290 );
291 }
292
293 /**
294 * Test default formatting with formatter_settings.autolink set to false:
295 * URLs and hashtags should not be transformed
296 */
297 public function testFormatDescriptionWithoutLinkification(): void
298 {
299 $this->conf->set('formatter_settings.autolink', false);
300 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
301
302 $bookmark = new Bookmark();
303 $bookmark->setDescription('Hi!' . PHP_EOL . 'https://thisisaurl.tld #hashtag');
304
305 $link = $this->formatter->format($bookmark);
306
307 static::assertSame(
308 'Hi!<br />' . PHP_EOL . 'https://thisisaurl.tld &nbsp;#hashtag',
309 $link['description']
310 );
311 }
177} 312}
diff --git a/tests/formatter/BookmarkMarkdownExtraFormatterTest.php b/tests/formatter/BookmarkMarkdownExtraFormatterTest.php
new file mode 100644
index 00000000..d4941ef3
--- /dev/null
+++ b/tests/formatter/BookmarkMarkdownExtraFormatterTest.php
@@ -0,0 +1,162 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9
10/**
11 * Class BookmarkMarkdownExtraFormatterTest
12 * @package Shaarli\Formatter
13 */
14class BookmarkMarkdownExtraFormatterTest extends TestCase
15{
16 /** @var string Path of test config file */
17 protected static $testConf = 'sandbox/config';
18
19 /** @var BookmarkFormatter */
20 protected $formatter;
21
22 /** @var ConfigManager instance */
23 protected $conf;
24
25 /**
26 * Initialize formatter instance.
27 */
28 public function setUp(): void
29 {
30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
31 $this->conf = new ConfigManager(self::$testConf);
32 $this->formatter = new BookmarkMarkdownExtraFormatter($this->conf, true);
33 }
34
35 /**
36 * Test formatting a bookmark with all its attribute filled.
37 */
38 public function testFormatExtra(): void
39 {
40 $bookmark = new Bookmark();
41 $bookmark->setId($id = 11);
42 $bookmark->setShortUrl($short = 'abcdef');
43 $bookmark->setUrl('https://sub.domain.tld?query=here&for=real#hash');
44 $bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
45 $bookmark->setDescription('<h2>Content</h2><p>`Here is some content</p>');
46 $bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
47 $bookmark->setThumbnail('http://domain2.tdl2/?type=img&name=file.png');
48 $bookmark->setSticky(true);
49 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412'));
50 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213'));
51 $bookmark->setPrivate(true);
52
53 $link = $this->formatter->format($bookmark);
54 $this->assertEquals($id, $link['id']);
55 $this->assertEquals($short, $link['shorturl']);
56 $this->assertEquals('https://sub.domain.tld?query=here&amp;for=real#hash', $link['url']);
57 $this->assertEquals(
58 'https://sub.domain.tld?query=here&amp;for=real#hash',
59 $link['real_url']
60 );
61 $this->assertEquals('This is a &lt;strong&gt;bookmark&lt;/strong&gt;', $link['title']);
62 $this->assertEquals(
63 '<div class="markdown"><p>'.
64 '&lt;h2&gt;Content&lt;/h2&gt;&lt;p&gt;`Here is some content&lt;/p&gt;'.
65 '</p></div>',
66 $link['description']
67 );
68 $tags[3] = '&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;';
69 $this->assertEquals($tags, $link['taglist']);
70 $this->assertEquals(implode(' ', $tags), $link['tags']);
71 $this->assertEquals(
72 'http://domain2.tdl2/?type=img&amp;name=file.png',
73 $link['thumbnail']
74 );
75 $this->assertEquals($created, $link['created']);
76 $this->assertEquals($created->getTimestamp(), $link['timestamp']);
77 $this->assertEquals($updated, $link['updated']);
78 $this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
79 $this->assertTrue($link['private']);
80 $this->assertTrue($link['sticky']);
81 $this->assertEquals('private', $link['class']);
82 }
83
84 /**
85 * Test formatting a bookmark with all its attribute filled.
86 */
87 public function testFormatExtraMinimal(): void
88 {
89 $bookmark = new Bookmark();
90
91 $link = $this->formatter->format($bookmark);
92 $this->assertEmpty($link['id']);
93 $this->assertEmpty($link['shorturl']);
94 $this->assertEmpty($link['url']);
95 $this->assertEmpty($link['real_url']);
96 $this->assertEmpty($link['title']);
97 $this->assertEmpty($link['description']);
98 $this->assertEmpty($link['taglist']);
99 $this->assertEmpty($link['tags']);
100 $this->assertEmpty($link['thumbnail']);
101 $this->assertEmpty($link['created']);
102 $this->assertEmpty($link['timestamp']);
103 $this->assertEmpty($link['updated']);
104 $this->assertEmpty($link['updated_timestamp']);
105 $this->assertFalse($link['private']);
106 $this->assertFalse($link['sticky']);
107 $this->assertEmpty($link['class']);
108 }
109
110 /**
111 * Make sure that the description is properly formatted by the default formatter.
112 */
113 public function testFormatExtrraDescription(): void
114 {
115 $description = 'This a <strong>description</strong>'. PHP_EOL;
116 $description .= 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
117 $description .= 'Also, there is an #hashtag added'. PHP_EOL;
118 $description .= ' A N D KEEP SPACES ! '. PHP_EOL;
119 $description .= '# Header {.class}'. PHP_EOL;
120
121 $bookmark = new Bookmark();
122 $bookmark->setDescription($description);
123 $link = $this->formatter->format($bookmark);
124
125 $description = '<div class="markdown"><p>';
126 $description .= 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'. PHP_EOL;
127 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
128 $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL;
129 $description .= 'Also, there is an <a href="./add-tag/hashtag">#hashtag</a> added<br />'. PHP_EOL;
130 $description .= 'A N D KEEP SPACES ! </p>' . PHP_EOL;
131 $description .= '<h1 class="class">Header</h1>';
132 $description .= '</div>';
133
134 $this->assertEquals($description, $link['description']);
135 }
136
137 /**
138 * Test formatting URL with an index_url set
139 * It should prepend relative links.
140 */
141 public function testFormatExtraNoteWithIndexUrl(): void
142 {
143 $bookmark = new Bookmark();
144 $bookmark->setUrl($short = '?abcdef');
145 $description = 'Text #hashtag more text';
146 $bookmark->setDescription($description);
147
148 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
149
150 $description = '<div class="markdown"><p>';
151 $description .= 'Text <a href="'. $root .'./add-tag/hashtag">#hashtag</a> more text';
152 $description .= '</p></div>';
153
154 $link = $this->formatter->format($bookmark);
155 $this->assertEquals($root . $short, $link['url']);
156 $this->assertEquals($root . $short, $link['real_url']);
157 $this->assertEquals(
158 $description,
159 $link['description']
160 );
161 }
162}
diff --git a/tests/front/controller/admin/ConfigureControllerTest.php b/tests/front/controller/admin/ConfigureControllerTest.php
index aca6cff3..d82db0a7 100644
--- a/tests/front/controller/admin/ConfigureControllerTest.php
+++ b/tests/front/controller/admin/ConfigureControllerTest.php
@@ -51,7 +51,7 @@ class ConfigureControllerTest extends TestCase
51 static::assertSame('general.title', $assignedVariables['title']); 51 static::assertSame('general.title', $assignedVariables['title']);
52 static::assertSame('resource.theme', $assignedVariables['theme']); 52 static::assertSame('resource.theme', $assignedVariables['theme']);
53 static::assertEmpty($assignedVariables['theme_available']); 53 static::assertEmpty($assignedVariables['theme_available']);
54 static::assertSame(['default', 'markdown'], $assignedVariables['formatter_available']); 54 static::assertSame(['default', 'markdown', 'markdownExtra'], $assignedVariables['formatter_available']);
55 static::assertNotEmpty($assignedVariables['continents']); 55 static::assertNotEmpty($assignedVariables['continents']);
56 static::assertNotEmpty($assignedVariables['cities']); 56 static::assertNotEmpty($assignedVariables['cities']);
57 static::assertSame('general.retrieve_description', $assignedVariables['retrieve_description']); 57 static::assertSame('general.retrieve_description', $assignedVariables['retrieve_description']);
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
deleted file mode 100644
index 0f27ec2f..00000000
--- a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
+++ /dev/null
@@ -1,47 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
8use Shaarli\Front\Controller\Admin\ManageShaareController;
9use Shaarli\Http\HttpAccess;
10use Shaarli\TestCase;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class 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
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\BookmarkFilter; 8use Shaarli\Bookmark\BookmarkFilter;
9use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\WrongTokenException; 10use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Security\SessionManager; 11use Shaarli\Security\SessionManager;
11use Shaarli\TestCase; 12use 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('&nbsp;', $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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Security\SessionManager;
9use Shaarli\TestCase;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13/**
14 * Test Server administration controller.
15 */
16class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Formatter\BookmarkMarkdownFormatter;
9use Shaarli\Http\HttpAccess;
10use Shaarli\TestCase;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class 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
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
@@ -10,7 +10,7 @@ use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\BookmarkRawFormatter; 10use Shaarli\Formatter\BookmarkRawFormatter;
11use Shaarli\Formatter\FormatterFactory; 11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 12use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
13use Shaarli\Front\Controller\Admin\ManageShaareController; 13use Shaarli\Front\Controller\Admin\ShaareManageController;
14use Shaarli\Http\HttpAccess; 14use Shaarli\Http\HttpAccess;
15use Shaarli\Security\SessionManager; 15use Shaarli\Security\SessionManager;
16use Shaarli\TestCase; 16use Shaarli\TestCase;
@@ -21,7 +21,7 @@ class ChangeVisibilityBookmarkTest extends TestCase
21{ 21{
22 use FrontAdminControllerMockHelper; 22 use FrontAdminControllerMockHelper;
23 23
24 /** @var ManageShaareController */ 24 /** @var ShaareManageController */
25 protected $controller; 25 protected $controller;
26 26
27 public function setUp(): void 27 public function setUp(): void
@@ -29,7 +29,7 @@ class ChangeVisibilityBookmarkTest extends TestCase
29 $this->createContainer(); 29 $this->createContainer();
30 30
31 $this->container->httpAccess = $this->createMock(HttpAccess::class); 31 $this->container->httpAccess = $this->createMock(HttpAccess::class);
32 $this->controller = new ManageShaareController($this->container); 32 $this->controller = new ShaareManageController($this->container);
33 } 33 }
34 34
35 /** 35 /**
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php
index ba774e21..a276d988 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php
+++ b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php
@@ -2,14 +2,14 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkFormatter; 9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\FormatterFactory; 10use Shaarli\Formatter\FormatterFactory;
11use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 11use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
12use Shaarli\Front\Controller\Admin\ManageShaareController; 12use Shaarli\Front\Controller\Admin\ShaareManageController;
13use Shaarli\Http\HttpAccess; 13use Shaarli\Http\HttpAccess;
14use Shaarli\Security\SessionManager; 14use Shaarli\Security\SessionManager;
15use Shaarli\TestCase; 15use Shaarli\TestCase;
@@ -20,7 +20,7 @@ class DeleteBookmarkTest extends TestCase
20{ 20{
21 use FrontAdminControllerMockHelper; 21 use FrontAdminControllerMockHelper;
22 22
23 /** @var ManageShaareController */ 23 /** @var ShaareManageController */
24 protected $controller; 24 protected $controller;
25 25
26 public function setUp(): void 26 public function setUp(): void
@@ -28,7 +28,7 @@ class DeleteBookmarkTest extends TestCase
28 $this->createContainer(); 28 $this->createContainer();
29 29
30 $this->container->httpAccess = $this->createMock(HttpAccess::class); 30 $this->container->httpAccess = $this->createMock(HttpAccess::class);
31 $this->controller = new ManageShaareController($this->container); 31 $this->controller = new ShaareManageController($this->container);
32 } 32 }
33 33
34 /** 34 /**
@@ -38,6 +38,8 @@ class DeleteBookmarkTest extends TestCase
38 { 38 {
39 $parameters = ['id' => '123']; 39 $parameters = ['id' => '123'];
40 40
41 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/shaare/abcdef';
42
41 $request = $this->createMock(Request::class); 43 $request = $this->createMock(Request::class);
42 $request 44 $request
43 ->method('getParam') 45 ->method('getParam')
@@ -90,6 +92,8 @@ class DeleteBookmarkTest extends TestCase
90 { 92 {
91 $parameters = ['id' => '123 456 789']; 93 $parameters = ['id' => '123 456 789'];
92 94
95 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/?searchtags=abcdef';
96
93 $request = $this->createMock(Request::class); 97 $request = $this->createMock(Request::class);
94 $request 98 $request
95 ->method('getParam') 99 ->method('getParam')
@@ -152,7 +156,7 @@ class DeleteBookmarkTest extends TestCase
152 $result = $this->controller->deleteBookmark($request, $response); 156 $result = $this->controller->deleteBookmark($request, $response);
153 157
154 static::assertSame(302, $result->getStatusCode()); 158 static::assertSame(302, $result->getStatusCode());
155 static::assertSame(['/subfolder/'], $result->getHeader('location')); 159 static::assertSame(['/subfolder/?searchtags=abcdef'], $result->getHeader('location'));
156 } 160 }
157 161
158 /** 162 /**
@@ -356,6 +360,10 @@ class DeleteBookmarkTest extends TestCase
356 ; 360 ;
357 $response = new Response(); 361 $response = new Response();
358 362
363 $this->container->bookmarkService->method('get')->with('123')->willReturn(
364 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')
365 );
366
359 $this->container->formatterFactory = $this->createMock(FormatterFactory::class); 367 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
360 $this->container->formatterFactory 368 $this->container->formatterFactory
361 ->expects(static::once()) 369 ->expects(static::once())
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php
index 50ce7df1..b89206ce 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php
+++ b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php
@@ -2,12 +2,12 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController; 10use Shaarli\Front\Controller\Admin\ShaareManageController;
11use Shaarli\Http\HttpAccess; 11use Shaarli\Http\HttpAccess;
12use Shaarli\Security\SessionManager; 12use Shaarli\Security\SessionManager;
13use Shaarli\TestCase; 13use 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
9use Shaarli\Front\Controller\Admin\ShaareManageController;
10use Shaarli\Http\HttpAccess;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15/**
16 * Test GET /admin/shaare/private/{hash}
17 */
18class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6
7use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
8use Shaarli\Front\Controller\Admin\ShaarePublishController;
9use Shaarli\Http\HttpAccess;
10use Shaarli\Http\MetadataRetriever;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class 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
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController; 10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Http\HttpAccess; 11use Shaarli\Http\HttpAccess;
12use Shaarli\Http\MetadataRetriever;
12use Shaarli\TestCase; 13use Shaarli\TestCase;
13use Slim\Http\Request; 14use Slim\Http\Request;
14use Slim\Http\Response; 15use 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
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController; 10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Http\HttpAccess; 11use Shaarli\Http\HttpAccess;
12use Shaarli\Security\SessionManager; 12use Shaarli\Security\SessionManager;
13use Shaarli\TestCase; 13use Shaarli\TestCase;
@@ -18,7 +18,7 @@ class DisplayEditFormTest extends TestCase
18{ 18{
19 use FrontAdminControllerMockHelper; 19 use FrontAdminControllerMockHelper;
20 20
21 /** @var ManageShaareController */ 21 /** @var ShaarePublishController */
22 protected $controller; 22 protected $controller;
23 23
24 public function setUp(): void 24 public function setUp(): void
@@ -26,7 +26,7 @@ class DisplayEditFormTest extends TestCase
26 $this->createContainer(); 26 $this->createContainer();
27 27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class); 28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->controller = new ManageShaareController($this->container); 29 $this->controller = new ShaarePublishController($this->container);
30 } 30 }
31 31
32 /** 32 /**
@@ -74,7 +74,7 @@ class DisplayEditFormTest extends TestCase
74 static::assertSame($url, $assignedVariables['link']['url']); 74 static::assertSame($url, $assignedVariables['link']['url']);
75 static::assertSame($title, $assignedVariables['link']['title']); 75 static::assertSame($title, $assignedVariables['link']['title']);
76 static::assertSame($description, $assignedVariables['link']['description']); 76 static::assertSame($description, $assignedVariables['link']['description']);
77 static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']); 77 static::assertSame(implode('@', $tags) . '@', $assignedVariables['link']['tags']);
78 static::assertTrue($assignedVariables['link']['private']); 78 static::assertTrue($assignedVariables['link']['private']);
79 static::assertSame($createdAt, $assignedVariables['link']['created']); 79 static::assertSame($createdAt, $assignedVariables['link']['created']);
80 } 80 }
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php
index f7a68226..b6a861bc 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php
@@ -2,12 +2,12 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController; 10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Front\Exception\WrongTokenException; 11use Shaarli\Front\Exception\WrongTokenException;
12use Shaarli\Http\HttpAccess; 12use Shaarli\Http\HttpAccess;
13use Shaarli\Security\SessionManager; 13use Shaarli\Security\SessionManager;
@@ -20,7 +20,7 @@ class SaveBookmarkTest extends TestCase
20{ 20{
21 use FrontAdminControllerMockHelper; 21 use FrontAdminControllerMockHelper;
22 22
23 /** @var ManageShaareController */ 23 /** @var ShaarePublishController */
24 protected $controller; 24 protected $controller;
25 25
26 public function setUp(): void 26 public function setUp(): void
@@ -28,7 +28,7 @@ class SaveBookmarkTest extends TestCase
28 $this->createContainer(); 28 $this->createContainer();
29 29
30 $this->container->httpAccess = $this->createMock(HttpAccess::class); 30 $this->container->httpAccess = $this->createMock(HttpAccess::class);
31 $this->controller = new ManageShaareController($this->container); 31 $this->controller = new ShaarePublishController($this->container);
32 } 32 }
33 33
34 /** 34 /**
@@ -66,23 +66,27 @@ class SaveBookmarkTest extends TestCase
66 $this->container->bookmarkService 66 $this->container->bookmarkService
67 ->expects(static::once()) 67 ->expects(static::once())
68 ->method('addOrSet') 68 ->method('addOrSet')
69 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { 69 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark {
70 static::assertFalse($save); 70 static::assertFalse($save);
71 71
72 $checkBookmark($bookmark); 72 $checkBookmark($bookmark);
73 73
74 $bookmark->setId($id); 74 $bookmark->setId($id);
75
76 return $bookmark;
75 }) 77 })
76 ; 78 ;
77 $this->container->bookmarkService 79 $this->container->bookmarkService
78 ->expects(static::once()) 80 ->expects(static::once())
79 ->method('set') 81 ->method('set')
80 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { 82 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark {
81 static::assertTrue($save); 83 static::assertTrue($save);
82 84
83 $checkBookmark($bookmark); 85 $checkBookmark($bookmark);
84 86
85 static::assertSame($id, $bookmark->getId()); 87 static::assertSame($id, $bookmark->getId());
88
89 return $bookmark;
86 }) 90 })
87 ; 91 ;
88 92
@@ -155,21 +159,25 @@ class SaveBookmarkTest extends TestCase
155 $this->container->bookmarkService 159 $this->container->bookmarkService
156 ->expects(static::once()) 160 ->expects(static::once())
157 ->method('addOrSet') 161 ->method('addOrSet')
158 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { 162 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark {
159 static::assertFalse($save); 163 static::assertFalse($save);
160 164
161 $checkBookmark($bookmark); 165 $checkBookmark($bookmark);
166
167 return $bookmark;
162 }) 168 })
163 ; 169 ;
164 $this->container->bookmarkService 170 $this->container->bookmarkService
165 ->expects(static::once()) 171 ->expects(static::once())
166 ->method('set') 172 ->method('set')
167 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { 173 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark {
168 static::assertTrue($save); 174 static::assertTrue($save);
169 175
170 $checkBookmark($bookmark); 176 $checkBookmark($bookmark);
171 177
172 static::assertSame($id, $bookmark->getId()); 178 static::assertSame($id, $bookmark->getId());
179
180 return $bookmark;
173 }) 181 })
174 ; 182 ;
175 183
@@ -201,7 +209,7 @@ class SaveBookmarkTest extends TestCase
201 /** 209 /**
202 * Test save a bookmark - try to retrieve the thumbnail 210 * Test save a bookmark - try to retrieve the thumbnail
203 */ 211 */
204 public function testSaveBookmarkWithThumbnail(): void 212 public function testSaveBookmarkWithThumbnailSync(): void
205 { 213 {
206 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; 214 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
207 215
@@ -216,7 +224,13 @@ class SaveBookmarkTest extends TestCase
216 224
217 $this->container->conf = $this->createMock(ConfigManager::class); 225 $this->container->conf = $this->createMock(ConfigManager::class);
218 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { 226 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
219 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; 227 if ($key === 'thumbnails.mode') {
228 return Thumbnailer::MODE_ALL;
229 } elseif ($key === 'general.enable_async_metadata') {
230 return false;
231 }
232
233 return $default;
220 }); 234 });
221 235
222 $this->container->thumbnailer = $this->createMock(Thumbnailer::class); 236 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
@@ -230,8 +244,10 @@ class SaveBookmarkTest extends TestCase
230 $this->container->bookmarkService 244 $this->container->bookmarkService
231 ->expects(static::once()) 245 ->expects(static::once())
232 ->method('addOrSet') 246 ->method('addOrSet')
233 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void { 247 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): Bookmark {
234 static::assertSame($thumb, $bookmark->getThumbnail()); 248 static::assertSame($thumb, $bookmark->getThumbnail());
249
250 return $bookmark;
235 }) 251 })
236 ; 252 ;
237 253
@@ -265,6 +281,51 @@ class SaveBookmarkTest extends TestCase
265 } 281 }
266 282
267 /** 283 /**
284 * Test save a bookmark - do not attempt to retrieve thumbnails if async mode is enabled.
285 */
286 public function testSaveBookmarkWithThumbnailAsync(): void
287 {
288 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
289
290 $request = $this->createMock(Request::class);
291 $request
292 ->method('getParam')
293 ->willReturnCallback(function (string $key) use ($parameters): ?string {
294 return $parameters[$key] ?? null;
295 })
296 ;
297 $response = new Response();
298
299 $this->container->conf = $this->createMock(ConfigManager::class);
300 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
301 if ($key === 'thumbnails.mode') {
302 return Thumbnailer::MODE_ALL;
303 } elseif ($key === 'general.enable_async_metadata') {
304 return true;
305 }
306
307 return $default;
308 });
309
310 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
311 $this->container->thumbnailer->expects(static::never())->method('get');
312
313 $this->container->bookmarkService
314 ->expects(static::once())
315 ->method('addOrSet')
316 ->willReturnCallback(function (Bookmark $bookmark): Bookmark {
317 static::assertNull($bookmark->getThumbnail());
318
319 return $bookmark;
320 })
321 ;
322
323 $result = $this->controller->save($request, $response);
324
325 static::assertSame(302, $result->getStatusCode());
326 }
327
328 /**
268 * Change the password with a wrong existing password 329 * Change the password with a wrong existing password
269 */ 330 */
270 public function testSaveBookmarkFromBookmarklet(): void 331 public function testSaveBookmarkFromBookmarklet(): void
diff --git a/tests/front/controller/admin/ThumbnailsControllerTest.php b/tests/front/controller/admin/ThumbnailsControllerTest.php
index f4a8acff..e5749654 100644
--- a/tests/front/controller/admin/ThumbnailsControllerTest.php
+++ b/tests/front/controller/admin/ThumbnailsControllerTest.php
@@ -89,8 +89,10 @@ class ThumbnailsControllerTest extends TestCase
89 $this->container->bookmarkService 89 $this->container->bookmarkService
90 ->expects(static::once()) 90 ->expects(static::once())
91 ->method('set') 91 ->method('set')
92 ->willReturnCallback(function (Bookmark $bookmark) use ($thumb) { 92 ->willReturnCallback(function (Bookmark $bookmark) use ($thumb): Bookmark {
93 static::assertSame($thumb, $bookmark->getThumbnail()); 93 static::assertSame($thumb, $bookmark->getThumbnail());
94
95 return $bookmark;
94 }) 96 })
95 ; 97 ;
96 98
diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php
index 0c95df97..dec938f2 100644
--- a/tests/front/controller/visitor/BookmarkListControllerTest.php
+++ b/tests/front/controller/visitor/BookmarkListControllerTest.php
@@ -173,7 +173,7 @@ class BookmarkListControllerTest extends TestCase
173 $request = $this->createMock(Request::class); 173 $request = $this->createMock(Request::class);
174 $request->method('getParam')->willReturnCallback(function (string $key) { 174 $request->method('getParam')->willReturnCallback(function (string $key) {
175 if ('searchtags' === $key) { 175 if ('searchtags' === $key) {
176 return 'abc def'; 176 return 'abc@def';
177 } 177 }
178 if ('searchterm' === $key) { 178 if ('searchterm' === $key) {
179 return 'ghi jkl'; 179 return 'ghi jkl';
@@ -204,7 +204,7 @@ class BookmarkListControllerTest extends TestCase
204 ->expects(static::once()) 204 ->expects(static::once())
205 ->method('search') 205 ->method('search')
206 ->with( 206 ->with(
207 ['searchtags' => 'abc def', 'searchterm' => 'ghi jkl'], 207 ['searchtags' => 'abc@def', 'searchterm' => 'ghi jkl'],
208 'private', 208 'private',
209 false, 209 false,
210 true 210 true
@@ -222,7 +222,7 @@ class BookmarkListControllerTest extends TestCase
222 static::assertSame('linklist', (string) $result->getBody()); 222 static::assertSame('linklist', (string) $result->getBody());
223 223
224 static::assertSame('Search: ghi jkl [abc] [def] - Shaarli', $assignedVariables['pagetitle']); 224 static::assertSame('Search: ghi jkl [abc] [def] - Shaarli', $assignedVariables['pagetitle']);
225 static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc+def', $assignedVariables['previous_page_url']); 225 static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc%40def', $assignedVariables['previous_page_url']);
226 } 226 }
227 227
228 /** 228 /**
@@ -292,6 +292,37 @@ class BookmarkListControllerTest extends TestCase
292 } 292 }
293 293
294 /** 294 /**
295 * Test GET /shaare/{hash}?key={key} - Find a link by hash using a private link.
296 */
297 public function testPermalinkWithPrivateKey(): void
298 {
299 $hash = 'abcdef';
300 $privateKey = 'this is a private key';
301
302 $assignedVariables = [];
303 $this->assignTemplateVars($assignedVariables);
304
305 $request = $this->createMock(Request::class);
306 $request->method('getParam')->willReturnCallback(function (string $key, $default = null) use ($privateKey) {
307 return $key === 'key' ? $privateKey : $default;
308 });
309 $response = new Response();
310
311 $this->container->bookmarkService
312 ->expects(static::once())
313 ->method('findByHash')
314 ->with($hash, $privateKey)
315 ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld'))
316 ;
317
318 $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
319
320 static::assertSame(200, $result->getStatusCode());
321 static::assertSame('linklist', (string) $result->getBody());
322 static::assertCount(1, $assignedVariables['links']);
323 }
324
325 /**
295 * Test getting link list with thumbnail updates. 326 * Test getting link list with thumbnail updates.
296 * -> 2 thumbnails update, only 1 datastore write 327 * -> 2 thumbnails update, only 1 datastore write
297 */ 328 */
@@ -307,7 +338,13 @@ class BookmarkListControllerTest extends TestCase
307 $this->container->conf 338 $this->container->conf
308 ->method('get') 339 ->method('get')
309 ->willReturnCallback(function (string $key, $default) { 340 ->willReturnCallback(function (string $key, $default) {
310 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; 341 if ($key === 'thumbnails.mode') {
342 return Thumbnailer::MODE_ALL;
343 } elseif ($key === 'general.enable_async_metadata') {
344 return false;
345 }
346
347 return $default;
311 }) 348 })
312 ; 349 ;
313 350
@@ -357,7 +394,13 @@ class BookmarkListControllerTest extends TestCase
357 $this->container->conf 394 $this->container->conf
358 ->method('get') 395 ->method('get')
359 ->willReturnCallback(function (string $key, $default) { 396 ->willReturnCallback(function (string $key, $default) {
360 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; 397 if ($key === 'thumbnails.mode') {
398 return Thumbnailer::MODE_ALL;
399 } elseif ($key === 'general.enable_async_metadata') {
400 return false;
401 }
402
403 return $default;
361 }) 404 })
362 ; 405 ;
363 406
@@ -379,6 +422,47 @@ class BookmarkListControllerTest extends TestCase
379 } 422 }
380 423
381 /** 424 /**
425 * Test getting a permalink with thumbnail update with async setting: no update should run.
426 */
427 public function testThumbnailUpdateFromPermalinkAsync(): void
428 {
429 $request = $this->createMock(Request::class);
430 $response = new Response();
431
432 $this->container->loginManager = $this->createMock(LoginManager::class);
433 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
434
435 $this->container->conf = $this->createMock(ConfigManager::class);
436 $this->container->conf
437 ->method('get')
438 ->willReturnCallback(function (string $key, $default) {
439 if ($key === 'thumbnails.mode') {
440 return Thumbnailer::MODE_ALL;
441 } elseif ($key === 'general.enable_async_metadata') {
442 return true;
443 }
444
445 return $default;
446 })
447 ;
448
449 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
450 $this->container->thumbnailer->expects(static::never())->method('get');
451
452 $this->container->bookmarkService
453 ->expects(static::once())
454 ->method('findByHash')
455 ->willReturn((new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1'))
456 ;
457 $this->container->bookmarkService->expects(static::never())->method('set');
458 $this->container->bookmarkService->expects(static::never())->method('save');
459
460 $result = $this->controller->permalink($request, $response, ['hash' => 'abc']);
461
462 static::assertSame(200, $result->getStatusCode());
463 }
464
465 /**
382 * Trigger legacy controller in link list controller: permalink 466 * Trigger legacy controller in link list controller: permalink
383 */ 467 */
384 public function testLegacyControllerPermalink(): void 468 public function testLegacyControllerPermalink(): void
diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php
index fc78bc13..70fbce54 100644
--- a/tests/front/controller/visitor/DailyControllerTest.php
+++ b/tests/front/controller/visitor/DailyControllerTest.php
@@ -28,52 +28,49 @@ class DailyControllerTest extends TestCase
28 public function testValidIndexControllerInvokeDefault(): void 28 public function testValidIndexControllerInvokeDefault(): void
29 { 29 {
30 $currentDay = new \DateTimeImmutable('2020-05-13'); 30 $currentDay = new \DateTimeImmutable('2020-05-13');
31 $previousDate = new \DateTime('2 days ago 00:00:00');
32 $nextDate = new \DateTime('today 00:00:00');
31 33
32 $request = $this->createMock(Request::class); 34 $request = $this->createMock(Request::class);
33 $request->method('getQueryParam')->willReturn($currentDay->format('Ymd')); 35 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
36 return $key === 'day' ? $currentDay->format('Ymd') : null;
37 });
34 $response = new Response(); 38 $response = new Response();
35 39
36 // Save RainTPL assigned variables 40 // Save RainTPL assigned variables
37 $assignedVariables = []; 41 $assignedVariables = [];
38 $this->assignTemplateVars($assignedVariables); 42 $this->assignTemplateVars($assignedVariables);
39 43
40 // Links dataset: 2 links with thumbnails
41 $this->container->bookmarkService
42 ->expects(static::once())
43 ->method('days')
44 ->willReturnCallback(function () use ($currentDay): array {
45 return [
46 '20200510',
47 $currentDay->format('Ymd'),
48 '20200516',
49 ];
50 })
51 ;
52 $this->container->bookmarkService 44 $this->container->bookmarkService
53 ->expects(static::once()) 45 ->expects(static::once())
54 ->method('filterDay') 46 ->method('findByDate')
55 ->willReturnCallback(function (): array { 47 ->willReturnCallback(
56 return [ 48 function ($from, $to, &$previous, &$next) use ($currentDay, $previousDate, $nextDate): array {
57 (new Bookmark()) 49 $previous = $previousDate;
58 ->setId(1) 50 $next = $nextDate;
59 ->setUrl('http://url.tld') 51
60 ->setTitle(static::generateString(50)) 52 return [
61 ->setDescription(static::generateString(500)) 53 (new Bookmark())
62 , 54 ->setId(1)
63 (new Bookmark()) 55 ->setUrl('http://url.tld')
64 ->setId(2) 56 ->setTitle(static::generateString(50))
65 ->setUrl('http://url2.tld') 57 ->setDescription(static::generateString(500))
66 ->setTitle(static::generateString(50)) 58 ,
67 ->setDescription(static::generateString(500)) 59 (new Bookmark())
68 , 60 ->setId(2)
69 (new Bookmark()) 61 ->setUrl('http://url2.tld')
70 ->setId(3) 62 ->setTitle(static::generateString(50))
71 ->setUrl('http://url3.tld') 63 ->setDescription(static::generateString(500))
72 ->setTitle(static::generateString(50)) 64 ,
73 ->setDescription(static::generateString(500)) 65 (new Bookmark())
74 , 66 ->setId(3)
75 ]; 67 ->setUrl('http://url3.tld')
76 }) 68 ->setTitle(static::generateString(50))
69 ->setDescription(static::generateString(500))
70 ,
71 ];
72 }
73 )
77 ; 74 ;
78 75
79 // Make sure that PluginManager hook is triggered 76 // Make sure that PluginManager hook is triggered
@@ -81,20 +78,22 @@ class DailyControllerTest extends TestCase
81 ->expects(static::atLeastOnce()) 78 ->expects(static::atLeastOnce())
82 ->method('executeHooks') 79 ->method('executeHooks')
83 ->withConsecutive(['render_daily']) 80 ->withConsecutive(['render_daily'])
84 ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { 81 ->willReturnCallback(
85 if ('render_daily' === $hook) { 82 function (string $hook, array $data, array $param) use ($currentDay, $previousDate, $nextDate): array {
86 static::assertArrayHasKey('linksToDisplay', $data); 83 if ('render_daily' === $hook) {
87 static::assertCount(3, $data['linksToDisplay']); 84 static::assertArrayHasKey('linksToDisplay', $data);
88 static::assertSame(1, $data['linksToDisplay'][0]['id']); 85 static::assertCount(3, $data['linksToDisplay']);
89 static::assertSame($currentDay->getTimestamp(), $data['day']); 86 static::assertSame(1, $data['linksToDisplay'][0]['id']);
90 static::assertSame('20200510', $data['previousday']); 87 static::assertSame($currentDay->getTimestamp(), $data['day']);
91 static::assertSame('20200516', $data['nextday']); 88 static::assertSame($previousDate->format('Ymd'), $data['previousday']);
92 89 static::assertSame($nextDate->format('Ymd'), $data['nextday']);
93 static::assertArrayHasKey('loggedin', $param); 90
91 static::assertArrayHasKey('loggedin', $param);
92 }
93
94 return $data;
94 } 95 }
95 96 )
96 return $data;
97 })
98 ; 97 ;
99 98
100 $result = $this->controller->index($request, $response); 99 $result = $this->controller->index($request, $response);
@@ -107,6 +106,11 @@ class DailyControllerTest extends TestCase
107 ); 106 );
108 static::assertEquals($currentDay, $assignedVariables['dayDate']); 107 static::assertEquals($currentDay, $assignedVariables['dayDate']);
109 static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); 108 static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']);
109 static::assertSame($previousDate->format('Ymd'), $assignedVariables['previousday']);
110 static::assertSame($nextDate->format('Ymd'), $assignedVariables['nextday']);
111 static::assertSame('day', $assignedVariables['type']);
112 static::assertSame('May 13, 2020', $assignedVariables['dayDesc']);
113 static::assertSame('Daily', $assignedVariables['localizedType']);
110 static::assertCount(3, $assignedVariables['linksToDisplay']); 114 static::assertCount(3, $assignedVariables['linksToDisplay']);
111 115
112 $link = $assignedVariables['linksToDisplay'][0]; 116 $link = $assignedVariables['linksToDisplay'][0];
@@ -171,27 +175,20 @@ class DailyControllerTest extends TestCase
171 $currentDay = new \DateTimeImmutable('2020-05-13'); 175 $currentDay = new \DateTimeImmutable('2020-05-13');
172 176
173 $request = $this->createMock(Request::class); 177 $request = $this->createMock(Request::class);
178 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
179 return $key === 'day' ? $currentDay->format('Ymd') : null;
180 });
174 $response = new Response(); 181 $response = new Response();
175 182
176 // Save RainTPL assigned variables 183 // Save RainTPL assigned variables
177 $assignedVariables = []; 184 $assignedVariables = [];
178 $this->assignTemplateVars($assignedVariables); 185 $this->assignTemplateVars($assignedVariables);
179 186
180 // Links dataset: 2 links with thumbnails
181 $this->container->bookmarkService 187 $this->container->bookmarkService
182 ->expects(static::once()) 188 ->expects(static::once())
183 ->method('days') 189 ->method('findByDate')
184 ->willReturnCallback(function () use ($currentDay): array { 190 ->willReturnCallback(function () use ($currentDay): array {
185 return [ 191 return [
186 $currentDay->format($currentDay->format('Ymd')),
187 ];
188 })
189 ;
190 $this->container->bookmarkService
191 ->expects(static::once())
192 ->method('filterDay')
193 ->willReturnCallback(function (): array {
194 return [
195 (new Bookmark()) 192 (new Bookmark())
196 ->setId(1) 193 ->setId(1)
197 ->setUrl('http://url.tld') 194 ->setUrl('http://url.tld')
@@ -250,21 +247,11 @@ class DailyControllerTest extends TestCase
250 $assignedVariables = []; 247 $assignedVariables = [];
251 $this->assignTemplateVars($assignedVariables); 248 $this->assignTemplateVars($assignedVariables);
252 249
253 // Links dataset: 2 links with thumbnails
254 $this->container->bookmarkService 250 $this->container->bookmarkService
255 ->expects(static::once()) 251 ->expects(static::once())
256 ->method('days') 252 ->method('findByDate')
257 ->willReturnCallback(function () use ($currentDay): array { 253 ->willReturnCallback(function () use ($currentDay): array {
258 return [ 254 return [
259 $currentDay->format($currentDay->format('Ymd')),
260 ];
261 })
262 ;
263 $this->container->bookmarkService
264 ->expects(static::once())
265 ->method('filterDay')
266 ->willReturnCallback(function (): array {
267 return [
268 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), 255 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'),
269 (new Bookmark()) 256 (new Bookmark())
270 ->setId(2) 257 ->setId(2)
@@ -320,14 +307,7 @@ class DailyControllerTest extends TestCase
320 // Links dataset: 2 links with thumbnails 307 // Links dataset: 2 links with thumbnails
321 $this->container->bookmarkService 308 $this->container->bookmarkService
322 ->expects(static::once()) 309 ->expects(static::once())
323 ->method('days') 310 ->method('findByDate')
324 ->willReturnCallback(function (): array {
325 return [];
326 })
327 ;
328 $this->container->bookmarkService
329 ->expects(static::once())
330 ->method('filterDay')
331 ->willReturnCallback(function (): array { 311 ->willReturnCallback(function (): array {
332 return []; 312 return [];
333 }) 313 })
@@ -347,7 +327,7 @@ class DailyControllerTest extends TestCase
347 static::assertSame(200, $result->getStatusCode()); 327 static::assertSame(200, $result->getStatusCode());
348 static::assertSame('daily', (string) $result->getBody()); 328 static::assertSame('daily', (string) $result->getBody());
349 static::assertCount(0, $assignedVariables['linksToDisplay']); 329 static::assertCount(0, $assignedVariables['linksToDisplay']);
350 static::assertSame('Today', $assignedVariables['dayDesc']); 330 static::assertSame('Today - ' . (new \DateTime())->format('F j, Y'), $assignedVariables['dayDesc']);
351 static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); 331 static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
352 static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); 332 static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']);
353 } 333 }
@@ -361,6 +341,7 @@ class DailyControllerTest extends TestCase
361 new \DateTimeImmutable('2020-05-17'), 341 new \DateTimeImmutable('2020-05-17'),
362 new \DateTimeImmutable('2020-05-15'), 342 new \DateTimeImmutable('2020-05-15'),
363 new \DateTimeImmutable('2020-05-13'), 343 new \DateTimeImmutable('2020-05-13'),
344 new \DateTimeImmutable('+1 month'),
364 ]; 345 ];
365 346
366 $request = $this->createMock(Request::class); 347 $request = $this->createMock(Request::class);
@@ -371,6 +352,7 @@ class DailyControllerTest extends TestCase
371 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), 352 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
372 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), 353 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
373 (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), 354 (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
355 (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'),
374 ]); 356 ]);
375 357
376 $this->container->pageCacheManager 358 $this->container->pageCacheManager
@@ -397,13 +379,14 @@ class DailyControllerTest extends TestCase
397 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); 379 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
398 static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']); 380 static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
399 static::assertFalse($assignedVariables['hide_timestamps']); 381 static::assertFalse($assignedVariables['hide_timestamps']);
400 static::assertCount(2, $assignedVariables['days']); 382 static::assertCount(3, $assignedVariables['days']);
401 383
402 $day = $assignedVariables['days'][$dates[0]->format('Ymd')]; 384 $day = $assignedVariables['days'][$dates[0]->format('Ymd')];
385 $date = $dates[0]->setTime(23, 59, 59);
403 386
404 static::assertEquals($dates[0], $day['date']); 387 static::assertEquals($date, $day['date']);
405 static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']); 388 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
406 static::assertSame(format_date($dates[0], false), $day['date_human']); 389 static::assertSame(format_date($date, false), $day['date_human']);
407 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); 390 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
408 static::assertCount(1, $day['links']); 391 static::assertCount(1, $day['links']);
409 static::assertSame(1, $day['links'][0]['id']); 392 static::assertSame(1, $day['links'][0]['id']);
@@ -411,10 +394,11 @@ class DailyControllerTest extends TestCase
411 static::assertEquals($dates[0], $day['links'][0]['created']); 394 static::assertEquals($dates[0], $day['links'][0]['created']);
412 395
413 $day = $assignedVariables['days'][$dates[1]->format('Ymd')]; 396 $day = $assignedVariables['days'][$dates[1]->format('Ymd')];
397 $date = $dates[1]->setTime(23, 59, 59);
414 398
415 static::assertEquals($dates[1], $day['date']); 399 static::assertEquals($date, $day['date']);
416 static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']); 400 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
417 static::assertSame(format_date($dates[1], false), $day['date_human']); 401 static::assertSame(format_date($date, false), $day['date_human']);
418 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); 402 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
419 static::assertCount(2, $day['links']); 403 static::assertCount(2, $day['links']);
420 404
@@ -424,6 +408,18 @@ class DailyControllerTest extends TestCase
424 static::assertSame(3, $day['links'][1]['id']); 408 static::assertSame(3, $day['links'][1]['id']);
425 static::assertSame('http://domain.tld/3', $day['links'][1]['url']); 409 static::assertSame('http://domain.tld/3', $day['links'][1]['url']);
426 static::assertEquals($dates[1], $day['links'][1]['created']); 410 static::assertEquals($dates[1], $day['links'][1]['created']);
411
412 $day = $assignedVariables['days'][$dates[2]->format('Ymd')];
413 $date = $dates[2]->setTime(23, 59, 59);
414
415 static::assertEquals($date, $day['date']);
416 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
417 static::assertSame(format_date($date, false), $day['date_human']);
418 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[2]->format('Ymd'), $day['absolute_url']);
419 static::assertCount(1, $day['links']);
420 static::assertSame(4, $day['links'][0]['id']);
421 static::assertSame('http://domain.tld/4', $day['links'][0]['url']);
422 static::assertEquals($dates[2], $day['links'][0]['created']);
427 } 423 }
428 424
429 /** 425 /**
@@ -475,4 +471,246 @@ class DailyControllerTest extends TestCase
475 static::assertFalse($assignedVariables['hide_timestamps']); 471 static::assertFalse($assignedVariables['hide_timestamps']);
476 static::assertCount(0, $assignedVariables['days']); 472 static::assertCount(0, $assignedVariables['days']);
477 } 473 }
474
475 /**
476 * Test simple display index with week parameter
477 */
478 public function testSimpleIndexWeekly(): void
479 {
480 $currentDay = new \DateTimeImmutable('2020-05-13');
481 $expectedDay = new \DateTimeImmutable('2020-05-11');
482
483 $request = $this->createMock(Request::class);
484 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
485 return $key === 'week' ? $currentDay->format('YW') : null;
486 });
487 $response = new Response();
488
489 // Save RainTPL assigned variables
490 $assignedVariables = [];
491 $this->assignTemplateVars($assignedVariables);
492
493 $this->container->bookmarkService
494 ->expects(static::once())
495 ->method('findByDate')
496 ->willReturnCallback(
497 function (): array {
498 return [
499 (new Bookmark())
500 ->setId(1)
501 ->setUrl('http://url.tld')
502 ->setTitle(static::generateString(50))
503 ->setDescription(static::generateString(500))
504 ,
505 (new Bookmark())
506 ->setId(2)
507 ->setUrl('http://url2.tld')
508 ->setTitle(static::generateString(50))
509 ->setDescription(static::generateString(500))
510 ,
511 ];
512 }
513 )
514 ;
515
516 $result = $this->controller->index($request, $response);
517
518 static::assertSame(200, $result->getStatusCode());
519 static::assertSame('daily', (string) $result->getBody());
520 static::assertSame(
521 'Weekly - Week 20 (May 11, 2020) - Shaarli',
522 $assignedVariables['pagetitle']
523 );
524
525 static::assertCount(2, $assignedVariables['linksToDisplay']);
526 static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
527 static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
528 static::assertSame('', $assignedVariables['previousday']);
529 static::assertSame('', $assignedVariables['nextday']);
530 static::assertSame('Week 20 (May 11, 2020)', $assignedVariables['dayDesc']);
531 static::assertSame('week', $assignedVariables['type']);
532 static::assertSame('Weekly', $assignedVariables['localizedType']);
533 }
534
535 /**
536 * Test simple display index with month parameter
537 */
538 public function testSimpleIndexMonthly(): void
539 {
540 $currentDay = new \DateTimeImmutable('2020-05-13');
541 $expectedDay = new \DateTimeImmutable('2020-05-01');
542
543 $request = $this->createMock(Request::class);
544 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
545 return $key === 'month' ? $currentDay->format('Ym') : null;
546 });
547 $response = new Response();
548
549 // Save RainTPL assigned variables
550 $assignedVariables = [];
551 $this->assignTemplateVars($assignedVariables);
552
553 $this->container->bookmarkService
554 ->expects(static::once())
555 ->method('findByDate')
556 ->willReturnCallback(
557 function (): array {
558 return [
559 (new Bookmark())
560 ->setId(1)
561 ->setUrl('http://url.tld')
562 ->setTitle(static::generateString(50))
563 ->setDescription(static::generateString(500))
564 ,
565 (new Bookmark())
566 ->setId(2)
567 ->setUrl('http://url2.tld')
568 ->setTitle(static::generateString(50))
569 ->setDescription(static::generateString(500))
570 ,
571 ];
572 }
573 )
574 ;
575
576 $result = $this->controller->index($request, $response);
577
578 static::assertSame(200, $result->getStatusCode());
579 static::assertSame('daily', (string) $result->getBody());
580 static::assertSame(
581 'Monthly - May, 2020 - Shaarli',
582 $assignedVariables['pagetitle']
583 );
584
585 static::assertCount(2, $assignedVariables['linksToDisplay']);
586 static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
587 static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
588 static::assertSame('', $assignedVariables['previousday']);
589 static::assertSame('', $assignedVariables['nextday']);
590 static::assertSame('May, 2020', $assignedVariables['dayDesc']);
591 static::assertSame('month', $assignedVariables['type']);
592 static::assertSame('Monthly', $assignedVariables['localizedType']);
593 }
594
595 /**
596 * Test simple display RSS with week parameter
597 */
598 public function testSimpleRssWeekly(): void
599 {
600 $dates = [
601 new \DateTimeImmutable('2020-05-19'),
602 new \DateTimeImmutable('2020-05-13'),
603 ];
604 $expectedDates = [
605 new \DateTimeImmutable('2020-05-24 23:59:59'),
606 new \DateTimeImmutable('2020-05-17 23:59:59'),
607 ];
608
609 $this->container->environment['QUERY_STRING'] = 'week';
610 $request = $this->createMock(Request::class);
611 $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
612 return $key === 'week' ? '' : null;
613 });
614 $response = new Response();
615
616 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
617 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
618 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
619 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
620 ]);
621
622 // Save RainTPL assigned variables
623 $assignedVariables = [];
624 $this->assignTemplateVars($assignedVariables);
625
626 $result = $this->controller->rss($request, $response);
627
628 static::assertSame(200, $result->getStatusCode());
629 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
630 static::assertSame('dailyrss', (string) $result->getBody());
631 static::assertSame('Shaarli', $assignedVariables['title']);
632 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
633 static::assertSame('http://shaarli/subfolder/daily-rss?week', $assignedVariables['page_url']);
634 static::assertFalse($assignedVariables['hide_timestamps']);
635 static::assertCount(2, $assignedVariables['days']);
636
637 $day = $assignedVariables['days'][$dates[0]->format('YW')];
638 $date = $expectedDates[0];
639
640 static::assertEquals($date, $day['date']);
641 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
642 static::assertSame('Week 21 (May 18, 2020)', $day['date_human']);
643 static::assertSame('http://shaarli/subfolder/daily?week='. $dates[0]->format('YW'), $day['absolute_url']);
644 static::assertCount(1, $day['links']);
645
646 $day = $assignedVariables['days'][$dates[1]->format('YW')];
647 $date = $expectedDates[1];
648
649 static::assertEquals($date, $day['date']);
650 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
651 static::assertSame('Week 20 (May 11, 2020)', $day['date_human']);
652 static::assertSame('http://shaarli/subfolder/daily?week='. $dates[1]->format('YW'), $day['absolute_url']);
653 static::assertCount(2, $day['links']);
654 }
655
656 /**
657 * Test simple display RSS with month parameter
658 */
659 public function testSimpleRssMonthly(): void
660 {
661 $dates = [
662 new \DateTimeImmutable('2020-05-19'),
663 new \DateTimeImmutable('2020-04-13'),
664 ];
665 $expectedDates = [
666 new \DateTimeImmutable('2020-05-31 23:59:59'),
667 new \DateTimeImmutable('2020-04-30 23:59:59'),
668 ];
669
670 $this->container->environment['QUERY_STRING'] = 'month';
671 $request = $this->createMock(Request::class);
672 $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
673 return $key === 'month' ? '' : null;
674 });
675 $response = new Response();
676
677 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
678 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
679 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
680 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
681 ]);
682
683 // Save RainTPL assigned variables
684 $assignedVariables = [];
685 $this->assignTemplateVars($assignedVariables);
686
687 $result = $this->controller->rss($request, $response);
688
689 static::assertSame(200, $result->getStatusCode());
690 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
691 static::assertSame('dailyrss', (string) $result->getBody());
692 static::assertSame('Shaarli', $assignedVariables['title']);
693 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
694 static::assertSame('http://shaarli/subfolder/daily-rss?month', $assignedVariables['page_url']);
695 static::assertFalse($assignedVariables['hide_timestamps']);
696 static::assertCount(2, $assignedVariables['days']);
697
698 $day = $assignedVariables['days'][$dates[0]->format('Ym')];
699 $date = $expectedDates[0];
700
701 static::assertEquals($date, $day['date']);
702 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
703 static::assertSame('May, 2020', $day['date_human']);
704 static::assertSame('http://shaarli/subfolder/daily?month='. $dates[0]->format('Ym'), $day['absolute_url']);
705 static::assertCount(1, $day['links']);
706
707 $day = $assignedVariables['days'][$dates[1]->format('Ym')];
708 $date = $expectedDates[1];
709
710 static::assertEquals($date, $day['date']);
711 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
712 static::assertSame('April, 2020', $day['date_human']);
713 static::assertSame('http://shaarli/subfolder/daily?month='. $dates[1]->format('Ym'), $day['absolute_url']);
714 static::assertCount(2, $day['links']);
715 }
478} 716}
diff --git a/tests/front/controller/visitor/ErrorControllerTest.php b/tests/front/controller/visitor/ErrorControllerTest.php
index 75408cf4..e18a6fa2 100644
--- a/tests/front/controller/visitor/ErrorControllerTest.php
+++ b/tests/front/controller/visitor/ErrorControllerTest.php
@@ -50,7 +50,31 @@ class ErrorControllerTest extends TestCase
50 } 50 }
51 51
52 /** 52 /**
53 * Test displaying error with any exception (no debug): only display an error occurred with HTTP 500. 53 * Test displaying error with any exception (no debug) while logged in:
54 * display full error details
55 */
56 public function testDisplayAnyExceptionErrorNoDebugLoggedIn(): void
57 {
58 $request = $this->createMock(Request::class);
59 $response = new Response();
60
61 // Save RainTPL assigned variables
62 $assignedVariables = [];
63 $this->assignTemplateVars($assignedVariables);
64
65 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
66
67 $result = ($this->controller)($request, $response, new \Exception('abc'));
68
69 static::assertSame(500, $result->getStatusCode());
70 static::assertSame('Error: abc', $assignedVariables['message']);
71 static::assertContainsPolyfill('Please report it on Github', $assignedVariables['text']);
72 static::assertArrayHasKey('stacktrace', $assignedVariables);
73 }
74
75 /**
76 * Test displaying error with any exception (no debug) while logged out:
77 * display standard error without detail
54 */ 78 */
55 public function testDisplayAnyExceptionErrorNoDebug(): void 79 public function testDisplayAnyExceptionErrorNoDebug(): void
56 { 80 {
@@ -61,10 +85,13 @@ class ErrorControllerTest extends TestCase
61 $assignedVariables = []; 85 $assignedVariables = [];
62 $this->assignTemplateVars($assignedVariables); 86 $this->assignTemplateVars($assignedVariables);
63 87
88 $this->container->loginManager->method('isLoggedIn')->willReturn(false);
89
64 $result = ($this->controller)($request, $response, new \Exception('abc')); 90 $result = ($this->controller)($request, $response, new \Exception('abc'));
65 91
66 static::assertSame(500, $result->getStatusCode()); 92 static::assertSame(500, $result->getStatusCode());
67 static::assertSame('An unexpected error occurred.', $assignedVariables['message']); 93 static::assertSame('An unexpected error occurred.', $assignedVariables['message']);
94 static::assertArrayNotHasKey('text', $assignedVariables);
68 static::assertArrayNotHasKey('stacktrace', $assignedVariables); 95 static::assertArrayNotHasKey('stacktrace', $assignedVariables);
69 } 96 }
70} 97}
diff --git a/tests/front/controller/visitor/FrontControllerMockHelper.php b/tests/front/controller/visitor/FrontControllerMockHelper.php
index fc0bb7d1..02229f68 100644
--- a/tests/front/controller/visitor/FrontControllerMockHelper.php
+++ b/tests/front/controller/visitor/FrontControllerMockHelper.php
@@ -41,6 +41,10 @@ trait FrontControllerMockHelper
41 // Config 41 // Config
42 $this->container->conf = $this->createMock(ConfigManager::class); 42 $this->container->conf = $this->createMock(ConfigManager::class);
43 $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { 43 $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
44 if ($parameter === 'general.tags_separator') {
45 return '@';
46 }
47
44 return $default === null ? $parameter : $default; 48 return $default === null ? $parameter : $default;
45 }); 49 });
46 50
diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php
index 345ad544..2105ed77 100644
--- a/tests/front/controller/visitor/InstallControllerTest.php
+++ b/tests/front/controller/visitor/InstallControllerTest.php
@@ -79,6 +79,15 @@ class InstallControllerTest extends TestCase
79 static::assertIsArray($assignedVariables['languages']); 79 static::assertIsArray($assignedVariables['languages']);
80 static::assertSame('Automatic', $assignedVariables['languages']['auto']); 80 static::assertSame('Automatic', $assignedVariables['languages']['auto']);
81 static::assertSame('French', $assignedVariables['languages']['fr']); 81 static::assertSame('French', $assignedVariables['languages']['fr']);
82
83 static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
84 static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
85 static::assertArrayHasKey('php_eol', $assignedVariables);
86 static::assertArrayHasKey('php_extensions', $assignedVariables);
87 static::assertArrayHasKey('permissions', $assignedVariables);
88 static::assertEmpty($assignedVariables['permissions']);
89
90 static::assertSame('Install Shaarli', $assignedVariables['pagetitle']);
82 } 91 }
83 92
84 /** 93 /**
diff --git a/tests/front/controller/visitor/LoginControllerTest.php b/tests/front/controller/visitor/LoginControllerTest.php
index 1312ccb7..00d9eab3 100644
--- a/tests/front/controller/visitor/LoginControllerTest.php
+++ b/tests/front/controller/visitor/LoginControllerTest.php
@@ -195,7 +195,7 @@ class LoginControllerTest extends TestCase
195 $this->container->loginManager 195 $this->container->loginManager
196 ->expects(static::once()) 196 ->expects(static::once())
197 ->method('checkCredentials') 197 ->method('checkCredentials')
198 ->with('1.2.3.4', '1.2.3.4', 'bob', 'pass') 198 ->with('1.2.3.4', 'bob', 'pass')
199 ->willReturn(true) 199 ->willReturn(true)
200 ; 200 ;
201 $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8))); 201 $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
diff --git a/tests/front/controller/visitor/TagCloudControllerTest.php b/tests/front/controller/visitor/TagCloudControllerTest.php
index 9305612e..4915573d 100644
--- a/tests/front/controller/visitor/TagCloudControllerTest.php
+++ b/tests/front/controller/visitor/TagCloudControllerTest.php
@@ -100,7 +100,7 @@ class TagCloudControllerTest extends TestCase
100 ->with() 100 ->with()
101 ->willReturnCallback(function (string $key): ?string { 101 ->willReturnCallback(function (string $key): ?string {
102 if ('searchtags' === $key) { 102 if ('searchtags' === $key) {
103 return 'ghi def'; 103 return 'ghi@def';
104 } 104 }
105 105
106 return null; 106 return null;
@@ -131,7 +131,7 @@ class TagCloudControllerTest extends TestCase
131 ->withConsecutive(['render_tagcloud']) 131 ->withConsecutive(['render_tagcloud'])
132 ->willReturnCallback(function (string $hook, array $data, array $param): array { 132 ->willReturnCallback(function (string $hook, array $data, array $param): array {
133 if ('render_tagcloud' === $hook) { 133 if ('render_tagcloud' === $hook) {
134 static::assertSame('ghi def', $data['search_tags']); 134 static::assertSame('ghi@def@', $data['search_tags']);
135 static::assertCount(1, $data['tags']); 135 static::assertCount(1, $data['tags']);
136 136
137 static::assertArrayHasKey('loggedin', $param); 137 static::assertArrayHasKey('loggedin', $param);
@@ -147,7 +147,7 @@ class TagCloudControllerTest extends TestCase
147 static::assertSame('tag.cloud', (string) $result->getBody()); 147 static::assertSame('tag.cloud', (string) $result->getBody());
148 static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']); 148 static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']);
149 149
150 static::assertSame('ghi def', $assignedVariables['search_tags']); 150 static::assertSame('ghi@def@', $assignedVariables['search_tags']);
151 static::assertCount(1, $assignedVariables['tags']); 151 static::assertCount(1, $assignedVariables['tags']);
152 152
153 static::assertArrayHasKey('abc', $assignedVariables['tags']); 153 static::assertArrayHasKey('abc', $assignedVariables['tags']);
@@ -277,7 +277,7 @@ class TagCloudControllerTest extends TestCase
277 ->with() 277 ->with()
278 ->willReturnCallback(function (string $key): ?string { 278 ->willReturnCallback(function (string $key): ?string {
279 if ('searchtags' === $key) { 279 if ('searchtags' === $key) {
280 return 'ghi def'; 280 return 'ghi@def';
281 } elseif ('sort' === $key) { 281 } elseif ('sort' === $key) {
282 return 'alpha'; 282 return 'alpha';
283 } 283 }
@@ -310,7 +310,7 @@ class TagCloudControllerTest extends TestCase
310 ->withConsecutive(['render_taglist']) 310 ->withConsecutive(['render_taglist'])
311 ->willReturnCallback(function (string $hook, array $data, array $param): array { 311 ->willReturnCallback(function (string $hook, array $data, array $param): array {
312 if ('render_taglist' === $hook) { 312 if ('render_taglist' === $hook) {
313 static::assertSame('ghi def', $data['search_tags']); 313 static::assertSame('ghi@def@', $data['search_tags']);
314 static::assertCount(1, $data['tags']); 314 static::assertCount(1, $data['tags']);
315 315
316 static::assertArrayHasKey('loggedin', $param); 316 static::assertArrayHasKey('loggedin', $param);
@@ -326,7 +326,7 @@ class TagCloudControllerTest extends TestCase
326 static::assertSame('tag.list', (string) $result->getBody()); 326 static::assertSame('tag.list', (string) $result->getBody());
327 static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']); 327 static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']);
328 328
329 static::assertSame('ghi def', $assignedVariables['search_tags']); 329 static::assertSame('ghi@def@', $assignedVariables['search_tags']);
330 static::assertCount(1, $assignedVariables['tags']); 330 static::assertCount(1, $assignedVariables['tags']);
331 static::assertSame(3, $assignedVariables['tags']['abc']); 331 static::assertSame(3, $assignedVariables['tags']['abc']);
332 } 332 }
diff --git a/tests/front/controller/visitor/TagControllerTest.php b/tests/front/controller/visitor/TagControllerTest.php
index 750ea02d..5a556c6d 100644
--- a/tests/front/controller/visitor/TagControllerTest.php
+++ b/tests/front/controller/visitor/TagControllerTest.php
@@ -50,7 +50,7 @@ class TagControllerTest extends TestCase
50 50
51 static::assertInstanceOf(Response::class, $result); 51 static::assertInstanceOf(Response::class, $result);
52 static::assertSame(302, $result->getStatusCode()); 52 static::assertSame(302, $result->getStatusCode());
53 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); 53 static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
54 } 54 }
55 55
56 public function testAddTagWithoutRefererAndExistingSearch(): void 56 public function testAddTagWithoutRefererAndExistingSearch(): void
@@ -80,7 +80,7 @@ class TagControllerTest extends TestCase
80 80
81 static::assertInstanceOf(Response::class, $result); 81 static::assertInstanceOf(Response::class, $result);
82 static::assertSame(302, $result->getStatusCode()); 82 static::assertSame(302, $result->getStatusCode());
83 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); 83 static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
84 } 84 }
85 85
86 public function testAddTagResetPagination(): void 86 public function testAddTagResetPagination(): void
@@ -96,7 +96,7 @@ class TagControllerTest extends TestCase
96 96
97 static::assertInstanceOf(Response::class, $result); 97 static::assertInstanceOf(Response::class, $result);
98 static::assertSame(302, $result->getStatusCode()); 98 static::assertSame(302, $result->getStatusCode());
99 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); 99 static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
100 } 100 }
101 101
102 public function testAddTagWithRefererAndEmptySearch(): void 102 public function testAddTagWithRefererAndEmptySearch(): void
diff --git a/tests/ApplicationUtilsTest.php b/tests/helper/ApplicationUtilsTest.php
index a232b351..654857b9 100644
--- a/tests/ApplicationUtilsTest.php
+++ b/tests/helper/ApplicationUtilsTest.php
@@ -1,7 +1,8 @@
1<?php 1<?php
2namespace Shaarli; 2namespace Shaarli\Helper;
3 3
4use Shaarli\Config\ConfigManager; 4use Shaarli\Config\ConfigManager;
5use Shaarli\FakeApplicationUtils;
5 6
6require_once 'tests/utils/FakeApplicationUtils.php'; 7require_once 'tests/utils/FakeApplicationUtils.php';
7 8
@@ -340,6 +341,35 @@ class ApplicationUtilsTest extends \Shaarli\TestCase
340 } 341 }
341 342
342 /** 343 /**
344 * Checks resource permissions in minimal mode.
345 */
346 public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void
347 {
348 $conf = new ConfigManager('');
349 $conf->set('resource.thumbnails_cache', 'null/cache');
350 $conf->set('resource.config', 'null/data/config.php');
351 $conf->set('resource.data_dir', 'null/data');
352 $conf->set('resource.datastore', 'null/data/store.php');
353 $conf->set('resource.ban_file', 'null/data/ipbans.php');
354 $conf->set('resource.log', 'null/data/log.txt');
355 $conf->set('resource.page_cache', 'null/pagecache');
356 $conf->set('resource.raintpl_tmp', 'null/tmp');
357 $conf->set('resource.raintpl_tpl', 'null/tpl');
358 $conf->set('resource.raintpl_theme', 'null/tpl/default');
359 $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt');
360
361 static::assertSame(
362 [
363 '"null/tpl" directory is not readable',
364 '"null/tpl/default" directory is not readable',
365 '"null/tmp" directory is not readable',
366 '"null/tmp" directory is not writable'
367 ],
368 ApplicationUtils::checkResourcePermissions($conf, true)
369 );
370 }
371
372 /**
343 * Check update with 'dev' as curent version (master branch). 373 * Check update with 'dev' as curent version (master branch).
344 * It should always return false. 374 * It should always return false.
345 */ 375 */
@@ -349,4 +379,37 @@ class ApplicationUtilsTest extends \Shaarli\TestCase
349 ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) 379 ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true)
350 ); 380 );
351 } 381 }
382
383 /**
384 * Basic test of getPhpExtensionsRequirement()
385 */
386 public function testGetPhpExtensionsRequirementSimple(): void
387 {
388 static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement());
389 static::assertSame([
390 'name' => 'json',
391 'required' => true,
392 'desc' => 'Configuration parsing',
393 'loaded' => true,
394 ], ApplicationUtils::getPhpExtensionsRequirement()[0]);
395 }
396
397 /**
398 * Test getPhpEol with a known version: 7.4 -> 2022
399 */
400 public function testGetKnownPhpEol(): void
401 {
402 static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7'));
403 }
404
405 /**
406 * Test getPhpEol with an unknown version: 7.4 -> 2022
407 */
408 public function testGetUnknownPhpEol(): void
409 {
410 static::assertSame(
411 (((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'),
412 ApplicationUtils::getPhpEol('7.51.34')
413 );
414 }
352} 415}
diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php
new file mode 100644
index 00000000..5255b7b1
--- /dev/null
+++ b/tests/helper/DailyPageHelperTest.php
@@ -0,0 +1,262 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Helper;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10
11class DailyPageHelperTest extends TestCase
12{
13 /**
14 * @dataProvider getRequestedTypes
15 */
16 public function testExtractRequestedType(array $queryParams, string $expectedType): void
17 {
18 $request = $this->createMock(Request::class);
19 $request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string {
20 return $queryParams[$key] ?? null;
21 });
22
23 $type = DailyPageHelper::extractRequestedType($request);
24
25 static::assertSame($type, $expectedType);
26 }
27
28 /**
29 * @dataProvider getRequestedDateTimes
30 */
31 public function testExtractRequestedDateTime(
32 string $type,
33 string $input,
34 ?Bookmark $bookmark,
35 \DateTimeInterface $expectedDateTime,
36 string $compareFormat = 'Ymd'
37 ): void {
38 $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
39
40 static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat));
41 }
42
43 public function testExtractRequestedDateTimeExceptionUnknownType(): void
44 {
45 $this->expectException(\Exception::class);
46 $this->expectExceptionMessage('Unsupported daily format type');
47
48 DailyPageHelper::extractRequestedDateTime('nope', null, null);
49 }
50
51 /**
52 * @dataProvider getFormatsByType
53 */
54 public function testGetFormatByType(string $type, string $expectedFormat): void
55 {
56 $format = DailyPageHelper::getFormatByType($type);
57
58 static::assertSame($expectedFormat, $format);
59 }
60
61 public function testGetFormatByTypeExceptionUnknownType(): void
62 {
63 $this->expectException(\Exception::class);
64 $this->expectExceptionMessage('Unsupported daily format type');
65
66 DailyPageHelper::getFormatByType('nope');
67 }
68
69 /**
70 * @dataProvider getStartDatesByType
71 */
72 public function testGetStartDatesByType(
73 string $type,
74 \DateTimeImmutable $dateTime,
75 \DateTimeInterface $expectedDateTime
76 ): void {
77 $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
78
79 static::assertEquals($expectedDateTime, $startDateTime);
80 }
81
82 public function testGetStartDatesByTypeExceptionUnknownType(): void
83 {
84 $this->expectException(\Exception::class);
85 $this->expectExceptionMessage('Unsupported daily format type');
86
87 DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
88 }
89
90 /**
91 * @dataProvider getEndDatesByType
92 */
93 public function testGetEndDatesByType(
94 string $type,
95 \DateTimeImmutable $dateTime,
96 \DateTimeInterface $expectedDateTime
97 ): void {
98 $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
99
100 static::assertEquals($expectedDateTime, $endDateTime);
101 }
102
103 public function testGetEndDatesByTypeExceptionUnknownType(): void
104 {
105 $this->expectException(\Exception::class);
106 $this->expectExceptionMessage('Unsupported daily format type');
107
108 DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
109 }
110
111 /**
112 * @dataProvider getDescriptionsByType
113 */
114 public function testGeDescriptionsByType(
115 string $type,
116 \DateTimeImmutable $dateTime,
117 string $expectedDescription
118 ): void {
119 $description = DailyPageHelper::getDescriptionByType($type, $dateTime);
120
121 static::assertEquals($expectedDescription, $description);
122 }
123
124 public function getDescriptionByTypeExceptionUnknownType(): void
125 {
126 $this->expectException(\Exception::class);
127 $this->expectExceptionMessage('Unsupported daily format type');
128
129 DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable());
130 }
131
132 /**
133 * @dataProvider getRssLengthsByType
134 */
135 public function testGeRssLengthsByType(string $type): void {
136 $length = DailyPageHelper::getRssLengthByType($type);
137
138 static::assertIsInt($length);
139 }
140
141 public function testGeRssLengthsByTypeExceptionUnknownType(): void
142 {
143 $this->expectException(\Exception::class);
144 $this->expectExceptionMessage('Unsupported daily format type');
145
146 DailyPageHelper::getRssLengthByType('nope');
147 }
148
149 /**
150 * Data provider for testExtractRequestedType() test method.
151 */
152 public function getRequestedTypes(): array
153 {
154 return [
155 [['month' => null], DailyPageHelper::DAY],
156 [['month' => ''], DailyPageHelper::MONTH],
157 [['month' => 'content'], DailyPageHelper::MONTH],
158 [['week' => null], DailyPageHelper::DAY],
159 [['week' => ''], DailyPageHelper::WEEK],
160 [['week' => 'content'], DailyPageHelper::WEEK],
161 [['day' => null], DailyPageHelper::DAY],
162 [['day' => ''], DailyPageHelper::DAY],
163 [['day' => 'content'], DailyPageHelper::DAY],
164 ];
165 }
166
167 /**
168 * Data provider for testExtractRequestedDateTime() test method.
169 */
170 public function getRequestedDateTimes(): array
171 {
172 return [
173 [DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')],
174 [
175 DailyPageHelper::DAY,
176 '',
177 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
178 $date,
179 ],
180 [DailyPageHelper::DAY, '', null, new \DateTime()],
181 [DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')],
182 [
183 DailyPageHelper::WEEK,
184 '',
185 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
186 new \DateTime('2020-10-13'),
187 ],
188 [DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'],
189 [DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'],
190 [
191 DailyPageHelper::MONTH,
192 '',
193 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
194 new \DateTime('2020-10-13'),
195 'Ym'
196 ],
197 [DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'],
198 ];
199 }
200
201 /**
202 * Data provider for testGetFormatByType() test method.
203 */
204 public function getFormatsByType(): array
205 {
206 return [
207 [DailyPageHelper::DAY, 'Ymd'],
208 [DailyPageHelper::WEEK, 'YW'],
209 [DailyPageHelper::MONTH, 'Ym'],
210 ];
211 }
212
213 /**
214 * Data provider for testGetStartDatesByType() test method.
215 */
216 public function getStartDatesByType(): array
217 {
218 return [
219 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
220 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
221 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
222 ];
223 }
224
225 /**
226 * Data provider for testGetEndDatesByType() test method.
227 */
228 public function getEndDatesByType(): array
229 {
230 return [
231 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
232 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
233 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
234 ];
235 }
236
237 /**
238 * Data provider for testGetDescriptionsByType() test method.
239 */
240 public function getDescriptionsByType(): array
241 {
242 return [
243 [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
244 [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
245 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
246 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
247 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
248 ];
249 }
250
251 /**
252 * Data provider for testGetDescriptionsByType() test method.
253 */
254 public function getRssLengthsByType(): array
255 {
256 return [
257 [DailyPageHelper::DAY],
258 [DailyPageHelper::WEEK],
259 [DailyPageHelper::MONTH],
260 ];
261 }
262}
diff --git a/tests/FileUtilsTest.php b/tests/helper/FileUtilsTest.php
index 9163bdf1..8035f79c 100644
--- a/tests/FileUtilsTest.php
+++ b/tests/helper/FileUtilsTest.php
@@ -1,27 +1,51 @@
1<?php 1<?php
2 2
3namespace Shaarli; 3namespace Shaarli\Helper;
4 4
5use Exception; 5use Exception;
6use Shaarli\Exceptions\IOException;
7use 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 */
12class FileUtilsTest extends \Shaarli\TestCase 14class FileUtilsTest extends TestCase
13{ 15{
14 /** 16 /**
15 * @var string Test file path. 17 * @var string Test file path.
16 */ 18 */
17 protected static $file = 'sandbox/flat.db'; 19 protected static $file = 'sandbox/flat.db';
18 20
21 protected function setUp(): void
22 {
23 @mkdir('sandbox');
24 mkdir('sandbox/folder2');
25 touch('sandbox/file1');
26 touch('sandbox/file2');
27 mkdir('sandbox/folder1');
28 touch('sandbox/folder1/file1');
29 touch('sandbox/folder1/file2');
30 mkdir('sandbox/folder3');
31 mkdir('/tmp/shaarli-to-delete');
32 }
33
19 /** 34 /**
20 * Delete test file after every test. 35 * Delete test file after every test.
21 */ 36 */
22 protected function tearDown(): void 37 protected function tearDown(): void
23 { 38 {
24 @unlink(self::$file); 39 @unlink(self::$file);
40
41 @unlink('sandbox/folder1/file1');
42 @unlink('sandbox/folder1/file2');
43 @rmdir('sandbox/folder1');
44 @unlink('sandbox/file1');
45 @unlink('sandbox/file2');
46 @rmdir('sandbox/folder2');
47 @rmdir('sandbox/folder3');
48 @rmdir('/tmp/shaarli-to-delete');
25 } 49 }
26 50
27 /** 51 /**
@@ -107,4 +131,67 @@ class FileUtilsTest extends \Shaarli\TestCase
107 $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); 131 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
108 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); 132 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
109 } 133 }
134
135 /**
136 * Test clearFolder with self delete and excluded files
137 */
138 public function testClearFolderSelfDeleteWithExclusion(): void
139 {
140 FileUtils::clearFolder('sandbox', true, ['file2']);
141
142 static::assertFileExists('sandbox/folder1/file2');
143 static::assertFileExists('sandbox/folder1');
144 static::assertFileExists('sandbox/file2');
145 static::assertFileExists('sandbox');
146
147 static::assertFileNotExists('sandbox/folder1/file1');
148 static::assertFileNotExists('sandbox/file1');
149 static::assertFileNotExists('sandbox/folder3');
150 }
151
152 /**
153 * Test clearFolder with self delete and excluded files
154 */
155 public function testClearFolderSelfDeleteWithoutExclusion(): void
156 {
157 FileUtils::clearFolder('sandbox', true);
158
159 static::assertFileNotExists('sandbox');
160 }
161
162 /**
163 * Test clearFolder with self delete and excluded files
164 */
165 public function testClearFolderNoSelfDeleteWithoutExclusion(): void
166 {
167 FileUtils::clearFolder('sandbox', false);
168
169 static::assertFileExists('sandbox');
170
171 // 2 because '.' and '..'
172 static::assertCount(2, new \DirectoryIterator('sandbox'));
173 }
174
175 /**
176 * Test clearFolder on a file instead of a folder
177 */
178 public function testClearFolderOnANonDirectory(): void
179 {
180 $this->expectException(IOException::class);
181 $this->expectExceptionMessage('Provided path is not a directory.');
182
183 FileUtils::clearFolder('sandbox/file1', false);
184 }
185
186 /**
187 * Test clearFolder on a file instead of a folder
188 */
189 public function testClearFolderOutsideOfShaarliDirectory(): void
190 {
191 $this->expectException(IOException::class);
192 $this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.');
193
194
195 FileUtils::clearFolder('/tmp/shaarli-to-delete', true);
196 }
110} 197}
diff --git a/tests/http/MetadataRetrieverTest.php b/tests/http/MetadataRetrieverTest.php
new file mode 100644
index 00000000..3c9eaa0e
--- /dev/null
+++ b/tests/http/MetadataRetrieverTest.php
@@ -0,0 +1,154 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Http;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9
10class MetadataRetrieverTest extends TestCase
11{
12 /** @var MetadataRetriever */
13 protected $retriever;
14
15 /** @var ConfigManager */
16 protected $conf;
17
18 /** @var HttpAccess */
19 protected $httpAccess;
20
21 public function setUp(): void
22 {
23 $this->conf = $this->createMock(ConfigManager::class);
24 $this->httpAccess = $this->createMock(HttpAccess::class);
25 $this->retriever = new MetadataRetriever($this->conf, $this->httpAccess);
26
27 $this->conf->method('get')->willReturnCallback(function (string $param, $default) {
28 return $default === null ? $param : $default;
29 });
30 }
31
32 /**
33 * Test metadata retrieve() with values returned
34 */
35 public function testFullRetrieval(): void
36 {
37 $url = 'https://domain.tld/link';
38 $remoteTitle = 'Remote Title ';
39 $remoteDesc = 'Sometimes the meta description is relevant.';
40 $remoteTags = 'abc def';
41 $remoteCharset = 'utf-8';
42
43 $expectedResult = [
44 'title' => $remoteTitle,
45 'description' => $remoteDesc,
46 'tags' => $remoteTags,
47 ];
48
49 $this->httpAccess
50 ->expects(static::once())
51 ->method('getCurlHeaderCallback')
52 ->willReturnCallback(
53 function (&$charset) use (
54 $remoteCharset
55 ): callable {
56 return function () use (
57 &$charset,
58 $remoteCharset
59 ): void {
60 $charset = $remoteCharset;
61 };
62 }
63 )
64 ;
65 $this->httpAccess
66 ->expects(static::once())
67 ->method('getCurlDownloadCallback')
68 ->willReturnCallback(
69 function (&$charset, &$title, &$description, &$tags) use (
70 $remoteCharset,
71 $remoteTitle,
72 $remoteDesc,
73 $remoteTags
74 ): callable {
75 return function () use (
76 &$charset,
77 &$title,
78 &$description,
79 &$tags,
80 $remoteCharset,
81 $remoteTitle,
82 $remoteDesc,
83 $remoteTags
84 ): void {
85 static::assertSame($remoteCharset, $charset);
86
87 $title = $remoteTitle;
88 $description = $remoteDesc;
89 $tags = $remoteTags;
90 };
91 }
92 )
93 ;
94 $this->httpAccess
95 ->expects(static::once())
96 ->method('getHttpResponse')
97 ->with($url, 30, 4194304)
98 ->willReturnCallback(function($url, $timeout, $maxBytes, $headerCallback, $dlCallback): void {
99 $headerCallback();
100 $dlCallback();
101 })
102 ;
103
104 $result = $this->retriever->retrieve($url);
105
106 static::assertSame($expectedResult, $result);
107 }
108
109 /**
110 * Test metadata retrieve() without any value
111 */
112 public function testEmptyRetrieval(): void
113 {
114 $url = 'https://domain.tld/link';
115
116 $expectedResult = [
117 'title' => null,
118 'description' => null,
119 'tags' => null,
120 ];
121
122 $this->httpAccess
123 ->expects(static::once())
124 ->method('getCurlDownloadCallback')
125 ->willReturnCallback(
126 function (): callable {
127 return function (): void {};
128 }
129 )
130 ;
131 $this->httpAccess
132 ->expects(static::once())
133 ->method('getCurlHeaderCallback')
134 ->willReturnCallback(
135 function (): callable {
136 return function (): void {};
137 }
138 )
139 ;
140 $this->httpAccess
141 ->expects(static::once())
142 ->method('getHttpResponse')
143 ->with($url, 30, 4194304)
144 ->willReturnCallback(function($url, $timeout, $maxBytes, $headerCallback, $dlCallback): void {
145 $headerCallback();
146 $dlCallback();
147 })
148 ;
149
150 $result = $this->retriever->retrieve($url);
151
152 static::assertSame($expectedResult, $result);
153 }
154}
diff --git a/tests/legacy/LegacyLinkDBTest.php b/tests/legacy/LegacyLinkDBTest.php
index df2cad62..5c3fd425 100644
--- a/tests/legacy/LegacyLinkDBTest.php
+++ b/tests/legacy/LegacyLinkDBTest.php
@@ -296,6 +296,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase
296 // They need to be grouped with the first case found - order by date DESC: `sTuff`. 296 // They need to be grouped with the first case found - order by date DESC: `sTuff`.
297 'sTuff' => 2, 297 'sTuff' => 2,
298 'ut' => 1, 298 'ut' => 1,
299 'assurance' => 1,
300 'coding-style' => 1,
301 'quality' => 1,
302 'standards' => 1,
299 ), 303 ),
300 self::$publicLinkDB->linksCountPerTag() 304 self::$publicLinkDB->linksCountPerTag()
301 ); 305 );
@@ -324,6 +328,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase
324 'tag3' => 1, 328 'tag3' => 1,
325 'tag4' => 1, 329 'tag4' => 1,
326 'ut' => 1, 330 'ut' => 1,
331 'assurance' => 1,
332 'coding-style' => 1,
333 'quality' => 1,
334 'standards' => 1,
327 ), 335 ),
328 self::$privateLinkDB->linksCountPerTag() 336 self::$privateLinkDB->linksCountPerTag()
329 ); 337 );
@@ -544,6 +552,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase
544 'tag4' => 1, 552 'tag4' => 1,
545 'ut' => 1, 553 'ut' => 1,
546 'w3c' => 1, 554 'w3c' => 1,
555 'assurance' => 1,
556 'coding-style' => 1,
557 'quality' => 1,
558 'standards' => 1,
547 ]; 559 ];
548 $tags = self::$privateLinkDB->linksCountPerTag(); 560 $tags = self::$privateLinkDB->linksCountPerTag();
549 561
diff --git a/tests/legacy/LegacyUpdaterTest.php b/tests/legacy/LegacyUpdaterTest.php
index f7391b86..395dd4b7 100644
--- a/tests/legacy/LegacyUpdaterTest.php
+++ b/tests/legacy/LegacyUpdaterTest.php
@@ -51,10 +51,10 @@ class LegacyUpdaterTest extends \Shaarli\TestCase
51 */ 51 */
52 public function testReadEmptyUpdatesFile() 52 public function testReadEmptyUpdatesFile()
53 { 53 {
54 $this->assertEquals(array(), UpdaterUtils::read_updates_file('')); 54 $this->assertEquals(array(), UpdaterUtils::readUpdatesFile(''));
55 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 55 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
56 touch($updatesFile); 56 touch($updatesFile);
57 $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile)); 57 $this->assertEquals(array(), UpdaterUtils::readUpdatesFile($updatesFile));
58 unlink($updatesFile); 58 unlink($updatesFile);
59 } 59 }
60 60
@@ -66,14 +66,14 @@ class LegacyUpdaterTest extends \Shaarli\TestCase
66 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 66 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
67 $updatesMethods = array('m1', 'm2', 'm3'); 67 $updatesMethods = array('m1', 'm2', 'm3');
68 68
69 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); 69 UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods);
70 $readMethods = UpdaterUtils::read_updates_file($updatesFile); 70 $readMethods = UpdaterUtils::readUpdatesFile($updatesFile);
71 $this->assertEquals($readMethods, $updatesMethods); 71 $this->assertEquals($readMethods, $updatesMethods);
72 72
73 // Update 73 // Update
74 $updatesMethods[] = 'm4'; 74 $updatesMethods[] = 'm4';
75 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); 75 UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods);
76 $readMethods = UpdaterUtils::read_updates_file($updatesFile); 76 $readMethods = UpdaterUtils::readUpdatesFile($updatesFile);
77 $this->assertEquals($readMethods, $updatesMethods); 77 $this->assertEquals($readMethods, $updatesMethods);
78 unlink($updatesFile); 78 unlink($updatesFile);
79 } 79 }
@@ -86,7 +86,7 @@ class LegacyUpdaterTest extends \Shaarli\TestCase
86 $this->expectException(\Exception::class); 86 $this->expectException(\Exception::class);
87 $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); 87 $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/');
88 88
89 UpdaterUtils::write_updates_file('', array('test')); 89 UpdaterUtils::writeUpdatesFile('', array('test'));
90 } 90 }
91 91
92 /** 92 /**
@@ -101,7 +101,7 @@ class LegacyUpdaterTest extends \Shaarli\TestCase
101 touch($updatesFile); 101 touch($updatesFile);
102 chmod($updatesFile, 0444); 102 chmod($updatesFile, 0444);
103 try { 103 try {
104 @UpdaterUtils::write_updates_file($updatesFile, array('test')); 104 @UpdaterUtils::writeUpdatesFile($updatesFile, array('test'));
105 } catch (Exception $e) { 105 } catch (Exception $e) {
106 unlink($updatesFile); 106 unlink($updatesFile);
107 throw $e; 107 throw $e;
diff --git a/tests/netscape/BookmarkExportTest.php b/tests/netscape/BookmarkExportTest.php
index 9b95ccc9..ad288f78 100644
--- a/tests/netscape/BookmarkExportTest.php
+++ b/tests/netscape/BookmarkExportTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Netscape; 3namespace Shaarli\Netscape;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\BookmarkFileService; 6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
7use Shaarli\Formatter\BookmarkFormatter; 8use Shaarli\Formatter\BookmarkFormatter;
@@ -56,12 +57,13 @@ class BookmarkExportTest extends TestCase
56 */ 57 */
57 public static function setUpBeforeClass(): void 58 public static function setUpBeforeClass(): void
58 { 59 {
60 $mutex = new NoMutex();
59 static::$conf = new ConfigManager('tests/utils/config/configJson'); 61 static::$conf = new ConfigManager('tests/utils/config/configJson');
60 static::$conf->set('resource.datastore', static::$testDatastore); 62 static::$conf->set('resource.datastore', static::$testDatastore);
61 static::$refDb = new \ReferenceLinkDB(); 63 static::$refDb = new \ReferenceLinkDB();
62 static::$refDb->write(static::$testDatastore); 64 static::$refDb->write(static::$testDatastore);
63 static::$history = new History('sandbox/history.php'); 65 static::$history = new History('sandbox/history.php');
64 static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, true); 66 static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, $mutex, true);
65 $factory = new FormatterFactory(static::$conf, true); 67 $factory = new FormatterFactory(static::$conf, true);
66 static::$formatter = $factory->getFormatter('raw'); 68 static::$formatter = $factory->getFormatter('raw');
67 } 69 }
diff --git a/tests/netscape/BookmarkImportTest.php b/tests/netscape/BookmarkImportTest.php
index c1e49b5f..6856ebca 100644
--- a/tests/netscape/BookmarkImportTest.php
+++ b/tests/netscape/BookmarkImportTest.php
@@ -3,6 +3,7 @@
3namespace Shaarli\Netscape; 3namespace Shaarli\Netscape;
4 4
5use DateTime; 5use DateTime;
6use malkusch\lock\mutex\NoMutex;
6use Psr\Http\Message\UploadedFileInterface; 7use Psr\Http\Message\UploadedFileInterface;
7use Shaarli\Bookmark\Bookmark; 8use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\BookmarkFileService; 9use Shaarli\Bookmark\BookmarkFileService;
@@ -87,6 +88,7 @@ class BookmarkImportTest extends TestCase
87 */ 88 */
88 protected function setUp(): void 89 protected function setUp(): void
89 { 90 {
91 $mutex = new NoMutex();
90 if (file_exists(self::$testDatastore)) { 92 if (file_exists(self::$testDatastore)) {
91 unlink(self::$testDatastore); 93 unlink(self::$testDatastore);
92 } 94 }
@@ -97,7 +99,7 @@ class BookmarkImportTest extends TestCase
97 $this->conf->set('resource.page_cache', $this->pagecache); 99 $this->conf->set('resource.page_cache', $this->pagecache);
98 $this->conf->set('resource.datastore', self::$testDatastore); 100 $this->conf->set('resource.datastore', self::$testDatastore);
99 $this->history = new History(self::$historyFilePath); 101 $this->history = new History(self::$historyFilePath);
100 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 102 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true);
101 $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history); 103 $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history);
102 } 104 }
103 105
@@ -529,7 +531,7 @@ class BookmarkImportTest extends TestCase
529 { 531 {
530 $post = array( 532 $post = array(
531 'privacy' => 'public', 533 'privacy' => 'public',
532 'default_tags' => 'tag1,tag2 tag3' 534 'default_tags' => 'tag1 tag2 tag3'
533 ); 535 );
534 $files = file2array('netscape_basic.htm'); 536 $files = file2array('netscape_basic.htm');
535 $this->assertStringMatchesFormat( 537 $this->assertStringMatchesFormat(
@@ -550,7 +552,7 @@ class BookmarkImportTest extends TestCase
550 { 552 {
551 $post = array( 553 $post = array(
552 'privacy' => 'public', 554 'privacy' => 'public',
553 'default_tags' => 'tag1&,tag2 "tag3"' 555 'default_tags' => 'tag1& tag2 "tag3"'
554 ); 556 );
555 $files = file2array('netscape_basic.htm'); 557 $files = file2array('netscape_basic.htm');
556 $this->assertStringMatchesFormat( 558 $this->assertStringMatchesFormat(
@@ -571,6 +573,43 @@ class BookmarkImportTest extends TestCase
571 } 573 }
572 574
573 /** 575 /**
576 * Add user-specified tags to all imported bookmarks
577 */
578 public function testSetDefaultTagsWithCustomSeparator()
579 {
580 $separator = '@';
581 $this->conf->set('general.tags_separator', $separator);
582 $post = [
583 'privacy' => 'public',
584 'default_tags' => 'tag1@tag2@tag3@multiple words tag'
585 ];
586 $files = file2array('netscape_basic.htm');
587 $this->assertStringMatchesFormat(
588 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
589 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
590 $this->netscapeBookmarkUtils->import($post, $files)
591 );
592 $this->assertEquals(2, $this->bookmarkService->count());
593 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
594 $this->assertEquals(
595 'tag1@tag2@tag3@multiple words tag@private@secret',
596 $this->bookmarkService->get(0)->getTagsString($separator)
597 );
598 $this->assertEquals(
599 ['tag1', 'tag2', 'tag3', 'multiple words tag', 'private', 'secret'],
600 $this->bookmarkService->get(0)->getTags()
601 );
602 $this->assertEquals(
603 'tag1@tag2@tag3@multiple words tag@public@hello@world',
604 $this->bookmarkService->get(1)->getTagsString($separator)
605 );
606 $this->assertEquals(
607 ['tag1', 'tag2', 'tag3', 'multiple words tag', 'public', 'hello', 'world'],
608 $this->bookmarkService->get(1)->getTags()
609 );
610 }
611
612 /**
574 * Ensure each imported bookmark has a unique id 613 * Ensure each imported bookmark has a unique id
575 * 614 *
576 * See https://github.com/shaarli/Shaarli/issues/351 615 * See https://github.com/shaarli/Shaarli/issues/351
diff --git a/tests/plugins/PluginWallabagTest.php b/tests/plugins/PluginWallabagTest.php
index 36317215..9a402fb7 100644
--- a/tests/plugins/PluginWallabagTest.php
+++ b/tests/plugins/PluginWallabagTest.php
@@ -49,14 +49,15 @@ class PluginWallabagTest extends \Shaarli\TestCase
49 $conf = new ConfigManager(''); 49 $conf = new ConfigManager('');
50 $conf->set('plugins.WALLABAG_URL', 'value'); 50 $conf->set('plugins.WALLABAG_URL', 'value');
51 $str = 'http://randomstr.com/test'; 51 $str = 'http://randomstr.com/test';
52 $data = array( 52 $data = [
53 'title' => $str, 53 'title' => $str,
54 'links' => array( 54 'links' => [
55 array( 55 [
56 'url' => $str, 56 'url' => $str,
57 ) 57 ]
58 ) 58 ],
59 ); 59 '_LOGGEDIN_' => true,
60 ];
60 61
61 $data = hook_wallabag_render_linklist($data, $conf); 62 $data = hook_wallabag_render_linklist($data, $conf);
62 $link = $data['links'][0]; 63 $link = $data['links'][0];
@@ -69,4 +70,26 @@ class PluginWallabagTest extends \Shaarli\TestCase
69 $this->assertNotFalse(strpos($link['link_plugin'][0], urlencode($str))); 70 $this->assertNotFalse(strpos($link['link_plugin'][0], urlencode($str)));
70 $this->assertNotFalse(strpos($link['link_plugin'][0], $conf->get('plugins.WALLABAG_URL'))); 71 $this->assertNotFalse(strpos($link['link_plugin'][0], $conf->get('plugins.WALLABAG_URL')));
71 } 72 }
73
74 /**
75 * Test render_linklist hook while logged out: no change.
76 */
77 public function testWallabagLinklistLoggedOut(): void
78 {
79 $conf = new ConfigManager('');
80 $str = 'http://randomstr.com/test';
81 $data = [
82 'title' => $str,
83 'links' => [
84 [
85 'url' => $str,
86 ]
87 ],
88 '_LOGGEDIN_' => false,
89 ];
90
91 $result = hook_wallabag_render_linklist($data, $conf);
92
93 static::assertSame($data, $result);
94 }
72} 95}
diff --git a/tests/security/BanManagerTest.php b/tests/security/BanManagerTest.php
index 698d3d10..29d2791b 100644
--- a/tests/security/BanManagerTest.php
+++ b/tests/security/BanManagerTest.php
@@ -3,7 +3,8 @@
3 3
4namespace Shaarli\Security; 4namespace Shaarli\Security;
5 5
6use Shaarli\FileUtils; 6use Psr\Log\LoggerInterface;
7use Shaarli\Helper\FileUtils;
7use Shaarli\TestCase; 8use 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
3namespace Shaarli\Security; 3namespace Shaarli\Security;
4 4
5use Psr\Log\LoggerInterface;
6use Shaarli\FakeConfigManager;
5use Shaarli\TestCase; 7use Shaarli\TestCase;
6 8
7/** 9/**
@@ -9,7 +11,7 @@ use Shaarli\TestCase;
9 */ 11 */
10class LoginManagerTest extends TestCase 12class 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
3namespace Shaarli\Security; 3namespace Shaarli\Security;
4 4
5use Shaarli\FakeConfigManager;
5use Shaarli\TestCase; 6use Shaarli\TestCase;
6 7
7/** 8/**
@@ -12,7 +13,7 @@ class SessionManagerTest extends TestCase
12 /** @var array Session ID hashes */ 13 /** @var array Session ID hashes */
13 protected static $sidHashes = null; 14 protected static $sidHashes = null;
14 15
15 /** @var \FakeConfigManager ConfigManager substitute for testing */ 16 /** @var FakeConfigManager ConfigManager substitute for testing */
16 protected $conf = null; 17 protected $conf = null;
17 18
18 /** @var array $_SESSION array for testing */ 19 /** @var array $_SESSION array for testing */
@@ -34,7 +35,7 @@ class SessionManagerTest extends TestCase
34 */ 35 */
35 protected function setUp(): void 36 protected function setUp(): void
36 { 37 {
37 $this->conf = new \FakeConfigManager([ 38 $this->conf = new FakeConfigManager([
38 'credentials.login' => 'johndoe', 39 'credentials.login' => 'johndoe',
39 'credentials.salt' => 'salt', 40 'credentials.salt' => 'salt',
40 'security.session_protection_disabled' => false, 41 'security.session_protection_disabled' => false,
diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php
index a6280b8c..cadd8265 100644
--- a/tests/updater/UpdaterTest.php
+++ b/tests/updater/UpdaterTest.php
@@ -2,6 +2,7 @@
2namespace Shaarli\Updater; 2namespace Shaarli\Updater;
3 3
4use Exception; 4use Exception;
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\BookmarkFileService; 6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\BookmarkServiceInterface; 7use Shaarli\Bookmark\BookmarkServiceInterface;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
@@ -44,12 +45,13 @@ class UpdaterTest extends TestCase
44 */ 45 */
45 protected function setUp(): void 46 protected function setUp(): void
46 { 47 {
48 $mutex = new NoMutex();
47 $this->refDB = new \ReferenceLinkDB(); 49 $this->refDB = new \ReferenceLinkDB();
48 $this->refDB->write(self::$testDatastore); 50 $this->refDB->write(self::$testDatastore);
49 51
50 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); 52 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
51 $this->conf = new ConfigManager(self::$configFile); 53 $this->conf = new ConfigManager(self::$configFile);
52 $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true); 54 $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), $mutex, true);
53 $this->updater = new Updater([], $this->bookmarkService, $this->conf, true); 55 $this->updater = new Updater([], $this->bookmarkService, $this->conf, true);
54 } 56 }
55 57
@@ -58,10 +60,10 @@ class UpdaterTest extends TestCase
58 */ 60 */
59 public function testReadEmptyUpdatesFile() 61 public function testReadEmptyUpdatesFile()
60 { 62 {
61 $this->assertEquals(array(), UpdaterUtils::read_updates_file('')); 63 $this->assertEquals(array(), UpdaterUtils::readUpdatesFile(''));
62 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 64 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
63 touch($updatesFile); 65 touch($updatesFile);
64 $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile)); 66 $this->assertEquals(array(), UpdaterUtils::readUpdatesFile($updatesFile));
65 unlink($updatesFile); 67 unlink($updatesFile);
66 } 68 }
67 69
@@ -73,14 +75,14 @@ class UpdaterTest extends TestCase
73 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 75 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
74 $updatesMethods = array('m1', 'm2', 'm3'); 76 $updatesMethods = array('m1', 'm2', 'm3');
75 77
76 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); 78 UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods);
77 $readMethods = UpdaterUtils::read_updates_file($updatesFile); 79 $readMethods = UpdaterUtils::readUpdatesFile($updatesFile);
78 $this->assertEquals($readMethods, $updatesMethods); 80 $this->assertEquals($readMethods, $updatesMethods);
79 81
80 // Update 82 // Update
81 $updatesMethods[] = 'm4'; 83 $updatesMethods[] = 'm4';
82 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); 84 UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods);
83 $readMethods = UpdaterUtils::read_updates_file($updatesFile); 85 $readMethods = UpdaterUtils::readUpdatesFile($updatesFile);
84 $this->assertEquals($readMethods, $updatesMethods); 86 $this->assertEquals($readMethods, $updatesMethods);
85 unlink($updatesFile); 87 unlink($updatesFile);
86 } 88 }
@@ -93,7 +95,7 @@ class UpdaterTest extends TestCase
93 $this->expectException(\Exception::class); 95 $this->expectException(\Exception::class);
94 $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); 96 $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/');
95 97
96 UpdaterUtils::write_updates_file('', array('test')); 98 UpdaterUtils::writeUpdatesFile('', array('test'));
97 } 99 }
98 100
99 /** 101 /**
@@ -108,7 +110,7 @@ class UpdaterTest extends TestCase
108 touch($updatesFile); 110 touch($updatesFile);
109 chmod($updatesFile, 0444); 111 chmod($updatesFile, 0444);
110 try { 112 try {
111 @UpdaterUtils::write_updates_file($updatesFile, array('test')); 113 @UpdaterUtils::writeUpdatesFile($updatesFile, array('test'));
112 } catch (Exception $e) { 114 } catch (Exception $e) {
113 unlink($updatesFile); 115 unlink($updatesFile);
114 throw $e; 116 throw $e;
diff --git a/tests/utils/FakeApplicationUtils.php b/tests/utils/FakeApplicationUtils.php
index de83d598..d5289ede 100644
--- a/tests/utils/FakeApplicationUtils.php
+++ b/tests/utils/FakeApplicationUtils.php
@@ -2,6 +2,8 @@
2 2
3namespace Shaarli; 3namespace Shaarli;
4 4
5use 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
3namespace Shaarli;
4
5use Shaarli\Config\ConfigManager;
6
3/** 7/**
4 * Fake ConfigManager 8 * Fake ConfigManager
5 */ 9 */
6class FakeConfigManager 10class 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
3use Shaarli\FileUtils; 3use Shaarli\Helper\FileUtils;
4use Shaarli\History; 4use Shaarli\History;
5 5
6/** 6/**
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index fc3cb109..1f53dc3c 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -82,7 +82,7 @@ class ReferenceLinkDB
82 'This guide extends and expands on PSR-1, the basic coding standard.', 82 'This guide extends and expands on PSR-1, the basic coding standard.',
83 0, 83 0,
84 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'), 84 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'),
85 '' 85 'coding-style standards quality assurance'
86 ); 86 );
87 87
88 $this->addLink( 88 $this->addLink(
diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html
index 67d3ebd1..4aac7ff1 100644
--- a/tpl/default/addlink.html
+++ b/tpl/default/addlink.html
@@ -20,6 +20,62 @@
20 </form> 20 </form>
21 </div> 21 </div>
22</div> 22</div>
23
24<div class="pure-g addlink-batch-show-more-block pure-u-0">
25 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
26 <div class="pure-u-lg-1-3 pure-u-22-24 addlink-batch-show-more">
27 <a href="#">{'BULK CREATION'|t}&nbsp;<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 &nbsp; <label for="lf_private">{'Private'|t}</label>
68 </div>
69 </div>
70 <div>
71 <input type="hidden" name="token" value="{$token}">
72 <input type="submit" value="{'Add links'|t}">
73 </div>
74 </form>
75 </div>
76 </div>
77</div>
78
23{include="page.footer"} 79{include="page.footer"}
24</body> 80</body>
25</html> 81</html>
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index 16c55896..13b7f24a 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -27,14 +27,38 @@
27 <div><i class="fa fa-info-circle" aria-hidden="true"></i> {'Case sensitive'|t}</div> 27 <div><i class="fa fa-info-circle" aria-hidden="true"></i> {'Case sensitive'|t}</div>
28 <input type="hidden" name="token" value="{$token}"> 28 <input type="hidden" name="token" value="{$token}">
29 <div> 29 <div>
30 <input type="submit" value="{'Rename'|t}" name="renametag"> 30 <input type="submit" value="{'Rename tag'|t}" name="renametag">
31 <input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete"> 31 <input type="submit" value="{'Delete tag'|t}" name="deletetag"
32 class="button button-red confirm-delete" data-type="tag">
32 </div> 33 </div>
33 </form> 34 </form>
34 35
35 <p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p> 36 <p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p>
36 </div> 37 </div>
37</div> 38</div>
39
40<div class="pure-g">
41 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
42 <div class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
43 <h2 class="window-title">{"Change tags separator"|t}</h2>
44 <form method="POST" action="{$base_path}/admin/tags/change-separator" name="changeseparator" id="changeseparator">
45 <p>
46 {'Your current tag separator is'|t} <code>{$tags_separator}</code>{if="!empty($tags_separator_desc)"} ({$tags_separator_desc}){/if}.
47 </p>
48 <div>
49 <input type="text" name="separator" placeholder="{'New separator'|t}"
50 id="separator">
51 </div>
52 <input type="hidden" name="token" value="{$token}">
53 <div>
54 <input type="submit" value="{'Save'|t}" name="saveseparator">
55 </div>
56 <p>
57 {'Note that hashtags won\'t fully work with a non-whitespace separator.'|t}
58 </p>
59 </form>
60 </div>
61</div>
38{include="page.footer"} 62{include="page.footer"}
39</body> 63</body>
40</html> 64</html>
diff --git a/tpl/default/daily.html b/tpl/default/daily.html
index 3ab8053f..5e038c39 100644
--- a/tpl/default/daily.html
+++ b/tpl/default/daily.html
@@ -7,11 +7,24 @@
7{include="page.header"} 7{include="page.header"}
8 8
9<div class="pure-g"> 9<div class="pure-g">
10 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
11 <a href="{$base_path}/daily?day">{'Daily'|t}</a>
12 <a href="{$base_path}/daily?week">{'Weekly'|t}</a>
13 <a href="{$base_path}/daily?month">{'Monthly'|t}</a>
14 </div>
15</div>
16
17
18<div class="pure-g">
10 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 19 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily"> 20 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily">
12 <h2 class="window-title"> 21 <h2 class="window-title">
13 {'The Daily Shaarli'|t} 22 {$localizedType} Shaarli
14 <a href="{$base_path}/daily-rss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a> 23 <a href="{$base_path}/daily-rss?{$type}"
24 title="{function="t('1 RSS entry per :type', '', 1, 'shaarli', [':type' => t($type)])"}"
25 >
26 <i class="fa fa-rss"></i>
27 </a>
15 </h2> 28 </h2>
16 29
17 <div id="plugin_zone_start_daily" class="plugin_zone"> 30 <div id="plugin_zone_start_daily" class="plugin_zone">
@@ -25,19 +38,19 @@
25 <div class="pure-g"> 38 <div class="pure-g">
26 <div class="pure-u-lg-1-3 pure-u-1 center"> 39 <div class="pure-u-lg-1-3 pure-u-1 center">
27 {if="$previousday"} 40 {if="$previousday"}
28 <a href="{$base_path}/daily?day={$previousday}"> 41 <a href="{$base_path}/daily?{$type}={$previousday}">
29 <i class="fa fa-arrow-left"></i> 42 <i class="fa fa-arrow-left"></i>
30 {'Previous day'|t} 43 {function="t('Previous :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
31 </a> 44 </a>
32 {/if} 45 {/if}
33 </div> 46 </div>
34 <div class="daily-desc pure-u-lg-1-3 pure-u-1 center"> 47 <div class="daily-desc pure-u-lg-1-3 pure-u-1 center">
35 {'All links of one day in a single page.'|t} 48 {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}
36 </div> 49 </div>
37 <div class="pure-u-lg-1-3 pure-u-1 center"> 50 <div class="pure-u-lg-1-3 pure-u-1 center">
38 {if="$nextday"} 51 {if="$nextday"}
39 <a href="{$base_path}/daily?day={$nextday}"> 52 <a href="{$base_path}/daily?{$type}={$nextday}">
40 {'Next day'|t} 53 {function="t('Next :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
41 <i class="fa fa-arrow-right"></i> 54 <i class="fa fa-arrow-right"></i>
42 </a> 55 </a>
43 {/if} 56 {/if}
@@ -45,10 +58,7 @@
45 </div> 58 </div>
46 <div> 59 <div>
47 <h3 class="window-subtitle"> 60 <h3 class="window-subtitle">
48 {if="!empty($dayDesc)"} 61 {$dayDesc}
49 {$dayDesc} -
50 {/if}
51 {function="format_date($dayDate, false)"}
52 </h3> 62 </h3>
53 63
54 <div id="plugin_zone_about_daily" class="plugin_zone"> 64 <div id="plugin_zone_about_daily" class="plugin_zone">
@@ -76,7 +86,7 @@
76 </div> 86 </div>
77 {if="$thumbnails_enabled && !empty($link.thumbnail)"} 87 {if="$thumbnails_enabled && !empty($link.thumbnail)"}
78 <div class="daily-entry-thumbnail"> 88 <div class="daily-entry-thumbnail">
79 <img data-src="{$link.thumbnail}#" class="b-lazy" 89 <img data-src="{$root_path}/{$link.thumbnail}#" class="b-lazy"
80 src="" 90 src=""
81 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 91 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
82 </div> 92 </div>
diff --git a/tpl/default/dailyrss.html b/tpl/default/dailyrss.html
index d40d9496..871a3ba7 100644
--- a/tpl/default/dailyrss.html
+++ b/tpl/default/dailyrss.html
@@ -1,9 +1,9 @@
1<?xml version="1.0" encoding="UTF-8"?> 1<?xml version="1.0" encoding="UTF-8"?>
2<rss version="2.0"> 2<rss version="2.0">
3 <channel> 3 <channel>
4 <title>Daily - {$title}</title> 4 <title>{$localizedType} - {$title}</title>
5 <link>{$index_url}</link> 5 <link>{$index_url}</link>
6 <description>Daily shaared bookmarks</description> 6 <description>{function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}</description>
7 <language>{$language}</language> 7 <language>{$language}</language>
8 <copyright>{$index_url}</copyright> 8 <copyright>{$index_url}</copyright>
9 <generator>Shaarli</generator> 9 <generator>Shaarli</generator>
@@ -18,12 +18,15 @@
18 {loop="$value.links"} 18 {loop="$value.links"}
19 <h3><a href="{$value.url}">{$value.title}</a></h3> 19 <h3><a href="{$value.url}">{$value.title}</a></h3>
20 <small> 20 <small>
21 {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> 21 {if="!$hide_timestamps"}{$value.created|format_date} &#8212; {/if}
22 <a href="{$index_url}shaare/{$value.shorturl}">{'Permalink'|t}</a>
23 {if="$value.tags"} &#8212; {$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 &nbsp;<label for="lf_private">{'Private'|t}</label> 71 &nbsp;<label for="lf_private">{'Private'|t}</label>
@@ -70,6 +88,13 @@
70 88
71 89
72 <div class="submit-buttons center"> 90 <div class="submit-buttons center">
91 {if="!empty($batch_mode)"}
92 <a href="#" class="button button-grey" name="cancel-batch-link"
93 title="{'Remove this bookmark from batch creation/modification.'}"
94 >
95 {'Cancel'|t}
96 </a>
97 {/if}
73 <input type="submit" name="save_edit" class="" id="button-save-edit" 98 <input type="submit" name="save_edit" class="" id="button-save-edit"
74 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}"> 99 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}">
75 {if="!$link_is_new"} 100 {if="!$link_is_new"}
@@ -87,6 +112,10 @@
87 {/if} 112 {/if}
88 </form> 113 </form>
89 </div> 114 </div>
115
116{if="empty($batch_mode)"}
90 {include="page.footer"} 117 {include="page.footer"}
118 {if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
91</body> 119</body>
92</html> 120</html>
121{/if}
diff --git a/tpl/default/error.html b/tpl/default/error.html
index c3e0c3c1..34f9707d 100644
--- a/tpl/default/error.html
+++ b/tpl/default/error.html
@@ -9,13 +9,17 @@
9<div id="pageError" class="page-error-container center"> 9<div id="pageError" class="page-error-container center">
10 <h2>{$message}</h2> 10 <h2>{$message}</h2>
11 11
12 <img src="{$asset_path}/img/sad_star.png#" alt="">
13
14 {if="!empty($text)"}
15 <p>{$text}</p>
16 {/if}
17
12 {if="!empty($stacktrace)"} 18 {if="!empty($stacktrace)"}
13 <pre> 19 <pre>
14 {$stacktrace} 20 {$stacktrace}
15 </pre> 21 </pre>
16 {/if} 22 {/if}
17
18 <img src="{$asset_path}/img/sad_star.png#" alt="">
19</div> 23</div>
20{include="page.footer"} 24{include="page.footer"}
21</body> 25</body>
diff --git a/tpl/default/includes.html b/tpl/default/includes.html
index 227f9b52..3e3fb664 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -8,14 +8,14 @@
8<link href="{$asset_path}/img/favicon.png#" rel="shortcut icon" type="image/png" /> 8<link href="{$asset_path}/img/favicon.png#" rel="shortcut icon" type="image/png" />
9<link href="{$asset_path}/img/apple-touch-icon.png#" rel="apple-touch-icon" sizes="180x180" /> 9<link href="{$asset_path}/img/apple-touch-icon.png#" rel="apple-touch-icon" sizes="180x180" />
10<link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css?v={$version_hash}#" /> 10<link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css?v={$version_hash}#" />
11{if="$formatter==='markdown'"} 11{if="strpos($formatter, 'markdown') !== false"}
12 <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" /> 12 <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
13{/if} 13{/if}
14{loop="$plugins_includes.css_files"} 14{loop="$plugins_includes.css_files"}
15 <link type="text/css" rel="stylesheet" href="{$base_path}/{$value}?v={$version_hash}#"/> 15 <link type="text/css" rel="stylesheet" href="{$root_path}/{$value}?v={$version_hash}#"/>
16{/loop} 16{/loop}
17{if="is_file('data/user.css')"} 17{if="is_file('data/user.css')"}
18 <link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" /> 18 <link type="text/css" rel="stylesheet" href="{$root_path}/data/user.css#" />
19{/if} 19{/if}
20<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" 20<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
21 title="Shaarli search - {$shaarlititle}" /> 21 title="Shaarli search - {$shaarlititle}" />
diff --git a/tpl/default/install.html b/tpl/default/install.html
index a506a2eb..4f98d49d 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -163,6 +163,16 @@
163 </div> 163 </div>
164</div> 164</div>
165</form> 165</form>
166
167<div class="pure-g">
168 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
169 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
170 <h2 class="window-title">{'Server requirements'|t}</h2>
171
172 {include="server.requirements"}
173 </div>
174</div>
175
166{include="page.footer"} 176{include="page.footer"}
167</body> 177</body>
168</html> 178</html>
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index b08773d8..7208a3b6 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -90,7 +90,7 @@
90 {'for'|t} <em><strong>{$search_term}</strong></em> 90 {'for'|t} <em><strong>{$search_term}</strong></em>
91 {/if} 91 {/if}
92 {if="!empty($search_tags)"} 92 {if="!empty($search_tags)"}
93 {$exploded_tags=explode(' ', $search_tags)} 93 {$exploded_tags=tags_str2array($search_tags, $tags_separator)}
94 {'tagged'|t} 94 {'tagged'|t}
95 {loop="$exploded_tags"} 95 {loop="$exploded_tags"}
96 <span class="label label-tag" title="{'Remove tag'|t}"> 96 <span class="label label-tag" title="{'Remove tag'|t}">
@@ -129,18 +129,23 @@
129 {$strAddTag=t('Add tag')} 129 {$strAddTag=t('Add tag')}
130 {$strToggleSticky=t('Toggle sticky')} 130 {$strToggleSticky=t('Toggle sticky')}
131 {$strSticky=t('Sticky')} 131 {$strSticky=t('Sticky')}
132 {$strShaarePrivate=t('Share a private link')}
132 {ignore}End of translations{/ignore} 133 {ignore}End of translations{/ignore}
133 {loop="links"} 134 {loop="links"}
134 <div class="anchor" id="{$value.shorturl}"></div> 135 <div class="anchor" id="{$value.shorturl}"></div>
135 136
136 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> 137 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
137 <div class="linklist-item-title"> 138 <div class="linklist-item-title">
138 {if="$thumbnails_enabled && !empty($value.thumbnail)"} 139 {if="$thumbnails_enabled && $value.thumbnail !== false"}
139 <div class="linklist-item-thumbnail" style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;"> 140 <div
141 class="linklist-item-thumbnail {if="$value.thumbnail === null"}hidden{/if}"
142 style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;"
143 {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if}
144 >
140 <div class="thumbnail"> 145 <div class="thumbnail">
141 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 146 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
142 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1"> 147 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1">
143 <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy" 148 <img data-src="{$root_path}/{$value.thumbnail}#" class="b-lazy"
144 src="" 149 src=""
145 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 150 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
146 </a> 151 </a>
@@ -158,14 +163,14 @@
158 </div> 163 </div>
159 164
160 <h2> 165 <h2>
161 <a href="{$value.real_url}"> 166 <a href="{$value.real_url}" class="linklist-real-url">
162 {if="strpos($value.url, $value.shorturl) === false"} 167 {if="strpos($value.url, $value.shorturl) === false"}
163 <i class="fa fa-external-link" aria-hidden="true"></i> 168 <i class="fa fa-external-link" aria-hidden="true"></i>
164 {else} 169 {else}
165 <i class="fa fa-sticky-note" aria-hidden="true"></i> 170 <i class="fa fa-sticky-note" aria-hidden="true"></i>
166 {/if} 171 {/if}
167 172
168 <span class="linklist-link">{$value.title}</span> 173 <span class="linklist-link">{$value.title_html}</span>
169 </a> 174 </a>
170 </h2> 175 </h2>
171 </div> 176 </div>
@@ -183,7 +188,7 @@
183 {$tag_counter=count($value.taglist)} 188 {$tag_counter=count($value.taglist)}
184 {loop="value.taglist"} 189 {loop="value.taglist"}
185 <span class="label label-tag" title="{$strAddTag}"> 190 <span class="label label-tag" title="{$strAddTag}">
186 <a href="{$base_path}/add-tag/{$value1.urlencoded_taglist.$key2}">{$value}</a> 191 <a href="{$base_path}/add-tag/{$value1.taglist_urlencoded.$key2}">{$value1.taglist_html.$key2}</a>
187 </span> 192 </span>
188 {if="$tag_counter - 1 != $counter"}&middot;{/if} 193 {if="$tag_counter - 1 != $counter"}&middot;{/if}
189 {/loop} 194 {/loop}
@@ -237,6 +242,12 @@
237 {$strPermalinkLc} 242 {$strPermalinkLc}
238 </a> 243 </a>
239 244
245 {if="$is_logged_in && $value.private"}
246 <a href="{$base_path}/admin/shaare/private/{$value.shorturl}?token={$token}" title="{$strShaarePrivate}">
247 <i class="fa fa-share-alt"></i>
248 </a>
249 {/if}
250
240 <div class="pure-u-0 pure-u-lg-visible"> 251 <div class="pure-u-0 pure-u-lg-visible">
241 {if="isset($value.link_plugin)"} 252 {if="isset($value.link_plugin)"}
242 &middot; 253 &middot;
@@ -251,7 +262,7 @@
251 {ignore}do not add space or line break between these div - Firefox issue{/ignore} 262 {ignore}do not add space or line break between these div - Firefox issue{/ignore}
252 class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1"> 263 class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1">
253 <a href="{$value.real_url}" aria-label="{$value.title}" title="{$value.title}"> 264 <a href="{$value.real_url}" aria-label="{$value.title}" title="{$value.title}">
254 <i class="fa fa-link" aria-hidden="true"></i> {$value.url} 265 <i class="fa fa-link" aria-hidden="true"></i> {$value.url_html}
255 </a> 266 </a>
256 <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible"> 267 <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible">
257 <a href="#" aria-label="{$strFold}" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up" aria-hidden="true"></i></a> 268 <a href="#" aria-label="{$strFold}" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up" aria-hidden="true"></i></a>
@@ -308,5 +319,6 @@
308 319
309{include="page.footer"} 320{include="page.footer"}
310<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script> 321<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
322{if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
311</body> 323</body>
312</html> 324</html>
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index 51bdb2f0..58ca18c5 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -10,7 +10,7 @@
10 {/if} 10 {/if}
11 &middot; 11 &middot;
12 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot; 12 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot;
13 <a href="{$base_path}/doc/html/index.html" rel="nofollow">{'Documentation'|t}</a> 13 <a href="{$root_path}/doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
14 {loop="$plugins_footer.text"} 14 {loop="$plugins_footer.text"}
15 {$value} 15 {$value}
16 {/loop} 16 {/loop}
@@ -18,26 +18,28 @@
18 <div class="pure-u-2-24"></div> 18 <div class="pure-u-2-24"></div>
19</div> 19</div>
20 20
21<input type="hidden" name="token" value="{$token}" id="token" />
22
23{loop="$plugins_footer.endofpage"} 21{loop="$plugins_footer.endofpage"}
24 {$value} 22 {$value}
25{/loop} 23{/loop}
26 24
27{loop="$plugins_footer.js_files"} 25{loop="$plugins_footer.js_files"}
28 <script src="{$base_path}/{$value}#"></script> 26 <script src="{$root_path}/{$value}#"></script>
29{/loop} 27{/loop}
30 28
31<div id="js-translations" class="hidden"> 29<div id="js-translations" class="hidden" aria-hidden="true">
32 <span id="translation-fold">{'Fold'|t}</span> 30 <span id="translation-fold">{'Fold'|t}</span>
33 <span id="translation-fold-all">{'Fold all'|t}</span> 31 <span id="translation-fold-all">{'Fold all'|t}</span>
34 <span id="translation-expand">{'Expand'|t}</span> 32 <span id="translation-expand">{'Expand'|t}</span>
35 <span id="translation-expand-all">{'Expand all'|t}</span> 33 <span id="translation-expand-all">{'Expand all'|t}</span>
36 <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span> 34 <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span>
35 <span id="translation-delete-tag">{'Are you sure you want to delete this tag?'|t}</span>
37 <span id="translation-shaarli-desc"> 36 <span id="translation-shaarli-desc">
38 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} 37 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t}
39 </span> 38 </span>
40</div> 39</div>
41 40
42<input type="hidden" name="js_base_path" value="{$base_path}" /> 41<input type="hidden" name="js_base_path" value="{$base_path}" />
42<input type="hidden" name="token" value="{$token}" id="token" />
43<input type="hidden" name="tags_separator" value="{$tags_separator}" id="tags_separator" />
44
43<script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script> 45<script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script>
diff --git a/tpl/default/picwall.html b/tpl/default/picwall.html
index b7a56c89..ac613b35 100644
--- a/tpl/default/picwall.html
+++ b/tpl/default/picwall.html
@@ -31,7 +31,7 @@
31 {loop="$linksToDisplay"} 31 {loop="$linksToDisplay"}
32 <div class="picwall-pictureframe" role="listitem"> 32 <div class="picwall-pictureframe" role="listitem">
33 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 33 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
34 <img data-src="{$value.thumbnail}#" class="b-lazy" 34 <img data-src="{$root_path}/{$value.thumbnail}#" class="b-lazy"
35 src="" 35 src=""
36 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 36 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
37 <a href="{$value.real_url}"><span class="info">{$value.title}</span></a> 37 <a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index 05d13556..5c073da6 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -117,7 +117,7 @@
117 117
118 <div class="center more"> 118 <div class="center more">
119 {"More plugins available"|t} 119 {"More plugins available"|t}
120 <a href="doc/html/Community-&-Related-software/#third-party-plugins">{"in the documentation"|t}</a>. 120 <a href="{$root_path}/doc/html/Community-&-Related-software/#third-party-plugins">{"in the documentation"|t}</a>.
121 </div> 121 </div>
122 <div class="center"> 122 <div class="center">
123 <input type="submit" value="{'Save'|t}" name="save"> 123 <input type="submit" value="{'Save'|t}" name="save">
diff --git a/tpl/default/server.html b/tpl/default/server.html
new file mode 100644
index 00000000..de1c8b53
--- /dev/null
+++ b/tpl/default/server.html
@@ -0,0 +1,129 @@
1<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head>
4 {include="includes"}
5</head>
6<body>
7{include="page.header"}
8
9<div class="pure-g">
10 <div class="pure-u-lg-1-4 pure-u-1-24"></div>
11 <div class="pure-u-lg-1-2 pure-u-22-24 page-form server-tables-page">
12 <h2 class="window-title">{'Server administration'|t}</h2>
13
14 <h3 class="window-subtitle">{'General'|t}</h3>
15
16 <div class="pure-g server-row">
17 <div class="pure-u-lg-1-2 pure-u-1 server-label">
18 <p>{'Index URL'|t}</p>
19 </div>
20 <div class="pure-u-lg-1-2 pure-u-1">
21 <p><a href="{$index_url}" title="{$pagetitle}">{$index_url}</a></p>
22 </div>
23 </div>
24 <div class="pure-g server-row">
25 <div class="pure-u-lg-1-2 pure-u-1 server-label">
26 <p>{'Base path'|t}</p>
27 </div>
28 <div class="pure-u-lg-1-2 pure-u-1">
29 <p>{$base_path}</p>
30 </div>
31 </div>
32 <div class="pure-g server-row">
33 <div class="pure-u-lg-1-2 pure-u-1 server-label">
34 <p>{'Client IP'|t}</p>
35 </div>
36 <div class="pure-u-lg-1-2 pure-u-1">
37 <p>{$client_ip}</p>
38 </div>
39 </div>
40 <div class="pure-g server-row">
41 <div class="pure-u-lg-1-2 pure-u-1 server-label">
42 <p>{'Trusted reverse proxies'|t}</p>
43 </div>
44 <div class="pure-u-lg-1-2 pure-u-1">
45 {if="count($trusted_proxies) > 0"}
46 <p>
47 {loop="$trusted_proxies"}
48 {$value}<br>
49 {/loop}
50 </p>
51 {else}
52 <p>{'N/A'|t}</p>
53 {/if}
54 </div>
55 </div>
56
57 {include="server.requirements"}
58
59 <h3 class="window-subtitle">Version</h3>
60
61 <div class="pure-g server-row">
62 <div class="pure-u-lg-1-2 pure-u-1 server-label">
63 <p>Current version</p>
64 </div>
65 <div class="pure-u-lg-1-2 pure-u-1">
66 <p>{$current_version}</p>
67 </div>
68 </div>
69
70 <div class="pure-g server-row">
71 <div class="pure-u-lg-1-2 pure-u-1 server-label">
72 <p>Latest release</p>
73 </div>
74 <div class="pure-u-lg-1-2 pure-u-1">
75 <p>
76 <a href="{$release_url}" title="{'Visit releases page on Github'|t}">
77 {$latest_version}
78 </a>
79 </p>
80 </div>
81 </div>
82
83 <h3 class="window-subtitle">Thumbnails</h3>
84
85 <div class="pure-g server-row">
86 <div class="pure-u-lg-1-2 pure-u-1 server-label">
87 <p>Thumbnails status</p>
88 </div>
89 <div class="pure-u-lg-1-2 pure-u-1">
90 <p>
91 {if="$thumbnails_mode==='all'"}
92 {'All'|t}
93 {elseif="$thumbnails_mode==='common'"}
94 {'Only common media hosts'|t}
95 {else}
96 {'None'|t}
97 {/if}
98 </p>
99 </div>
100 </div>
101
102 {if="$thumbnails_mode!=='none'"}
103 <div class="center tools-item">
104 <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
105 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
106 </a>
107 </div>
108 {/if}
109
110 <h3 class="window-subtitle">Cache</h3>
111
112 <div class="center tools-item">
113 <a href="{$base_path}/admin/clear-cache?type=main">
114 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear main cache</span>
115 </a>
116 </div>
117
118 <div class="center tools-item">
119 <a href="{$base_path}/admin/clear-cache?type=thumbnails">
120 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear thumbnails cache</span>
121 </a>
122 </div>
123 </div>
124</div>
125
126{include="page.footer"}
127
128</body>
129</html>
diff --git a/tpl/default/server.requirements.html b/tpl/default/server.requirements.html
new file mode 100644
index 00000000..85def9b7
--- /dev/null
+++ b/tpl/default/server.requirements.html
@@ -0,0 +1,68 @@
1<div class="server-tables">
2 <h3 class="window-subtitle">{'Permissions'|t}</h3>
3
4 {if="count($permissions) > 0"}
5 <p class="center">
6 <i class="fa fa-close fa-color-red" aria-hidden="true"></i>
7 {'There are permissions that need to be fixed.'|t}
8 </p>
9
10 <p>
11 {loop="$permissions"}
12 <div class="center">{$value}</div>
13 {/loop}
14 </p>
15 {else}
16 <p class="center">
17 <i class="fa fa-check fa-color-green" aria-hidden="true"></i>
18 {'All read/write permissions are properly set.'|t}
19 </p>
20 {/if}
21
22 <h3 class="window-subtitle">PHP</h3>
23
24 <p class="center">
25 <strong>{'Running PHP'|t} {$php_version}</strong>
26 {if="$php_has_reached_eol"}
27 <i class="fa fa-circle fa-color-orange" aria-label="hidden"></i><br>
28 {'End of life: '|t} {$php_eol}
29 {else}
30 <i class="fa fa-circle fa-color-green" aria-label="hidden"></i><br>
31 {/if}
32 </p>
33
34 <table class="center">
35 <thead>
36 <tr>
37 <th>{'Extension'|t}</th>
38 <th>{'Usage'|t}</th>
39 <th>{'Status'|t}</th>
40 <th>{'Loaded'|t}</th>
41 </tr>
42 </thead>
43 <tbody>
44 {loop="$php_extensions"}
45 <tr>
46 <td>{$value.name}</td>
47 <td>{$value.desc}</td>
48 <td>{$value.required ? t('Required') : t('Optional')}</td>
49 <td>
50 {if="$value.loaded"}
51 {$classLoaded="fa-color-green"}
52 {$strLoaded=t('Loaded')}
53 {else}
54 {$strLoaded=t('Not loaded')}
55 {if="$value.required"}
56 {$classLoaded="fa-color-red"}
57 {else}
58 {$classLoaded="fa-color-orange"}
59 {/if}
60 {/if}
61
62 <i class="fa fa-circle {$classLoaded}" aria-label="{$strLoaded}" title="{$strLoaded}"></i>
63 </td>
64 </tr>
65 {/loop}
66 </tbody>
67 </table>
68</div>
diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html
index c067e1d4..01b50b02 100644
--- a/tpl/default/tag.cloud.html
+++ b/tpl/default/tag.cloud.html
@@ -48,7 +48,7 @@
48 48
49 <div id="cloudtag" class="cloudtag-container"> 49 <div id="cloudtag" class="cloudtag-container">
50 {loop="tags"} 50 {loop="tags"}
51 <a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a 51 <a href="{$base_path}/?searchtags={$tags_url.$key1}{$tags_separator|urlencode}{$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a
52 ><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a> 52 ><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
53 {loop="$value.tag_plugin"} 53 {loop="$value.tag_plugin"}
54 {$value} 54 {$value}
diff --git a/tpl/default/tools.html b/tpl/default/tools.html
index 2cb08e38..2df73598 100644
--- a/tpl/default/tools.html
+++ b/tpl/default/tools.html
@@ -20,6 +20,12 @@
20 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span> 20 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span>
21 </a> 21 </a>
22 </div> 22 </div>
23 <div class="tools-item">
24 <a href="{$base_path}/admin/server"
25 title="{'Check instance\'s server configuration'|t}">
26 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Server administration'|t}</span>
27 </a>
28 </div>
23 {if="!$openshaarli"} 29 {if="!$openshaarli"}
24 <div class="tools-item"> 30 <div class="tools-item">
25 <a href="{$base_path}/admin/password" title="{'Change your password'|t}"> 31 <a href="{$base_path}/admin/password" title="{'Change your password'|t}">
@@ -45,14 +51,6 @@
45 </a> 51 </a>
46 </div> 52 </div>
47 53
48 {if="$thumbnails_enabled"}
49 <div class="tools-item">
50 <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
51 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
52 </a>
53 </div>
54 {/if}
55
56 {loop="$tools_plugin"} 54 {loop="$tools_plugin"}
57 <div class="tools-item"> 55 <div class="tools-item">
58 {$value} 56 {$value}
diff --git a/tpl/vintage/daily.html b/tpl/vintage/daily.html
index 74f6cdc7..28ba9f90 100644
--- a/tpl/vintage/daily.html
+++ b/tpl/vintage/daily.html
@@ -14,9 +14,9 @@
14 14
15 <div class="dailyAbout"> 15 <div class="dailyAbout">
16 All links of one day<br>in a single page.<br> 16 All links of one day<br>in a single page.<br>
17 {if="$previousday"} <a href="{$base_path}/daily&amp;day={$previousday}"><b>&lt;</b>Previous day</a>{else}<b>&lt;</b>Previous day{/if} 17 {if="$previousday"} <a href="{$base_path}/daily?day={$previousday}"><b>&lt;</b>Previous day</a>{else}<b>&lt;</b>Previous day{/if}
18 - 18 -
19 {if="$nextday"}<a href="{$base_path}/daily&amp;day={$nextday}">Next day<b>&gt;</b></a>{else}Next day<b>&gt;</b>{/if} 19 {if="$nextday"}<a href="{$base_path}/daily?day={$nextday}">Next day<b>&gt;</b></a>{else}Next day<b>&gt;</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 ],
diff --git a/yarn.lock b/yarn.lock
index 0a12820c..55bd9827 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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
2915he@^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
2915hmac-drbg@^1.0.0: 2920hmac-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"