aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.editorconfig2
-rw-r--r--application/Thumbnailer.php3
-rw-r--r--application/Utils.php14
-rw-r--r--application/api/ApiMiddleware.php9
-rw-r--r--application/api/ApiUtils.php2
-rw-r--r--application/bookmark/Bookmark.php25
-rw-r--r--application/bookmark/BookmarkFileService.php38
-rw-r--r--application/bookmark/BookmarkFilter.php2
-rw-r--r--application/bookmark/BookmarkIO.php10
-rw-r--r--application/bookmark/BookmarkInitializer.php9
-rw-r--r--application/bookmark/BookmarkServiceInterface.php1
-rw-r--r--application/bookmark/LinkUtils.php108
-rw-r--r--application/bookmark/exception/DatastoreNotInitializedException.php10
-rw-r--r--application/config/ConfigManager.php4
-rw-r--r--application/config/ConfigPlugin.php17
-rw-r--r--application/container/ContainerBuilder.php83
-rw-r--r--application/container/ShaarliContainer.php26
-rw-r--r--application/feed/Cache.php38
-rw-r--r--application/feed/FeedBuilder.php141
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php22
-rw-r--r--application/formatter/BookmarkFormatter.php6
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php4
-rw-r--r--application/formatter/FormatterFactory.php2
-rw-r--r--application/front/ShaarliAdminMiddleware.php27
-rw-r--r--application/front/ShaarliMiddleware.php83
-rw-r--r--application/front/controller/admin/ConfigureController.php126
-rw-r--r--application/front/controller/admin/ExportController.php80
-rw-r--r--application/front/controller/admin/ImportController.php82
-rw-r--r--application/front/controller/admin/LogoutController.php33
-rw-r--r--application/front/controller/admin/ManageShaareController.php371
-rw-r--r--application/front/controller/admin/ManageTagController.php88
-rw-r--r--application/front/controller/admin/PasswordController.php101
-rw-r--r--application/front/controller/admin/PluginsController.php84
-rw-r--r--application/front/controller/admin/SessionFilterController.php50
-rw-r--r--application/front/controller/admin/ShaarliAdminController.php73
-rw-r--r--application/front/controller/admin/ThumbnailsController.php65
-rw-r--r--application/front/controller/admin/TokenController.php26
-rw-r--r--application/front/controller/admin/ToolsController.php35
-rw-r--r--application/front/controller/visitor/BookmarkListController.php240
-rw-r--r--application/front/controller/visitor/DailyController.php192
-rw-r--r--application/front/controller/visitor/ErrorController.php45
-rw-r--r--application/front/controller/visitor/FeedController.php58
-rw-r--r--application/front/controller/visitor/InstallController.php165
-rw-r--r--application/front/controller/visitor/LoginController.php154
-rw-r--r--application/front/controller/visitor/OpenSearchController.php27
-rw-r--r--application/front/controller/visitor/PictureWallController.php54
-rw-r--r--application/front/controller/visitor/PublicSessionFilterController.php46
-rw-r--r--application/front/controller/visitor/ShaarliVisitorController.php170
-rw-r--r--application/front/controller/visitor/TagCloudController.php113
-rw-r--r--application/front/controller/visitor/TagController.php118
-rw-r--r--application/front/controllers/LoginController.php48
-rw-r--r--application/front/controllers/ShaarliController.php69
-rw-r--r--application/front/exceptions/AlreadyInstalledException.php15
-rw-r--r--application/front/exceptions/CantLoginException.php10
-rw-r--r--application/front/exceptions/LoginBannedException.php2
-rw-r--r--application/front/exceptions/OpenShaarliPasswordException.php18
-rw-r--r--application/front/exceptions/ResourcePermissionException.php13
-rw-r--r--application/front/exceptions/ShaarliFrontException.php (renamed from application/front/exceptions/ShaarliException.php)4
-rw-r--r--application/front/exceptions/ThumbnailsDisabledException.php15
-rw-r--r--application/front/exceptions/UnauthorizedException.php15
-rw-r--r--application/front/exceptions/WrongTokenException.php18
-rw-r--r--application/http/HttpAccess.php39
-rw-r--r--application/http/HttpUtils.php121
-rw-r--r--application/legacy/LegacyController.php130
-rw-r--r--application/legacy/LegacyLinkDB.php4
-rw-r--r--application/legacy/LegacyRouter.php (renamed from application/Router.php)7
-rw-r--r--application/legacy/LegacyUpdater.php5
-rw-r--r--application/legacy/UnknowLegacyRouteException.php9
-rw-r--r--application/netscape/NetscapeBookmarkUtils.php133
-rw-r--r--application/plugin/PluginManager.php6
-rw-r--r--application/render/PageBuilder.php79
-rw-r--r--application/render/PageCacheManager.php60
-rw-r--r--application/render/TemplatePage.php33
-rw-r--r--application/security/CookieManager.php33
-rw-r--r--application/security/LoginManager.php16
-rw-r--r--application/security/SessionManager.php107
-rw-r--r--application/updater/Updater.php75
-rw-r--r--assets/common/js/thumbnails-update.js14
-rw-r--r--assets/default/js/base.js35
-rw-r--r--assets/default/scss/shaarli.scss4
-rw-r--r--composer.json3
-rw-r--r--composer.lock211
-rw-r--r--doc/md/Plugin-System.md6
-rw-r--r--doc/md/RSS-feeds.md12
-rw-r--r--doc/md/Translations.md26
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po988
-rw-r--r--index.php1949
-rw-r--r--init.php85
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.php6
-rw-r--r--plugins/archiveorg/archiveorg.html2
-rw-r--r--plugins/archiveorg/archiveorg.php3
-rw-r--r--plugins/demo_plugin/demo_plugin.php10
-rw-r--r--plugins/isso/isso.php4
-rw-r--r--plugins/isso/isso_button.html5
-rw-r--r--plugins/playvideos/playvideos.php6
-rw-r--r--plugins/pubsubhubbub/pubsubhubbub.php8
-rw-r--r--plugins/qrcode/qrcode.php11
-rw-r--r--plugins/qrcode/shaarli-qrcode.js15
-rw-r--r--plugins/wallabag/README.md2
-rw-r--r--plugins/wallabag/wallabag.php4
-rw-r--r--tests/api/controllers/links/GetLinkIdTest.php2
-rw-r--r--tests/api/controllers/links/GetLinksTest.php2
-rw-r--r--tests/api/controllers/links/PostLinkTest.php4
-rw-r--r--tests/api/controllers/links/PutLinkTest.php4
-rw-r--r--tests/bookmark/BookmarkFileServiceTest.php33
-rw-r--r--tests/bookmark/BookmarkInitializerTest.php14
-rw-r--r--tests/bookmark/BookmarkTest.php4
-rw-r--r--tests/bookmark/LinkUtilsTest.php4
-rw-r--r--tests/bootstrap.php13
-rw-r--r--tests/config/ConfigPluginTest.php16
-rw-r--r--tests/container/ContainerBuilderTest.php34
-rw-r--r--tests/container/ShaarliTestContainer.php42
-rw-r--r--tests/feed/CachedPageTest.php6
-rw-r--r--tests/feed/FeedBuilderTest.php83
-rw-r--r--tests/formatter/BookmarkDefaultFormatterTest.php4
-rw-r--r--tests/formatter/BookmarkMarkdownFormatterTest.php4
-rw-r--r--tests/front/ShaarliAdminMiddlewareTest.php100
-rw-r--r--tests/front/ShaarliMiddlewareTest.php161
-rw-r--r--tests/front/controller/LoginControllerTest.php178
-rw-r--r--tests/front/controller/ShaarliControllerTest.php116
-rw-r--r--tests/front/controller/admin/ConfigureControllerTest.php252
-rw-r--r--tests/front/controller/admin/ExportControllerTest.php163
-rw-r--r--tests/front/controller/admin/FrontAdminControllerMockHelper.php56
-rw-r--r--tests/front/controller/admin/ImportControllerTest.php148
-rw-r--r--tests/front/controller/admin/LogoutControllerTest.php51
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php47
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php418
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php376
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php315
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php155
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php145
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php282
-rw-r--r--tests/front/controller/admin/ManageTagControllerTest.php272
-rw-r--r--tests/front/controller/admin/PasswordControllerTest.php203
-rw-r--r--tests/front/controller/admin/PluginsControllerTest.php204
-rw-r--r--tests/front/controller/admin/SessionFilterControllerTest.php177
-rw-r--r--tests/front/controller/admin/ShaarliAdminControllerTest.php184
-rw-r--r--tests/front/controller/admin/ThumbnailsControllerTest.php154
-rw-r--r--tests/front/controller/admin/TokenControllerTest.php41
-rw-r--r--tests/front/controller/admin/ToolsControllerTest.php69
-rw-r--r--tests/front/controller/visitor/BookmarkListControllerTest.php448
-rw-r--r--tests/front/controller/visitor/DailyControllerTest.php476
-rw-r--r--tests/front/controller/visitor/ErrorControllerTest.php70
-rw-r--r--tests/front/controller/visitor/FeedControllerTest.php145
-rw-r--r--tests/front/controller/visitor/FrontControllerMockHelper.php119
-rw-r--r--tests/front/controller/visitor/InstallControllerTest.php262
-rw-r--r--tests/front/controller/visitor/LoginControllerTest.php404
-rw-r--r--tests/front/controller/visitor/OpenSearchControllerTest.php44
-rw-r--r--tests/front/controller/visitor/PictureWallControllerTest.php121
-rw-r--r--tests/front/controller/visitor/PublicSessionFilterControllerTest.php122
-rw-r--r--tests/front/controller/visitor/ShaarliVisitorControllerTest.php215
-rw-r--r--tests/front/controller/visitor/TagCloudControllerTest.php369
-rw-r--r--tests/front/controller/visitor/TagControllerTest.php215
-rw-r--r--tests/http/HttpUtils/IndexUrlTest.php32
-rw-r--r--tests/legacy/LegacyControllerTest.php99
-rw-r--r--tests/legacy/LegacyLinkDBTest.php1
-rw-r--r--tests/legacy/LegacyRouterTest.php (renamed from tests/RouterTest.php)243
-rw-r--r--tests/netscape/BookmarkExportTest.php69
-rw-r--r--tests/netscape/BookmarkImportTest.php68
-rw-r--r--tests/plugins/PluginAddlinkTest.php10
-rw-r--r--tests/plugins/PluginPlayvideosTest.php6
-rw-r--r--tests/plugins/PluginPubsubhubbubTest.php6
-rw-r--r--tests/plugins/PluginQrcodeTest.php4
-rw-r--r--tests/plugins/resources/hashtags.md10
-rw-r--r--tests/plugins/resources/hashtags.raw10
-rw-r--r--tests/plugins/resources/markdown.html33
-rw-r--r--tests/plugins/resources/markdown.md34
-rw-r--r--tests/render/PageCacheManagerTest.php (renamed from tests/feed/CacheTest.php)39
-rw-r--r--tests/security/LoginManagerTest.php30
-rw-r--r--tests/security/SessionManagerTest.php71
-rw-r--r--tests/updater/UpdaterTest.php59
-rw-r--r--tests/utils/ReferenceLinkDB.php8
-rw-r--r--tpl/default/404.html2
-rw-r--r--tpl/default/addlink.html2
-rw-r--r--tpl/default/changepassword.html2
-rw-r--r--tpl/default/changetag.html4
-rw-r--r--tpl/default/configure.html6
-rw-r--r--tpl/default/daily.html12
-rw-r--r--tpl/default/dailyrss.html48
-rw-r--r--tpl/default/editlink.html9
-rw-r--r--tpl/default/error.html2
-rw-r--r--tpl/default/export.html3
-rw-r--r--tpl/default/feed.atom.html2
-rw-r--r--tpl/default/feed.rss.html4
-rw-r--r--tpl/default/import.html2
-rw-r--r--tpl/default/includes.html19
-rw-r--r--tpl/default/install.html2
-rw-r--r--tpl/default/linklist.html22
-rw-r--r--tpl/default/linklist.paging.html16
-rw-r--r--tpl/default/opensearch.html4
-rw-r--r--tpl/default/page.footer.html7
-rw-r--r--tpl/default/page.header.html56
-rw-r--r--tpl/default/picwall.html86
-rw-r--r--tpl/default/pluginsadmin.html7
-rw-r--r--tpl/default/tag.cloud.html6
-rw-r--r--tpl/default/tag.list.html8
-rw-r--r--tpl/default/tag.sort.html8
-rw-r--r--tpl/default/thumbnails.html2
-rw-r--r--tpl/default/tools.html16
-rw-r--r--tpl/vintage/404.html2
-rw-r--r--tpl/vintage/addlink.html2
-rw-r--r--tpl/vintage/changepassword.html4
-rw-r--r--tpl/vintage/changetag.html2
-rw-r--r--tpl/vintage/configure.html6
-rw-r--r--tpl/vintage/daily.html20
-rw-r--r--tpl/vintage/dailyrss.html46
-rw-r--r--tpl/vintage/editlink.html11
-rw-r--r--tpl/vintage/error.html2
-rw-r--r--tpl/vintage/export.html5
-rw-r--r--tpl/vintage/feed.atom.html4
-rw-r--r--tpl/vintage/feed.rss.html2
-rw-r--r--tpl/vintage/import.html2
-rw-r--r--tpl/vintage/includes.html15
-rw-r--r--tpl/vintage/install.html2
-rw-r--r--tpl/vintage/linklist.html36
-rw-r--r--tpl/vintage/linklist.paging.html15
-rw-r--r--tpl/vintage/loginform.html2
-rw-r--r--tpl/vintage/opensearch.html4
-rw-r--r--tpl/vintage/page.footer.html6
-rw-r--r--tpl/vintage/page.header.html22
-rw-r--r--tpl/vintage/picwall.html2
-rw-r--r--tpl/vintage/pluginsadmin.html6
-rw-r--r--tpl/vintage/tag.cloud.html4
-rw-r--r--tpl/vintage/thumbnails.html2
-rw-r--r--tpl/vintage/tools.html14
225 files changed, 12738 insertions, 4010 deletions
diff --git a/.editorconfig b/.editorconfig
index 34bd7994..c2ab80eb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -14,7 +14,7 @@ indent_size = 4
14indent_size = 2 14indent_size = 2
15 15
16[*.php] 16[*.php]
17max_line_length = 100 17max_line_length = 120
18 18
19[Dockerfile] 19[Dockerfile]
20max_line_length = 80 20max_line_length = 80
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php
index 314baf0d..5aec23c8 100644
--- a/application/Thumbnailer.php
+++ b/application/Thumbnailer.php
@@ -4,7 +4,6 @@ namespace Shaarli;
4 4
5use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
6use WebThumbnailer\Application\ConfigManager as WTConfigManager; 6use WebThumbnailer\Application\ConfigManager as WTConfigManager;
7use WebThumbnailer\Exception\WebThumbnailerException;
8use WebThumbnailer\WebThumbnailer; 7use WebThumbnailer\WebThumbnailer;
9 8
10/** 9/**
@@ -90,7 +89,7 @@ class Thumbnailer
90 89
91 try { 90 try {
92 return $this->wt->thumbnail($url); 91 return $this->wt->thumbnail($url);
93 } catch (WebThumbnailerException $e) { 92 } catch (\Throwable $e) {
94 // Exceptions are only thrown in debug mode. 93 // Exceptions are only thrown in debug mode.
95 error_log(get_class($e) . ': ' . $e->getMessage()); 94 error_log(get_class($e) . ': ' . $e->getMessage());
96 } 95 }
diff --git a/application/Utils.php b/application/Utils.php
index 4b7fc546..9c9eaaa2 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -87,10 +87,14 @@ function endsWith($haystack, $needle, $case = true)
87 * 87 *
88 * @param mixed $input Data to escape: a single string or an array of strings. 88 * @param mixed $input Data to escape: a single string or an array of strings.
89 * 89 *
90 * @return string escaped. 90 * @return string|array escaped.
91 */ 91 */
92function escape($input) 92function escape($input)
93{ 93{
94 if (null === $input) {
95 return null;
96 }
97
94 if (is_bool($input)) { 98 if (is_bool($input)) {
95 return $input; 99 return $input;
96 } 100 }
@@ -294,15 +298,15 @@ function normalize_spaces($string)
294 * Requires php-intl to display international datetimes, 298 * Requires php-intl to display international datetimes,
295 * otherwise default format '%c' will be returned. 299 * otherwise default format '%c' will be returned.
296 * 300 *
297 * @param DateTime $date to format. 301 * @param DateTimeInterface $date to format.
298 * @param bool $time Displays time if true. 302 * @param bool $time Displays time if true.
299 * @param bool $intl Use international format if true. 303 * @param bool $intl Use international format if true.
300 * 304 *
301 * @return bool|string Formatted date, or false if the input is invalid. 305 * @return bool|string Formatted date, or false if the input is invalid.
302 */ 306 */
303function format_date($date, $time = true, $intl = true) 307function format_date($date, $time = true, $intl = true)
304{ 308{
305 if (! $date instanceof DateTime) { 309 if (! $date instanceof DateTimeInterface) {
306 return false; 310 return false;
307 } 311 }
308 312
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index 4745ac94..09ce6445 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -71,7 +71,14 @@ class ApiMiddleware
71 $response = $e->getApiResponse(); 71 $response = $e->getApiResponse();
72 } 72 }
73 73
74 return $response; 74 return $response
75 ->withHeader('Access-Control-Allow-Origin', '*')
76 ->withHeader(
77 'Access-Control-Allow-Headers',
78 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
79 )
80 ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
81 ;
75 } 82 }
76 83
77 /** 84 /**
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index 5156a5f7..faebb8f5 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -67,7 +67,7 @@ class ApiUtils
67 if (! $bookmark->isNote()) { 67 if (! $bookmark->isNote()) {
68 $out['url'] = $bookmark->getUrl(); 68 $out['url'] = $bookmark->getUrl();
69 } else { 69 } else {
70 $out['url'] = $indexUrl . $bookmark->getUrl(); 70 $out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
71 } 71 }
72 $out['shorturl'] = $bookmark->getShortUrl(); 72 $out['shorturl'] = $bookmark->getShortUrl();
73 $out['title'] = $bookmark->getTitle(); 73 $out['title'] = $bookmark->getTitle();
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php
index f9b21d3d..1beb8be2 100644
--- a/application/bookmark/Bookmark.php
+++ b/application/bookmark/Bookmark.php
@@ -3,6 +3,7 @@
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use DateTime; 5use DateTime;
6use DateTimeInterface;
6use Shaarli\Bookmark\Exception\InvalidBookmarkException; 7use Shaarli\Bookmark\Exception\InvalidBookmarkException;
7 8
8/** 9/**
@@ -36,16 +37,16 @@ class Bookmark
36 /** @var array List of bookmark's tags */ 37 /** @var array List of bookmark's tags */
37 protected $tags; 38 protected $tags;
38 39
39 /** @var string Thumbnail's URL - false if no thumbnail could be found */ 40 /** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */
40 protected $thumbnail; 41 protected $thumbnail;
41 42
42 /** @var bool Set to true if the bookmark is set as sticky */ 43 /** @var bool Set to true if the bookmark is set as sticky */
43 protected $sticky; 44 protected $sticky;
44 45
45 /** @var DateTime Creation datetime */ 46 /** @var DateTimeInterface Creation datetime */
46 protected $created; 47 protected $created;
47 48
48 /** @var DateTime Update datetime */ 49 /** @var DateTimeInterface datetime */
49 protected $updated; 50 protected $updated;
50 51
51 /** @var bool True if the bookmark can only be seen while logged in */ 52 /** @var bool True if the bookmark can only be seen while logged in */
@@ -100,12 +101,12 @@ class Bookmark
100 || ! is_int($this->id) 101 || ! is_int($this->id)
101 || empty($this->shortUrl) 102 || empty($this->shortUrl)
102 || empty($this->created) 103 || empty($this->created)
103 || ! $this->created instanceof DateTime 104 || ! $this->created instanceof DateTimeInterface
104 ) { 105 ) {
105 throw new InvalidBookmarkException($this); 106 throw new InvalidBookmarkException($this);
106 } 107 }
107 if (empty($this->url)) { 108 if (empty($this->url)) {
108 $this->url = '?'. $this->shortUrl; 109 $this->url = '/shaare/'. $this->shortUrl;
109 } 110 }
110 if (empty($this->title)) { 111 if (empty($this->title)) {
111 $this->title = $this->url; 112 $this->title = $this->url;
@@ -188,7 +189,7 @@ class Bookmark
188 /** 189 /**
189 * Get the Created. 190 * Get the Created.
190 * 191 *
191 * @return DateTime 192 * @return DateTimeInterface
192 */ 193 */
193 public function getCreated() 194 public function getCreated()
194 { 195 {
@@ -198,7 +199,7 @@ class Bookmark
198 /** 199 /**
199 * Get the Updated. 200 * Get the Updated.
200 * 201 *
201 * @return DateTime 202 * @return DateTimeInterface
202 */ 203 */
203 public function getUpdated() 204 public function getUpdated()
204 { 205 {
@@ -270,7 +271,7 @@ class Bookmark
270 * Set the Created. 271 * Set the Created.
271 * Note: you shouldn't set this manually except for special cases (like bookmark import) 272 * Note: you shouldn't set this manually except for special cases (like bookmark import)
272 * 273 *
273 * @param DateTime $created 274 * @param DateTimeInterface $created
274 * 275 *
275 * @return Bookmark 276 * @return Bookmark
276 */ 277 */
@@ -284,7 +285,7 @@ class Bookmark
284 /** 285 /**
285 * Set the Updated. 286 * Set the Updated.
286 * 287 *
287 * @param DateTime $updated 288 * @param DateTimeInterface $updated
288 * 289 *
289 * @return Bookmark 290 * @return Bookmark
290 */ 291 */
@@ -346,7 +347,7 @@ class Bookmark
346 /** 347 /**
347 * Get the Thumbnail. 348 * Get the Thumbnail.
348 * 349 *
349 * @return string|bool 350 * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found
350 */ 351 */
351 public function getThumbnail() 352 public function getThumbnail()
352 { 353 {
@@ -356,7 +357,7 @@ class Bookmark
356 /** 357 /**
357 * Set the Thumbnail. 358 * Set the Thumbnail.
358 * 359 *
359 * @param string|bool $thumbnail 360 * @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found
360 * 361 *
361 * @return Bookmark 362 * @return Bookmark
362 */ 363 */
@@ -405,7 +406,7 @@ class Bookmark
405 public function isNote() 406 public function isNote()
406 { 407 {
407 // We check empty value to get a valid result if the link has not been saved yet 408 // We check empty value to get a valid result if the link has not been saved yet
408 return empty($this->url) || $this->url[0] === '?'; 409 return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
409 } 410 }
410 411
411 /** 412 /**
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
index 9c59e139..b3a90ed4 100644
--- a/application/bookmark/BookmarkFileService.php
+++ b/application/bookmark/BookmarkFileService.php
@@ -6,12 +6,14 @@ namespace Shaarli\Bookmark;
6 6
7use Exception; 7use Exception;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
9use Shaarli\Bookmark\Exception\EmptyDataStoreException; 10use Shaarli\Bookmark\Exception\EmptyDataStoreException;
10use Shaarli\Config\ConfigManager; 11use Shaarli\Config\ConfigManager;
11use Shaarli\Formatter\BookmarkMarkdownFormatter; 12use Shaarli\Formatter\BookmarkMarkdownFormatter;
12use Shaarli\History; 13use Shaarli\History;
13use Shaarli\Legacy\LegacyLinkDB; 14use Shaarli\Legacy\LegacyLinkDB;
14use Shaarli\Legacy\LegacyUpdater; 15use Shaarli\Legacy\LegacyUpdater;
16use Shaarli\Render\PageCacheManager;
15use Shaarli\Updater\UpdaterUtils; 17use Shaarli\Updater\UpdaterUtils;
16 18
17/** 19/**
@@ -39,6 +41,9 @@ class BookmarkFileService implements BookmarkServiceInterface
39 /** @var History instance */ 41 /** @var History instance */
40 protected $history; 42 protected $history;
41 43
44 /** @var PageCacheManager instance */
45 protected $pageCacheManager;
46
42 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ 47 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
43 protected $isLoggedIn; 48 protected $isLoggedIn;
44 49
@@ -49,6 +54,7 @@ class BookmarkFileService implements BookmarkServiceInterface
49 { 54 {
50 $this->conf = $conf; 55 $this->conf = $conf;
51 $this->history = $history; 56 $this->history = $history;
57 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
52 $this->bookmarksIO = new BookmarkIO($this->conf); 58 $this->bookmarksIO = new BookmarkIO($this->conf);
53 $this->isLoggedIn = $isLoggedIn; 59 $this->isLoggedIn = $isLoggedIn;
54 60
@@ -57,10 +63,16 @@ class BookmarkFileService implements BookmarkServiceInterface
57 } else { 63 } else {
58 try { 64 try {
59 $this->bookmarks = $this->bookmarksIO->read(); 65 $this->bookmarks = $this->bookmarksIO->read();
60 } catch (EmptyDataStoreException $e) { 66 } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
61 $this->bookmarks = new BookmarkArray(); 67 $this->bookmarks = new BookmarkArray();
62 if ($isLoggedIn) { 68
63 $this->save(); 69 if ($this->isLoggedIn) {
70 // Datastore file does not exists, we initialize it with default bookmarks.
71 if ($e instanceof DatastoreNotInitializedException) {
72 $this->initialize();
73 } else {
74 $this->save();
75 }
64 } 76 }
65 } 77 }
66 78
@@ -88,7 +100,7 @@ class BookmarkFileService implements BookmarkServiceInterface
88 throw new Exception('Not authorized'); 100 throw new Exception('Not authorized');
89 } 101 }
90 102
91 return $bookmark; 103 return $first;
92 } 104 }
93 105
94 /** 106 /**
@@ -149,7 +161,7 @@ class BookmarkFileService implements BookmarkServiceInterface
149 */ 161 */
150 public function set($bookmark, $save = true) 162 public function set($bookmark, $save = true)
151 { 163 {
152 if ($this->isLoggedIn !== true) { 164 if (true !== $this->isLoggedIn) {
153 throw new Exception(t('You\'re not authorized to alter the datastore')); 165 throw new Exception(t('You\'re not authorized to alter the datastore'));
154 } 166 }
155 if (! $bookmark instanceof Bookmark) { 167 if (! $bookmark instanceof Bookmark) {
@@ -174,7 +186,7 @@ class BookmarkFileService implements BookmarkServiceInterface
174 */ 186 */
175 public function add($bookmark, $save = true) 187 public function add($bookmark, $save = true)
176 { 188 {
177 if ($this->isLoggedIn !== true) { 189 if (true !== $this->isLoggedIn) {
178 throw new Exception(t('You\'re not authorized to alter the datastore')); 190 throw new Exception(t('You\'re not authorized to alter the datastore'));
179 } 191 }
180 if (! $bookmark instanceof Bookmark) { 192 if (! $bookmark instanceof Bookmark) {
@@ -199,7 +211,7 @@ class BookmarkFileService implements BookmarkServiceInterface
199 */ 211 */
200 public function addOrSet($bookmark, $save = true) 212 public function addOrSet($bookmark, $save = true)
201 { 213 {
202 if ($this->isLoggedIn !== true) { 214 if (true !== $this->isLoggedIn) {
203 throw new Exception(t('You\'re not authorized to alter the datastore')); 215 throw new Exception(t('You\'re not authorized to alter the datastore'));
204 } 216 }
205 if (! $bookmark instanceof Bookmark) { 217 if (! $bookmark instanceof Bookmark) {
@@ -216,7 +228,7 @@ class BookmarkFileService implements BookmarkServiceInterface
216 */ 228 */
217 public function remove($bookmark, $save = true) 229 public function remove($bookmark, $save = true)
218 { 230 {
219 if ($this->isLoggedIn !== true) { 231 if (true !== $this->isLoggedIn) {
220 throw new Exception(t('You\'re not authorized to alter the datastore')); 232 throw new Exception(t('You\'re not authorized to alter the datastore'));
221 } 233 }
222 if (! $bookmark instanceof Bookmark) { 234 if (! $bookmark instanceof Bookmark) {
@@ -269,13 +281,14 @@ class BookmarkFileService implements BookmarkServiceInterface
269 */ 281 */
270 public function save() 282 public function save()
271 { 283 {
272 if (!$this->isLoggedIn) { 284 if (true !== $this->isLoggedIn) {
273 // TODO: raise an Exception instead 285 // TODO: raise an Exception instead
274 die('You are not authorized to change the database.'); 286 die('You are not authorized to change the database.');
275 } 287 }
288
276 $this->bookmarks->reorder(); 289 $this->bookmarks->reorder();
277 $this->bookmarksIO->write($this->bookmarks); 290 $this->bookmarksIO->write($this->bookmarks);
278 invalidateCaches($this->conf->get('resource.page_cache')); 291 $this->pageCacheManager->invalidateCaches();
279 } 292 }
280 293
281 /** 294 /**
@@ -291,6 +304,7 @@ class BookmarkFileService implements BookmarkServiceInterface
291 if (empty($tag) 304 if (empty($tag)
292 || (! $this->isLoggedIn && startsWith($tag, '.')) 305 || (! $this->isLoggedIn && startsWith($tag, '.'))
293 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG 306 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
307 || in_array($tag, $filteringTags, true)
294 ) { 308 ) {
295 continue; 309 continue;
296 } 310 }
@@ -349,6 +363,10 @@ class BookmarkFileService implements BookmarkServiceInterface
349 { 363 {
350 $initializer = new BookmarkInitializer($this); 364 $initializer = new BookmarkInitializer($this);
351 $initializer->initialize(); 365 $initializer->initialize();
366
367 if (true === $this->isLoggedIn) {
368 $this->save();
369 }
352 } 370 }
353 371
354 /** 372 /**
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php
index fd556679..797a36b8 100644
--- a/application/bookmark/BookmarkFilter.php
+++ b/application/bookmark/BookmarkFilter.php
@@ -436,7 +436,7 @@ class BookmarkFilter
436 throw new Exception('Invalid date format'); 436 throw new Exception('Invalid date format');
437 } 437 }
438 438
439 $filtered = array(); 439 $filtered = [];
440 foreach ($this->bookmarks as $key => $l) { 440 foreach ($this->bookmarks as $key => $l) {
441 if ($l->getCreated()->format('Ymd') == $day) { 441 if ($l->getCreated()->format('Ymd') == $day) {
442 $filtered[$key] = $l; 442 $filtered[$key] = $l;
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php
index ae9ffcb4..6bf7f365 100644
--- a/application/bookmark/BookmarkIO.php
+++ b/application/bookmark/BookmarkIO.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
5use Shaarli\Bookmark\Exception\EmptyDataStoreException; 6use Shaarli\Bookmark\Exception\EmptyDataStoreException;
6use Shaarli\Bookmark\Exception\NotWritableDataStoreException; 7use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
@@ -52,13 +53,14 @@ class BookmarkIO
52 * 53 *
53 * @return BookmarkArray instance 54 * @return BookmarkArray instance
54 * 55 *
55 * @throws NotWritableDataStoreException Data couldn't be loaded 56 * @throws NotWritableDataStoreException Data couldn't be loaded
56 * @throws EmptyDataStoreException Datastore doesn't exist 57 * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark
58 * @throws DatastoreNotInitializedException File does not exists
57 */ 59 */
58 public function read() 60 public function read()
59 { 61 {
60 if (! file_exists($this->datastore)) { 62 if (! file_exists($this->datastore)) {
61 throw new EmptyDataStoreException(); 63 throw new DatastoreNotInitializedException();
62 } 64 }
63 65
64 if (!is_writable($this->datastore)) { 66 if (!is_writable($this->datastore)) {
@@ -102,7 +104,5 @@ class BookmarkIO
102 $this->datastore, 104 $this->datastore,
103 self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix 105 self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
104 ); 106 );
105
106 invalidateCaches($this->conf->get('resource.page_cache'));
107 } 107 }
108} 108}
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php
index 9eee9a35..cd2d1724 100644
--- a/application/bookmark/BookmarkInitializer.php
+++ b/application/bookmark/BookmarkInitializer.php
@@ -6,8 +6,7 @@ namespace Shaarli\Bookmark;
6 * Class BookmarkInitializer 6 * Class BookmarkInitializer
7 * 7 *
8 * This class is used to initialized default bookmarks after a fresh install of Shaarli. 8 * This class is used to initialized default bookmarks after a fresh install of Shaarli.
9 * It is no longer call when the data store is empty, 9 * It should be only called if the datastore file does not exist(users might want to delete the default bookmarks).
10 * because user might want to delete default bookmarks after the install.
11 * 10 *
12 * To prevent data corruption, it does not overwrite existing bookmarks, 11 * To prevent data corruption, it does not overwrite existing bookmarks,
13 * even though there should not be any. 12 * even though there should not be any.
@@ -36,11 +35,11 @@ class BookmarkInitializer
36 { 35 {
37 $bookmark = new Bookmark(); 36 $bookmark = new Bookmark();
38 $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); 37 $bookmark->setTitle(t('My secret stuff... - Pastebin.com'));
39 $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []); 38 $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=');
40 $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.')); 39 $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'));
41 $bookmark->setTagsString('secretstuff'); 40 $bookmark->setTagsString('secretstuff');
42 $bookmark->setPrivate(true); 41 $bookmark->setPrivate(true);
43 $this->bookmarkService->add($bookmark); 42 $this->bookmarkService->add($bookmark, false);
44 43
45 $bookmark = new Bookmark(); 44 $bookmark = new Bookmark();
46 $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); 45 $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service'));
@@ -54,6 +53,6 @@ To learn how to use Shaarli, consult the link "Documentation" at the bottom of t
54You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' 53You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
55 )); 54 ));
56 $bookmark->setTagsString('opensource software'); 55 $bookmark->setTagsString('opensource software');
57 $this->bookmarkService->add($bookmark); 56 $this->bookmarkService->add($bookmark, false);
58 } 57 }
59} 58}
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php
index 7b7a4f09..ce8bd912 100644
--- a/application/bookmark/BookmarkServiceInterface.php
+++ b/application/bookmark/BookmarkServiceInterface.php
@@ -6,7 +6,6 @@ namespace Shaarli\Bookmark;
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 6use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7use Shaarli\Bookmark\Exception\NotWritableDataStoreException; 7use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
8use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\Exceptions\IOException;
10use Shaarli\History; 9use Shaarli\History;
11 10
12/** 11/**
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index 88379430..68914fca 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -3,112 +3,6 @@
3use Shaarli\Bookmark\Bookmark; 3use Shaarli\Bookmark\Bookmark;
4 4
5/** 5/**
6 * Get cURL callback function for CURLOPT_WRITEFUNCTION
7 *
8 * @param string $charset to extract from the downloaded page (reference)
9 * @param string $title to extract from the downloaded page (reference)
10 * @param string $description to extract from the downloaded page (reference)
11 * @param string $keywords to extract from the downloaded page (reference)
12 * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
13 * @param string $curlGetInfo Optionally overrides curl_getinfo function
14 *
15 * @return Closure
16 */
17function get_curl_download_callback(
18 &$charset,
19 &$title,
20 &$description,
21 &$keywords,
22 $retrieveDescription,
23 $curlGetInfo = 'curl_getinfo'
24) {
25 $isRedirected = false;
26 $currentChunk = 0;
27 $foundChunk = null;
28
29 /**
30 * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
31 *
32 * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
33 * Then we extract the title and the charset and stop the download when it's done.
34 *
35 * @param resource $ch cURL resource
36 * @param string $data chunk of data being downloaded
37 *
38 * @return int|bool length of $data or false if we need to stop the download
39 */
40 return function (&$ch, $data) use (
41 $retrieveDescription,
42 $curlGetInfo,
43 &$charset,
44 &$title,
45 &$description,
46 &$keywords,
47 &$isRedirected,
48 &$currentChunk,
49 &$foundChunk
50 ) {
51 $currentChunk++;
52 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
53 if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
54 $isRedirected = true;
55 return strlen($data);
56 }
57 if (!empty($responseCode) && $responseCode !== 200) {
58 return false;
59 }
60 // After a redirection, the content type will keep the previous request value
61 // until it finds the next content-type header.
62 if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
63 $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
64 }
65 if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
66 return false;
67 }
68 if (!empty($contentType) && empty($charset)) {
69 $charset = header_extract_charset($contentType);
70 }
71 if (empty($charset)) {
72 $charset = html_extract_charset($data);
73 }
74 if (empty($title)) {
75 $title = html_extract_title($data);
76 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
77 }
78 if ($retrieveDescription && empty($description)) {
79 $description = html_extract_tag('description', $data);
80 $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
81 }
82 if ($retrieveDescription && empty($keywords)) {
83 $keywords = html_extract_tag('keywords', $data);
84 if (! empty($keywords)) {
85 $foundChunk = $currentChunk;
86 // Keywords use the format tag1, tag2 multiple words, tag
87 // So we format them to match Shaarli's separator and glue multiple words with '-'
88 $keywords = implode(' ', array_map(function($keyword) {
89 return implode('-', preg_split('/\s+/', trim($keyword)));
90 }, explode(',', $keywords)));
91 }
92 }
93
94 // We got everything we want, stop the download.
95 // If we already found either the title, description or keywords,
96 // it's highly unlikely that we'll found the other metas further than
97 // in the same chunk of data or the next one. So we also stop the download after that.
98 if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
99 && (! $retrieveDescription
100 || $foundChunk < $currentChunk
101 || (!empty($title) && !empty($description) && !empty($keywords))
102 )
103 ) {
104 return false;
105 }
106
107 return strlen($data);
108 };
109}
110
111/**
112 * Extract title from an HTML document. 6 * Extract title from an HTML document.
113 * 7 *
114 * @param string $html HTML content where to look for a title. 8 * @param string $html HTML content where to look for a title.
@@ -220,7 +114,7 @@ function hashtag_autolink($description, $indexUrl = '')
220 * \p{Mn} - any non marking space (accents, umlauts, etc) 114 * \p{Mn} - any non marking space (accents, umlauts, etc)
221 */ 115 */
222 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 116 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
223 $replacement = '$1<a href="'. $indexUrl .'?addtag=$2" title="Hashtag $2">#$2</a>'; 117 $replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>';
224 return preg_replace($regex, $replacement, $description); 118 return preg_replace($regex, $replacement, $description);
225} 119}
226 120
diff --git a/application/bookmark/exception/DatastoreNotInitializedException.php b/application/bookmark/exception/DatastoreNotInitializedException.php
new file mode 100644
index 00000000..f495049d
--- /dev/null
+++ b/application/bookmark/exception/DatastoreNotInitializedException.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Bookmark\Exception;
6
7class DatastoreNotInitializedException extends \Exception
8{
9
10}
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index e45bb4c3..4c98be30 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -3,6 +3,7 @@ namespace Shaarli\Config;
3 3
4use Shaarli\Config\Exception\MissingFieldConfigException; 4use Shaarli\Config\Exception\MissingFieldConfigException;
5use Shaarli\Config\Exception\UnauthorizedConfigException; 5use Shaarli\Config\Exception\UnauthorizedConfigException;
6use Shaarli\Thumbnailer;
6 7
7/** 8/**
8 * Class ConfigManager 9 * Class ConfigManager
@@ -361,7 +362,7 @@ class ConfigManager
361 $this->setEmpty('security.open_shaarli', false); 362 $this->setEmpty('security.open_shaarli', false);
362 $this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']); 363 $this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']);
363 364
364 $this->setEmpty('general.header_link', '?'); 365 $this->setEmpty('general.header_link', '/');
365 $this->setEmpty('general.links_per_page', 20); 366 $this->setEmpty('general.links_per_page', 20);
366 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); 367 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
367 $this->setEmpty('general.default_note_title', 'Note: '); 368 $this->setEmpty('general.default_note_title', 'Note: ');
@@ -381,6 +382,7 @@ class ConfigManager
381 // default state of the 'remember me' checkbox of the login form 382 // default state of the 'remember me' checkbox of the login form
382 $this->setEmpty('privacy.remember_user_default', true); 383 $this->setEmpty('privacy.remember_user_default', true);
383 384
385 $this->setEmpty('thumbnails.mode', Thumbnailer::MODE_ALL);
384 $this->setEmpty('thumbnails.width', '125'); 386 $this->setEmpty('thumbnails.width', '125');
385 $this->setEmpty('thumbnails.height', '90'); 387 $this->setEmpty('thumbnails.height', '90');
386 388
diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php
index dbb24937..ea8dfbda 100644
--- a/application/config/ConfigPlugin.php
+++ b/application/config/ConfigPlugin.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2 2
3use Shaarli\Config\Exception\PluginConfigOrderException; 3use Shaarli\Config\Exception\PluginConfigOrderException;
4use Shaarli\Plugin\PluginManager;
4 5
5/** 6/**
6 * Plugin configuration helper functions. 7 * Plugin configuration helper functions.
@@ -19,6 +20,20 @@ use Shaarli\Config\Exception\PluginConfigOrderException;
19 */ 20 */
20function save_plugin_config($formData) 21function save_plugin_config($formData)
21{ 22{
23 // We can only save existing plugins
24 $directories = str_replace(
25 PluginManager::$PLUGINS_PATH . '/',
26 '',
27 glob(PluginManager::$PLUGINS_PATH . '/*')
28 );
29 $formData = array_filter(
30 $formData,
31 function ($value, string $key) use ($directories) {
32 return startsWith($key, 'order') || in_array($key, $directories);
33 },
34 ARRAY_FILTER_USE_BOTH
35 );
36
22 // Make sure there are no duplicates in orders. 37 // Make sure there are no duplicates in orders.
23 if (!validate_plugin_order($formData)) { 38 if (!validate_plugin_order($formData)) {
24 throw new PluginConfigOrderException(); 39 throw new PluginConfigOrderException();
@@ -69,7 +84,7 @@ function validate_plugin_order($formData)
69 $orders = array(); 84 $orders = array();
70 foreach ($formData as $key => $value) { 85 foreach ($formData as $key => $value) {
71 // No duplicate order allowed. 86 // No duplicate order allowed.
72 if (in_array($value, $orders)) { 87 if (in_array($value, $orders, true)) {
73 return false; 88 return false;
74 } 89 }
75 90
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php
index e2c78ccc..4a1a6ea7 100644
--- a/application/container/ContainerBuilder.php
+++ b/application/container/ContainerBuilder.php
@@ -7,11 +7,21 @@ namespace Shaarli\Container;
7use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Feed\FeedBuilder;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\Front\Controller\Visitor\ErrorController;
10use Shaarli\History; 13use Shaarli\History;
14use Shaarli\Http\HttpAccess;
15use Shaarli\Netscape\NetscapeBookmarkUtils;
11use Shaarli\Plugin\PluginManager; 16use Shaarli\Plugin\PluginManager;
12use Shaarli\Render\PageBuilder; 17use Shaarli\Render\PageBuilder;
18use Shaarli\Render\PageCacheManager;
19use Shaarli\Security\CookieManager;
13use Shaarli\Security\LoginManager; 20use Shaarli\Security\LoginManager;
14use Shaarli\Security\SessionManager; 21use Shaarli\Security\SessionManager;
22use Shaarli\Thumbnailer;
23use Shaarli\Updater\Updater;
24use Shaarli\Updater\UpdaterUtils;
15 25
16/** 26/**
17 * Class ContainerBuilder 27 * Class ContainerBuilder
@@ -30,22 +40,37 @@ class ContainerBuilder
30 /** @var SessionManager */ 40 /** @var SessionManager */
31 protected $session; 41 protected $session;
32 42
43 /** @var CookieManager */
44 protected $cookieManager;
45
33 /** @var LoginManager */ 46 /** @var LoginManager */
34 protected $login; 47 protected $login;
35 48
36 public function __construct(ConfigManager $conf, SessionManager $session, LoginManager $login) 49 /** @var string|null */
37 { 50 protected $basePath = null;
51
52 public function __construct(
53 ConfigManager $conf,
54 SessionManager $session,
55 CookieManager $cookieManager,
56 LoginManager $login
57 ) {
38 $this->conf = $conf; 58 $this->conf = $conf;
39 $this->session = $session; 59 $this->session = $session;
40 $this->login = $login; 60 $this->login = $login;
61 $this->cookieManager = $cookieManager;
41 } 62 }
42 63
43 public function build(): ShaarliContainer 64 public function build(): ShaarliContainer
44 { 65 {
45 $container = new ShaarliContainer(); 66 $container = new ShaarliContainer();
67
46 $container['conf'] = $this->conf; 68 $container['conf'] = $this->conf;
47 $container['sessionManager'] = $this->session; 69 $container['sessionManager'] = $this->session;
70 $container['cookieManager'] = $this->cookieManager;
48 $container['loginManager'] = $this->login; 71 $container['loginManager'] = $this->login;
72 $container['basePath'] = $this->basePath;
73
49 $container['plugins'] = function (ShaarliContainer $container): PluginManager { 74 $container['plugins'] = function (ShaarliContainer $container): PluginManager {
50 return new PluginManager($container->conf); 75 return new PluginManager($container->conf);
51 }; 76 };
@@ -73,7 +98,59 @@ class ContainerBuilder
73 }; 98 };
74 99
75 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager { 100 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
76 return new PluginManager($container->conf); 101 $pluginManager = new PluginManager($container->conf);
102
103 $pluginManager->load($container->conf->get('general.enabled_plugins'));
104
105 return $pluginManager;
106 };
107
108 $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
109 return new FormatterFactory(
110 $container->conf,
111 $container->loginManager->isLoggedIn()
112 );
113 };
114
115 $container['pageCacheManager'] = function (ShaarliContainer $container): PageCacheManager {
116 return new PageCacheManager(
117 $container->conf->get('resource.page_cache'),
118 $container->loginManager->isLoggedIn()
119 );
120 };
121
122 $container['feedBuilder'] = function (ShaarliContainer $container): FeedBuilder {
123 return new FeedBuilder(
124 $container->bookmarkService,
125 $container->formatterFactory->getFormatter(),
126 $container->environment,
127 $container->loginManager->isLoggedIn()
128 );
129 };
130
131 $container['thumbnailer'] = function (ShaarliContainer $container): Thumbnailer {
132 return new Thumbnailer($container->conf);
133 };
134
135 $container['httpAccess'] = function (): HttpAccess {
136 return new HttpAccess();
137 };
138
139 $container['netscapeBookmarkUtils'] = function (ShaarliContainer $container): NetscapeBookmarkUtils {
140 return new NetscapeBookmarkUtils($container->bookmarkService, $container->conf, $container->history);
141 };
142
143 $container['updater'] = function (ShaarliContainer $container): Updater {
144 return new Updater(
145 UpdaterUtils::read_updates_file($container->conf->get('resource.updates')),
146 $container->bookmarkService,
147 $container->conf,
148 $container->loginManager->isLoggedIn()
149 );
150 };
151
152 $container['errorHandler'] = function (ShaarliContainer $container): ErrorController {
153 return new ErrorController($container);
77 }; 154 };
78 155
79 return $container; 156 return $container;
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php
index 3fa9116e..c4fe753e 100644
--- a/application/container/ShaarliContainer.php
+++ b/application/container/ShaarliContainer.php
@@ -4,25 +4,45 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Container; 5namespace Shaarli\Container;
6 6
7use http\Cookie;
7use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Feed\FeedBuilder;
11use Shaarli\Formatter\FormatterFactory;
9use Shaarli\History; 12use Shaarli\History;
13use Shaarli\Http\HttpAccess;
14use Shaarli\Netscape\NetscapeBookmarkUtils;
10use Shaarli\Plugin\PluginManager; 15use Shaarli\Plugin\PluginManager;
11use Shaarli\Render\PageBuilder; 16use Shaarli\Render\PageBuilder;
17use Shaarli\Render\PageCacheManager;
18use Shaarli\Security\CookieManager;
12use Shaarli\Security\LoginManager; 19use Shaarli\Security\LoginManager;
13use Shaarli\Security\SessionManager; 20use Shaarli\Security\SessionManager;
21use Shaarli\Thumbnailer;
22use Shaarli\Updater\Updater;
14use Slim\Container; 23use Slim\Container;
15 24
16/** 25/**
17 * Extension of Slim container to document the injected objects. 26 * Extension of Slim container to document the injected objects.
18 * 27 *
28 * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`)
29 * @property BookmarkServiceInterface $bookmarkService
30 * @property CookieManager $cookieManager
19 * @property ConfigManager $conf 31 * @property ConfigManager $conf
20 * @property SessionManager $sessionManager 32 * @property mixed[] $environment $_SERVER automatically injected by Slim
21 * @property LoginManager $loginManager 33 * @property callable $errorHandler Overrides default Slim error display
34 * @property FeedBuilder $feedBuilder
35 * @property FormatterFactory $formatterFactory
22 * @property History $history 36 * @property History $history
23 * @property BookmarkServiceInterface $bookmarkService 37 * @property HttpAccess $httpAccess
38 * @property LoginManager $loginManager
39 * @property NetscapeBookmarkUtils $netscapeBookmarkUtils
24 * @property PageBuilder $pageBuilder 40 * @property PageBuilder $pageBuilder
41 * @property PageCacheManager $pageCacheManager
25 * @property PluginManager $pluginManager 42 * @property PluginManager $pluginManager
43 * @property SessionManager $sessionManager
44 * @property Thumbnailer $thumbnailer
45 * @property Updater $updater
26 */ 46 */
27class ShaarliContainer extends Container 47class ShaarliContainer extends Container
28{ 48{
diff --git a/application/feed/Cache.php b/application/feed/Cache.php
deleted file mode 100644
index e5d43e61..00000000
--- a/application/feed/Cache.php
+++ /dev/null
@@ -1,38 +0,0 @@
1<?php
2/**
3 * Cache utilities
4 */
5
6/**
7 * Purges all cached pages
8 *
9 * @param string $pageCacheDir page cache directory
10 *
11 * @return mixed an error string if the directory is missing
12 */
13function purgeCachedPages($pageCacheDir)
14{
15 if (! is_dir($pageCacheDir)) {
16 $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
17 error_log($error);
18 return $error;
19 }
20
21 array_map('unlink', glob($pageCacheDir.'/*.cache'));
22}
23
24/**
25 * Invalidates caches when the database is changed or the user logs out.
26 *
27 * @param string $pageCacheDir page cache directory
28 */
29function invalidateCaches($pageCacheDir)
30{
31 // Purge cache attached to session.
32 if (isset($_SESSION['tags'])) {
33 unset($_SESSION['tags']);
34 }
35
36 // Purge page cache shared by sessions.
37 purgeCachedPages($pageCacheDir);
38}
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php
index 40bd4f15..269ad877 100644
--- a/application/feed/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -43,22 +43,10 @@ class FeedBuilder
43 */ 43 */
44 protected $formatter; 44 protected $formatter;
45 45
46 /** 46 /** @var mixed[] $_SERVER */
47 * @var string RSS or ATOM feed.
48 */
49 protected $feedType;
50
51 /**
52 * @var array $_SERVER
53 */
54 protected $serverInfo; 47 protected $serverInfo;
55 48
56 /** 49 /**
57 * @var array $_GET
58 */
59 protected $userInput;
60
61 /**
62 * @var boolean True if the user is currently logged in, false otherwise. 50 * @var boolean True if the user is currently logged in, false otherwise.
63 */ 51 */
64 protected $isLoggedIn; 52 protected $isLoggedIn;
@@ -77,7 +65,6 @@ class FeedBuilder
77 * @var string server locale. 65 * @var string server locale.
78 */ 66 */
79 protected $locale; 67 protected $locale;
80
81 /** 68 /**
82 * @var DateTime Latest item date. 69 * @var DateTime Latest item date.
83 */ 70 */
@@ -88,37 +75,36 @@ class FeedBuilder
88 * 75 *
89 * @param BookmarkServiceInterface $linkDB LinkDB instance. 76 * @param BookmarkServiceInterface $linkDB LinkDB instance.
90 * @param BookmarkFormatter $formatter instance. 77 * @param BookmarkFormatter $formatter instance.
91 * @param string $feedType Type of feed.
92 * @param array $serverInfo $_SERVER. 78 * @param array $serverInfo $_SERVER.
93 * @param array $userInput $_GET.
94 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise. 79 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
95 */ 80 */
96 public function __construct($linkDB, $formatter, $feedType, $serverInfo, $userInput, $isLoggedIn) 81 public function __construct($linkDB, $formatter, $serverInfo, $isLoggedIn)
97 { 82 {
98 $this->linkDB = $linkDB; 83 $this->linkDB = $linkDB;
99 $this->formatter = $formatter; 84 $this->formatter = $formatter;
100 $this->feedType = $feedType;
101 $this->serverInfo = $serverInfo; 85 $this->serverInfo = $serverInfo;
102 $this->userInput = $userInput;
103 $this->isLoggedIn = $isLoggedIn; 86 $this->isLoggedIn = $isLoggedIn;
104 } 87 }
105 88
106 /** 89 /**
107 * Build data for feed templates. 90 * Build data for feed templates.
108 * 91 *
92 * @param string $feedType Type of feed (RSS/ATOM).
93 * @param array $userInput $_GET.
94 *
109 * @return array Formatted data for feeds templates. 95 * @return array Formatted data for feeds templates.
110 */ 96 */
111 public function buildData() 97 public function buildData(string $feedType, ?array $userInput)
112 { 98 {
113 // Search for untagged bookmarks 99 // Search for untagged bookmarks
114 if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { 100 if (isset($this->userInput['searchtags']) && empty($userInput['searchtags'])) {
115 $this->userInput['searchtags'] = false; 101 $userInput['searchtags'] = false;
116 } 102 }
117 103
118 // Optionally filter the results: 104 // Optionally filter the results:
119 $linksToDisplay = $this->linkDB->search($this->userInput); 105 $linksToDisplay = $this->linkDB->search($userInput);
120 106
121 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay)); 107 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
122 108
123 // Can't use array_keys() because $link is a LinkDB instance and not a real array. 109 // Can't use array_keys() because $link is a LinkDB instance and not a real array.
124 $keys = array(); 110 $keys = array();
@@ -130,11 +116,11 @@ class FeedBuilder
130 $this->formatter->addContextData('index_url', $pageaddr); 116 $this->formatter->addContextData('index_url', $pageaddr);
131 $linkDisplayed = array(); 117 $linkDisplayed = array();
132 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { 118 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
133 $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr); 119 $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
134 } 120 }
135 121
136 $data['language'] = $this->getTypeLanguage(); 122 $data['language'] = $this->getTypeLanguage($feedType);
137 $data['last_update'] = $this->getLatestDateFormatted(); 123 $data['last_update'] = $this->getLatestDateFormatted($feedType);
138 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; 124 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
139 // Remove leading slash from REQUEST_URI. 125 // Remove leading slash from REQUEST_URI.
140 $data['self_link'] = escape(server_url($this->serverInfo)) 126 $data['self_link'] = escape(server_url($this->serverInfo))
@@ -147,17 +133,48 @@ class FeedBuilder
147 } 133 }
148 134
149 /** 135 /**
136 * Set this to true to use permalinks instead of direct bookmarks.
137 *
138 * @param boolean $usePermalinks true to force permalinks.
139 */
140 public function setUsePermalinks($usePermalinks)
141 {
142 $this->usePermalinks = $usePermalinks;
143 }
144
145 /**
146 * Set this to true to hide timestamps in feeds.
147 *
148 * @param boolean $hideDates true to enable.
149 */
150 public function setHideDates($hideDates)
151 {
152 $this->hideDates = $hideDates;
153 }
154
155 /**
156 * Set the locale. Used to show feed language.
157 *
158 * @param string $locale The locale (eg. 'fr_FR.UTF8').
159 */
160 public function setLocale($locale)
161 {
162 $this->locale = strtolower($locale);
163 }
164
165 /**
150 * Build a feed item (one per shaare). 166 * Build a feed item (one per shaare).
151 * 167 *
168 * @param string $feedType Type of feed (RSS/ATOM).
152 * @param Bookmark $link Single link array extracted from LinkDB. 169 * @param Bookmark $link Single link array extracted from LinkDB.
153 * @param string $pageaddr Index URL. 170 * @param string $pageaddr Index URL.
154 * 171 *
155 * @return array Link array with feed attributes. 172 * @return array Link array with feed attributes.
156 */ 173 */
157 protected function buildItem($link, $pageaddr) 174 protected function buildItem(string $feedType, $link, $pageaddr)
158 { 175 {
159 $data = $this->formatter->format($link); 176 $data = $this->formatter->format($link);
160 $data['guid'] = $pageaddr . '?' . $data['shorturl']; 177 $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl'];
161 if ($this->usePermalinks === true) { 178 if ($this->usePermalinks === true) {
162 $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>'; 179 $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
163 } else { 180 } else {
@@ -165,13 +182,13 @@ class FeedBuilder
165 } 182 }
166 $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink; 183 $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
167 184
168 $data['pub_iso_date'] = $this->getIsoDate($data['created']); 185 $data['pub_iso_date'] = $this->getIsoDate($feedType, $data['created']);
169 186
170 // atom:entry elements MUST contain exactly one atom:updated element. 187 // atom:entry elements MUST contain exactly one atom:updated element.
171 if (!empty($link->getUpdated())) { 188 if (!empty($link->getUpdated())) {
172 $data['up_iso_date'] = $this->getIsoDate($data['updated'], DateTime::ATOM); 189 $data['up_iso_date'] = $this->getIsoDate($feedType, $data['updated'], DateTime::ATOM);
173 } else { 190 } else {
174 $data['up_iso_date'] = $this->getIsoDate($data['created'], DateTime::ATOM); 191 $data['up_iso_date'] = $this->getIsoDate($feedType, $data['created'], DateTime::ATOM);
175 } 192 }
176 193
177 // Save the more recent item. 194 // Save the more recent item.
@@ -186,51 +203,23 @@ class FeedBuilder
186 } 203 }
187 204
188 /** 205 /**
189 * Set this to true to use permalinks instead of direct bookmarks.
190 *
191 * @param boolean $usePermalinks true to force permalinks.
192 */
193 public function setUsePermalinks($usePermalinks)
194 {
195 $this->usePermalinks = $usePermalinks;
196 }
197
198 /**
199 * Set this to true to hide timestamps in feeds.
200 *
201 * @param boolean $hideDates true to enable.
202 */
203 public function setHideDates($hideDates)
204 {
205 $this->hideDates = $hideDates;
206 }
207
208 /**
209 * Set the locale. Used to show feed language.
210 *
211 * @param string $locale The locale (eg. 'fr_FR.UTF8').
212 */
213 public function setLocale($locale)
214 {
215 $this->locale = strtolower($locale);
216 }
217
218 /**
219 * Get the language according to the feed type, based on the locale: 206 * Get the language according to the feed type, based on the locale:
220 * 207 *
221 * - RSS format: en-us (default: 'en-en'). 208 * - RSS format: en-us (default: 'en-en').
222 * - ATOM format: fr (default: 'en'). 209 * - ATOM format: fr (default: 'en').
223 * 210 *
211 * @param string $feedType Type of feed (RSS/ATOM).
212 *
224 * @return string The language. 213 * @return string The language.
225 */ 214 */
226 public function getTypeLanguage() 215 protected function getTypeLanguage(string $feedType)
227 { 216 {
228 // Use the locale do define the language, if available. 217 // Use the locale do define the language, if available.
229 if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) { 218 if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) {
230 $length = ($this->feedType === self::$FEED_RSS) ? 5 : 2; 219 $length = ($feedType === self::$FEED_RSS) ? 5 : 2;
231 return str_replace('_', '-', substr($this->locale, 0, $length)); 220 return str_replace('_', '-', substr($this->locale, 0, $length));
232 } 221 }
233 return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en'; 222 return ($feedType === self::$FEED_RSS) ? 'en-en' : 'en';
234 } 223 }
235 224
236 /** 225 /**
@@ -238,32 +227,35 @@ class FeedBuilder
238 * 227 *
239 * Return an empty string if invalid DateTime is passed. 228 * Return an empty string if invalid DateTime is passed.
240 * 229 *
230 * @param string $feedType Type of feed (RSS/ATOM).
231 *
241 * @return string Formatted date. 232 * @return string Formatted date.
242 */ 233 */
243 protected function getLatestDateFormatted() 234 protected function getLatestDateFormatted(string $feedType)
244 { 235 {
245 if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) { 236 if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) {
246 return ''; 237 return '';
247 } 238 }
248 239
249 $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM; 240 $type = ($feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
250 return $this->latestDate->format($type); 241 return $this->latestDate->format($type);
251 } 242 }
252 243
253 /** 244 /**
254 * Get ISO date from DateTime according to feed type. 245 * Get ISO date from DateTime according to feed type.
255 * 246 *
247 * @param string $feedType Type of feed (RSS/ATOM).
256 * @param DateTime $date Date to format. 248 * @param DateTime $date Date to format.
257 * @param string|bool $format Force format. 249 * @param string|bool $format Force format.
258 * 250 *
259 * @return string Formatted date. 251 * @return string Formatted date.
260 */ 252 */
261 protected function getIsoDate(DateTime $date, $format = false) 253 protected function getIsoDate(string $feedType, DateTime $date, $format = false)
262 { 254 {
263 if ($format !== false) { 255 if ($format !== false) {
264 return $date->format($format); 256 return $date->format($format);
265 } 257 }
266 if ($this->feedType == self::$FEED_RSS) { 258 if ($feedType == self::$FEED_RSS) {
267 return $date->format(DateTime::RSS); 259 return $date->format(DateTime::RSS);
268 } 260 }
269 return $date->format(DateTime::ATOM); 261 return $date->format(DateTime::ATOM);
@@ -275,21 +267,22 @@ class FeedBuilder
275 * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. 267 * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
276 * If 'nb' is set to 'all', display all filtered bookmarks (max parameter). 268 * If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
277 * 269 *
278 * @param int $max maximum number of bookmarks to display. 270 * @param int $max maximum number of bookmarks to display.
271 * @param array $userInput $_GET.
279 * 272 *
280 * @return int number of bookmarks to display. 273 * @return int number of bookmarks to display.
281 */ 274 */
282 public function getNbLinks($max) 275 protected function getNbLinks($max, ?array $userInput)
283 { 276 {
284 if (empty($this->userInput['nb'])) { 277 if (empty($userInput['nb'])) {
285 return self::$DEFAULT_NB_LINKS; 278 return self::$DEFAULT_NB_LINKS;
286 } 279 }
287 280
288 if ($this->userInput['nb'] == 'all') { 281 if ($userInput['nb'] == 'all') {
289 return $max; 282 return $max;
290 } 283 }
291 284
292 $intNb = intval($this->userInput['nb']); 285 $intNb = intval($userInput['nb']);
293 if (!is_int($intNb) || $intNb == 0) { 286 if (!is_int($intNb) || $intNb == 0) {
294 return self::$DEFAULT_NB_LINKS; 287 return self::$DEFAULT_NB_LINKS;
295 } 288 }
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
index c6c59064..9d4a0fa0 100644
--- a/application/formatter/BookmarkDefaultFormatter.php
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -50,11 +50,10 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
50 */ 50 */
51 public function formatUrl($bookmark) 51 public function formatUrl($bookmark)
52 { 52 {
53 if (! empty($this->contextData['index_url']) && ( 53 if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
54 startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') 54 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
55 )) {
56 return $this->contextData['index_url'] . escape($bookmark->getUrl());
57 } 55 }
56
58 return escape($bookmark->getUrl()); 57 return escape($bookmark->getUrl());
59 } 58 }
60 59
@@ -63,11 +62,18 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
63 */ 62 */
64 protected function formatRealUrl($bookmark) 63 protected function formatRealUrl($bookmark)
65 { 64 {
66 if (! empty($this->contextData['index_url']) && ( 65 if ($bookmark->isNote()) {
67 startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') 66 if (isset($this->contextData['index_url'])) {
68 )) { 67 $prefix = rtrim($this->contextData['index_url'], '/') . '/';
69 return $this->contextData['index_url'] . escape($bookmark->getUrl()); 68 }
69
70 if (isset($this->contextData['base_path'])) {
71 $prefix = rtrim($this->contextData['base_path'], '/') . '/';
72 }
73
74 return escape($prefix ?? '') . escape(ltrim($bookmark->getUrl(), '/'));
70 } 75 }
76
71 return escape($bookmark->getUrl()); 77 return escape($bookmark->getUrl());
72 } 78 }
73 79
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php
index a80d83fc..22ba7aae 100644
--- a/application/formatter/BookmarkFormatter.php
+++ b/application/formatter/BookmarkFormatter.php
@@ -3,8 +3,8 @@
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use DateTime; 5use DateTime;
6use Shaarli\Config\ConfigManager;
7use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigManager;
8 8
9/** 9/**
10 * Class BookmarkFormatter 10 * Class BookmarkFormatter
@@ -80,6 +80,8 @@ abstract class BookmarkFormatter
80 public function addContextData($key, $value) 80 public function addContextData($key, $value)
81 { 81 {
82 $this->contextData[$key] = $value; 82 $this->contextData[$key] = $value;
83
84 return $this;
83 } 85 }
84 86
85 /** 87 /**
@@ -128,7 +130,7 @@ abstract class BookmarkFormatter
128 */ 130 */
129 protected function formatRealUrl($bookmark) 131 protected function formatRealUrl($bookmark)
130 { 132 {
131 return $bookmark->getUrl(); 133 return $this->formatUrl($bookmark);
132 } 134 }
133 135
134 /** 136 /**
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
index 077e5312..5d244d4c 100644
--- a/application/formatter/BookmarkMarkdownFormatter.php
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -114,7 +114,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
114 114
115 /** 115 /**
116 * Replace hashtag in Markdown links format 116 * Replace hashtag in Markdown links format
117 * E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)` 117 * E.g. `#hashtag` becomes `[#hashtag](./add-tag/hashtag)`
118 * It includes the index URL if specified. 118 * It includes the index URL if specified.
119 * 119 *
120 * @param string $description 120 * @param string $description
@@ -133,7 +133,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
133 * \p{Mn} - any non marking space (accents, umlauts, etc) 133 * \p{Mn} - any non marking space (accents, umlauts, etc)
134 */ 134 */
135 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 135 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
136 $replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)'; 136 $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)';
137 137
138 $descriptionLines = explode(PHP_EOL, $description); 138 $descriptionLines = explode(PHP_EOL, $description);
139 $descriptionOut = ''; 139 $descriptionOut = '';
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php
index 5f282f68..a029579f 100644
--- a/application/formatter/FormatterFactory.php
+++ b/application/formatter/FormatterFactory.php
@@ -38,7 +38,7 @@ class FormatterFactory
38 * 38 *
39 * @return BookmarkFormatter instance. 39 * @return BookmarkFormatter instance.
40 */ 40 */
41 public function getFormatter(string $type = null) 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';
diff --git a/application/front/ShaarliAdminMiddleware.php b/application/front/ShaarliAdminMiddleware.php
new file mode 100644
index 00000000..35ce4a3b
--- /dev/null
+++ b/application/front/ShaarliAdminMiddleware.php
@@ -0,0 +1,27 @@
1<?php
2
3namespace Shaarli\Front;
4
5use Slim\Http\Request;
6use Slim\Http\Response;
7
8/**
9 * Middleware used for controller requiring to be authenticated.
10 * It extends ShaarliMiddleware, and just make sure that the user is authenticated.
11 * Otherwise, it redirects to the login page.
12 */
13class ShaarliAdminMiddleware extends ShaarliMiddleware
14{
15 public function __invoke(Request $request, Response $response, callable $next): Response
16 {
17 $this->initBasePath($request);
18
19 if (true !== $this->container->loginManager->isLoggedIn()) {
20 $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
21
22 return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
23 }
24
25 return parent::__invoke($request, $response, $next);
26 }
27}
diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php
index fa6c6467..c015c0c6 100644
--- a/application/front/ShaarliMiddleware.php
+++ b/application/front/ShaarliMiddleware.php
@@ -3,7 +3,7 @@
3namespace Shaarli\Front; 3namespace Shaarli\Front;
4 4
5use Shaarli\Container\ShaarliContainer; 5use Shaarli\Container\ShaarliContainer;
6use Shaarli\Front\Exception\ShaarliException; 6use Shaarli\Front\Exception\UnauthorizedException;
7use Slim\Http\Request; 7use Slim\Http\Request;
8use Slim\Http\Response; 8use Slim\Http\Response;
9 9
@@ -24,6 +24,8 @@ class ShaarliMiddleware
24 24
25 /** 25 /**
26 * Middleware execution: 26 * Middleware execution:
27 * - run updates
28 * - if not logged in open shaarli, redirect to login
27 * - execute the controller 29 * - execute the controller
28 * - return the response 30 * - return the response
29 * 31 *
@@ -35,23 +37,78 @@ class ShaarliMiddleware
35 * 37 *
36 * @return Response response. 38 * @return Response response.
37 */ 39 */
38 public function __invoke(Request $request, Response $response, callable $next) 40 public function __invoke(Request $request, Response $response, callable $next): Response
39 { 41 {
42 $this->initBasePath($request);
43
40 try { 44 try {
41 $response = $next($request, $response); 45 if (!is_file($this->container->conf->getConfigFileExt())
42 } catch (ShaarliException $e) { 46 && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
43 $this->container->pageBuilder->assign('message', $e->getMessage()); 47 ) {
44 if ($this->container->conf->get('dev.debug', false)) { 48 return $response->withRedirect($this->container->basePath . '/install');
45 $this->container->pageBuilder->assign(
46 'stacktrace',
47 nl2br(get_class($this) .': '. $e->getTraceAsString())
48 );
49 } 49 }
50 50
51 $response = $response->withStatus($e->getCode()); 51 $this->runUpdates();
52 $response = $response->write($this->container->pageBuilder->render('error')); 52 $this->checkOpenShaarli($request, $response, $next);
53
54 return $next($request, $response);
55 } catch (UnauthorizedException $e) {
56 $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
57
58 return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
59 }
60 // Other exceptions are handled by ErrorController
61 }
62
63 /**
64 * Run the updater for every requests processed while logged in.
65 */
66 protected function runUpdates(): void
67 {
68 if ($this->container->loginManager->isLoggedIn() !== true) {
69 return;
70 }
71
72 $this->container->updater->setBasePath($this->container->basePath);
73 $newUpdates = $this->container->updater->update();
74 if (!empty($newUpdates)) {
75 $this->container->updater->writeUpdates(
76 $this->container->conf->get('resource.updates'),
77 $this->container->updater->getDoneUpdates()
78 );
79
80 $this->container->pageCacheManager->invalidateCaches();
81 }
82 }
83
84 /**
85 * Access is denied to most pages with `hide_public_links` + `force_login` settings.
86 */
87 protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool
88 {
89 if (// if the user isn't logged in
90 !$this->container->loginManager->isLoggedIn()
91 // and Shaarli doesn't have public content...
92 && $this->container->conf->get('privacy.hide_public_links')
93 // and is configured to enforce the login
94 && $this->container->conf->get('privacy.force_login')
95 // and the current page isn't already the login page
96 // and the user is not requesting a feed (which would lead to a different content-type as expected)
97 && !in_array($next->getName(), ['login', 'atom', 'rss'], true)
98 ) {
99 throw new UnauthorizedException();
53 } 100 }
54 101
55 return $response; 102 return true;
103 }
104
105 /**
106 * Initialize the URL base path if it hasn't been defined yet.
107 */
108 protected function initBasePath(Request $request): void
109 {
110 if (null === $this->container->basePath) {
111 $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
112 }
56 } 113 }
57} 114}
diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php
new file mode 100644
index 00000000..e675fcca
--- /dev/null
+++ b/application/front/controller/admin/ConfigureController.php
@@ -0,0 +1,126 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Languages;
8use Shaarli\Render\TemplatePage;
9use Shaarli\Render\ThemeUtils;
10use Shaarli\Thumbnailer;
11use Slim\Http\Request;
12use Slim\Http\Response;
13use Throwable;
14
15/**
16 * Class ConfigureController
17 *
18 * Slim controller used to handle Shaarli configuration page (display + save new config).
19 */
20class ConfigureController extends ShaarliAdminController
21{
22 /**
23 * GET /admin/configure - Displays the configuration page
24 */
25 public function index(Request $request, Response $response): Response
26 {
27 $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
28 $this->assignView('theme', $this->container->conf->get('resource.theme'));
29 $this->assignView(
30 'theme_available',
31 ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl'))
32 );
33 $this->assignView('formatter_available', ['default', 'markdown']);
34 list($continents, $cities) = generateTimeZoneData(
35 timezone_identifiers_list(),
36 $this->container->conf->get('general.timezone')
37 );
38 $this->assignView('continents', $continents);
39 $this->assignView('cities', $cities);
40 $this->assignView('retrieve_description', $this->container->conf->get('general.retrieve_description', false));
41 $this->assignView('private_links_default', $this->container->conf->get('privacy.default_private_links', false));
42 $this->assignView(
43 'session_protection_disabled',
44 $this->container->conf->get('security.session_protection_disabled', false)
45 );
46 $this->assignView('enable_rss_permalinks', $this->container->conf->get('feed.rss_permalinks', false));
47 $this->assignView('enable_update_check', $this->container->conf->get('updates.check_updates', true));
48 $this->assignView('hide_public_links', $this->container->conf->get('privacy.hide_public_links', false));
49 $this->assignView('api_enabled', $this->container->conf->get('api.enabled', true));
50 $this->assignView('api_secret', $this->container->conf->get('api.secret'));
51 $this->assignView('languages', Languages::getAvailableLanguages());
52 $this->assignView('gd_enabled', extension_loaded('gd'));
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'));
55
56 return $response->write($this->render(TemplatePage::CONFIGURE));
57 }
58
59 /**
60 * POST /admin/configure - Update Shaarli's configuration
61 */
62 public function save(Request $request, Response $response): Response
63 {
64 $this->checkToken($request);
65
66 $continent = $request->getParam('continent');
67 $city = $request->getParam('city');
68 $tz = 'UTC';
69 if (null !== $continent && null !== $city && isTimeZoneValid($continent, $city)) {
70 $tz = $continent . '/' . $city;
71 }
72
73 $this->container->conf->set('general.timezone', $tz);
74 $this->container->conf->set('general.title', escape($request->getParam('title')));
75 $this->container->conf->set('general.header_link', escape($request->getParam('titleLink')));
76 $this->container->conf->set('general.retrieve_description', !empty($request->getParam('retrieveDescription')));
77 $this->container->conf->set('resource.theme', escape($request->getParam('theme')));
78 $this->container->conf->set(
79 'security.session_protection_disabled',
80 !empty($request->getParam('disablesessionprotection'))
81 );
82 $this->container->conf->set(
83 'privacy.default_private_links',
84 !empty($request->getParam('privateLinkByDefault'))
85 );
86 $this->container->conf->set('feed.rss_permalinks', !empty($request->getParam('enableRssPermalinks')));
87 $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
88 $this->container->conf->set('privacy.hide_public_links', !empty($request->getParam('hidePublicLinks')));
89 $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
90 $this->container->conf->set('api.secret', escape($request->getParam('apiSecret')));
91 $this->container->conf->set('formatter', escape($request->getParam('formatter')));
92
93 if (!empty($request->getParam('language'))) {
94 $this->container->conf->set('translation.language', escape($request->getParam('language')));
95 }
96
97 $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE;
98 if ($thumbnailsMode !== Thumbnailer::MODE_NONE
99 && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
100 ) {
101 $this->saveWarningMessage(
102 t('You have enabled or changed thumbnails mode.') .
103 '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>'
104 );
105 }
106 $this->container->conf->set('thumbnails.mode', $thumbnailsMode);
107
108 try {
109 $this->container->conf->write($this->container->loginManager->isLoggedIn());
110 $this->container->history->updateSettings();
111 $this->container->pageCacheManager->invalidateCaches();
112 } catch (Throwable $e) {
113 $this->assignView('message', t('Error while writing config file after configuration update.'));
114
115 if ($this->container->conf->get('dev.debug', false)) {
116 $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
117 }
118
119 return $response->write($this->render('error'));
120 }
121
122 $this->saveSuccessMessage(t('Configuration was saved.'));
123
124 return $this->redirect($response, '/admin/configure');
125 }
126}
diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php
new file mode 100644
index 00000000..2be957fa
--- /dev/null
+++ b/application/front/controller/admin/ExportController.php
@@ -0,0 +1,80 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use DateTime;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Render\TemplatePage;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13/**
14 * Class ExportController
15 *
16 * Slim controller used to display Shaarli data export page,
17 * and process the bookmarks export as a Netscape Bookmarks file.
18 */
19class ExportController extends ShaarliAdminController
20{
21 /**
22 * GET /admin/export - Display export page
23 */
24 public function index(Request $request, Response $response): Response
25 {
26 $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
27
28 return $response->write($this->render(TemplatePage::EXPORT));
29 }
30
31 /**
32 * POST /admin/export - Process export, and serve download file named
33 * bookmarks_(all|private|public)_datetime.html
34 */
35 public function export(Request $request, Response $response): Response
36 {
37 $this->checkToken($request);
38
39 $selection = $request->getParam('selection');
40
41 if (empty($selection)) {
42 $this->saveErrorMessage(t('Please select an export mode.'));
43
44 return $this->redirect($response, '/admin/export');
45 }
46
47 $prependNoteUrl = filter_var($request->getParam('prepend_note_url') ?? false, FILTER_VALIDATE_BOOLEAN);
48
49 try {
50 $formatter = $this->container->formatterFactory->getFormatter('raw');
51
52 $this->assignView(
53 'links',
54 $this->container->netscapeBookmarkUtils->filterAndFormat(
55 $formatter,
56 $selection,
57 $prependNoteUrl,
58 index_url($this->container->environment)
59 )
60 );
61 } catch (\Exception $exc) {
62 $this->saveErrorMessage($exc->getMessage());
63
64 return $this->redirect($response, '/admin/export');
65 }
66
67 $now = new DateTime();
68 $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
69 $response = $response->withHeader(
70 'Content-disposition',
71 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
72 );
73
74 $this->assignView('date', $now->format(DateTime::RFC822));
75 $this->assignView('eol', PHP_EOL);
76 $this->assignView('selection', $selection);
77
78 return $response->write($this->render(TemplatePage::NETSCAPE_EXPORT_BOOKMARKS));
79 }
80}
diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php
new file mode 100644
index 00000000..758d5ef9
--- /dev/null
+++ b/application/front/controller/admin/ImportController.php
@@ -0,0 +1,82 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Psr\Http\Message\UploadedFileInterface;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ImportController
14 *
15 * Slim controller used to display Shaarli data import page,
16 * and import bookmarks from Netscape Bookmarks file.
17 */
18class ImportController extends ShaarliAdminController
19{
20 /**
21 * GET /admin/import - Display import page
22 */
23 public function index(Request $request, Response $response): Response
24 {
25 $this->assignView(
26 'maxfilesize',
27 get_max_upload_size(
28 ini_get('post_max_size'),
29 ini_get('upload_max_filesize'),
30 false
31 )
32 );
33 $this->assignView(
34 'maxfilesizeHuman',
35 get_max_upload_size(
36 ini_get('post_max_size'),
37 ini_get('upload_max_filesize'),
38 true
39 )
40 );
41 $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
42
43 return $response->write($this->render(TemplatePage::IMPORT));
44 }
45
46 /**
47 * POST /admin/import - Process import file provided and create bookmarks
48 */
49 public function import(Request $request, Response $response): Response
50 {
51 $this->checkToken($request);
52
53 $file = ($request->getUploadedFiles() ?? [])['filetoupload'] ?? null;
54 if (!$file instanceof UploadedFileInterface) {
55 $this->saveErrorMessage(t('No import file provided.'));
56
57 return $this->redirect($response, '/admin/import');
58 }
59
60
61 // Import bookmarks from an uploaded file
62 if (0 === $file->getSize()) {
63 // The file is too big or some form field may be missing.
64 $msg = sprintf(
65 t(
66 'The file you are trying to upload is probably bigger than what this webserver can accept'
67 .' (%s). Please upload in smaller chunks.'
68 ),
69 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
70 );
71 $this->saveErrorMessage($msg);
72
73 return $this->redirect($response, '/admin/import');
74 }
75
76 $status = $this->container->netscapeBookmarkUtils->import($request->getParams(), $file);
77
78 $this->saveSuccessMessage($status);
79
80 return $this->redirect($response, '/admin/import');
81 }
82}
diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php
new file mode 100644
index 00000000..28165129
--- /dev/null
+++ b/application/front/controller/admin/LogoutController.php
@@ -0,0 +1,33 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Security\CookieManager;
8use Shaarli\Security\LoginManager;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class LogoutController
14 *
15 * Slim controller used to logout the user.
16 * It invalidates page cache and terminate the user session. Then it redirects to the homepage.
17 */
18class LogoutController extends ShaarliAdminController
19{
20 public function index(Request $request, Response $response): Response
21 {
22 $this->container->pageCacheManager->invalidateCaches();
23 $this->container->sessionManager->logout();
24 $this->container->cookieManager->setCookieParameter(
25 CookieManager::STAY_SIGNED_IN,
26 'false',
27 0,
28 $this->container->basePath . '/'
29 );
30
31 return $this->redirect($response, '/');
32 }
33}
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php
new file mode 100644
index 00000000..33e1188e
--- /dev/null
+++ b/application/front/controller/admin/ManageShaareController.php
@@ -0,0 +1,371 @@
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') {
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 = escape([
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') ? 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 ['add-shaare', '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 = [
349 'link' => $link,
350 'link_is_new' => $isNew,
351 'http_referer' => escape($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
new file mode 100644
index 00000000..0380ef1f
--- /dev/null
+++ b/application/front/controller/admin/ManageTagController.php
@@ -0,0 +1,88 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ManageTagController
14 *
15 * Slim controller used to handle Shaarli manage tags page (rename and delete tags).
16 */
17class ManageTagController extends ShaarliAdminController
18{
19 /**
20 * GET /admin/tags - Displays the manage tags page
21 */
22 public function index(Request $request, Response $response): Response
23 {
24 $fromTag = $request->getParam('fromtag') ?? '';
25
26 $this->assignView('fromtag', escape($fromTag));
27 $this->assignView(
28 'pagetitle',
29 t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli')
30 );
31
32 return $response->write($this->render(TemplatePage::CHANGE_TAG));
33 }
34
35 /**
36 * POST /admin/tags - Update or delete provided tag
37 */
38 public function save(Request $request, Response $response): Response
39 {
40 $this->checkToken($request);
41
42 $isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag');
43
44 $fromTag = escape(trim($request->getParam('fromtag') ?? ''));
45 $toTag = escape(trim($request->getParam('totag') ?? ''));
46
47 if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) {
48 $this->saveWarningMessage(t('Invalid tags provided.'));
49
50 return $this->redirect($response, '/admin/tags');
51 }
52
53 // TODO: move this to bookmark service
54 $count = 0;
55 $bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
56 foreach ($bookmarks as $bookmark) {
57 if (false === $isDelete) {
58 $bookmark->renameTag($fromTag, $toTag);
59 } else {
60 $bookmark->deleteTag($fromTag);
61 }
62
63 $this->container->bookmarkService->set($bookmark, false);
64 $this->container->history->updateLink($bookmark);
65 $count++;
66 }
67
68 $this->container->bookmarkService->save();
69
70 if (true === $isDelete) {
71 $alert = sprintf(
72 t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count),
73 $count
74 );
75 } else {
76 $alert = sprintf(
77 t('The tag was renamed in %d bookmark.', 'The tag was renamed in %d bookmarks.', $count),
78 $count
79 );
80 }
81
82 $this->saveSuccessMessage($alert);
83
84 $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag);
85
86 return $this->redirect($response, $redirect);
87 }
88}
diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php
new file mode 100644
index 00000000..5ec0d24b
--- /dev/null
+++ b/application/front/controller/admin/PasswordController.php
@@ -0,0 +1,101 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Container\ShaarliContainer;
8use Shaarli\Front\Exception\OpenShaarliPasswordException;
9use Shaarli\Front\Exception\ShaarliFrontException;
10use Shaarli\Render\TemplatePage;
11use Slim\Http\Request;
12use Slim\Http\Response;
13use Throwable;
14
15/**
16 * Class PasswordController
17 *
18 * Slim controller used to handle passwords update.
19 */
20class PasswordController extends ShaarliAdminController
21{
22 public function __construct(ShaarliContainer $container)
23 {
24 parent::__construct($container);
25
26 $this->assignView(
27 'pagetitle',
28 t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli')
29 );
30 }
31
32 /**
33 * GET /admin/password - Displays the change password template
34 */
35 public function index(Request $request, Response $response): Response
36 {
37 return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
38 }
39
40 /**
41 * POST /admin/password - Change admin password - existing and new passwords need to be provided.
42 */
43 public function change(Request $request, Response $response): Response
44 {
45 $this->checkToken($request);
46
47 if ($this->container->conf->get('security.open_shaarli', false)) {
48 throw new OpenShaarliPasswordException();
49 }
50
51 $oldPassword = $request->getParam('oldpassword');
52 $newPassword = $request->getParam('setpassword');
53
54 if (empty($newPassword) || empty($oldPassword)) {
55 $this->saveErrorMessage(t('You must provide the current and new password to change it.'));
56
57 return $response
58 ->withStatus(400)
59 ->write($this->render(TemplatePage::CHANGE_PASSWORD))
60 ;
61 }
62
63 // Make sure old password is correct.
64 $oldHash = sha1(
65 $oldPassword .
66 $this->container->conf->get('credentials.login') .
67 $this->container->conf->get('credentials.salt')
68 );
69
70 if ($oldHash !== $this->container->conf->get('credentials.hash')) {
71 $this->saveErrorMessage(t('The old password is not correct.'));
72
73 return $response
74 ->withStatus(400)
75 ->write($this->render(TemplatePage::CHANGE_PASSWORD))
76 ;
77 }
78
79 // Save new password
80 // Salt renders rainbow-tables attacks useless.
81 $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
82 $this->container->conf->set(
83 'credentials.hash',
84 sha1(
85 $newPassword
86 . $this->container->conf->get('credentials.login')
87 . $this->container->conf->get('credentials.salt')
88 )
89 );
90
91 try {
92 $this->container->conf->write($this->container->loginManager->isLoggedIn());
93 } catch (Throwable $e) {
94 throw new ShaarliFrontException($e->getMessage(), 500, $e);
95 }
96
97 $this->saveSuccessMessage(t('Your password has been changed'));
98
99 return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
100 }
101}
diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php
new file mode 100644
index 00000000..0e09116e
--- /dev/null
+++ b/application/front/controller/admin/PluginsController.php
@@ -0,0 +1,84 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Exception;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class PluginsController
14 *
15 * Slim controller used to handle Shaarli plugins configuration page (display + save new config).
16 */
17class PluginsController extends ShaarliAdminController
18{
19 /**
20 * GET /admin/plugins - Displays the configuration page
21 */
22 public function index(Request $request, Response $response): Response
23 {
24 $pluginMeta = $this->container->pluginManager->getPluginsMeta();
25
26 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
27 $enabledPlugins = array_filter($pluginMeta, function ($v) {
28 return ($v['order'] ?? false) !== false;
29 });
30 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $this->container->conf->get('plugins', []));
31 uasort(
32 $enabledPlugins,
33 function ($a, $b) {
34 return $a['order'] - $b['order'];
35 }
36 );
37 $disabledPlugins = array_filter($pluginMeta, function ($v) {
38 return ($v['order'] ?? false) === false;
39 });
40
41 $this->assignView('enabledPlugins', $enabledPlugins);
42 $this->assignView('disabledPlugins', $disabledPlugins);
43 $this->assignView(
44 'pagetitle',
45 t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli')
46 );
47
48 return $response->write($this->render(TemplatePage::PLUGINS_ADMIN));
49 }
50
51 /**
52 * POST /admin/plugins - Update Shaarli's configuration
53 */
54 public function save(Request $request, Response $response): Response
55 {
56 $this->checkToken($request);
57
58 try {
59 $parameters = $request->getParams() ?? [];
60
61 $this->executePageHooks('save_plugin_parameters', $parameters);
62
63 if (isset($parameters['parameters_form'])) {
64 unset($parameters['parameters_form']);
65 foreach ($parameters as $param => $value) {
66 $this->container->conf->set('plugins.'. $param, escape($value));
67 }
68 } else {
69 $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters));
70 }
71
72 $this->container->conf->write($this->container->loginManager->isLoggedIn());
73 $this->container->history->updateSettings();
74
75 $this->saveSuccessMessage(t('Setting successfully saved.'));
76 } catch (Exception $e) {
77 $this->saveErrorMessage(
78 t('Error while saving plugin configuration: ') . PHP_EOL . $e->getMessage()
79 );
80 }
81
82 return $this->redirect($response, '/admin/plugins');
83 }
84}
diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php
new file mode 100644
index 00000000..d9a7a2e0
--- /dev/null
+++ b/application/front/controller/admin/SessionFilterController.php
@@ -0,0 +1,50 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Security\SessionManager;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class SessionFilterController
14 *
15 * Slim controller used to handle filters stored in the user session, such as visibility, etc.
16 */
17class SessionFilterController extends ShaarliAdminController
18{
19 /**
20 * GET /admin/visibility: allows to display only public or only private bookmarks in linklist
21 */
22 public function visibility(Request $request, Response $response, array $args): Response
23 {
24 if (false === $this->container->loginManager->isLoggedIn()) {
25 return $this->redirectFromReferer($request, $response, ['visibility']);
26 }
27
28 $newVisibility = $args['visibility'] ?? null;
29 if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) {
30 $newVisibility = null;
31 }
32
33 $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY);
34
35 // Visibility not set or not already expected value, set expected value, otherwise reset it
36 if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) {
37 // See only public bookmarks
38 $this->container->sessionManager->setSessionParameter(
39 SessionManager::KEY_VISIBILITY,
40 $newVisibility
41 );
42 } else {
43 $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY);
44 }
45
46 return $this->redirectFromReferer($request, $response, ['visibility']);
47 }
48
49
50}
diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php
new file mode 100644
index 00000000..3b5939bb
--- /dev/null
+++ b/application/front/controller/admin/ShaarliAdminController.php
@@ -0,0 +1,73 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Container\ShaarliContainer;
8use Shaarli\Front\Controller\Visitor\ShaarliVisitorController;
9use Shaarli\Front\Exception\UnauthorizedException;
10use Shaarli\Front\Exception\WrongTokenException;
11use Shaarli\Security\SessionManager;
12use Slim\Http\Request;
13
14/**
15 * Class ShaarliAdminController
16 *
17 * All admin controllers (for logged in users) MUST extend this abstract class.
18 * It makes sure that the user is properly logged in, and otherwise throw an exception
19 * which will redirect to the login page.
20 *
21 * @package Shaarli\Front\Controller\Admin
22 */
23abstract class ShaarliAdminController extends ShaarliVisitorController
24{
25 /**
26 * Any persistent action to the config or data store must check the XSRF token validity.
27 */
28 protected function checkToken(Request $request): bool
29 {
30 if (!$this->container->sessionManager->checkToken($request->getParam('token'))) {
31 throw new WrongTokenException();
32 }
33
34 return true;
35 }
36
37 /**
38 * Save a SUCCESS message in user session, which will be displayed on any template page.
39 */
40 protected function saveSuccessMessage(string $message): void
41 {
42 $this->saveMessage(SessionManager::KEY_SUCCESS_MESSAGES, $message);
43 }
44
45 /**
46 * Save a WARNING message in user session, which will be displayed on any template page.
47 */
48 protected function saveWarningMessage(string $message): void
49 {
50 $this->saveMessage(SessionManager::KEY_WARNING_MESSAGES, $message);
51 }
52
53 /**
54 * Save an ERROR message in user session, which will be displayed on any template page.
55 */
56 protected function saveErrorMessage(string $message): void
57 {
58 $this->saveMessage(SessionManager::KEY_ERROR_MESSAGES, $message);
59 }
60
61 /**
62 * Use the sessionManager to save the provided message using the proper type.
63 *
64 * @param string $type successed/warnings/errors
65 */
66 protected function saveMessage(string $type, string $message): void
67 {
68 $messages = $this->container->sessionManager->getSessionParameter($type) ?? [];
69 $messages[] = $message;
70
71 $this->container->sessionManager->setSessionParameter($type, $messages);
72 }
73}
diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php
new file mode 100644
index 00000000..81c87ed0
--- /dev/null
+++ b/application/front/controller/admin/ThumbnailsController.php
@@ -0,0 +1,65 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ToolsController
14 *
15 * Slim controller used to handle thumbnails update.
16 */
17class ThumbnailsController extends ShaarliAdminController
18{
19 /**
20 * GET /admin/thumbnails - Display thumbnails update page
21 */
22 public function index(Request $request, Response $response): Response
23 {
24 $ids = [];
25 foreach ($this->container->bookmarkService->search() as $bookmark) {
26 // A note or not HTTP(S)
27 if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) {
28 continue;
29 }
30
31 $ids[] = $bookmark->getId();
32 }
33
34 $this->assignView('ids', $ids);
35 $this->assignView(
36 'pagetitle',
37 t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli')
38 );
39
40 return $response->write($this->render(TemplatePage::THUMBNAILS));
41 }
42
43 /**
44 * PATCH /admin/shaare/{id}/thumbnail-update - Route for AJAX calls
45 */
46 public function ajaxUpdate(Request $request, Response $response, array $args): Response
47 {
48 $id = $args['id'] ?? null;
49
50 if (false === ctype_digit($id)) {
51 return $response->withStatus(400);
52 }
53
54 try {
55 $bookmark = $this->container->bookmarkService->get($id);
56 } catch (BookmarkNotFoundException $e) {
57 return $response->withStatus(404);
58 }
59
60 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
61 $this->container->bookmarkService->set($bookmark);
62
63 return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark));
64 }
65}
diff --git a/application/front/controller/admin/TokenController.php b/application/front/controller/admin/TokenController.php
new file mode 100644
index 00000000..08d68d0a
--- /dev/null
+++ b/application/front/controller/admin/TokenController.php
@@ -0,0 +1,26 @@
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 * Class TokenController
12 *
13 * Endpoint used to retrieve a XSRF token. Useful for AJAX requests.
14 */
15class TokenController extends ShaarliAdminController
16{
17 /**
18 * GET /admin/token
19 */
20 public function getToken(Request $request, Response $response): Response
21 {
22 $response = $response->withHeader('Content-Type', 'text/plain');
23
24 return $response->write($this->container->sessionManager->generateToken());
25 }
26}
diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php
new file mode 100644
index 00000000..a87f20d2
--- /dev/null
+++ b/application/front/controller/admin/ToolsController.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Render\TemplatePage;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class ToolsController
13 *
14 * Slim controller used to display the tools page.
15 */
16class ToolsController extends ShaarliAdminController
17{
18 public function index(Request $request, Response $response): Response
19 {
20 $data = [
21 'pageabsaddr' => index_url($this->container->environment),
22 'sslenabled' => is_https($this->container->environment),
23 ];
24
25 $this->executePageHooks('render_tools', $data, TemplatePage::TOOLS);
26
27 foreach ($data as $key => $value) {
28 $this->assignView($key, $value);
29 }
30
31 $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
32
33 return $response->write($this->render(TemplatePage::TOOLS));
34 }
35}
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php
new file mode 100644
index 00000000..2988bee6
--- /dev/null
+++ b/application/front/controller/visitor/BookmarkListController.php
@@ -0,0 +1,240 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Legacy\LegacyController;
10use Shaarli\Legacy\UnknowLegacyRouteException;
11use Shaarli\Render\TemplatePage;
12use Shaarli\Thumbnailer;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16/**
17 * Class BookmarkListController
18 *
19 * Slim controller used to render the bookmark list, the home page of Shaarli.
20 * It also displays permalinks, and process legacy routes based on GET parameters.
21 */
22class BookmarkListController extends ShaarliVisitorController
23{
24 /**
25 * GET / - Displays the bookmark list, with optional filter parameters.
26 */
27 public function index(Request $request, Response $response): Response
28 {
29 $legacyResponse = $this->processLegacyController($request, $response);
30 if (null !== $legacyResponse) {
31 return $legacyResponse;
32 }
33
34 $formatter = $this->container->formatterFactory->getFormatter();
35 $formatter->addContextData('base_path', $this->container->basePath);
36
37 $searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? ''));
38 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));;
39
40 // Filter bookmarks according search parameters.
41 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
42 $search = [
43 'searchtags' => $searchTags,
44 'searchterm' => $searchTerm,
45 ];
46 $linksToDisplay = $this->container->bookmarkService->search(
47 $search,
48 $visibility,
49 false,
50 !!$this->container->sessionManager->getSessionParameter('untaggedonly')
51 ) ?? [];
52
53 // ---- Handle paging.
54 $keys = [];
55 foreach ($linksToDisplay as $key => $value) {
56 $keys[] = $key;
57 }
58
59 $linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
60
61 // Select articles according to paging.
62 $pageCount = (int) ceil(count($keys) / $linksPerPage) ?: 1;
63 $page = (int) $request->getParam('page') ?? 1;
64 $page = $page < 1 ? 1 : $page;
65 $page = $page > $pageCount ? $pageCount : $page;
66
67 // Start index.
68 $i = ($page - 1) * $linksPerPage;
69 $end = $i + $linksPerPage;
70
71 $linkDisp = [];
72 $save = false;
73 while ($i < $end && $i < count($keys)) {
74 $save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save;
75 $link = $formatter->format($linksToDisplay[$keys[$i]]);
76
77 $linkDisp[$keys[$i]] = $link;
78 $i++;
79 }
80
81 if ($save) {
82 $this->container->bookmarkService->save();
83 }
84
85 // Compute paging navigation
86 $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
87 $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
88
89 $previous_page_url = '';
90 if ($i !== count($keys)) {
91 $previous_page_url = '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl;
92 }
93 $next_page_url = '';
94 if ($page > 1) {
95 $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
96 }
97
98 // Fill all template fields.
99 $data = array_merge(
100 $this->initializeTemplateVars(),
101 [
102 'previous_page_url' => $previous_page_url,
103 'next_page_url' => $next_page_url,
104 'page_current' => $page,
105 'page_max' => $pageCount,
106 'result_count' => count($linksToDisplay),
107 'search_term' => $searchTerm,
108 'search_tags' => $searchTags,
109 'visibility' => $visibility,
110 'links' => $linkDisp,
111 ]
112 );
113
114 if (!empty($searchTerm) || !empty($searchTags)) {
115 $data['pagetitle'] = t('Search: ');
116 $data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : '';
117 $bracketWrap = function ($tag) {
118 return '[' . $tag . ']';
119 };
120 $data['pagetitle'] .= ! empty($searchTags)
121 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' '
122 : '';
123 $data['pagetitle'] .= '- ';
124 }
125
126 $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli');
127
128 $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
129 $this->assignAllView($data);
130
131 return $response->write($this->render(TemplatePage::LINKLIST));
132 }
133
134 /**
135 * GET /shaare/{hash} - Display a single shaare
136 */
137 public function permalink(Request $request, Response $response, array $args): Response
138 {
139 try {
140 $bookmark = $this->container->bookmarkService->findByHash($args['hash']);
141 } catch (BookmarkNotFoundException $e) {
142 $this->assignView('error_message', $e->getMessage());
143
144 return $response->write($this->render(TemplatePage::ERROR_404));
145 }
146
147 $this->updateThumbnail($bookmark);
148
149 $formatter = $this->container->formatterFactory->getFormatter();
150 $formatter->addContextData('base_path', $this->container->basePath);
151
152 $data = array_merge(
153 $this->initializeTemplateVars(),
154 [
155 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'),
156 'links' => [$formatter->format($bookmark)],
157 ]
158 );
159
160 $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
161 $this->assignAllView($data);
162
163 return $response->write($this->render(TemplatePage::LINKLIST));
164 }
165
166 /**
167 * Update the thumbnail of a single bookmark if necessary.
168 */
169 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
170 {
171 // Logged in, thumbnails enabled, not a note, is HTTP
172 // and (never retrieved yet or no valid cache file)
173 if ($this->container->loginManager->isLoggedIn()
174 && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
175 && false !== $bookmark->getThumbnail()
176 && !$bookmark->isNote()
177 && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail()))
178 && startsWith(strtolower($bookmark->getUrl()), 'http')
179 ) {
180 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
181 $this->container->bookmarkService->set($bookmark, $writeDatastore);
182
183 return true;
184 }
185
186 return false;
187 }
188
189 /**
190 * @return string[] Default template variables without values.
191 */
192 protected function initializeTemplateVars(): array
193 {
194 return [
195 'previous_page_url' => '',
196 'next_page_url' => '',
197 'page_max' => '',
198 'search_tags' => '',
199 'result_count' => '',
200 ];
201 }
202
203 /**
204 * Process legacy routes if necessary. They used query parameters.
205 * If no legacy routes is passed, return null.
206 */
207 protected function processLegacyController(Request $request, Response $response): ?Response
208 {
209 // Legacy smallhash filter
210 $queryString = $this->container->environment['QUERY_STRING'] ?? null;
211 if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) {
212 return $this->redirect($response, '/shaare/' . $match[1]);
213 }
214
215 // Legacy controllers (mostly used for redirections)
216 if (null !== $request->getQueryParam('do')) {
217 $legacyController = new LegacyController($this->container);
218
219 try {
220 return $legacyController->process($request, $response, $request->getQueryParam('do'));
221 } catch (UnknowLegacyRouteException $e) {
222 // We ignore legacy 404
223 return null;
224 }
225 }
226
227 // Legacy GET admin routes
228 $legacyGetRoutes = array_intersect(
229 LegacyController::LEGACY_GET_ROUTES,
230 array_keys($request->getQueryParams() ?? [])
231 );
232 if (1 === count($legacyGetRoutes)) {
233 $legacyController = new LegacyController($this->container);
234
235 return $legacyController->process($request, $response, $legacyGetRoutes[0]);
236 }
237
238 return null;
239 }
240}
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php
new file mode 100644
index 00000000..54a4778f
--- /dev/null
+++ b/application/front/controller/visitor/DailyController.php
@@ -0,0 +1,192 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use DateTime;
8use DateTimeImmutable;
9use Shaarli\Bookmark\Bookmark;
10use Shaarli\Render\TemplatePage;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14/**
15 * Class DailyController
16 *
17 * Slim controller used to render the daily page.
18 */
19class DailyController extends ShaarliVisitorController
20{
21 public static $DAILY_RSS_NB_DAYS = 8;
22
23 /**
24 * Controller displaying all bookmarks published in a single day.
25 * It take a `day` date query parameter (format YYYYMMDD).
26 */
27 public function index(Request $request, Response $response): Response
28 {
29 $day = $request->getQueryParam('day') ?? date('Ymd');
30
31 $availableDates = $this->container->bookmarkService->days();
32 $nbAvailableDates = count($availableDates);
33 $index = array_search($day, $availableDates);
34
35 if ($index === false) {
36 // no bookmarks for day, but at least one day with bookmarks
37 $day = $availableDates[$nbAvailableDates - 1] ?? $day;
38 $previousDay = $availableDates[$nbAvailableDates - 2] ?? '';
39 } else {
40 $previousDay = $availableDates[$index - 1] ?? '';
41 $nextDay = $availableDates[$index + 1] ?? '';
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
56 $formatter = $this->container->formatterFactory->getFormatter();
57 $formatter->addContextData('base_path', $this->container->basePath);
58 // We pre-format some fields for proper output.
59 foreach ($linksToDisplay as $key => $bookmark) {
60 $linksToDisplay[$key] = $formatter->format($bookmark);
61 // This page is a bit specific, we need raw description to calculate the length
62 $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
63 $linksToDisplay[$key]['description'] = $bookmark->getDescription();
64 }
65
66 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
67 $data = [
68 'linksToDisplay' => $linksToDisplay,
69 'day' => $dayDate->getTimestamp(),
70 'dayDate' => $dayDate,
71 'previousday' => $previousDay ?? '',
72 'nextday' => $nextDay ?? '',
73 ];
74
75 // Hooks are called before column construction so that plugins don't have to deal with columns.
76 $this->executePageHooks('render_daily', $data, TemplatePage::DAILY);
77
78 $data['cols'] = $this->calculateColumns($data['linksToDisplay']);
79
80 $this->assignAllView($data);
81
82 $mainTitle = $this->container->conf->get('general.title', 'Shaarli');
83 $this->assignView(
84 'pagetitle',
85 t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle
86 );
87
88 return $response->write($this->render(TemplatePage::DAILY));
89 }
90
91 /**
92 * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
93 * Gives the last 7 days (which have bookmarks).
94 * This RSS feed cannot be filtered and does not trigger plugins yet.
95 */
96 public function rss(Request $request, Response $response): Response
97 {
98 $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
99
100 $pageUrl = page_url($this->container->environment);
101 $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
102
103 $cached = $cache->cachedVersion();
104 if (!empty($cached)) {
105 return $response->write($cached);
106 }
107
108 $days = [];
109 foreach ($this->container->bookmarkService->search() as $bookmark) {
110 $day = $bookmark->getCreated()->format('Ymd');
111
112 // Stop iterating after DAILY_RSS_NB_DAYS entries
113 if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) {
114 break;
115 }
116
117 $days[$day][] = $bookmark;
118 }
119
120 // Build the RSS feed.
121 $indexUrl = escape(index_url($this->container->environment));
122
123 $formatter = $this->container->formatterFactory->getFormatter();
124 $formatter->addContextData('index_url', $indexUrl);
125
126 $dataPerDay = [];
127
128 /** @var Bookmark[] $bookmarks */
129 foreach ($days as $day => $bookmarks) {
130 $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
131 $dataPerDay[$day] = [
132 'date' => $dayDatetime,
133 'date_rss' => $dayDatetime->format(DateTime::RSS),
134 'date_human' => format_date($dayDatetime, false, true),
135 'absolute_url' => $indexUrl . '/daily?day=' . $day,
136 'links' => [],
137 ];
138
139 foreach ($bookmarks as $key => $bookmark) {
140 $dataPerDay[$day]['links'][$key] = $formatter->format($bookmark);
141
142 // Make permalink URL absolute
143 if ($bookmark->isNote()) {
144 $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl();
145 }
146 }
147 }
148
149 $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
150 $this->assignView('index_url', $indexUrl);
151 $this->assignView('page_url', $pageUrl);
152 $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false));
153 $this->assignView('days', $dataPerDay);
154
155 $rssContent = $this->render(TemplatePage::DAILY_RSS);
156
157 $cache->cache($rssContent);
158
159 return $response->write($rssContent);
160 }
161
162 /**
163 * We need to spread the articles on 3 columns.
164 * did not want to use a JavaScript lib like http://masonry.desandro.com/
165 * so I manually spread entries with a simple method: I roughly evaluate the
166 * height of a div according to title and description length.
167 */
168 protected function calculateColumns(array $links): array
169 {
170 // Entries to display, for each column.
171 $columns = [[], [], []];
172 // Rough estimate of columns fill.
173 $fill = [0, 0, 0];
174 foreach ($links as $link) {
175 // Roughly estimate length of entry (by counting characters)
176 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
177 // Description: 836 characters gives roughly 342 pixel height.
178 // This is not perfect, but it's usually OK.
179 $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836;
180 if (! empty($link['thumbnail'])) {
181 $length += 100; // 1 thumbnails roughly takes 100 pixels height.
182 }
183 // Then put in column which is the less filled:
184 $smallest = min($fill); // find smallest value in array.
185 $index = array_search($smallest, $fill); // find index of this smallest value.
186 array_push($columns[$index], $link); // Put entry in this column.
187 $fill[$index] += $length;
188 }
189
190 return $columns;
191 }
192}
diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php
new file mode 100644
index 00000000..10aa84c8
--- /dev/null
+++ b/application/front/controller/visitor/ErrorController.php
@@ -0,0 +1,45 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Front\Exception\ShaarliFrontException;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Controller used to render the error page, with a provided exception.
13 * It is actually used as a Slim error handler.
14 */
15class ErrorController extends ShaarliVisitorController
16{
17 public function __invoke(Request $request, Response $response, \Throwable $throwable): Response
18 {
19 // Unknown error encountered
20 $this->container->pageBuilder->reset();
21
22 if ($throwable instanceof ShaarliFrontException) {
23 // Functional error
24 $this->assignView('message', nl2br($throwable->getMessage()));
25
26 $response = $response->withStatus($throwable->getCode());
27 } else {
28 // Internal error (any other Throwable)
29 if ($this->container->conf->get('dev.debug', false)) {
30 $this->assignView('message', $throwable->getMessage());
31 $this->assignView(
32 'stacktrace',
33 nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString())
34 );
35 } else {
36 $this->assignView('message', t('An unexpected error occurred.'));
37 }
38
39 $response = $response->withStatus(500);
40 }
41
42
43 return $response->write($this->render('error'));
44 }
45}
diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php
new file mode 100644
index 00000000..da2848c2
--- /dev/null
+++ b/application/front/controller/visitor/FeedController.php
@@ -0,0 +1,58 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Feed\FeedBuilder;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class FeedController
13 *
14 * Slim controller handling ATOM and RSS feed.
15 */
16class FeedController extends ShaarliVisitorController
17{
18 public function atom(Request $request, Response $response): Response
19 {
20 return $this->processRequest(FeedBuilder::$FEED_ATOM, $request, $response);
21 }
22
23 public function rss(Request $request, Response $response): Response
24 {
25 return $this->processRequest(FeedBuilder::$FEED_RSS, $request, $response);
26 }
27
28 protected function processRequest(string $feedType, Request $request, Response $response): Response
29 {
30 $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8');
31
32 $pageUrl = page_url($this->container->environment);
33 $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
34
35 $cached = $cache->cachedVersion();
36 if (!empty($cached)) {
37 return $response->write($cached);
38 }
39
40 // Generate data.
41 $this->container->feedBuilder->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
42 $this->container->feedBuilder->setHideDates($this->container->conf->get('privacy.hide_timestamps', false));
43 $this->container->feedBuilder->setUsePermalinks(
44 null !== $request->getParam('permalinks') || !$this->container->conf->get('feed.rss_permalinks')
45 );
46
47 $data = $this->container->feedBuilder->buildData($feedType, $request->getParams());
48
49 $this->executePageHooks('render_feed', $data, $feedType);
50 $this->assignAllView($data);
51
52 $content = $this->render('feed.'. $feedType);
53
54 $cache->cache($content);
55
56 return $response->write($content);
57 }
58}
diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php
new file mode 100644
index 00000000..7cb32777
--- /dev/null
+++ b/application/front/controller/visitor/InstallController.php
@@ -0,0 +1,165 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\ApplicationUtils;
8use Shaarli\Container\ShaarliContainer;
9use Shaarli\Front\Exception\AlreadyInstalledException;
10use Shaarli\Front\Exception\ResourcePermissionException;
11use Shaarli\Languages;
12use Shaarli\Security\SessionManager;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16/**
17 * Slim controller used to render install page, and create initial configuration file.
18 */
19class InstallController extends ShaarliVisitorController
20{
21 public const SESSION_TEST_KEY = 'session_tested';
22 public const SESSION_TEST_VALUE = 'Working';
23
24 public function __construct(ShaarliContainer $container)
25 {
26 parent::__construct($container);
27
28 if (is_file($this->container->conf->getConfigFileExt())) {
29 throw new AlreadyInstalledException();
30 }
31 }
32
33 /**
34 * Display the install template page.
35 * Also test file permissions and sessions beforehand.
36 */
37 public function index(Request $request, Response $response): Response
38 {
39 // Before installation, we'll make sure that permissions are set properly, and sessions are working.
40 $this->checkPermissions();
41
42 if (static::SESSION_TEST_VALUE
43 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
44 ) {
45 $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE);
46
47 return $this->redirect($response, '/install/session-test');
48 }
49
50 [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
51
52 $this->assignView('continents', $continents);
53 $this->assignView('cities', $cities);
54 $this->assignView('languages', Languages::getAvailableLanguages());
55
56 return $response->write($this->render('install'));
57 }
58
59 /**
60 * Route checking that the session parameter has been properly saved between two distinct requests.
61 * If the session parameter is preserved, redirect to install template page, otherwise displays error.
62 */
63 public function sessionTest(Request $request, Response $response): Response
64 {
65 // This part makes sure sessions works correctly.
66 // (Because on some hosts, session.save_path may not be set correctly,
67 // or we may not have write access to it.)
68 if (static::SESSION_TEST_VALUE
69 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
70 ) {
71 // Step 2: Check if data in session is correct.
72 $msg = t(
73 '<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, '.
75 'and that you have write access to it.<br>'.
76 'It currently points to %s.<br>'.
77 'On some browsers, accessing your server via a hostname like \'localhost\' '.
78 '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>'
80 );
81 $msg = sprintf($msg, $this->container->sessionManager->getSavePath());
82
83 $this->assignView('message', $msg);
84
85 return $response->write($this->render('error'));
86 }
87
88 return $this->redirect($response, '/install');
89 }
90
91 /**
92 * Save installation form and initialize config file and datastore if necessary.
93 */
94 public function save(Request $request, Response $response): Response
95 {
96 $timezone = 'UTC';
97 if (!empty($request->getParam('continent'))
98 && !empty($request->getParam('city'))
99 && isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
100 ) {
101 $timezone = $request->getParam('continent') . '/' . $request->getParam('city');
102 }
103 $this->container->conf->set('general.timezone', $timezone);
104
105 $login = $request->getParam('setlogin');
106 $this->container->conf->set('credentials.login', $login);
107 $salt = sha1(uniqid('', true) .'_'. mt_rand());
108 $this->container->conf->set('credentials.salt', $salt);
109 $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
110
111 if (!empty($request->getParam('title'))) {
112 $this->container->conf->set('general.title', escape($request->getParam('title')));
113 } else {
114 $this->container->conf->set(
115 'general.title',
116 'Shared bookmarks on '.escape(index_url($this->container->environment))
117 );
118 }
119
120 $this->container->conf->set('translation.language', escape($request->getParam('language')));
121 $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
122 $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
123 $this->container->conf->set(
124 'api.secret',
125 generate_api_secret(
126 $this->container->conf->get('credentials.login'),
127 $this->container->conf->get('credentials.salt')
128 )
129 );
130 $this->container->conf->set('general.header_link', $this->container->basePath . '/');
131
132 try {
133 // Everything is ok, let's create config file.
134 $this->container->conf->write($this->container->loginManager->isLoggedIn());
135 } catch (\Exception $e) {
136 $this->assignView('message', t('Error while writing config file after configuration update.'));
137 $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
138
139 return $response->write($this->render('error'));
140 }
141
142 $this->container->sessionManager->setSessionParameter(
143 SessionManager::KEY_SUCCESS_MESSAGES,
144 [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')]
145 );
146
147 return $this->redirect($response, '/login');
148 }
149
150 protected function checkPermissions(): bool
151 {
152 // Ensure Shaarli has proper access to its resources
153 $errors = ApplicationUtils::checkResourcePermissions($this->container->conf);
154 if (empty($errors)) {
155 return true;
156 }
157
158 $message = t('Insufficient permissions:') . PHP_EOL;
159 foreach ($errors as $error) {
160 $message .= PHP_EOL . $error;
161 }
162
163 throw new ResourcePermissionException($message);
164 }
165}
diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php
new file mode 100644
index 00000000..121ba40b
--- /dev/null
+++ b/application/front/controller/visitor/LoginController.php
@@ -0,0 +1,154 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Front\Exception\CantLoginException;
8use Shaarli\Front\Exception\LoginBannedException;
9use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Render\TemplatePage;
11use Shaarli\Security\CookieManager;
12use Shaarli\Security\SessionManager;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16/**
17 * Class LoginController
18 *
19 * Slim controller used to render the login page.
20 *
21 * The login page is not available if the user is banned
22 * or if open shaarli setting is enabled.
23 */
24class LoginController extends ShaarliVisitorController
25{
26 /**
27 * GET /login - Display the login page.
28 */
29 public function index(Request $request, Response $response): Response
30 {
31 try {
32 $this->checkLoginState();
33 } catch (CantLoginException $e) {
34 return $this->redirect($response, '/');
35 }
36
37 if ($request->getParam('login') !== null) {
38 $this->assignView('username', escape($request->getParam('login')));
39 }
40
41 $returnUrl = $request->getParam('returnurl') ?? $this->container->environment['HTTP_REFERER'] ?? null;
42
43 $this
44 ->assignView('returnurl', escape($returnUrl))
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'))
47 ;
48
49 return $response->write($this->render(TemplatePage::LOGIN));
50 }
51
52 /**
53 * POST /login - Process login
54 */
55 public function login(Request $request, Response $response): Response
56 {
57 if (!$this->container->sessionManager->checkToken($request->getParam('token'))) {
58 throw new WrongTokenException();
59 }
60
61 try {
62 $this->checkLoginState();
63 } catch (CantLoginException $e) {
64 return $this->redirect($response, '/');
65 }
66
67 if (!$this->container->loginManager->checkCredentials(
68 $this->container->environment['REMOTE_ADDR'],
69 client_ip_id($this->container->environment),
70 $request->getParam('login'),
71 $request->getParam('password')
72 )
73 ) {
74 $this->container->loginManager->handleFailedLogin($this->container->environment);
75
76 $this->container->sessionManager->setSessionParameter(
77 SessionManager::KEY_ERROR_MESSAGES,
78 [t('Wrong login/password.')]
79 );
80
81 // Call controller directly instead of unnecessary redirection
82 return $this->index($request, $response);
83 }
84
85 $this->container->loginManager->handleSuccessfulLogin($this->container->environment);
86
87 $cookiePath = $this->container->basePath . '/';
88 $expirationTime = $this->saveLongLastingSession($request, $cookiePath);
89 $this->renewUserSession($cookiePath, $expirationTime);
90
91 // Force referer from given return URL
92 $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
93
94 return $this->redirectFromReferer($request, $response, ['login', 'install']);
95 }
96
97 /**
98 * Make sure that the user is allowed to login and/or displaying the login page:
99 * - not already logged in
100 * - not open shaarli
101 * - not banned
102 */
103 protected function checkLoginState(): bool
104 {
105 if ($this->container->loginManager->isLoggedIn()
106 || $this->container->conf->get('security.open_shaarli', false)
107 ) {
108 throw new CantLoginException();
109 }
110
111 if (true !== $this->container->loginManager->canLogin($this->container->environment)) {
112 throw new LoginBannedException();
113 }
114
115 return true;
116 }
117
118 /**
119 * @return int Session duration in seconds
120 */
121 protected function saveLongLastingSession(Request $request, string $cookiePath): int
122 {
123 if (empty($request->getParam('longlastingsession'))) {
124 // Standard session expiration (=when browser closes)
125 $expirationTime = 0;
126 } else {
127 // Keep the session cookie even after the browser closes
128 $this->container->sessionManager->setStaySignedIn(true);
129 $expirationTime = $this->container->sessionManager->extendSession();
130 }
131
132 $this->container->cookieManager->setCookieParameter(
133 CookieManager::STAY_SIGNED_IN,
134 $this->container->loginManager->getStaySignedInToken(),
135 $expirationTime,
136 $cookiePath
137 );
138
139 return $expirationTime;
140 }
141
142 protected function renewUserSession(string $cookiePath, int $expirationTime): void
143 {
144 // Send cookie with the new expiration date to the browser
145 $this->container->sessionManager->destroy();
146 $this->container->sessionManager->cookieParameters(
147 $expirationTime,
148 $cookiePath,
149 $this->container->environment['SERVER_NAME']
150 );
151 $this->container->sessionManager->start();
152 $this->container->sessionManager->regenerateId(true);
153 }
154}
diff --git a/application/front/controller/visitor/OpenSearchController.php b/application/front/controller/visitor/OpenSearchController.php
new file mode 100644
index 00000000..36d60acf
--- /dev/null
+++ b/application/front/controller/visitor/OpenSearchController.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Render\TemplatePage;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class OpenSearchController
13 *
14 * Slim controller used to render open search template.
15 * This allows to add Shaarli as a search engine within the browser.
16 */
17class OpenSearchController extends ShaarliVisitorController
18{
19 public function index(Request $request, Response $response): Response
20 {
21 $response = $response->withHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8');
22
23 $this->assignView('serverurl', index_url($this->container->environment));
24
25 return $response->write($this->render(TemplatePage::OPEN_SEARCH));
26 }
27}
diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php
new file mode 100644
index 00000000..3c57f8dd
--- /dev/null
+++ b/application/front/controller/visitor/PictureWallController.php
@@ -0,0 +1,54 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Front\Exception\ThumbnailsDisabledException;
8use Shaarli\Render\TemplatePage;
9use Shaarli\Thumbnailer;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13/**
14 * Class PicturesWallController
15 *
16 * Slim controller used to render the pictures wall page.
17 * If thumbnails mode is set to NONE, we just render the template without any image.
18 */
19class PictureWallController extends ShaarliVisitorController
20{
21 public function index(Request $request, Response $response): Response
22 {
23 if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
24 throw new ThumbnailsDisabledException();
25 }
26
27 $this->assignView(
28 'pagetitle',
29 t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli')
30 );
31
32 // Optionally filter the results:
33 $links = $this->container->bookmarkService->search($request->getQueryParams());
34 $linksToDisplay = [];
35
36 // Get only bookmarks which have a thumbnail.
37 // Note: we do not retrieve thumbnails here, the request is too heavy.
38 $formatter = $this->container->formatterFactory->getFormatter('raw');
39 foreach ($links as $key => $link) {
40 if (!empty($link->getThumbnail())) {
41 $linksToDisplay[] = $formatter->format($link);
42 }
43 }
44
45 $data = ['linksToDisplay' => $linksToDisplay];
46 $this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL);
47
48 foreach ($data as $key => $value) {
49 $this->assignView($key, $value);
50 }
51
52 return $response->write($this->render(TemplatePage::PICTURE_WALL));
53 }
54}
diff --git a/application/front/controller/visitor/PublicSessionFilterController.php b/application/front/controller/visitor/PublicSessionFilterController.php
new file mode 100644
index 00000000..1a66362d
--- /dev/null
+++ b/application/front/controller/visitor/PublicSessionFilterController.php
@@ -0,0 +1,46 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Security\SessionManager;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Slim controller used to handle filters stored in the visitor session, links per page, etc.
13 */
14class PublicSessionFilterController extends ShaarliVisitorController
15{
16 /**
17 * GET /links-per-page: set the number of bookmarks to display per page in homepage
18 */
19 public function linksPerPage(Request $request, Response $response): Response
20 {
21 $linksPerPage = $request->getParam('nb') ?? null;
22 if (null === $linksPerPage || false === is_numeric($linksPerPage)) {
23 $linksPerPage = $this->container->conf->get('general.links_per_page', 20);
24 }
25
26 $this->container->sessionManager->setSessionParameter(
27 SessionManager::KEY_LINKS_PER_PAGE,
28 abs(intval($linksPerPage))
29 );
30
31 return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']);
32 }
33
34 /**
35 * GET /untagged-only: allows to display only bookmarks without any tag
36 */
37 public function untaggedOnly(Request $request, Response $response): Response
38 {
39 $this->container->sessionManager->setSessionParameter(
40 SessionManager::KEY_UNTAGGED_ONLY,
41 empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY))
42 );
43
44 return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']);
45 }
46}
diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php
new file mode 100644
index 00000000..47057d97
--- /dev/null
+++ b/application/front/controller/visitor/ShaarliVisitorController.php
@@ -0,0 +1,170 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Container\ShaarliContainer;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ShaarliVisitorController
14 *
15 * All controllers accessible by visitors (non logged in users) should extend this abstract class.
16 * Contains a few helper function for template rendering, plugins, etc.
17 *
18 * @package Shaarli\Front\Controller\Visitor
19 */
20abstract class ShaarliVisitorController
21{
22 /** @var ShaarliContainer */
23 protected $container;
24
25 /** @param ShaarliContainer $container Slim container (extended for attribute completion). */
26 public function __construct(ShaarliContainer $container)
27 {
28 $this->container = $container;
29 }
30
31 /**
32 * Assign variables to RainTPL template through the PageBuilder.
33 *
34 * @param mixed $value Value to assign to the template
35 */
36 protected function assignView(string $name, $value): self
37 {
38 $this->container->pageBuilder->assign($name, $value);
39
40 return $this;
41 }
42
43 /**
44 * Assign variables to RainTPL template through the PageBuilder.
45 *
46 * @param mixed $data Values to assign to the template and their keys
47 */
48 protected function assignAllView(array $data): self
49 {
50 foreach ($data as $key => $value) {
51 $this->assignView($key, $value);
52 }
53
54 return $this;
55 }
56
57 protected function render(string $template): string
58 {
59 $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));
60 $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));
61 $this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
62
63 $this->executeDefaultHooks($template);
64
65 return $this->container->pageBuilder->render($template, $this->container->basePath);
66 }
67
68 /**
69 * Call plugin hooks for header, footer and includes, specifying which page will be rendered.
70 * Then assign generated data to RainTPL.
71 */
72 protected function executeDefaultHooks(string $template): void
73 {
74 $common_hooks = [
75 'includes',
76 'header',
77 'footer',
78 ];
79
80 foreach ($common_hooks as $name) {
81 $pluginData = [];
82 $this->container->pluginManager->executeHooks(
83 'render_' . $name,
84 $pluginData,
85 [
86 'target' => $template,
87 'loggedin' => $this->container->loginManager->isLoggedIn(),
88 'basePath' => $this->container->basePath,
89 ]
90 );
91 $this->assignView('plugins_' . $name, $pluginData);
92 }
93 }
94
95 protected function executePageHooks(string $hook, array &$data, string $template = null): void
96 {
97 $params = [
98 'target' => $template,
99 'loggedin' => $this->container->loginManager->isLoggedIn(),
100 'basePath' => $this->container->basePath,
101 ];
102
103 $this->container->pluginManager->executeHooks(
104 $hook,
105 $data,
106 $params
107 );
108 }
109
110 /**
111 * Simple helper which prepend the base path to redirect path.
112 *
113 * @param Response $response
114 * @param string $path Absolute path, e.g.: `/`, or `/admin/shaare/123` regardless of install directory
115 *
116 * @return Response updated
117 */
118 protected function redirect(Response $response, string $path): Response
119 {
120 return $response->withRedirect($this->container->basePath . $path);
121 }
122
123 /**
124 * Generates a redirection to the previous page, based on the HTTP_REFERER.
125 * It fails back to the home page.
126 *
127 * @param array $loopTerms Terms to remove from path and query string to prevent direction loop.
128 * @param array $clearParams List of parameter to remove from the query string of the referrer.
129 */
130 protected function redirectFromReferer(
131 Request $request,
132 Response $response,
133 array $loopTerms = [],
134 array $clearParams = [],
135 string $anchor = null
136 ): Response {
137 $defaultPath = $this->container->basePath . '/';
138 $referer = $this->container->environment['HTTP_REFERER'] ?? null;
139
140 if (null !== $referer) {
141 $currentUrl = parse_url($referer);
142 parse_str($currentUrl['query'] ?? '', $params);
143 $path = $currentUrl['path'] ?? $defaultPath;
144 } else {
145 $params = [];
146 $path = $defaultPath;
147 }
148
149 // Prevent redirection loop
150 if (isset($currentUrl)) {
151 foreach ($clearParams as $value) {
152 unset($params[$value]);
153 }
154
155 $checkQuery = implode('', array_keys($params));
156 foreach ($loopTerms as $value) {
157 if (strpos($path . $checkQuery, $value) !== false) {
158 $params = [];
159 $path = $defaultPath;
160 break;
161 }
162 }
163 }
164
165 $queryString = count($params) > 0 ? '?'. http_build_query($params) : '';
166 $anchor = $anchor ? '#' . $anchor : '';
167
168 return $response->withRedirect($path . $queryString . $anchor);
169 }
170}
diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php
new file mode 100644
index 00000000..f9c529bc
--- /dev/null
+++ b/application/front/controller/visitor/TagCloudController.php
@@ -0,0 +1,113 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Class TagCloud
12 *
13 * Slim controller used to render the tag cloud and tag list pages.
14 */
15class TagCloudController extends ShaarliVisitorController
16{
17 protected const TYPE_CLOUD = 'cloud';
18 protected const TYPE_LIST = 'list';
19
20 /**
21 * Display the tag cloud through the template engine.
22 * This controller a few filters:
23 * - Visibility stored in the session for logged in users
24 * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
25 */
26 public function cloud(Request $request, Response $response): Response
27 {
28 return $this->processRequest(static::TYPE_CLOUD, $request, $response);
29 }
30
31 /**
32 * Display the tag list through the template engine.
33 * This controller a few filters:
34 * - Visibility stored in the session for logged in users
35 * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
36 * - `sort` query parameters:
37 * + `usage` (default): most used tags first
38 * + `alpha`: alphabetical order
39 */
40 public function list(Request $request, Response $response): Response
41 {
42 return $this->processRequest(static::TYPE_LIST, $request, $response);
43 }
44
45 /**
46 * Process the request for both tag cloud and tag list endpoints.
47 */
48 protected function processRequest(string $type, Request $request, Response $response): Response
49 {
50 if ($this->container->loginManager->isLoggedIn() === true) {
51 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
52 }
53
54 $sort = $request->getQueryParam('sort');
55 $searchTags = $request->getQueryParam('searchtags');
56 $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : [];
57
58 $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
59
60 if (static::TYPE_CLOUD === $type || 'alpha' === $sort) {
61 // TODO: the sorting should be handled by bookmarkService instead of the controller
62 alphabetical_sort($tags, false, true);
63 }
64
65 if (static::TYPE_CLOUD === $type) {
66 $tags = $this->formatTagsForCloud($tags);
67 }
68
69 $searchTags = implode(' ', escape($filteringTags));
70 $data = [
71 'search_tags' => $searchTags,
72 'tags' => $tags,
73 ];
74 $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
75 $this->assignAllView($data);
76
77 $searchTags = !empty($searchTags) ? $searchTags .' - ' : '';
78 $this->assignView(
79 'pagetitle',
80 $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli')
81 );
82
83 return $response->write($this->render('tag.' . $type));
84 }
85
86 /**
87 * Format the tags array for the tag cloud template.
88 *
89 * @param array<string, int> $tags List of tags as key with count as value
90 *
91 * @return mixed[] List of tags as key, with count and expected font size in a subarray
92 */
93 protected function formatTagsForCloud(array $tags): array
94 {
95 // We sort tags alphabetically, then choose a font size according to count.
96 // First, find max value.
97 $maxCount = count($tags) > 0 ? max($tags) : 0;
98 $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1;
99 $tagList = [];
100 foreach ($tags as $key => $value) {
101 // Tag font size scaling:
102 // default 15 and 30 logarithm bases affect scaling,
103 // 2.2 and 0.8 are arbitrary font sizes in em.
104 $size = log($value, 15) / $logMaxCount * 2.2 + 0.8;
105 $tagList[$key] = [
106 'count' => $value,
107 'size' => number_format($size, 2, '.', ''),
108 ];
109 }
110
111 return $tagList;
112 }
113}
diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php
new file mode 100644
index 00000000..de4e7ea2
--- /dev/null
+++ b/application/front/controller/visitor/TagController.php
@@ -0,0 +1,118 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Class TagController
12 *
13 * Slim controller handle tags.
14 */
15class TagController extends ShaarliVisitorController
16{
17 /**
18 * Add another tag in the current search through an HTTP redirection.
19 *
20 * @param array $args Should contain `newTag` key as tag to add to current search
21 */
22 public function addTag(Request $request, Response $response, array $args): Response
23 {
24 $newTag = $args['newTag'] ?? null;
25 $referer = $this->container->environment['HTTP_REFERER'] ?? null;
26
27 // In case browser does not send HTTP_REFERER, we search a single tag
28 if (null === $referer) {
29 if (null !== $newTag) {
30 return $this->redirect($response, '/?searchtags='. urlencode($newTag));
31 }
32
33 return $this->redirect($response, '/');
34 }
35
36 $currentUrl = parse_url($referer);
37 parse_str($currentUrl['query'] ?? '', $params);
38
39 if (null === $newTag) {
40 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
41 }
42
43 // Prevent redirection loop
44 if (isset($params['addtag'])) {
45 unset($params['addtag']);
46 }
47
48 // 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 $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : [];
51
52 $addtag = true;
53 foreach ($currentTags as $value) {
54 if ($value === $newTag) {
55 $addtag = false;
56 break;
57 }
58 }
59
60 // Append the tag if necessary
61 if (true === $addtag) {
62 $currentTags[] = trim($newTag);
63 }
64
65 $params['searchtags'] = trim(implode(' ', $currentTags));
66
67 // We also remove page (keeping the same page has no sense, since the results are different)
68 unset($params['page']);
69
70 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
71 }
72
73 /**
74 * Remove a tag from the current search through an HTTP redirection.
75 *
76 * @param array $args Should contain `tag` key as tag to remove from current search
77 */
78 public function removeTag(Request $request, Response $response, array $args): Response
79 {
80 $referer = $this->container->environment['HTTP_REFERER'] ?? null;
81
82 // If the referrer is not provided, we can update the search, so we failback on the bookmark list
83 if (empty($referer)) {
84 return $this->redirect($response, '/');
85 }
86
87 $tagToRemove = $args['tag'] ?? null;
88 $currentUrl = parse_url($referer);
89 parse_str($currentUrl['query'] ?? '', $params);
90
91 if (null === $tagToRemove) {
92 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
93 }
94
95 // Prevent redirection loop
96 if (isset($params['removetag'])) {
97 unset($params['removetag']);
98 }
99
100 if (isset($params['searchtags'])) {
101 $tags = explode(' ', $params['searchtags']);
102 // Remove value from array $tags.
103 $tags = array_diff($tags, [$tagToRemove]);
104 $params['searchtags'] = implode(' ', $tags);
105
106 if (empty($params['searchtags'])) {
107 unset($params['searchtags']);
108 }
109
110 // We also remove page (keeping the same page has no sense, since the results are different)
111 unset($params['page']);
112 }
113
114 $queryParams = count($params) > 0 ? '?' . http_build_query($params) : '';
115
116 return $response->withRedirect(($currentUrl['path'] ?? './') . $queryParams);
117 }
118}
diff --git a/application/front/controllers/LoginController.php b/application/front/controllers/LoginController.php
deleted file mode 100644
index ae3599e0..00000000
--- a/application/front/controllers/LoginController.php
+++ /dev/null
@@ -1,48 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use Shaarli\Front\Exception\LoginBannedException;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class LoginController
13 *
14 * Slim controller used to render the login page.
15 *
16 * The login page is not available if the user is banned
17 * or if open shaarli setting is enabled.
18 *
19 * @package Front\Controller
20 */
21class LoginController extends ShaarliController
22{
23 public function index(Request $request, Response $response): Response
24 {
25 if ($this->container->loginManager->isLoggedIn()
26 || $this->container->conf->get('security.open_shaarli', false)
27 ) {
28 return $response->withRedirect('./');
29 }
30
31 $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams());
32 if ($userCanLogin !== true) {
33 throw new LoginBannedException();
34 }
35
36 if ($request->getParam('username') !== null) {
37 $this->assignView('username', escape($request->getParam('username')));
38 }
39
40 $this
41 ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER')))
42 ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
43 ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
44 ;
45
46 return $response->write($this->render('loginform'));
47 }
48}
diff --git a/application/front/controllers/ShaarliController.php b/application/front/controllers/ShaarliController.php
deleted file mode 100644
index 2b828588..00000000
--- a/application/front/controllers/ShaarliController.php
+++ /dev/null
@@ -1,69 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Container\ShaarliContainer;
9
10abstract class ShaarliController
11{
12 /** @var ShaarliContainer */
13 protected $container;
14
15 /** @param ShaarliContainer $container Slim container (extended for attribute completion). */
16 public function __construct(ShaarliContainer $container)
17 {
18 $this->container = $container;
19 }
20
21 /**
22 * Assign variables to RainTPL template through the PageBuilder.
23 *
24 * @param mixed $value Value to assign to the template
25 */
26 protected function assignView(string $name, $value): self
27 {
28 $this->container->pageBuilder->assign($name, $value);
29
30 return $this;
31 }
32
33 protected function render(string $template): string
34 {
35 $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));
36 $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));
37 $this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
38
39 $this->executeDefaultHooks($template);
40
41 return $this->container->pageBuilder->render($template);
42 }
43
44 /**
45 * Call plugin hooks for header, footer and includes, specifying which page will be rendered.
46 * Then assign generated data to RainTPL.
47 */
48 protected function executeDefaultHooks(string $template): void
49 {
50 $common_hooks = [
51 'includes',
52 'header',
53 'footer',
54 ];
55
56 foreach ($common_hooks as $name) {
57 $plugin_data = [];
58 $this->container->pluginManager->executeHooks(
59 'render_' . $name,
60 $plugin_data,
61 [
62 'target' => $template,
63 'loggedin' => $this->container->loginManager->isLoggedIn()
64 ]
65 );
66 $this->assignView('plugins_' . $name, $plugin_data);
67 }
68 }
69}
diff --git a/application/front/exceptions/AlreadyInstalledException.php b/application/front/exceptions/AlreadyInstalledException.php
new file mode 100644
index 00000000..4add86cf
--- /dev/null
+++ b/application/front/exceptions/AlreadyInstalledException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class AlreadyInstalledException extends ShaarliFrontException
8{
9 public function __construct()
10 {
11 $message = t('Shaarli has already been installed. Login to edit the configuration.');
12
13 parent::__construct($message, 401);
14 }
15}
diff --git a/application/front/exceptions/CantLoginException.php b/application/front/exceptions/CantLoginException.php
new file mode 100644
index 00000000..cd16635d
--- /dev/null
+++ b/application/front/exceptions/CantLoginException.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class CantLoginException extends \Exception
8{
9
10}
diff --git a/application/front/exceptions/LoginBannedException.php b/application/front/exceptions/LoginBannedException.php
index b31a4a14..79d0ea15 100644
--- a/application/front/exceptions/LoginBannedException.php
+++ b/application/front/exceptions/LoginBannedException.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Front\Exception; 5namespace Shaarli\Front\Exception;
6 6
7class LoginBannedException extends ShaarliException 7class LoginBannedException extends ShaarliFrontException
8{ 8{
9 public function __construct() 9 public function __construct()
10 { 10 {
diff --git a/application/front/exceptions/OpenShaarliPasswordException.php b/application/front/exceptions/OpenShaarliPasswordException.php
new file mode 100644
index 00000000..a6f0b3ae
--- /dev/null
+++ b/application/front/exceptions/OpenShaarliPasswordException.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7/**
8 * Class OpenShaarliPasswordException
9 *
10 * Raised if the user tries to change the admin password on an open shaarli instance.
11 */
12class OpenShaarliPasswordException extends ShaarliFrontException
13{
14 public function __construct()
15 {
16 parent::__construct(t('You are not supposed to change a password on an Open Shaarli.'), 403);
17 }
18}
diff --git a/application/front/exceptions/ResourcePermissionException.php b/application/front/exceptions/ResourcePermissionException.php
new file mode 100644
index 00000000..8fbf03b9
--- /dev/null
+++ b/application/front/exceptions/ResourcePermissionException.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class ResourcePermissionException extends ShaarliFrontException
8{
9 public function __construct(string $message)
10 {
11 parent::__construct($message, 500);
12 }
13}
diff --git a/application/front/exceptions/ShaarliException.php b/application/front/exceptions/ShaarliFrontException.php
index 800bfbec..73847e6d 100644
--- a/application/front/exceptions/ShaarliException.php
+++ b/application/front/exceptions/ShaarliFrontException.php
@@ -9,11 +9,11 @@ use Throwable;
9/** 9/**
10 * Class ShaarliException 10 * Class ShaarliException
11 * 11 *
12 * Abstract exception class used to defined any custom exception thrown during front rendering. 12 * Exception class used to defined any custom exception thrown during front rendering.
13 * 13 *
14 * @package Front\Exception 14 * @package Front\Exception
15 */ 15 */
16abstract class ShaarliException extends \Exception 16class ShaarliFrontException extends \Exception
17{ 17{
18 /** Override parent constructor to force $message and $httpCode parameters to be set. */ 18 /** Override parent constructor to force $message and $httpCode parameters to be set. */
19 public function __construct(string $message, int $httpCode, Throwable $previous = null) 19 public function __construct(string $message, int $httpCode, Throwable $previous = null)
diff --git a/application/front/exceptions/ThumbnailsDisabledException.php b/application/front/exceptions/ThumbnailsDisabledException.php
new file mode 100644
index 00000000..0ed337f5
--- /dev/null
+++ b/application/front/exceptions/ThumbnailsDisabledException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class ThumbnailsDisabledException extends ShaarliFrontException
8{
9 public function __construct()
10 {
11 $message = t('Picture wall unavailable (thumbnails are disabled).');
12
13 parent::__construct($message, 400);
14 }
15}
diff --git a/application/front/exceptions/UnauthorizedException.php b/application/front/exceptions/UnauthorizedException.php
new file mode 100644
index 00000000..4231094a
--- /dev/null
+++ b/application/front/exceptions/UnauthorizedException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7/**
8 * Class UnauthorizedException
9 *
10 * Exception raised if the user tries to access a ShaarliAdminController while logged out.
11 */
12class UnauthorizedException extends \Exception
13{
14
15}
diff --git a/application/front/exceptions/WrongTokenException.php b/application/front/exceptions/WrongTokenException.php
new file mode 100644
index 00000000..42002720
--- /dev/null
+++ b/application/front/exceptions/WrongTokenException.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7/**
8 * Class OpenShaarliPasswordException
9 *
10 * Raised if the user tries to perform an action with an invalid XSRF token.
11 */
12class WrongTokenException extends ShaarliFrontException
13{
14 public function __construct()
15 {
16 parent::__construct(t('Wrong token.'), 403);
17 }
18}
diff --git a/application/http/HttpAccess.php b/application/http/HttpAccess.php
new file mode 100644
index 00000000..81d9e076
--- /dev/null
+++ b/application/http/HttpAccess.php
@@ -0,0 +1,39 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Http;
6
7/**
8 * Class HttpAccess
9 *
10 * This is mostly an OOP wrapper for HTTP functions defined in `HttpUtils`.
11 * It is used as dependency injection in Shaarli's container.
12 *
13 * @package Shaarli\Http
14 */
15class HttpAccess
16{
17 public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
18 {
19 return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction);
20 }
21
22 public function getCurlDownloadCallback(
23 &$charset,
24 &$title,
25 &$description,
26 &$keywords,
27 $retrieveDescription,
28 $curlGetInfo = 'curl_getinfo'
29 ) {
30 return get_curl_download_callback(
31 $charset,
32 $title,
33 $description,
34 $keywords,
35 $retrieveDescription,
36 $curlGetInfo
37 );
38 }
39}
diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php
index 2ea9195d..4fc4e3dc 100644
--- a/application/http/HttpUtils.php
+++ b/application/http/HttpUtils.php
@@ -369,7 +369,7 @@ function server_url($server)
369 */ 369 */
370function index_url($server) 370function index_url($server)
371{ 371{
372 $scriptname = $server['SCRIPT_NAME']; 372 $scriptname = $server['SCRIPT_NAME'] ?? '';
373 if (endsWith($scriptname, 'index.php')) { 373 if (endsWith($scriptname, 'index.php')) {
374 $scriptname = substr($scriptname, 0, -9); 374 $scriptname = substr($scriptname, 0, -9);
375 } 375 }
@@ -377,7 +377,7 @@ function index_url($server)
377} 377}
378 378
379/** 379/**
380 * Returns the absolute URL of the current script, with the query 380 * Returns the absolute URL of the current script, with current route and query
381 * 381 *
382 * If the resource is "index.php", then it is removed (for better-looking URLs) 382 * If the resource is "index.php", then it is removed (for better-looking URLs)
383 * 383 *
@@ -387,10 +387,17 @@ function index_url($server)
387 */ 387 */
388function page_url($server) 388function page_url($server)
389{ 389{
390 $scriptname = $server['SCRIPT_NAME'] ?? '';
391 if (endsWith($scriptname, 'index.php')) {
392 $scriptname = substr($scriptname, 0, -9);
393 }
394
395 $route = ltrim($server['REQUEST_URI'] ?? '', $scriptname);
390 if (! empty($server['QUERY_STRING'])) { 396 if (! empty($server['QUERY_STRING'])) {
391 return index_url($server).'?'.$server['QUERY_STRING']; 397 return index_url($server) . $route . '?' . $server['QUERY_STRING'];
392 } 398 }
393 return index_url($server); 399
400 return index_url($server) . $route;
394} 401}
395 402
396/** 403/**
@@ -477,3 +484,109 @@ function is_https($server)
477 484
478 return ! empty($server['HTTPS']); 485 return ! empty($server['HTTPS']);
479} 486}
487
488/**
489 * Get cURL callback function for CURLOPT_WRITEFUNCTION
490 *
491 * @param string $charset to extract from the downloaded page (reference)
492 * @param string $title to extract from the downloaded page (reference)
493 * @param string $description to extract from the downloaded page (reference)
494 * @param string $keywords to extract from the downloaded page (reference)
495 * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
496 * @param string $curlGetInfo Optionally overrides curl_getinfo function
497 *
498 * @return Closure
499 */
500function get_curl_download_callback(
501 &$charset,
502 &$title,
503 &$description,
504 &$keywords,
505 $retrieveDescription,
506 $curlGetInfo = 'curl_getinfo'
507) {
508 $isRedirected = false;
509 $currentChunk = 0;
510 $foundChunk = null;
511
512 /**
513 * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
514 *
515 * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
516 * Then we extract the title and the charset and stop the download when it's done.
517 *
518 * @param resource $ch cURL resource
519 * @param string $data chunk of data being downloaded
520 *
521 * @return int|bool length of $data or false if we need to stop the download
522 */
523 return function (&$ch, $data) use (
524 $retrieveDescription,
525 $curlGetInfo,
526 &$charset,
527 &$title,
528 &$description,
529 &$keywords,
530 &$isRedirected,
531 &$currentChunk,
532 &$foundChunk
533 ) {
534 $currentChunk++;
535 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
536 if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
537 $isRedirected = true;
538 return strlen($data);
539 }
540 if (!empty($responseCode) && $responseCode !== 200) {
541 return false;
542 }
543 // After a redirection, the content type will keep the previous request value
544 // until it finds the next content-type header.
545 if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
546 $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
547 }
548 if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
549 return false;
550 }
551 if (!empty($contentType) && empty($charset)) {
552 $charset = header_extract_charset($contentType);
553 }
554 if (empty($charset)) {
555 $charset = html_extract_charset($data);
556 }
557 if (empty($title)) {
558 $title = html_extract_title($data);
559 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
560 }
561 if ($retrieveDescription && empty($description)) {
562 $description = html_extract_tag('description', $data);
563 $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
564 }
565 if ($retrieveDescription && empty($keywords)) {
566 $keywords = html_extract_tag('keywords', $data);
567 if (! empty($keywords)) {
568 $foundChunk = $currentChunk;
569 // Keywords use the format tag1, tag2 multiple words, tag
570 // So we format them to match Shaarli's separator and glue multiple words with '-'
571 $keywords = implode(' ', array_map(function($keyword) {
572 return implode('-', preg_split('/\s+/', trim($keyword)));
573 }, explode(',', $keywords)));
574 }
575 }
576
577 // We got everything we want, stop the download.
578 // If we already found either the title, description or keywords,
579 // it's highly unlikely that we'll found the other metas further than
580 // in the same chunk of data or the next one. So we also stop the download after that.
581 if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
582 && (! $retrieveDescription
583 || $foundChunk < $currentChunk
584 || (!empty($title) && !empty($description) && !empty($keywords))
585 )
586 ) {
587 return false;
588 }
589
590 return strlen($data);
591 };
592}
diff --git a/application/legacy/LegacyController.php b/application/legacy/LegacyController.php
new file mode 100644
index 00000000..26465d2c
--- /dev/null
+++ b/application/legacy/LegacyController.php
@@ -0,0 +1,130 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Legacy;
6
7use Shaarli\Feed\FeedBuilder;
8use Shaarli\Front\Controller\Visitor\ShaarliVisitorController;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * We use this to maintain legacy routes, and redirect requests to the corresponding Slim route.
14 * Only public routes, and both `?addlink` and `?post` were kept here.
15 * Other routes will just display the linklist.
16 *
17 * @deprecated
18 */
19class LegacyController extends ShaarliVisitorController
20{
21 /** @var string[] Both `?post` and `?addlink` do not use `?do=` format. */
22 public const LEGACY_GET_ROUTES = [
23 'post',
24 'addlink',
25 ];
26
27 /**
28 * This method will call `$action` method, which will redirect to corresponding Slim route.
29 */
30 public function process(Request $request, Response $response, string $action): Response
31 {
32 if (!method_exists($this, $action)) {
33 throw new UnknowLegacyRouteException();
34 }
35
36 return $this->{$action}($request, $response);
37 }
38
39 /** Legacy route: ?post= */
40 public function post(Request $request, Response $response): Response
41 {
42 $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : '';
43
44 if (!$this->container->loginManager->isLoggedIn()) {
45 return $this->redirect($response, '/login' . $parameters);
46 }
47
48 return $this->redirect($response, '/admin/shaare' . $parameters);
49 }
50
51 /** Legacy route: ?addlink= */
52 protected function addlink(Request $request, Response $response): Response
53 {
54 if (!$this->container->loginManager->isLoggedIn()) {
55 return $this->redirect($response, '/login');
56 }
57
58 return $this->redirect($response, '/admin/add-shaare');
59 }
60
61 /** Legacy route: ?do=login */
62 protected function login(Request $request, Response $response): Response
63 {
64 return $this->redirect($response, '/login');
65 }
66
67 /** Legacy route: ?do=logout */
68 protected function logout(Request $request, Response $response): Response
69 {
70 return $this->redirect($response, '/admin/logout');
71 }
72
73 /** Legacy route: ?do=picwall */
74 protected function picwall(Request $request, Response $response): Response
75 {
76 return $this->redirect($response, '/picture-wall');
77 }
78
79 /** Legacy route: ?do=tagcloud */
80 protected function tagcloud(Request $request, Response $response): Response
81 {
82 return $this->redirect($response, '/tags/cloud');
83 }
84
85 /** Legacy route: ?do=taglist */
86 protected function taglist(Request $request, Response $response): Response
87 {
88 return $this->redirect($response, '/tags/list');
89 }
90
91 /** Legacy route: ?do=daily */
92 protected function daily(Request $request, Response $response): Response
93 {
94 $dayParam = !empty($request->getParam('day')) ? '?day=' . escape($request->getParam('day')) : '';
95
96 return $this->redirect($response, '/daily' . $dayParam);
97 }
98
99 /** Legacy route: ?do=rss */
100 protected function rss(Request $request, Response $response): Response
101 {
102 return $this->feed($request, $response, FeedBuilder::$FEED_RSS);
103 }
104
105 /** Legacy route: ?do=atom */
106 protected function atom(Request $request, Response $response): Response
107 {
108 return $this->feed($request, $response, FeedBuilder::$FEED_ATOM);
109 }
110
111 /** Legacy route: ?do=opensearch */
112 protected function opensearch(Request $request, Response $response): Response
113 {
114 return $this->redirect($response, '/open-search');
115 }
116
117 /** Legacy route: ?do=dailyrss */
118 protected function dailyrss(Request $request, Response $response): Response
119 {
120 return $this->redirect($response, '/daily-rss');
121 }
122
123 /** Legacy route: ?do=feed */
124 protected function feed(Request $request, Response $response, string $feedType): Response
125 {
126 $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : '';
127
128 return $this->redirect($response, '/feed/' . $feedType . $parameters);
129 }
130}
diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php
index 7ccf5e54..7bf76fd4 100644
--- a/application/legacy/LegacyLinkDB.php
+++ b/application/legacy/LegacyLinkDB.php
@@ -9,6 +9,7 @@ use Iterator;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Exceptions\IOException; 10use Shaarli\Exceptions\IOException;
11use Shaarli\FileUtils; 11use Shaarli\FileUtils;
12use Shaarli\Render\PageCacheManager;
12 13
13/** 14/**
14 * Data storage for bookmarks. 15 * Data storage for bookmarks.
@@ -352,7 +353,8 @@ You use the community supported version of the original Shaarli project, by Seba
352 353
353 $this->write(); 354 $this->write();
354 355
355 invalidateCaches($pageCacheDir); 356 $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
357 $pageCacheManager->invalidateCaches();
356 } 358 }
357 359
358 /** 360 /**
diff --git a/application/Router.php b/application/legacy/LegacyRouter.php
index d7187487..cea99154 100644
--- a/application/Router.php
+++ b/application/legacy/LegacyRouter.php
@@ -1,12 +1,15 @@
1<?php 1<?php
2namespace Shaarli; 2
3namespace Shaarli\Legacy;
3 4
4/** 5/**
5 * Class Router 6 * Class Router
6 * 7 *
7 * (only displayable pages here) 8 * (only displayable pages here)
9 *
10 * @deprecated
8 */ 11 */
9class Router 12class LegacyRouter
10{ 13{
11 public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update'; 14 public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
12 15
diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php
index 3a5de79f..0ab3a55b 100644
--- a/application/legacy/LegacyUpdater.php
+++ b/application/legacy/LegacyUpdater.php
@@ -10,9 +10,9 @@ use ReflectionMethod;
10use Shaarli\ApplicationUtils; 10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\Bookmark; 11use Shaarli\Bookmark\Bookmark;
12use Shaarli\Bookmark\BookmarkArray; 12use Shaarli\Bookmark\BookmarkArray;
13use Shaarli\Bookmark\LinkDB;
14use Shaarli\Bookmark\BookmarkFilter; 13use Shaarli\Bookmark\BookmarkFilter;
15use Shaarli\Bookmark\BookmarkIO; 14use Shaarli\Bookmark\BookmarkIO;
15use Shaarli\Bookmark\LinkDB;
16use Shaarli\Config\ConfigJson; 16use Shaarli\Config\ConfigJson;
17use Shaarli\Config\ConfigManager; 17use Shaarli\Config\ConfigManager;
18use Shaarli\Config\ConfigPhp; 18use Shaarli\Config\ConfigPhp;
@@ -534,7 +534,8 @@ class LegacyUpdater
534 534
535 if ($thumbnailsEnabled) { 535 if ($thumbnailsEnabled) {
536 $this->session['warnings'][] = t( 536 $this->session['warnings'][] = t(
537 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.' 537 t('You have enabled or changed thumbnails mode.') .
538 '<a href="./admin/thumbnails">' . t('Please synchronize them.') . '</a>'
538 ); 539 );
539 } 540 }
540 541
diff --git a/application/legacy/UnknowLegacyRouteException.php b/application/legacy/UnknowLegacyRouteException.php
new file mode 100644
index 00000000..ae1518ad
--- /dev/null
+++ b/application/legacy/UnknowLegacyRouteException.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Legacy;
6
7class UnknowLegacyRouteException extends \Exception
8{
9}
diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php
index d64eef7f..b83f16f8 100644
--- a/application/netscape/NetscapeBookmarkUtils.php
+++ b/application/netscape/NetscapeBookmarkUtils.php
@@ -6,6 +6,7 @@ use DateTime;
6use DateTimeZone; 6use DateTimeZone;
7use Exception; 7use Exception;
8use Katzgrau\KLogger\Logger; 8use Katzgrau\KLogger\Logger;
9use Psr\Http\Message\UploadedFileInterface;
9use Psr\Log\LogLevel; 10use Psr\Log\LogLevel;
10use Shaarli\Bookmark\Bookmark; 11use Shaarli\Bookmark\Bookmark;
11use Shaarli\Bookmark\BookmarkServiceInterface; 12use Shaarli\Bookmark\BookmarkServiceInterface;
@@ -16,10 +17,24 @@ use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser;
16 17
17/** 18/**
18 * Utilities to import and export bookmarks using the Netscape format 19 * Utilities to import and export bookmarks using the Netscape format
19 * TODO: Not static, use a container.
20 */ 20 */
21class NetscapeBookmarkUtils 21class NetscapeBookmarkUtils
22{ 22{
23 /** @var BookmarkServiceInterface */
24 protected $bookmarkService;
25
26 /** @var ConfigManager */
27 protected $conf;
28
29 /** @var History */
30 protected $history;
31
32 public function __construct(BookmarkServiceInterface $bookmarkService, ConfigManager $conf, History $history)
33 {
34 $this->bookmarkService = $bookmarkService;
35 $this->conf = $conf;
36 $this->history = $history;
37 }
23 38
24 /** 39 /**
25 * Filters bookmarks and adds Netscape-formatted fields 40 * Filters bookmarks and adds Netscape-formatted fields
@@ -28,18 +43,16 @@ class NetscapeBookmarkUtils
28 * - timestamp link addition date, using the Unix epoch format 43 * - timestamp link addition date, using the Unix epoch format
29 * - taglist comma-separated tag list 44 * - taglist comma-separated tag list
30 * 45 *
31 * @param BookmarkServiceInterface $bookmarkService Link datastore
32 * @param BookmarkFormatter $formatter instance 46 * @param BookmarkFormatter $formatter instance
33 * @param string $selection Which bookmarks to export: (all|private|public) 47 * @param string $selection Which bookmarks to export: (all|private|public)
34 * @param bool $prependNoteUrl Prepend note permalinks with the server's URL 48 * @param bool $prependNoteUrl Prepend note permalinks with the server's URL
35 * @param string $indexUrl Absolute URL of the Shaarli index page 49 * @param string $indexUrl Absolute URL of the Shaarli index page
36 * 50 *
37 * @return array The bookmarks to be exported, with additional fields 51 * @return array The bookmarks to be exported, with additional fields
38 *@throws Exception Invalid export selection
39 * 52 *
53 * @throws Exception Invalid export selection
40 */ 54 */
41 public static function filterAndFormat( 55 public function filterAndFormat(
42 $bookmarkService,
43 $formatter, 56 $formatter,
44 $selection, 57 $selection,
45 $prependNoteUrl, 58 $prependNoteUrl,
@@ -51,11 +64,11 @@ class NetscapeBookmarkUtils
51 } 64 }
52 65
53 $bookmarkLinks = array(); 66 $bookmarkLinks = array();
54 foreach ($bookmarkService->search([], $selection) as $bookmark) { 67 foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
55 $link = $formatter->format($bookmark); 68 $link = $formatter->format($bookmark);
56 $link['taglist'] = implode(',', $bookmark->getTags()); 69 $link['taglist'] = implode(',', $bookmark->getTags());
57 if ($bookmark->isNote() && $prependNoteUrl) { 70 if ($bookmark->isNote() && $prependNoteUrl) {
58 $link['url'] = $indexUrl . $link['url']; 71 $link['url'] = rtrim($indexUrl, '/') . '/' . ltrim($link['url'], '/');
59 } 72 }
60 73
61 $bookmarkLinks[] = $link; 74 $bookmarkLinks[] = $link;
@@ -65,60 +78,22 @@ class NetscapeBookmarkUtils
65 } 78 }
66 79
67 /** 80 /**
68 * Generates an import status summary
69 *
70 * @param string $filename name of the file to import
71 * @param int $filesize size of the file to import
72 * @param int $importCount how many bookmarks were imported
73 * @param int $overwriteCount how many bookmarks were overwritten
74 * @param int $skipCount how many bookmarks were skipped
75 * @param int $duration how many seconds did the import take
76 *
77 * @return string Summary of the bookmark import status
78 */
79 private static function importStatus(
80 $filename,
81 $filesize,
82 $importCount = 0,
83 $overwriteCount = 0,
84 $skipCount = 0,
85 $duration = 0
86 ) {
87 $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
88 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
89 $status .= t('has an unknown file format. Nothing was imported.');
90 } else {
91 $status .= vsprintf(
92 t(
93 'was successfully processed in %d seconds: '
94 . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
95 ),
96 [$duration, $importCount, $overwriteCount, $skipCount]
97 );
98 }
99 return $status;
100 }
101
102 /**
103 * Imports Web bookmarks from an uploaded Netscape bookmark dump 81 * Imports Web bookmarks from an uploaded Netscape bookmark dump
104 * 82 *
105 * @param array $post Server $_POST parameters 83 * @param array $post Server $_POST parameters
106 * @param array $files Server $_FILES parameters 84 * @param UploadedFileInterface $file File in PSR-7 object format
107 * @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance
108 * @param ConfigManager $conf instance
109 * @param History $history History instance
110 * 85 *
111 * @return string Summary of the bookmark import status 86 * @return string Summary of the bookmark import status
112 */ 87 */
113 public static function import($post, $files, $bookmarkService, $conf, $history) 88 public function import($post, UploadedFileInterface $file)
114 { 89 {
115 $start = time(); 90 $start = time();
116 $filename = $files['filetoupload']['name']; 91 $filename = $file->getClientFilename();
117 $filesize = $files['filetoupload']['size']; 92 $filesize = $file->getSize();
118 $data = file_get_contents($files['filetoupload']['tmp_name']); 93 $data = (string) $file->getStream();
119 94
120 if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) { 95 if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) {
121 return self::importStatus($filename, $filesize); 96 return $this->importStatus($filename, $filesize);
122 } 97 }
123 98
124 // Overwrite existing bookmarks? 99 // Overwrite existing bookmarks?
@@ -141,11 +116,11 @@ class NetscapeBookmarkUtils
141 true, // nested tag support 116 true, // nested tag support
142 $defaultTags, // additional user-specified tags 117 $defaultTags, // additional user-specified tags
143 strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy 118 strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy
144 $conf->get('resource.data_dir') // log path, will be overridden 119 $this->conf->get('resource.data_dir') // log path, will be overridden
145 ); 120 );
146 $logger = new Logger( 121 $logger = new Logger(
147 $conf->get('resource.data_dir'), 122 $this->conf->get('resource.data_dir'),
148 !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, 123 !$this->conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
149 [ 124 [
150 'prefix' => 'import.', 125 'prefix' => 'import.',
151 'extension' => 'log', 126 'extension' => 'log',
@@ -171,7 +146,7 @@ class NetscapeBookmarkUtils
171 $private = 0; 146 $private = 0;
172 } 147 }
173 148
174 $link = $bookmarkService->findByUrl($bkm['uri']); 149 $link = $this->bookmarkService->findByUrl($bkm['uri']);
175 $existingLink = $link !== null; 150 $existingLink = $link !== null;
176 if (! $existingLink) { 151 if (! $existingLink) {
177 $link = new Bookmark(); 152 $link = new Bookmark();
@@ -193,20 +168,21 @@ class NetscapeBookmarkUtils
193 } 168 }
194 169
195 $link->setTitle($bkm['title']); 170 $link->setTitle($bkm['title']);
196 $link->setUrl($bkm['uri'], $conf->get('security.allowed_protocols')); 171 $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
197 $link->setDescription($bkm['note']); 172 $link->setDescription($bkm['note']);
198 $link->setPrivate($private); 173 $link->setPrivate($private);
199 $link->setTagsString($bkm['tags']); 174 $link->setTagsString($bkm['tags']);
200 175
201 $bookmarkService->addOrSet($link, false); 176 $this->bookmarkService->addOrSet($link, false);
202 $importCount++; 177 $importCount++;
203 } 178 }
204 179
205 $bookmarkService->save(); 180 $this->bookmarkService->save();
206 $history->importLinks(); 181 $this->history->importLinks();
207 182
208 $duration = time() - $start; 183 $duration = time() - $start;
209 return self::importStatus( 184
185 return $this->importStatus(
210 $filename, 186 $filename,
211 $filesize, 187 $filesize,
212 $importCount, 188 $importCount,
@@ -215,4 +191,39 @@ class NetscapeBookmarkUtils
215 $duration 191 $duration
216 ); 192 );
217 } 193 }
194
195 /**
196 * Generates an import status summary
197 *
198 * @param string $filename name of the file to import
199 * @param int $filesize size of the file to import
200 * @param int $importCount how many bookmarks were imported
201 * @param int $overwriteCount how many bookmarks were overwritten
202 * @param int $skipCount how many bookmarks were skipped
203 * @param int $duration how many seconds did the import take
204 *
205 * @return string Summary of the bookmark import status
206 */
207 protected function importStatus(
208 $filename,
209 $filesize,
210 $importCount = 0,
211 $overwriteCount = 0,
212 $skipCount = 0,
213 $duration = 0
214 ) {
215 $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
216 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
217 $status .= t('has an unknown file format. Nothing was imported.');
218 } else {
219 $status .= vsprintf(
220 t(
221 'was successfully processed in %d seconds: '
222 . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
223 ),
224 [$duration, $importCount, $overwriteCount, $skipCount]
225 );
226 }
227 return $status;
228 }
218} 229}
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php
index f7b24a8e..b3e8b2f8 100644
--- a/application/plugin/PluginManager.php
+++ b/application/plugin/PluginManager.php
@@ -16,7 +16,7 @@ class PluginManager
16 * 16 *
17 * @var array $authorizedPlugins 17 * @var array $authorizedPlugins
18 */ 18 */
19 private $authorizedPlugins; 19 private $authorizedPlugins = [];
20 20
21 /** 21 /**
22 * List of loaded plugins. 22 * List of loaded plugins.
@@ -108,6 +108,10 @@ class PluginManager
108 $data['_LOGGEDIN_'] = $params['loggedin']; 108 $data['_LOGGEDIN_'] = $params['loggedin'];
109 } 109 }
110 110
111 if (isset($params['basePath'])) {
112 $data['_BASE_PATH_'] = $params['basePath'];
113 }
114
111 foreach ($this->loadedPlugins as $plugin) { 115 foreach ($this->loadedPlugins as $plugin) {
112 $hookFunction = $this->buildHookName($hook, $plugin); 116 $hookFunction = $this->buildHookName($hook, $plugin);
113 117
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index f4fefda8..7a716673 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -3,10 +3,12 @@
3namespace Shaarli\Render; 3namespace Shaarli\Render;
4 4
5use Exception; 5use Exception;
6use exceptions\MissingBasePathException;
6use RainTPL; 7use RainTPL;
7use Shaarli\ApplicationUtils; 8use Shaarli\ApplicationUtils;
8use Shaarli\Bookmark\BookmarkServiceInterface; 9use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 10use Shaarli\Config\ConfigManager;
11use Shaarli\Security\SessionManager;
10use Shaarli\Thumbnailer; 12use Shaarli\Thumbnailer;
11 13
12/** 14/**
@@ -69,6 +71,15 @@ class PageBuilder
69 } 71 }
70 72
71 /** 73 /**
74 * Reset current state of template rendering.
75 * Mostly useful for error handling. We remove everything, and display the error template.
76 */
77 public function reset(): void
78 {
79 $this->tpl = false;
80 }
81
82 /**
72 * Initialize all default tpl tags. 83 * Initialize all default tpl tags.
73 */ 84 */
74 private function initialize() 85 private function initialize()
@@ -136,11 +147,6 @@ class PageBuilder
136 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); 147 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
137 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); 148 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
138 149
139 if (!empty($_SESSION['warnings'])) {
140 $this->tpl->assign('global_warnings', $_SESSION['warnings']);
141 unset($_SESSION['warnings']);
142 }
143
144 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); 150 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
145 151
146 // To be removed with a proper theme configuration. 152 // To be removed with a proper theme configuration.
@@ -148,6 +154,34 @@ class PageBuilder
148 } 154 }
149 155
150 /** 156 /**
157 * Affect variable after controller processing.
158 * Used for alert messages.
159 */
160 protected function finalize(string $basePath): void
161 {
162 // TODO: use the SessionManager
163 $messageKeys = [
164 SessionManager::KEY_SUCCESS_MESSAGES,
165 SessionManager::KEY_WARNING_MESSAGES,
166 SessionManager::KEY_ERROR_MESSAGES
167 ];
168 foreach ($messageKeys as $messageKey) {
169 if (!empty($_SESSION[$messageKey])) {
170 $this->tpl->assign('global_' . $messageKey, $_SESSION[$messageKey]);
171 unset($_SESSION[$messageKey]);
172 }
173 }
174
175 $this->assign('base_path', $basePath);
176 $this->assign(
177 'asset_path',
178 $basePath . '/' .
179 rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' .
180 $this->conf->get('resource.theme', 'default')
181 );
182 }
183
184 /**
151 * The following assign() method is basically the same as RainTPL (except lazy loading) 185 * The following assign() method is basically the same as RainTPL (except lazy loading)
152 * 186 *
153 * @param string $placeholder Template placeholder. 187 * @param string $placeholder Template placeholder.
@@ -185,21 +219,6 @@ class PageBuilder
185 } 219 }
186 220
187 /** 221 /**
188 * Render a specific page (using a template file).
189 * e.g. $pb->renderPage('picwall');
190 *
191 * @param string $page Template filename (without extension).
192 */
193 public function renderPage($page)
194 {
195 if ($this->tpl === false) {
196 $this->initialize();
197 }
198
199 $this->tpl->draw($page);
200 }
201
202 /**
203 * Render a specific page as string (using a template file). 222 * Render a specific page as string (using a template file).
204 * e.g. $pb->render('picwall'); 223 * e.g. $pb->render('picwall');
205 * 224 *
@@ -207,28 +226,14 @@ class PageBuilder
207 * 226 *
208 * @return string Processed template content 227 * @return string Processed template content
209 */ 228 */
210 public function render(string $page): string 229 public function render(string $page, string $basePath): string
211 { 230 {
212 if ($this->tpl === false) { 231 if ($this->tpl === false) {
213 $this->initialize(); 232 $this->initialize();
214 } 233 }
215 234
216 return $this->tpl->draw($page, true); 235 $this->finalize($basePath);
217 }
218 236
219 /** 237 return $this->tpl->draw($page, true);
220 * Render a 404 page (uses the template : tpl/404.tpl)
221 * usage: $PAGE->render404('The link was deleted')
222 *
223 * @param string $message A message to display what is not found
224 */
225 public function render404($message = '')
226 {
227 if (empty($message)) {
228 $message = t('The page you are trying to reach does not exist or has been deleted.');
229 }
230 header($_SERVER['SERVER_PROTOCOL'] . ' ' . t('404 Not Found'));
231 $this->tpl->assign('error_message', $message);
232 $this->renderPage('404');
233 } 238 }
234} 239}
diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php
new file mode 100644
index 00000000..97805c35
--- /dev/null
+++ b/application/render/PageCacheManager.php
@@ -0,0 +1,60 @@
1<?php
2
3namespace Shaarli\Render;
4
5use Shaarli\Feed\CachedPage;
6
7/**
8 * Cache utilities
9 */
10class PageCacheManager
11{
12 /** @var string Cache directory */
13 protected $pageCacheDir;
14
15 /** @var bool */
16 protected $isLoggedIn;
17
18 public function __construct(string $pageCacheDir, bool $isLoggedIn)
19 {
20 $this->pageCacheDir = $pageCacheDir;
21 $this->isLoggedIn = $isLoggedIn;
22 }
23
24 /**
25 * Purges all cached pages
26 *
27 * @return string|null an error string if the directory is missing
28 */
29 public function purgeCachedPages(): ?string
30 {
31 if (!is_dir($this->pageCacheDir)) {
32 $error = sprintf(t('Cannot purge %s: no directory'), $this->pageCacheDir);
33 error_log($error);
34
35 return $error;
36 }
37
38 array_map('unlink', glob($this->pageCacheDir . '/*.cache'));
39
40 return null;
41 }
42
43 /**
44 * Invalidates caches when the database is changed or the user logs out.
45 */
46 public function invalidateCaches(): void
47 {
48 // Purge page cache shared by sessions.
49 $this->purgeCachedPages();
50 }
51
52 public function getCachePage(string $pageUrl): CachedPage
53 {
54 return new CachedPage(
55 $this->pageCacheDir,
56 $pageUrl,
57 false === $this->isLoggedIn
58 );
59 }
60}
diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php
new file mode 100644
index 00000000..8af8228a
--- /dev/null
+++ b/application/render/TemplatePage.php
@@ -0,0 +1,33 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Render;
6
7interface TemplatePage
8{
9 public const ERROR_404 = '404';
10 public const ADDLINK = 'addlink';
11 public const CHANGE_PASSWORD = 'changepassword';
12 public const CHANGE_TAG = 'changetag';
13 public const CONFIGURE = 'configure';
14 public const DAILY = 'daily';
15 public const DAILY_RSS = 'dailyrss';
16 public const EDIT_LINK = 'editlink';
17 public const ERROR = 'error';
18 public const EXPORT = 'export';
19 public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
20 public const FEED_ATOM = 'feed.atom';
21 public const FEED_RSS = 'feed.rss';
22 public const IMPORT = 'import';
23 public const INSTALL = 'install';
24 public const LINKLIST = 'linklist';
25 public const LOGIN = 'loginform';
26 public const OPEN_SEARCH = 'opensearch';
27 public const PICTURE_WALL = 'picwall';
28 public const PLUGINS_ADMIN = 'pluginsadmin';
29 public const TAG_CLOUD = 'tag.cloud';
30 public const TAG_LIST = 'tag.list';
31 public const THUMBNAILS = 'thumbnails';
32 public const TOOLS = 'tools';
33}
diff --git a/application/security/CookieManager.php b/application/security/CookieManager.php
new file mode 100644
index 00000000..cde4746e
--- /dev/null
+++ b/application/security/CookieManager.php
@@ -0,0 +1,33 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Security;
6
7class CookieManager
8{
9 /** @var string Name of the cookie set after logging in **/
10 public const STAY_SIGNED_IN = 'shaarli_staySignedIn';
11
12 /** @var mixed $_COOKIE set by reference */
13 protected $cookies;
14
15 public function __construct(array &$cookies)
16 {
17 $this->cookies = $cookies;
18 }
19
20 public function setCookieParameter(string $key, string $value, int $expires, string $path): self
21 {
22 $this->cookies[$key] = $value;
23
24 setcookie($key, $value, $expires, $path);
25
26 return $this;
27 }
28
29 public function getCookieParameter(string $key, string $default = null): ?string
30 {
31 return $this->cookies[$key] ?? $default;
32 }
33}
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
index 39ec9b2e..d74c3118 100644
--- a/application/security/LoginManager.php
+++ b/application/security/LoginManager.php
@@ -9,9 +9,6 @@ use Shaarli\Config\ConfigManager;
9 */ 9 */
10class LoginManager 10class LoginManager
11{ 11{
12 /** @var string Name of the cookie set after logging in **/
13 public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
14
15 /** @var array A reference to the $_GLOBALS array */ 12 /** @var array A reference to the $_GLOBALS array */
16 protected $globals = []; 13 protected $globals = [];
17 14
@@ -32,17 +29,21 @@ class LoginManager
32 29
33 /** @var string User sign-in token depending on remote IP and credentials */ 30 /** @var string User sign-in token depending on remote IP and credentials */
34 protected $staySignedInToken = ''; 31 protected $staySignedInToken = '';
32 /** @var CookieManager */
33 protected $cookieManager;
35 34
36 /** 35 /**
37 * Constructor 36 * Constructor
38 * 37 *
39 * @param ConfigManager $configManager Configuration Manager instance 38 * @param ConfigManager $configManager Configuration Manager instance
40 * @param SessionManager $sessionManager SessionManager instance 39 * @param SessionManager $sessionManager SessionManager instance
40 * @param CookieManager $cookieManager CookieManager instance
41 */ 41 */
42 public function __construct($configManager, $sessionManager) 42 public function __construct($configManager, $sessionManager, $cookieManager)
43 { 43 {
44 $this->configManager = $configManager; 44 $this->configManager = $configManager;
45 $this->sessionManager = $sessionManager; 45 $this->sessionManager = $sessionManager;
46 $this->cookieManager = $cookieManager;
46 $this->banManager = new BanManager( 47 $this->banManager = new BanManager(
47 $this->configManager->get('security.trusted_proxies', []), 48 $this->configManager->get('security.trusted_proxies', []),
48 $this->configManager->get('security.ban_after'), 49 $this->configManager->get('security.ban_after'),
@@ -86,10 +87,9 @@ class LoginManager
86 /** 87 /**
87 * Check user session state and validity (expiration) 88 * Check user session state and validity (expiration)
88 * 89 *
89 * @param array $cookie The $_COOKIE array
90 * @param string $clientIpId Client IP address identifier 90 * @param string $clientIpId Client IP address identifier
91 */ 91 */
92 public function checkLoginState($cookie, $clientIpId) 92 public function checkLoginState($clientIpId)
93 { 93 {
94 if (! $this->configManager->exists('credentials.login')) { 94 if (! $this->configManager->exists('credentials.login')) {
95 // Shaarli is not configured yet 95 // Shaarli is not configured yet
@@ -97,9 +97,7 @@ class LoginManager
97 return; 97 return;
98 } 98 }
99 99
100 if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) 100 if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
101 && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
102 ) {
103 // The user client has a valid stay-signed-in cookie 101 // The user client has a valid stay-signed-in cookie
104 // Session information is updated with the current client information 102 // Session information is updated with the current client information
105 $this->sessionManager->storeLoginInfo($clientIpId); 103 $this->sessionManager->storeLoginInfo($clientIpId);
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
index 994fcbe5..76b0afe8 100644
--- a/application/security/SessionManager.php
+++ b/application/security/SessionManager.php
@@ -8,6 +8,14 @@ use Shaarli\Config\ConfigManager;
8 */ 8 */
9class SessionManager 9class SessionManager
10{ 10{
11 public const KEY_LINKS_PER_PAGE = 'LINKS_PER_PAGE';
12 public const KEY_VISIBILITY = 'visibility';
13 public const KEY_UNTAGGED_ONLY = 'untaggedonly';
14
15 public const KEY_SUCCESS_MESSAGES = 'successes';
16 public const KEY_WARNING_MESSAGES = 'warnings';
17 public const KEY_ERROR_MESSAGES = 'errors';
18
11 /** @var int Session expiration timeout, in seconds */ 19 /** @var int Session expiration timeout, in seconds */
12 public static $SHORT_TIMEOUT = 3600; // 1 hour 20 public static $SHORT_TIMEOUT = 3600; // 1 hour
13 21
@@ -23,16 +31,35 @@ class SessionManager
23 /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ 31 /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
24 protected $staySignedIn = false; 32 protected $staySignedIn = false;
25 33
34 /** @var string */
35 protected $savePath;
36
26 /** 37 /**
27 * Constructor 38 * Constructor
28 * 39 *
29 * @param array $session The $_SESSION array (reference) 40 * @param array $session The $_SESSION array (reference)
30 * @param ConfigManager $conf ConfigManager instance 41 * @param ConfigManager $conf ConfigManager instance
42 * @param string $savePath Session save path returned by builtin function session_save_path()
31 */ 43 */
32 public function __construct(& $session, $conf) 44 public function __construct(&$session, $conf, string $savePath)
33 { 45 {
34 $this->session = &$session; 46 $this->session = &$session;
35 $this->conf = $conf; 47 $this->conf = $conf;
48 $this->savePath = $savePath;
49 }
50
51 /**
52 * Initialize XSRF token and links per page session variables.
53 */
54 public function initialize(): void
55 {
56 if (!isset($this->session['tokens'])) {
57 $this->session['tokens'] = [];
58 }
59
60 if (!isset($this->session['LINKS_PER_PAGE'])) {
61 $this->session['LINKS_PER_PAGE'] = $this->conf->get('general.links_per_page', 20);
62 }
36 } 63 }
37 64
38 /** 65 /**
@@ -202,4 +229,78 @@ class SessionManager
202 { 229 {
203 return $this->session; 230 return $this->session;
204 } 231 }
232
233 /**
234 * @param mixed $default value which will be returned if the $key is undefined
235 *
236 * @return mixed Content stored in session
237 */
238 public function getSessionParameter(string $key, $default = null)
239 {
240 return $this->session[$key] ?? $default;
241 }
242
243 /**
244 * Store a variable in user session.
245 *
246 * @param string $key Session key
247 * @param mixed $value Session value to store
248 *
249 * @return $this
250 */
251 public function setSessionParameter(string $key, $value): self
252 {
253 $this->session[$key] = $value;
254
255 return $this;
256 }
257
258 /**
259 * Store a variable in user session.
260 *
261 * @param string $key Session key
262 *
263 * @return $this
264 */
265 public function deleteSessionParameter(string $key): self
266 {
267 unset($this->session[$key]);
268
269 return $this;
270 }
271
272 public function getSavePath(): string
273 {
274 return $this->savePath;
275 }
276
277 /*
278 * Next public functions wrapping native PHP session API.
279 */
280
281 public function destroy(): bool
282 {
283 $this->session = [];
284
285 return session_destroy();
286 }
287
288 public function start(): bool
289 {
290 if (session_status() === PHP_SESSION_ACTIVE) {
291 $this->destroy();
292 }
293
294 return session_start();
295 }
296
297 public function cookieParameters(int $lifeTime, string $path, string $domain): bool
298 {
299 return session_set_cookie_params($lifeTime, $path, $domain);
300 }
301
302 public function regenerateId(bool $deleteOldSession = false): bool
303 {
304 return session_regenerate_id($deleteOldSession);
305 }
205} 306}
diff --git a/application/updater/Updater.php b/application/updater/Updater.php
index 95654d81..88a7bc7b 100644
--- a/application/updater/Updater.php
+++ b/application/updater/Updater.php
@@ -2,8 +2,8 @@
2 2
3namespace Shaarli\Updater; 3namespace Shaarli\Updater;
4 4
5use Shaarli\Config\ConfigManager;
6use Shaarli\Bookmark\BookmarkServiceInterface; 5use Shaarli\Bookmark\BookmarkServiceInterface;
6use Shaarli\Config\ConfigManager;
7use Shaarli\Updater\Exception\UpdaterException; 7use Shaarli\Updater\Exception\UpdaterException;
8 8
9/** 9/**
@@ -21,7 +21,7 @@ class Updater
21 /** 21 /**
22 * @var BookmarkServiceInterface instance. 22 * @var BookmarkServiceInterface instance.
23 */ 23 */
24 protected $linkServices; 24 protected $bookmarkService;
25 25
26 /** 26 /**
27 * @var ConfigManager $conf Configuration Manager instance. 27 * @var ConfigManager $conf Configuration Manager instance.
@@ -39,6 +39,11 @@ class Updater
39 protected $methods; 39 protected $methods;
40 40
41 /** 41 /**
42 * @var string $basePath Shaarli root directory (from HTTP Request)
43 */
44 protected $basePath = null;
45
46 /**
42 * Object constructor. 47 * Object constructor.
43 * 48 *
44 * @param array $doneUpdates Updates which are already done. 49 * @param array $doneUpdates Updates which are already done.
@@ -49,7 +54,7 @@ class Updater
49 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) 54 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
50 { 55 {
51 $this->doneUpdates = $doneUpdates; 56 $this->doneUpdates = $doneUpdates;
52 $this->linkServices = $linkDB; 57 $this->bookmarkService = $linkDB;
53 $this->conf = $conf; 58 $this->conf = $conf;
54 $this->isLoggedIn = $isLoggedIn; 59 $this->isLoggedIn = $isLoggedIn;
55 60
@@ -62,13 +67,15 @@ class Updater
62 * Run all new updates. 67 * Run all new updates.
63 * Update methods have to start with 'updateMethod' and return true (on success). 68 * Update methods have to start with 'updateMethod' and return true (on success).
64 * 69 *
70 * @param string $basePath Shaarli root directory (from HTTP Request)
71 *
65 * @return array An array containing ran updates. 72 * @return array An array containing ran updates.
66 * 73 *
67 * @throws UpdaterException If something went wrong. 74 * @throws UpdaterException If something went wrong.
68 */ 75 */
69 public function update() 76 public function update(string $basePath = null)
70 { 77 {
71 $updatesRan = array(); 78 $updatesRan = [];
72 79
73 // If the user isn't logged in, exit without updating. 80 // If the user isn't logged in, exit without updating.
74 if ($this->isLoggedIn !== true) { 81 if ($this->isLoggedIn !== true) {
@@ -111,4 +118,62 @@ class Updater
111 { 118 {
112 return $this->doneUpdates; 119 return $this->doneUpdates;
113 } 120 }
121
122 public function readUpdates(string $updatesFilepath): array
123 {
124 return UpdaterUtils::read_updates_file($updatesFilepath);
125 }
126
127 public function writeUpdates(string $updatesFilepath, array $updates): void
128 {
129 UpdaterUtils::write_updates_file($updatesFilepath, $updates);
130 }
131
132 /**
133 * With the Slim routing system, default header link should be `/subfolder/` instead of `?`.
134 * Otherwise you can not go back to the home page.
135 * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`.
136 */
137 public function updateMethodRelativeHomeLink(): bool
138 {
139 if ('?' === trim($this->conf->get('general.header_link'))) {
140 $this->conf->set('general.header_link', $this->basePath . '/', true, true);
141 }
142
143 return true;
144 }
145
146 /**
147 * With the Slim routing system, note bookmarks URL formatted `?abcdef`
148 * should be replaced with `/shaare/abcdef`
149 */
150 public function updateMethodMigrateExistingNotesUrl(): bool
151 {
152 $updated = false;
153
154 foreach ($this->bookmarkService->search() as $bookmark) {
155 if ($bookmark->isNote()
156 && startsWith($bookmark->getUrl(), '?')
157 && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match)
158 ) {
159 $updated = true;
160 $bookmark = $bookmark->setUrl('/shaare/' . $match[1]);
161
162 $this->bookmarkService->set($bookmark, false);
163 }
164 }
165
166 if ($updated) {
167 $this->bookmarkService->save();
168 }
169
170 return true;
171 }
172
173 public function setBasePath(string $basePath): self
174 {
175 $this->basePath = $basePath;
176
177 return $this;
178 }
114} 179}
diff --git a/assets/common/js/thumbnails-update.js b/assets/common/js/thumbnails-update.js
index b66ca3ae..3cd4c2a7 100644
--- a/assets/common/js/thumbnails-update.js
+++ b/assets/common/js/thumbnails-update.js
@@ -10,13 +10,14 @@
10 * It contains a recursive call to retrieve the thumb of the next link when it succeed. 10 * It contains a recursive call to retrieve the thumb of the next link when it succeed.
11 * It also update the progress bar and other visual feedback elements. 11 * It also update the progress bar and other visual feedback elements.
12 * 12 *
13 * @param {string} basePath Shaarli subfolder for XHR requests
13 * @param {array} ids List of LinkID to update 14 * @param {array} ids List of LinkID to update
14 * @param {int} i Current index in ids 15 * @param {int} i Current index in ids
15 * @param {object} elements List of DOM element to avoid retrieving them at each iteration 16 * @param {object} elements List of DOM element to avoid retrieving them at each iteration
16 */ 17 */
17function updateThumb(ids, i, elements) { 18function updateThumb(basePath, ids, i, elements) {
18 const xhr = new XMLHttpRequest(); 19 const xhr = new XMLHttpRequest();
19 xhr.open('POST', '?do=ajax_thumb_update'); 20 xhr.open('PATCH', `${basePath}/admin/shaare/${ids[i]}/update-thumbnail`);
20 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 21 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
21 xhr.responseType = 'json'; 22 xhr.responseType = 'json';
22 xhr.onload = () => { 23 xhr.onload = () => {
@@ -29,17 +30,18 @@ function updateThumb(ids, i, elements) {
29 elements.current.innerHTML = i; 30 elements.current.innerHTML = i;
30 elements.title.innerHTML = response.title; 31 elements.title.innerHTML = response.title;
31 if (response.thumbnail !== false) { 32 if (response.thumbnail !== false) {
32 elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`; 33 elements.thumbnail.innerHTML = `<img src="${basePath}/${response.thumbnail}">`;
33 } 34 }
34 if (i < ids.length) { 35 if (i < ids.length) {
35 updateThumb(ids, i, elements); 36 updateThumb(basePath, ids, i, elements);
36 } 37 }
37 } 38 }
38 }; 39 };
39 xhr.send(`id=${ids[i]}`); 40 xhr.send();
40} 41}
41 42
42(() => { 43(() => {
44 const basePath = document.querySelector('input[name="js_base_path"]').value;
43 const ids = document.getElementsByName('ids')[0].value.split(','); 45 const ids = document.getElementsByName('ids')[0].value.split(',');
44 const elements = { 46 const elements = {
45 progressBar: document.querySelector('.progressbar > div'), 47 progressBar: document.querySelector('.progressbar > div'),
@@ -47,5 +49,5 @@ function updateThumb(ids, i, elements) {
47 thumbnail: document.querySelector('.thumbnail-placeholder'), 49 thumbnail: document.querySelector('.thumbnail-placeholder'),
48 title: document.querySelector('.thumbnail-link-title'), 50 title: document.querySelector('.thumbnail-link-title'),
49 }; 51 };
50 updateThumb(ids, 0, elements); 52 updateThumb(basePath, ids, 0, elements);
51})(); 53})();
diff --git a/assets/default/js/base.js b/assets/default/js/base.js
index d5c29c69..0f29799d 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -25,12 +25,16 @@ function findParent(element, tagName, attributes) {
25/** 25/**
26 * Ajax request to refresh the CSRF token. 26 * Ajax request to refresh the CSRF token.
27 */ 27 */
28function refreshToken() { 28function refreshToken(basePath) {
29 console.log('refresh');
29 const xhr = new XMLHttpRequest(); 30 const xhr = new XMLHttpRequest();
30 xhr.open('GET', '?do=token'); 31 xhr.open('GET', `${basePath}/admin/token`);
31 xhr.onload = () => { 32 xhr.onload = () => {
32 const token = document.getElementById('token'); 33 const elements = document.querySelectorAll('input[name="token"]');
33 token.setAttribute('value', xhr.responseText); 34 [...elements].forEach((element) => {
35 console.log(element);
36 element.setAttribute('value', xhr.responseText);
37 });
34 }; 38 };
35 xhr.send(); 39 xhr.send();
36} 40}
@@ -215,6 +219,8 @@ function init(description) {
215} 219}
216 220
217(() => { 221(() => {
222 const basePath = document.querySelector('input[name="js_base_path"]').value;
223
218 /** 224 /**
219 * Handle responsive menu. 225 * Handle responsive menu.
220 * Source: http://purecss.io/layouts/tucked-menu-vertical/ 226 * Source: http://purecss.io/layouts/tucked-menu-vertical/
@@ -461,7 +467,7 @@ function init(description) {
461 }); 467 });
462 468
463 if (window.confirm(message)) { 469 if (window.confirm(message)) {
464 window.location = `?delete_link&lf_linkdate=${ids.join('+')}&token=${token.value}`; 470 window.location = `${basePath}/admin/shaare/delete?id=${ids.join('+')}&token=${token.value}`;
465 } 471 }
466 }); 472 });
467 } 473 }
@@ -483,7 +489,8 @@ function init(description) {
483 }); 489 });
484 490
485 const ids = links.map(item => item.id); 491 const ids = links.map(item => item.id);
486 window.location = `?change_visibility&token=${token.value}&newVisibility=${visibility}&ids=${ids.join('+')}`; 492 window.location =
493 `${basePath}/admin/shaare/visibility?token=${token.value}&newVisibility=${visibility}&id=${ids.join('+')}`;
487 }); 494 });
488 }); 495 });
489 } 496 }
@@ -546,7 +553,7 @@ function init(description) {
546 const refreshedToken = document.getElementById('token').value; 553 const refreshedToken = document.getElementById('token').value;
547 const fromtag = block.getAttribute('data-tag'); 554 const fromtag = block.getAttribute('data-tag');
548 const xhr = new XMLHttpRequest(); 555 const xhr = new XMLHttpRequest();
549 xhr.open('POST', '?do=changetag'); 556 xhr.open('POST', `${basePath}/admin/tags`);
550 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 557 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
551 xhr.onload = () => { 558 xhr.onload = () => {
552 if (xhr.status !== 200) { 559 if (xhr.status !== 200) {
@@ -558,8 +565,12 @@ function init(description) {
558 input.setAttribute('value', totag); 565 input.setAttribute('value', totag);
559 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; 566 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
560 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag); 567 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
561 block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`); 568 block
562 block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`); 569 .querySelector('a.tag-link')
570 .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
571 block
572 .querySelector('a.rename-tag')
573 .setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
563 574
564 // Refresh awesomplete values 575 // Refresh awesomplete values
565 existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag)); 576 existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag));
@@ -567,7 +578,7 @@ function init(description) {
567 } 578 }
568 }; 579 };
569 xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`); 580 xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
570 refreshToken(); 581 refreshToken(basePath);
571 }); 582 });
572 }); 583 });
573 584
@@ -593,13 +604,13 @@ function init(description) {
593 604
594 if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) { 605 if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
595 const xhr = new XMLHttpRequest(); 606 const xhr = new XMLHttpRequest();
596 xhr.open('POST', '?do=changetag'); 607 xhr.open('POST', `${basePath}/admin/tags`);
597 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 608 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
598 xhr.onload = () => { 609 xhr.onload = () => {
599 block.remove(); 610 block.remove();
600 }; 611 };
601 xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`)); 612 xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
602 refreshToken(); 613 refreshToken(basePath);
603 614
604 existingTags = existingTags.filter(tagItem => tagItem !== tag); 615 existingTags = existingTags.filter(tagItem => tagItem !== tag);
605 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 616 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index 243ab1b2..759dff29 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -490,6 +490,10 @@ body,
490 } 490 }
491} 491}
492 492
493.header-alert-message {
494 text-align: center;
495}
496
493// CONTENT - GENERAL 497// CONTENT - GENERAL
494.container { 498.container {
495 position: relative; 499 position: relative;
diff --git a/composer.json b/composer.json
index 6b670fa2..738d9f58 100644
--- a/composer.json
+++ b/composer.json
@@ -53,7 +53,8 @@
53 "Shaarli\\Feed\\": "application/feed", 53 "Shaarli\\Feed\\": "application/feed",
54 "Shaarli\\Formatter\\": "application/formatter", 54 "Shaarli\\Formatter\\": "application/formatter",
55 "Shaarli\\Front\\": "application/front", 55 "Shaarli\\Front\\": "application/front",
56 "Shaarli\\Front\\Controller\\": "application/front/controllers", 56 "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
57 "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
57 "Shaarli\\Front\\Exception\\": "application/front/exceptions", 58 "Shaarli\\Front\\Exception\\": "application/front/exceptions",
58 "Shaarli\\Http\\": "application/http", 59 "Shaarli\\Http\\": "application/http",
59 "Shaarli\\Legacy\\": "application/legacy", 60 "Shaarli\\Legacy\\": "application/legacy",
diff --git a/composer.lock b/composer.lock
index b3373a32..ae7a9269 100644
--- a/composer.lock
+++ b/composer.lock
@@ -508,16 +508,16 @@
508 }, 508 },
509 { 509 {
510 "name": "psr/log", 510 "name": "psr/log",
511 "version": "1.1.2", 511 "version": "1.1.3",
512 "source": { 512 "source": {
513 "type": "git", 513 "type": "git",
514 "url": "https://github.com/php-fig/log.git", 514 "url": "https://github.com/php-fig/log.git",
515 "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801" 515 "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
516 }, 516 },
517 "dist": { 517 "dist": {
518 "type": "zip", 518 "type": "zip",
519 "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801", 519 "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
520 "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801", 520 "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
521 "shasum": "" 521 "shasum": ""
522 }, 522 },
523 "require": { 523 "require": {
@@ -551,7 +551,7 @@
551 "psr", 551 "psr",
552 "psr-3" 552 "psr-3"
553 ], 553 ],
554 "time": "2019-11-01T11:05:21+00:00" 554 "time": "2020-03-23T09:12:05+00:00"
555 }, 555 },
556 { 556 {
557 "name": "pubsubhubbub/publisher", 557 "name": "pubsubhubbub/publisher",
@@ -936,24 +936,21 @@
936 }, 936 },
937 { 937 {
938 "name": "phpdocumentor/reflection-common", 938 "name": "phpdocumentor/reflection-common",
939 "version": "2.0.0", 939 "version": "2.1.0",
940 "source": { 940 "source": {
941 "type": "git", 941 "type": "git",
942 "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 942 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
943 "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" 943 "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
944 }, 944 },
945 "dist": { 945 "dist": {
946 "type": "zip", 946 "type": "zip",
947 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", 947 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
948 "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", 948 "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
949 "shasum": "" 949 "shasum": ""
950 }, 950 },
951 "require": { 951 "require": {
952 "php": ">=7.1" 952 "php": ">=7.1"
953 }, 953 },
954 "require-dev": {
955 "phpunit/phpunit": "~6"
956 },
957 "type": "library", 954 "type": "library",
958 "extra": { 955 "extra": {
959 "branch-alias": { 956 "branch-alias": {
@@ -984,7 +981,7 @@
984 "reflection", 981 "reflection",
985 "static analysis" 982 "static analysis"
986 ], 983 ],
987 "time": "2018-08-07T13:53:10+00:00" 984 "time": "2020-04-27T09:25:28+00:00"
988 }, 985 },
989 { 986 {
990 "name": "phpdocumentor/reflection-docblock", 987 "name": "phpdocumentor/reflection-docblock",
@@ -1087,24 +1084,24 @@
1087 }, 1084 },
1088 { 1085 {
1089 "name": "phpspec/prophecy", 1086 "name": "phpspec/prophecy",
1090 "version": "1.10.1", 1087 "version": "v1.10.3",
1091 "source": { 1088 "source": {
1092 "type": "git", 1089 "type": "git",
1093 "url": "https://github.com/phpspec/prophecy.git", 1090 "url": "https://github.com/phpspec/prophecy.git",
1094 "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc" 1091 "reference": "451c3cd1418cf640de218914901e51b064abb093"
1095 }, 1092 },
1096 "dist": { 1093 "dist": {
1097 "type": "zip", 1094 "type": "zip",
1098 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc", 1095 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
1099 "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc", 1096 "reference": "451c3cd1418cf640de218914901e51b064abb093",
1100 "shasum": "" 1097 "shasum": ""
1101 }, 1098 },
1102 "require": { 1099 "require": {
1103 "doctrine/instantiator": "^1.0.2", 1100 "doctrine/instantiator": "^1.0.2",
1104 "php": "^5.3|^7.0", 1101 "php": "^5.3|^7.0",
1105 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 1102 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
1106 "sebastian/comparator": "^1.2.3|^2.0|^3.0", 1103 "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
1107 "sebastian/recursion-context": "^1.0|^2.0|^3.0" 1104 "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
1108 }, 1105 },
1109 "require-dev": { 1106 "require-dev": {
1110 "phpspec/phpspec": "^2.5 || ^3.2", 1107 "phpspec/phpspec": "^2.5 || ^3.2",
@@ -1146,7 +1143,7 @@
1146 "spy", 1143 "spy",
1147 "stub" 1144 "stub"
1148 ], 1145 ],
1149 "time": "2019-12-22T21:05:45+00:00" 1146 "time": "2020-03-05T15:02:03+00:00"
1150 }, 1147 },
1151 { 1148 {
1152 "name": "phpunit/php-code-coverage", 1149 "name": "phpunit/php-code-coverage",
@@ -1501,12 +1498,12 @@
1501 "source": { 1498 "source": {
1502 "type": "git", 1499 "type": "git",
1503 "url": "https://github.com/Roave/SecurityAdvisories.git", 1500 "url": "https://github.com/Roave/SecurityAdvisories.git",
1504 "reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389" 1501 "reference": "5a342e2dc0408d026b97ee3176b5b406e54e3766"
1505 }, 1502 },
1506 "dist": { 1503 "dist": {
1507 "type": "zip", 1504 "type": "zip",
1508 "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389", 1505 "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/5a342e2dc0408d026b97ee3176b5b406e54e3766",
1509 "reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389", 1506 "reference": "5a342e2dc0408d026b97ee3176b5b406e54e3766",
1510 "shasum": "" 1507 "shasum": ""
1511 }, 1508 },
1512 "conflict": { 1509 "conflict": {
@@ -1518,11 +1515,17 @@
1518 "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6", 1515 "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
1519 "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99", 1516 "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
1520 "aws/aws-sdk-php": ">=3,<3.2.1", 1517 "aws/aws-sdk-php": ">=3,<3.2.1",
1518 "bagisto/bagisto": "<0.1.5",
1519 "barrelstrength/sprout-base-email": "<3.9",
1520 "bolt/bolt": "<3.6.10",
1521 "brightlocal/phpwhois": "<=4.2.5", 1521 "brightlocal/phpwhois": "<=4.2.5",
1522 "buddypress/buddypress": "<5.1.2",
1522 "bugsnag/bugsnag-laravel": ">=2,<2.0.2", 1523 "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
1523 "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7", 1524 "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7",
1524 "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", 1525 "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
1525 "cartalyst/sentry": "<=2.1.6", 1526 "cartalyst/sentry": "<=2.1.6",
1527 "centreon/centreon": "<18.10.8|>=19,<19.4.5",
1528 "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
1526 "codeigniter/framework": "<=3.0.6", 1529 "codeigniter/framework": "<=3.0.6",
1527 "composer/composer": "<=1-alpha.11", 1530 "composer/composer": "<=1-alpha.11",
1528 "contao-components/mediaelement": ">=2.14.2,<2.21.1", 1531 "contao-components/mediaelement": ">=2.14.2,<2.21.1",
@@ -1540,22 +1543,32 @@
1540 "doctrine/mongodb-odm": ">=1,<1.0.2", 1543 "doctrine/mongodb-odm": ">=1,<1.0.2",
1541 "doctrine/mongodb-odm-bundle": ">=2,<3.0.1", 1544 "doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
1542 "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1", 1545 "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1",
1546 "dolibarr/dolibarr": "<=10.0.6",
1543 "dompdf/dompdf": ">=0.6,<0.6.2", 1547 "dompdf/dompdf": ">=0.6,<0.6.2",
1544 "drupal/core": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1", 1548 "drupal/core": ">=7,<7.69|>=8,<8.7.12|>=8.8,<8.8.4",
1545 "drupal/drupal": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1", 1549 "drupal/drupal": ">=7,<7.69|>=8,<8.7.12|>=8.8,<8.8.4",
1546 "endroid/qr-code-bundle": "<3.4.2", 1550 "endroid/qr-code-bundle": "<3.4.2",
1551 "enshrined/svg-sanitize": "<0.13.1",
1547 "erusev/parsedown": "<1.7.2", 1552 "erusev/parsedown": "<1.7.2",
1548 "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.4", 1553 "ezsystems/demobundle": ">=5.4,<5.4.6.1",
1549 "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.13.1|>=6,<6.7.9.1|>=6.8,<6.13.5.1|>=7,<7.2.4.1|>=7.3,<7.3.2.1", 1554 "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
1550 "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.12.3|>=2011,<2017.12.4.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3", 1555 "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
1556 "ezsystems/ezplatform": ">=1.7,<1.7.9.1|>=1.13,<1.13.5.1|>=2.5,<2.5.4",
1557 "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6",
1558 "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2",
1559 "ezsystems/ezplatform-user": ">=1,<1.0.1",
1560 "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.14.1|>=6,<6.7.9.1|>=6.8,<6.13.6.2|>=7,<7.2.4.1|>=7.3,<7.3.2.1|>=7.5,<7.5.6.2",
1561 "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",
1551 "ezsystems/repository-forms": ">=2.3,<2.3.2.1", 1562 "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
1552 "ezyang/htmlpurifier": "<4.1.1", 1563 "ezyang/htmlpurifier": "<4.1.1",
1553 "firebase/php-jwt": "<2", 1564 "firebase/php-jwt": "<2",
1554 "fooman/tcpdf": "<6.2.22", 1565 "fooman/tcpdf": "<6.2.22",
1555 "fossar/tcpdf-parser": "<6.2.22", 1566 "fossar/tcpdf-parser": "<6.2.22",
1567 "friendsofsymfony/oauth2-php": "<1.3",
1556 "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", 1568 "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
1557 "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", 1569 "friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
1558 "fuel/core": "<1.8.1", 1570 "fuel/core": "<1.8.1",
1571 "getgrav/grav": "<1.7-beta.8",
1559 "gree/jose": "<=2.2", 1572 "gree/jose": "<=2.2",
1560 "gregwar/rst": "<1.0.3", 1573 "gregwar/rst": "<1.0.3",
1561 "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1", 1574 "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
@@ -1563,6 +1576,7 @@
1563 "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", 1576 "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30",
1564 "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29", 1577 "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29",
1565 "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", 1578 "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
1579 "illuminate/view": ">=7,<7.1.2",
1566 "ivankristianto/phpwhois": "<=4.3", 1580 "ivankristianto/phpwhois": "<=4.3",
1567 "james-heinrich/getid3": "<1.9.9", 1581 "james-heinrich/getid3": "<1.9.9",
1568 "joomla/session": "<1.3.1", 1582 "joomla/session": "<1.3.1",
@@ -1570,15 +1584,19 @@
1570 "kazist/phpwhois": "<=4.2.6", 1584 "kazist/phpwhois": "<=4.2.6",
1571 "kreait/firebase-php": ">=3.2,<3.8.1", 1585 "kreait/firebase-php": ">=3.2,<3.8.1",
1572 "la-haute-societe/tcpdf": "<6.2.22", 1586 "la-haute-societe/tcpdf": "<6.2.22",
1573 "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", 1587 "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30|>=7,<7.1.2",
1574 "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", 1588 "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
1575 "league/commonmark": "<0.18.3", 1589 "league/commonmark": "<0.18.3",
1590 "librenms/librenms": "<1.53",
1591 "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
1576 "magento/magento1ce": "<1.9.4.3", 1592 "magento/magento1ce": "<1.9.4.3",
1577 "magento/magento1ee": ">=1,<1.14.4.3", 1593 "magento/magento1ee": ">=1,<1.14.4.3",
1578 "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", 1594 "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
1579 "monolog/monolog": ">=1.8,<1.12", 1595 "monolog/monolog": ">=1.8,<1.12",
1580 "namshi/jose": "<2.2", 1596 "namshi/jose": "<2.2",
1597 "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
1581 "onelogin/php-saml": "<2.10.4", 1598 "onelogin/php-saml": "<2.10.4",
1599 "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
1582 "openid/php-openid": "<2.3", 1600 "openid/php-openid": "<2.3",
1583 "oro/crm": ">=1.7,<1.7.4", 1601 "oro/crm": ">=1.7,<1.7.4",
1584 "oro/platform": ">=1.7,<1.7.4", 1602 "oro/platform": ">=1.7,<1.7.4",
@@ -1587,49 +1605,67 @@
1587 "paragonie/random_compat": "<2", 1605 "paragonie/random_compat": "<2",
1588 "paypal/merchant-sdk-php": "<3.12", 1606 "paypal/merchant-sdk-php": "<3.12",
1589 "pear/archive_tar": "<1.4.4", 1607 "pear/archive_tar": "<1.4.4",
1608 "phpfastcache/phpfastcache": ">=5,<5.0.13",
1590 "phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6", 1609 "phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6",
1591 "phpoffice/phpexcel": "<=1.8.1", 1610 "phpmyadmin/phpmyadmin": "<4.9.2",
1592 "phpoffice/phpspreadsheet": "<=1.5", 1611 "phpoffice/phpexcel": "<1.8.2",
1612 "phpoffice/phpspreadsheet": "<1.8",
1593 "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", 1613 "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
1594 "phpwhois/phpwhois": "<=4.2.5", 1614 "phpwhois/phpwhois": "<=4.2.5",
1595 "phpxmlrpc/extras": "<0.6.1", 1615 "phpxmlrpc/extras": "<0.6.1",
1616 "pimcore/pimcore": "<6.3",
1617 "prestashop/autoupgrade": ">=4,<4.10.1",
1618 "prestashop/gamification": "<2.3.2",
1619 "prestashop/ps_facetedsearch": "<3.4.1",
1620 "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2",
1596 "propel/propel": ">=2-alpha.1,<=2-alpha.7", 1621 "propel/propel": ">=2-alpha.1,<=2-alpha.7",
1597 "propel/propel1": ">=1,<=1.7.1", 1622 "propel/propel1": ">=1,<=1.7.1",
1598 "pusher/pusher-php-server": "<2.2.1", 1623 "pusher/pusher-php-server": "<2.2.1",
1599 "robrichards/xmlseclibs": ">=1,<3.0.4", 1624 "robrichards/xmlseclibs": "<3.0.4",
1600 "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9", 1625 "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
1601 "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", 1626 "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
1602 "sensiolabs/connect": "<4.2.3", 1627 "sensiolabs/connect": "<4.2.3",
1603 "serluck/phpwhois": "<=4.2.6", 1628 "serluck/phpwhois": "<=4.2.6",
1604 "shopware/shopware": "<5.3.7", 1629 "shopware/shopware": "<5.3.7",
1605 "silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11", 1630 "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
1631 "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
1632 "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
1633 "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
1606 "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", 1634 "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
1607 "silverstripe/framework": ">=3,<3.6.7|>=3.7,<3.7.3|>=4,<4.4", 1635 "silverstripe/framework": "<4.4.5|>=4.5,<4.5.2",
1608 "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2", 1636 "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2",
1609 "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", 1637 "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
1610 "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4", 1638 "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
1639 "silverstripe/subsites": ">=2,<2.1.1",
1640 "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1",
1611 "silverstripe/userforms": "<3", 1641 "silverstripe/userforms": "<3",
1612 "simple-updates/phpwhois": "<=1", 1642 "simple-updates/phpwhois": "<=1",
1613 "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4", 1643 "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
1614 "simplesamlphp/simplesamlphp": "<1.17.8", 1644 "simplesamlphp/simplesamlphp": "<1.18.6",
1615 "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", 1645 "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
1646 "simplito/elliptic-php": "<1.0.6",
1616 "slim/slim": "<2.6", 1647 "slim/slim": "<2.6",
1617 "smarty/smarty": "<3.1.33", 1648 "smarty/smarty": "<3.1.33",
1618 "socalnick/scn-social-auth": "<1.15.2", 1649 "socalnick/scn-social-auth": "<1.15.2",
1619 "spoonity/tcpdf": "<6.2.22", 1650 "spoonity/tcpdf": "<6.2.22",
1620 "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", 1651 "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
1652 "ssddanbrown/bookstack": "<0.29.2",
1621 "stormpath/sdk": ">=0,<9.9.99", 1653 "stormpath/sdk": ">=0,<9.9.99",
1622 "studio-42/elfinder": "<2.1.48", 1654 "studio-42/elfinder": "<2.1.49",
1623 "swiftmailer/swiftmailer": ">=4,<5.4.5", 1655 "swiftmailer/swiftmailer": ">=4,<5.4.5",
1624 "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", 1656 "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
1625 "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", 1657 "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",
1626 "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", 1658 "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",
1627 "sylius/sylius": ">=1,<1.1.18|>=1.2,<1.2.17|>=1.3,<1.3.12|>=1.4,<1.4.4", 1659 "sylius/resource-bundle": "<1.3.13|>=1.4,<1.4.6|>=1.5,<1.5.1|>=1.6,<1.6.3",
1660 "sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5",
1661 "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
1662 "symbiote/silverstripe-versionedfiles": "<=2.0.3",
1628 "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1663 "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
1629 "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1664 "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
1665 "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
1630 "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", 1666 "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
1631 "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1667 "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
1632 "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1668 "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
1633 "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1669 "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
1634 "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", 1670 "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
1635 "symfony/mime": ">=4.3,<4.3.8", 1671 "symfony/mime": ">=4.3,<4.3.8",
@@ -1638,14 +1674,14 @@
1638 "symfony/polyfill-php55": ">=1,<1.10", 1674 "symfony/polyfill-php55": ">=1,<1.10",
1639 "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1675 "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
1640 "symfony/routing": ">=2,<2.0.19", 1676 "symfony/routing": ">=2,<2.0.19",
1641 "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1677 "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=4.4,<4.4.7|>=5,<5.0.7",
1642 "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", 1678 "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
1643 "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7", 1679 "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7",
1644 "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", 1680 "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
1645 "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", 1681 "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
1646 "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8", 1682 "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
1647 "symfony/serializer": ">=2,<2.0.11", 1683 "symfony/serializer": ">=2,<2.0.11",
1648 "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1684 "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
1649 "symfony/translation": ">=2,<2.0.17", 1685 "symfony/translation": ">=2,<2.0.17",
1650 "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", 1686 "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
1651 "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", 1687 "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
@@ -1658,14 +1694,17 @@
1658 "titon/framework": ">=0,<9.9.99", 1694 "titon/framework": ">=0,<9.9.99",
1659 "truckersmp/phpwhois": "<=4.3.1", 1695 "truckersmp/phpwhois": "<=4.3.1",
1660 "twig/twig": "<1.38|>=2,<2.7", 1696 "twig/twig": "<1.38|>=2,<2.7",
1661 "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1", 1697 "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.17|>=10,<10.4.2",
1662 "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1", 1698 "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.17|>=10,<10.4.2",
1663 "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", 1699 "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",
1664 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4", 1700 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4",
1665 "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", 1701 "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
1666 "ua-parser/uap-php": "<3.8", 1702 "ua-parser/uap-php": "<3.8",
1703 "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
1704 "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
1667 "wallabag/tcpdf": "<6.2.22", 1705 "wallabag/tcpdf": "<6.2.22",
1668 "willdurand/js-translation-bundle": "<2.1.1", 1706 "willdurand/js-translation-bundle": "<2.1.1",
1707 "yii2mod/yii2-cms": "<1.9.2",
1669 "yiisoft/yii": ">=1.1.14,<1.1.15", 1708 "yiisoft/yii": ">=1.1.14,<1.1.15",
1670 "yiisoft/yii2": "<2.0.15", 1709 "yiisoft/yii2": "<2.0.15",
1671 "yiisoft/yii2-bootstrap": "<2.0.4", 1710 "yiisoft/yii2-bootstrap": "<2.0.4",
@@ -1674,6 +1713,7 @@
1674 "yiisoft/yii2-gii": "<2.0.4", 1713 "yiisoft/yii2-gii": "<2.0.4",
1675 "yiisoft/yii2-jui": "<2.0.4", 1714 "yiisoft/yii2-jui": "<2.0.4",
1676 "yiisoft/yii2-redis": "<2.0.8", 1715 "yiisoft/yii2-redis": "<2.0.8",
1716 "yourls/yourls": "<1.7.4",
1677 "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", 1717 "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
1678 "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", 1718 "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
1679 "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", 1719 "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
@@ -1710,10 +1750,15 @@
1710 "name": "Marco Pivetta", 1750 "name": "Marco Pivetta",
1711 "email": "ocramius@gmail.com", 1751 "email": "ocramius@gmail.com",
1712 "role": "maintainer" 1752 "role": "maintainer"
1753 },
1754 {
1755 "name": "Ilya Tribusean",
1756 "email": "slash3b@gmail.com",
1757 "role": "maintainer"
1713 } 1758 }
1714 ], 1759 ],
1715 "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", 1760 "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
1716 "time": "2020-01-06T19:16:46+00:00" 1761 "time": "2020-05-12T11:18:47+00:00"
1717 }, 1762 },
1718 { 1763 {
1719 "name": "sebastian/code-unit-reverse-lookup", 1764 "name": "sebastian/code-unit-reverse-lookup",
@@ -2326,16 +2371,16 @@
2326 }, 2371 },
2327 { 2372 {
2328 "name": "squizlabs/php_codesniffer", 2373 "name": "squizlabs/php_codesniffer",
2329 "version": "3.5.3", 2374 "version": "3.5.5",
2330 "source": { 2375 "source": {
2331 "type": "git", 2376 "type": "git",
2332 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 2377 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
2333 "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb" 2378 "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6"
2334 }, 2379 },
2335 "dist": { 2380 "dist": {
2336 "type": "zip", 2381 "type": "zip",
2337 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", 2382 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
2338 "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", 2383 "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
2339 "shasum": "" 2384 "shasum": ""
2340 }, 2385 },
2341 "require": { 2386 "require": {
@@ -2373,20 +2418,20 @@
2373 "phpcs", 2418 "phpcs",
2374 "standards" 2419 "standards"
2375 ], 2420 ],
2376 "time": "2019-12-04T04:46:47+00:00" 2421 "time": "2020-04-17T01:09:41+00:00"
2377 }, 2422 },
2378 { 2423 {
2379 "name": "symfony/console", 2424 "name": "symfony/console",
2380 "version": "v4.4.2", 2425 "version": "v4.4.8",
2381 "source": { 2426 "source": {
2382 "type": "git", 2427 "type": "git",
2383 "url": "https://github.com/symfony/console.git", 2428 "url": "https://github.com/symfony/console.git",
2384 "reference": "82437719dab1e6bdd28726af14cb345c2ec816d0" 2429 "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7"
2385 }, 2430 },
2386 "dist": { 2431 "dist": {
2387 "type": "zip", 2432 "type": "zip",
2388 "url": "https://api.github.com/repos/symfony/console/zipball/82437719dab1e6bdd28726af14cb345c2ec816d0", 2433 "url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
2389 "reference": "82437719dab1e6bdd28726af14cb345c2ec816d0", 2434 "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
2390 "shasum": "" 2435 "shasum": ""
2391 }, 2436 },
2392 "require": { 2437 "require": {
@@ -2449,20 +2494,20 @@
2449 ], 2494 ],
2450 "description": "Symfony Console Component", 2495 "description": "Symfony Console Component",
2451 "homepage": "https://symfony.com", 2496 "homepage": "https://symfony.com",
2452 "time": "2019-12-17T10:32:23+00:00" 2497 "time": "2020-03-30T11:41:10+00:00"
2453 }, 2498 },
2454 { 2499 {
2455 "name": "symfony/finder", 2500 "name": "symfony/finder",
2456 "version": "v4.4.2", 2501 "version": "v4.4.8",
2457 "source": { 2502 "source": {
2458 "type": "git", 2503 "type": "git",
2459 "url": "https://github.com/symfony/finder.git", 2504 "url": "https://github.com/symfony/finder.git",
2460 "reference": "ce8743441da64c41e2a667b8eb66070444ed911e" 2505 "reference": "5729f943f9854c5781984ed4907bbb817735776b"
2461 }, 2506 },
2462 "dist": { 2507 "dist": {
2463 "type": "zip", 2508 "type": "zip",
2464 "url": "https://api.github.com/repos/symfony/finder/zipball/ce8743441da64c41e2a667b8eb66070444ed911e", 2509 "url": "https://api.github.com/repos/symfony/finder/zipball/5729f943f9854c5781984ed4907bbb817735776b",
2465 "reference": "ce8743441da64c41e2a667b8eb66070444ed911e", 2510 "reference": "5729f943f9854c5781984ed4907bbb817735776b",
2466 "shasum": "" 2511 "shasum": ""
2467 }, 2512 },
2468 "require": { 2513 "require": {
@@ -2498,20 +2543,20 @@
2498 ], 2543 ],
2499 "description": "Symfony Finder Component", 2544 "description": "Symfony Finder Component",
2500 "homepage": "https://symfony.com", 2545 "homepage": "https://symfony.com",
2501 "time": "2019-11-17T21:56:56+00:00" 2546 "time": "2020-03-27T16:54:36+00:00"
2502 }, 2547 },
2503 { 2548 {
2504 "name": "symfony/polyfill-ctype", 2549 "name": "symfony/polyfill-ctype",
2505 "version": "v1.13.1", 2550 "version": "v1.17.0",
2506 "source": { 2551 "source": {
2507 "type": "git", 2552 "type": "git",
2508 "url": "https://github.com/symfony/polyfill-ctype.git", 2553 "url": "https://github.com/symfony/polyfill-ctype.git",
2509 "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" 2554 "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
2510 }, 2555 },
2511 "dist": { 2556 "dist": {
2512 "type": "zip", 2557 "type": "zip",
2513 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", 2558 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
2514 "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", 2559 "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
2515 "shasum": "" 2560 "shasum": ""
2516 }, 2561 },
2517 "require": { 2562 "require": {
@@ -2523,7 +2568,7 @@
2523 "type": "library", 2568 "type": "library",
2524 "extra": { 2569 "extra": {
2525 "branch-alias": { 2570 "branch-alias": {
2526 "dev-master": "1.13-dev" 2571 "dev-master": "1.17-dev"
2527 } 2572 }
2528 }, 2573 },
2529 "autoload": { 2574 "autoload": {
@@ -2556,20 +2601,20 @@
2556 "polyfill", 2601 "polyfill",
2557 "portable" 2602 "portable"
2558 ], 2603 ],
2559 "time": "2019-11-27T13:56:44+00:00" 2604 "time": "2020-05-12T16:14:59+00:00"
2560 }, 2605 },
2561 { 2606 {
2562 "name": "symfony/polyfill-mbstring", 2607 "name": "symfony/polyfill-mbstring",
2563 "version": "v1.13.1", 2608 "version": "v1.17.0",
2564 "source": { 2609 "source": {
2565 "type": "git", 2610 "type": "git",
2566 "url": "https://github.com/symfony/polyfill-mbstring.git", 2611 "url": "https://github.com/symfony/polyfill-mbstring.git",
2567 "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f" 2612 "reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
2568 }, 2613 },
2569 "dist": { 2614 "dist": {
2570 "type": "zip", 2615 "type": "zip",
2571 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f", 2616 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
2572 "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f", 2617 "reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
2573 "shasum": "" 2618 "shasum": ""
2574 }, 2619 },
2575 "require": { 2620 "require": {
@@ -2581,7 +2626,7 @@
2581 "type": "library", 2626 "type": "library",
2582 "extra": { 2627 "extra": {
2583 "branch-alias": { 2628 "branch-alias": {
2584 "dev-master": "1.13-dev" 2629 "dev-master": "1.17-dev"
2585 } 2630 }
2586 }, 2631 },
2587 "autoload": { 2632 "autoload": {
@@ -2615,20 +2660,20 @@
2615 "portable", 2660 "portable",
2616 "shim" 2661 "shim"
2617 ], 2662 ],
2618 "time": "2019-11-27T14:18:11+00:00" 2663 "time": "2020-05-12T16:47:27+00:00"
2619 }, 2664 },
2620 { 2665 {
2621 "name": "symfony/polyfill-php73", 2666 "name": "symfony/polyfill-php73",
2622 "version": "v1.13.1", 2667 "version": "v1.17.0",
2623 "source": { 2668 "source": {
2624 "type": "git", 2669 "type": "git",
2625 "url": "https://github.com/symfony/polyfill-php73.git", 2670 "url": "https://github.com/symfony/polyfill-php73.git",
2626 "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f" 2671 "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc"
2627 }, 2672 },
2628 "dist": { 2673 "dist": {
2629 "type": "zip", 2674 "type": "zip",
2630 "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/4b0e2222c55a25b4541305a053013d5647d3a25f", 2675 "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc",
2631 "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f", 2676 "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc",
2632 "shasum": "" 2677 "shasum": ""
2633 }, 2678 },
2634 "require": { 2679 "require": {
@@ -2637,7 +2682,7 @@
2637 "type": "library", 2682 "type": "library",
2638 "extra": { 2683 "extra": {
2639 "branch-alias": { 2684 "branch-alias": {
2640 "dev-master": "1.13-dev" 2685 "dev-master": "1.17-dev"
2641 } 2686 }
2642 }, 2687 },
2643 "autoload": { 2688 "autoload": {
@@ -2673,7 +2718,7 @@
2673 "portable", 2718 "portable",
2674 "shim" 2719 "shim"
2675 ], 2720 ],
2676 "time": "2019-11-27T16:25:15+00:00" 2721 "time": "2020-05-12T16:47:27+00:00"
2677 }, 2722 },
2678 { 2723 {
2679 "name": "symfony/service-contracts", 2724 "name": "symfony/service-contracts",
@@ -2815,16 +2860,16 @@
2815 }, 2860 },
2816 { 2861 {
2817 "name": "webmozart/assert", 2862 "name": "webmozart/assert",
2818 "version": "1.6.0", 2863 "version": "1.8.0",
2819 "source": { 2864 "source": {
2820 "type": "git", 2865 "type": "git",
2821 "url": "https://github.com/webmozart/assert.git", 2866 "url": "https://github.com/webmozart/assert.git",
2822 "reference": "573381c0a64f155a0d9a23f4b0c797194805b925" 2867 "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
2823 }, 2868 },
2824 "dist": { 2869 "dist": {
2825 "type": "zip", 2870 "type": "zip",
2826 "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", 2871 "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
2827 "reference": "573381c0a64f155a0d9a23f4b0c797194805b925", 2872 "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
2828 "shasum": "" 2873 "shasum": ""
2829 }, 2874 },
2830 "require": { 2875 "require": {
@@ -2832,7 +2877,7 @@
2832 "symfony/polyfill-ctype": "^1.8" 2877 "symfony/polyfill-ctype": "^1.8"
2833 }, 2878 },
2834 "conflict": { 2879 "conflict": {
2835 "vimeo/psalm": "<3.6.0" 2880 "vimeo/psalm": "<3.9.1"
2836 }, 2881 },
2837 "require-dev": { 2882 "require-dev": {
2838 "phpunit/phpunit": "^4.8.36 || ^7.5.13" 2883 "phpunit/phpunit": "^4.8.36 || ^7.5.13"
@@ -2859,7 +2904,7 @@
2859 "check", 2904 "check",
2860 "validate" 2905 "validate"
2861 ], 2906 ],
2862 "time": "2019-11-24T13:36:37+00:00" 2907 "time": "2020-04-18T12:12:48+00:00"
2863 } 2908 }
2864 ], 2909 ],
2865 "aliases": [], 2910 "aliases": [],
diff --git a/doc/md/Plugin-System.md b/doc/md/Plugin-System.md
index d5b16e2d..f264e873 100644
--- a/doc/md/Plugin-System.md
+++ b/doc/md/Plugin-System.md
@@ -131,7 +131,7 @@ If it's still not working, please [open an issue](https://github.com/shaarli/Sha
131| ------------- |:-------------:| 131| ------------- |:-------------:|
132| [render_header](#render_header) | Allow plugin to add content in page headers. | 132| [render_header](#render_header) | Allow plugin to add content in page headers. |
133| [render_includes](#render_includes) | Allow plugin to include their own CSS files. | 133| [render_includes](#render_includes) | Allow plugin to include their own CSS files. |
134| [render_footer](#render_footer) | Allow plugin to add content in page footer and include their own JS files. | 134| [render_footer](#render_footer) | Allow plugin to add content in page footer and include their own JS files. |
135| [render_linklist](#render_linklist) | It allows to add content at the begining and end of the page, after every link displayed and to alter link data. | 135| [render_linklist](#render_linklist) | It allows to add content at the begining and end of the page, after every link displayed and to alter link data. |
136| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. | 136| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. |
137| [render_tools](#render_tools) | Allow to add content at the end of the page. | 137| [render_tools](#render_tools) | Allow to add content at the end of the page. |
@@ -515,7 +515,7 @@ Otherwise, you can use your own JS as long as this field is send by the form:
515 515
516### Placeholder system 516### Placeholder system
517 517
518In order to make plugins work with every custom themes, you need to add variable placeholder in your templates. 518In order to make plugins work with every custom themes, you need to add variable placeholder in your templates.
519 519
520It's a RainTPL loop like this: 520It's a RainTPL loop like this:
521 521
@@ -537,7 +537,7 @@ At the end of the menu:
537 537
538At the end of file, before clearing floating blocks: 538At the end of file, before clearing floating blocks:
539 539
540 {if="!empty($plugin_errors) && isLoggedIn()"} 540 {if="!empty($plugin_errors) && $is_logged_in"}
541 <ul class="errors"> 541 <ul class="errors">
542 {loop="plugin_errors"} 542 {loop="plugin_errors"}
543 <li>{$value}</li> 543 <li>{$value}</li>
diff --git a/doc/md/RSS-feeds.md b/doc/md/RSS-feeds.md
index d943218e..ecbff09a 100644
--- a/doc/md/RSS-feeds.md
+++ b/doc/md/RSS-feeds.md
@@ -1,14 +1,14 @@
1### Feeds options 1### Feeds options
2 2
3Feeds are available in ATOM with `?do=atom` and RSS with `do=RSS`. 3Feeds are available in ATOM with `/feed/atom` and RSS with `/feed/rss`.
4 4
5Options: 5Options:
6 6
7- You can use `permalinks` in the feed URL to get permalink to Shaares instead of direct link to shaared URL. 7- You can use `permalinks` in the feed URL to get permalink to Shaares instead of direct link to shaared URL.
8 - E.G. `https://my.shaarli.domain/?do=atom&permalinks`. 8 - E.G. `https://my.shaarli.domain/feed/atom?permalinks`.
9- You can use `nb` parameter in the feed URL to specify the number of Shaares you want in a feed (default if not specified: `50`). The keyword `all` is available if you want everything. 9- You can use `nb` parameter in the feed URL to specify the number of Shaares you want in a feed (default if not specified: `50`). The keyword `all` is available if you want everything.
10 - `https://my.shaarli.domain/?do=atom&permalinks&nb=42` 10 - `https://my.shaarli.domain/feed/atom?permalinks&nb=42`
11 - `https://my.shaarli.domain/?do=atom&permalinks&nb=all` 11 - `https://my.shaarli.domain/feed/atom?permalinks&nb=all`
12 12
13### RSS Feeds or Picture Wall for a specific search/tag 13### RSS Feeds or Picture Wall for a specific search/tag
14 14
@@ -21,8 +21,8 @@ For example, if you want to subscribe only to links tagged `photography`:
21- Click on the `RSS Feed` button. 21- Click on the `RSS Feed` button.
22- You are presented with an RSS feed showing only these links. Subscribe to it to receive only updates with this tag. 22- You are presented with an RSS feed showing only these links. Subscribe to it to receive only updates with this tag.
23- The same method **also works for a full-text search** (_Search_ box) **and for the Picture Wall** (want to only see pictures about `nature`?) 23- The same method **also works for a full-text search** (_Search_ box) **and for the Picture Wall** (want to only see pictures about `nature`?)
24- You can also build the URLs manually: 24- You can also build the URLs manually:
25 - `https://my.shaarli.domain/?do=rss&searchtags=nature` 25 - `https://my.shaarli.domain/?do=rss&searchtags=nature`
26 - `https://my.shaarli.domain/links/?do=picwall&searchterm=poney` 26 - `https://my.shaarli.domain/links/picture-wall?searchterm=poney`
27 27
28![](images/rss-filter-1.png) ![](images/rss-filter-2.png) 28![](images/rss-filter-1.png) ![](images/rss-filter-2.png)
diff --git a/doc/md/Translations.md b/doc/md/Translations.md
index 58b92da3..c23ec962 100644
--- a/doc/md/Translations.md
+++ b/doc/md/Translations.md
@@ -32,20 +32,20 @@ Here is a list :
32``` 32```
33http://<replace_domain>/ 33http://<replace_domain>/
34http://<replace_domain>/?nonope 34http://<replace_domain>/?nonope
35http://<replace_domain>/?do=addlink 35http://<replace_domain>/admin/add-shaare
36http://<replace_domain>/?do=changepasswd 36http://<replace_domain>/admin/password
37http://<replace_domain>/?do=changetag 37http://<replace_domain>/admin/tags
38http://<replace_domain>/?do=configure 38http://<replace_domain>/admin/configure
39http://<replace_domain>/?do=tools 39http://<replace_domain>/admin/tools
40http://<replace_domain>/?do=daily 40http://<replace_domain>/daily
41http://<replace_domain>/?post 41http://<replace_domain>/admin/shaare
42http://<replace_domain>/?do=export 42http://<replace_domain>/admin/export
43http://<replace_domain>/?do=import 43http://<replace_domain>/admin/import
44http://<replace_domain>/login 44http://<replace_domain>/login
45http://<replace_domain>/?do=picwall 45http://<replace_domain>/picture-wall
46http://<replace_domain>/?do=pluginadmin 46http://<replace_domain>/admin/plugins
47http://<replace_domain>/?do=tagcloud 47http://<replace_domain>/tags/cloud
48http://<replace_domain>/?do=taglist 48http://<replace_domain>/tags/list
49``` 49```
50 50
51#### Improve existing translation 51#### Improve existing translation
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
index 026d0101..fda0e62f 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -1,24 +1,26 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: Shaarli\n" 3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2019-07-13 10:45+0200\n" 4"POT-Creation-Date: 2020-07-23 17:11+0200\n"
5"PO-Revision-Date: 2019-07-13 10:49+0200\n" 5"PO-Revision-Date: 2020-07-23 17:12+0200\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"
9"MIME-Version: 1.0\n" 9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n" 10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n" 11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.2.1\n" 12"X-Generator: Poedit 2.0.6\n"
13"X-Poedit-Basepath: ../../../..\n" 13"X-Poedit-Basepath: ../../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n" 14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-SourceCharset: UTF-8\n" 15"X-Poedit-SourceCharset: UTF-8\n"
16"X-Poedit-KeywordsList: t:1,2;t\n" 16"X-Poedit-KeywordsList: t:1,2;t\n"
17"X-Poedit-SearchPath-0: .\n" 17"X-Poedit-SearchPath-0: application\n"
18"X-Poedit-SearchPathExcluded-0: node_modules\n" 18"X-Poedit-SearchPath-1: tmp\n"
19"X-Poedit-SearchPathExcluded-1: vendor\n" 19"X-Poedit-SearchPath-2: index.php\n"
20"X-Poedit-SearchPath-3: init.php\n"
21"X-Poedit-SearchPath-4: plugins\n"
20 22
21#: application/ApplicationUtils.php:159 23#: application/ApplicationUtils.php:161
22#, php-format 24#, php-format
23msgid "" 25msgid ""
24"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " 26"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
@@ -29,27 +31,27 @@ msgstr ""
29"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " 31"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
30"connues et devrait être mise à jour au plus tôt." 32"connues et devrait être mise à jour au plus tôt."
31 33
32#: application/ApplicationUtils.php:189 application/ApplicationUtils.php:201 34#: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204
33msgid "directory is not readable" 35msgid "directory is not readable"
34msgstr "le répertoire n'est pas accessible en lecture" 36msgstr "le répertoire n'est pas accessible en lecture"
35 37
36#: application/ApplicationUtils.php:204 38#: application/ApplicationUtils.php:207
37msgid "directory is not writable" 39msgid "directory is not writable"
38msgstr "le répertoire n'est pas accessible en écriture" 40msgstr "le répertoire n'est pas accessible en écriture"
39 41
40#: application/ApplicationUtils.php:222 42#: application/ApplicationUtils.php:225
41msgid "file is not readable" 43msgid "file is not readable"
42msgstr "le fichier n'est pas accessible en lecture" 44msgstr "le fichier n'est pas accessible en lecture"
43 45
44#: application/ApplicationUtils.php:225 46#: application/ApplicationUtils.php:228
45msgid "file is not writable" 47msgid "file is not writable"
46msgstr "le fichier n'est pas accessible en écriture" 48msgstr "le fichier n'est pas accessible en écriture"
47 49
48#: application/History.php:178 50#: application/History.php:179
49msgid "History file isn't readable or writable" 51msgid "History file isn't readable or writable"
50msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" 52msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
51 53
52#: application/History.php:189 54#: application/History.php:190
53msgid "Could not parse history file" 55msgid "Could not parse history file"
54msgstr "Format incorrect pour le fichier d'historique" 56msgstr "Format incorrect pour le fichier d'historique"
55 57
@@ -77,50 +79,61 @@ msgstr ""
77"l'extension php-gd doit être chargée pour utiliser les miniatures. Les " 79"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
78"miniatures sont désormais désactivées. Rechargez la page." 80"miniatures sont désormais désactivées. Rechargez la page."
79 81
80#: application/Utils.php:379 tests/UtilsTest.php:343 82#: application/Utils.php:383
81msgid "Setting not set" 83msgid "Setting not set"
82msgstr "Paramètre non défini" 84msgstr "Paramètre non défini"
83 85
84#: application/Utils.php:386 tests/UtilsTest.php:341 tests/UtilsTest.php:342 86#: application/Utils.php:390
85msgid "Unlimited" 87msgid "Unlimited"
86msgstr "Illimité" 88msgstr "Illimité"
87 89
88#: application/Utils.php:389 tests/UtilsTest.php:338 tests/UtilsTest.php:339 90#: application/Utils.php:393
89#: tests/UtilsTest.php:353
90msgid "B" 91msgid "B"
91msgstr "o" 92msgstr "o"
92 93
93#: application/Utils.php:389 tests/UtilsTest.php:332 tests/UtilsTest.php:333 94#: application/Utils.php:393
94#: tests/UtilsTest.php:340
95msgid "kiB" 95msgid "kiB"
96msgstr "ko" 96msgstr "ko"
97 97
98#: application/Utils.php:389 tests/UtilsTest.php:334 tests/UtilsTest.php:335 98#: application/Utils.php:393
99#: tests/UtilsTest.php:351 tests/UtilsTest.php:352
100msgid "MiB" 99msgid "MiB"
101msgstr "Mo" 100msgstr "Mo"
102 101
103#: application/Utils.php:389 tests/UtilsTest.php:336 tests/UtilsTest.php:337 102#: application/Utils.php:393
104msgid "GiB" 103msgid "GiB"
105msgstr "Go" 104msgstr "Go"
106 105
107#: application/bookmark/LinkDB.php:128 106#: application/bookmark/BookmarkFileService.php:161
108msgid "You are not authorized to add a link." 107#: application/bookmark/BookmarkFileService.php:186
109msgstr "Vous n'êtes pas autorisé à ajouter un lien." 108#: application/bookmark/BookmarkFileService.php:211
110 109#: application/bookmark/BookmarkFileService.php:228
111#: application/bookmark/LinkDB.php:131 110msgid "You're not authorized to alter the datastore"
112msgid "Internal Error: A link should always have an id and URL." 111msgstr "Vous n'êtes pas autorisé à modifier les données"
113msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL." 112
114 113#: application/bookmark/BookmarkFileService.php:164
115#: application/bookmark/LinkDB.php:134 114#: application/bookmark/BookmarkFileService.php:189
116msgid "You must specify an integer as a key." 115#: application/bookmark/BookmarkFileService.php:231
117msgstr "Vous devez utiliser un entier comme clé." 116msgid "Provided data is invalid"
117msgstr "Les informations fournies ne sont pas valides"
118
119#: application/bookmark/BookmarkFileService.php:192
120msgid "This bookmarks already exists"
121msgstr "Ce marque-page existe déjà."
122
123#: application/bookmark/BookmarkInitializer.php:40
124#: application/legacy/LegacyLinkDB.php:266
125msgid "My secret stuff... - Pastebin.com"
126msgstr "Mes trucs secrets... - Pastebin.com"
118 127
119#: application/bookmark/LinkDB.php:137 128#: application/bookmark/BookmarkInitializer.php:42
120msgid "Array offset and link ID must be equal." 129#: application/legacy/LegacyLinkDB.php:268
121msgstr "La clé du tableau et l'ID du lien doivent être identiques." 130msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
131msgstr ""
132"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
133"supprimer aussi."
122 134
123#: application/bookmark/LinkDB.php:243 135#: application/bookmark/BookmarkInitializer.php:48
136#: application/legacy/LegacyLinkDB.php:246
124#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 137#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
125#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 138#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
126#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 139#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
@@ -131,7 +144,8 @@ msgstr ""
131"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " 144"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
132"données" 145"données"
133 146
134#: application/bookmark/LinkDB.php:246 147#: application/bookmark/BookmarkInitializer.php:51
148#: application/legacy/LegacyLinkDB.php:249
135msgid "" 149msgid ""
136"Welcome to Shaarli! This is your first public bookmark. To edit or delete " 150"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
137"me, you must first login.\n" 151"me, you must first login.\n"
@@ -151,17 +165,7 @@ msgstr ""
151"Vous utilisez la version supportée par la communauté du projet original " 165"Vous utilisez la version supportée par la communauté du projet original "
152"Shaarli de Sébastien Sauvage." 166"Shaarli de Sébastien Sauvage."
153 167
154#: application/bookmark/LinkDB.php:263 168#: application/bookmark/exception/BookmarkNotFoundException.php:13
155msgid "My secret stuff... - Pastebin.com"
156msgstr "Mes trucs secrets... - Pastebin.com"
157
158#: application/bookmark/LinkDB.php:265
159msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
160msgstr ""
161"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
162"supprimer aussi."
163
164#: application/bookmark/exception/LinkNotFoundException.php:13
165msgid "The link you are trying to reach does not exist or has been deleted." 169msgid "The link you are trying to reach does not exist or has been deleted."
166msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." 170msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
167 171
@@ -173,8 +177,8 @@ msgstr ""
173"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " 177"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
174"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." 178"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
175 179
176#: application/config/ConfigManager.php:135 180#: application/config/ConfigManager.php:136
177#: application/config/ConfigManager.php:162 181#: application/config/ConfigManager.php:163
178msgid "Invalid setting key parameter. String expected, got: " 182msgid "Invalid setting key parameter. String expected, got: "
179msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " 183msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
180 184
@@ -196,268 +200,342 @@ msgstr "Vous n'êtes pas autorisé à modifier la configuration."
196msgid "Error accessing" 200msgid "Error accessing"
197msgstr "Une erreur s'est produite en accédant à" 201msgstr "Une erreur s'est produite en accédant à"
198 202
199#: application/feed/Cache.php:16 203#: application/feed/FeedBuilder.php:179
200#, php-format
201msgid "Cannot purge %s: no directory"
202msgstr "Impossible de purger %s : le répertoire n'existe pas"
203
204#: application/feed/FeedBuilder.php:155
205msgid "Direct link" 204msgid "Direct link"
206msgstr "Liens directs" 205msgstr "Liens directs"
207 206
208#: application/feed/FeedBuilder.php:157 207#: application/feed/FeedBuilder.php:181
209#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 208#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
210#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177 209#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
211msgid "Permalink" 210msgid "Permalink"
212msgstr "Permalien" 211msgstr "Permalien"
213 212
214#: application/netscape/NetscapeBookmarkUtils.php:42 213#: application/front/ShaarliMiddleware.php:78
215msgid "Invalid export selection:" 214msgid "An unexpected error occurred."
216msgstr "Sélection d'export invalide :" 215msgstr "Une erreur inattendue s'est produite."
217 216
218#: application/netscape/NetscapeBookmarkUtils.php:87 217#: application/front/controller/admin/ConfigureController.php:54
219#, php-format 218#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
220msgid "File %s (%d bytes) " 219msgid "Configure"
221msgstr "Le fichier %s (%d octets) " 220msgstr "Configurer"
222 221
223#: application/netscape/NetscapeBookmarkUtils.php:89 222#: application/front/controller/admin/ConfigureController.php:102
224msgid "has an unknown file format. Nothing was imported." 223#: application/legacy/LegacyUpdater.php:537
225msgstr "a un format inconnu. Rien n'a été importé." 224msgid "You have enabled or changed thumbnails mode."
225msgstr "Vous avez activé ou changé le mode de miniatures."
226
227#: application/front/controller/admin/ConfigureController.php:103
228#: application/legacy/LegacyUpdater.php:538
229msgid "Please synchronize them."
230msgstr "Merci de les synchroniser."
231
232#: application/front/controller/admin/ConfigureController.php:113
233#: application/front/controller/visitor/InstallController.php:137
234msgid "Error while writing config file after configuration update."
235msgstr ""
236"Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
237
238#: application/front/controller/admin/ConfigureController.php:122
239msgid "Configuration was saved."
240msgstr "La configuration a été sauvegardée."
241
242#: application/front/controller/admin/ExportController.php:26
243#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
244msgid "Export"
245msgstr "Exporter"
246
247#: application/front/controller/admin/ExportController.php:42
248msgid "Please select an export mode."
249msgstr "Merci de choisir un mode d'export."
226 250
227#: application/netscape/NetscapeBookmarkUtils.php:93 251#: application/front/controller/admin/ImportController.php:41
252#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
253msgid "Import"
254msgstr "Importer"
255
256#: application/front/controller/admin/ImportController.php:55
257msgid "No import file provided."
258msgstr "Aucun fichier à importer n'a été fourni."
259
260#: application/front/controller/admin/ImportController.php:66
228#, php-format 261#, php-format
229msgid "" 262msgid ""
230"was successfully processed in %d seconds: %d links imported, %d links " 263"The file you are trying to upload is probably bigger than what this "
231"overwritten, %d links skipped." 264"webserver can accept (%s). Please upload in smaller chunks."
232msgstr "" 265msgstr ""
233"a été importé avec succès en %d secondes : %d liens importés, %d liens " 266"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
234"écrasés, %d liens ignorés." 267"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
268"légères."
235 269
236#: application/plugin/exception/PluginFileNotFoundException.php:21 270#: application/front/controller/admin/ManageShaareController.php:29
271#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
272msgid "Shaare a new link"
273msgstr "Partager un nouveau lien"
274
275#: application/front/controller/admin/ManageShaareController.php:78
276msgid "Note: "
277msgstr "Note : "
278
279#: application/front/controller/admin/ManageShaareController.php:109
280#: application/front/controller/admin/ManageShaareController.php:206
281#: application/front/controller/admin/ManageShaareController.php:275
282#: application/front/controller/admin/ManageShaareController.php:315
237#, php-format 283#, php-format
238msgid "Plugin \"%s\" files not found." 284msgid "Bookmark with identifier %s could not be found."
239msgstr "Les fichiers de l'extension \"%s\" sont introuvables." 285msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
240 286
241#: application/render/PageBuilder.php:209 287#: application/front/controller/admin/ManageShaareController.php:194
242msgid "The page you are trying to reach does not exist or has been deleted." 288#: application/front/controller/admin/ManageShaareController.php:252
243msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée." 289msgid "Invalid bookmark ID provided."
290msgstr "ID du lien non valide."
244 291
245#: application/render/PageBuilder.php:211 292#: application/front/controller/admin/ManageShaareController.php:260
246msgid "404 Not Found" 293msgid "Invalid visibility provided."
247msgstr "404 Introuvable" 294msgstr "Visibilité du lien non valide."
248 295
249#: application/updater/Updater.php:99 296#: application/front/controller/admin/ManageShaareController.php:363
250#, fuzzy 297#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
251#| msgid "Couldn't retrieve Updater class methods." 298msgid "Edit"
252msgid "Couldn't retrieve updater class methods." 299msgstr "Modifier"
253msgstr "Impossible de récupérer les méthodes de la classe Updater."
254 300
255#: application/updater/Updater.php:526 index.php:1034 301#: application/front/controller/admin/ManageShaareController.php:366
256msgid "" 302#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
257"You have enabled or changed thumbnails mode. <a href=\"?do=thumbs_update" 303#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
258"\">Please synchronize them</a>." 304msgid "Shaare"
259msgstr "" 305msgstr "Shaare"
260"Vous avez activé ou changé le mode de miniatures. <a href=\"?do=thumbs_update" 306
261"\">Merci de les synchroniser</a>." 307#: application/front/controller/admin/ManageTagController.php:29
308#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
309#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
310msgid "Manage tags"
311msgstr "Gérer les tags"
312
313#: application/front/controller/admin/ManageTagController.php:48
314msgid "Invalid tags provided."
315msgstr "Les tags fournis ne sont pas valides."
316
317#: application/front/controller/admin/ManageTagController.php:72
318#, php-format
319msgid "The tag was removed from %d bookmark."
320msgid_plural "The tag was removed from %d bookmarks."
321msgstr[0] "Le tag a été supprimé du %d lien."
322msgstr[1] "Le tag a été supprimé de %d liens."
323
324#: application/front/controller/admin/ManageTagController.php:77
325#, php-format
326msgid "The tag was renamed in %d bookmark."
327msgid_plural "The tag was renamed in %d bookmarks."
328msgstr[0] "Le tag a été renommé dans %d lien."
329msgstr[1] "Le tag a été renommé dans %d liens."
330
331#: application/front/controller/admin/PasswordController.php:28
332#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
333#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
334msgid "Change password"
335msgstr "Modifier le mot de passe"
262 336
263#: application/updater/UpdaterUtils.php:32 337#: application/front/controller/admin/PasswordController.php:55
264msgid "Updates file path is not set, can't write updates." 338msgid "You must provide the current and new password to change it."
265msgstr "" 339msgstr ""
266"Le chemin vers le fichier de mise à jour n'est pas défini, impossible " 340"Vous devez fournir les mots de passe actuel et nouveau pour pouvoir le "
267"d'écrire les mises à jour." 341"modifier."
268 342
269#: application/updater/UpdaterUtils.php:37 343#: application/front/controller/admin/PasswordController.php:71
270msgid "Unable to write updates in " 344msgid "The old password is not correct."
271msgstr "Impossible d'écrire les mises à jour dans " 345msgstr "L'ancien mot de passe est incorrect."
272 346
273#: application/updater/exception/UpdaterException.php:51 347#: application/front/controller/admin/PasswordController.php:97
274msgid "An error occurred while running the update " 348msgid "Your password has been changed"
275msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " 349msgstr "Votre mot de passe a été modifié"
276 350
277#: index.php:145 351#: application/front/controller/admin/PluginsController.php:45
278msgid "Shared links on " 352msgid "Plugin Administration"
279msgstr "Liens partagés sur " 353msgstr "Administration des plugins"
280 354
281#: index.php:167 355#: application/front/controller/admin/PluginsController.php:75
282msgid "Insufficient permissions:" 356msgid "Setting successfully saved."
283msgstr "Permissions insuffisantes :" 357msgstr "Les paramètres ont été sauvegardés avec succès."
284 358
285#: index.php:203 359#: application/front/controller/admin/PluginsController.php:78
286msgid "I said: NO. You are banned for the moment. Go away." 360msgid "Error while saving plugin configuration: "
287msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard." 361msgstr ""
362"Une erreur s'est produite lors de la sauvegarde de la configuration des "
363"plugins : "
288 364
289#: index.php:275 365#: application/front/controller/admin/ThumbnailsController.php:37
290msgid "Wrong login/password." 366#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
291msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." 367msgid "Thumbnails update"
368msgstr "Mise à jour des miniatures"
369
370#: application/front/controller/admin/ToolsController.php:31
371#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
372#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33
373msgid "Tools"
374msgstr "Outils"
292 375
293#: index.php:398 index.php:404 376#: application/front/controller/visitor/BookmarkListController.php:114
377msgid "Search: "
378msgstr "Recherche : "
379
380#: application/front/controller/visitor/DailyController.php:45
294msgid "Today" 381msgid "Today"
295msgstr "Aujourd'hui" 382msgstr "Aujourd'hui"
296 383
297#: index.php:400 384#: application/front/controller/visitor/DailyController.php:47
298msgid "Yesterday" 385msgid "Yesterday"
299msgstr "Hier" 386msgstr "Hier"
300 387
301#: index.php:484 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 388#: application/front/controller/visitor/DailyController.php:86
302#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:46 389#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
390#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
303msgid "Daily" 391msgid "Daily"
304msgstr "Quotidien" 392msgstr "Quotidien"
305 393
306#: index.php:593 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 394#: application/front/controller/visitor/InstallController.php:74
307#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 395#, php-format
308#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 396msgid ""
309#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 397"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
310#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:75 398"variable \"session.save_path\" is set correctly in your PHP config, and that "
311#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:99 399"you have write access to it.<br>It currently points to %s.<br>On some "
400"browsers, accessing your server via a hostname like 'localhost' or any "
401"custom hostname without a dot causes cookie storage to fail. We recommend "
402"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
403msgstr ""
404"<pre>Les sesssions ne semblent pas fonctionner sur ce serveur.<br>Assurez "
405"vous que la variable « session.save_path » est correctement définie dans "
406"votre fichier de configuration PHP, et que vous avez les droits d'écriture "
407"dessus.<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains "
408"navigateurs, accéder à votre serveur depuis un nom d'hôte comme « localhost "
409"» ou autre nom personnalisé sans point '.' entraine l'échec de la sauvegarde "
410"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
411"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
412
413#: application/front/controller/visitor/InstallController.php:149
414msgid ""
415"Shaarli is now configured. Please login and start shaaring your bookmarks!"
416msgstr ""
417"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
418"shaare vos liens !"
419
420#: application/front/controller/visitor/InstallController.php:163
421msgid "Insufficient permissions:"
422msgstr "Permissions insuffisantes :"
423
424#: application/front/controller/visitor/LoginController.php:46
425#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
426#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
427#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
428#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
429#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77
430#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101
312msgid "Login" 431msgid "Login"
313msgstr "Connexion" 432msgstr "Connexion"
314 433
315#: index.php:608 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 434#: application/front/controller/visitor/LoginController.php:78
316#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:41 435msgid "Wrong login/password."
436msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
437
438#: application/front/controller/visitor/PictureWallController.php:29
439#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
440#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43
317msgid "Picture wall" 441msgid "Picture wall"
318msgstr "Mur d'images" 442msgstr "Mur d'images"
319 443
320#: index.php:683 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 444#: application/front/controller/visitor/TagCloudController.php:82
321#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 445#, fuzzy
322#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 446#| msgid "Tag list"
323msgid "Tag cloud" 447msgid "Tag "
324msgstr "Nuage de tags"
325
326#: index.php:715 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
327msgid "Tag list"
328msgstr "Liste des tags" 448msgstr "Liste des tags"
329 449
330#: index.php:944 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 450#: application/front/exceptions/AlreadyInstalledException.php:11
331#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 451msgid "Shaarli has already been installed. Login to edit the configuration."
332msgid "Tools" 452msgstr ""
333msgstr "Outils" 453"Shaarli est déjà installé. Connectez-vous pour modifier la configuration."
334 454
335#: index.php:952 455#: application/front/exceptions/LoginBannedException.php:11
456msgid ""
457"You have been banned after too many failed login attempts. Try again later."
458msgstr ""
459"Vous avez été banni après trop d'échecs d'authentification. Merci de "
460"réessayer plus tard."
461
462#: application/front/exceptions/OpenShaarliPasswordException.php:16
336msgid "You are not supposed to change a password on an Open Shaarli." 463msgid "You are not supposed to change a password on an Open Shaarli."
337msgstr "" 464msgstr ""
338"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert." 465"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
339 466
340#: index.php:957 index.php:1007 index.php:1094 index.php:1124 index.php:1234 467#: application/front/exceptions/ThumbnailsDisabledException.php:11
341#: index.php:1281 468msgid "Picture wall unavailable (thumbnails are disabled)."
469msgstr ""
470"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
471
472#: application/front/exceptions/WrongTokenException.php:16
342msgid "Wrong token." 473msgid "Wrong token."
343msgstr "Jeton invalide." 474msgstr "Jeton invalide."
344 475
345#: index.php:966 476#: application/legacy/LegacyLinkDB.php:131
346msgid "The old password is not correct." 477msgid "You are not authorized to add a link."
347msgstr "L'ancien mot de passe est incorrect." 478msgstr "Vous n'êtes pas autorisé à ajouter un lien."
348 479
349#: index.php:993 480#: application/legacy/LegacyLinkDB.php:134
350msgid "Your password has been changed" 481msgid "Internal Error: A link should always have an id and URL."
351msgstr "Votre mot de passe a été modifié" 482msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
352 483
353#: index.php:997 484#: application/legacy/LegacyLinkDB.php:137
354#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 485msgid "You must specify an integer as a key."
355#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 486msgstr "Vous devez utiliser un entier comme clé."
356msgid "Change password"
357msgstr "Modifier le mot de passe"
358 487
359#: index.php:1054 488#: application/legacy/LegacyLinkDB.php:140
360msgid "Configuration was saved." 489msgid "Array offset and link ID must be equal."
361msgstr "La configuration a été sauvegardée." 490msgstr "La clé du tableau et l'ID du lien doivent être identiques."
362 491
363#: index.php:1078 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 492#: application/legacy/LegacyUpdater.php:104
364msgid "Configure" 493msgid "Couldn't retrieve updater class methods."
365msgstr "Configurer" 494msgstr "Impossible de récupérer les méthodes de la classe Updater."
366 495
367#: index.php:1088 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 496#: application/legacy/LegacyUpdater.php:538
368#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 497msgid "<a href=\"./admin/thumbnails\">"
369msgid "Manage tags" 498msgstr "<a href=\"./admin/thumbnails\">"
370msgstr "Gérer les tags"
371 499
372#: index.php:1107 500#: application/netscape/NetscapeBookmarkUtils.php:63
373#, php-format 501msgid "Invalid export selection:"
374msgid "The tag was removed from %d link." 502msgstr "Sélection d'export invalide :"
375msgid_plural "The tag was removed from %d links."
376msgstr[0] "Le tag a été supprimé de %d lien."
377msgstr[1] "Le tag a été supprimé de %d liens."
378 503
379#: index.php:1108 504#: application/netscape/NetscapeBookmarkUtils.php:215
380#, php-format 505#, php-format
381msgid "The tag was renamed in %d link." 506msgid "File %s (%d bytes) "
382msgid_plural "The tag was renamed in %d links." 507msgstr "Le fichier %s (%d octets) "
383msgstr[0] "Le tag a été renommé dans %d lien."
384msgstr[1] "Le tag a été renommé dans %d liens."
385
386#: index.php:1115 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
387msgid "Shaare a new link"
388msgstr "Partager un nouveau lien"
389
390#: index.php:1344 tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
391msgid "Edit"
392msgstr "Modifier"
393
394#: index.php:1344 index.php:1416
395#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
396#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
397msgid "Shaare"
398msgstr "Shaare"
399
400#: index.php:1385
401msgid "Note: "
402msgstr "Note : "
403
404#: index.php:1424
405msgid "Invalid link ID provided"
406msgstr "ID du lien non valide"
407
408#: index.php:1444 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
409msgid "Export"
410msgstr "Exporter"
411 508
412#: index.php:1506 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 509#: application/netscape/NetscapeBookmarkUtils.php:217
413msgid "Import" 510msgid "has an unknown file format. Nothing was imported."
414msgstr "Importer" 511msgstr "a un format inconnu. Rien n'a été importé."
415 512
416#: index.php:1516 513#: application/netscape/NetscapeBookmarkUtils.php:221
417#, php-format 514#, php-format
418msgid "" 515msgid ""
419"The file you are trying to upload is probably bigger than what this " 516"was successfully processed in %d seconds: %d bookmarks imported, %d "
420"webserver can accept (%s). Please upload in smaller chunks." 517"bookmarks overwritten, %d bookmarks skipped."
421msgstr "" 518msgstr ""
422"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que " 519"a été importé avec succès en %d secondes : %d liens importés, %d liens "
423"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " 520"écrasés, %d liens ignorés."
424"légères."
425
426#: index.php:1561 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
427#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
428msgid "Plugin administration"
429msgstr "Administration des plugins"
430
431#: index.php:1616 tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
432msgid "Thumbnails update"
433msgstr "Mise à jour des miniatures"
434 521
435#: index.php:1782 522#: application/plugin/exception/PluginFileNotFoundException.php:21
436msgid "Search: " 523#, php-format
437msgstr "Recherche : " 524msgid "Plugin \"%s\" files not found."
525msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
438 526
439#: index.php:1825 527#: application/render/PageCacheManager.php:32
440#, php-format 528#, php-format
441msgid "" 529msgid "Cannot purge %s: no directory"
442"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " 530msgstr "Impossible de purger %s : le répertoire n'existe pas"
443"variable \"session.save_path\" is set correctly in your PHP config, and that "
444"you have write access to it.<br>It currently points to %s.<br>On some "
445"browsers, accessing your server via a hostname like 'localhost' or any "
446"custom hostname without a dot causes cookie storage to fail. We recommend "
447"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
448msgstr ""
449"<pre>Les sesssions ne semblent pas fonctionner sur ce serveur.<br>Assurez "
450"vous que la variable « session.save_path » est correctement définie dans "
451"votre fichier de configuration PHP, et que vous avez les droits d'écriture "
452"dessus.<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains "
453"navigateurs, accéder à votre serveur depuis un nom d'hôte comme « localhost "
454"» ou autre nom personnalisé sans point '.' entraine l'échec de la sauvegarde "
455"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
456"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
457 531
458#: index.php:1835 532#: application/updater/exception/UpdaterException.php:51
459msgid "Click to try again." 533msgid "An error occurred while running the update "
460msgstr "Cliquer ici pour réessayer." 534msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
535
536#: index.php:62
537msgid "Shared bookmarks on "
538msgstr "Liens partagés sur "
461 539
462#: plugins/addlink_toolbar/addlink_toolbar.php:31 540#: plugins/addlink_toolbar/addlink_toolbar.php:31
463msgid "URI" 541msgid "URI"
@@ -506,7 +584,7 @@ msgstr "Couleur de fond (gris léger)"
506msgid "Dark main color (e.g. visited links)" 584msgid "Dark main color (e.g. visited links)"
507msgstr "Couleur principale sombre (ex : les liens visités)" 585msgstr "Couleur principale sombre (ex : les liens visités)"
508 586
509#: plugins/demo_plugin/demo_plugin.php:482 587#: plugins/demo_plugin/demo_plugin.php:477
510msgid "" 588msgid ""
511"A demo plugin covering all use cases for template designers and plugin " 589"A demo plugin covering all use cases for template designers and plugin "
512"developers." 590"developers."
@@ -514,11 +592,11 @@ msgstr ""
514"Une extension de démonstration couvrant tous les cas d'utilisation pour les " 592"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
515"designers de thèmes et les développeurs d'extensions." 593"designers de thèmes et les développeurs d'extensions."
516 594
517#: plugins/demo_plugin/demo_plugin.php:483 595#: plugins/demo_plugin/demo_plugin.php:478
518msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." 596msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
519msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé." 597msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé."
520 598
521#: plugins/demo_plugin/demo_plugin.php:484 599#: plugins/demo_plugin/demo_plugin.php:479
522msgid "Other demo parameter" 600msgid "Other demo parameter"
523msgstr "Un autre paramètre de démo" 601msgstr "Un autre paramètre de démo"
524 602
@@ -540,36 +618,6 @@ msgstr ""
540msgid "Isso server URL (without 'http://')" 618msgid "Isso server URL (without 'http://')"
541msgstr "URL du serveur Isso (sans 'http://')" 619msgstr "URL du serveur Isso (sans 'http://')"
542 620
543#: plugins/markdown/markdown.php:163
544msgid "Description will be rendered with"
545msgstr "La description sera générée avec"
546
547#: plugins/markdown/markdown.php:164
548msgid "Markdown syntax documentation"
549msgstr "Documentation sur la syntaxe Markdown"
550
551#: plugins/markdown/markdown.php:165
552msgid "Markdown syntax"
553msgstr "la syntaxe Markdown"
554
555#: plugins/markdown/markdown.php:361
556msgid ""
557"Render shaare description with Markdown syntax.<br><strong>Warning</"
558"strong>:\n"
559"If your shaared descriptions contained HTML tags before enabling the "
560"markdown plugin,\n"
561"enabling it might break your page.\n"
562"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
563"markdown#html-rendering\">README</a>."
564msgstr ""
565"Utilise la syntaxe Markdown pour la description des liens."
566"<br><strong>Attention</strong> :\n"
567"Si vous aviez des descriptions contenant du HTML avant d'activer cette "
568"extension,\n"
569"l'activer pourrait déformer vos pages.\n"
570"Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
571"markdown#html-rendering\">README</a>."
572
573#: plugins/piwik/piwik.php:23 621#: plugins/piwik/piwik.php:23
574msgid "" 622msgid ""
575"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " 623"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
@@ -650,16 +698,6 @@ msgstr "URL de l'API Wallabag"
650msgid "Wallabag API version (1 or 2)" 698msgid "Wallabag API version (1 or 2)"
651msgstr "Version de l'API Wallabag (1 ou 2)" 699msgstr "Version de l'API Wallabag (1 ou 2)"
652 700
653#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
654#: tests/languages/fr/LanguagesFrTest.php:159
655#: tests/languages/fr/LanguagesFrTest.php:172
656#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
657#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:85
658msgid "Search"
659msgid_plural "Search"
660msgstr[0] "Rechercher"
661msgstr[1] "Rechercher"
662
663#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 701#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
664msgid "Sorry, nothing to see here." 702msgid "Sorry, nothing to see here."
665msgstr "Désolé, il y a rien à voir ici." 703msgstr "Désolé, il y a rien à voir ici."
@@ -698,10 +736,11 @@ msgid "Rename"
698msgstr "Renommer" 736msgstr "Renommer"
699 737
700#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 738#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
701#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 739#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93
702#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 740#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
703#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145 741#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
704#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:145 742#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
743#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
705msgid "Delete" 744msgid "Delete"
706msgstr "Supprimer" 745msgstr "Supprimer"
707 746
@@ -713,33 +752,6 @@ msgstr "Vous pouvez aussi modifier les tags dans la"
713msgid "tag list" 752msgid "tag list"
714msgstr "liste des tags" 753msgstr "liste des tags"
715 754
716#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:143
717#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:312
718#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
719msgid "All"
720msgstr "Tous"
721
722#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:147
723#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:316
724msgid "Only common media hosts"
725msgstr "Seulement les hébergeurs de média connus"
726
727#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:151
728#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
729msgid "None"
730msgstr "Aucune"
731
732#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:158
733#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:297
734msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
735msgstr ""
736"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
737"miniatures."
738
739#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:162
740msgid "Synchonize thumbnails"
741msgstr "Synchroniser les miniatures"
742
743#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 755#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
744msgid "title" 756msgid "title"
745msgstr "titre" 757msgstr "titre"
@@ -756,109 +768,132 @@ msgstr "Valeur par défaut"
756msgid "Theme" 768msgid "Theme"
757msgstr "Thème" 769msgstr "Thème"
758 770
759#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 771#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
760#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 772msgid "Description formatter"
773msgstr "Format des descriptions"
774
775#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
776#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
761msgid "Language" 777msgid "Language"
762msgstr "Langue" 778msgstr "Langue"
763 779
764#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116 780#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
765#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 781#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
766msgid "Timezone" 782msgid "Timezone"
767msgstr "Fuseau horaire" 783msgstr "Fuseau horaire"
768 784
769#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 785#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
770#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 786#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
771msgid "Continent" 787msgid "Continent"
772msgstr "Continent" 788msgstr "Continent"
773 789
774#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 790#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
775#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 791#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
776msgid "City" 792msgid "City"
777msgstr "Ville" 793msgstr "Ville"
778 794
779#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164 795#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
780msgid "Disable session cookie hijacking protection" 796msgid "Disable session cookie hijacking protection"
781msgstr "Désactiver la protection contre le détournement de cookies" 797msgstr "Désactiver la protection contre le détournement de cookies"
782 798
783#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 799#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193
784msgid "Check this if you get disconnected or if your IP address changes often" 800msgid "Check this if you get disconnected or if your IP address changes often"
785msgstr "" 801msgstr ""
786"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP " 802"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP "
787"change souvent" 803"change souvent"
788 804
789#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 805#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210
790msgid "Private links by default" 806msgid "Private links by default"
791msgstr "Liens privés par défaut" 807msgstr "Liens privés par défaut"
792 808
793#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184 809#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211
794msgid "All new links are private by default" 810msgid "All new links are private by default"
795msgstr "Tous les nouveaux liens sont privés par défaut" 811msgstr "Tous les nouveaux liens sont privés par défaut"
796 812
797#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 813#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226
798msgid "RSS direct links" 814msgid "RSS direct links"
799msgstr "Liens directs dans le flux RSS" 815msgstr "Liens directs dans le flux RSS"
800 816
801#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200 817#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227
802msgid "Check this to use direct URL instead of permalink in feeds" 818msgid "Check this to use direct URL instead of permalink in feeds"
803msgstr "" 819msgstr ""
804"Cocher cette case pour utiliser des liens directs au lieu des permaliens " 820"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
805"dans le flux RSS" 821"dans le flux RSS"
806 822
807#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215 823#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242
808msgid "Hide public links" 824msgid "Hide public links"
809msgstr "Cacher les liens publics" 825msgstr "Cacher les liens publics"
810 826
811#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216 827#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243
812msgid "Do not show any links if the user is not logged in" 828msgid "Do not show any links if the user is not logged in"
813msgstr "N'afficher aucun lien sans être connecté" 829msgstr "N'afficher aucun lien sans être connecté"
814 830
815#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231 831#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258
816#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 832#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149
817msgid "Check updates" 833msgid "Check updates"
818msgstr "Vérifier les mises à jour" 834msgstr "Vérifier les mises à jour"
819 835
820#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232 836#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259
821#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 837#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
822msgid "Notify me when a new release is ready" 838msgid "Notify me when a new release is ready"
823msgstr "Me notifier lorsqu'une nouvelle version est disponible" 839msgstr "Me notifier lorsqu'une nouvelle version est disponible"
824 840
825#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247 841#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
826msgid "Automatically retrieve description for new bookmarks" 842msgid "Automatically retrieve description for new bookmarks"
827msgstr "Récupérer automatiquement la description" 843msgstr "Récupérer automatiquement la description"
828 844
829#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248 845#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275
830msgid "Shaarli will try to retrieve the description from meta HTML headers" 846msgid "Shaarli will try to retrieve the description from meta HTML headers"
831msgstr "" 847msgstr ""
832"Shaarli essaiera de récupérer la description depuis les balises HTML meta " 848"Shaarli essaiera de récupérer la description depuis les balises HTML meta "
833"dans les entêtes" 849"dans les entêtes"
834 850
835#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263 851#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290
836#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 852#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
837msgid "Enable REST API" 853msgid "Enable REST API"
838msgstr "Activer l'API REST" 854msgstr "Activer l'API REST"
839 855
840#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:264 856#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291
841#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 857#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
842msgid "Allow third party software to use Shaarli such as mobile application" 858msgid "Allow third party software to use Shaarli such as mobile application"
843msgstr "" 859msgstr ""
844"Permet aux applications tierces d'utiliser Shaarli, par exemple les " 860"Permet aux applications tierces d'utiliser Shaarli, par exemple les "
845"applications mobiles" 861"applications mobiles"
846 862
847#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:279 863#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306
848msgid "API secret" 864msgid "API secret"
849msgstr "Clé d'API secrète" 865msgstr "Clé d'API secrète"
850 866
851#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:293 867#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
852msgid "Enable thumbnails" 868msgid "Enable thumbnails"
853msgstr "Activer les miniatures" 869msgstr "Activer les miniatures"
854 870
855#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:301 871#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324
872msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
873msgstr ""
874"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
875"miniatures."
876
877#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
856#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 878#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
857msgid "Synchronize thumbnails" 879msgid "Synchronize thumbnails"
858msgstr "Synchroniser les miniatures" 880msgstr "Synchroniser les miniatures"
859 881
860#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 882#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
861#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 883#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
884msgid "All"
885msgstr "Tous"
886
887#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
888msgid "Only common media hosts"
889msgstr "Seulement les hébergeurs de média connus"
890
891#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
892msgid "None"
893msgstr "Aucune"
894
895#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
896#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
862#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 897#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
863#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 898#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
864msgid "Save" 899msgid "Save"
@@ -884,27 +919,27 @@ msgstr "Tous les liens d'un jour sur une page."
884msgid "Next day" 919msgid "Next day"
885msgstr "Jour suivant" 920msgstr "Jour suivant"
886 921
887#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 922#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
888msgid "Edit Shaare" 923msgid "Edit Shaare"
889msgstr "Modifier le Shaare" 924msgstr "Modifier le Shaare"
890 925
891#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 926#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
892msgid "New Shaare" 927msgid "New Shaare"
893msgstr "Nouveau Shaare" 928msgstr "Nouveau Shaare"
894 929
895#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 930#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
896msgid "Created:" 931msgid "Created:"
897msgstr "Création :" 932msgstr "Création :"
898 933
899#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 934#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
900msgid "URL" 935msgid "URL"
901msgstr "URL" 936msgstr "URL"
902 937
903#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 938#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
904msgid "Title" 939msgid "Title"
905msgstr "Titre" 940msgstr "Titre"
906 941
907#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 942#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
908#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 943#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
909#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 944#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
910#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 945#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@@ -912,17 +947,29 @@ msgstr "Titre"
912msgid "Description" 947msgid "Description"
913msgstr "Description" 948msgstr "Description"
914 949
915#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 950#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
916msgid "Tags" 951msgid "Tags"
917msgstr "Tags" 952msgstr "Tags"
918 953
919#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 954#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
920#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 955#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
921#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167 956#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
922msgid "Private" 957msgid "Private"
923msgstr "Privé" 958msgstr "Privé"
924 959
925#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 960#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
961msgid "Description will be rendered with"
962msgstr "La description sera générée avec"
963
964#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
965msgid "Markdown syntax documentation"
966msgstr "Documentation sur la syntaxe Markdown"
967
968#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
969msgid "Markdown syntax"
970msgstr "la syntaxe Markdown"
971
972#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
926msgid "Apply Changes" 973msgid "Apply Changes"
927msgstr "Appliquer les changements" 974msgstr "Appliquer les changements"
928 975
@@ -930,19 +977,19 @@ msgstr "Appliquer les changements"
930msgid "Export Database" 977msgid "Export Database"
931msgstr "Exporter les données" 978msgstr "Exporter les données"
932 979
933#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 980#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
934msgid "Selection" 981msgid "Selection"
935msgstr "Choisir" 982msgstr "Choisir"
936 983
937#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 984#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
938msgid "Public" 985msgid "Public"
939msgstr "Publics" 986msgstr "Publics"
940 987
941#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 988#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
942msgid "Prepend note permalinks with this Shaarli instance's URL" 989msgid "Prepend note permalinks with this Shaarli instance's URL"
943msgstr "Préfixer les liens de note avec l'URL de l'instance de Shaarli" 990msgstr "Préfixer les liens de note avec l'URL de l'instance de Shaarli"
944 991
945#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 992#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
946msgid "Useful to import bookmarks in a web browser" 993msgid "Useful to import bookmarks in a web browser"
947msgstr "Utile pour importer les marques-pages dans un navigateur" 994msgstr "Utile pour importer les marques-pages dans un navigateur"
948 995
@@ -993,29 +1040,29 @@ msgstr ""
993"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de " 1040"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de "
994"le configurer." 1041"le configurer."
995 1042
996#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 1043#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
997#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 1044#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
998#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 1045#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167
999#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:165 1046#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167
1000msgid "Username" 1047msgid "Username"
1001msgstr "Nom d'utilisateur" 1048msgstr "Nom d'utilisateur"
1002 1049
1003#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 1050#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1004#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 1051#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
1005#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 1052#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
1006#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:166 1053#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168
1007msgid "Password" 1054msgid "Password"
1008msgstr "Mot de passe" 1055msgstr "Mot de passe"
1009 1056
1010#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 1057#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62
1011msgid "Shaarli title" 1058msgid "Shaarli title"
1012msgstr "Titre du Shaarli" 1059msgstr "Titre du Shaarli"
1013 1060
1014#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 1061#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
1015msgid "My links" 1062msgid "My links"
1016msgstr "Mes liens" 1063msgstr "Mes liens"
1017 1064
1018#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 1065#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
1019msgid "Install" 1066msgid "Install"
1020msgstr "Installer" 1067msgstr "Installer"
1021 1068
@@ -1034,21 +1081,31 @@ msgstr[0] "lien privé"
1034msgstr[1] "liens privés" 1081msgstr[1] "liens privés"
1035 1082
1036#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 1083#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1037#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 1084#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1038#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:121 1085#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123
1039msgid "Search text" 1086msgid "Search text"
1040msgstr "Recherche texte" 1087msgstr "Recherche texte"
1041 1088
1042#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 1089#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
1043#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128 1090#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
1044#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:128 1091#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130
1045#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1092#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1046#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 1093#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
1047#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1094#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1048#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 1095#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1049msgid "Filter by tag" 1096msgid "Filter by tag"
1050msgstr "Filtrer par tag" 1097msgstr "Filtrer par tag"
1051 1098
1099#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1100#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
1101#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
1102#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:87
1103#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:139
1104#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1105#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
1106msgid "Search"
1107msgstr "Rechercher"
1108
1052#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 1109#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
1053msgid "Nothing found." 1110msgid "Nothing found."
1054msgstr "Aucun résultat." 1111msgstr "Aucun résultat."
@@ -1069,40 +1126,41 @@ msgid "tagged"
1069msgstr "taggé" 1126msgstr "taggé"
1070 1127
1071#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133 1128#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
1129#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1072msgid "Remove tag" 1130msgid "Remove tag"
1073msgstr "Retirer le tag" 1131msgstr "Retirer le tag"
1074 1132
1075#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:142 1133#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
1076msgid "with status" 1134msgid "with status"
1077msgstr "avec le statut" 1135msgstr "avec le statut"
1078 1136
1079#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 1137#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1080msgid "without any tag" 1138msgid "without any tag"
1081msgstr "sans tag" 1139msgstr "sans tag"
1082 1140
1083#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 1141#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
1084#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 1142#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1085#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 1143#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
1086msgid "Fold" 1144msgid "Fold"
1087msgstr "Replier" 1145msgstr "Replier"
1088 1146
1089#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 1147#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
1090msgid "Edited: " 1148msgid "Edited: "
1091msgstr "Modifié : " 1149msgstr "Modifié : "
1092 1150
1093#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 1151#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
1094msgid "permalink" 1152msgid "permalink"
1095msgstr "permalien" 1153msgstr "permalien"
1096 1154
1097#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 1155#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
1098msgid "Add tag" 1156msgid "Add tag"
1099msgstr "Ajouter un tag" 1157msgstr "Ajouter un tag"
1100 1158
1101#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 1159#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
1102msgid "Toggle sticky" 1160msgid "Toggle sticky"
1103msgstr "Changer statut épinglé" 1161msgstr "Changer statut épinglé"
1104 1162
1105#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185 1163#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
1106msgid "Sticky" 1164msgid "Sticky"
1107msgstr "Épinglé" 1165msgstr "Épinglé"
1108 1166
@@ -1145,16 +1203,9 @@ msgstr "Replier tout"
1145msgid "Links per page" 1203msgid "Links per page"
1146msgstr "Liens par page" 1204msgstr "Liens par page"
1147 1205
1148#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1206#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
1149msgid "" 1207#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
1150"You have been banned after too many failed login attempts. Try again later." 1208#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171
1151msgstr ""
1152"Vous avez été banni après trop d'échecs d'authentification. Merci de "
1153"réessayer plus tard."
1154
1155#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1156#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1157#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
1158msgid "Remember me" 1209msgid "Remember me"
1159msgstr "Rester connecté" 1210msgstr "Rester connecté"
1160 1211
@@ -1185,62 +1236,64 @@ msgstr "Déplier tout"
1185msgid "Are you sure you want to delete this link?" 1236msgid "Are you sure you want to delete this link?"
1186msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" 1237msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
1187 1238
1188#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 1239#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11
1189#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:90 1240#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11
1190#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:65 1241msgid "Menu"
1191#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:90 1242msgstr "Menu"
1243
1244#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
1245#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38
1246#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1247msgid "Tag cloud"
1248msgstr "Nuage de tags"
1249
1250#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
1251#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
1252#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67
1253#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92
1192msgid "RSS Feed" 1254msgid "RSS Feed"
1193msgstr "Flux RSS" 1255msgstr "Flux RSS"
1194 1256
1195#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 1257#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
1196#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 1258#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
1197#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:70 1259#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72
1198#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:106 1260#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108
1199msgid "Logout" 1261msgid "Logout"
1200msgstr "Déconnexion" 1262msgstr "Déconnexion"
1201 1263
1202#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 1264#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
1203#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:150 1265#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
1204msgid "Set public" 1266msgid "Set public"
1205msgstr "Rendre public" 1267msgstr "Rendre public"
1206 1268
1207#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 1269#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157
1208#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:155 1270#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157
1209msgid "Set private" 1271msgid "Set private"
1210msgstr "Rendre privé" 1272msgstr "Rendre privé"
1211 1273
1212#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187 1274#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
1213#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:187 1275#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189
1214msgid "is available" 1276msgid "is available"
1215msgstr "est disponible" 1277msgstr "est disponible"
1216 1278
1217#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:194 1279#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196
1218#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:194 1280#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196
1219msgid "Error" 1281msgid "Error"
1220msgstr "Erreur" 1282msgstr "Erreur"
1221 1283
1222#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1284#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1223msgid "Picture wall unavailable (thumbnails are disabled)." 1285msgid "There is no cached thumbnail."
1224msgstr "" 1286msgstr "Il n'y a aucune miniature dans le cache."
1225"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
1226 1287
1227#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 1288#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
1228#, fuzzy 1289msgid "Try to synchronize them."
1229#| msgid "" 1290msgstr "Essayer de les synchroniser."
1230#| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
1231#| "\">synchronize them</a>."
1232msgid ""
1233"There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
1234"\">synchronize them</a>."
1235msgstr ""
1236"Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
1237"\">les synchroniser</a>."
1238 1291
1239#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1292#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1240msgid "Picture Wall" 1293msgid "Picture Wall"
1241msgstr "Mur d'images" 1294msgstr "Mur d'images"
1242 1295
1243#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1296#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1244msgid "pics" 1297msgid "pics"
1245msgstr "images" 1298msgstr "images"
1246 1299
@@ -1249,6 +1302,11 @@ msgid "You need to enable Javascript to change plugin loading order."
1249msgstr "" 1302msgstr ""
1250"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions." 1303"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
1251 1304
1305#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
1306#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
1307msgid "Plugin administration"
1308msgstr "Administration des plugins"
1309
1252#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 1310#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1253msgid "Enabled Plugins" 1311msgid "Enabled Plugins"
1254msgstr "Extensions activées" 1312msgstr "Extensions activées"
@@ -1314,6 +1372,14 @@ msgstr "tags"
1314msgid "List all links with those tags" 1372msgid "List all links with those tags"
1315msgstr "Lister tous les liens avec ces tags" 1373msgstr "Lister tous les liens avec ces tags"
1316 1374
1375#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1376msgid "Tag list"
1377msgstr "Liste des tags"
1378
1379#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
1380msgid "Rename tag"
1381msgstr "Renommer le tag"
1382
1317#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 1383#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1318#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 1384#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1319msgid "Sort by:" 1385msgid "Sort by:"
@@ -1457,6 +1523,68 @@ msgstr ""
1457"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1523"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1458"Ajouter aux favoris »" 1524"Ajouter aux favoris »"
1459 1525
1526#, fuzzy
1527#~| msgid "Selection"
1528#~ msgid ".ui-selecting"
1529#~ msgstr "Choisir"
1530
1531#, fuzzy
1532#~| msgid "Documentation"
1533#~ msgid "document"
1534#~ msgstr "Documentation"
1535
1536#~ msgid "The page you are trying to reach does not exist or has been deleted."
1537#~ msgstr ""
1538#~ "La page que vous essayez de consulter n'existe pas ou a été supprimée."
1539
1540#~ msgid "404 Not Found"
1541#~ msgstr "404 Introuvable"
1542
1543#~ msgid "Updates file path is not set, can't write updates."
1544#~ msgstr ""
1545#~ "Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
1546#~ "d'écrire les mises à jour."
1547
1548#~ msgid "Unable to write updates in "
1549#~ msgstr "Impossible d'écrire les mises à jour dans "
1550
1551#~ msgid "I said: NO. You are banned for the moment. Go away."
1552#~ msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
1553
1554#~ msgid "Click to try again."
1555#~ msgstr "Cliquer ici pour réessayer."
1556
1557#~ msgid ""
1558#~ "Render shaare description with Markdown syntax.<br><strong>Warning</"
1559#~ "strong>:\n"
1560#~ "If your shaared descriptions contained HTML tags before enabling the "
1561#~ "markdown plugin,\n"
1562#~ "enabling it might break your page.\n"
1563#~ "See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
1564#~ "markdown#html-rendering\">README</a>."
1565#~ msgstr ""
1566#~ "Utilise la syntaxe Markdown pour la description des liens."
1567#~ "<br><strong>Attention</strong> :\n"
1568#~ "Si vous aviez des descriptions contenant du HTML avant d'activer cette "
1569#~ "extension,\n"
1570#~ "l'activer pourrait déformer vos pages.\n"
1571#~ "Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
1572#~ "markdown#html-rendering\">README</a>."
1573
1574#~ msgid "Synchonize thumbnails"
1575#~ msgstr "Synchroniser les miniatures"
1576
1577#, fuzzy
1578#~| msgid ""
1579#~| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
1580#~| "\">synchronize them</a>."
1581#~ msgid ""
1582#~ "There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
1583#~ "\">synchronize them</a>."
1584#~ msgstr ""
1585#~ "Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
1586#~ "\">les synchroniser</a>."
1587
1460#~ msgid "" 1588#~ msgid ""
1461#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this " 1589#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1462#~ "functionality." 1590#~ "functionality."
diff --git a/index.php b/index.php
index b53b16fe..e7471823 100644
--- a/index.php
+++ b/index.php
@@ -12,120 +12,27 @@
12 * Licence: http://www.opensource.org/licenses/zlib-license.php 12 * Licence: http://www.opensource.org/licenses/zlib-license.php
13 */ 13 */
14 14
15// Set 'UTC' as the default timezone if it is not defined in php.ini
16// See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
17if (date_default_timezone_get() == '') {
18 date_default_timezone_set('UTC');
19}
20
21/*
22 * PHP configuration
23 */
24
25// http://server.com/x/shaarli --> /shaarli/
26define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
27
28// High execution time in case of problematic imports/exports.
29ini_set('max_input_time', '60');
30
31// Try to set max upload file size and read
32ini_set('memory_limit', '128M');
33ini_set('post_max_size', '16M');
34ini_set('upload_max_filesize', '16M');
35
36// See all error except warnings
37error_reporting(E_ALL^E_WARNING);
38
39// 3rd-party libraries
40if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
41 header('Content-Type: text/plain; charset=utf-8');
42 echo "Error: missing Composer configuration\n\n"
43 ."If you installed Shaarli through Git or using the development branch,\n"
44 ."please refer to the installation documentation to install PHP"
45 ." dependencies using Composer:\n"
46 ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
47 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
48 exit;
49}
50require_once 'inc/rain.tpl.class.php'; 15require_once 'inc/rain.tpl.class.php';
51require_once __DIR__ . '/vendor/autoload.php'; 16require_once __DIR__ . '/vendor/autoload.php';
52 17
53// Shaarli library 18// Shaarli library
54require_once 'application/bookmark/LinkUtils.php'; 19require_once 'application/bookmark/LinkUtils.php';
55require_once 'application/config/ConfigPlugin.php'; 20require_once 'application/config/ConfigPlugin.php';
56require_once 'application/feed/Cache.php';
57require_once 'application/http/HttpUtils.php'; 21require_once 'application/http/HttpUtils.php';
58require_once 'application/http/UrlUtils.php'; 22require_once 'application/http/UrlUtils.php';
59require_once 'application/updater/UpdaterUtils.php';
60require_once 'application/FileUtils.php';
61require_once 'application/TimeZone.php'; 23require_once 'application/TimeZone.php';
62require_once 'application/Utils.php'; 24require_once 'application/Utils.php';
63 25
64use Shaarli\ApplicationUtils; 26require_once __DIR__ . '/init.php';
65use Shaarli\Bookmark\Bookmark; 27
66use Shaarli\Bookmark\BookmarkFileService;
67use Shaarli\Bookmark\BookmarkFilter;
68use Shaarli\Bookmark\BookmarkServiceInterface;
69use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
70use Shaarli\Config\ConfigManager; 28use Shaarli\Config\ConfigManager;
71use Shaarli\Container\ContainerBuilder; 29use Shaarli\Container\ContainerBuilder;
72use Shaarli\Feed\CachedPage;
73use Shaarli\Feed\FeedBuilder;
74use Shaarli\Formatter\BookmarkMarkdownFormatter;
75use Shaarli\Formatter\FormatterFactory;
76use Shaarli\History;
77use Shaarli\Languages; 30use Shaarli\Languages;
78use Shaarli\Netscape\NetscapeBookmarkUtils; 31use Shaarli\Security\CookieManager;
79use Shaarli\Plugin\PluginManager;
80use Shaarli\Render\PageBuilder;
81use Shaarli\Render\ThemeUtils;
82use Shaarli\Router;
83use Shaarli\Security\LoginManager; 32use Shaarli\Security\LoginManager;
84use Shaarli\Security\SessionManager; 33use Shaarli\Security\SessionManager;
85use Shaarli\Thumbnailer;
86use Shaarli\Updater\Updater;
87use Shaarli\Updater\UpdaterUtils;
88use Slim\App; 34use Slim\App;
89 35
90// Ensure the PHP version is supported
91try {
92 ApplicationUtils::checkPHPVersion('7.1', PHP_VERSION);
93} catch (Exception $exc) {
94 header('Content-Type: text/plain; charset=utf-8');
95 echo $exc->getMessage();
96 exit;
97}
98
99define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
100
101// Force cookie path (but do not change lifetime)
102$cookie = session_get_cookie_params();
103$cookiedir = '';
104if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
105 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
106}
107// Set default cookie expiration and path.
108session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
109// Set session parameters on server side.
110// Use cookies to store session.
111ini_set('session.use_cookies', 1);
112// Force cookies for session (phpsessionID forbidden in URL).
113ini_set('session.use_only_cookies', 1);
114// Prevent PHP form using sessionID in URL if cookies are disabled.
115ini_set('session.use_trans_sid', false);
116
117session_name('shaarli');
118// Start session if needed (Some server auto-start sessions).
119if (session_status() == PHP_SESSION_NONE) {
120 session_start();
121}
122
123// Regenerate session ID if invalid or not defined in cookie.
124if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
125 session_regenerate_id(true);
126 $_COOKIE['shaarli'] = session_id();
127}
128
129$conf = new ConfigManager(); 36$conf = new ConfigManager();
130 37
131// In dev mode, throw exception on any warning 38// In dev mode, throw exception on any warning
@@ -133,20 +40,16 @@ if ($conf->get('dev.debug', false)) {
133 // See all errors (for debugging only) 40 // See all errors (for debugging only)
134 error_reporting(-1); 41 error_reporting(-1);
135 42
136 set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) { 43 set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) {
137 throw new ErrorException($errstr, 0, $errno, $errfile, $errline); 44 throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
138 }); 45 });
139} 46}
140 47
141$sessionManager = new SessionManager($_SESSION, $conf); 48$sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
142$loginManager = new LoginManager($conf, $sessionManager); 49$sessionManager->initialize();
50$cookieManager = new CookieManager($_COOKIE);
51$loginManager = new LoginManager($conf, $sessionManager, $cookieManager);
143$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); 52$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
144$clientIpId = client_ip_id($_SERVER);
145
146// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
147if (! defined('LC_MESSAGES')) {
148 define('LC_MESSAGES', LC_COLLATE);
149}
150 53
151// Sniff browser language and set date format accordingly. 54// Sniff browser language and set date format accordingly.
152if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 55if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
@@ -157,1773 +60,76 @@ new Languages(setlocale(LC_MESSAGES, 0), $conf);
157 60
158$conf->setEmpty('general.timezone', date_default_timezone_get()); 61$conf->setEmpty('general.timezone', date_default_timezone_get());
159$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER))); 62$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER)));
63
160RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory 64RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
161RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 65RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
162 66
163$pluginManager = new PluginManager($conf);
164$pluginManager->load($conf->get('general.enabled_plugins'));
165
166date_default_timezone_set($conf->get('general.timezone', 'UTC')); 67date_default_timezone_set($conf->get('general.timezone', 'UTC'));
167 68
168ob_start(); // Output buffering for the page cache. 69$loginManager->checkLoginState(client_ip_id($_SERVER));
169
170// Prevent caching on client side or proxy: (yes, it's ugly)
171header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
172header("Cache-Control: no-store, no-cache, must-revalidate");
173header("Cache-Control: post-check=0, pre-check=0", false);
174header("Pragma: no-cache");
175
176if (! is_file($conf->getConfigFileExt())) {
177 // Ensure Shaarli has proper access to its resources
178 $errors = ApplicationUtils::checkResourcePermissions($conf);
179
180 if ($errors != array()) {
181 $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
182
183 foreach ($errors as $error) {
184 $message .= '<li>'.$error.'</li>';
185 }
186 $message .= '</ul>';
187
188 header('Content-Type: text/html; charset=utf-8');
189 echo $message;
190 exit;
191 }
192
193 // Display the installation form if no existing config is found
194 install($conf, $sessionManager, $loginManager);
195}
196
197$loginManager->checkLoginState($_COOKIE, $clientIpId);
198
199/**
200 * Adapter function to ensure compatibility with third-party templates
201 *
202 * @see https://github.com/shaarli/Shaarli/pull/1086
203 *
204 * @return bool true when the user is logged in, false otherwise
205 */
206function isLoggedIn()
207{
208 global $loginManager;
209 return $loginManager->isLoggedIn();
210}
211
212
213// ------------------------------------------------------------------------------------------
214// Process login form: Check if login/password is correct.
215if (isset($_POST['login'])) {
216 if (! $loginManager->canLogin($_SERVER)) {
217 die(t('I said: NO. You are banned for the moment. Go away.'));
218 }
219 if (isset($_POST['password'])
220 && $sessionManager->checkToken($_POST['token'])
221 && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
222 ) {
223 $loginManager->handleSuccessfulLogin($_SERVER);
224
225 $cookiedir = '';
226 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
227 // Note: Never forget the trailing slash on the cookie path!
228 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
229 }
230
231 if (!empty($_POST['longlastingsession'])) {
232 // Keep the session cookie even after the browser closes
233 $sessionManager->setStaySignedIn(true);
234 $expirationTime = $sessionManager->extendSession();
235
236 setcookie(
237 $loginManager::$STAY_SIGNED_IN_COOKIE,
238 $loginManager->getStaySignedInToken(),
239 $expirationTime,
240 WEB_PATH
241 );
242 } else {
243 // Standard session expiration (=when browser closes)
244 $expirationTime = 0;
245 }
246
247 // Send cookie with the new expiration date to the browser
248 session_destroy();
249 session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
250 session_start();
251 session_regenerate_id(true);
252
253 // Optional redirect after login:
254 if (isset($_GET['post'])) {
255 $uri = './?post='. urlencode($_GET['post']);
256 foreach (array('description', 'source', 'title', 'tags') as $param) {
257 if (!empty($_GET[$param])) {
258 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
259 }
260 }
261 header('Location: '. $uri);
262 exit;
263 }
264
265 if (isset($_GET['edit_link'])) {
266 header('Location: ./?edit_link='. escape($_GET['edit_link']));
267 exit;
268 }
269
270 if (isset($_POST['returnurl'])) {
271 // Prevent loops over login screen.
272 if (strpos($_POST['returnurl'], '/login') === false) {
273 header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
274 exit;
275 }
276 }
277 header('Location: ./?');
278 exit;
279 } else {
280 $loginManager->handleFailedLogin($_SERVER);
281 $redir = '?username='. urlencode($_POST['login']);
282 if (isset($_GET['post'])) {
283 $redir .= '&post=' . urlencode($_GET['post']);
284 foreach (array('description', 'source', 'title', 'tags') as $param) {
285 if (!empty($_GET[$param])) {
286 $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
287 }
288 }
289 }
290 // Redirect to login screen.
291 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'./login'.$redir.'\';</script>';
292 exit;
293 }
294}
295
296// ------------------------------------------------------------------------------------------
297// Token management for XSRF protection
298// Token should be used in any form which acts on data (create,update,delete,import...).
299if (!isset($_SESSION['tokens'])) {
300 $_SESSION['tokens']=array(); // Token are attached to the session.
301}
302
303/**
304 * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
305 * Gives the last 7 days (which have bookmarks).
306 * This RSS feed cannot be filtered.
307 *
308 * @param BookmarkServiceInterface $bookmarkService
309 * @param ConfigManager $conf Configuration Manager instance
310 * @param LoginManager $loginManager LoginManager instance
311 */
312function showDailyRSS($bookmarkService, $conf, $loginManager)
313{
314 // Cache system
315 $query = $_SERVER['QUERY_STRING'];
316 $cache = new CachedPage(
317 $conf->get('config.PAGE_CACHE'),
318 page_url($_SERVER),
319 startsWith($query, 'do=dailyrss') && !$loginManager->isLoggedIn()
320 );
321 $cached = $cache->cachedVersion();
322 if (!empty($cached)) {
323 echo $cached;
324 exit;
325 }
326
327 /* Some Shaarlies may have very few bookmarks, so we need to look
328 back in time until we have enough days ($nb_of_days).
329 */
330 $nb_of_days = 7; // We take 7 days.
331 $today = date('Ymd');
332 $days = array();
333
334 foreach ($bookmarkService->search() as $bookmark) {
335 $day = $bookmark->getCreated()->format('Ymd'); // Extract day (without time)
336 if (strcmp($day, $today) < 0) {
337 if (empty($days[$day])) {
338 $days[$day] = array();
339 }
340 $days[$day][] = $bookmark;
341 }
342
343 if (count($days) > $nb_of_days) {
344 break; // Have we collected enough days?
345 }
346 }
347
348 // Build the RSS feed.
349 header('Content-Type: application/rss+xml; charset=utf-8');
350 $pageaddr = escape(index_url($_SERVER));
351 echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">';
352 echo '<channel>';
353 echo '<title>Daily - '. $conf->get('general.title') . '</title>';
354 echo '<link>'. $pageaddr .'</link>';
355 echo '<description>Daily shared bookmarks</description>';
356 echo '<language>en-en</language>';
357 echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
358
359 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
360 $formatter = $factory->getFormatter();
361 $formatter->addContextData('index_url', index_url($_SERVER));
362 // For each day.
363 /** @var Bookmark[] $bookmarks */
364 foreach ($days as $day => $bookmarks) {
365 $formattedBookmarks = [];
366 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
367 $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
368
369 // We pre-format some fields for proper output.
370 foreach ($bookmarks as $key => $bookmark) {
371 $formattedBookmarks[$key] = $formatter->format($bookmark);
372 // This page is a bit specific, we need raw description to calculate the length
373 $formattedBookmarks[$key]['formatedDescription'] = $formattedBookmarks[$key]['description'];
374 $formattedBookmarks[$key]['description'] = $bookmark->getDescription();
375
376 if ($bookmark->isNote()) {
377 $link['url'] = index_url($_SERVER) . $bookmark->getUrl(); // make permalink URL absolute
378 }
379 }
380
381 // Then build the HTML for this day:
382 $tpl = new RainTPL();
383 $tpl->assign('title', $conf->get('general.title'));
384 $tpl->assign('daydate', $dayDate->getTimestamp());
385 $tpl->assign('absurl', $absurl);
386 $tpl->assign('links', $formattedBookmarks);
387 $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
388 $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
389 $tpl->assign('index_url', $pageaddr);
390 $html = $tpl->draw('dailyrss', true);
391
392 echo $html . PHP_EOL;
393 }
394 echo '</channel></rss><!-- Cached version of '. escape(page_url($_SERVER)) .' -->';
395
396 $cache->cache(ob_get_contents());
397 ob_end_flush();
398 exit;
399}
400
401/**
402 * Show the 'Daily' page.
403 *
404 * @param PageBuilder $pageBuilder Template engine wrapper.
405 * @param BookmarkServiceInterface $bookmarkService instance.
406 * @param ConfigManager $conf Configuration Manager instance.
407 * @param PluginManager $pluginManager Plugin Manager instance.
408 * @param LoginManager $loginManager Login Manager instance
409 */
410function showDaily($pageBuilder, $bookmarkService, $conf, $pluginManager, $loginManager)
411{
412 if (isset($_GET['day'])) {
413 $day = $_GET['day'];
414 if ($day === date('Ymd', strtotime('now'))) {
415 $pageBuilder->assign('dayDesc', t('Today'));
416 } elseif ($day === date('Ymd', strtotime('-1 days'))) {
417 $pageBuilder->assign('dayDesc', t('Yesterday'));
418 }
419 } else {
420 $day = date('Ymd', strtotime('now')); // Today, in format YYYYMMDD.
421 $pageBuilder->assign('dayDesc', t('Today'));
422 }
423
424 $days = $bookmarkService->days();
425 $i = array_search($day, $days);
426 if ($i === false && count($days)) {
427 // no bookmarks for day, but at least one day with bookmarks
428 $i = count($days) - 1;
429 $day = $days[$i];
430 }
431 $previousday = '';
432 $nextday = '';
433
434 if ($i !== false) {
435 if ($i >= 1) {
436 $previousday = $days[$i - 1];
437 }
438 if ($i < count($days) - 1) {
439 $nextday = $days[$i + 1];
440 }
441 }
442 try {
443 $linksToDisplay = $bookmarkService->filterDay($day);
444 } catch (Exception $exc) {
445 error_log($exc);
446 $linksToDisplay = [];
447 }
448
449 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
450 $formatter = $factory->getFormatter();
451 // We pre-format some fields for proper output.
452 foreach ($linksToDisplay as $key => $bookmark) {
453 $linksToDisplay[$key] = $formatter->format($bookmark);
454 // This page is a bit specific, we need raw description to calculate the length
455 $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
456 $linksToDisplay[$key]['description'] = $bookmark->getDescription();
457 }
458
459 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
460 $data = array(
461 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
462 'linksToDisplay' => $linksToDisplay,
463 'day' => $dayDate->getTimestamp(),
464 'dayDate' => $dayDate,
465 'previousday' => $previousday,
466 'nextday' => $nextday,
467 );
468
469 /* Hook is called before column construction so that plugins don't have
470 to deal with columns. */
471 $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
472
473 /* We need to spread the articles on 3 columns.
474 I did not want to use a JavaScript lib like http://masonry.desandro.com/
475 so I manually spread entries with a simple method: I roughly evaluate the
476 height of a div according to title and description length.
477 */
478 $columns = array(array(), array(), array()); // Entries to display, for each column.
479 $fill = array(0, 0, 0); // Rough estimate of columns fill.
480 foreach ($data['linksToDisplay'] as $key => $bookmark) {
481 // Roughly estimate length of entry (by counting characters)
482 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
483 // Description: 836 characters gives roughly 342 pixel height.
484 // This is not perfect, but it's usually OK.
485 $length = strlen($bookmark['title']) + (342 * strlen($bookmark['description'])) / 836;
486 if (! empty($bookmark['thumbnail'])) {
487 $length += 100; // 1 thumbnails roughly takes 100 pixels height.
488 }
489 // Then put in column which is the less filled:
490 $smallest = min($fill); // find smallest value in array.
491 $index = array_search($smallest, $fill); // find index of this smallest value.
492 array_push($columns[$index], $bookmark); // Put entry in this column.
493 $fill[$index] += $length;
494 }
495
496 $data['cols'] = $columns;
497
498 foreach ($data as $key => $value) {
499 $pageBuilder->assign($key, $value);
500 }
501
502 $pageBuilder->assign('pagetitle', t('Daily') .' - '. $conf->get('general.title', 'Shaarli'));
503 $pageBuilder->renderPage('daily');
504 exit;
505}
506
507/**
508 * Renders the linklist
509 *
510 * @param pageBuilder $PAGE pageBuilder instance.
511 * @param BookmarkServiceInterface $linkDb instance.
512 * @param ConfigManager $conf Configuration Manager instance.
513 * @param PluginManager $pluginManager Plugin Manager instance.
514 */
515function showLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
516{
517 buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager);
518 $PAGE->renderPage('linklist');
519}
520
521/**
522 * Render HTML page (according to URL parameters and user rights)
523 *
524 * @param ConfigManager $conf Configuration Manager instance.
525 * @param PluginManager $pluginManager Plugin Manager instance,
526 * @param BookmarkServiceInterface $bookmarkService
527 * @param History $history instance
528 * @param SessionManager $sessionManager SessionManager instance
529 * @param LoginManager $loginManager LoginManager instance
530 */
531function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionManager, $loginManager)
532{
533 $updater = new Updater(
534 UpdaterUtils::read_updates_file($conf->get('resource.updates')),
535 $bookmarkService,
536 $conf,
537 $loginManager->isLoggedIn()
538 );
539 try {
540 $newUpdates = $updater->update();
541 if (! empty($newUpdates)) {
542 UpdaterUtils::write_updates_file(
543 $conf->get('resource.updates'),
544 $updater->getDoneUpdates()
545 );
546 }
547 } catch (Exception $e) {
548 die($e->getMessage());
549 }
550
551 $PAGE = new PageBuilder($conf, $_SESSION, $bookmarkService, $sessionManager->generateToken(), $loginManager->isLoggedIn());
552 $PAGE->assign('linkcount', $bookmarkService->count(BookmarkFilter::$ALL));
553 $PAGE->assign('privateLinkcount', $bookmarkService->count(BookmarkFilter::$PRIVATE));
554 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
555
556 // Determine which page will be rendered.
557 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
558 $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
559
560 if (// if the user isn't logged in
561 !$loginManager->isLoggedIn() &&
562 // and Shaarli doesn't have public content...
563 $conf->get('privacy.hide_public_links') &&
564 // and is configured to enforce the login
565 $conf->get('privacy.force_login') &&
566 // and the current page isn't already the login page
567 $targetPage !== Router::$PAGE_LOGIN &&
568 // and the user is not requesting a feed (which would lead to a different content-type as expected)
569 $targetPage !== Router::$PAGE_FEED_ATOM &&
570 $targetPage !== Router::$PAGE_FEED_RSS
571 ) {
572 // force current page to be the login page
573 $targetPage = Router::$PAGE_LOGIN;
574 }
575
576 // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
577 // Then assign generated data to RainTPL.
578 $common_hooks = array(
579 'includes',
580 'header',
581 'footer',
582 );
583
584 foreach ($common_hooks as $name) {
585 $plugin_data = array();
586 $pluginManager->executeHooks(
587 'render_' . $name,
588 $plugin_data,
589 array(
590 'target' => $targetPage,
591 'loggedin' => $loginManager->isLoggedIn()
592 )
593 );
594 $PAGE->assign('plugins_' . $name, $plugin_data);
595 }
596
597 // -------- Display login form.
598 if ($targetPage == Router::$PAGE_LOGIN) {
599 header('Location: ./login');
600 exit;
601 }
602 // -------- User wants to logout.
603 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) {
604 invalidateCaches($conf->get('resource.page_cache'));
605 $sessionManager->logout();
606 setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
607 header('Location: ?');
608 exit;
609 }
610
611 // -------- Picture wall
612 if ($targetPage == Router::$PAGE_PICWALL) {
613 $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
614 if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
615 $PAGE->assign('linksToDisplay', []);
616 $PAGE->renderPage('picwall');
617 exit;
618 }
619
620 // Optionally filter the results:
621 $links = $bookmarkService->search($_GET);
622 $linksToDisplay = [];
623
624 // Get only bookmarks which have a thumbnail.
625 // Note: we do not retrieve thumbnails here, the request is too heavy.
626 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
627 $formatter = $factory->getFormatter();
628 foreach ($links as $key => $link) {
629 if ($link->getThumbnail() !== false) {
630 $linksToDisplay[] = $formatter->format($link);
631 }
632 }
633
634 $data = [
635 'linksToDisplay' => $linksToDisplay,
636 ];
637 $pluginManager->executeHooks('render_picwall', $data, ['loggedin' => $loginManager->isLoggedIn()]);
638
639 foreach ($data as $key => $value) {
640 $PAGE->assign($key, $value);
641 }
642
643 $PAGE->renderPage('picwall');
644 exit;
645 }
646
647 // -------- Tag cloud
648 if ($targetPage == Router::$PAGE_TAGCLOUD) {
649 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
650 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
651 $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
652
653 // We sort tags alphabetically, then choose a font size according to count.
654 // First, find max value.
655 $maxcount = 0;
656 foreach ($tags as $value) {
657 $maxcount = max($maxcount, $value);
658 }
659
660 alphabetical_sort($tags, false, true);
661
662 $logMaxCount = $maxcount > 1 ? log($maxcount, 30) : 1;
663 $tagList = array();
664 foreach ($tags as $key => $value) {
665 if (in_array($key, $filteringTags)) {
666 continue;
667 }
668 // Tag font size scaling:
669 // default 15 and 30 logarithm bases affect scaling,
670 // 2.2 and 0.8 are arbitrary font sizes in em.
671 $size = log($value, 15) / $logMaxCount * 2.2 + 0.8;
672 $tagList[$key] = array(
673 'count' => $value,
674 'size' => number_format($size, 2, '.', ''),
675 );
676 }
677
678 $searchTags = implode(' ', escape($filteringTags));
679 $data = array(
680 'search_tags' => $searchTags,
681 'tags' => $tagList,
682 );
683 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
684
685 foreach ($data as $key => $value) {
686 $PAGE->assign($key, $value);
687 }
688
689 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
690 $PAGE->assign('pagetitle', $searchTags. t('Tag cloud') .' - '. $conf->get('general.title', 'Shaarli'));
691 $PAGE->renderPage('tag.cloud');
692 exit;
693 }
694
695 // -------- Tag list
696 if ($targetPage == Router::$PAGE_TAGLIST) {
697 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
698 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
699 $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
700 foreach ($filteringTags as $tag) {
701 if (array_key_exists($tag, $tags)) {
702 unset($tags[$tag]);
703 }
704 }
705
706 if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
707 alphabetical_sort($tags, false, true);
708 }
709
710 $searchTags = implode(' ', escape($filteringTags));
711 $data = [
712 'search_tags' => $searchTags,
713 'tags' => $tags,
714 ];
715 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
716
717 foreach ($data as $key => $value) {
718 $PAGE->assign($key, $value);
719 }
720
721 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
722 $PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
723 $PAGE->renderPage('tag.list');
724 exit;
725 }
726
727 // Daily page.
728 if ($targetPage == Router::$PAGE_DAILY) {
729 showDaily($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
730 }
731
732 // ATOM and RSS feed.
733 if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) {
734 $feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
735 header('Content-Type: application/'. $feedType .'+xml; charset=utf-8');
736
737 // Cache system
738 $query = $_SERVER['QUERY_STRING'];
739 $cache = new CachedPage(
740 $conf->get('resource.page_cache'),
741 page_url($_SERVER),
742 startsWith($query, 'do='. $targetPage) && !$loginManager->isLoggedIn()
743 );
744 $cached = $cache->cachedVersion();
745 if (!empty($cached)) {
746 echo $cached;
747 exit;
748 }
749
750 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
751 // Generate data.
752 $feedGenerator = new FeedBuilder(
753 $bookmarkService,
754 $factory->getFormatter(),
755 $feedType,
756 $_SERVER,
757 $_GET,
758 $loginManager->isLoggedIn()
759 );
760 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
761 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
762 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
763 $data = $feedGenerator->buildData();
764
765 // Process plugin hook.
766 $pluginManager->executeHooks('render_feed', $data, array(
767 'loggedin' => $loginManager->isLoggedIn(),
768 'target' => $targetPage,
769 ));
770
771 // Render the template.
772 $PAGE->assignAll($data);
773 $PAGE->renderPage('feed.'. $feedType);
774 $cache->cache(ob_get_contents());
775 ob_end_flush();
776 exit;
777 }
778
779 // Display opensearch plugin (XML)
780 if ($targetPage == Router::$PAGE_OPENSEARCH) {
781 header('Content-Type: application/xml; charset=utf-8');
782 $PAGE->assign('serverurl', index_url($_SERVER));
783 $PAGE->renderPage('opensearch');
784 exit;
785 }
786
787 // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
788 if (isset($_GET['addtag'])) {
789 // Get previous URL (http_referer) and add the tag to the searchtags parameters in query.
790 if (empty($_SERVER['HTTP_REFERER'])) {
791 // In case browser does not send HTTP_REFERER
792 header('Location: ?searchtags='.urlencode($_GET['addtag']));
793 exit;
794 }
795 parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
796
797 // Prevent redirection loop
798 if (isset($params['addtag'])) {
799 unset($params['addtag']);
800 }
801
802 // Check if this tag is already in the search query and ignore it if it is.
803 // Each tag is always separated by a space
804 if (isset($params['searchtags'])) {
805 $current_tags = explode(' ', $params['searchtags']);
806 } else {
807 $current_tags = array();
808 }
809 $addtag = true;
810 foreach ($current_tags as $value) {
811 if ($value === $_GET['addtag']) {
812 $addtag = false;
813 break;
814 }
815 }
816 // Append the tag if necessary
817 if (empty($params['searchtags'])) {
818 $params['searchtags'] = trim($_GET['addtag']);
819 } elseif ($addtag) {
820 $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']);
821 }
822
823 // We also remove page (keeping the same page has no sense, since the
824 // results are different)
825 unset($params['page']);
826
827 header('Location: ?'.http_build_query($params));
828 exit;
829 }
830
831 // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...)
832 if (isset($_GET['removetag'])) {
833 // Get previous URL (http_referer) and remove the tag from the searchtags parameters in query.
834 if (empty($_SERVER['HTTP_REFERER'])) {
835 header('Location: ?');
836 exit;
837 }
838
839 // In case browser does not send HTTP_REFERER
840 parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
841 70
842 // Prevent redirection loop 71$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager);
843 if (isset($params['removetag'])) { 72$container = $containerBuilder->build();
844 unset($params['removetag']); 73$app = new App($container);
845 }
846
847 if (isset($params['searchtags'])) {
848 $tags = explode(' ', $params['searchtags']);
849 // Remove value from array $tags.
850 $tags = array_diff($tags, array($_GET['removetag']));
851 $params['searchtags'] = implode(' ', $tags);
852
853 if (empty($params['searchtags'])) {
854 unset($params['searchtags']);
855 }
856
857 // We also remove page (keeping the same page has no sense, since
858 // the results are different)
859 unset($params['page']);
860 }
861 header('Location: ?'.http_build_query($params));
862 exit;
863 }
864
865 // -------- User wants to change the number of bookmarks per page (linksperpage=...)
866 if (isset($_GET['linksperpage'])) {
867 if (is_numeric($_GET['linksperpage'])) {
868 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
869 }
870
871 if (! empty($_SERVER['HTTP_REFERER'])) {
872 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
873 } else {
874 $location = '?';
875 }
876 header('Location: '. $location);
877 exit;
878 }
879
880 // -------- User wants to see only private bookmarks (toggle)
881 if (isset($_GET['visibility'])) {
882 if ($_GET['visibility'] === 'private') {
883 // Visibility not set or not already private, set private, otherwise reset it
884 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
885 // See only private bookmarks
886 $_SESSION['visibility'] = 'private';
887 } else {
888 unset($_SESSION['visibility']);
889 }
890 } elseif ($_GET['visibility'] === 'public') {
891 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
892 // See only public bookmarks
893 $_SESSION['visibility'] = 'public';
894 } else {
895 unset($_SESSION['visibility']);
896 }
897 }
898
899 if (! empty($_SERVER['HTTP_REFERER'])) {
900 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
901 } else {
902 $location = '?';
903 }
904 header('Location: '. $location);
905 exit;
906 }
907
908 // -------- User wants to see only untagged bookmarks (toggle)
909 if (isset($_GET['untaggedonly'])) {
910 $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
911
912 if (! empty($_SERVER['HTTP_REFERER'])) {
913 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
914 } else {
915 $location = '?';
916 }
917 header('Location: '. $location);
918 exit;
919 }
920
921 // -------- Handle other actions allowed for non-logged in users:
922 if (!$loginManager->isLoggedIn()) {
923 // User tries to post new link but is not logged in:
924 // Show login screen, then redirect to ?post=...
925 if (isset($_GET['post'])) {
926 header( // Redirect to login page, then back to post link.
927 'Location: /login?post='.urlencode($_GET['post']).
928 (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
929 (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
930 (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
931 (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
932 );
933 exit;
934 }
935
936 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
937 if (isset($_GET['edit_link'])) {
938 header('Location: /login?edit_link='. escape($_GET['edit_link']));
939 exit;
940 }
941
942 exit; // Never remove this one! All operations below are reserved for logged in user.
943 }
944
945 // -------- All other functions are reserved for the registered user:
946
947 // -------- Display the Tools menu if requested (import/export/bookmarklet...)
948 if ($targetPage == Router::$PAGE_TOOLS) {
949 $data = [
950 'pageabsaddr' => index_url($_SERVER),
951 'sslenabled' => is_https($_SERVER),
952 ];
953 $pluginManager->executeHooks('render_tools', $data);
954
955 foreach ($data as $key => $value) {
956 $PAGE->assign($key, $value);
957 }
958
959 $PAGE->assign('pagetitle', t('Tools') .' - '. $conf->get('general.title', 'Shaarli'));
960 $PAGE->renderPage('tools');
961 exit;
962 }
963
964 // -------- User wants to change his/her password.
965 if ($targetPage == Router::$PAGE_CHANGEPASSWORD) {
966 if ($conf->get('security.open_shaarli')) {
967 die(t('You are not supposed to change a password on an Open Shaarli.'));
968 }
969
970 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) {
971 if (!$sessionManager->checkToken($_POST['token'])) {
972 die(t('Wrong token.')); // Go away!
973 }
974
975 // Make sure old password is correct.
976 $oldhash = sha1(
977 $_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')
978 );
979 if ($oldhash != $conf->get('credentials.hash')) {
980 echo '<script>alert("'
981 . t('The old password is not correct.')
982 .'");document.location=\'?do=changepasswd\';</script>';
983 exit;
984 }
985 // Save new password
986 // Salt renders rainbow-tables attacks useless.
987 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
988 $conf->set(
989 'credentials.hash',
990 sha1(
991 $_POST['setpassword']
992 . $conf->get('credentials.login')
993 . $conf->get('credentials.salt')
994 )
995 );
996 try {
997 $conf->write($loginManager->isLoggedIn());
998 } catch (Exception $e) {
999 error_log(
1000 'ERROR while writing config file after changing password.' . PHP_EOL .
1001 $e->getMessage()
1002 );
1003
1004 // TODO: do not handle exceptions/errors in JS.
1005 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
1006 exit;
1007 }
1008 echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
1009 exit;
1010 } else {
1011 // show the change password form.
1012 $PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
1013 $PAGE->renderPage('changepassword');
1014 exit;
1015 }
1016 }
1017
1018 // -------- User wants to change configuration
1019 if ($targetPage == Router::$PAGE_CONFIGURE) {
1020 if (!empty($_POST['title'])) {
1021 if (!$sessionManager->checkToken($_POST['token'])) {
1022 die(t('Wrong token.')); // Go away!
1023 }
1024 $tz = 'UTC';
1025 if (!empty($_POST['continent']) && !empty($_POST['city'])
1026 && isTimeZoneValid($_POST['continent'], $_POST['city'])
1027 ) {
1028 $tz = $_POST['continent'] . '/' . $_POST['city'];
1029 }
1030 $conf->set('general.timezone', $tz);
1031 $conf->set('general.title', escape($_POST['title']));
1032 $conf->set('general.header_link', escape($_POST['titleLink']));
1033 $conf->set('general.retrieve_description', !empty($_POST['retrieveDescription']));
1034 $conf->set('resource.theme', escape($_POST['theme']));
1035 $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
1036 $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
1037 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
1038 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1039 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1040 $conf->set('api.enabled', !empty($_POST['enableApi']));
1041 $conf->set('api.secret', escape($_POST['apiSecret']));
1042 $conf->set('formatter', escape($_POST['formatter']));
1043
1044 if (! empty($_POST['language'])) {
1045 $conf->set('translation.language', escape($_POST['language']));
1046 }
1047
1048 $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
1049 if ($thumbnailsMode !== Thumbnailer::MODE_NONE
1050 && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
1051 ) {
1052 $_SESSION['warnings'][] = t(
1053 'You have enabled or changed thumbnails mode. '
1054 .'<a href="?do=thumbs_update">Please synchronize them</a>.'
1055 );
1056 }
1057 $conf->set('thumbnails.mode', $thumbnailsMode);
1058
1059 try {
1060 $conf->write($loginManager->isLoggedIn());
1061 $history->updateSettings();
1062 invalidateCaches($conf->get('resource.page_cache'));
1063 } catch (Exception $e) {
1064 error_log(
1065 'ERROR while writing config file after configuration update.' . PHP_EOL .
1066 $e->getMessage()
1067 );
1068
1069 // TODO: do not handle exceptions/errors in JS.
1070 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
1071 exit;
1072 }
1073 echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
1074 exit;
1075 } else {
1076 // Show the configuration form.
1077 $PAGE->assign('title', $conf->get('general.title'));
1078 $PAGE->assign('theme', $conf->get('resource.theme'));
1079 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
1080 $PAGE->assign('formatter_available', ['default', 'markdown']);
1081 list($continents, $cities) = generateTimeZoneData(
1082 timezone_identifiers_list(),
1083 $conf->get('general.timezone')
1084 );
1085 $PAGE->assign('continents', $continents);
1086 $PAGE->assign('cities', $cities);
1087 $PAGE->assign('retrieve_description', $conf->get('general.retrieve_description'));
1088 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
1089 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
1090 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
1091 $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
1092 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
1093 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
1094 $PAGE->assign('api_secret', $conf->get('api.secret'));
1095 $PAGE->assign('languages', Languages::getAvailableLanguages());
1096 $PAGE->assign('gd_enabled', extension_loaded('gd'));
1097 $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
1098 $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
1099 $PAGE->renderPage('configure');
1100 exit;
1101 }
1102 }
1103
1104 // -------- User wants to rename a tag or delete it
1105 if ($targetPage == Router::$PAGE_CHANGETAG) {
1106 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
1107 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
1108 $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
1109 $PAGE->renderPage('changetag');
1110 exit;
1111 }
1112
1113 if (!$sessionManager->checkToken($_POST['token'])) {
1114 die(t('Wrong token.'));
1115 }
1116
1117 $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
1118 $fromTag = escape($_POST['fromtag']);
1119 $count = 0;
1120 $bookmarks = $bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
1121 foreach ($bookmarks as $bookmark) {
1122 if ($toTag) {
1123 $bookmark->renameTag($fromTag, $toTag);
1124 } else {
1125 $bookmark->deleteTag($fromTag);
1126 }
1127 $bookmarkService->set($bookmark, false);
1128 $history->updateLink($bookmark);
1129 $count++;
1130 }
1131 $bookmarkService->save();
1132 $delete = empty($_POST['totag']);
1133 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
1134 $alert = $delete
1135 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d bookmarks.', $count), $count)
1136 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d bookmarks.', $count), $count);
1137 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
1138 exit;
1139 }
1140
1141 // -------- User wants to add a link without using the bookmarklet: Show form.
1142 if ($targetPage == Router::$PAGE_ADDLINK) {
1143 $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
1144 $PAGE->renderPage('addlink');
1145 exit;
1146 }
1147
1148 // -------- User clicked the "Save" button when editing a link: Save link to database.
1149 if (isset($_POST['save_edit'])) {
1150 // Go away!
1151 if (! $sessionManager->checkToken($_POST['token'])) {
1152 die(t('Wrong token.'));
1153 }
1154
1155 // lf_id should only be present if the link exists.
1156 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null;
1157 if ($id && $bookmarkService->exists($id)) {
1158 // Edit
1159 $bookmark = $bookmarkService->get($id);
1160 } else {
1161 // New link
1162 $bookmark = new Bookmark();
1163 }
1164
1165 $bookmark->setTitle($_POST['lf_title']);
1166 $bookmark->setDescription($_POST['lf_description']);
1167 $bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols'));
1168 $bookmark->setPrivate(isset($_POST['lf_private']));
1169 $bookmark->setTagsString($_POST['lf_tags']);
1170
1171 if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
1172 && ! $bookmark->isNote()
1173 ) {
1174 $thumbnailer = new Thumbnailer($conf);
1175 $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
1176 }
1177 $bookmarkService->addOrSet($bookmark, false);
1178
1179 // To preserve backward compatibility with 3rd parties, plugins still use arrays
1180 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1181 $formatter = $factory->getFormatter('raw');
1182 $data = $formatter->format($bookmark);
1183 $pluginManager->executeHooks('save_link', $data);
1184
1185 $bookmark->fromArray($data);
1186 $bookmarkService->set($bookmark);
1187
1188 // If we are called from the bookmarklet, we must close the popup:
1189 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1190 echo '<script>self.close();</script>';
1191 exit;
1192 }
1193
1194 $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
1195 $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
1196 // Scroll to the link which has been edited.
1197 $location .= '#' . $bookmark->getShortUrl();
1198 // After saving the link, redirect to the page the user was on.
1199 header('Location: '. $location);
1200 exit;
1201 }
1202
1203 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1204 if ($targetPage == Router::$PAGE_DELETELINK) {
1205 if (! $sessionManager->checkToken($_GET['token'])) {
1206 die(t('Wrong token.'));
1207 }
1208
1209 $ids = trim($_GET['lf_linkdate']);
1210 if (strpos($ids, ' ') !== false) {
1211 // multiple, space-separated ids provided
1212 $ids = array_values(array_filter(
1213 preg_split('/\s+/', escape($ids)),
1214 function ($item) {
1215 return $item !== '';
1216 }
1217 ));
1218 } else {
1219 // only a single id provided
1220 $shortUrl = $bookmarkService->get($ids)->getShortUrl();
1221 $ids = [$ids];
1222 }
1223 // assert at least one id is given
1224 if (!count($ids)) {
1225 die('no id provided');
1226 }
1227 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1228 $formatter = $factory->getFormatter('raw');
1229 foreach ($ids as $id) {
1230 $id = (int) escape($id);
1231 $bookmark = $bookmarkService->get($id);
1232 $data = $formatter->format($bookmark);
1233 $pluginManager->executeHooks('delete_link', $data);
1234 $bookmarkService->remove($bookmark, false);
1235 }
1236 $bookmarkService->save();
1237
1238 // If we are called from the bookmarklet, we must close the popup:
1239 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1240 echo '<script>self.close();</script>';
1241 exit;
1242 }
1243
1244 $location = '?';
1245 if (isset($_SERVER['HTTP_REFERER'])) {
1246 // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404.
1247 $location = generateLocation(
1248 $_SERVER['HTTP_REFERER'],
1249 $_SERVER['HTTP_HOST'],
1250 ['delete_link', 'edit_link', ! empty($shortUrl) ? $shortUrl : null]
1251 );
1252 }
1253
1254 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
1255 exit;
1256 }
1257
1258 // -------- User clicked either "Set public" or "Set private" bulk operation
1259 if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) {
1260 if (! $sessionManager->checkToken($_GET['token'])) {
1261 die(t('Wrong token.'));
1262 }
1263
1264 $ids = trim($_GET['ids']);
1265 if (strpos($ids, ' ') !== false) {
1266 // multiple, space-separated ids provided
1267 $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
1268 } else {
1269 // only a single id provided
1270 $ids = [$ids];
1271 }
1272
1273 // assert at least one id is given
1274 if (!count($ids)) {
1275 die('no id provided');
1276 }
1277 // assert that the visibility is valid
1278 if (!isset($_GET['newVisibility']) || !in_array($_GET['newVisibility'], ['public', 'private'])) {
1279 die('invalid visibility');
1280 } else {
1281 $private = $_GET['newVisibility'] === 'private';
1282 }
1283 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1284 $formatter = $factory->getFormatter('raw');
1285 foreach ($ids as $id) {
1286 $id = (int) escape($id);
1287 $bookmark = $bookmarkService->get($id);
1288 $bookmark->setPrivate($private);
1289
1290 // To preserve backward compatibility with 3rd parties, plugins still use arrays
1291 $data = $formatter->format($bookmark);
1292 $pluginManager->executeHooks('save_link', $data);
1293 $bookmark->fromArray($data);
1294
1295 $bookmarkService->set($bookmark);
1296 }
1297 $bookmarkService->save();
1298
1299 $location = '?';
1300 if (isset($_SERVER['HTTP_REFERER'])) {
1301 $location = generateLocation(
1302 $_SERVER['HTTP_REFERER'],
1303 $_SERVER['HTTP_HOST']
1304 );
1305 }
1306 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
1307 exit;
1308 }
1309
1310 // -------- User clicked the "EDIT" button on a link: Display link edit form.
1311 if (isset($_GET['edit_link'])) {
1312 $id = (int) escape($_GET['edit_link']);
1313 try {
1314 $link = $bookmarkService->get($id); // Read database
1315 } catch (BookmarkNotFoundException $e) {
1316 // Link not found in database.
1317 header('Location: ?');
1318 exit;
1319 }
1320
1321 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1322 $formatter = $factory->getFormatter('raw');
1323 $formattedLink = $formatter->format($link);
1324 $tags = $bookmarkService->bookmarksCountPerTag();
1325 if ($conf->get('formatter') === 'markdown') {
1326 $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
1327 }
1328 $data = array(
1329 'link' => $formattedLink,
1330 'link_is_new' => false,
1331 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1332 'tags' => $tags,
1333 );
1334 $pluginManager->executeHooks('render_editlink', $data);
1335
1336 foreach ($data as $key => $value) {
1337 $PAGE->assign($key, $value);
1338 }
1339
1340 $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1341 $PAGE->renderPage('editlink');
1342 exit;
1343 }
1344
1345 // -------- User want to post a new link: Display link edit form.
1346 if (isset($_GET['post'])) {
1347 $url = cleanup_url($_GET['post']);
1348
1349 $link_is_new = false;
1350 // Check if URL is not already in database (in this case, we will edit the existing link)
1351 $bookmark = $bookmarkService->findByUrl($url);
1352 if (! $bookmark) {
1353 $link_is_new = true;
1354 // Get title if it was provided in URL (by the bookmarklet).
1355 $title = empty($_GET['title']) ? '' : escape($_GET['title']);
1356 // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
1357 $description = empty($_GET['description']) ? '' : escape($_GET['description']);
1358 $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
1359 $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
1360
1361 // If this is an HTTP(S) link, we try go get the page to extract
1362 // the title (otherwise we will to straight to the edit form.)
1363 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
1364 $retrieveDescription = $conf->get('general.retrieve_description');
1365 // Short timeout to keep the application responsive
1366 // The callback will fill $charset and $title with data from the downloaded page.
1367 get_http_response(
1368 $url,
1369 $conf->get('general.download_timeout', 30),
1370 $conf->get('general.download_max_size', 4194304),
1371 get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
1372 );
1373 if (! empty($title) && strtolower($charset) != 'utf-8') {
1374 $title = mb_convert_encoding($title, 'utf-8', $charset);
1375 }
1376 }
1377
1378 if ($url == '') {
1379 $title = $conf->get('general.default_note_title', t('Note: '));
1380 }
1381 $url = escape($url);
1382 $title = escape($title);
1383
1384 $link = [
1385 'title' => $title,
1386 'url' => $url,
1387 'description' => $description,
1388 'tags' => $tags,
1389 'private' => $private,
1390 ];
1391 } else {
1392 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1393 $formatter = $factory->getFormatter('raw');
1394 $link = $formatter->format($bookmark);
1395 }
1396
1397 $tags = $bookmarkService->bookmarksCountPerTag();
1398 if ($conf->get('formatter') === 'markdown') {
1399 $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
1400 }
1401 $data = [
1402 'link' => $link,
1403 'link_is_new' => $link_is_new,
1404 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1405 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
1406 'tags' => $tags,
1407 'default_private_links' => $conf->get('privacy.default_private_links', false),
1408 ];
1409 $pluginManager->executeHooks('render_editlink', $data);
1410
1411 foreach ($data as $key => $value) {
1412 $PAGE->assign($key, $value);
1413 }
1414
1415 $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1416 $PAGE->renderPage('editlink');
1417 exit;
1418 }
1419
1420 if ($targetPage == Router::$PAGE_PINLINK) {
1421 if (! isset($_GET['id']) || !$bookmarkService->exists($_GET['id'])) {
1422 // FIXME! Use a proper error system.
1423 $msg = t('Invalid link ID provided');
1424 echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>';
1425 exit;
1426 }
1427 if (! $sessionManager->checkToken($_GET['token'])) {
1428 die('Wrong token.');
1429 }
1430
1431 $link = $bookmarkService->get($_GET['id']);
1432 $link->setSticky(! $link->isSticky());
1433 $bookmarkService->set($link);
1434 header('Location: '.index_url($_SERVER));
1435 exit;
1436 }
1437
1438 if ($targetPage == Router::$PAGE_EXPORT) {
1439 // Export bookmarks as a Netscape Bookmarks file
1440
1441 if (empty($_GET['selection'])) {
1442 $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
1443 $PAGE->renderPage('export');
1444 exit;
1445 }
1446
1447 // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
1448 $selection = $_GET['selection'];
1449 if (isset($_GET['prepend_note_url'])) {
1450 $prependNoteUrl = $_GET['prepend_note_url'];
1451 } else {
1452 $prependNoteUrl = false;
1453 }
1454
1455 try {
1456 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1457 $formatter = $factory->getFormatter('raw');
1458 $PAGE->assign(
1459 'links',
1460 NetscapeBookmarkUtils::filterAndFormat(
1461 $bookmarkService,
1462 $formatter,
1463 $selection,
1464 $prependNoteUrl,
1465 index_url($_SERVER)
1466 )
1467 );
1468 } catch (Exception $exc) {
1469 header('Content-Type: text/plain; charset=utf-8');
1470 echo $exc->getMessage();
1471 exit;
1472 }
1473 $now = new DateTime();
1474 header('Content-Type: text/html; charset=utf-8');
1475 header(
1476 'Content-disposition: attachment; filename=bookmarks_'
1477 .$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
1478 );
1479 $PAGE->assign('date', $now->format(DateTime::RFC822));
1480 $PAGE->assign('eol', PHP_EOL);
1481 $PAGE->assign('selection', $selection);
1482 $PAGE->renderPage('export.bookmarks');
1483 exit;
1484 }
1485
1486 if ($targetPage == Router::$PAGE_IMPORT) {
1487 // Upload a Netscape bookmark dump to import its contents
1488
1489 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1490 // Show import dialog
1491 $PAGE->assign(
1492 'maxfilesize',
1493 get_max_upload_size(
1494 ini_get('post_max_size'),
1495 ini_get('upload_max_filesize'),
1496 false
1497 )
1498 );
1499 $PAGE->assign(
1500 'maxfilesizeHuman',
1501 get_max_upload_size(
1502 ini_get('post_max_size'),
1503 ini_get('upload_max_filesize'),
1504 true
1505 )
1506 );
1507 $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli'));
1508 $PAGE->renderPage('import');
1509 exit;
1510 }
1511
1512 // Import bookmarks from an uploaded file
1513 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1514 // The file is too big or some form field may be missing.
1515 $msg = sprintf(
1516 t(
1517 'The file you are trying to upload is probably bigger than what this webserver can accept'
1518 .' (%s). Please upload in smaller chunks.'
1519 ),
1520 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
1521 );
1522 echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
1523 exit;
1524 }
1525 if (! $sessionManager->checkToken($_POST['token'])) {
1526 die('Wrong token.');
1527 }
1528 $status = NetscapeBookmarkUtils::import(
1529 $_POST,
1530 $_FILES,
1531 $bookmarkService,
1532 $conf,
1533 $history
1534 );
1535 echo '<script>alert("'.$status.'");document.location=\'?do='
1536 .Router::$PAGE_IMPORT .'\';</script>';
1537 exit;
1538 }
1539
1540 // Plugin administration page
1541 if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
1542 $pluginMeta = $pluginManager->getPluginsMeta();
1543
1544 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
1545 $enabledPlugins = array_filter($pluginMeta, function ($v) {
1546 return $v['order'] !== false;
1547 });
1548 // Load parameters.
1549 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array()));
1550 uasort(
1551 $enabledPlugins,
1552 function ($a, $b) {
1553 return $a['order'] - $b['order'];
1554 }
1555 );
1556 $disabledPlugins = array_filter($pluginMeta, function ($v) {
1557 return $v['order'] === false;
1558 });
1559
1560 $PAGE->assign('enabledPlugins', $enabledPlugins);
1561 $PAGE->assign('disabledPlugins', $disabledPlugins);
1562 $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
1563 $PAGE->renderPage('pluginsadmin');
1564 exit;
1565 }
1566
1567 // Plugin administration form action
1568 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
1569 try {
1570 if (isset($_POST['parameters_form'])) {
1571 $pluginManager->executeHooks('save_plugin_parameters', $_POST);
1572 unset($_POST['parameters_form']);
1573 foreach ($_POST as $param => $value) {
1574 $conf->set('plugins.'. $param, escape($value));
1575 }
1576 } else {
1577 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
1578 }
1579 $conf->write($loginManager->isLoggedIn());
1580 $history->updateSettings();
1581 } catch (Exception $e) {
1582 error_log(
1583 'ERROR while saving plugin configuration:.' . PHP_EOL .
1584 $e->getMessage()
1585 );
1586
1587 // TODO: do not handle exceptions/errors in JS.
1588 echo '<script>alert("'
1589 . $e->getMessage()
1590 .'");document.location=\'?do='
1591 . Router::$PAGE_PLUGINSADMIN
1592 .'\';</script>';
1593 exit;
1594 }
1595 header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
1596 exit;
1597 }
1598
1599 // Get a fresh token
1600 if ($targetPage == Router::$GET_TOKEN) {
1601 header('Content-Type:text/plain');
1602 echo $sessionManager->generateToken();
1603 exit;
1604 }
1605
1606 // -------- Thumbnails Update
1607 if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
1608 $ids = [];
1609 foreach ($bookmarkService->search() as $bookmark) {
1610 // A note or not HTTP(S)
1611 if ($bookmark->isNote() || ! startsWith(strtolower($bookmark->getUrl()), 'http')) {
1612 continue;
1613 }
1614 $ids[] = $bookmark->getId();
1615 }
1616 $PAGE->assign('ids', $ids);
1617 $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
1618 $PAGE->renderPage('thumbnails');
1619 exit;
1620 }
1621
1622 // -------- Single Thumbnail Update
1623 if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
1624 if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
1625 http_response_code(400);
1626 exit;
1627 }
1628 $id = (int) $_POST['id'];
1629 if (! $bookmarkService->exists($id)) {
1630 http_response_code(404);
1631 exit;
1632 }
1633 $thumbnailer = new Thumbnailer($conf);
1634 $bookmark = $bookmarkService->get($id);
1635 $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
1636 $bookmarkService->set($bookmark);
1637
1638 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1639 echo json_encode($factory->getFormatter('raw')->format($bookmark));
1640 exit;
1641 }
1642
1643 // -------- Otherwise, simply display search form and bookmarks:
1644 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
1645 exit;
1646}
1647
1648/**
1649 * Template for the list of bookmarks (<div id="linklist">)
1650 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1651 *
1652 * @param pageBuilder $PAGE pageBuilder instance.
1653 * @param BookmarkServiceInterface $linkDb LinkDB instance.
1654 * @param ConfigManager $conf Configuration Manager instance.
1655 * @param PluginManager $pluginManager Plugin Manager instance.
1656 * @param LoginManager $loginManager LoginManager instance
1657 */
1658function buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
1659{
1660 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1661 $formatter = $factory->getFormatter();
1662
1663 // Used in templates
1664 if (isset($_GET['searchtags'])) {
1665 if (! empty($_GET['searchtags'])) {
1666 $searchtags = escape(normalize_spaces($_GET['searchtags']));
1667 } else {
1668 $searchtags = false;
1669 }
1670 } else {
1671 $searchtags = '';
1672 }
1673 $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : '';
1674
1675 // Smallhash filter
1676 if (! empty($_SERVER['QUERY_STRING'])
1677 && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
1678 try {
1679 $linksToDisplay = $linkDb->findByHash($_SERVER['QUERY_STRING']);
1680 } catch (BookmarkNotFoundException $e) {
1681 $PAGE->render404($e->getMessage());
1682 exit;
1683 }
1684 } else {
1685 // Filter bookmarks according search parameters.
1686 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : null;
1687 $request = [
1688 'searchtags' => $searchtags,
1689 'searchterm' => $searchterm,
1690 ];
1691 $linksToDisplay = $linkDb->search($request, $visibility, false, !empty($_SESSION['untaggedonly']));
1692 }
1693
1694 // ---- Handle paging.
1695 $keys = array();
1696 foreach ($linksToDisplay as $key => $value) {
1697 $keys[] = $key;
1698 }
1699
1700 // Select articles according to paging.
1701 $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
1702 $pagecount = $pagecount == 0 ? 1 : $pagecount;
1703 $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
1704 $page = $page < 1 ? 1 : $page;
1705 $page = $page > $pagecount ? $pagecount : $page;
1706 // Start index.
1707 $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
1708 $end = $i + $_SESSION['LINKS_PER_PAGE'];
1709
1710 $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
1711 if ($thumbnailsEnabled) {
1712 $thumbnailer = new Thumbnailer($conf);
1713 }
1714
1715 $linkDisp = array();
1716 while ($i<$end && $i<count($keys)) {
1717 $link = $formatter->format($linksToDisplay[$keys[$i]]);
1718
1719 // Logged in, thumbnails enabled, not a note,
1720 // and (never retrieved yet or no valid cache file)
1721 if ($loginManager->isLoggedIn()
1722 && $thumbnailsEnabled
1723 && !$linksToDisplay[$keys[$i]]->isNote()
1724 && $linksToDisplay[$keys[$i]]->getThumbnail() !== false
1725 && ! is_file($linksToDisplay[$keys[$i]]->getThumbnail())
1726 ) {
1727 $linksToDisplay[$keys[$i]]->setThumbnail($thumbnailer->get($link['url']));
1728 $linkDb->set($linksToDisplay[$keys[$i]], false);
1729 $updateDB = true;
1730 $link['thumbnail'] = $linksToDisplay[$keys[$i]]->getThumbnail();
1731 }
1732
1733 // Check for both signs of a note: starting with ? and 7 chars long.
1734// if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
1735// $link['url'] = index_url($_SERVER) . $link['url'];
1736// }
1737
1738 $linkDisp[$keys[$i]] = $link;
1739 $i++;
1740 }
1741
1742 // If we retrieved new thumbnails, we update the database.
1743 if (!empty($updateDB)) {
1744 $linkDb->save();
1745 }
1746 74
1747 // Compute paging navigation 75// Main Shaarli routes
1748 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); 76$app->group('', function () {
1749 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); 77 $this->get('/install', '\Shaarli\Front\Controller\Visitor\InstallController:index')->setName('displayInstall');
1750 $previous_page_url = ''; 78 $this->get('/install/session-test', '\Shaarli\Front\Controller\Visitor\InstallController:sessionTest');
1751 if ($i != count($keys)) { 79 $this->post('/install', '\Shaarli\Front\Controller\Visitor\InstallController:save')->setName('saveInstall');
1752 $previous_page_url = '?page=' . ($page+1) . $searchtermUrl . $searchtagsUrl; 80
1753 } 81 /* -- PUBLIC --*/
1754 $next_page_url=''; 82 $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index');
1755 if ($page>1) { 83 $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink');
1756 $next_page_url = '?page=' . ($page-1) . $searchtermUrl . $searchtagsUrl; 84 $this->get('/login', '\Shaarli\Front\Controller\Visitor\LoginController:index')->setName('login');
1757 } 85 $this->post('/login', '\Shaarli\Front\Controller\Visitor\LoginController:login')->setName('processLogin');
86 $this->get('/picture-wall', '\Shaarli\Front\Controller\Visitor\PictureWallController:index');
87 $this->get('/tags/cloud', '\Shaarli\Front\Controller\Visitor\TagCloudController:cloud');
88 $this->get('/tags/list', '\Shaarli\Front\Controller\Visitor\TagCloudController:list');
89 $this->get('/daily', '\Shaarli\Front\Controller\Visitor\DailyController:index');
90 $this->get('/daily-rss', '\Shaarli\Front\Controller\Visitor\DailyController:rss')->setName('rss');
91 $this->get('/feed/atom', '\Shaarli\Front\Controller\Visitor\FeedController:atom')->setName('atom');
92 $this->get('/feed/rss', '\Shaarli\Front\Controller\Visitor\FeedController:rss');
93 $this->get('/open-search', '\Shaarli\Front\Controller\Visitor\OpenSearchController:index');
94
95 $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\Visitor\TagController:addTag');
96 $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\Visitor\TagController:removeTag');
97 $this->get('/links-per-page', '\Shaarli\Front\Controller\Visitor\PublicSessionFilterController:linksPerPage');
98 $this->get('/untagged-only', '\Shaarli\Front\Controller\Admin\PublicSessionFilterController:untaggedOnly');
99})->add('\Shaarli\Front\ShaarliMiddleware');
1758 100
1759 // Fill all template fields. 101$app->group('/admin', function () {
1760 $data = array( 102 $this->get('/logout', '\Shaarli\Front\Controller\Admin\LogoutController:index');
1761 'previous_page_url' => $previous_page_url, 103 $this->get('/tools', '\Shaarli\Front\Controller\Admin\ToolsController:index');
1762 'next_page_url' => $next_page_url, 104 $this->get('/password', '\Shaarli\Front\Controller\Admin\PasswordController:index');
1763 'page_current' => $page, 105 $this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change');
1764 'page_max' => $pagecount, 106 $this->get('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:index');
1765 'result_count' => count($linksToDisplay), 107 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
1766 'search_term' => $searchterm, 108 $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
1767 'search_tags' => $searchtags, 109 $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
1768 'visibility' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '', 110 $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare');
1769 'links' => $linkDisp, 111 $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm');
112 $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm');
113 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save');
114 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark');
115 $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility');
116 $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark');
117 $this->patch(
118 '/shaare/{id:[0-9]+}/update-thumbnail',
119 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
1770 ); 120 );
121 $this->get('/export', '\Shaarli\Front\Controller\Admin\ExportController:index');
122 $this->post('/export', '\Shaarli\Front\Controller\Admin\ExportController:export');
123 $this->get('/import', '\Shaarli\Front\Controller\Admin\ImportController:index');
124 $this->post('/import', '\Shaarli\Front\Controller\Admin\ImportController:import');
125 $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
126 $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
127 $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
128 $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
1771 129
1772 // If there is only a single link, we change on-the-fly the title of the page. 130 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
1773 if (count($linksToDisplay) == 1) { 131})->add('\Shaarli\Front\ShaarliAdminMiddleware');
1774 $data['pagetitle'] = $linksToDisplay[$keys[0]]->getTitle() .' - '. $conf->get('general.title');
1775 } elseif (! empty($searchterm) || ! empty($searchtags)) {
1776 $data['pagetitle'] = t('Search: ');
1777 $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
1778 $bracketWrap = function ($tag) {
1779 return '['. $tag .']';
1780 };
1781 $data['pagetitle'] .= ! empty($searchtags)
1782 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchtags))).' '
1783 : '';
1784 $data['pagetitle'] .= '- '. $conf->get('general.title');
1785 }
1786
1787 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
1788
1789 foreach ($data as $key => $value) {
1790 $PAGE->assign($key, $value);
1791 }
1792
1793 return;
1794}
1795
1796/**
1797 * Installation
1798 * This function should NEVER be called if the file data/config.php exists.
1799 *
1800 * @param ConfigManager $conf Configuration Manager instance.
1801 * @param SessionManager $sessionManager SessionManager instance
1802 * @param LoginManager $loginManager LoginManager instance
1803 */
1804function install($conf, $sessionManager, $loginManager)
1805{
1806 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
1807 if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) {
1808 mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705);
1809 }
1810
1811
1812 // This part makes sure sessions works correctly.
1813 // (Because on some hosts, session.save_path may not be set correctly,
1814 // or we may not have write access to it.)
1815 if (isset($_GET['test_session'])
1816 && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) {
1817 // Step 2: Check if data in session is correct.
1818 $msg = t(
1819 '<pre>Sessions do not seem to work correctly on your server.<br>'.
1820 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
1821 'and that you have write access to it.<br>'.
1822 'It currently points to %s.<br>'.
1823 'On some browsers, accessing your server via a hostname like \'localhost\' '.
1824 'or any custom hostname without a dot causes cookie storage to fail. '.
1825 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
1826 );
1827 $msg = sprintf($msg, session_save_path());
1828 echo $msg;
1829 echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
1830 die;
1831 }
1832 if (!isset($_SESSION['session_tested'])) {
1833 // Step 1 : Try to store data in session and reload page.
1834 $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session.
1835 header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data.
1836 }
1837 if (isset($_GET['test_session'])) {
1838 // Step 3: Sessions are OK. Remove test parameter from URL.
1839 header('Location: '.index_url($_SERVER));
1840 }
1841
1842
1843 if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) {
1844 $tz = 'UTC';
1845 if (!empty($_POST['continent']) && !empty($_POST['city'])
1846 && isTimeZoneValid($_POST['continent'], $_POST['city'])
1847 ) {
1848 $tz = $_POST['continent'].'/'.$_POST['city'];
1849 }
1850 $conf->set('general.timezone', $tz);
1851 $login = $_POST['setlogin'];
1852 $conf->set('credentials.login', $login);
1853 $salt = sha1(uniqid('', true) .'_'. mt_rand());
1854 $conf->set('credentials.salt', $salt);
1855 $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
1856 if (!empty($_POST['title'])) {
1857 $conf->set('general.title', escape($_POST['title']));
1858 } else {
1859 $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER)));
1860 }
1861 $conf->set('translation.language', escape($_POST['language']));
1862 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1863 $conf->set('api.enabled', !empty($_POST['enableApi']));
1864 $conf->set(
1865 'api.secret',
1866 generate_api_secret(
1867 $conf->get('credentials.login'),
1868 $conf->get('credentials.salt')
1869 )
1870 );
1871 try {
1872 // Everything is ok, let's create config file.
1873 $conf->write($loginManager->isLoggedIn());
1874 } catch (Exception $e) {
1875 error_log(
1876 'ERROR while writing config file after installation.' . PHP_EOL .
1877 $e->getMessage()
1878 );
1879
1880 // TODO: do not handle exceptions/errors in JS.
1881 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
1882 exit;
1883 }
1884
1885 $history = new History($conf->get('resource.history'));
1886 $bookmarkService = new BookmarkFileService($conf, $history, true);
1887 if ($bookmarkService->count() === 0) {
1888 $bookmarkService->initialize();
1889 }
1890 132
1891 echo '<script>alert('
1892 .'"Shaarli is now configured. '
1893 .'Please enter your login/password and start shaaring your bookmarks!"'
1894 .');document.location=\'./login\';</script>';
1895 exit;
1896 }
1897
1898 $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
1899 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
1900 $PAGE->assign('continents', $continents);
1901 $PAGE->assign('cities', $cities);
1902 $PAGE->assign('languages', Languages::getAvailableLanguages());
1903 $PAGE->renderPage('install');
1904 exit;
1905}
1906
1907if (!isset($_SESSION['LINKS_PER_PAGE'])) {
1908 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
1909}
1910
1911try {
1912 $history = new History($conf->get('resource.history'));
1913} catch (Exception $e) {
1914 die($e->getMessage());
1915}
1916
1917$linkDb = new BookmarkFileService($conf, $history, $loginManager->isLoggedIn());
1918
1919if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
1920 showDailyRSS($linkDb, $conf, $loginManager);
1921 exit;
1922}
1923
1924$containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager);
1925$container = $containerBuilder->build();
1926$app = new App($container);
1927 133
1928// REST API routes 134// REST API routes
1929$app->group('/api/v1', function () { 135$app->group('/api/v1', function () {
@@ -1942,25 +148,6 @@ $app->group('/api/v1', function () {
1942 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); 148 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
1943})->add('\Shaarli\Api\ApiMiddleware'); 149})->add('\Shaarli\Api\ApiMiddleware');
1944 150
1945$app->group('', function () {
1946 $this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login');
1947})->add('\Shaarli\Front\ShaarliMiddleware');
1948
1949$response = $app->run(true); 151$response = $app->run(true);
1950 152
1951// Hack to make Slim and Shaarli router work together: 153$app->respond($response);
1952// If a Slim route isn't found and NOT API call, we call renderPage().
1953if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
1954 // We use UTF-8 for proper international characters handling.
1955 header('Content-Type: text/html; charset=utf-8');
1956 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
1957} else {
1958 $response = $response
1959 ->withHeader('Access-Control-Allow-Origin', '*')
1960 ->withHeader(
1961 'Access-Control-Allow-Headers',
1962 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
1963 )
1964 ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
1965 $app->respond($response);
1966}
diff --git a/init.php b/init.php
new file mode 100644
index 00000000..f0b84368
--- /dev/null
+++ b/init.php
@@ -0,0 +1,85 @@
1<?php
2
3require_once __DIR__ . '/vendor/autoload.php';
4
5use Shaarli\ApplicationUtils;
6use Shaarli\Security\SessionManager;
7
8// Set 'UTC' as the default timezone if it is not defined in php.ini
9// See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
10if (date_default_timezone_get() == '') {
11 date_default_timezone_set('UTC');
12}
13
14// High execution time in case of problematic imports/exports.
15ini_set('max_input_time', '60');
16
17// Try to set max upload file size and read
18ini_set('memory_limit', '128M');
19ini_set('post_max_size', '16M');
20ini_set('upload_max_filesize', '16M');
21
22// See all error except warnings
23error_reporting(E_ALL^E_WARNING);
24
25// 3rd-party libraries
26if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
27 header('Content-Type: text/plain; charset=utf-8');
28 echo "Error: missing Composer configuration\n\n"
29 ."If you installed Shaarli through Git or using the development branch,\n"
30 ."please refer to the installation documentation to install PHP"
31 ." dependencies using Composer:\n"
32 ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
33 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
34 exit;
35}
36
37// Ensure the PHP version is supported
38try {
39 ApplicationUtils::checkPHPVersion('7.1', PHP_VERSION);
40} catch (Exception $exc) {
41 header('Content-Type: text/plain; charset=utf-8');
42 echo $exc->getMessage();
43 exit;
44}
45
46// Force cookie path (but do not change lifetime)
47$cookie = session_get_cookie_params();
48$cookiedir = '';
49if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
50 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
51}
52// Set default cookie expiration and path.
53session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
54// Set session parameters on server side.
55// Use cookies to store session.
56ini_set('session.use_cookies', 1);
57// Force cookies for session (phpsessionID forbidden in URL).
58ini_set('session.use_only_cookies', 1);
59// Prevent PHP form using sessionID in URL if cookies are disabled.
60ini_set('session.use_trans_sid', false);
61
62define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
63
64session_name('shaarli');
65// Start session if needed (Some server auto-start sessions).
66if (session_status() == PHP_SESSION_NONE) {
67 session_start();
68}
69
70// Regenerate session ID if invalid or not defined in cookie.
71if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
72 session_regenerate_id(true);
73 $_COOKIE['shaarli'] = session_id();
74}
75
76// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
77if (! defined('LC_MESSAGES')) {
78 define('LC_MESSAGES', LC_COLLATE);
79}
80
81// Prevent caching on client side or proxy: (yes, it's ugly)
82header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
83header("Cache-Control: no-store, no-cache, must-revalidate");
84header("Cache-Control: post-check=0, pre-check=0", false);
85header("Pragma: no-cache");
diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php
index 8bf4ed46..ab6ed6de 100644
--- a/plugins/addlink_toolbar/addlink_toolbar.php
+++ b/plugins/addlink_toolbar/addlink_toolbar.php
@@ -5,7 +5,7 @@
5 * Adds the addlink input on the linklist page. 5 * Adds the addlink input on the linklist page.
6 */ 6 */
7 7
8use Shaarli\Router; 8use Shaarli\Render\TemplatePage;
9 9
10/** 10/**
11 * When linklist is displayed, add play videos to header's toolbar. 11 * When linklist is displayed, add play videos to header's toolbar.
@@ -16,11 +16,11 @@ use Shaarli\Router;
16 */ 16 */
17function hook_addlink_toolbar_render_header($data) 17function hook_addlink_toolbar_render_header($data)
18{ 18{
19 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST && $data['_LOGGEDIN_'] === true) { 19 if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) {
20 $form = array( 20 $form = array(
21 'attr' => array( 21 'attr' => array(
22 'method' => 'GET', 22 'method' => 'GET',
23 'action' => '', 23 'action' => $data['_BASE_PATH_'] . '/admin/shaare',
24 'name' => 'addform', 24 'name' => 'addform',
25 'class' => 'addform', 25 'class' => 'addform',
26 ), 26 ),
diff --git a/plugins/archiveorg/archiveorg.html b/plugins/archiveorg/archiveorg.html
index ad501f47..e37d887e 100644
--- a/plugins/archiveorg/archiveorg.html
+++ b/plugins/archiveorg/archiveorg.html
@@ -1,5 +1,5 @@
1<span> 1<span>
2 <a href="https://web.archive.org/web/%s"> 2 <a href="https://web.archive.org/web/%s">
3 <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" /> 3 <img class="linklist-plugin-icon" src="%s/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
4 </a> 4 </a>
5</span> 5</span>
diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php
index 0ee1c73c..f26e6129 100644
--- a/plugins/archiveorg/archiveorg.php
+++ b/plugins/archiveorg/archiveorg.php
@@ -17,12 +17,13 @@ use Shaarli\Plugin\PluginManager;
17function hook_archiveorg_render_linklist($data) 17function hook_archiveorg_render_linklist($data)
18{ 18{
19 $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html'); 19 $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html');
20 $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
20 21
21 foreach ($data['links'] as &$value) { 22 foreach ($data['links'] as &$value) {
22 if ($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) { 23 if ($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
23 continue; 24 continue;
24 } 25 }
25 $archive = sprintf($archive_html, $value['url'], t('View on archive.org')); 26 $archive = sprintf($archive_html, $value['url'], $path, t('View on archive.org'));
26 $value['link_plugin'][] = $archive; 27 $value['link_plugin'][] = $archive;
27 } 28 }
28 29
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php
index 8ae1b479..defb01f7 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -16,7 +16,7 @@
16 16
17use Shaarli\Config\ConfigManager; 17use Shaarli\Config\ConfigManager;
18use Shaarli\Plugin\PluginManager; 18use Shaarli\Plugin\PluginManager;
19use Shaarli\Router; 19use Shaarli\Render\TemplatePage;
20 20
21/** 21/**
22 * In the footer hook, there is a working example of a translation extension for Shaarli. 22 * In the footer hook, there is a working example of a translation extension for Shaarli.
@@ -74,7 +74,7 @@ function demo_plugin_init($conf)
74function hook_demo_plugin_render_header($data) 74function hook_demo_plugin_render_header($data)
75{ 75{
76 // Only execute when linklist is rendered. 76 // Only execute when linklist is rendered.
77 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 77 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
78 // If loggedin 78 // If loggedin
79 if ($data['_LOGGEDIN_'] === true) { 79 if ($data['_LOGGEDIN_'] === true) {
80 /* 80 /*
@@ -118,7 +118,7 @@ function hook_demo_plugin_render_header($data)
118 $form = array( 118 $form = array(
119 'attr' => array( 119 'attr' => array(
120 'method' => 'GET', 120 'method' => 'GET',
121 'action' => '?', 121 'action' => $data['_BASE_PATH_'] . '/',
122 'class' => 'addform', 122 'class' => 'addform',
123 ), 123 ),
124 'inputs' => array( 124 'inputs' => array(
@@ -441,9 +441,9 @@ function hook_demo_plugin_delete_link($data)
441function hook_demo_plugin_render_feed($data) 441function hook_demo_plugin_render_feed($data)
442{ 442{
443 foreach ($data['links'] as &$link) { 443 foreach ($data['links'] as &$link) {
444 if ($data['_PAGE_'] == Router::$PAGE_FEED_ATOM) { 444 if ($data['_PAGE_'] == TemplatePage::FEED_ATOM) {
445 $link['description'] .= ' - ATOM Feed' ; 445 $link['description'] .= ' - ATOM Feed' ;
446 } elseif ($data['_PAGE_'] == Router::$PAGE_FEED_RSS) { 446 } elseif ($data['_PAGE_'] == TemplatePage::FEED_RSS) {
447 $link['description'] .= ' - RSS Feed'; 447 $link['description'] .= ' - RSS Feed';
448 } 448 }
449 } 449 }
diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php
index dab75dd5..16edd9a6 100644
--- a/plugins/isso/isso.php
+++ b/plugins/isso/isso.php
@@ -6,7 +6,7 @@
6 6
7use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\Plugin\PluginManager; 8use Shaarli\Plugin\PluginManager;
9use Shaarli\Router; 9use Shaarli\Render\TemplatePage;
10 10
11/** 11/**
12 * Display an error everywhere if the plugin is enabled without configuration. 12 * Display an error everywhere if the plugin is enabled without configuration.
@@ -76,7 +76,7 @@ function hook_isso_render_linklist($data, $conf)
76 */ 76 */
77function hook_isso_render_includes($data) 77function hook_isso_render_includes($data)
78{ 78{
79 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 79 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
80 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/isso/isso.css'; 80 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/isso/isso.css';
81 } 81 }
82 82
diff --git a/plugins/isso/isso_button.html b/plugins/isso/isso_button.html
deleted file mode 100644
index 3f828480..00000000
--- a/plugins/isso/isso_button.html
+++ /dev/null
@@ -1,5 +0,0 @@
1<span>
2 <a href="?%s#isso-thread">
3 <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
4 </a>
5</span>
diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php
index 0341ed59..91a9c1e5 100644
--- a/plugins/playvideos/playvideos.php
+++ b/plugins/playvideos/playvideos.php
@@ -7,7 +7,7 @@
7 */ 7 */
8 8
9use Shaarli\Plugin\PluginManager; 9use Shaarli\Plugin\PluginManager;
10use Shaarli\Router; 10use Shaarli\Render\TemplatePage;
11 11
12/** 12/**
13 * When linklist is displayed, add play videos to header's toolbar. 13 * When linklist is displayed, add play videos to header's toolbar.
@@ -18,7 +18,7 @@ use Shaarli\Router;
18 */ 18 */
19function hook_playvideos_render_header($data) 19function hook_playvideos_render_header($data)
20{ 20{
21 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 21 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
22 $playvideo = array( 22 $playvideo = array(
23 'attr' => array( 23 'attr' => array(
24 'href' => '#', 24 'href' => '#',
@@ -42,7 +42,7 @@ function hook_playvideos_render_header($data)
42 */ 42 */
43function hook_playvideos_render_footer($data) 43function hook_playvideos_render_footer($data)
44{ 44{
45 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 45 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
46 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/jquery-1.11.2.min.js'; 46 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/jquery-1.11.2.min.js';
47 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/youtube_playlist.js'; 47 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/youtube_playlist.js';
48 } 48 }
diff --git a/plugins/pubsubhubbub/pubsubhubbub.php b/plugins/pubsubhubbub/pubsubhubbub.php
index 2878c050..8fe6799c 100644
--- a/plugins/pubsubhubbub/pubsubhubbub.php
+++ b/plugins/pubsubhubbub/pubsubhubbub.php
@@ -13,7 +13,7 @@ use pubsubhubbub\publisher\Publisher;
13use Shaarli\Config\ConfigManager; 13use Shaarli\Config\ConfigManager;
14use Shaarli\Feed\FeedBuilder; 14use Shaarli\Feed\FeedBuilder;
15use Shaarli\Plugin\PluginManager; 15use Shaarli\Plugin\PluginManager;
16use Shaarli\Router; 16use Shaarli\Render\TemplatePage;
17 17
18/** 18/**
19 * Plugin init function - set the hub to the default appspot one. 19 * Plugin init function - set the hub to the default appspot one.
@@ -41,7 +41,7 @@ function pubsubhubbub_init($conf)
41 */ 41 */
42function hook_pubsubhubbub_render_feed($data, $conf) 42function hook_pubsubhubbub_render_feed($data, $conf)
43{ 43{
44 $feedType = $data['_PAGE_'] == Router::$PAGE_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
@@ -60,8 +60,8 @@ function hook_pubsubhubbub_render_feed($data, $conf)
60function hook_pubsubhubbub_save_link($data, $conf) 60function hook_pubsubhubbub_save_link($data, $conf)
61{ 61{
62 $feeds = array( 62 $feeds = array(
63 index_url($_SERVER) .'?do=atom', 63 index_url($_SERVER) .'feed/atom',
64 index_url($_SERVER) .'?do=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';
diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php
index c1d237d5..3b5dae34 100644
--- a/plugins/qrcode/qrcode.php
+++ b/plugins/qrcode/qrcode.php
@@ -6,7 +6,7 @@
6 */ 6 */
7 7
8use Shaarli\Plugin\PluginManager; 8use Shaarli\Plugin\PluginManager;
9use Shaarli\Router; 9use Shaarli\Render\TemplatePage;
10 10
11/** 11/**
12 * Add qrcode icon to link_plugin when rendering linklist. 12 * Add qrcode icon to link_plugin when rendering linklist.
@@ -19,11 +19,12 @@ function hook_qrcode_render_linklist($data)
19{ 19{
20 $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); 20 $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html');
21 21
22 $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
22 foreach ($data['links'] as &$value) { 23 foreach ($data['links'] as &$value) {
23 $qrcode = sprintf( 24 $qrcode = sprintf(
24 $qrcode_html, 25 $qrcode_html,
25 $value['url'], 26 $value['url'],
26 PluginManager::$PLUGINS_PATH 27 $path
27 ); 28 );
28 $value['link_plugin'][] = $qrcode; 29 $value['link_plugin'][] = $qrcode;
29 } 30 }
@@ -40,8 +41,8 @@ function hook_qrcode_render_linklist($data)
40 */ 41 */
41function hook_qrcode_render_footer($data) 42function hook_qrcode_render_footer($data)
42{ 43{
43 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 44 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
44 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js'; 45 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js';
45 } 46 }
46 47
47 return $data; 48 return $data;
@@ -56,7 +57,7 @@ function hook_qrcode_render_footer($data)
56 */ 57 */
57function hook_qrcode_render_includes($data) 58function hook_qrcode_render_includes($data)
58{ 59{
59 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 60 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
60 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.css'; 61 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.css';
61 } 62 }
62 63
diff --git a/plugins/qrcode/shaarli-qrcode.js b/plugins/qrcode/shaarli-qrcode.js
index fe77c4cd..3316d6f6 100644
--- a/plugins/qrcode/shaarli-qrcode.js
+++ b/plugins/qrcode/shaarli-qrcode.js
@@ -28,14 +28,15 @@
28 28
29// Show the QR-Code of a permalink (when the QR-Code icon is clicked). 29// Show the QR-Code of a permalink (when the QR-Code icon is clicked).
30function showQrCode(caller,loading) 30function showQrCode(caller,loading)
31{ 31{
32 // Dynamic javascript lib loading: We only load qr.js if the QR code icon is clicked: 32 // Dynamic javascript lib loading: We only load qr.js if the QR code icon is clicked:
33 if (typeof(qr) == 'undefined') // Load qr.js only if not present. 33 if (typeof(qr) == 'undefined') // Load qr.js only if not present.
34 { 34 {
35 if (!loading) // If javascript lib is still loading, do not append script to body. 35 if (!loading) // If javascript lib is still loading, do not append script to body.
36 { 36 {
37 var element = document.createElement("script"); 37 var basePath = document.querySelector('input[name="js_base_path"]').value;
38 element.src = "plugins/qrcode/qr-1.1.3.min.js"; 38 var element = document.createElement("script");
39 element.src = basePath + "/plugins/qrcode/qr-1.1.3.min.js";
39 document.body.appendChild(element); 40 document.body.appendChild(element);
40 } 41 }
41 setTimeout(function() { showQrCode(caller,true);}, 200); // Retry in 200 milliseconds. 42 setTimeout(function() { showQrCode(caller,true);}, 200); // Retry in 200 milliseconds.
@@ -44,7 +45,7 @@ function showQrCode(caller,loading)
44 45
45 // Remove previous qrcode if present. 46 // Remove previous qrcode if present.
46 removeQrcode(); 47 removeQrcode();
47 48
48 // Build the div which contains the QR-Code: 49 // Build the div which contains the QR-Code:
49 var element = document.createElement('div'); 50 var element = document.createElement('div');
50 element.id = 'permalinkQrcode'; 51 element.id = 'permalinkQrcode';
@@ -57,11 +58,11 @@ function showQrCode(caller,loading)
57 // Damn IE 58 // Damn IE
58 element.setAttribute('onclick', 'this.parentNode.removeChild(this);' ); 59 element.setAttribute('onclick', 'this.parentNode.removeChild(this);' );
59 } 60 }
60 61
61 // Build the QR-Code: 62 // Build the QR-Code:
62 var image = qr.image({size: 8,value: caller.dataset.permalink}); 63 var image = qr.image({size: 8,value: caller.dataset.permalink});
63 if (image) 64 if (image)
64 { 65 {
65 element.appendChild(image); 66 element.appendChild(image);
66 element.innerHTML += "<br>Click to close"; 67 element.innerHTML += "<br>Click to close";
67 caller.parentNode.appendChild(element); 68 caller.parentNode.appendChild(element);
@@ -87,4 +88,4 @@ function removeQrcode()
87 elem.parentNode.removeChild(elem); 88 elem.parentNode.removeChild(elem);
88 } 89 }
89 return false; 90 return false;
90} \ No newline at end of file 91}
diff --git a/plugins/wallabag/README.md b/plugins/wallabag/README.md
index ea21a519..c53a04d9 100644
--- a/plugins/wallabag/README.md
+++ b/plugins/wallabag/README.md
@@ -21,7 +21,7 @@ The directory structure should look like:
21 21
22To enable the plugin, you can either: 22To enable the plugin, you can either:
23 23
24 * enable it in the plugins administration page (`?do=pluginadmin`). 24 * enable it in the plugins administration page (`/admin/plugins`).
25 * add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section). 25 * add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section).
26 26
27### Configuration 27### Configuration
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php
index bc35df08..805c1ad9 100644
--- a/plugins/wallabag/wallabag.php
+++ b/plugins/wallabag/wallabag.php
@@ -45,12 +45,14 @@ function hook_wallabag_render_linklist($data, $conf)
45 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); 45 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
46 46
47 $linkTitle = t('Save to wallabag'); 47 $linkTitle = t('Save to wallabag');
48 $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
49
48 foreach ($data['links'] as &$value) { 50 foreach ($data['links'] as &$value) {
49 $wallabag = sprintf( 51 $wallabag = sprintf(
50 $wallabagHtml, 52 $wallabagHtml,
51 $wallabagInstance->getWallabagUrl(), 53 $wallabagInstance->getWallabagUrl(),
52 urlencode($value['url']), 54 urlencode($value['url']),
53 PluginManager::$PLUGINS_PATH, 55 $path,
54 $linkTitle 56 $linkTitle
55 ); 57 );
56 $value['link_plugin'][] = $wallabag; 58 $value['link_plugin'][] = $wallabag;
diff --git a/tests/api/controllers/links/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php
index c26411ac..8bb81dc8 100644
--- a/tests/api/controllers/links/GetLinkIdTest.php
+++ b/tests/api/controllers/links/GetLinkIdTest.php
@@ -102,7 +102,7 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
102 $this->assertEquals($id, $data['id']); 102 $this->assertEquals($id, $data['id']);
103 103
104 // Check link elements 104 // Check link elements
105 $this->assertEquals('http://domain.tld/?WDWyig', $data['url']); 105 $this->assertEquals('http://domain.tld/shaare/WDWyig', $data['url']);
106 $this->assertEquals('WDWyig', $data['shorturl']); 106 $this->assertEquals('WDWyig', $data['shorturl']);
107 $this->assertEquals('Link title: @website', $data['title']); 107 $this->assertEquals('Link title: @website', $data['title']);
108 $this->assertEquals( 108 $this->assertEquals(
diff --git a/tests/api/controllers/links/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php
index 4e2d55ac..d02e6fad 100644
--- a/tests/api/controllers/links/GetLinksTest.php
+++ b/tests/api/controllers/links/GetLinksTest.php
@@ -109,7 +109,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
109 109
110 // Check first element fields 110 // Check first element fields
111 $first = $data[2]; 111 $first = $data[2];
112 $this->assertEquals('http://domain.tld/?WDWyig', $first['url']); 112 $this->assertEquals('http://domain.tld/shaare/WDWyig', $first['url']);
113 $this->assertEquals('WDWyig', $first['shorturl']); 113 $this->assertEquals('WDWyig', $first['shorturl']);
114 $this->assertEquals('Link title: @website', $first['title']); 114 $this->assertEquals('Link title: @website', $first['title']);
115 $this->assertEquals( 115 $this->assertEquals(
diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php
index b2dd09eb..4e791a04 100644
--- a/tests/api/controllers/links/PostLinkTest.php
+++ b/tests/api/controllers/links/PostLinkTest.php
@@ -131,8 +131,8 @@ class PostLinkTest extends TestCase
131 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 131 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
132 $this->assertEquals(43, $data['id']); 132 $this->assertEquals(43, $data['id']);
133 $this->assertRegExp('/[\w_-]{6}/', $data['shorturl']); 133 $this->assertRegExp('/[\w_-]{6}/', $data['shorturl']);
134 $this->assertEquals('http://domain.tld/?' . $data['shorturl'], $data['url']); 134 $this->assertEquals('http://domain.tld/shaare/' . $data['shorturl'], $data['url']);
135 $this->assertEquals('?' . $data['shorturl'], $data['title']); 135 $this->assertEquals('/shaare/' . $data['shorturl'], $data['title']);
136 $this->assertEquals('', $data['description']); 136 $this->assertEquals('', $data['description']);
137 $this->assertEquals([], $data['tags']); 137 $this->assertEquals([], $data['tags']);
138 $this->assertEquals(true, $data['private']); 138 $this->assertEquals(true, $data['private']);
diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php
index cb63742e..302cac0f 100644
--- a/tests/api/controllers/links/PutLinkTest.php
+++ b/tests/api/controllers/links/PutLinkTest.php
@@ -114,8 +114,8 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
114 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 114 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
115 $this->assertEquals($id, $data['id']); 115 $this->assertEquals($id, $data['id']);
116 $this->assertEquals('WDWyig', $data['shorturl']); 116 $this->assertEquals('WDWyig', $data['shorturl']);
117 $this->assertEquals('http://domain.tld/?WDWyig', $data['url']); 117 $this->assertEquals('http://domain.tld/shaare/WDWyig', $data['url']);
118 $this->assertEquals('?WDWyig', $data['title']); 118 $this->assertEquals('/shaare/WDWyig', $data['title']);
119 $this->assertEquals('', $data['description']); 119 $this->assertEquals('', $data['description']);
120 $this->assertEquals([], $data['tags']); 120 $this->assertEquals([], $data['tags']);
121 $this->assertEquals(true, $data['private']); 121 $this->assertEquals(true, $data['private']);
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php
index 4900d41d..7b1906d3 100644
--- a/tests/bookmark/BookmarkFileServiceTest.php
+++ b/tests/bookmark/BookmarkFileServiceTest.php
@@ -200,7 +200,7 @@ class BookmarkFileServiceTest extends TestCase
200 200
201 $bookmark = $this->privateLinkDB->get(43); 201 $bookmark = $this->privateLinkDB->get(43);
202 $this->assertEquals(43, $bookmark->getId()); 202 $this->assertEquals(43, $bookmark->getId());
203 $this->assertRegExp('/\?[\w\-]{6}/', $bookmark->getUrl()); 203 $this->assertRegExp('#/shaare/[\w\-]{6}#', $bookmark->getUrl());
204 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl()); 204 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
205 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle()); 205 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
206 $this->assertEmpty($bookmark->getDescription()); 206 $this->assertEmpty($bookmark->getDescription());
@@ -216,7 +216,7 @@ class BookmarkFileServiceTest extends TestCase
216 216
217 $bookmark = $this->privateLinkDB->get(43); 217 $bookmark = $this->privateLinkDB->get(43);
218 $this->assertEquals(43, $bookmark->getId()); 218 $this->assertEquals(43, $bookmark->getId());
219 $this->assertRegExp('/\?[\w\-]{6}/', $bookmark->getUrl()); 219 $this->assertRegExp('#/shaare/[\w\-]{6}#', $bookmark->getUrl());
220 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl()); 220 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
221 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle()); 221 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
222 $this->assertEmpty($bookmark->getDescription()); 222 $this->assertEmpty($bookmark->getDescription());
@@ -340,7 +340,7 @@ class BookmarkFileServiceTest extends TestCase
340 340
341 $bookmark = $this->privateLinkDB->get(42); 341 $bookmark = $this->privateLinkDB->get(42);
342 $this->assertEquals(42, $bookmark->getId()); 342 $this->assertEquals(42, $bookmark->getId());
343 $this->assertEquals('?WDWyig', $bookmark->getUrl()); 343 $this->assertEquals('/shaare/WDWyig', $bookmark->getUrl());
344 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl()); 344 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
345 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle()); 345 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
346 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription()); 346 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
@@ -359,7 +359,7 @@ class BookmarkFileServiceTest extends TestCase
359 359
360 $bookmark = $this->privateLinkDB->get(42); 360 $bookmark = $this->privateLinkDB->get(42);
361 $this->assertEquals(42, $bookmark->getId()); 361 $this->assertEquals(42, $bookmark->getId());
362 $this->assertEquals('?WDWyig', $bookmark->getUrl()); 362 $this->assertEquals('/shaare/WDWyig', $bookmark->getUrl());
363 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl()); 363 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
364 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle()); 364 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
365 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription()); 365 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
@@ -816,7 +816,6 @@ class BookmarkFileServiceTest extends TestCase
816 ); 816 );
817 $this->assertEquals( 817 $this->assertEquals(
818 [ 818 [
819 'web' => 4,
820 'cartoon' => 2, 819 'cartoon' => 2,
821 'gnu' => 1, 820 'gnu' => 1,
822 'dev' => 1, 821 'dev' => 1,
@@ -833,7 +832,6 @@ class BookmarkFileServiceTest extends TestCase
833 ); 832 );
834 $this->assertEquals( 833 $this->assertEquals(
835 [ 834 [
836 'web' => 1,
837 'html' => 1, 835 'html' => 1,
838 'w3c' => 1, 836 'w3c' => 1,
839 'css' => 1, 837 'css' => 1,
@@ -894,35 +892,35 @@ class BookmarkFileServiceTest extends TestCase
894 public function testFilterHashValid() 892 public function testFilterHashValid()
895 { 893 {
896 $request = smallHash('20150310_114651'); 894 $request = smallHash('20150310_114651');
897 $this->assertEquals( 895 $this->assertSame(
898 1, 896 $request,
899 count($this->publicLinkDB->findByHash($request)) 897 $this->publicLinkDB->findByHash($request)->getShortUrl()
900 ); 898 );
901 $request = smallHash('20150310_114633' . 8); 899 $request = smallHash('20150310_114633' . 8);
902 $this->assertEquals( 900 $this->assertSame(
903 1, 901 $request,
904 count($this->publicLinkDB->findByHash($request)) 902 $this->publicLinkDB->findByHash($request)->getShortUrl()
905 ); 903 );
906 } 904 }
907 905
908 /** 906 /**
909 * Test filterHash() with an invalid smallhash. 907 * Test filterHash() with an invalid smallhash.
910 *
911 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
912 */ 908 */
913 public function testFilterHashInValid1() 909 public function testFilterHashInValid1()
914 { 910 {
911 $this->expectException(BookmarkNotFoundException::class);
912
915 $request = 'blabla'; 913 $request = 'blabla';
916 $this->publicLinkDB->findByHash($request); 914 $this->publicLinkDB->findByHash($request);
917 } 915 }
918 916
919 /** 917 /**
920 * Test filterHash() with an empty smallhash. 918 * Test filterHash() with an empty smallhash.
921 *
922 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
923 */ 919 */
924 public function testFilterHashInValid() 920 public function testFilterHashInValid()
925 { 921 {
922 $this->expectException(BookmarkNotFoundException::class);
923
926 $this->publicLinkDB->findByHash(''); 924 $this->publicLinkDB->findByHash('');
927 } 925 }
928 926
@@ -968,7 +966,6 @@ class BookmarkFileServiceTest extends TestCase
968 public function testCountLinkPerTagAllWithFilter() 966 public function testCountLinkPerTagAllWithFilter()
969 { 967 {
970 $expected = [ 968 $expected = [
971 'gnu' => 2,
972 'hashtag' => 2, 969 'hashtag' => 2,
973 '-exclude' => 1, 970 '-exclude' => 1,
974 '.hidden' => 1, 971 '.hidden' => 1,
@@ -991,7 +988,6 @@ class BookmarkFileServiceTest extends TestCase
991 public function testCountLinkPerTagPublicWithFilter() 988 public function testCountLinkPerTagPublicWithFilter()
992 { 989 {
993 $expected = [ 990 $expected = [
994 'gnu' => 2,
995 'hashtag' => 2, 991 'hashtag' => 2,
996 '-exclude' => 1, 992 '-exclude' => 1,
997 '.hidden' => 1, 993 '.hidden' => 1,
@@ -1015,7 +1011,6 @@ class BookmarkFileServiceTest extends TestCase
1015 { 1011 {
1016 $expected = [ 1012 $expected = [
1017 'cartoon' => 1, 1013 'cartoon' => 1,
1018 'dev' => 1,
1019 'tag1' => 1, 1014 'tag1' => 1,
1020 'tag2' => 1, 1015 'tag2' => 1,
1021 'tag3' => 1, 1016 'tag3' => 1,
diff --git a/tests/bookmark/BookmarkInitializerTest.php b/tests/bookmark/BookmarkInitializerTest.php
index d23eb069..3906cc7f 100644
--- a/tests/bookmark/BookmarkInitializerTest.php
+++ b/tests/bookmark/BookmarkInitializerTest.php
@@ -3,7 +3,6 @@
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use PHPUnit\Framework\TestCase; 5use PHPUnit\Framework\TestCase;
6use ReferenceLinkDB;
7use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
8use Shaarli\History; 7use Shaarli\History;
9 8
@@ -54,9 +53,9 @@ class BookmarkInitializerTest extends TestCase
54 } 53 }
55 54
56 /** 55 /**
57 * Test initialize() with an empty data store. 56 * Test initialize() with a data store containing bookmarks.
58 */ 57 */
59 public function testInitializeEmptyDataStore() 58 public function testInitializeNotEmptyDataStore(): void
60 { 59 {
61 $refDB = new \ReferenceLinkDB(); 60 $refDB = new \ReferenceLinkDB();
62 $refDB->write(self::$testDatastore); 61 $refDB->write(self::$testDatastore);
@@ -79,6 +78,8 @@ class BookmarkInitializerTest extends TestCase
79 ); 78 );
80 $this->assertFalse($bookmark->isPrivate()); 79 $this->assertFalse($bookmark->isPrivate());
81 80
81 $this->bookmarkService->save();
82
82 // Reload from file 83 // Reload from file
83 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 84 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
84 $this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count()); 85 $this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count());
@@ -97,10 +98,13 @@ class BookmarkInitializerTest extends TestCase
97 } 98 }
98 99
99 /** 100 /**
100 * Test initialize() with a data store containing bookmarks. 101 * Test initialize() with an a non existent datastore file .
101 */ 102 */
102 public function testInitializeNotEmptyDataStore() 103 public function testInitializeNonExistentDataStore(): void
103 { 104 {
105 $this->conf->set('resource.datastore', static::$testDatastore . '_empty');
106 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
107
104 $this->initializer->initialize(); 108 $this->initializer->initialize();
105 109
106 $this->assertEquals(2, $this->bookmarkService->count()); 110 $this->assertEquals(2, $this->bookmarkService->count());
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php
index 9a3bbbfc..4b6a3c07 100644
--- a/tests/bookmark/BookmarkTest.php
+++ b/tests/bookmark/BookmarkTest.php
@@ -124,8 +124,8 @@ class BookmarkTest extends TestCase
124 $this->assertEquals(1, $bookmark->getId()); 124 $this->assertEquals(1, $bookmark->getId());
125 $this->assertEquals('abc', $bookmark->getShortUrl()); 125 $this->assertEquals('abc', $bookmark->getShortUrl());
126 $this->assertEquals($date, $bookmark->getCreated()); 126 $this->assertEquals($date, $bookmark->getCreated());
127 $this->assertEquals('?abc', $bookmark->getUrl()); 127 $this->assertEquals('/shaare/abc', $bookmark->getUrl());
128 $this->assertEquals('?abc', $bookmark->getTitle()); 128 $this->assertEquals('/shaare/abc', $bookmark->getTitle());
129 $this->assertEquals('', $bookmark->getDescription()); 129 $this->assertEquals('', $bookmark->getDescription());
130 $this->assertEquals([], $bookmark->getTags()); 130 $this->assertEquals([], $bookmark->getTags());
131 $this->assertEquals('', $bookmark->getTagsString()); 131 $this->assertEquals('', $bookmark->getTagsString());
diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php
index 591976f2..7d4a7b89 100644
--- a/tests/bookmark/LinkUtilsTest.php
+++ b/tests/bookmark/LinkUtilsTest.php
@@ -3,8 +3,6 @@
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use PHPUnit\Framework\TestCase; 5use PHPUnit\Framework\TestCase;
6use ReferenceLinkDB;
7use Shaarli\Config\ConfigManager;
8 6
9require_once 'tests/utils/CurlUtils.php'; 7require_once 'tests/utils/CurlUtils.php';
10 8
@@ -491,7 +489,7 @@ class LinkUtilsTest extends TestCase
491 */ 489 */
492 private function getHashtagLink($hashtag, $index = '') 490 private function getHashtagLink($hashtag, $index = '')
493 { 491 {
494 $hashtagLink = '<a href="' . $index . '?addtag=$1" title="Hashtag $1">#$1</a>'; 492 $hashtagLink = '<a href="' . $index . './add-tag/$1" title="Hashtag $1">#$1</a>';
495 return str_replace('$1', $hashtag, $hashtagLink); 493 return str_replace('$1', $hashtag, $hashtagLink);
496 } 494 }
497} 495}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 0afbcba6..d4ddedd5 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -18,7 +18,14 @@ require_once 'application/bookmark/LinkUtils.php';
18require_once 'application/Utils.php'; 18require_once 'application/Utils.php';
19require_once 'application/http/UrlUtils.php'; 19require_once 'application/http/UrlUtils.php';
20require_once 'application/http/HttpUtils.php'; 20require_once 'application/http/HttpUtils.php';
21require_once 'application/feed/Cache.php'; 21require_once 'tests/container/ShaarliTestContainer.php';
22require_once 'tests/utils/ReferenceLinkDB.php'; 22require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php';
23require_once 'tests/utils/ReferenceHistory.php'; 23require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php';
24require_once 'tests/updater/DummyUpdater.php';
24require_once 'tests/utils/FakeBookmarkService.php'; 25require_once 'tests/utils/FakeBookmarkService.php';
26require_once 'tests/utils/FakeConfigManager.php';
27require_once 'tests/utils/ReferenceHistory.php';
28require_once 'tests/utils/ReferenceLinkDB.php';
29require_once 'tests/utils/ReferenceSessionIdHashes.php';
30
31\ReferenceSessionIdHashes::genAllHashes();
diff --git a/tests/config/ConfigPluginTest.php b/tests/config/ConfigPluginTest.php
index d7a70e68..b2cc0045 100644
--- a/tests/config/ConfigPluginTest.php
+++ b/tests/config/ConfigPluginTest.php
@@ -2,6 +2,7 @@
2namespace Shaarli\Config; 2namespace Shaarli\Config;
3 3
4use Shaarli\Config\Exception\PluginConfigOrderException; 4use Shaarli\Config\Exception\PluginConfigOrderException;
5use Shaarli\Plugin\PluginManager;
5 6
6require_once 'application/config/ConfigPlugin.php'; 7require_once 'application/config/ConfigPlugin.php';
7 8
@@ -17,23 +18,30 @@ class ConfigPluginTest extends \PHPUnit\Framework\TestCase
17 */ 18 */
18 public function testSavePluginConfigValid() 19 public function testSavePluginConfigValid()
19 { 20 {
20 $data = array( 21 $data = [
21 'order_plugin1' => 2, // no plugin related 22 'order_plugin1' => 2, // no plugin related
22 'plugin2' => 0, // new - at the end 23 'plugin2' => 0, // new - at the end
23 'plugin3' => 0, // 2nd 24 'plugin3' => 0, // 2nd
24 'order_plugin3' => 8, 25 'order_plugin3' => 8,
25 'plugin4' => 0, // 1st 26 'plugin4' => 0, // 1st
26 'order_plugin4' => 5, 27 'order_plugin4' => 5,
27 ); 28 ];
28 29
29 $expected = array( 30 $expected = [
30 'plugin3', 31 'plugin3',
31 'plugin4', 32 'plugin4',
32 'plugin2', 33 'plugin2',
33 ); 34 ];
35
36 mkdir($path = __DIR__ . '/folder');
37 PluginManager::$PLUGINS_PATH = $path;
38 array_map(function (string $plugin) use ($path) { touch($path . '/' . $plugin); }, $expected);
34 39
35 $out = save_plugin_config($data); 40 $out = save_plugin_config($data);
36 $this->assertEquals($expected, $out); 41 $this->assertEquals($expected, $out);
42
43 array_map(function (string $plugin) use ($path) { unlink($path . '/' . $plugin); }, $expected);
44 rmdir($path);
37 } 45 }
38 46
39 /** 47 /**
diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php
index 9b97ed6d..fa77bf31 100644
--- a/tests/container/ContainerBuilderTest.php
+++ b/tests/container/ContainerBuilderTest.php
@@ -7,10 +7,19 @@ namespace Shaarli\Container;
7use PHPUnit\Framework\TestCase; 7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Feed\FeedBuilder;
11use Shaarli\Formatter\FormatterFactory;
10use Shaarli\History; 12use Shaarli\History;
13use Shaarli\Http\HttpAccess;
14use Shaarli\Netscape\NetscapeBookmarkUtils;
15use Shaarli\Plugin\PluginManager;
11use Shaarli\Render\PageBuilder; 16use Shaarli\Render\PageBuilder;
17use Shaarli\Render\PageCacheManager;
18use Shaarli\Security\CookieManager;
12use Shaarli\Security\LoginManager; 19use Shaarli\Security\LoginManager;
13use Shaarli\Security\SessionManager; 20use Shaarli\Security\SessionManager;
21use Shaarli\Thumbnailer;
22use Shaarli\Updater\Updater;
14 23
15class ContainerBuilderTest extends TestCase 24class ContainerBuilderTest extends TestCase
16{ 25{
@@ -26,13 +35,24 @@ class ContainerBuilderTest extends TestCase
26 /** @var ContainerBuilder */ 35 /** @var ContainerBuilder */
27 protected $containerBuilder; 36 protected $containerBuilder;
28 37
38 /** @var CookieManager */
39 protected $cookieManager;
40
29 public function setUp(): void 41 public function setUp(): void
30 { 42 {
31 $this->conf = new ConfigManager('tests/utils/config/configJson'); 43 $this->conf = new ConfigManager('tests/utils/config/configJson');
32 $this->sessionManager = $this->createMock(SessionManager::class); 44 $this->sessionManager = $this->createMock(SessionManager::class);
45 $this->cookieManager = $this->createMock(CookieManager::class);
46
33 $this->loginManager = $this->createMock(LoginManager::class); 47 $this->loginManager = $this->createMock(LoginManager::class);
48 $this->loginManager->method('isLoggedIn')->willReturn(true);
34 49
35 $this->containerBuilder = new ContainerBuilder($this->conf, $this->sessionManager, $this->loginManager); 50 $this->containerBuilder = new ContainerBuilder(
51 $this->conf,
52 $this->sessionManager,
53 $this->cookieManager,
54 $this->loginManager
55 );
36 } 56 }
37 57
38 public function testBuildContainer(): void 58 public function testBuildContainer(): void
@@ -41,9 +61,21 @@ class ContainerBuilderTest extends TestCase
41 61
42 static::assertInstanceOf(ConfigManager::class, $container->conf); 62 static::assertInstanceOf(ConfigManager::class, $container->conf);
43 static::assertInstanceOf(SessionManager::class, $container->sessionManager); 63 static::assertInstanceOf(SessionManager::class, $container->sessionManager);
64 static::assertInstanceOf(CookieManager::class, $container->cookieManager);
44 static::assertInstanceOf(LoginManager::class, $container->loginManager); 65 static::assertInstanceOf(LoginManager::class, $container->loginManager);
45 static::assertInstanceOf(History::class, $container->history); 66 static::assertInstanceOf(History::class, $container->history);
46 static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService); 67 static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
47 static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); 68 static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
69 static::assertInstanceOf(PluginManager::class, $container->pluginManager);
70 static::assertInstanceOf(FormatterFactory::class, $container->formatterFactory);
71 static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager);
72 static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder);
73 static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer);
74 static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
75 static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
76 static::assertInstanceOf(Updater::class, $container->updater);
77
78 // Set by the middleware
79 static::assertNull($container->basePath);
48 } 80 }
49} 81}
diff --git a/tests/container/ShaarliTestContainer.php b/tests/container/ShaarliTestContainer.php
new file mode 100644
index 00000000..7dbe914c
--- /dev/null
+++ b/tests/container/ShaarliTestContainer.php
@@ -0,0 +1,42 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Container;
6
7use PHPUnit\Framework\MockObject\MockObject;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Feed\FeedBuilder;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\History;
13use Shaarli\Http\HttpAccess;
14use Shaarli\Plugin\PluginManager;
15use Shaarli\Render\PageBuilder;
16use Shaarli\Render\PageCacheManager;
17use Shaarli\Security\LoginManager;
18use Shaarli\Security\SessionManager;
19use Shaarli\Thumbnailer;
20
21/**
22 * Test helper allowing auto-completion for MockObjects.
23 *
24 * @property mixed[] $environment $_SERVER automatically injected by Slim
25 * @property MockObject|ConfigManager $conf
26 * @property MockObject|SessionManager $sessionManager
27 * @property MockObject|LoginManager $loginManager
28 * @property MockObject|string $webPath
29 * @property MockObject|History $history
30 * @property MockObject|BookmarkServiceInterface $bookmarkService
31 * @property MockObject|PageBuilder $pageBuilder
32 * @property MockObject|PluginManager $pluginManager
33 * @property MockObject|FormatterFactory $formatterFactory
34 * @property MockObject|PageCacheManager $pageCacheManager
35 * @property MockObject|FeedBuilder $feedBuilder
36 * @property MockObject|Thumbnailer $thumbnailer
37 * @property MockObject|HttpAccess $httpAccess
38 */
39class ShaarliTestContainer extends ShaarliContainer
40{
41
42}
diff --git a/tests/feed/CachedPageTest.php b/tests/feed/CachedPageTest.php
index 363028a2..2e716432 100644
--- a/tests/feed/CachedPageTest.php
+++ b/tests/feed/CachedPageTest.php
@@ -11,7 +11,7 @@ class CachedPageTest extends \PHPUnit\Framework\TestCase
11{ 11{
12 // test cache directory 12 // test cache directory
13 protected static $testCacheDir = 'sandbox/pagecache'; 13 protected static $testCacheDir = 'sandbox/pagecache';
14 protected static $url = 'http://shaar.li/?do=atom'; 14 protected static $url = 'http://shaar.li/feed/atom';
15 protected static $filename; 15 protected static $filename;
16 16
17 /** 17 /**
@@ -42,8 +42,8 @@ class CachedPageTest extends \PHPUnit\Framework\TestCase
42 { 42 {
43 new CachedPage(self::$testCacheDir, '', true); 43 new CachedPage(self::$testCacheDir, '', true);
44 new CachedPage(self::$testCacheDir, '', false); 44 new CachedPage(self::$testCacheDir, '', false);
45 new CachedPage(self::$testCacheDir, 'http://shaar.li/?do=rss', true); 45 new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true);
46 new CachedPage(self::$testCacheDir, 'http://shaar.li/?do=atom', false); 46 new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false);
47 $this->addToAssertionCount(1); 47 $this->addToAssertionCount(1);
48 } 48 }
49 49
diff --git a/tests/feed/FeedBuilderTest.php b/tests/feed/FeedBuilderTest.php
index 54671891..5c2aaedb 100644
--- a/tests/feed/FeedBuilderTest.php
+++ b/tests/feed/FeedBuilderTest.php
@@ -65,23 +65,6 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
65 } 65 }
66 66
67 /** 67 /**
68 * Test GetTypeLanguage().
69 */
70 public function testGetTypeLanguage()
71 {
72 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
73 $feedBuilder->setLocale(self::$LOCALE);
74 $this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage());
75 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
76 $feedBuilder->setLocale(self::$LOCALE);
77 $this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage());
78 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
79 $this->assertEquals('en', $feedBuilder->getTypeLanguage());
80 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
81 $this->assertEquals('en-en', $feedBuilder->getTypeLanguage());
82 }
83
84 /**
85 * Test buildData with RSS feed. 68 * Test buildData with RSS feed.
86 */ 69 */
87 public function testRSSBuildData() 70 public function testRSSBuildData()
@@ -89,13 +72,11 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
89 $feedBuilder = new FeedBuilder( 72 $feedBuilder = new FeedBuilder(
90 self::$bookmarkService, 73 self::$bookmarkService,
91 self::$formatter, 74 self::$formatter,
92 FeedBuilder::$FEED_RSS, 75 static::$serverInfo,
93 self::$serverInfo,
94 null,
95 false 76 false
96 ); 77 );
97 $feedBuilder->setLocale(self::$LOCALE); 78 $feedBuilder->setLocale(self::$LOCALE);
98 $data = $feedBuilder->buildData(); 79 $data = $feedBuilder->buildData(FeedBuilder::$FEED_RSS, null);
99 // Test headers (RSS) 80 // Test headers (RSS)
100 $this->assertEquals(self::$RSS_LANGUAGE, $data['language']); 81 $this->assertEquals(self::$RSS_LANGUAGE, $data['language']);
101 $this->assertRegExp('/Wed, 03 Aug 2016 09:30:33 \+\d{4}/', $data['last_update']); 82 $this->assertRegExp('/Wed, 03 Aug 2016 09:30:33 \+\d{4}/', $data['last_update']);
@@ -109,15 +90,15 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
109 $link = $data['links'][array_keys($data['links'])[2]]; 90 $link = $data['links'][array_keys($data['links'])[2]];
110 $this->assertEquals(41, $link['id']); 91 $this->assertEquals(41, $link['id']);
111 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 92 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
112 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']); 93 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['guid']);
113 $this->assertEquals('http://host.tld/?WDWyig', $link['url']); 94 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['url']);
114 $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']); 95 $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
115 $pub = DateTime::createFromFormat(DateTime::RSS, $link['pub_iso_date']); 96 $pub = DateTime::createFromFormat(DateTime::RSS, $link['pub_iso_date']);
116 $up = DateTime::createFromFormat(DateTime::ATOM, $link['up_iso_date']); 97 $up = DateTime::createFromFormat(DateTime::ATOM, $link['up_iso_date']);
117 $this->assertEquals($pub, $up); 98 $this->assertEquals($pub, $up);
118 $this->assertContains('Stallman has a beard', $link['description']); 99 $this->assertContains('Stallman has a beard', $link['description']);
119 $this->assertContains('Permalink', $link['description']); 100 $this->assertContains('Permalink', $link['description']);
120 $this->assertContains('http://host.tld/?WDWyig', $link['description']); 101 $this->assertContains('http://host.tld/shaare/WDWyig', $link['description']);
121 $this->assertEquals(1, count($link['taglist'])); 102 $this->assertEquals(1, count($link['taglist']));
122 $this->assertEquals('sTuff', $link['taglist'][0]); 103 $this->assertEquals('sTuff', $link['taglist'][0]);
123 104
@@ -140,13 +121,11 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
140 $feedBuilder = new FeedBuilder( 121 $feedBuilder = new FeedBuilder(
141 self::$bookmarkService, 122 self::$bookmarkService,
142 self::$formatter, 123 self::$formatter,
143 FeedBuilder::$FEED_ATOM, 124 static::$serverInfo,
144 self::$serverInfo,
145 null,
146 false 125 false
147 ); 126 );
148 $feedBuilder->setLocale(self::$LOCALE); 127 $feedBuilder->setLocale(self::$LOCALE);
149 $data = $feedBuilder->buildData(); 128 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
150 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 129 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
151 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']); 130 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']);
152 $link = $data['links'][array_keys($data['links'])[2]]; 131 $link = $data['links'][array_keys($data['links'])[2]];
@@ -166,13 +145,11 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
166 $feedBuilder = new FeedBuilder( 145 $feedBuilder = new FeedBuilder(
167 self::$bookmarkService, 146 self::$bookmarkService,
168 self::$formatter, 147 self::$formatter,
169 FeedBuilder::$FEED_ATOM, 148 static::$serverInfo,
170 self::$serverInfo,
171 $criteria,
172 false 149 false
173 ); 150 );
174 $feedBuilder->setLocale(self::$LOCALE); 151 $feedBuilder->setLocale(self::$LOCALE);
175 $data = $feedBuilder->buildData(); 152 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, $criteria);
176 $this->assertEquals(1, count($data['links'])); 153 $this->assertEquals(1, count($data['links']));
177 $link = array_shift($data['links']); 154 $link = array_shift($data['links']);
178 $this->assertEquals(41, $link['id']); 155 $this->assertEquals(41, $link['id']);
@@ -190,13 +167,11 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
190 $feedBuilder = new FeedBuilder( 167 $feedBuilder = new FeedBuilder(
191 self::$bookmarkService, 168 self::$bookmarkService,
192 self::$formatter, 169 self::$formatter,
193 FeedBuilder::$FEED_ATOM, 170 static::$serverInfo,
194 self::$serverInfo,
195 $criteria,
196 false 171 false
197 ); 172 );
198 $feedBuilder->setLocale(self::$LOCALE); 173 $feedBuilder->setLocale(self::$LOCALE);
199 $data = $feedBuilder->buildData(); 174 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, $criteria);
200 $this->assertEquals(3, count($data['links'])); 175 $this->assertEquals(3, count($data['links']));
201 $link = $data['links'][array_keys($data['links'])[2]]; 176 $link = $data['links'][array_keys($data['links'])[2]];
202 $this->assertEquals(41, $link['id']); 177 $this->assertEquals(41, $link['id']);
@@ -211,29 +186,27 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
211 $feedBuilder = new FeedBuilder( 186 $feedBuilder = new FeedBuilder(
212 self::$bookmarkService, 187 self::$bookmarkService,
213 self::$formatter, 188 self::$formatter,
214 FeedBuilder::$FEED_ATOM, 189 static::$serverInfo,
215 self::$serverInfo,
216 null,
217 false 190 false
218 ); 191 );
219 $feedBuilder->setLocale(self::$LOCALE); 192 $feedBuilder->setLocale(self::$LOCALE);
220 $feedBuilder->setUsePermalinks(true); 193 $feedBuilder->setUsePermalinks(true);
221 $data = $feedBuilder->buildData(); 194 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
222 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 195 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
223 $this->assertTrue($data['usepermalinks']); 196 $this->assertTrue($data['usepermalinks']);
224 // First link is a permalink 197 // First link is a permalink
225 $link = $data['links'][array_keys($data['links'])[2]]; 198 $link = $data['links'][array_keys($data['links'])[2]];
226 $this->assertEquals(41, $link['id']); 199 $this->assertEquals(41, $link['id']);
227 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 200 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
228 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']); 201 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['guid']);
229 $this->assertEquals('http://host.tld/?WDWyig', $link['url']); 202 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['url']);
230 $this->assertContains('Direct link', $link['description']); 203 $this->assertContains('Direct link', $link['description']);
231 $this->assertContains('http://host.tld/?WDWyig', $link['description']); 204 $this->assertContains('http://host.tld/shaare/WDWyig', $link['description']);
232 // Second link is a direct link 205 // Second link is a direct link
233 $link = $data['links'][array_keys($data['links'])[3]]; 206 $link = $data['links'][array_keys($data['links'])[3]];
234 $this->assertEquals(8, $link['id']); 207 $this->assertEquals(8, $link['id']);
235 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'), $link['created']); 208 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'), $link['created']);
236 $this->assertEquals('http://host.tld/?RttfEw', $link['guid']); 209 $this->assertEquals('http://host.tld/shaare/RttfEw', $link['guid']);
237 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']); 210 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
238 $this->assertContains('Direct link', $link['description']); 211 $this->assertContains('Direct link', $link['description']);
239 $this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']); 212 $this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']);
@@ -247,14 +220,12 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
247 $feedBuilder = new FeedBuilder( 220 $feedBuilder = new FeedBuilder(
248 self::$bookmarkService, 221 self::$bookmarkService,
249 self::$formatter, 222 self::$formatter,
250 FeedBuilder::$FEED_ATOM, 223 static::$serverInfo,
251 self::$serverInfo,
252 null,
253 false 224 false
254 ); 225 );
255 $feedBuilder->setLocale(self::$LOCALE); 226 $feedBuilder->setLocale(self::$LOCALE);
256 $feedBuilder->setHideDates(true); 227 $feedBuilder->setHideDates(true);
257 $data = $feedBuilder->buildData(); 228 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
258 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 229 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
259 $this->assertFalse($data['show_dates']); 230 $this->assertFalse($data['show_dates']);
260 231
@@ -262,14 +233,12 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
262 $feedBuilder = new FeedBuilder( 233 $feedBuilder = new FeedBuilder(
263 self::$bookmarkService, 234 self::$bookmarkService,
264 self::$formatter, 235 self::$formatter,
265 FeedBuilder::$FEED_ATOM, 236 static::$serverInfo,
266 self::$serverInfo,
267 null,
268 true 237 true
269 ); 238 );
270 $feedBuilder->setLocale(self::$LOCALE); 239 $feedBuilder->setLocale(self::$LOCALE);
271 $feedBuilder->setHideDates(true); 240 $feedBuilder->setHideDates(true);
272 $data = $feedBuilder->buildData(); 241 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
273 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 242 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
274 $this->assertTrue($data['show_dates']); 243 $this->assertTrue($data['show_dates']);
275 } 244 }
@@ -289,13 +258,11 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
289 $feedBuilder = new FeedBuilder( 258 $feedBuilder = new FeedBuilder(
290 self::$bookmarkService, 259 self::$bookmarkService,
291 self::$formatter, 260 self::$formatter,
292 FeedBuilder::$FEED_ATOM,
293 $serverInfo, 261 $serverInfo,
294 null,
295 false 262 false
296 ); 263 );
297 $feedBuilder->setLocale(self::$LOCALE); 264 $feedBuilder->setLocale(self::$LOCALE);
298 $data = $feedBuilder->buildData(); 265 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
299 266
300 $this->assertEquals( 267 $this->assertEquals(
301 'http://host.tld:8080/~user/shaarli/index.php?do=feed', 268 'http://host.tld:8080/~user/shaarli/index.php?do=feed',
@@ -304,8 +271,8 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
304 271
305 // Test first link (note link) 272 // Test first link (note link)
306 $link = $data['links'][array_keys($data['links'])[2]]; 273 $link = $data['links'][array_keys($data['links'])[2]];
307 $this->assertEquals('http://host.tld:8080/~user/shaarli/?WDWyig', $link['guid']); 274 $this->assertEquals('http://host.tld:8080/~user/shaarli/shaare/WDWyig', $link['guid']);
308 $this->assertEquals('http://host.tld:8080/~user/shaarli/?WDWyig', $link['url']); 275 $this->assertEquals('http://host.tld:8080/~user/shaarli/shaare/WDWyig', $link['url']);
309 $this->assertContains('http://host.tld:8080/~user/shaarli/?addtag=hashtag', $link['description']); 276 $this->assertContains('http://host.tld:8080/~user/shaarli/./add-tag/hashtag', $link['description']);
310 } 277 }
311} 278}
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php
index 382a560e..cf48b00b 100644
--- a/tests/formatter/BookmarkDefaultFormatterTest.php
+++ b/tests/formatter/BookmarkDefaultFormatterTest.php
@@ -123,7 +123,7 @@ class BookmarkDefaultFormatterTest extends TestCase
123 $description[0] = 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'; 123 $description[0] = 'This a &lt;strong&gt;description&lt;/strong&gt;<br />';
124 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash'; 124 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
125 $description[1] = 'text <a href="'. $url .'">'. $url .'</a> more text<br />'; 125 $description[1] = 'text <a href="'. $url .'">'. $url .'</a> more text<br />';
126 $description[2] = 'Also, there is an <a href="?addtag=hashtag" '. 126 $description[2] = 'Also, there is an <a href="./add-tag/hashtag" '.
127 'title="Hashtag hashtag">#hashtag</a> added<br />'; 127 'title="Hashtag hashtag">#hashtag</a> added<br />';
128 $description[3] = '&nbsp; &nbsp; A &nbsp;N &nbsp;D KEEP &nbsp; &nbsp; '. 128 $description[3] = '&nbsp; &nbsp; A &nbsp;N &nbsp;D KEEP &nbsp; &nbsp; '.
129 'SPACES &nbsp; &nbsp;! &nbsp; <br />'; 129 'SPACES &nbsp; &nbsp;! &nbsp; <br />';
@@ -148,7 +148,7 @@ class BookmarkDefaultFormatterTest extends TestCase
148 $this->assertEquals($root . $short, $link['url']); 148 $this->assertEquals($root . $short, $link['url']);
149 $this->assertEquals($root . $short, $link['real_url']); 149 $this->assertEquals($root . $short, $link['real_url']);
150 $this->assertEquals( 150 $this->assertEquals(
151 'Text <a href="'. $root .'?addtag=hashtag" title="Hashtag hashtag">'. 151 'Text <a href="'. $root .'./add-tag/hashtag" title="Hashtag hashtag">'.
152 '#hashtag</a> more text', 152 '#hashtag</a> more text',
153 $link['description'] 153 $link['description']
154 ); 154 );
diff --git a/tests/formatter/BookmarkMarkdownFormatterTest.php b/tests/formatter/BookmarkMarkdownFormatterTest.php
index f1f12c04..3e72d1ee 100644
--- a/tests/formatter/BookmarkMarkdownFormatterTest.php
+++ b/tests/formatter/BookmarkMarkdownFormatterTest.php
@@ -125,7 +125,7 @@ class BookmarkMarkdownFormatterTest extends TestCase
125 $description .= 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'. PHP_EOL; 125 $description .= 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'. PHP_EOL;
126 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash'; 126 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
127 $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL; 127 $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL;
128 $description .= 'Also, there is an <a href="?addtag=hashtag">#hashtag</a> added<br />'. PHP_EOL; 128 $description .= 'Also, there is an <a href="./add-tag/hashtag">#hashtag</a> added<br />'. PHP_EOL;
129 $description .= 'A N D KEEP SPACES ! '; 129 $description .= 'A N D KEEP SPACES ! ';
130 $description .= '</p></div>'; 130 $description .= '</p></div>';
131 131
@@ -146,7 +146,7 @@ class BookmarkMarkdownFormatterTest extends TestCase
146 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/'); 146 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
147 147
148 $description = '<div class="markdown"><p>'; 148 $description = '<div class="markdown"><p>';
149 $description .= 'Text <a href="'. $root .'?addtag=hashtag">#hashtag</a> more text'; 149 $description .= 'Text <a href="'. $root .'./add-tag/hashtag">#hashtag</a> more text';
150 $description .= '</p></div>'; 150 $description .= '</p></div>';
151 151
152 $link = $this->formatter->format($bookmark); 152 $link = $this->formatter->format($bookmark);
diff --git a/tests/front/ShaarliAdminMiddlewareTest.php b/tests/front/ShaarliAdminMiddlewareTest.php
new file mode 100644
index 00000000..7451330b
--- /dev/null
+++ b/tests/front/ShaarliAdminMiddlewareTest.php
@@ -0,0 +1,100 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Container\ShaarliContainer;
10use Shaarli\Security\LoginManager;
11use Shaarli\Updater\Updater;
12use Slim\Http\Request;
13use Slim\Http\Response;
14use Slim\Http\Uri;
15
16class ShaarliAdminMiddlewareTest extends TestCase
17{
18 protected const TMP_MOCK_FILE = '.tmp';
19
20 /** @var ShaarliContainer */
21 protected $container;
22
23 /** @var ShaarliMiddleware */
24 protected $middleware;
25
26 public function setUp(): void
27 {
28 $this->container = $this->createMock(ShaarliContainer::class);
29
30 touch(static::TMP_MOCK_FILE);
31
32 $this->container->conf = $this->createMock(ConfigManager::class);
33 $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
34
35 $this->container->loginManager = $this->createMock(LoginManager::class);
36 $this->container->updater = $this->createMock(Updater::class);
37
38 $this->container->environment = ['REQUEST_URI' => 'http://shaarli/subfolder/path'];
39
40 $this->middleware = new ShaarliAdminMiddleware($this->container);
41 }
42
43 public function tearDown(): void
44 {
45 unlink(static::TMP_MOCK_FILE);
46 }
47
48 /**
49 * Try to access an admin controller while logged out -> redirected to login page.
50 */
51 public function testMiddlewareWhileLoggedOut(): void
52 {
53 $this->container->loginManager->expects(static::once())->method('isLoggedIn')->willReturn(false);
54
55 $request = $this->createMock(Request::class);
56 $request->method('getUri')->willReturnCallback(function (): Uri {
57 $uri = $this->createMock(Uri::class);
58 $uri->method('getBasePath')->willReturn('/subfolder');
59
60 return $uri;
61 });
62
63 $response = new Response();
64
65 /** @var Response $result */
66 $result = $this->middleware->__invoke($request, $response, function () {});
67
68 static::assertSame(302, $result->getStatusCode());
69 static::assertSame(
70 '/subfolder/login?returnurl=' . urlencode('http://shaarli/subfolder/path'),
71 $result->getHeader('location')[0]
72 );
73 }
74
75 /**
76 * Process controller while logged in.
77 */
78 public function testMiddlewareWhileLoggedIn(): void
79 {
80 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
81
82 $request = $this->createMock(Request::class);
83 $request->method('getUri')->willReturnCallback(function (): Uri {
84 $uri = $this->createMock(Uri::class);
85 $uri->method('getBasePath')->willReturn('/subfolder');
86
87 return $uri;
88 });
89
90 $response = new Response();
91 $controller = function (Request $request, Response $response): Response {
92 return $response->withStatus(418); // I'm a tea pot
93 };
94
95 /** @var Response $result */
96 $result = $this->middleware->__invoke($request, $response, $controller);
97
98 static::assertSame(418, $result->getStatusCode());
99 }
100}
diff --git a/tests/front/ShaarliMiddlewareTest.php b/tests/front/ShaarliMiddlewareTest.php
index 80974f37..05aa34a9 100644
--- a/tests/front/ShaarliMiddlewareTest.php
+++ b/tests/front/ShaarliMiddlewareTest.php
@@ -8,12 +8,19 @@ use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\Container\ShaarliContainer; 9use Shaarli\Container\ShaarliContainer;
10use Shaarli\Front\Exception\LoginBannedException; 10use Shaarli\Front\Exception\LoginBannedException;
11use Shaarli\Front\Exception\UnauthorizedException;
11use Shaarli\Render\PageBuilder; 12use Shaarli\Render\PageBuilder;
13use Shaarli\Render\PageCacheManager;
14use Shaarli\Security\LoginManager;
15use Shaarli\Updater\Updater;
12use Slim\Http\Request; 16use Slim\Http\Request;
13use Slim\Http\Response; 17use Slim\Http\Response;
18use Slim\Http\Uri;
14 19
15class ShaarliMiddlewareTest extends TestCase 20class ShaarliMiddlewareTest extends TestCase
16{ 21{
22 protected const TMP_MOCK_FILE = '.tmp';
23
17 /** @var ShaarliContainer */ 24 /** @var ShaarliContainer */
18 protected $container; 25 protected $container;
19 26
@@ -23,12 +30,37 @@ class ShaarliMiddlewareTest extends TestCase
23 public function setUp(): void 30 public function setUp(): void
24 { 31 {
25 $this->container = $this->createMock(ShaarliContainer::class); 32 $this->container = $this->createMock(ShaarliContainer::class);
33
34 touch(static::TMP_MOCK_FILE);
35
36 $this->container->conf = $this->createMock(ConfigManager::class);
37 $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
38
39 $this->container->loginManager = $this->createMock(LoginManager::class);
40
41 $this->container->environment = ['REQUEST_URI' => 'http://shaarli/subfolder/path'];
42
26 $this->middleware = new ShaarliMiddleware($this->container); 43 $this->middleware = new ShaarliMiddleware($this->container);
27 } 44 }
28 45
46 public function tearDown(): void
47 {
48 unlink(static::TMP_MOCK_FILE);
49 }
50
51 /**
52 * Test middleware execution with valid controller call
53 */
29 public function testMiddlewareExecution(): void 54 public function testMiddlewareExecution(): void
30 { 55 {
31 $request = $this->createMock(Request::class); 56 $request = $this->createMock(Request::class);
57 $request->method('getUri')->willReturnCallback(function (): Uri {
58 $uri = $this->createMock(Uri::class);
59 $uri->method('getBasePath')->willReturn('/subfolder');
60
61 return $uri;
62 });
63
32 $response = new Response(); 64 $response = new Response();
33 $controller = function (Request $request, Response $response): Response { 65 $controller = function (Request $request, Response $response): Response {
34 return $response->withStatus(418); // I'm a tea pot 66 return $response->withStatus(418); // I'm a tea pot
@@ -41,9 +73,20 @@ class ShaarliMiddlewareTest extends TestCase
41 static::assertSame(418, $result->getStatusCode()); 73 static::assertSame(418, $result->getStatusCode());
42 } 74 }
43 75
44 public function testMiddlewareExecutionWithException(): void 76 /**
77 * Test middleware execution with controller throwing a known front exception.
78 * The exception should be thrown to be later handled by the error handler.
79 */
80 public function testMiddlewareExecutionWithFrontException(): void
45 { 81 {
46 $request = $this->createMock(Request::class); 82 $request = $this->createMock(Request::class);
83 $request->method('getUri')->willReturnCallback(function (): Uri {
84 $uri = $this->createMock(Uri::class);
85 $uri->method('getBasePath')->willReturn('/subfolder');
86
87 return $uri;
88 });
89
47 $response = new Response(); 90 $response = new Response();
48 $controller = function (): void { 91 $controller = function (): void {
49 $exception = new LoginBannedException(); 92 $exception = new LoginBannedException();
@@ -57,14 +100,122 @@ class ShaarliMiddlewareTest extends TestCase
57 }); 100 });
58 $this->container->pageBuilder = $pageBuilder; 101 $this->container->pageBuilder = $pageBuilder;
59 102
60 $conf = $this->createMock(ConfigManager::class); 103 $this->expectException(LoginBannedException::class);
61 $this->container->conf = $conf; 104
105 $this->middleware->__invoke($request, $response, $controller);
106 }
107
108 /**
109 * Test middleware execution with controller throwing a not authorized exception
110 * The middle should send a redirection response to the login page.
111 */
112 public function testMiddlewareExecutionWithUnauthorizedException(): void
113 {
114 $request = $this->createMock(Request::class);
115 $request->method('getUri')->willReturnCallback(function (): Uri {
116 $uri = $this->createMock(Uri::class);
117 $uri->method('getBasePath')->willReturn('/subfolder');
118
119 return $uri;
120 });
121
122 $response = new Response();
123 $controller = function (): void {
124 throw new UnauthorizedException();
125 };
126
127 /** @var Response $result */
128 $result = $this->middleware->__invoke($request, $response, $controller);
129
130 static::assertSame(302, $result->getStatusCode());
131 static::assertSame(
132 '/subfolder/login?returnurl=' . urlencode('http://shaarli/subfolder/path'),
133 $result->getHeader('location')[0]
134 );
135 }
136
137 /**
138 * Test middleware execution with controller throwing a not authorized exception.
139 * The exception should be thrown to be later handled by the error handler.
140 */
141 public function testMiddlewareExecutionWithServerException(): void
142 {
143 $request = $this->createMock(Request::class);
144 $request->method('getUri')->willReturnCallback(function (): Uri {
145 $uri = $this->createMock(Uri::class);
146 $uri->method('getBasePath')->willReturn('/subfolder');
147
148 return $uri;
149 });
150
151 $dummyException = new class() extends \Exception {};
152
153 $response = new Response();
154 $controller = function () use ($dummyException): void {
155 throw $dummyException;
156 };
157
158 $parameters = [];
159 $this->container->pageBuilder = $this->createMock(PageBuilder::class);
160 $this->container->pageBuilder->method('render')->willReturnCallback(function (string $message): string {
161 return $message;
162 });
163 $this->container->pageBuilder
164 ->method('assign')
165 ->willReturnCallback(function (string $key, string $value) use (&$parameters): void {
166 $parameters[$key] = $value;
167 })
168 ;
169
170 $this->expectException(get_class($dummyException));
171
172 $this->middleware->__invoke($request, $response, $controller);
173 }
174
175 public function testMiddlewareExecutionWithUpdates(): void
176 {
177 $request = $this->createMock(Request::class);
178 $request->method('getUri')->willReturnCallback(function (): Uri {
179 $uri = $this->createMock(Uri::class);
180 $uri->method('getBasePath')->willReturn('/subfolder');
181
182 return $uri;
183 });
184
185 $response = new Response();
186 $controller = function (Request $request, Response $response): Response {
187 return $response->withStatus(418); // I'm a tea pot
188 };
189
190 $this->container->loginManager = $this->createMock(LoginManager::class);
191 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
192
193 $this->container->conf = $this->createMock(ConfigManager::class);
194 $this->container->conf->method('get')->willReturnCallback(function (string $key): string {
195 return $key;
196 });
197 $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
198
199 $this->container->pageCacheManager = $this->createMock(PageCacheManager::class);
200 $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches');
201
202 $this->container->updater = $this->createMock(Updater::class);
203 $this->container->updater
204 ->expects(static::once())
205 ->method('update')
206 ->willReturn(['update123'])
207 ;
208 $this->container->updater->method('getDoneUpdates')->willReturn($updates = ['update123', 'other']);
209 $this->container->updater
210 ->expects(static::once())
211 ->method('writeUpdates')
212 ->with('resource.updates', $updates)
213 ;
62 214
63 /** @var Response $result */ 215 /** @var Response $result */
64 $result = $this->middleware->__invoke($request, $response, $controller); 216 $result = $this->middleware->__invoke($request, $response, $controller);
65 217
66 static::assertInstanceOf(Response::class, $result); 218 static::assertInstanceOf(Response::class, $result);
67 static::assertSame(401, $result->getStatusCode()); 219 static::assertSame(418, $result->getStatusCode());
68 static::assertContains('error', (string) $result->getBody());
69 } 220 }
70} 221}
diff --git a/tests/front/controller/LoginControllerTest.php b/tests/front/controller/LoginControllerTest.php
deleted file mode 100644
index 8cf8ece7..00000000
--- a/tests/front/controller/LoginControllerTest.php
+++ /dev/null
@@ -1,178 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Container\ShaarliContainer;
11use Shaarli\Front\Exception\LoginBannedException;
12use Shaarli\Plugin\PluginManager;
13use Shaarli\Render\PageBuilder;
14use Shaarli\Security\LoginManager;
15use Slim\Http\Request;
16use Slim\Http\Response;
17
18class LoginControllerTest extends TestCase
19{
20 /** @var ShaarliContainer */
21 protected $container;
22
23 /** @var LoginController */
24 protected $controller;
25
26 public function setUp(): void
27 {
28 $this->container = $this->createMock(ShaarliContainer::class);
29 $this->controller = new LoginController($this->container);
30 }
31
32 public function testValidControllerInvoke(): void
33 {
34 $this->createValidContainerMockSet();
35
36 $request = $this->createMock(Request::class);
37 $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
38 $response = new Response();
39
40 $assignedVariables = [];
41 $this->container->pageBuilder
42 ->method('assign')
43 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
44 $assignedVariables[$key] = $value;
45
46 return $this;
47 })
48 ;
49
50 $result = $this->controller->index($request, $response);
51
52 static::assertInstanceOf(Response::class, $result);
53 static::assertSame(200, $result->getStatusCode());
54 static::assertSame('loginform', (string) $result->getBody());
55
56 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
57 static::assertSame(true, $assignedVariables['remember_user_default']);
58 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
59 }
60
61 public function testValidControllerInvokeWithUserName(): void
62 {
63 $this->createValidContainerMockSet();
64
65 $request = $this->createMock(Request::class);
66 $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
67 $request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>');
68 $response = new Response();
69
70 $assignedVariables = [];
71 $this->container->pageBuilder
72 ->method('assign')
73 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
74 $assignedVariables[$key] = $value;
75
76 return $this;
77 })
78 ;
79
80 $result = $this->controller->index($request, $response);
81
82 static::assertInstanceOf(Response::class, $result);
83 static::assertSame(200, $result->getStatusCode());
84 static::assertSame('loginform', (string) $result->getBody());
85
86 static::assertSame('myUser&gt;', $assignedVariables['username']);
87 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
88 static::assertSame(true, $assignedVariables['remember_user_default']);
89 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
90 }
91
92 public function testLoginControllerWhileLoggedIn(): void
93 {
94 $request = $this->createMock(Request::class);
95 $response = new Response();
96
97 $loginManager = $this->createMock(LoginManager::class);
98 $loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true);
99 $this->container->loginManager = $loginManager;
100
101 $result = $this->controller->index($request, $response);
102
103 static::assertInstanceOf(Response::class, $result);
104 static::assertSame(302, $result->getStatusCode());
105 static::assertSame(['./'], $result->getHeader('Location'));
106 }
107
108 public function testLoginControllerOpenShaarli(): void
109 {
110 $this->createValidContainerMockSet();
111
112 $request = $this->createMock(Request::class);
113 $response = new Response();
114
115 $conf = $this->createMock(ConfigManager::class);
116 $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
117 if ($parameter === 'security.open_shaarli') {
118 return true;
119 }
120 return $default;
121 });
122 $this->container->conf = $conf;
123
124 $result = $this->controller->index($request, $response);
125
126 static::assertInstanceOf(Response::class, $result);
127 static::assertSame(302, $result->getStatusCode());
128 static::assertSame(['./'], $result->getHeader('Location'));
129 }
130
131 public function testLoginControllerWhileBanned(): void
132 {
133 $this->createValidContainerMockSet();
134
135 $request = $this->createMock(Request::class);
136 $response = new Response();
137
138 $loginManager = $this->createMock(LoginManager::class);
139 $loginManager->method('isLoggedIn')->willReturn(false);
140 $loginManager->method('canLogin')->willReturn(false);
141 $this->container->loginManager = $loginManager;
142
143 $this->expectException(LoginBannedException::class);
144
145 $this->controller->index($request, $response);
146 }
147
148 protected function createValidContainerMockSet(): void
149 {
150 // User logged out
151 $loginManager = $this->createMock(LoginManager::class);
152 $loginManager->method('isLoggedIn')->willReturn(false);
153 $loginManager->method('canLogin')->willReturn(true);
154 $this->container->loginManager = $loginManager;
155
156 // Config
157 $conf = $this->createMock(ConfigManager::class);
158 $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
159 return $default;
160 });
161 $this->container->conf = $conf;
162
163 // PageBuilder
164 $pageBuilder = $this->createMock(PageBuilder::class);
165 $pageBuilder
166 ->method('render')
167 ->willReturnCallback(function (string $template): string {
168 return $template;
169 })
170 ;
171 $this->container->pageBuilder = $pageBuilder;
172
173 $pluginManager = $this->createMock(PluginManager::class);
174 $this->container->pluginManager = $pluginManager;
175 $bookmarkService = $this->createMock(BookmarkServiceInterface::class);
176 $this->container->bookmarkService = $bookmarkService;
177 }
178}
diff --git a/tests/front/controller/ShaarliControllerTest.php b/tests/front/controller/ShaarliControllerTest.php
deleted file mode 100644
index 6fa3feb9..00000000
--- a/tests/front/controller/ShaarliControllerTest.php
+++ /dev/null
@@ -1,116 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkFilter;
9use Shaarli\Bookmark\BookmarkServiceInterface;
10use Shaarli\Container\ShaarliContainer;
11use Shaarli\Plugin\PluginManager;
12use Shaarli\Render\PageBuilder;
13use Shaarli\Security\LoginManager;
14
15/**
16 * Class ShaarliControllerTest
17 *
18 * This class is used to test default behavior of ShaarliController abstract class.
19 * It uses a dummy non abstract controller.
20 */
21class ShaarliControllerTest extends TestCase
22{
23 /** @var ShaarliContainer */
24 protected $container;
25
26 /** @var LoginController */
27 protected $controller;
28
29 /** @var mixed[] List of variable assigned to the template */
30 protected $assignedValues;
31
32 public function setUp(): void
33 {
34 $this->container = $this->createMock(ShaarliContainer::class);
35 $this->controller = new class($this->container) extends ShaarliController
36 {
37 public function assignView(string $key, $value): ShaarliController
38 {
39 return parent::assignView($key, $value);
40 }
41
42 public function render(string $template): string
43 {
44 return parent::render($template);
45 }
46 };
47 $this->assignedValues = [];
48 }
49
50 public function testAssignView(): void
51 {
52 $this->createValidContainerMockSet();
53
54 $self = $this->controller->assignView('variableName', 'variableValue');
55
56 static::assertInstanceOf(ShaarliController::class, $self);
57 static::assertSame('variableValue', $this->assignedValues['variableName']);
58 }
59
60 public function testRender(): void
61 {
62 $this->createValidContainerMockSet();
63
64 $render = $this->controller->render('templateName');
65
66 static::assertSame('templateName', $render);
67
68 static::assertSame(10, $this->assignedValues['linkcount']);
69 static::assertSame(5, $this->assignedValues['privateLinkcount']);
70 static::assertSame(['error'], $this->assignedValues['plugin_errors']);
71
72 static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']);
73 static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']);
74 static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']);
75 static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']);
76 static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
77 static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
78 }
79
80 protected function createValidContainerMockSet(): void
81 {
82 $pageBuilder = $this->createMock(PageBuilder::class);
83 $pageBuilder
84 ->method('assign')
85 ->willReturnCallback(function (string $key, $value): void {
86 $this->assignedValues[$key] = $value;
87 });
88 $pageBuilder
89 ->method('render')
90 ->willReturnCallback(function (string $template): string {
91 return $template;
92 });
93 $this->container->pageBuilder = $pageBuilder;
94
95 $bookmarkService = $this->createMock(BookmarkServiceInterface::class);
96 $bookmarkService
97 ->method('count')
98 ->willReturnCallback(function (string $visibility): int {
99 return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10;
100 });
101 $this->container->bookmarkService = $bookmarkService;
102
103 $pluginManager = $this->createMock(PluginManager::class);
104 $pluginManager
105 ->method('executeHooks')
106 ->willReturnCallback(function (string $hook, array &$data, array $params): array {
107 return $data[$hook] = $params;
108 });
109 $pluginManager->method('getErrors')->willReturn(['error']);
110 $this->container->pluginManager = $pluginManager;
111
112 $loginManager = $this->createMock(LoginManager::class);
113 $loginManager->method('isLoggedIn')->willReturn(true);
114 $this->container->loginManager = $loginManager;
115 }
116}
diff --git a/tests/front/controller/admin/ConfigureControllerTest.php b/tests/front/controller/admin/ConfigureControllerTest.php
new file mode 100644
index 00000000..f2f84bac
--- /dev/null
+++ b/tests/front/controller/admin/ConfigureControllerTest.php
@@ -0,0 +1,252 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Security\SessionManager;
11use Shaarli\Thumbnailer;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class ConfigureControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ConfigureController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new ConfigureController($this->container);
27 }
28
29 /**
30 * Test displaying configure page - it should display all config variables
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $this->container->conf = $this->createMock(ConfigManager::class);
41 $this->container->conf->method('get')->willReturnCallback(function (string $key) {
42 return $key;
43 });
44
45 $result = $this->controller->index($request, $response);
46
47 static::assertSame(200, $result->getStatusCode());
48 static::assertSame('configure', (string) $result->getBody());
49
50 static::assertSame('Configure - general.title', $assignedVariables['pagetitle']);
51 static::assertSame('general.title', $assignedVariables['title']);
52 static::assertSame('resource.theme', $assignedVariables['theme']);
53 static::assertEmpty($assignedVariables['theme_available']);
54 static::assertSame(['default', 'markdown'], $assignedVariables['formatter_available']);
55 static::assertNotEmpty($assignedVariables['continents']);
56 static::assertNotEmpty($assignedVariables['cities']);
57 static::assertSame('general.retrieve_description', $assignedVariables['retrieve_description']);
58 static::assertSame('privacy.default_private_links', $assignedVariables['private_links_default']);
59 static::assertSame('security.session_protection_disabled', $assignedVariables['session_protection_disabled']);
60 static::assertSame('feed.rss_permalinks', $assignedVariables['enable_rss_permalinks']);
61 static::assertSame('updates.check_updates', $assignedVariables['enable_update_check']);
62 static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']);
63 static::assertSame('api.enabled', $assignedVariables['api_enabled']);
64 static::assertSame('api.secret', $assignedVariables['api_secret']);
65 static::assertCount(4, $assignedVariables['languages']);
66 static::assertArrayHasKey('gd_enabled', $assignedVariables);
67 static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']);
68 }
69
70 /**
71 * Test posting a new config - make sure that everything is saved properly, without errors.
72 */
73 public function testSaveNewConfig(): void
74 {
75 $session = [];
76 $this->assignSessionVars($session);
77
78 $parameters = [
79 'token' => 'token',
80 'continent' => 'Europe',
81 'city' => 'Moscow',
82 'title' => 'Shaarli',
83 'titleLink' => './',
84 'retrieveDescription' => 'on',
85 'theme' => 'vintage',
86 'disablesessionprotection' => null,
87 'privateLinkByDefault' => true,
88 'enableRssPermalinks' => true,
89 'updateCheck' => false,
90 'hidePublicLinks' => 'on',
91 'enableApi' => 'on',
92 'apiSecret' => 'abcdef',
93 'formatter' => 'markdown',
94 'language' => 'fr',
95 'enableThumbnails' => Thumbnailer::MODE_NONE,
96 ];
97
98 $parametersConfigMapping = [
99 'general.timezone' => $parameters['continent'] . '/' . $parameters['city'],
100 'general.title' => $parameters['title'],
101 'general.header_link' => $parameters['titleLink'],
102 'general.retrieve_description' => !!$parameters['retrieveDescription'],
103 'resource.theme' => $parameters['theme'],
104 'security.session_protection_disabled' => !!$parameters['disablesessionprotection'],
105 'privacy.default_private_links' => !!$parameters['privateLinkByDefault'],
106 'feed.rss_permalinks' => !!$parameters['enableRssPermalinks'],
107 'updates.check_updates' => !!$parameters['updateCheck'],
108 'privacy.hide_public_links' => !!$parameters['hidePublicLinks'],
109 'api.enabled' => !!$parameters['enableApi'],
110 'api.secret' => $parameters['apiSecret'],
111 'formatter' => $parameters['formatter'],
112 'translation.language' => $parameters['language'],
113 'thumbnails.mode' => $parameters['enableThumbnails'],
114 ];
115
116 $request = $this->createMock(Request::class);
117 $request
118 ->expects(static::atLeastOnce())
119 ->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
120 if (false === array_key_exists($key, $parameters)) {
121 static::fail('unknown key: ' . $key);
122 }
123
124 return $parameters[$key];
125 }
126 );
127
128 $response = new Response();
129
130 $this->container->conf = $this->createMock(ConfigManager::class);
131 $this->container->conf
132 ->expects(static::atLeastOnce())
133 ->method('set')
134 ->willReturnCallback(function (string $key, $value) use ($parametersConfigMapping): void {
135 if (false === array_key_exists($key, $parametersConfigMapping)) {
136 static::fail('unknown key: ' . $key);
137 }
138
139 static::assertSame($parametersConfigMapping[$key], $value);
140 }
141 );
142
143 $result = $this->controller->save($request, $response);
144 static::assertSame(302, $result->getStatusCode());
145 static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
146
147 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
148 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
149 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
150 static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
151 }
152
153 /**
154 * Test posting a new config - wrong token.
155 */
156 public function testSaveNewConfigWrongToken(): void
157 {
158 $this->container->sessionManager = $this->createMock(SessionManager::class);
159 $this->container->sessionManager->method('checkToken')->willReturn(false);
160
161 $this->container->conf->expects(static::never())->method('set');
162 $this->container->conf->expects(static::never())->method('write');
163
164 $request = $this->createMock(Request::class);
165 $response = new Response();
166
167 $this->expectException(WrongTokenException::class);
168
169 $this->controller->save($request, $response);
170 }
171
172 /**
173 * Test posting a new config - thumbnail activation.
174 */
175 public function testSaveNewConfigThumbnailsActivation(): void
176 {
177 $session = [];
178 $this->assignSessionVars($session);
179
180 $request = $this->createMock(Request::class);
181 $request
182 ->expects(static::atLeastOnce())
183 ->method('getParam')->willReturnCallback(function (string $key) {
184 if ('enableThumbnails' === $key) {
185 return Thumbnailer::MODE_ALL;
186 }
187
188 return $key;
189 })
190 ;
191 $response = new Response();
192
193 $result = $this->controller->save($request, $response);
194
195 static::assertSame(302, $result->getStatusCode());
196 static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
197
198 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
199 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
200 static::assertStringContainsString(
201 'You have enabled or changed thumbnails mode',
202 $session[SessionManager::KEY_WARNING_MESSAGES][0]
203 );
204 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
205 static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
206 }
207
208 /**
209 * Test posting a new config - thumbnail activation.
210 */
211 public function testSaveNewConfigThumbnailsAlreadyActive(): void
212 {
213 $session = [];
214 $this->assignSessionVars($session);
215
216 $request = $this->createMock(Request::class);
217 $request
218 ->expects(static::atLeastOnce())
219 ->method('getParam')->willReturnCallback(function (string $key) {
220 if ('enableThumbnails' === $key) {
221 return Thumbnailer::MODE_ALL;
222 }
223
224 return $key;
225 })
226 ;
227 $response = new Response();
228
229 $this->container->conf = $this->createMock(ConfigManager::class);
230 $this->container->conf
231 ->expects(static::atLeastOnce())
232 ->method('get')
233 ->willReturnCallback(function (string $key): string {
234 if ('thumbnails.mode' === $key) {
235 return Thumbnailer::MODE_ALL;
236 }
237
238 return $key;
239 })
240 ;
241
242 $result = $this->controller->save($request, $response);
243
244 static::assertSame(302, $result->getStatusCode());
245 static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
246
247 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
248 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
249 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
250 static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
251 }
252}
diff --git a/tests/front/controller/admin/ExportControllerTest.php b/tests/front/controller/admin/ExportControllerTest.php
new file mode 100644
index 00000000..50d9e378
--- /dev/null
+++ b/tests/front/controller/admin/ExportControllerTest.php
@@ -0,0 +1,163 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\BookmarkRawFormatter;
11use Shaarli\Netscape\NetscapeBookmarkUtils;
12use Shaarli\Security\SessionManager;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class ExportControllerTest extends TestCase
17{
18 use FrontAdminControllerMockHelper;
19
20 /** @var ExportController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->controller = new ExportController($this->container);
28 }
29
30 /**
31 * Test displaying export page
32 */
33 public function testIndex(): void
34 {
35 $assignedVariables = [];
36 $this->assignTemplateVars($assignedVariables);
37
38 $request = $this->createMock(Request::class);
39 $response = new Response();
40
41 $result = $this->controller->index($request, $response);
42
43 static::assertSame(200, $result->getStatusCode());
44 static::assertSame('export', (string) $result->getBody());
45
46 static::assertSame('Export - Shaarli', $assignedVariables['pagetitle']);
47 }
48
49 /**
50 * Test posting an export request
51 */
52 public function testExportDefault(): void
53 {
54 $assignedVariables = [];
55 $this->assignTemplateVars($assignedVariables);
56
57 $parameters = [
58 'selection' => 'all',
59 'prepend_note_url' => 'on',
60 ];
61
62 $request = $this->createMock(Request::class);
63 $request->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
64 return $parameters[$key] ?? null;
65 });
66 $response = new Response();
67
68 $bookmarks = [
69 (new Bookmark())->setUrl('http://link1.tld')->setTitle('Title 1'),
70 (new Bookmark())->setUrl('http://link2.tld')->setTitle('Title 2'),
71 ];
72
73 $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
74 $this->container->netscapeBookmarkUtils
75 ->expects(static::once())
76 ->method('filterAndFormat')
77 ->willReturnCallback(
78 function (
79 BookmarkFormatter $formatter,
80 string $selection,
81 bool $prependNoteUrl,
82 string $indexUrl
83 ) use ($parameters, $bookmarks): array {
84 static::assertInstanceOf(BookmarkRawFormatter::class, $formatter);
85 static::assertSame($parameters['selection'], $selection);
86 static::assertTrue($prependNoteUrl);
87 static::assertSame('http://shaarli', $indexUrl);
88
89 return $bookmarks;
90 }
91 )
92 ;
93
94 $result = $this->controller->export($request, $response);
95
96 static::assertSame(200, $result->getStatusCode());
97 static::assertSame('export.bookmarks', (string) $result->getBody());
98 static::assertSame(['text/html; charset=utf-8'], $result->getHeader('content-type'));
99 static::assertRegExp(
100 '/attachment; filename=bookmarks_all_[\d]{8}_[\d]{6}\.html/',
101 $result->getHeader('content-disposition')[0]
102 );
103
104 static::assertNotEmpty($assignedVariables['date']);
105 static::assertSame(PHP_EOL, $assignedVariables['eol']);
106 static::assertSame('all', $assignedVariables['selection']);
107 static::assertSame($bookmarks, $assignedVariables['links']);
108 }
109
110 /**
111 * Test posting an export request - without selection parameter
112 */
113 public function testExportSelectionMissing(): void
114 {
115 $request = $this->createMock(Request::class);
116 $response = new Response();
117
118 $this->container->sessionManager
119 ->expects(static::once())
120 ->method('setSessionParameter')
121 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Please select an export mode.'])
122 ;
123
124 $result = $this->controller->export($request, $response);
125
126 static::assertSame(302, $result->getStatusCode());
127 static::assertSame(['/subfolder/admin/export'], $result->getHeader('location'));
128 }
129
130 /**
131 * Test posting an export request - without selection parameter
132 */
133 public function testExportErrorEncountered(): void
134 {
135 $parameters = [
136 'selection' => 'all',
137 ];
138
139 $request = $this->createMock(Request::class);
140 $request->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
141 return $parameters[$key] ?? null;
142 });
143 $response = new Response();
144
145 $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
146 $this->container->netscapeBookmarkUtils
147 ->expects(static::once())
148 ->method('filterAndFormat')
149 ->willThrowException(new \Exception($message = 'error message'));
150 ;
151
152 $this->container->sessionManager
153 ->expects(static::once())
154 ->method('setSessionParameter')
155 ->with(SessionManager::KEY_ERROR_MESSAGES, [$message])
156 ;
157
158 $result = $this->controller->export($request, $response);
159
160 static::assertSame(302, $result->getStatusCode());
161 static::assertSame(['/subfolder/admin/export'], $result->getHeader('location'));
162 }
163}
diff --git a/tests/front/controller/admin/FrontAdminControllerMockHelper.php b/tests/front/controller/admin/FrontAdminControllerMockHelper.php
new file mode 100644
index 00000000..2b9f2ef1
--- /dev/null
+++ b/tests/front/controller/admin/FrontAdminControllerMockHelper.php
@@ -0,0 +1,56 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Container\ShaarliTestContainer;
8use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
9use Shaarli\History;
10
11/**
12 * Trait FrontControllerMockHelper
13 *
14 * Helper trait used to initialize the ShaarliContainer and mock its services for admin controller tests.
15 *
16 * @property ShaarliTestContainer $container
17 */
18trait FrontAdminControllerMockHelper
19{
20 use FrontControllerMockHelper {
21 FrontControllerMockHelper::createContainer as parentCreateContainer;
22 }
23
24 /**
25 * Mock the container instance
26 */
27 protected function createContainer(): void
28 {
29 $this->parentCreateContainer();
30
31 $this->container->history = $this->createMock(History::class);
32
33 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
34 $this->container->sessionManager->method('checkToken')->willReturn(true);
35 }
36
37
38 /**
39 * Pass a reference of an array which will be populated by `sessionManager->setSessionParameter`
40 * calls during execution.
41 *
42 * @param mixed $variables Array reference to populate.
43 */
44 protected function assignSessionVars(array &$variables): void
45 {
46 $this->container->sessionManager
47 ->expects(static::atLeastOnce())
48 ->method('setSessionParameter')
49 ->willReturnCallback(function ($key, $value) use (&$variables) {
50 $variables[$key] = $value;
51
52 return $this->container->sessionManager;
53 })
54 ;
55 }
56}
diff --git a/tests/front/controller/admin/ImportControllerTest.php b/tests/front/controller/admin/ImportControllerTest.php
new file mode 100644
index 00000000..eb31fad0
--- /dev/null
+++ b/tests/front/controller/admin/ImportControllerTest.php
@@ -0,0 +1,148 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Psr\Http\Message\UploadedFileInterface;
9use Shaarli\Netscape\NetscapeBookmarkUtils;
10use Shaarli\Security\SessionManager;
11use Slim\Http\Request;
12use Slim\Http\Response;
13use Slim\Http\UploadedFile;
14
15class ImportControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ImportController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new ImportController($this->container);
27 }
28
29 /**
30 * Test displaying import page
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $result = $this->controller->index($request, $response);
41
42 static::assertSame(200, $result->getStatusCode());
43 static::assertSame('import', (string) $result->getBody());
44
45 static::assertSame('Import - Shaarli', $assignedVariables['pagetitle']);
46 static::assertIsInt($assignedVariables['maxfilesize']);
47 static::assertRegExp('/\d+[KM]iB/', $assignedVariables['maxfilesizeHuman']);
48 }
49
50 /**
51 * Test importing a file with default and valid parameters
52 */
53 public function testImportDefault(): void
54 {
55 $parameters = [
56 'abc' => 'def',
57 'other' => 'param',
58 ];
59
60 $requestFile = new UploadedFile('file', 'name', 'type', 123);
61
62 $request = $this->createMock(Request::class);
63 $request->method('getParams')->willReturnCallback(function () use ($parameters) {
64 return $parameters;
65 });
66 $request->method('getUploadedFiles')->willReturn(['filetoupload' => $requestFile]);
67 $response = new Response();
68
69 $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
70 $this->container->netscapeBookmarkUtils
71 ->expects(static::once())
72 ->method('import')
73 ->willReturnCallback(
74 function (
75 array $post,
76 UploadedFileInterface $file
77 ) use ($parameters, $requestFile): string {
78 static::assertSame($parameters, $post);
79 static::assertSame($requestFile, $file);
80
81 return 'status';
82 }
83 )
84 ;
85
86 $this->container->sessionManager
87 ->expects(static::once())
88 ->method('setSessionParameter')
89 ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['status'])
90 ;
91
92 $result = $this->controller->import($request, $response);
93
94 static::assertSame(302, $result->getStatusCode());
95 static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
96 }
97
98 /**
99 * Test posting an import request - without import file
100 */
101 public function testImportFileMissing(): void
102 {
103 $request = $this->createMock(Request::class);
104 $response = new Response();
105
106 $this->container->sessionManager
107 ->expects(static::once())
108 ->method('setSessionParameter')
109 ->with(SessionManager::KEY_ERROR_MESSAGES, ['No import file provided.'])
110 ;
111
112 $result = $this->controller->import($request, $response);
113
114 static::assertSame(302, $result->getStatusCode());
115 static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
116 }
117
118 /**
119 * Test posting an import request - with an empty file
120 */
121 public function testImportEmptyFile(): void
122 {
123 $requestFile = new UploadedFile('file', 'name', 'type', 0);
124
125 $request = $this->createMock(Request::class);
126 $request->method('getUploadedFiles')->willReturn(['filetoupload' => $requestFile]);
127 $response = new Response();
128
129 $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
130 $this->container->netscapeBookmarkUtils->expects(static::never())->method('filterAndFormat');
131
132 $this->container->sessionManager
133 ->expects(static::once())
134 ->method('setSessionParameter')
135 ->willReturnCallback(function (string $key, array $value): SessionManager {
136 static::assertSame(SessionManager::KEY_ERROR_MESSAGES, $key);
137 static::assertStringStartsWith('The file you are trying to upload is probably bigger', $value[0]);
138
139 return $this->container->sessionManager;
140 })
141 ;
142
143 $result = $this->controller->import($request, $response);
144
145 static::assertSame(302, $result->getStatusCode());
146 static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
147 }
148}
diff --git a/tests/front/controller/admin/LogoutControllerTest.php b/tests/front/controller/admin/LogoutControllerTest.php
new file mode 100644
index 00000000..45e84dc0
--- /dev/null
+++ b/tests/front/controller/admin/LogoutControllerTest.php
@@ -0,0 +1,51 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Security\CookieManager;
9use Shaarli\Security\LoginManager;
10use Shaarli\Security\SessionManager;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class LogoutControllerTest extends TestCase
15{
16 use FrontAdminControllerMockHelper;
17
18 /** @var LogoutController */
19 protected $controller;
20
21 public function setUp(): void
22 {
23 $this->createContainer();
24
25 $this->controller = new LogoutController($this->container);
26 }
27
28 public function testValidControllerInvoke(): void
29 {
30 $request = $this->createMock(Request::class);
31 $response = new Response();
32
33 $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches');
34
35 $this->container->sessionManager = $this->createMock(SessionManager::class);
36 $this->container->sessionManager->expects(static::once())->method('logout');
37
38 $this->container->cookieManager = $this->createMock(CookieManager::class);
39 $this->container->cookieManager
40 ->expects(static::once())
41 ->method('setCookieParameter')
42 ->with(CookieManager::STAY_SIGNED_IN, 'false', 0, '/subfolder/')
43 ;
44
45 $result = $this->controller->index($request, $response);
46
47 static::assertInstanceOf(Response::class, $result);
48 static::assertSame(302, $result->getStatusCode());
49 static::assertSame(['/subfolder/'], $result->getHeader('location'));
50 }
51}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
new file mode 100644
index 00000000..7d5b752a
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
@@ -0,0 +1,47 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
9use Shaarli\Front\Controller\Admin\ManageShaareController;
10use Shaarli\Http\HttpAccess;
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/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php
new file mode 100644
index 00000000..5a615791
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php
@@ -0,0 +1,418 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Formatter\BookmarkFormatter;
11use Shaarli\Formatter\BookmarkRawFormatter;
12use Shaarli\Formatter\FormatterFactory;
13use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
14use Shaarli\Front\Controller\Admin\ManageShaareController;
15use Shaarli\Http\HttpAccess;
16use Shaarli\Security\SessionManager;
17use Slim\Http\Request;
18use Slim\Http\Response;
19
20class ChangeVisibilityBookmarkTest extends TestCase
21{
22 use FrontAdminControllerMockHelper;
23
24 /** @var ManageShaareController */
25 protected $controller;
26
27 public function setUp(): void
28 {
29 $this->createContainer();
30
31 $this->container->httpAccess = $this->createMock(HttpAccess::class);
32 $this->controller = new ManageShaareController($this->container);
33 }
34
35 /**
36 * Change bookmark visibility - Set private - Single public bookmark with valid parameters
37 */
38 public function testSetSingleBookmarkPrivate(): void
39 {
40 $parameters = ['id' => '123', 'newVisibility' => 'private'];
41
42 $request = $this->createMock(Request::class);
43 $request
44 ->method('getParam')
45 ->willReturnCallback(function (string $key) use ($parameters): ?string {
46 return $parameters[$key] ?? null;
47 })
48 ;
49 $response = new Response();
50
51 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(false);
52
53 static::assertFalse($bookmark->isPrivate());
54
55 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
56 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
57 $this->container->bookmarkService->expects(static::once())->method('save');
58 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
59 $this->container->formatterFactory
60 ->expects(static::once())
61 ->method('getFormatter')
62 ->with('raw')
63 ->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
64 return new BookmarkRawFormatter($this->container->conf, true);
65 })
66 ;
67
68 // Make sure that PluginManager hook is triggered
69 $this->container->pluginManager
70 ->expects(static::once())
71 ->method('executeHooks')
72 ->with('save_link')
73 ;
74
75 $result = $this->controller->changeVisibility($request, $response);
76
77 static::assertTrue($bookmark->isPrivate());
78
79 static::assertSame(302, $result->getStatusCode());
80 static::assertSame(['/subfolder/'], $result->getHeader('location'));
81 }
82
83 /**
84 * Change bookmark visibility - Set public - Single private bookmark with valid parameters
85 */
86 public function testSetSingleBookmarkPublic(): void
87 {
88 $parameters = ['id' => '123', 'newVisibility' => 'public'];
89
90 $request = $this->createMock(Request::class);
91 $request
92 ->method('getParam')
93 ->willReturnCallback(function (string $key) use ($parameters): ?string {
94 return $parameters[$key] ?? null;
95 })
96 ;
97 $response = new Response();
98
99 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true);
100
101 static::assertTrue($bookmark->isPrivate());
102
103 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
104 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
105 $this->container->bookmarkService->expects(static::once())->method('save');
106 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
107 $this->container->formatterFactory
108 ->expects(static::once())
109 ->method('getFormatter')
110 ->with('raw')
111 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
112 ;
113
114 // Make sure that PluginManager hook is triggered
115 $this->container->pluginManager
116 ->expects(static::once())
117 ->method('executeHooks')
118 ->with('save_link')
119 ;
120
121 $result = $this->controller->changeVisibility($request, $response);
122
123 static::assertFalse($bookmark->isPrivate());
124
125 static::assertSame(302, $result->getStatusCode());
126 static::assertSame(['/subfolder/'], $result->getHeader('location'));
127 }
128
129 /**
130 * Change bookmark visibility - Set private on single already private bookmark
131 */
132 public function testSetSinglePrivateBookmarkPrivate(): void
133 {
134 $parameters = ['id' => '123', 'newVisibility' => 'private'];
135
136 $request = $this->createMock(Request::class);
137 $request
138 ->method('getParam')
139 ->willReturnCallback(function (string $key) use ($parameters): ?string {
140 return $parameters[$key] ?? null;
141 })
142 ;
143 $response = new Response();
144
145 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true);
146
147 static::assertTrue($bookmark->isPrivate());
148
149 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
150 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
151 $this->container->bookmarkService->expects(static::once())->method('save');
152 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
153 $this->container->formatterFactory
154 ->expects(static::once())
155 ->method('getFormatter')
156 ->with('raw')
157 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
158 ;
159
160 // Make sure that PluginManager hook is triggered
161 $this->container->pluginManager
162 ->expects(static::once())
163 ->method('executeHooks')
164 ->with('save_link')
165 ;
166
167 $result = $this->controller->changeVisibility($request, $response);
168
169 static::assertTrue($bookmark->isPrivate());
170
171 static::assertSame(302, $result->getStatusCode());
172 static::assertSame(['/subfolder/'], $result->getHeader('location'));
173 }
174
175 /**
176 * Change bookmark visibility - Set multiple bookmarks private
177 */
178 public function testSetMultipleBookmarksPrivate(): void
179 {
180 $parameters = ['id' => '123 456 789', 'newVisibility' => 'private'];
181
182 $request = $this->createMock(Request::class);
183 $request
184 ->method('getParam')
185 ->willReturnCallback(function (string $key) use ($parameters): ?string {
186 return $parameters[$key] ?? null;
187 })
188 ;
189 $response = new Response();
190
191 $bookmarks = [
192 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(false),
193 (new Bookmark())->setId(456)->setUrl('http://domain.tld')->setTitle('Title 456')->setPrivate(true),
194 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789')->setPrivate(false),
195 ];
196
197 $this->container->bookmarkService
198 ->expects(static::exactly(3))
199 ->method('get')
200 ->withConsecutive([123], [456], [789])
201 ->willReturnOnConsecutiveCalls(...$bookmarks)
202 ;
203 $this->container->bookmarkService
204 ->expects(static::exactly(3))
205 ->method('set')
206 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
207 return [$bookmark, false];
208 }, $bookmarks))
209 ;
210 $this->container->bookmarkService->expects(static::once())->method('save');
211 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
212 $this->container->formatterFactory
213 ->expects(static::once())
214 ->method('getFormatter')
215 ->with('raw')
216 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
217 ;
218
219 // Make sure that PluginManager hook is triggered
220 $this->container->pluginManager
221 ->expects(static::exactly(3))
222 ->method('executeHooks')
223 ->with('save_link')
224 ;
225
226 $result = $this->controller->changeVisibility($request, $response);
227
228 static::assertTrue($bookmarks[0]->isPrivate());
229 static::assertTrue($bookmarks[1]->isPrivate());
230 static::assertTrue($bookmarks[2]->isPrivate());
231
232 static::assertSame(302, $result->getStatusCode());
233 static::assertSame(['/subfolder/'], $result->getHeader('location'));
234 }
235
236 /**
237 * Change bookmark visibility - Single bookmark not found.
238 */
239 public function testChangeVisibilitySingleBookmarkNotFound(): void
240 {
241 $parameters = ['id' => '123', 'newVisibility' => 'private'];
242
243 $request = $this->createMock(Request::class);
244 $request
245 ->method('getParam')
246 ->willReturnCallback(function (string $key) use ($parameters): ?string {
247 return $parameters[$key] ?? null;
248 })
249 ;
250 $response = new Response();
251
252 $this->container->bookmarkService
253 ->expects(static::once())
254 ->method('get')
255 ->willThrowException(new BookmarkNotFoundException())
256 ;
257 $this->container->bookmarkService->expects(static::never())->method('set');
258 $this->container->bookmarkService->expects(static::never())->method('save');
259 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
260 $this->container->formatterFactory
261 ->expects(static::once())
262 ->method('getFormatter')
263 ->with('raw')
264 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
265 ;
266
267 // Make sure that PluginManager hook is not triggered
268 $this->container->pluginManager
269 ->expects(static::never())
270 ->method('executeHooks')
271 ->with('save_link')
272 ;
273
274 $result = $this->controller->changeVisibility($request, $response);
275
276 static::assertSame(302, $result->getStatusCode());
277 static::assertSame(['/subfolder/'], $result->getHeader('location'));
278 }
279
280 /**
281 * Change bookmark visibility - Multiple bookmarks with one not found.
282 */
283 public function testChangeVisibilityMultipleBookmarksOneNotFound(): void
284 {
285 $parameters = ['id' => '123 456 789', 'newVisibility' => 'public'];
286
287 $request = $this->createMock(Request::class);
288 $request
289 ->method('getParam')
290 ->willReturnCallback(function (string $key) use ($parameters): ?string {
291 return $parameters[$key] ?? null;
292 })
293 ;
294 $response = new Response();
295
296 $bookmarks = [
297 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true),
298 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789')->setPrivate(false),
299 ];
300
301 $this->container->bookmarkService
302 ->expects(static::exactly(3))
303 ->method('get')
304 ->withConsecutive([123], [456], [789])
305 ->willReturnCallback(function (int $id) use ($bookmarks): Bookmark {
306 if ($id === 123) {
307 return $bookmarks[0];
308 }
309 if ($id === 789) {
310 return $bookmarks[1];
311 }
312 throw new BookmarkNotFoundException();
313 })
314 ;
315 $this->container->bookmarkService
316 ->expects(static::exactly(2))
317 ->method('set')
318 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
319 return [$bookmark, false];
320 }, $bookmarks))
321 ;
322 $this->container->bookmarkService->expects(static::once())->method('save');
323
324 // Make sure that PluginManager hook is not triggered
325 $this->container->pluginManager
326 ->expects(static::exactly(2))
327 ->method('executeHooks')
328 ->with('save_link')
329 ;
330
331 $this->container->sessionManager
332 ->expects(static::once())
333 ->method('setSessionParameter')
334 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 456 could not be found.'])
335 ;
336
337 $result = $this->controller->changeVisibility($request, $response);
338
339 static::assertSame(302, $result->getStatusCode());
340 static::assertSame(['/subfolder/'], $result->getHeader('location'));
341 }
342
343 /**
344 * Change bookmark visibility - Invalid ID
345 */
346 public function testChangeVisibilityInvalidId(): void
347 {
348 $parameters = ['id' => 'nope not an ID', 'newVisibility' => 'private'];
349
350 $request = $this->createMock(Request::class);
351 $request
352 ->method('getParam')
353 ->willReturnCallback(function (string $key) use ($parameters): ?string {
354 return $parameters[$key] ?? null;
355 })
356 ;
357 $response = new Response();
358
359 $this->container->sessionManager
360 ->expects(static::once())
361 ->method('setSessionParameter')
362 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
363 ;
364
365 $result = $this->controller->changeVisibility($request, $response);
366
367 static::assertSame(302, $result->getStatusCode());
368 static::assertSame(['/subfolder/'], $result->getHeader('location'));
369 }
370
371 /**
372 * Change bookmark visibility - Empty ID
373 */
374 public function testChangeVisibilityEmptyId(): void
375 {
376 $request = $this->createMock(Request::class);
377 $response = new Response();
378
379 $this->container->sessionManager
380 ->expects(static::once())
381 ->method('setSessionParameter')
382 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
383 ;
384
385 $result = $this->controller->changeVisibility($request, $response);
386
387 static::assertSame(302, $result->getStatusCode());
388 static::assertSame(['/subfolder/'], $result->getHeader('location'));
389 }
390
391 /**
392 * Change bookmark visibility - with invalid visibility
393 */
394 public function testChangeVisibilityWithInvalidVisibility(): void
395 {
396 $parameters = ['id' => '123', 'newVisibility' => 'invalid'];
397
398 $request = $this->createMock(Request::class);
399 $request
400 ->method('getParam')
401 ->willReturnCallback(function (string $key) use ($parameters): ?string {
402 return $parameters[$key] ?? null;
403 })
404 ;
405 $response = new Response();
406
407 $this->container->sessionManager
408 ->expects(static::once())
409 ->method('setSessionParameter')
410 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid visibility provided.'])
411 ;
412
413 $result = $this->controller->changeVisibility($request, $response);
414
415 static::assertSame(302, $result->getStatusCode());
416 static::assertSame(['/subfolder/'], $result->getHeader('location'));
417 }
418}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php
new file mode 100644
index 00000000..dee622bb
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php
@@ -0,0 +1,376 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Formatter\BookmarkFormatter;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
13use Shaarli\Front\Controller\Admin\ManageShaareController;
14use Shaarli\Http\HttpAccess;
15use Shaarli\Security\SessionManager;
16use Slim\Http\Request;
17use Slim\Http\Response;
18
19class DeleteBookmarkTest extends TestCase
20{
21 use FrontAdminControllerMockHelper;
22
23 /** @var ManageShaareController */
24 protected $controller;
25
26 public function setUp(): void
27 {
28 $this->createContainer();
29
30 $this->container->httpAccess = $this->createMock(HttpAccess::class);
31 $this->controller = new ManageShaareController($this->container);
32 }
33
34 /**
35 * Delete bookmark - Single bookmark with valid parameters
36 */
37 public function testDeleteSingleBookmark(): void
38 {
39 $parameters = ['id' => '123'];
40
41 $request = $this->createMock(Request::class);
42 $request
43 ->method('getParam')
44 ->willReturnCallback(function (string $key) use ($parameters): ?string {
45 return $parameters[$key] ?? null;
46 })
47 ;
48 $response = new Response();
49
50 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123');
51
52 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
53 $this->container->bookmarkService->expects(static::once())->method('remove')->with($bookmark, false);
54 $this->container->bookmarkService->expects(static::once())->method('save');
55 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
56 $this->container->formatterFactory
57 ->expects(static::once())
58 ->method('getFormatter')
59 ->with('raw')
60 ->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
61 $formatter = $this->createMock(BookmarkFormatter::class);
62 $formatter
63 ->expects(static::once())
64 ->method('format')
65 ->with($bookmark)
66 ->willReturn(['formatted' => $bookmark])
67 ;
68
69 return $formatter;
70 })
71 ;
72
73 // Make sure that PluginManager hook is triggered
74 $this->container->pluginManager
75 ->expects(static::once())
76 ->method('executeHooks')
77 ->with('delete_link', ['formatted' => $bookmark])
78 ;
79
80 $result = $this->controller->deleteBookmark($request, $response);
81
82 static::assertSame(302, $result->getStatusCode());
83 static::assertSame(['/subfolder/'], $result->getHeader('location'));
84 }
85
86 /**
87 * Delete bookmark - Multiple bookmarks with valid parameters
88 */
89 public function testDeleteMultipleBookmarks(): void
90 {
91 $parameters = ['id' => '123 456 789'];
92
93 $request = $this->createMock(Request::class);
94 $request
95 ->method('getParam')
96 ->willReturnCallback(function (string $key) use ($parameters): ?string {
97 return $parameters[$key] ?? null;
98 })
99 ;
100 $response = new Response();
101
102 $bookmarks = [
103 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123'),
104 (new Bookmark())->setId(456)->setUrl('http://domain.tld')->setTitle('Title 456'),
105 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789'),
106 ];
107
108 $this->container->bookmarkService
109 ->expects(static::exactly(3))
110 ->method('get')
111 ->withConsecutive([123], [456], [789])
112 ->willReturnOnConsecutiveCalls(...$bookmarks)
113 ;
114 $this->container->bookmarkService
115 ->expects(static::exactly(3))
116 ->method('remove')
117 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
118 return [$bookmark, false];
119 }, $bookmarks))
120 ;
121 $this->container->bookmarkService->expects(static::once())->method('save');
122 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
123 $this->container->formatterFactory
124 ->expects(static::once())
125 ->method('getFormatter')
126 ->with('raw')
127 ->willReturnCallback(function () use ($bookmarks): BookmarkFormatter {
128 $formatter = $this->createMock(BookmarkFormatter::class);
129
130 $formatter
131 ->expects(static::exactly(3))
132 ->method('format')
133 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
134 return [$bookmark];
135 }, $bookmarks))
136 ->willReturnOnConsecutiveCalls(...array_map(function (Bookmark $bookmark): array {
137 return ['formatted' => $bookmark];
138 }, $bookmarks))
139 ;
140
141 return $formatter;
142 })
143 ;
144
145 // Make sure that PluginManager hook is triggered
146 $this->container->pluginManager
147 ->expects(static::exactly(3))
148 ->method('executeHooks')
149 ->with('delete_link')
150 ;
151
152 $result = $this->controller->deleteBookmark($request, $response);
153
154 static::assertSame(302, $result->getStatusCode());
155 static::assertSame(['/subfolder/'], $result->getHeader('location'));
156 }
157
158 /**
159 * Delete bookmark - Single bookmark not found in the data store
160 */
161 public function testDeleteSingleBookmarkNotFound(): void
162 {
163 $parameters = ['id' => '123'];
164
165 $request = $this->createMock(Request::class);
166 $request
167 ->method('getParam')
168 ->willReturnCallback(function (string $key) use ($parameters): ?string {
169 return $parameters[$key] ?? null;
170 })
171 ;
172 $response = new Response();
173
174 $this->container->bookmarkService
175 ->expects(static::once())
176 ->method('get')
177 ->willThrowException(new BookmarkNotFoundException())
178 ;
179 $this->container->bookmarkService->expects(static::never())->method('remove');
180 $this->container->bookmarkService->expects(static::never())->method('save');
181 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
182 $this->container->formatterFactory
183 ->expects(static::once())
184 ->method('getFormatter')
185 ->with('raw')
186 ->willReturnCallback(function (): BookmarkFormatter {
187 $formatter = $this->createMock(BookmarkFormatter::class);
188
189 $formatter->expects(static::never())->method('format');
190
191 return $formatter;
192 })
193 ;
194 // Make sure that PluginManager hook is not triggered
195 $this->container->pluginManager
196 ->expects(static::never())
197 ->method('executeHooks')
198 ->with('delete_link')
199 ;
200
201 $result = $this->controller->deleteBookmark($request, $response);
202
203 static::assertSame(302, $result->getStatusCode());
204 static::assertSame(['/subfolder/'], $result->getHeader('location'));
205 }
206
207 /**
208 * Delete bookmark - Multiple bookmarks with one not found in the data store
209 */
210 public function testDeleteMultipleBookmarksOneNotFound(): void
211 {
212 $parameters = ['id' => '123 456 789'];
213
214 $request = $this->createMock(Request::class);
215 $request
216 ->method('getParam')
217 ->willReturnCallback(function (string $key) use ($parameters): ?string {
218 return $parameters[$key] ?? null;
219 })
220 ;
221 $response = new Response();
222
223 $bookmarks = [
224 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123'),
225 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789'),
226 ];
227
228 $this->container->bookmarkService
229 ->expects(static::exactly(3))
230 ->method('get')
231 ->withConsecutive([123], [456], [789])
232 ->willReturnCallback(function (int $id) use ($bookmarks): Bookmark {
233 if ($id === 123) {
234 return $bookmarks[0];
235 }
236 if ($id === 789) {
237 return $bookmarks[1];
238 }
239 throw new BookmarkNotFoundException();
240 })
241 ;
242 $this->container->bookmarkService
243 ->expects(static::exactly(2))
244 ->method('remove')
245 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
246 return [$bookmark, false];
247 }, $bookmarks))
248 ;
249 $this->container->bookmarkService->expects(static::once())->method('save');
250 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
251 $this->container->formatterFactory
252 ->expects(static::once())
253 ->method('getFormatter')
254 ->with('raw')
255 ->willReturnCallback(function () use ($bookmarks): BookmarkFormatter {
256 $formatter = $this->createMock(BookmarkFormatter::class);
257
258 $formatter
259 ->expects(static::exactly(2))
260 ->method('format')
261 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
262 return [$bookmark];
263 }, $bookmarks))
264 ->willReturnOnConsecutiveCalls(...array_map(function (Bookmark $bookmark): array {
265 return ['formatted' => $bookmark];
266 }, $bookmarks))
267 ;
268
269 return $formatter;
270 })
271 ;
272
273 // Make sure that PluginManager hook is not triggered
274 $this->container->pluginManager
275 ->expects(static::exactly(2))
276 ->method('executeHooks')
277 ->with('delete_link')
278 ;
279
280 $this->container->sessionManager
281 ->expects(static::once())
282 ->method('setSessionParameter')
283 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 456 could not be found.'])
284 ;
285
286 $result = $this->controller->deleteBookmark($request, $response);
287
288 static::assertSame(302, $result->getStatusCode());
289 static::assertSame(['/subfolder/'], $result->getHeader('location'));
290 }
291
292 /**
293 * Delete bookmark - Invalid ID
294 */
295 public function testDeleteInvalidId(): void
296 {
297 $parameters = ['id' => 'nope not an ID'];
298
299 $request = $this->createMock(Request::class);
300 $request
301 ->method('getParam')
302 ->willReturnCallback(function (string $key) use ($parameters): ?string {
303 return $parameters[$key] ?? null;
304 })
305 ;
306 $response = new Response();
307
308 $this->container->sessionManager
309 ->expects(static::once())
310 ->method('setSessionParameter')
311 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
312 ;
313
314 $result = $this->controller->deleteBookmark($request, $response);
315
316 static::assertSame(302, $result->getStatusCode());
317 static::assertSame(['/subfolder/'], $result->getHeader('location'));
318 }
319
320 /**
321 * Delete bookmark - Empty ID
322 */
323 public function testDeleteEmptyId(): void
324 {
325 $request = $this->createMock(Request::class);
326 $response = new Response();
327
328 $this->container->sessionManager
329 ->expects(static::once())
330 ->method('setSessionParameter')
331 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
332 ;
333
334 $result = $this->controller->deleteBookmark($request, $response);
335
336 static::assertSame(302, $result->getStatusCode());
337 static::assertSame(['/subfolder/'], $result->getHeader('location'));
338 }
339
340 /**
341 * Delete bookmark - from bookmarklet
342 */
343 public function testDeleteBookmarkFromBookmarklet(): void
344 {
345 $parameters = [
346 'id' => '123',
347 'source' => 'bookmarklet',
348 ];
349
350 $request = $this->createMock(Request::class);
351 $request
352 ->method('getParam')
353 ->willReturnCallback(function (string $key) use ($parameters): ?string {
354 return $parameters[$key] ?? null;
355 })
356 ;
357 $response = new Response();
358
359 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
360 $this->container->formatterFactory
361 ->expects(static::once())
362 ->method('getFormatter')
363 ->willReturnCallback(function (): BookmarkFormatter {
364 $formatter = $this->createMock(BookmarkFormatter::class);
365 $formatter->method('format')->willReturn(['formatted']);
366
367 return $formatter;
368 })
369 ;
370
371 $result = $this->controller->deleteBookmark($request, $response);
372
373 static::assertSame(200, $result->getStatusCode());
374 static::assertSame('<script>self.close();</script>', (string) $result->getBody('location'));
375 }
376}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
new file mode 100644
index 00000000..777583d5
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
@@ -0,0 +1,315 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
11use Shaarli\Front\Controller\Admin\ManageShaareController;
12use Shaarli\Http\HttpAccess;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class DisplayCreateFormTest extends TestCase
17{
18 use FrontAdminControllerMockHelper;
19
20 /** @var ManageShaareController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->container->httpAccess = $this->createMock(HttpAccess::class);
28 $this->controller = new ManageShaareController($this->container);
29 }
30
31 /**
32 * Test displaying bookmark create form
33 * Ensure that every step of the standard workflow works properly.
34 */
35 public function testDisplayCreateFormWithUrl(): void
36 {
37 $this->container->environment = [
38 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
39 ];
40
41 $assignedVariables = [];
42 $this->assignTemplateVars($assignedVariables);
43
44 $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
45 $expectedUrl = str_replace('&utm_ad=pay', '', $url);
46 $remoteTitle = 'Remote Title';
47 $remoteDesc = 'Sometimes the meta description is relevant.';
48 $remoteTags = 'abc def';
49
50 $request = $this->createMock(Request::class);
51 $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
52 return $key === 'post' ? $url : null;
53 });
54 $response = new Response();
55
56 $this->container->httpAccess
57 ->expects(static::once())
58 ->method('getCurlDownloadCallback')
59 ->willReturnCallback(
60 function (&$charset, &$title, &$description, &$tags) use (
61 $remoteTitle,
62 $remoteDesc,
63 $remoteTags
64 ): callable {
65 return function () use (
66 &$charset,
67 &$title,
68 &$description,
69 &$tags,
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
91 $this->container->bookmarkService
92 ->expects(static::once())
93 ->method('bookmarksCountPerTag')
94 ->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
95 ;
96
97 // Make sure that PluginManager hook is triggered
98 $this->container->pluginManager
99 ->expects(static::at(0))
100 ->method('executeHooks')
101 ->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array {
102 static::assertSame('render_editlink', $hook);
103 static::assertSame($remoteTitle, $data['link']['title']);
104 static::assertSame($remoteDesc, $data['link']['description']);
105
106 return $data;
107 })
108 ;
109
110 $result = $this->controller->displayCreateForm($request, $response);
111
112 static::assertSame(200, $result->getStatusCode());
113 static::assertSame('editlink', (string) $result->getBody());
114
115 static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
116
117 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
118 static::assertSame($remoteTitle, $assignedVariables['link']['title']);
119 static::assertSame($remoteDesc, $assignedVariables['link']['description']);
120 static::assertSame($remoteTags, $assignedVariables['link']['tags']);
121 static::assertFalse($assignedVariables['link']['private']);
122
123 static::assertTrue($assignedVariables['link_is_new']);
124 static::assertSame($referer, $assignedVariables['http_referer']);
125 static::assertSame($tags, $assignedVariables['tags']);
126 static::assertArrayHasKey('source', $assignedVariables);
127 static::assertArrayHasKey('default_private_links', $assignedVariables);
128 }
129
130 /**
131 * Test displaying bookmark create form
132 * Ensure all available query parameters are handled properly.
133 */
134 public function testDisplayCreateFormWithFullParameters(): void
135 {
136 $assignedVariables = [];
137 $this->assignTemplateVars($assignedVariables);
138
139 $parameters = [
140 'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
141 'title' => 'Provided Title',
142 'description' => 'Provided description.',
143 'tags' => 'abc def',
144 'private' => '1',
145 'source' => 'apps',
146 ];
147 $expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']);
148
149 $request = $this->createMock(Request::class);
150 $request
151 ->method('getParam')
152 ->willReturnCallback(function (string $key) use ($parameters): ?string {
153 return $parameters[$key] ?? null;
154 });
155 $response = new Response();
156
157 $result = $this->controller->displayCreateForm($request, $response);
158
159 static::assertSame(200, $result->getStatusCode());
160 static::assertSame('editlink', (string) $result->getBody());
161
162 static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
163
164 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
165 static::assertSame($parameters['title'], $assignedVariables['link']['title']);
166 static::assertSame($parameters['description'], $assignedVariables['link']['description']);
167 static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
168 static::assertTrue($assignedVariables['link']['private']);
169 static::assertTrue($assignedVariables['link_is_new']);
170 static::assertSame($parameters['source'], $assignedVariables['source']);
171 }
172
173 /**
174 * Test displaying bookmark create form
175 * Without any parameter.
176 */
177 public function testDisplayCreateFormEmpty(): void
178 {
179 $assignedVariables = [];
180 $this->assignTemplateVars($assignedVariables);
181
182 $request = $this->createMock(Request::class);
183 $response = new Response();
184
185 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
186 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
187
188 $result = $this->controller->displayCreateForm($request, $response);
189
190 static::assertSame(200, $result->getStatusCode());
191 static::assertSame('editlink', (string) $result->getBody());
192 static::assertSame('', $assignedVariables['link']['url']);
193 static::assertSame('Note: ', $assignedVariables['link']['title']);
194 static::assertSame('', $assignedVariables['link']['description']);
195 static::assertSame('', $assignedVariables['link']['tags']);
196 static::assertFalse($assignedVariables['link']['private']);
197 static::assertTrue($assignedVariables['link_is_new']);
198 }
199
200 /**
201 * Test displaying bookmark create form
202 * URL not using HTTP protocol: do not try to retrieve the title
203 */
204 public function testDisplayCreateFormNotHttp(): void
205 {
206 $assignedVariables = [];
207 $this->assignTemplateVars($assignedVariables);
208
209 $url = 'magnet://kubuntu.torrent';
210 $request = $this->createMock(Request::class);
211 $request
212 ->method('getParam')
213 ->willReturnCallback(function (string $key) use ($url): ?string {
214 return $key === 'post' ? $url : null;
215 });
216 $response = new Response();
217
218 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
219 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
220
221 $result = $this->controller->displayCreateForm($request, $response);
222
223 static::assertSame(200, $result->getStatusCode());
224 static::assertSame('editlink', (string) $result->getBody());
225 static::assertSame($url, $assignedVariables['link']['url']);
226 static::assertTrue($assignedVariables['link_is_new']);
227 }
228
229 /**
230 * Test displaying bookmark create form
231 * When markdown formatter is enabled, the no markdown tag should be added to existing tags.
232 */
233 public function testDisplayCreateFormWithMarkdownEnabled(): void
234 {
235 $assignedVariables = [];
236 $this->assignTemplateVars($assignedVariables);
237
238 $this->container->conf = $this->createMock(ConfigManager::class);
239 $this->container->conf
240 ->expects(static::atLeastOnce())
241 ->method('get')->willReturnCallback(function (string $key): ?string {
242 if ($key === 'formatter') {
243 return 'markdown';
244 }
245
246 return $key;
247 })
248 ;
249
250 $request = $this->createMock(Request::class);
251 $response = new Response();
252
253 $result = $this->controller->displayCreateForm($request, $response);
254
255 static::assertSame(200, $result->getStatusCode());
256 static::assertSame('editlink', (string) $result->getBody());
257 static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']);
258 }
259
260 /**
261 * Test displaying bookmark create form
262 * When an existing URL is submitted, we want to edit the existing link.
263 */
264 public function testDisplayCreateFormWithExistingUrl(): void
265 {
266 $assignedVariables = [];
267 $this->assignTemplateVars($assignedVariables);
268
269 $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
270 $expectedUrl = str_replace('&utm_ad=pay', '', $url);
271
272 $request = $this->createMock(Request::class);
273 $request
274 ->method('getParam')
275 ->willReturnCallback(function (string $key) use ($url): ?string {
276 return $key === 'post' ? $url : null;
277 });
278 $response = new Response();
279
280 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
281 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
282
283 $this->container->bookmarkService
284 ->expects(static::once())
285 ->method('findByUrl')
286 ->with($expectedUrl)
287 ->willReturn(
288 (new Bookmark())
289 ->setId($id = 23)
290 ->setUrl($expectedUrl)
291 ->setTitle($title = 'Bookmark Title')
292 ->setDescription($description = 'Bookmark description.')
293 ->setTags($tags = ['abc', 'def'])
294 ->setPrivate(true)
295 ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
296 )
297 ;
298
299 $result = $this->controller->displayCreateForm($request, $response);
300
301 static::assertSame(200, $result->getStatusCode());
302 static::assertSame('editlink', (string) $result->getBody());
303
304 static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
305 static::assertFalse($assignedVariables['link_is_new']);
306
307 static::assertSame($id, $assignedVariables['link']['id']);
308 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
309 static::assertSame($title, $assignedVariables['link']['title']);
310 static::assertSame($description, $assignedVariables['link']['description']);
311 static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
312 static::assertTrue($assignedVariables['link']['private']);
313 static::assertSame($createdAt, $assignedVariables['link']['created']);
314 }
315}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php
new file mode 100644
index 00000000..1a1cdcf3
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php
@@ -0,0 +1,155 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
11use Shaarli\Front\Controller\Admin\ManageShaareController;
12use Shaarli\Http\HttpAccess;
13use Shaarli\Security\SessionManager;
14use Slim\Http\Request;
15use Slim\Http\Response;
16
17class DisplayEditFormTest extends TestCase
18{
19 use FrontAdminControllerMockHelper;
20
21 /** @var ManageShaareController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->controller = new ManageShaareController($this->container);
30 }
31
32 /**
33 * Test displaying bookmark edit form
34 * When an existing ID is provided, ensure that default workflow works properly.
35 */
36 public function testDisplayEditFormDefault(): void
37 {
38 $assignedVariables = [];
39 $this->assignTemplateVars($assignedVariables);
40
41 $id = 11;
42
43 $request = $this->createMock(Request::class);
44 $response = new Response();
45
46 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
47 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
48
49 $this->container->bookmarkService
50 ->expects(static::once())
51 ->method('get')
52 ->with($id)
53 ->willReturn(
54 (new Bookmark())
55 ->setId($id)
56 ->setUrl($url = 'http://domain.tld')
57 ->setTitle($title = 'Bookmark Title')
58 ->setDescription($description = 'Bookmark description.')
59 ->setTags($tags = ['abc', 'def'])
60 ->setPrivate(true)
61 ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
62 )
63 ;
64
65 $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
66
67 static::assertSame(200, $result->getStatusCode());
68 static::assertSame('editlink', (string) $result->getBody());
69
70 static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
71 static::assertFalse($assignedVariables['link_is_new']);
72
73 static::assertSame($id, $assignedVariables['link']['id']);
74 static::assertSame($url, $assignedVariables['link']['url']);
75 static::assertSame($title, $assignedVariables['link']['title']);
76 static::assertSame($description, $assignedVariables['link']['description']);
77 static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
78 static::assertTrue($assignedVariables['link']['private']);
79 static::assertSame($createdAt, $assignedVariables['link']['created']);
80 }
81
82 /**
83 * Test displaying bookmark edit form
84 * Invalid ID provided.
85 */
86 public function testDisplayEditFormInvalidId(): void
87 {
88 $id = 'invalid';
89
90 $request = $this->createMock(Request::class);
91 $response = new Response();
92
93 $this->container->sessionManager
94 ->expects(static::once())
95 ->method('setSessionParameter')
96 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier invalid could not be found.'])
97 ;
98
99 $result = $this->controller->displayEditForm($request, $response, ['id' => $id]);
100
101 static::assertSame(302, $result->getStatusCode());
102 static::assertSame(['/subfolder/'], $result->getHeader('location'));
103 }
104
105 /**
106 * Test displaying bookmark edit form
107 * ID not provided.
108 */
109 public function testDisplayEditFormIdNotProvided(): void
110 {
111 $request = $this->createMock(Request::class);
112 $response = new Response();
113
114 $this->container->sessionManager
115 ->expects(static::once())
116 ->method('setSessionParameter')
117 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier could not be found.'])
118 ;
119
120 $result = $this->controller->displayEditForm($request, $response, []);
121
122 static::assertSame(302, $result->getStatusCode());
123 static::assertSame(['/subfolder/'], $result->getHeader('location'));
124 }
125
126 /**
127 * Test displaying bookmark edit form
128 * Bookmark not found.
129 */
130 public function testDisplayEditFormBookmarkNotFound(): void
131 {
132 $id = 123;
133
134 $request = $this->createMock(Request::class);
135 $response = new Response();
136
137 $this->container->bookmarkService
138 ->expects(static::once())
139 ->method('get')
140 ->with($id)
141 ->willThrowException(new BookmarkNotFoundException())
142 ;
143
144 $this->container->sessionManager
145 ->expects(static::once())
146 ->method('setSessionParameter')
147 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 123 could not be found.'])
148 ;
149
150 $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
151
152 static::assertSame(302, $result->getStatusCode());
153 static::assertSame(['/subfolder/'], $result->getHeader('location'));
154 }
155}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php
new file mode 100644
index 00000000..1607b475
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php
@@ -0,0 +1,145 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
11use Shaarli\Front\Controller\Admin\ManageShaareController;
12use Shaarli\Http\HttpAccess;
13use Shaarli\Security\SessionManager;
14use Slim\Http\Request;
15use Slim\Http\Response;
16
17class PinBookmarkTest extends TestCase
18{
19 use FrontAdminControllerMockHelper;
20
21 /** @var ManageShaareController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->controller = new ManageShaareController($this->container);
30 }
31
32 /**
33 * Test pin bookmark - with valid input
34 *
35 * @dataProvider initialStickyValuesProvider()
36 */
37 public function testPinBookmarkIsStickyNull(?bool $sticky, bool $expectedValue): void
38 {
39 $id = 123;
40
41 $request = $this->createMock(Request::class);
42 $response = new Response();
43
44 $bookmark = (new Bookmark())
45 ->setId(123)
46 ->setUrl('http://domain.tld')
47 ->setTitle('Title 123')
48 ->setSticky($sticky)
49 ;
50
51 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
52 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, true);
53
54 // Make sure that PluginManager hook is triggered
55 $this->container->pluginManager
56 ->expects(static::once())
57 ->method('executeHooks')
58 ->with('save_link')
59 ;
60
61 $result = $this->controller->pinBookmark($request, $response, ['id' => (string) $id]);
62
63 static::assertSame(302, $result->getStatusCode());
64 static::assertSame(['/subfolder/'], $result->getHeader('location'));
65
66 static::assertSame($expectedValue, $bookmark->isSticky());
67 }
68
69 public function initialStickyValuesProvider(): array
70 {
71 // [initialStickyState, isStickyAfterPin]
72 return [[null, true], [false, true], [true, false]];
73 }
74
75 /**
76 * Test pin bookmark - invalid bookmark ID
77 */
78 public function testDisplayEditFormInvalidId(): void
79 {
80 $id = 'invalid';
81
82 $request = $this->createMock(Request::class);
83 $response = new Response();
84
85 $this->container->sessionManager
86 ->expects(static::once())
87 ->method('setSessionParameter')
88 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier invalid could not be found.'])
89 ;
90
91 $result = $this->controller->pinBookmark($request, $response, ['id' => $id]);
92
93 static::assertSame(302, $result->getStatusCode());
94 static::assertSame(['/subfolder/'], $result->getHeader('location'));
95 }
96
97 /**
98 * Test pin bookmark - Bookmark ID not provided
99 */
100 public function testDisplayEditFormIdNotProvided(): void
101 {
102 $request = $this->createMock(Request::class);
103 $response = new Response();
104
105 $this->container->sessionManager
106 ->expects(static::once())
107 ->method('setSessionParameter')
108 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier could not be found.'])
109 ;
110
111 $result = $this->controller->pinBookmark($request, $response, []);
112
113 static::assertSame(302, $result->getStatusCode());
114 static::assertSame(['/subfolder/'], $result->getHeader('location'));
115 }
116
117 /**
118 * Test pin bookmark - bookmark not found
119 */
120 public function testDisplayEditFormBookmarkNotFound(): void
121 {
122 $id = 123;
123
124 $request = $this->createMock(Request::class);
125 $response = new Response();
126
127 $this->container->bookmarkService
128 ->expects(static::once())
129 ->method('get')
130 ->with($id)
131 ->willThrowException(new BookmarkNotFoundException())
132 ;
133
134 $this->container->sessionManager
135 ->expects(static::once())
136 ->method('setSessionParameter')
137 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 123 could not be found.'])
138 ;
139
140 $result = $this->controller->pinBookmark($request, $response, ['id' => (string) $id]);
141
142 static::assertSame(302, $result->getStatusCode());
143 static::assertSame(['/subfolder/'], $result->getHeader('location'));
144 }
145}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
new file mode 100644
index 00000000..dabcd60d
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
@@ -0,0 +1,282 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
11use Shaarli\Front\Controller\Admin\ManageShaareController;
12use Shaarli\Front\Exception\WrongTokenException;
13use Shaarli\Http\HttpAccess;
14use Shaarli\Security\SessionManager;
15use Shaarli\Thumbnailer;
16use Slim\Http\Request;
17use Slim\Http\Response;
18
19class SaveBookmarkTest extends TestCase
20{
21 use FrontAdminControllerMockHelper;
22
23 /** @var ManageShaareController */
24 protected $controller;
25
26 public function setUp(): void
27 {
28 $this->createContainer();
29
30 $this->container->httpAccess = $this->createMock(HttpAccess::class);
31 $this->controller = new ManageShaareController($this->container);
32 }
33
34 /**
35 * Test save a new bookmark
36 */
37 public function testSaveBookmark(): void
38 {
39 $id = 21;
40 $parameters = [
41 'lf_url' => 'http://url.tld/other?part=3#hash',
42 'lf_title' => 'Provided Title',
43 'lf_description' => 'Provided description.',
44 'lf_tags' => 'abc def',
45 'lf_private' => '1',
46 'returnurl' => 'http://shaarli.tld/subfolder/admin/add-shaare'
47 ];
48
49 $request = $this->createMock(Request::class);
50 $request
51 ->method('getParam')
52 ->willReturnCallback(function (string $key) use ($parameters): ?string {
53 return $parameters[$key] ?? null;
54 })
55 ;
56 $response = new Response();
57
58 $checkBookmark = function (Bookmark $bookmark) use ($parameters) {
59 static::assertSame($parameters['lf_url'], $bookmark->getUrl());
60 static::assertSame($parameters['lf_title'], $bookmark->getTitle());
61 static::assertSame($parameters['lf_description'], $bookmark->getDescription());
62 static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
63 static::assertTrue($bookmark->isPrivate());
64 };
65
66 $this->container->bookmarkService
67 ->expects(static::once())
68 ->method('addOrSet')
69 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
70 static::assertFalse($save);
71
72 $checkBookmark($bookmark);
73
74 $bookmark->setId($id);
75 })
76 ;
77 $this->container->bookmarkService
78 ->expects(static::once())
79 ->method('set')
80 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
81 static::assertTrue($save);
82
83 $checkBookmark($bookmark);
84
85 static::assertSame($id, $bookmark->getId());
86 })
87 ;
88
89 // Make sure that PluginManager hook is triggered
90 $this->container->pluginManager
91 ->expects(static::at(0))
92 ->method('executeHooks')
93 ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
94 static::assertSame('save_link', $hook);
95
96 static::assertSame($id, $data['id']);
97 static::assertSame($parameters['lf_url'], $data['url']);
98 static::assertSame($parameters['lf_title'], $data['title']);
99 static::assertSame($parameters['lf_description'], $data['description']);
100 static::assertSame($parameters['lf_tags'], $data['tags']);
101 static::assertTrue($data['private']);
102
103 return $data;
104 })
105 ;
106
107 $result = $this->controller->save($request, $response);
108
109 static::assertSame(302, $result->getStatusCode());
110 static::assertRegExp('@/subfolder/#[\w\-]{6}@', $result->getHeader('location')[0]);
111 }
112
113
114 /**
115 * Test save an existing bookmark
116 */
117 public function testSaveExistingBookmark(): void
118 {
119 $id = 21;
120 $parameters = [
121 'lf_id' => (string) $id,
122 'lf_url' => 'http://url.tld/other?part=3#hash',
123 'lf_title' => 'Provided Title',
124 'lf_description' => 'Provided description.',
125 'lf_tags' => 'abc def',
126 'lf_private' => '1',
127 'returnurl' => 'http://shaarli.tld/subfolder/?page=2'
128 ];
129
130 $request = $this->createMock(Request::class);
131 $request
132 ->method('getParam')
133 ->willReturnCallback(function (string $key) use ($parameters): ?string {
134 return $parameters[$key] ?? null;
135 })
136 ;
137 $response = new Response();
138
139 $checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) {
140 static::assertSame($id, $bookmark->getId());
141 static::assertSame($parameters['lf_url'], $bookmark->getUrl());
142 static::assertSame($parameters['lf_title'], $bookmark->getTitle());
143 static::assertSame($parameters['lf_description'], $bookmark->getDescription());
144 static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
145 static::assertTrue($bookmark->isPrivate());
146 };
147
148 $this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true);
149 $this->container->bookmarkService
150 ->expects(static::once())
151 ->method('get')
152 ->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url'))
153 ;
154 $this->container->bookmarkService
155 ->expects(static::once())
156 ->method('addOrSet')
157 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
158 static::assertFalse($save);
159
160 $checkBookmark($bookmark);
161 })
162 ;
163 $this->container->bookmarkService
164 ->expects(static::once())
165 ->method('set')
166 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
167 static::assertTrue($save);
168
169 $checkBookmark($bookmark);
170
171 static::assertSame($id, $bookmark->getId());
172 })
173 ;
174
175 // Make sure that PluginManager hook is triggered
176 $this->container->pluginManager
177 ->expects(static::at(0))
178 ->method('executeHooks')
179 ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
180 static::assertSame('save_link', $hook);
181
182 static::assertSame($id, $data['id']);
183 static::assertSame($parameters['lf_url'], $data['url']);
184 static::assertSame($parameters['lf_title'], $data['title']);
185 static::assertSame($parameters['lf_description'], $data['description']);
186 static::assertSame($parameters['lf_tags'], $data['tags']);
187 static::assertTrue($data['private']);
188
189 return $data;
190 })
191 ;
192
193 $result = $this->controller->save($request, $response);
194
195 static::assertSame(302, $result->getStatusCode());
196 static::assertRegExp('@/subfolder/\?page=2#[\w\-]{6}@', $result->getHeader('location')[0]);
197 }
198
199 /**
200 * Test save a bookmark - try to retrieve the thumbnail
201 */
202 public function testSaveBookmarkWithThumbnail(): void
203 {
204 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
205
206 $request = $this->createMock(Request::class);
207 $request
208 ->method('getParam')
209 ->willReturnCallback(function (string $key) use ($parameters): ?string {
210 return $parameters[$key] ?? null;
211 })
212 ;
213 $response = new Response();
214
215 $this->container->conf = $this->createMock(ConfigManager::class);
216 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
217 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
218 });
219
220 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
221 $this->container->thumbnailer
222 ->expects(static::once())
223 ->method('get')
224 ->with($parameters['lf_url'])
225 ->willReturn($thumb = 'http://thumb.url')
226 ;
227
228 $this->container->bookmarkService
229 ->expects(static::once())
230 ->method('addOrSet')
231 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void {
232 static::assertSame($thumb, $bookmark->getThumbnail());
233 })
234 ;
235
236 $result = $this->controller->save($request, $response);
237
238 static::assertSame(302, $result->getStatusCode());
239 }
240
241 /**
242 * Change the password with a wrong existing password
243 */
244 public function testSaveBookmarkFromBookmarklet(): void
245 {
246 $parameters = ['source' => 'bookmarklet'];
247
248 $request = $this->createMock(Request::class);
249 $request
250 ->method('getParam')
251 ->willReturnCallback(function (string $key) use ($parameters): ?string {
252 return $parameters[$key] ?? null;
253 })
254 ;
255 $response = new Response();
256
257 $result = $this->controller->save($request, $response);
258
259 static::assertSame(200, $result->getStatusCode());
260 static::assertSame('<script>self.close();</script>', (string) $result->getBody());
261 }
262
263 /**
264 * Change the password with a wrong existing password
265 */
266 public function testSaveBookmarkWrongToken(): void
267 {
268 $this->container->sessionManager = $this->createMock(SessionManager::class);
269 $this->container->sessionManager->method('checkToken')->willReturn(false);
270
271 $this->container->bookmarkService->expects(static::never())->method('addOrSet');
272 $this->container->bookmarkService->expects(static::never())->method('set');
273
274 $request = $this->createMock(Request::class);
275 $response = new Response();
276
277 $this->expectException(WrongTokenException::class);
278
279 $this->controller->save($request, $response);
280 }
281
282}
diff --git a/tests/front/controller/admin/ManageTagControllerTest.php b/tests/front/controller/admin/ManageTagControllerTest.php
new file mode 100644
index 00000000..09ba0b4b
--- /dev/null
+++ b/tests/front/controller/admin/ManageTagControllerTest.php
@@ -0,0 +1,272 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\BookmarkFilter;
10use Shaarli\Front\Exception\WrongTokenException;
11use Shaarli\Security\SessionManager;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class ManageTagControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ManageTagController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new ManageTagController($this->container);
27 }
28
29 /**
30 * Test displaying manage tag page
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $request->method('getParam')->with('fromtag')->willReturn('fromtag');
39 $response = new Response();
40
41 $result = $this->controller->index($request, $response);
42
43 static::assertSame(200, $result->getStatusCode());
44 static::assertSame('changetag', (string) $result->getBody());
45
46 static::assertSame('fromtag', $assignedVariables['fromtag']);
47 static::assertSame('Manage tags - Shaarli', $assignedVariables['pagetitle']);
48 }
49
50 /**
51 * Test posting a tag update - rename tag - valid info provided.
52 */
53 public function testSaveRenameTagValid(): void
54 {
55 $session = [];
56 $this->assignSessionVars($session);
57
58 $requestParameters = [
59 'renametag' => 'rename',
60 'fromtag' => 'old-tag',
61 'totag' => 'new-tag',
62 ];
63 $request = $this->createMock(Request::class);
64 $request
65 ->expects(static::atLeastOnce())
66 ->method('getParam')
67 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
68 return $requestParameters[$key] ?? null;
69 })
70 ;
71 $response = new Response();
72
73 $bookmark1 = $this->createMock(Bookmark::class);
74 $bookmark2 = $this->createMock(Bookmark::class);
75 $this->container->bookmarkService
76 ->expects(static::once())
77 ->method('search')
78 ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
79 ->willReturnCallback(function () use ($bookmark1, $bookmark2): array {
80 $bookmark1->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
81 $bookmark2->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
82
83 return [$bookmark1, $bookmark2];
84 })
85 ;
86 $this->container->bookmarkService
87 ->expects(static::exactly(2))
88 ->method('set')
89 ->withConsecutive([$bookmark1, false], [$bookmark2, false])
90 ;
91 $this->container->bookmarkService->expects(static::once())->method('save');
92
93 $result = $this->controller->save($request, $response);
94
95 static::assertSame(302, $result->getStatusCode());
96 static::assertSame(['/subfolder/?searchtags=new-tag'], $result->getHeader('location'));
97
98 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
99 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
100 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
101 static::assertSame(['The tag was renamed in 2 bookmarks.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
102 }
103
104 /**
105 * Test posting a tag update - delete tag - valid info provided.
106 */
107 public function testSaveDeleteTagValid(): void
108 {
109 $session = [];
110 $this->assignSessionVars($session);
111
112 $requestParameters = [
113 'deletetag' => 'delete',
114 'fromtag' => 'old-tag',
115 ];
116 $request = $this->createMock(Request::class);
117 $request
118 ->expects(static::atLeastOnce())
119 ->method('getParam')
120 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
121 return $requestParameters[$key] ?? null;
122 })
123 ;
124 $response = new Response();
125
126 $bookmark1 = $this->createMock(Bookmark::class);
127 $bookmark2 = $this->createMock(Bookmark::class);
128 $this->container->bookmarkService
129 ->expects(static::once())
130 ->method('search')
131 ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
132 ->willReturnCallback(function () use ($bookmark1, $bookmark2): array {
133 $bookmark1->expects(static::once())->method('deleteTag')->with('old-tag');
134 $bookmark2->expects(static::once())->method('deleteTag')->with('old-tag');
135
136 return [$bookmark1, $bookmark2];
137 })
138 ;
139 $this->container->bookmarkService
140 ->expects(static::exactly(2))
141 ->method('set')
142 ->withConsecutive([$bookmark1, false], [$bookmark2, false])
143 ;
144 $this->container->bookmarkService->expects(static::once())->method('save');
145
146 $result = $this->controller->save($request, $response);
147
148 static::assertSame(302, $result->getStatusCode());
149 static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
150
151 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
152 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
153 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
154 static::assertSame(['The tag was removed from 2 bookmarks.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
155 }
156
157 /**
158 * Test posting a tag update - wrong token.
159 */
160 public function testSaveWrongToken(): void
161 {
162 $this->container->sessionManager = $this->createMock(SessionManager::class);
163 $this->container->sessionManager->method('checkToken')->willReturn(false);
164
165 $this->container->conf->expects(static::never())->method('set');
166 $this->container->conf->expects(static::never())->method('write');
167
168 $request = $this->createMock(Request::class);
169 $response = new Response();
170
171 $this->expectException(WrongTokenException::class);
172
173 $this->controller->save($request, $response);
174 }
175
176 /**
177 * Test posting a tag update - rename tag - missing "FROM" tag.
178 */
179 public function testSaveRenameTagMissingFrom(): void
180 {
181 $session = [];
182 $this->assignSessionVars($session);
183
184 $requestParameters = [
185 'renametag' => 'rename',
186 ];
187 $request = $this->createMock(Request::class);
188 $request
189 ->expects(static::atLeastOnce())
190 ->method('getParam')
191 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
192 return $requestParameters[$key] ?? null;
193 })
194 ;
195 $response = new Response();
196
197 $result = $this->controller->save($request, $response);
198
199 static::assertSame(302, $result->getStatusCode());
200 static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
201
202 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
203 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
204 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
205 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
206 }
207
208 /**
209 * Test posting a tag update - delete tag - missing "FROM" tag.
210 */
211 public function testSaveDeleteTagMissingFrom(): void
212 {
213 $session = [];
214 $this->assignSessionVars($session);
215
216 $requestParameters = [
217 'deletetag' => 'delete',
218 ];
219 $request = $this->createMock(Request::class);
220 $request
221 ->expects(static::atLeastOnce())
222 ->method('getParam')
223 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
224 return $requestParameters[$key] ?? null;
225 })
226 ;
227 $response = new Response();
228
229 $result = $this->controller->save($request, $response);
230
231 static::assertSame(302, $result->getStatusCode());
232 static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
233
234 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
235 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
236 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
237 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
238 }
239
240 /**
241 * Test posting a tag update - rename tag - missing "TO" tag.
242 */
243 public function testSaveRenameTagMissingTo(): void
244 {
245 $session = [];
246 $this->assignSessionVars($session);
247
248 $requestParameters = [
249 'renametag' => 'rename',
250 'fromtag' => 'old-tag'
251 ];
252 $request = $this->createMock(Request::class);
253 $request
254 ->expects(static::atLeastOnce())
255 ->method('getParam')
256 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
257 return $requestParameters[$key] ?? null;
258 })
259 ;
260 $response = new Response();
261
262 $result = $this->controller->save($request, $response);
263
264 static::assertSame(302, $result->getStatusCode());
265 static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
266
267 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
268 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
269 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
270 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
271 }
272}
diff --git a/tests/front/controller/admin/PasswordControllerTest.php b/tests/front/controller/admin/PasswordControllerTest.php
new file mode 100644
index 00000000..9a01089e
--- /dev/null
+++ b/tests/front/controller/admin/PasswordControllerTest.php
@@ -0,0 +1,203 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\OpenShaarliPasswordException;
10use Shaarli\Front\Exception\WrongTokenException;
11use Shaarli\Security\SessionManager;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class PasswordControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var PasswordController */
20 protected $controller;
21
22 /** @var mixed[] Variables assigned to the template */
23 protected $assignedVariables = [];
24
25 public function setUp(): void
26 {
27 $this->createContainer();
28 $this->assignTemplateVars($this->assignedVariables);
29
30 $this->controller = new PasswordController($this->container);
31 }
32
33 /**
34 * Test displaying the change password page.
35 */
36 public function testGetPage(): void
37 {
38 $request = $this->createMock(Request::class);
39 $response = new Response();
40
41 $result = $this->controller->index($request, $response);
42
43 static::assertSame(200, $result->getStatusCode());
44 static::assertSame('changepassword', (string) $result->getBody());
45 static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
46 }
47
48 /**
49 * Change the password with valid parameters
50 */
51 public function testPostNewPasswordDefault(): void
52 {
53 $request = $this->createMock(Request::class);
54 $request->method('getParam')->willReturnCallback(function (string $key): string {
55 if ('oldpassword' === $key) {
56 return 'old';
57 }
58 if ('setpassword' === $key) {
59 return 'new';
60 }
61
62 return $key;
63 });
64 $response = new Response();
65
66 $this->container->conf = $this->createMock(ConfigManager::class);
67 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
68 if ('credentials.hash' === $key) {
69 return sha1('old' . 'credentials.login' . 'credentials.salt');
70 }
71
72 return strpos($key, 'credentials') !== false ? $key : $default;
73 });
74 $this->container->conf->expects(static::once())->method('write')->with(true);
75
76 $this->container->conf
77 ->method('set')
78 ->willReturnCallback(function (string $key, string $value) {
79 if ('credentials.hash' === $key) {
80 static::assertSame(sha1('new' . 'credentials.login' . 'credentials.salt'), $value);
81 }
82 })
83 ;
84
85 $result = $this->controller->change($request, $response);
86
87 static::assertSame(200, $result->getStatusCode());
88 static::assertSame('changepassword', (string) $result->getBody());
89 static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
90 }
91
92 /**
93 * Change the password with a wrong existing password
94 */
95 public function testPostNewPasswordWrongOldPassword(): void
96 {
97 $request = $this->createMock(Request::class);
98 $request->method('getParam')->willReturnCallback(function (string $key): string {
99 if ('oldpassword' === $key) {
100 return 'wrong';
101 }
102 if ('setpassword' === $key) {
103 return 'new';
104 }
105
106 return $key;
107 });
108 $response = new Response();
109
110 $this->container->conf = $this->createMock(ConfigManager::class);
111 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
112 if ('credentials.hash' === $key) {
113 return sha1('old' . 'credentials.login' . 'credentials.salt');
114 }
115
116 return strpos($key, 'credentials') !== false ? $key : $default;
117 });
118
119 $this->container->conf->expects(static::never())->method('set');
120 $this->container->conf->expects(static::never())->method('write');
121
122 $this->container->sessionManager
123 ->expects(static::once())
124 ->method('setSessionParameter')
125 ->with(SessionManager::KEY_ERROR_MESSAGES, ['The old password is not correct.'])
126 ;
127
128 $result = $this->controller->change($request, $response);
129
130 static::assertSame(400, $result->getStatusCode());
131 static::assertSame('changepassword', (string) $result->getBody());
132 static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
133 }
134
135 /**
136 * Change the password with a wrong existing password
137 */
138 public function testPostNewPasswordWrongToken(): void
139 {
140 $this->container->sessionManager = $this->createMock(SessionManager::class);
141 $this->container->sessionManager->method('checkToken')->willReturn(false);
142
143 $this->container->conf->expects(static::never())->method('set');
144 $this->container->conf->expects(static::never())->method('write');
145
146 $request = $this->createMock(Request::class);
147 $response = new Response();
148
149 $this->expectException(WrongTokenException::class);
150
151 $this->controller->change($request, $response);
152 }
153
154 /**
155 * Change the password with an empty new password
156 */
157 public function testPostNewEmptyPassword(): void
158 {
159 $this->container->sessionManager
160 ->expects(static::once())
161 ->method('setSessionParameter')
162 ->with(SessionManager::KEY_ERROR_MESSAGES, ['You must provide the current and new password to change it.'])
163 ;
164
165 $this->container->conf->expects(static::never())->method('set');
166 $this->container->conf->expects(static::never())->method('write');
167
168 $request = $this->createMock(Request::class);
169 $request->method('getParam')->willReturnCallback(function (string $key): string {
170 if ('oldpassword' === $key) {
171 return 'old';
172 }
173 if ('setpassword' === $key) {
174 return '';
175 }
176
177 return $key;
178 });
179 $response = new Response();
180
181 $result = $this->controller->change($request, $response);
182
183 static::assertSame(400, $result->getStatusCode());
184 static::assertSame('changepassword', (string) $result->getBody());
185 static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
186 }
187
188 /**
189 * Change the password on an open shaarli
190 */
191 public function testPostNewPasswordOnOpenShaarli(): void
192 {
193 $this->container->conf = $this->createMock(ConfigManager::class);
194 $this->container->conf->method('get')->with('security.open_shaarli')->willReturn(true);
195
196 $request = $this->createMock(Request::class);
197 $response = new Response();
198
199 $this->expectException(OpenShaarliPasswordException::class);
200
201 $this->controller->change($request, $response);
202 }
203}
diff --git a/tests/front/controller/admin/PluginsControllerTest.php b/tests/front/controller/admin/PluginsControllerTest.php
new file mode 100644
index 00000000..5b59285c
--- /dev/null
+++ b/tests/front/controller/admin/PluginsControllerTest.php
@@ -0,0 +1,204 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Plugin\PluginManager;
11use Shaarli\Security\SessionManager;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class PluginsControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 const PLUGIN_NAMES = ['plugin1', 'plugin2', 'plugin3', 'plugin4'];
20
21 /** @var PluginsController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->controller = new PluginsController($this->container);
29
30 mkdir($path = __DIR__ . '/folder');
31 PluginManager::$PLUGINS_PATH = $path;
32 array_map(function (string $plugin) use ($path) { touch($path . '/' . $plugin); }, static::PLUGIN_NAMES);
33 }
34
35 public function tearDown()
36 {
37 $path = __DIR__ . '/folder';
38 array_map(function (string $plugin) use ($path) { unlink($path . '/' . $plugin); }, static::PLUGIN_NAMES);
39 rmdir($path);
40 }
41
42 /**
43 * Test displaying plugins admin page
44 */
45 public function testIndex(): void
46 {
47 $assignedVariables = [];
48 $this->assignTemplateVars($assignedVariables);
49
50 $request = $this->createMock(Request::class);
51 $response = new Response();
52
53 $data = [
54 'plugin1' => ['order' => 2, 'other' => 'field'],
55 'plugin2' => ['order' => 1],
56 'plugin3' => ['order' => false, 'abc' => 'def'],
57 'plugin4' => [],
58 ];
59
60 $this->container->pluginManager
61 ->expects(static::once())
62 ->method('getPluginsMeta')
63 ->willReturn($data);
64
65 $result = $this->controller->index($request, $response);
66
67 static::assertSame(200, $result->getStatusCode());
68 static::assertSame('pluginsadmin', (string) $result->getBody());
69
70 static::assertSame('Plugin Administration - Shaarli', $assignedVariables['pagetitle']);
71 static::assertSame(
72 ['plugin2' => $data['plugin2'], 'plugin1' => $data['plugin1']],
73 $assignedVariables['enabledPlugins']
74 );
75 static::assertSame(
76 ['plugin3' => $data['plugin3'], 'plugin4' => $data['plugin4']],
77 $assignedVariables['disabledPlugins']
78 );
79 }
80
81 /**
82 * Test save plugins admin page
83 */
84 public function testSaveEnabledPlugins(): void
85 {
86 $parameters = [
87 'plugin1' => 'on',
88 'order_plugin1' => '2',
89 'plugin2' => 'on',
90 ];
91
92 $request = $this->createMock(Request::class);
93 $request
94 ->expects(static::atLeastOnce())
95 ->method('getParams')
96 ->willReturnCallback(function () use ($parameters): array {
97 return $parameters;
98 })
99 ;
100 $response = new Response();
101
102 $this->container->pluginManager
103 ->expects(static::once())
104 ->method('executeHooks')
105 ->with('save_plugin_parameters', $parameters)
106 ;
107 $this->container->conf
108 ->expects(static::atLeastOnce())
109 ->method('set')
110 ->with('general.enabled_plugins', ['plugin1', 'plugin2'])
111 ;
112
113 $result = $this->controller->save($request, $response);
114
115 static::assertSame(302, $result->getStatusCode());
116 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
117 }
118
119 /**
120 * Test save plugin parameters
121 */
122 public function testSavePluginParameters(): void
123 {
124 $parameters = [
125 'parameters_form' => true,
126 'parameter1' => 'blip',
127 'parameter2' => 'blop',
128 ];
129
130 $request = $this->createMock(Request::class);
131 $request
132 ->expects(static::atLeastOnce())
133 ->method('getParams')
134 ->willReturnCallback(function () use ($parameters): array {
135 return $parameters;
136 })
137 ;
138 $response = new Response();
139
140 $this->container->pluginManager
141 ->expects(static::once())
142 ->method('executeHooks')
143 ->with('save_plugin_parameters', $parameters)
144 ;
145 $this->container->conf
146 ->expects(static::atLeastOnce())
147 ->method('set')
148 ->withConsecutive(['plugins.parameter1', 'blip'], ['plugins.parameter2', 'blop'])
149 ;
150
151 $result = $this->controller->save($request, $response);
152
153 static::assertSame(302, $result->getStatusCode());
154 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
155 }
156
157 /**
158 * Test save plugin parameters - error encountered
159 */
160 public function testSaveWithError(): void
161 {
162 $request = $this->createMock(Request::class);
163 $response = new Response();
164
165 $this->container->conf = $this->createMock(ConfigManager::class);
166 $this->container->conf
167 ->expects(static::atLeastOnce())
168 ->method('write')
169 ->willThrowException(new \Exception($message = 'error message'))
170 ;
171
172 $this->container->sessionManager = $this->createMock(SessionManager::class);
173 $this->container->sessionManager->method('checkToken')->willReturn(true);
174 $this->container->sessionManager
175 ->expects(static::once())
176 ->method('setSessionParameter')
177 ->with(
178 SessionManager::KEY_ERROR_MESSAGES,
179 ['Error while saving plugin configuration: ' . PHP_EOL . $message]
180 )
181 ;
182
183 $result = $this->controller->save($request, $response);
184
185 static::assertSame(302, $result->getStatusCode());
186 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
187 }
188
189 /**
190 * Test save plugin parameters - wrong token
191 */
192 public function testSaveWrongToken(): void
193 {
194 $this->container->sessionManager = $this->createMock(SessionManager::class);
195 $this->container->sessionManager->method('checkToken')->willReturn(false);
196
197 $request = $this->createMock(Request::class);
198 $response = new Response();
199
200 $this->expectException(WrongTokenException::class);
201
202 $this->controller->save($request, $response);
203 }
204}
diff --git a/tests/front/controller/admin/SessionFilterControllerTest.php b/tests/front/controller/admin/SessionFilterControllerTest.php
new file mode 100644
index 00000000..d306c6e9
--- /dev/null
+++ b/tests/front/controller/admin/SessionFilterControllerTest.php
@@ -0,0 +1,177 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Security\LoginManager;
9use Shaarli\Security\SessionManager;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13class SessionFilterControllerTest extends TestCase
14{
15 use FrontAdminControllerMockHelper;
16
17 /** @var SessionFilterController */
18 protected $controller;
19
20 public function setUp(): void
21 {
22 $this->createContainer();
23
24 $this->controller = new SessionFilterController($this->container);
25 }
26
27 /**
28 * Visibility - Default call for private filter while logged in without current value
29 */
30 public function testVisibility(): void
31 {
32 $arg = ['visibility' => 'private'];
33
34 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
35
36 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
37 $this->container->sessionManager
38 ->expects(static::once())
39 ->method('setSessionParameter')
40 ->with(SessionManager::KEY_VISIBILITY, 'private')
41 ;
42
43 $request = $this->createMock(Request::class);
44 $response = new Response();
45
46 $result = $this->controller->visibility($request, $response, $arg);
47
48 static::assertInstanceOf(Response::class, $result);
49 static::assertSame(302, $result->getStatusCode());
50 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
51 }
52
53 /**
54 * Visibility - Toggle off private visibility
55 */
56 public function testVisibilityToggleOff(): void
57 {
58 $arg = ['visibility' => 'private'];
59
60 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
61
62 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
63 $this->container->sessionManager
64 ->method('getSessionParameter')
65 ->with(SessionManager::KEY_VISIBILITY)
66 ->willReturn('private')
67 ;
68 $this->container->sessionManager
69 ->expects(static::never())
70 ->method('setSessionParameter')
71 ;
72 $this->container->sessionManager
73 ->expects(static::once())
74 ->method('deleteSessionParameter')
75 ->with(SessionManager::KEY_VISIBILITY)
76 ;
77
78 $request = $this->createMock(Request::class);
79 $response = new Response();
80
81 $result = $this->controller->visibility($request, $response, $arg);
82
83 static::assertInstanceOf(Response::class, $result);
84 static::assertSame(302, $result->getStatusCode());
85 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
86 }
87
88 /**
89 * Visibility - Change private to public
90 */
91 public function testVisibilitySwitch(): void
92 {
93 $arg = ['visibility' => 'private'];
94
95 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
96 $this->container->sessionManager
97 ->method('getSessionParameter')
98 ->with(SessionManager::KEY_VISIBILITY)
99 ->willReturn('public')
100 ;
101 $this->container->sessionManager
102 ->expects(static::once())
103 ->method('setSessionParameter')
104 ->with(SessionManager::KEY_VISIBILITY, 'private')
105 ;
106
107 $request = $this->createMock(Request::class);
108 $response = new Response();
109
110 $result = $this->controller->visibility($request, $response, $arg);
111
112 static::assertInstanceOf(Response::class, $result);
113 static::assertSame(302, $result->getStatusCode());
114 static::assertSame(['/subfolder/'], $result->getHeader('location'));
115 }
116
117 /**
118 * Visibility - With invalid value - should remove any visibility setting
119 */
120 public function testVisibilityInvalidValue(): void
121 {
122 $arg = ['visibility' => 'test'];
123
124 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
125
126 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
127 $this->container->sessionManager
128 ->expects(static::never())
129 ->method('setSessionParameter')
130 ;
131 $this->container->sessionManager
132 ->expects(static::once())
133 ->method('deleteSessionParameter')
134 ->with(SessionManager::KEY_VISIBILITY)
135 ;
136
137 $request = $this->createMock(Request::class);
138 $response = new Response();
139
140 $result = $this->controller->visibility($request, $response, $arg);
141
142 static::assertInstanceOf(Response::class, $result);
143 static::assertSame(302, $result->getStatusCode());
144 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
145 }
146
147 /**
148 * Visibility - Try to change visibility while logged out
149 */
150 public function testVisibilityLoggedOut(): void
151 {
152 $arg = ['visibility' => 'test'];
153
154 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
155
156 $this->container->loginManager = $this->createMock(LoginManager::class);
157 $this->container->loginManager->method('isLoggedIn')->willReturn(false);
158 $this->container->sessionManager
159 ->expects(static::never())
160 ->method('setSessionParameter')
161 ;
162 $this->container->sessionManager
163 ->expects(static::never())
164 ->method('deleteSessionParameter')
165 ->with(SessionManager::KEY_VISIBILITY)
166 ;
167
168 $request = $this->createMock(Request::class);
169 $response = new Response();
170
171 $result = $this->controller->visibility($request, $response, $arg);
172
173 static::assertInstanceOf(Response::class, $result);
174 static::assertSame(302, $result->getStatusCode());
175 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
176 }
177}
diff --git a/tests/front/controller/admin/ShaarliAdminControllerTest.php b/tests/front/controller/admin/ShaarliAdminControllerTest.php
new file mode 100644
index 00000000..fff427cb
--- /dev/null
+++ b/tests/front/controller/admin/ShaarliAdminControllerTest.php
@@ -0,0 +1,184 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Front\Exception\WrongTokenException;
9use Shaarli\Security\SessionManager;
10use Slim\Http\Request;
11
12/**
13 * Class ShaarliControllerTest
14 *
15 * This class is used to test default behavior of ShaarliAdminController abstract class.
16 * It uses a dummy non abstract controller.
17 */
18class ShaarliAdminControllerTest extends TestCase
19{
20 use FrontAdminControllerMockHelper;
21
22 /** @var ShaarliAdminController */
23 protected $controller;
24
25 public function setUp(): void
26 {
27 $this->createContainer();
28
29 $this->controller = new class($this->container) extends ShaarliAdminController
30 {
31 public function checkToken(Request $request): bool
32 {
33 return parent::checkToken($request);
34 }
35
36 public function saveSuccessMessage(string $message): void
37 {
38 parent::saveSuccessMessage($message);
39 }
40
41 public function saveWarningMessage(string $message): void
42 {
43 parent::saveWarningMessage($message);
44 }
45
46 public function saveErrorMessage(string $message): void
47 {
48 parent::saveErrorMessage($message);
49 }
50 };
51 }
52
53 /**
54 * Trigger controller's checkToken with a valid token.
55 */
56 public function testCheckTokenWithValidToken(): void
57 {
58 $request = $this->createMock(Request::class);
59 $request->method('getParam')->with('token')->willReturn($token = '12345');
60
61 $this->container->sessionManager = $this->createMock(SessionManager::class);
62 $this->container->sessionManager->method('checkToken')->with($token)->willReturn(true);
63
64 static::assertTrue($this->controller->checkToken($request));
65 }
66
67 /**
68 * Trigger controller's checkToken with na valid token should raise an exception.
69 */
70 public function testCheckTokenWithNotValidToken(): void
71 {
72 $request = $this->createMock(Request::class);
73 $request->method('getParam')->with('token')->willReturn($token = '12345');
74
75 $this->container->sessionManager = $this->createMock(SessionManager::class);
76 $this->container->sessionManager->method('checkToken')->with($token)->willReturn(false);
77
78 $this->expectException(WrongTokenException::class);
79
80 $this->controller->checkToken($request);
81 }
82
83 /**
84 * Test saveSuccessMessage() with a first message.
85 */
86 public function testSaveSuccessMessage(): void
87 {
88 $this->container->sessionManager
89 ->expects(static::once())
90 ->method('setSessionParameter')
91 ->with(SessionManager::KEY_SUCCESS_MESSAGES, [$message = 'bravo!'])
92 ;
93
94 $this->controller->saveSuccessMessage($message);
95 }
96
97 /**
98 * Test saveSuccessMessage() with existing messages.
99 */
100 public function testSaveSuccessMessageWithExistingMessages(): void
101 {
102 $this->container->sessionManager
103 ->expects(static::once())
104 ->method('getSessionParameter')
105 ->with(SessionManager::KEY_SUCCESS_MESSAGES)
106 ->willReturn(['success1', 'success2'])
107 ;
108 $this->container->sessionManager
109 ->expects(static::once())
110 ->method('setSessionParameter')
111 ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['success1', 'success2', $message = 'bravo!'])
112 ;
113
114 $this->controller->saveSuccessMessage($message);
115 }
116
117 /**
118 * Test saveWarningMessage() with a first message.
119 */
120 public function testSaveWarningMessage(): void
121 {
122 $this->container->sessionManager
123 ->expects(static::once())
124 ->method('setSessionParameter')
125 ->with(SessionManager::KEY_WARNING_MESSAGES, [$message = 'warning!'])
126 ;
127
128 $this->controller->saveWarningMessage($message);
129 }
130
131 /**
132 * Test saveWarningMessage() with existing messages.
133 */
134 public function testSaveWarningMessageWithExistingMessages(): void
135 {
136 $this->container->sessionManager
137 ->expects(static::once())
138 ->method('getSessionParameter')
139 ->with(SessionManager::KEY_WARNING_MESSAGES)
140 ->willReturn(['warning1', 'warning2'])
141 ;
142 $this->container->sessionManager
143 ->expects(static::once())
144 ->method('setSessionParameter')
145 ->with(SessionManager::KEY_WARNING_MESSAGES, ['warning1', 'warning2', $message = 'warning!'])
146 ;
147
148 $this->controller->saveWarningMessage($message);
149 }
150
151 /**
152 * Test saveErrorMessage() with a first message.
153 */
154 public function testSaveErrorMessage(): void
155 {
156 $this->container->sessionManager
157 ->expects(static::once())
158 ->method('setSessionParameter')
159 ->with(SessionManager::KEY_ERROR_MESSAGES, [$message = 'error!'])
160 ;
161
162 $this->controller->saveErrorMessage($message);
163 }
164
165 /**
166 * Test saveErrorMessage() with existing messages.
167 */
168 public function testSaveErrorMessageWithExistingMessages(): void
169 {
170 $this->container->sessionManager
171 ->expects(static::once())
172 ->method('getSessionParameter')
173 ->with(SessionManager::KEY_ERROR_MESSAGES)
174 ->willReturn(['error1', 'error2'])
175 ;
176 $this->container->sessionManager
177 ->expects(static::once())
178 ->method('setSessionParameter')
179 ->with(SessionManager::KEY_ERROR_MESSAGES, ['error1', 'error2', $message = 'error!'])
180 ;
181
182 $this->controller->saveErrorMessage($message);
183 }
184}
diff --git a/tests/front/controller/admin/ThumbnailsControllerTest.php b/tests/front/controller/admin/ThumbnailsControllerTest.php
new file mode 100644
index 00000000..0c0c8a83
--- /dev/null
+++ b/tests/front/controller/admin/ThumbnailsControllerTest.php
@@ -0,0 +1,154 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Thumbnailer;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class ThumbnailsControllerTest extends TestCase
15{
16 use FrontAdminControllerMockHelper;
17
18 /** @var ThumbnailsController */
19 protected $controller;
20
21 public function setUp(): void
22 {
23 $this->createContainer();
24
25 $this->controller = new ThumbnailsController($this->container);
26 }
27
28 /**
29 * Test displaying the thumbnails update page
30 * Note that only non-note and HTTP bookmarks should be returned.
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $this->container->bookmarkService
41 ->expects(static::once())
42 ->method('search')
43 ->willReturn([
44 (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
45 (new Bookmark())->setId(2)->setUrl('?abcdef')->setTitle('Note 1'),
46 (new Bookmark())->setId(3)->setUrl('http://url2.tld')->setTitle('Title 2'),
47 (new Bookmark())->setId(4)->setUrl('ftp://domain.tld', ['ftp'])->setTitle('FTP'),
48 ])
49 ;
50
51 $result = $this->controller->index($request, $response);
52
53 static::assertSame(200, $result->getStatusCode());
54 static::assertSame('thumbnails', (string) $result->getBody());
55
56 static::assertSame('Thumbnails update - Shaarli', $assignedVariables['pagetitle']);
57 static::assertSame([1, 3], $assignedVariables['ids']);
58 }
59
60 /**
61 * Test updating a bookmark thumbnail with valid parameters
62 */
63 public function testAjaxUpdateValid(): void
64 {
65 $request = $this->createMock(Request::class);
66 $response = new Response();
67
68 $bookmark = (new Bookmark())
69 ->setId($id = 123)
70 ->setUrl($url = 'http://url1.tld')
71 ->setTitle('Title 1')
72 ->setThumbnail(false)
73 ;
74
75 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
76 $this->container->thumbnailer
77 ->expects(static::once())
78 ->method('get')
79 ->with($url)
80 ->willReturn($thumb = 'http://img.tld/pic.png')
81 ;
82
83 $this->container->bookmarkService
84 ->expects(static::once())
85 ->method('get')
86 ->with($id)
87 ->willReturn($bookmark)
88 ;
89 $this->container->bookmarkService
90 ->expects(static::once())
91 ->method('set')
92 ->willReturnCallback(function (Bookmark $bookmark) use ($thumb) {
93 static::assertSame($thumb, $bookmark->getThumbnail());
94 })
95 ;
96
97 $result = $this->controller->ajaxUpdate($request, $response, ['id' => (string) $id]);
98
99 static::assertSame(200, $result->getStatusCode());
100
101 $payload = json_decode((string) $result->getBody(), true);
102
103 static::assertSame($id, $payload['id']);
104 static::assertSame($url, $payload['url']);
105 static::assertSame($thumb, $payload['thumbnail']);
106 }
107
108 /**
109 * Test updating a bookmark thumbnail - Invalid ID
110 */
111 public function testAjaxUpdateInvalidId(): void
112 {
113 $request = $this->createMock(Request::class);
114 $response = new Response();
115
116 $result = $this->controller->ajaxUpdate($request, $response, ['id' => 'nope']);
117
118 static::assertSame(400, $result->getStatusCode());
119 }
120
121 /**
122 * Test updating a bookmark thumbnail - No ID
123 */
124 public function testAjaxUpdateNoId(): void
125 {
126 $request = $this->createMock(Request::class);
127 $response = new Response();
128
129 $result = $this->controller->ajaxUpdate($request, $response, []);
130
131 static::assertSame(400, $result->getStatusCode());
132 }
133
134 /**
135 * Test updating a bookmark thumbnail with valid parameters
136 */
137 public function testAjaxUpdateBookmarkNotFound(): void
138 {
139 $id = 123;
140 $request = $this->createMock(Request::class);
141 $response = new Response();
142
143 $this->container->bookmarkService
144 ->expects(static::once())
145 ->method('get')
146 ->with($id)
147 ->willThrowException(new BookmarkNotFoundException())
148 ;
149
150 $result = $this->controller->ajaxUpdate($request, $response, ['id' => (string) $id]);
151
152 static::assertSame(404, $result->getStatusCode());
153 }
154}
diff --git a/tests/front/controller/admin/TokenControllerTest.php b/tests/front/controller/admin/TokenControllerTest.php
new file mode 100644
index 00000000..04b0c0fa
--- /dev/null
+++ b/tests/front/controller/admin/TokenControllerTest.php
@@ -0,0 +1,41 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class TokenControllerTest extends TestCase
12{
13 use FrontAdminControllerMockHelper;
14
15 /** @var TokenController */
16 protected $controller;
17
18 public function setUp(): void
19 {
20 $this->createContainer();
21
22 $this->controller = new TokenController($this->container);
23 }
24
25 public function testGetToken(): void
26 {
27 $request = $this->createMock(Request::class);
28 $response = new Response();
29
30 $this->container->sessionManager
31 ->expects(static::once())
32 ->method('generateToken')
33 ->willReturn($token = 'token1234')
34 ;
35
36 $result = $this->controller->getToken($request, $response);
37
38 static::assertSame(200, $result->getStatusCode());
39 static::assertSame($token, (string) $result->getBody());
40 }
41}
diff --git a/tests/front/controller/admin/ToolsControllerTest.php b/tests/front/controller/admin/ToolsControllerTest.php
new file mode 100644
index 00000000..fc756f0f
--- /dev/null
+++ b/tests/front/controller/admin/ToolsControllerTest.php
@@ -0,0 +1,69 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class ToolsControllerTestControllerTest extends TestCase
12{
13 use FrontAdminControllerMockHelper;
14
15 /** @var ToolsController */
16 protected $controller;
17
18 public function setUp(): void
19 {
20 $this->createContainer();
21
22 $this->controller = new ToolsController($this->container);
23 }
24
25 public function testDefaultInvokeWithHttps(): void
26 {
27 $request = $this->createMock(Request::class);
28 $response = new Response();
29
30 $this->container->environment = [
31 'SERVER_NAME' => 'shaarli',
32 'SERVER_PORT' => 443,
33 'HTTPS' => 'on',
34 ];
35
36 // Save RainTPL assigned variables
37 $assignedVariables = [];
38 $this->assignTemplateVars($assignedVariables);
39
40 $result = $this->controller->index($request, $response);
41
42 static::assertSame(200, $result->getStatusCode());
43 static::assertSame('tools', (string) $result->getBody());
44 static::assertSame('https://shaarli', $assignedVariables['pageabsaddr']);
45 static::assertTrue($assignedVariables['sslenabled']);
46 }
47
48 public function testDefaultInvokeWithoutHttps(): void
49 {
50 $request = $this->createMock(Request::class);
51 $response = new Response();
52
53 $this->container->environment = [
54 'SERVER_NAME' => 'shaarli',
55 'SERVER_PORT' => 80,
56 ];
57
58 // Save RainTPL assigned variables
59 $assignedVariables = [];
60 $this->assignTemplateVars($assignedVariables);
61
62 $result = $this->controller->index($request, $response);
63
64 static::assertSame(200, $result->getStatusCode());
65 static::assertSame('tools', (string) $result->getBody());
66 static::assertSame('http://shaarli', $assignedVariables['pageabsaddr']);
67 static::assertFalse($assignedVariables['sslenabled']);
68 }
69}
diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php
new file mode 100644
index 00000000..5daaa2c4
--- /dev/null
+++ b/tests/front/controller/visitor/BookmarkListControllerTest.php
@@ -0,0 +1,448 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Config\ConfigManager;
11use Shaarli\Security\LoginManager;
12use Shaarli\Thumbnailer;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class BookmarkListControllerTest extends TestCase
17{
18 use FrontControllerMockHelper;
19
20 /** @var BookmarkListController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->controller = new BookmarkListController($this->container);
28 }
29
30 /**
31 * Test rendering list of bookmarks with default parameters (first page).
32 */
33 public function testIndexDefaultFirstPage(): void
34 {
35 $assignedVariables = [];
36 $this->assignTemplateVars($assignedVariables);
37
38 $request = $this->createMock(Request::class);
39 $response = new Response();
40
41 $this->container->bookmarkService
42 ->expects(static::once())
43 ->method('search')
44 ->with(
45 ['searchtags' => '', 'searchterm' => ''],
46 null,
47 false,
48 false
49 )
50 ->willReturn([
51 (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
52 (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
53 (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
54 ]
55 );
56
57 $this->container->sessionManager
58 ->method('getSessionParameter')
59 ->willReturnCallback(function (string $parameter, $default = null) {
60 if ('LINKS_PER_PAGE' === $parameter) {
61 return 2;
62 }
63
64 return $default;
65 })
66 ;
67
68 $result = $this->controller->index($request, $response);
69
70 static::assertSame(200, $result->getStatusCode());
71 static::assertSame('linklist', (string) $result->getBody());
72
73 static::assertSame('Shaarli', $assignedVariables['pagetitle']);
74 static::assertSame('?page=2', $assignedVariables['previous_page_url']);
75 static::assertSame('', $assignedVariables['next_page_url']);
76 static::assertSame(2, $assignedVariables['page_max']);
77 static::assertSame('', $assignedVariables['search_tags']);
78 static::assertSame(3, $assignedVariables['result_count']);
79 static::assertSame(1, $assignedVariables['page_current']);
80 static::assertSame('', $assignedVariables['search_term']);
81 static::assertNull($assignedVariables['visibility']);
82 static::assertCount(2, $assignedVariables['links']);
83
84 $link = $assignedVariables['links'][0];
85
86 static::assertSame(1, $link['id']);
87 static::assertSame('http://url1.tld', $link['url']);
88 static::assertSame('Title 1', $link['title']);
89
90 $link = $assignedVariables['links'][1];
91
92 static::assertSame(2, $link['id']);
93 static::assertSame('http://url2.tld', $link['url']);
94 static::assertSame('Title 2', $link['title']);
95 }
96
97 /**
98 * Test rendering list of bookmarks with default parameters (second page).
99 */
100 public function testIndexDefaultSecondPage(): void
101 {
102 $assignedVariables = [];
103 $this->assignTemplateVars($assignedVariables);
104
105 $request = $this->createMock(Request::class);
106 $request->method('getParam')->willReturnCallback(function (string $key) {
107 if ('page' === $key) {
108 return '2';
109 }
110
111 return null;
112 });
113 $response = new Response();
114
115 $this->container->bookmarkService
116 ->expects(static::once())
117 ->method('search')
118 ->with(
119 ['searchtags' => '', 'searchterm' => ''],
120 null,
121 false,
122 false
123 )
124 ->willReturn([
125 (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
126 (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
127 (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
128 ])
129 ;
130
131 $this->container->sessionManager
132 ->method('getSessionParameter')
133 ->willReturnCallback(function (string $parameter, $default = null) {
134 if ('LINKS_PER_PAGE' === $parameter) {
135 return 2;
136 }
137
138 return $default;
139 })
140 ;
141
142 $result = $this->controller->index($request, $response);
143
144 static::assertSame(200, $result->getStatusCode());
145 static::assertSame('linklist', (string) $result->getBody());
146
147 static::assertSame('Shaarli', $assignedVariables['pagetitle']);
148 static::assertSame('', $assignedVariables['previous_page_url']);
149 static::assertSame('?page=1', $assignedVariables['next_page_url']);
150 static::assertSame(2, $assignedVariables['page_max']);
151 static::assertSame('', $assignedVariables['search_tags']);
152 static::assertSame(3, $assignedVariables['result_count']);
153 static::assertSame(2, $assignedVariables['page_current']);
154 static::assertSame('', $assignedVariables['search_term']);
155 static::assertNull($assignedVariables['visibility']);
156 static::assertCount(1, $assignedVariables['links']);
157
158 $link = $assignedVariables['links'][2];
159
160 static::assertSame(3, $link['id']);
161 static::assertSame('http://url3.tld', $link['url']);
162 static::assertSame('Title 3', $link['title']);
163 }
164
165 /**
166 * Test rendering list of bookmarks with filters.
167 */
168 public function testIndexDefaultWithFilters(): void
169 {
170 $assignedVariables = [];
171 $this->assignTemplateVars($assignedVariables);
172
173 $request = $this->createMock(Request::class);
174 $request->method('getParam')->willReturnCallback(function (string $key) {
175 if ('searchtags' === $key) {
176 return 'abc def';
177 }
178 if ('searchterm' === $key) {
179 return 'ghi jkl';
180 }
181
182 return null;
183 });
184 $response = new Response();
185
186 $this->container->sessionManager
187 ->method('getSessionParameter')
188 ->willReturnCallback(function (string $key, $default) {
189 if ('LINKS_PER_PAGE' === $key) {
190 return 2;
191 }
192 if ('visibility' === $key) {
193 return 'private';
194 }
195 if ('untaggedonly' === $key) {
196 return true;
197 }
198
199 return $default;
200 })
201 ;
202
203 $this->container->bookmarkService
204 ->expects(static::once())
205 ->method('search')
206 ->with(
207 ['searchtags' => 'abc def', 'searchterm' => 'ghi jkl'],
208 'private',
209 false,
210 true
211 )
212 ->willReturn([
213 (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
214 (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
215 (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
216 ])
217 ;
218
219 $result = $this->controller->index($request, $response);
220
221 static::assertSame(200, $result->getStatusCode());
222 static::assertSame('linklist', (string) $result->getBody());
223
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']);
226 }
227
228 /**
229 * Test displaying a permalink with valid parameters
230 */
231 public function testPermalinkValid(): void
232 {
233 $hash = 'abcdef';
234
235 $assignedVariables = [];
236 $this->assignTemplateVars($assignedVariables);
237
238 $request = $this->createMock(Request::class);
239 $response = new Response();
240
241 $this->container->bookmarkService
242 ->expects(static::once())
243 ->method('findByHash')
244 ->with($hash)
245 ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld'))
246 ;
247
248 $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
249
250 static::assertSame(200, $result->getStatusCode());
251 static::assertSame('linklist', (string) $result->getBody());
252
253 static::assertSame('Title 1 - Shaarli', $assignedVariables['pagetitle']);
254 static::assertCount(1, $assignedVariables['links']);
255
256 $link = $assignedVariables['links'][0];
257
258 static::assertSame(123, $link['id']);
259 static::assertSame('http://url1.tld', $link['url']);
260 static::assertSame('Title 1', $link['title']);
261 }
262
263 /**
264 * Test displaying a permalink with an unknown small hash : renders a 404 template error
265 */
266 public function testPermalinkNotFound(): void
267 {
268 $hash = 'abcdef';
269
270 $assignedVariables = [];
271 $this->assignTemplateVars($assignedVariables);
272
273 $request = $this->createMock(Request::class);
274 $response = new Response();
275
276 $this->container->bookmarkService
277 ->expects(static::once())
278 ->method('findByHash')
279 ->with($hash)
280 ->willThrowException(new BookmarkNotFoundException())
281 ;
282
283 $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
284
285 static::assertSame(200, $result->getStatusCode());
286 static::assertSame('404', (string) $result->getBody());
287
288 static::assertSame(
289 'The link you are trying to reach does not exist or has been deleted.',
290 $assignedVariables['error_message']
291 );
292 }
293
294 /**
295 * Test getting link list with thumbnail updates.
296 * -> 2 thumbnails update, only 1 datastore write
297 */
298 public function testThumbnailUpdateFromLinkList(): void
299 {
300 $request = $this->createMock(Request::class);
301 $response = new Response();
302
303 $this->container->loginManager = $this->createMock(LoginManager::class);
304 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
305
306 $this->container->conf = $this->createMock(ConfigManager::class);
307 $this->container->conf
308 ->method('get')
309 ->willReturnCallback(function (string $key, $default) {
310 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
311 })
312 ;
313
314 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
315 $this->container->thumbnailer
316 ->expects(static::exactly(2))
317 ->method('get')
318 ->withConsecutive(['https://url2.tld'], ['https://url4.tld'])
319 ;
320
321 $this->container->bookmarkService
322 ->expects(static::once())
323 ->method('search')
324 ->willReturn([
325 (new Bookmark())->setId(1)->setUrl('https://url1.tld')->setTitle('Title 1')->setThumbnail(false),
326 $b1 = (new Bookmark())->setId(2)->setUrl('https://url2.tld')->setTitle('Title 2'),
327 (new Bookmark())->setId(3)->setUrl('https://url3.tld')->setTitle('Title 3')->setThumbnail(false),
328 $b2 = (new Bookmark())->setId(2)->setUrl('https://url4.tld')->setTitle('Title 4'),
329 (new Bookmark())->setId(2)->setUrl('ftp://url5.tld', ['ftp'])->setTitle('Title 5'),
330 ])
331 ;
332 $this->container->bookmarkService
333 ->expects(static::exactly(2))
334 ->method('set')
335 ->withConsecutive([$b1, false], [$b2, false])
336 ;
337 $this->container->bookmarkService->expects(static::once())->method('save');
338
339 $result = $this->controller->index($request, $response);
340
341 static::assertSame(200, $result->getStatusCode());
342 static::assertSame('linklist', (string) $result->getBody());
343 }
344
345 /**
346 * Test getting a permalink with thumbnail update.
347 */
348 public function testThumbnailUpdateFromPermalink(): void
349 {
350 $request = $this->createMock(Request::class);
351 $response = new Response();
352
353 $this->container->loginManager = $this->createMock(LoginManager::class);
354 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
355
356 $this->container->conf = $this->createMock(ConfigManager::class);
357 $this->container->conf
358 ->method('get')
359 ->willReturnCallback(function (string $key, $default) {
360 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
361 })
362 ;
363
364 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
365 $this->container->thumbnailer->expects(static::once())->method('get')->withConsecutive(['https://url.tld']);
366
367 $this->container->bookmarkService
368 ->expects(static::once())
369 ->method('findByHash')
370 ->willReturn($bookmark = (new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1'))
371 ;
372 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, true);
373 $this->container->bookmarkService->expects(static::never())->method('save');
374
375 $result = $this->controller->permalink($request, $response, ['hash' => 'abc']);
376
377 static::assertSame(200, $result->getStatusCode());
378 static::assertSame('linklist', (string) $result->getBody());
379 }
380
381 /**
382 * Trigger legacy controller in link list controller: permalink
383 */
384 public function testLegacyControllerPermalink(): void
385 {
386 $hash = 'abcdef';
387 $this->container->environment['QUERY_STRING'] = $hash;
388
389 $request = $this->createMock(Request::class);
390 $response = new Response();
391
392 $result = $this->controller->index($request, $response);
393
394 static::assertSame(302, $result->getStatusCode());
395 static::assertSame('/subfolder/shaare/' . $hash, $result->getHeader('location')[0]);
396 }
397
398 /**
399 * Trigger legacy controller in link list controller: ?do= query parameter
400 */
401 public function testLegacyControllerDoPage(): void
402 {
403 $request = $this->createMock(Request::class);
404 $request->method('getQueryParam')->with('do')->willReturn('picwall');
405 $response = new Response();
406
407 $result = $this->controller->index($request, $response);
408
409 static::assertSame(302, $result->getStatusCode());
410 static::assertSame('/subfolder/picture-wall', $result->getHeader('location')[0]);
411 }
412
413 /**
414 * Trigger legacy controller in link list controller: ?do= query parameter with unknown legacy route
415 */
416 public function testLegacyControllerUnknownDoPage(): void
417 {
418 $request = $this->createMock(Request::class);
419 $request->method('getQueryParam')->with('do')->willReturn('nope');
420 $response = new Response();
421
422 $result = $this->controller->index($request, $response);
423
424 static::assertSame(200, $result->getStatusCode());
425 static::assertSame('linklist', (string) $result->getBody());
426 }
427
428 /**
429 * Trigger legacy controller in link list controller: other GET route (e.g. ?post)
430 */
431 public function testLegacyControllerGetParameter(): void
432 {
433 $request = $this->createMock(Request::class);
434 $request->method('getQueryParams')->willReturn(['post' => $url = 'http://url.tld']);
435 $response = new Response();
436
437 $this->container->loginManager = $this->createMock(LoginManager::class);
438 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
439
440 $result = $this->controller->index($request, $response);
441
442 static::assertSame(302, $result->getStatusCode());
443 static::assertSame(
444 '/subfolder/admin/shaare?post=' . urlencode($url),
445 $result->getHeader('location')[0]
446 );
447 }
448}
diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php
new file mode 100644
index 00000000..b802c62c
--- /dev/null
+++ b/tests/front/controller/visitor/DailyControllerTest.php
@@ -0,0 +1,476 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Feed\CachedPage;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13class DailyControllerTest extends TestCase
14{
15 use FrontControllerMockHelper;
16
17 /** @var DailyController */
18 protected $controller;
19
20 public function setUp(): void
21 {
22 $this->createContainer();
23
24 $this->controller = new DailyController($this->container);
25 DailyController::$DAILY_RSS_NB_DAYS = 2;
26 }
27
28 public function testValidIndexControllerInvokeDefault(): void
29 {
30 $currentDay = new \DateTimeImmutable('2020-05-13');
31
32 $request = $this->createMock(Request::class);
33 $request->method('getQueryParam')->willReturn($currentDay->format('Ymd'));
34 $response = new Response();
35
36 // Save RainTPL assigned variables
37 $assignedVariables = [];
38 $this->assignTemplateVars($assignedVariables);
39
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
53 ->expects(static::once())
54 ->method('filterDay')
55 ->willReturnCallback(function (): array {
56 return [
57 (new Bookmark())
58 ->setId(1)
59 ->setUrl('http://url.tld')
60 ->setTitle(static::generateString(50))
61 ->setDescription(static::generateString(500))
62 ,
63 (new Bookmark())
64 ->setId(2)
65 ->setUrl('http://url2.tld')
66 ->setTitle(static::generateString(50))
67 ->setDescription(static::generateString(500))
68 ,
69 (new Bookmark())
70 ->setId(3)
71 ->setUrl('http://url3.tld')
72 ->setTitle(static::generateString(50))
73 ->setDescription(static::generateString(500))
74 ,
75 ];
76 })
77 ;
78
79 // Make sure that PluginManager hook is triggered
80 $this->container->pluginManager
81 ->expects(static::at(0))
82 ->method('executeHooks')
83 ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array {
84 static::assertSame('render_daily', $hook);
85
86 static::assertArrayHasKey('linksToDisplay', $data);
87 static::assertCount(3, $data['linksToDisplay']);
88 static::assertSame(1, $data['linksToDisplay'][0]['id']);
89 static::assertSame($currentDay->getTimestamp(), $data['day']);
90 static::assertSame('20200510', $data['previousday']);
91 static::assertSame('20200516', $data['nextday']);
92
93 static::assertArrayHasKey('loggedin', $param);
94
95 return $data;
96 })
97 ;
98
99 $result = $this->controller->index($request, $response);
100
101 static::assertSame(200, $result->getStatusCode());
102 static::assertSame('daily', (string) $result->getBody());
103 static::assertSame(
104 'Daily - '. format_date($currentDay, false, true) .' - Shaarli',
105 $assignedVariables['pagetitle']
106 );
107 static::assertEquals($currentDay, $assignedVariables['dayDate']);
108 static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']);
109 static::assertCount(3, $assignedVariables['linksToDisplay']);
110
111 $link = $assignedVariables['linksToDisplay'][0];
112
113 static::assertSame(1, $link['id']);
114 static::assertSame('http://url.tld', $link['url']);
115 static::assertNotEmpty($link['title']);
116 static::assertNotEmpty($link['description']);
117 static::assertNotEmpty($link['formatedDescription']);
118
119 $link = $assignedVariables['linksToDisplay'][1];
120
121 static::assertSame(2, $link['id']);
122 static::assertSame('http://url2.tld', $link['url']);
123 static::assertNotEmpty($link['title']);
124 static::assertNotEmpty($link['description']);
125 static::assertNotEmpty($link['formatedDescription']);
126
127 $link = $assignedVariables['linksToDisplay'][2];
128
129 static::assertSame(3, $link['id']);
130 static::assertSame('http://url3.tld', $link['url']);
131 static::assertNotEmpty($link['title']);
132 static::assertNotEmpty($link['description']);
133 static::assertNotEmpty($link['formatedDescription']);
134
135 static::assertCount(3, $assignedVariables['cols']);
136 static::assertCount(1, $assignedVariables['cols'][0]);
137 static::assertCount(1, $assignedVariables['cols'][1]);
138 static::assertCount(1, $assignedVariables['cols'][2]);
139
140 $link = $assignedVariables['cols'][0][0];
141
142 static::assertSame(1, $link['id']);
143 static::assertSame('http://url.tld', $link['url']);
144 static::assertNotEmpty($link['title']);
145 static::assertNotEmpty($link['description']);
146 static::assertNotEmpty($link['formatedDescription']);
147
148 $link = $assignedVariables['cols'][1][0];
149
150 static::assertSame(2, $link['id']);
151 static::assertSame('http://url2.tld', $link['url']);
152 static::assertNotEmpty($link['title']);
153 static::assertNotEmpty($link['description']);
154 static::assertNotEmpty($link['formatedDescription']);
155
156 $link = $assignedVariables['cols'][2][0];
157
158 static::assertSame(3, $link['id']);
159 static::assertSame('http://url3.tld', $link['url']);
160 static::assertNotEmpty($link['title']);
161 static::assertNotEmpty($link['description']);
162 static::assertNotEmpty($link['formatedDescription']);
163 }
164
165 /**
166 * Daily page - test that everything goes fine with no future or past bookmarks
167 */
168 public function testValidIndexControllerInvokeNoFutureOrPast(): void
169 {
170 $currentDay = new \DateTimeImmutable('2020-05-13');
171
172 $request = $this->createMock(Request::class);
173 $response = new Response();
174
175 // Save RainTPL assigned variables
176 $assignedVariables = [];
177 $this->assignTemplateVars($assignedVariables);
178
179 // Links dataset: 2 links with thumbnails
180 $this->container->bookmarkService
181 ->expects(static::once())
182 ->method('days')
183 ->willReturnCallback(function () use ($currentDay): array {
184 return [
185 $currentDay->format($currentDay->format('Ymd')),
186 ];
187 })
188 ;
189 $this->container->bookmarkService
190 ->expects(static::once())
191 ->method('filterDay')
192 ->willReturnCallback(function (): array {
193 return [
194 (new Bookmark())
195 ->setId(1)
196 ->setUrl('http://url.tld')
197 ->setTitle(static::generateString(50))
198 ->setDescription(static::generateString(500))
199 ,
200 ];
201 })
202 ;
203
204 // Make sure that PluginManager hook is triggered
205 $this->container->pluginManager
206 ->expects(static::at(0))
207 ->method('executeHooks')
208 ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array {
209 static::assertSame('render_daily', $hook);
210
211 static::assertArrayHasKey('linksToDisplay', $data);
212 static::assertCount(1, $data['linksToDisplay']);
213 static::assertSame(1, $data['linksToDisplay'][0]['id']);
214 static::assertSame($currentDay->getTimestamp(), $data['day']);
215 static::assertEmpty($data['previousday']);
216 static::assertEmpty($data['nextday']);
217
218 static::assertArrayHasKey('loggedin', $param);
219
220 return $data;
221 });
222
223 $result = $this->controller->index($request, $response);
224
225 static::assertSame(200, $result->getStatusCode());
226 static::assertSame('daily', (string) $result->getBody());
227 static::assertSame(
228 'Daily - '. format_date($currentDay, false, true) .' - Shaarli',
229 $assignedVariables['pagetitle']
230 );
231 static::assertCount(1, $assignedVariables['linksToDisplay']);
232
233 $link = $assignedVariables['linksToDisplay'][0];
234 static::assertSame(1, $link['id']);
235 }
236
237 /**
238 * Daily page - test that height adjustment in columns is working
239 */
240 public function testValidIndexControllerInvokeHeightAdjustment(): void
241 {
242 $currentDay = new \DateTimeImmutable('2020-05-13');
243
244 $request = $this->createMock(Request::class);
245 $response = new Response();
246
247 // Save RainTPL assigned variables
248 $assignedVariables = [];
249 $this->assignTemplateVars($assignedVariables);
250
251 // Links dataset: 2 links with thumbnails
252 $this->container->bookmarkService
253 ->expects(static::once())
254 ->method('days')
255 ->willReturnCallback(function () use ($currentDay): array {
256 return [
257 $currentDay->format($currentDay->format('Ymd')),
258 ];
259 })
260 ;
261 $this->container->bookmarkService
262 ->expects(static::once())
263 ->method('filterDay')
264 ->willReturnCallback(function (): array {
265 return [
266 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'),
267 (new Bookmark())
268 ->setId(2)
269 ->setUrl('http://url.tld')
270 ->setTitle(static::generateString(50))
271 ->setDescription(static::generateString(5000))
272 ,
273 (new Bookmark())->setId(3)->setUrl('http://url.tld')->setTitle('title'),
274 (new Bookmark())->setId(4)->setUrl('http://url.tld')->setTitle('title'),
275 (new Bookmark())->setId(5)->setUrl('http://url.tld')->setTitle('title'),
276 (new Bookmark())->setId(6)->setUrl('http://url.tld')->setTitle('title'),
277 (new Bookmark())->setId(7)->setUrl('http://url.tld')->setTitle('title'),
278 ];
279 })
280 ;
281
282 // Make sure that PluginManager hook is triggered
283 $this->container->pluginManager
284 ->expects(static::at(0))
285 ->method('executeHooks')
286 ->willReturnCallback(function (string $hook, array $data, array $param): array {
287 return $data;
288 })
289 ;
290
291 $result = $this->controller->index($request, $response);
292
293 static::assertSame(200, $result->getStatusCode());
294 static::assertSame('daily', (string) $result->getBody());
295 static::assertCount(7, $assignedVariables['linksToDisplay']);
296
297 $columnIds = function (array $column): array {
298 return array_map(function (array $item): int { return $item['id']; }, $column);
299 };
300
301 static::assertSame([1, 4, 6], $columnIds($assignedVariables['cols'][0]));
302 static::assertSame([2], $columnIds($assignedVariables['cols'][1]));
303 static::assertSame([3, 5, 7], $columnIds($assignedVariables['cols'][2]));
304 }
305
306 /**
307 * Daily page - no bookmark
308 */
309 public function testValidIndexControllerInvokeNoBookmark(): void
310 {
311 $request = $this->createMock(Request::class);
312 $response = new Response();
313
314 // Save RainTPL assigned variables
315 $assignedVariables = [];
316 $this->assignTemplateVars($assignedVariables);
317
318 // Links dataset: 2 links with thumbnails
319 $this->container->bookmarkService
320 ->expects(static::once())
321 ->method('days')
322 ->willReturnCallback(function (): array {
323 return [];
324 })
325 ;
326 $this->container->bookmarkService
327 ->expects(static::once())
328 ->method('filterDay')
329 ->willReturnCallback(function (): array {
330 return [];
331 })
332 ;
333
334 // Make sure that PluginManager hook is triggered
335 $this->container->pluginManager
336 ->expects(static::at(0))
337 ->method('executeHooks')
338 ->willReturnCallback(function (string $hook, array $data, array $param): array {
339 return $data;
340 })
341 ;
342
343 $result = $this->controller->index($request, $response);
344
345 static::assertSame(200, $result->getStatusCode());
346 static::assertSame('daily', (string) $result->getBody());
347 static::assertCount(0, $assignedVariables['linksToDisplay']);
348 static::assertSame('Today', $assignedVariables['dayDesc']);
349 static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
350 static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']);
351 }
352
353 /**
354 * Daily RSS - default behaviour
355 */
356 public function testValidRssControllerInvokeDefault(): void
357 {
358 $dates = [
359 new \DateTimeImmutable('2020-05-17'),
360 new \DateTimeImmutable('2020-05-15'),
361 new \DateTimeImmutable('2020-05-13'),
362 ];
363
364 $request = $this->createMock(Request::class);
365 $response = new Response();
366
367 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
368 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
369 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
370 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
371 (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
372 ]);
373
374 $this->container->pageCacheManager
375 ->expects(static::once())
376 ->method('getCachePage')
377 ->willReturnCallback(function (): CachedPage {
378 $cachedPage = $this->createMock(CachedPage::class);
379 $cachedPage->expects(static::once())->method('cache')->with('dailyrss');
380
381 return $cachedPage;
382 }
383 );
384
385 // Save RainTPL assigned variables
386 $assignedVariables = [];
387 $this->assignTemplateVars($assignedVariables);
388
389 $result = $this->controller->rss($request, $response);
390
391 static::assertSame(200, $result->getStatusCode());
392 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
393 static::assertSame('dailyrss', (string) $result->getBody());
394 static::assertSame('Shaarli', $assignedVariables['title']);
395 static::assertSame('http://shaarli', $assignedVariables['index_url']);
396 static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']);
397 static::assertFalse($assignedVariables['hide_timestamps']);
398 static::assertCount(2, $assignedVariables['days']);
399
400 $day = $assignedVariables['days'][$dates[0]->format('Ymd')];
401
402 static::assertEquals($dates[0], $day['date']);
403 static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']);
404 static::assertSame(format_date($dates[0], false), $day['date_human']);
405 static::assertSame('http://shaarli/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
406 static::assertCount(1, $day['links']);
407 static::assertSame(1, $day['links'][0]['id']);
408 static::assertSame('http://domain.tld/1', $day['links'][0]['url']);
409 static::assertEquals($dates[0], $day['links'][0]['created']);
410
411 $day = $assignedVariables['days'][$dates[1]->format('Ymd')];
412
413 static::assertEquals($dates[1], $day['date']);
414 static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']);
415 static::assertSame(format_date($dates[1], false), $day['date_human']);
416 static::assertSame('http://shaarli/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
417 static::assertCount(2, $day['links']);
418
419 static::assertSame(2, $day['links'][0]['id']);
420 static::assertSame('http://domain.tld/2', $day['links'][0]['url']);
421 static::assertEquals($dates[1], $day['links'][0]['created']);
422 static::assertSame(3, $day['links'][1]['id']);
423 static::assertSame('http://domain.tld/3', $day['links'][1]['url']);
424 static::assertEquals($dates[1], $day['links'][1]['created']);
425 }
426
427 /**
428 * Daily RSS - trigger cache rendering
429 */
430 public function testValidRssControllerInvokeTriggerCache(): void
431 {
432 $request = $this->createMock(Request::class);
433 $response = new Response();
434
435 $this->container->pageCacheManager->method('getCachePage')->willReturnCallback(function (): CachedPage {
436 $cachedPage = $this->createMock(CachedPage::class);
437 $cachedPage->method('cachedVersion')->willReturn('this is cache!');
438
439 return $cachedPage;
440 });
441
442 $this->container->bookmarkService->expects(static::never())->method('search');
443
444 $result = $this->controller->rss($request, $response);
445
446 static::assertSame(200, $result->getStatusCode());
447 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
448 static::assertSame('this is cache!', (string) $result->getBody());
449 }
450
451 /**
452 * Daily RSS - No bookmark
453 */
454 public function testValidRssControllerInvokeNoBookmark(): void
455 {
456 $request = $this->createMock(Request::class);
457 $response = new Response();
458
459 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([]);
460
461 // Save RainTPL assigned variables
462 $assignedVariables = [];
463 $this->assignTemplateVars($assignedVariables);
464
465 $result = $this->controller->rss($request, $response);
466
467 static::assertSame(200, $result->getStatusCode());
468 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
469 static::assertSame('dailyrss', (string) $result->getBody());
470 static::assertSame('Shaarli', $assignedVariables['title']);
471 static::assertSame('http://shaarli', $assignedVariables['index_url']);
472 static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']);
473 static::assertFalse($assignedVariables['hide_timestamps']);
474 static::assertCount(0, $assignedVariables['days']);
475 }
476}
diff --git a/tests/front/controller/visitor/ErrorControllerTest.php b/tests/front/controller/visitor/ErrorControllerTest.php
new file mode 100644
index 00000000..e497bfef
--- /dev/null
+++ b/tests/front/controller/visitor/ErrorControllerTest.php
@@ -0,0 +1,70 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Front\Exception\ShaarliFrontException;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class ErrorControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var ErrorController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new ErrorController($this->container);
24 }
25
26 /**
27 * Test displaying error with a ShaarliFrontException: display exception message and use its code for HTTTP code
28 */
29 public function testDisplayFrontExceptionError(): void
30 {
31 $request = $this->createMock(Request::class);
32 $response = new Response();
33
34 $message = 'error message';
35 $errorCode = 418;
36
37 // Save RainTPL assigned variables
38 $assignedVariables = [];
39 $this->assignTemplateVars($assignedVariables);
40
41 $result = ($this->controller)(
42 $request,
43 $response,
44 new class($message, $errorCode) extends ShaarliFrontException {}
45 );
46
47 static::assertSame($errorCode, $result->getStatusCode());
48 static::assertSame($message, $assignedVariables['message']);
49 static::assertArrayNotHasKey('stacktrace', $assignedVariables);
50 }
51
52 /**
53 * Test displaying error with any exception (no debug): only display an error occurred with HTTP 500.
54 */
55 public function testDisplayAnyExceptionErrorNoDebug(): void
56 {
57 $request = $this->createMock(Request::class);
58 $response = new Response();
59
60 // Save RainTPL assigned variables
61 $assignedVariables = [];
62 $this->assignTemplateVars($assignedVariables);
63
64 $result = ($this->controller)($request, $response, new \Exception('abc'));
65
66 static::assertSame(500, $result->getStatusCode());
67 static::assertSame('An unexpected error occurred.', $assignedVariables['message']);
68 static::assertArrayNotHasKey('stacktrace', $assignedVariables);
69 }
70}
diff --git a/tests/front/controller/visitor/FeedControllerTest.php b/tests/front/controller/visitor/FeedControllerTest.php
new file mode 100644
index 00000000..fb417e2a
--- /dev/null
+++ b/tests/front/controller/visitor/FeedControllerTest.php
@@ -0,0 +1,145 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Feed\FeedBuilder;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class FeedControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var FeedController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->container->feedBuilder = $this->createMock(FeedBuilder::class);
24
25 $this->controller = new FeedController($this->container);
26 }
27
28 /**
29 * Feed Controller - RSS default behaviour
30 */
31 public function testDefaultRssController(): void
32 {
33 $request = $this->createMock(Request::class);
34 $response = new Response();
35
36 $this->container->feedBuilder->expects(static::once())->method('setLocale');
37 $this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false);
38 $this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true);
39
40 // Save RainTPL assigned variables
41 $assignedVariables = [];
42 $this->assignTemplateVars($assignedVariables);
43
44 $this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']);
45
46 // Make sure that PluginManager hook is triggered
47 $this->container->pluginManager
48 ->expects(static::at(0))
49 ->method('executeHooks')
50 ->willReturnCallback(function (string $hook, array $data, array $param): void {
51 static::assertSame('render_feed', $hook);
52 static::assertSame('data', $data['content']);
53
54 static::assertArrayHasKey('loggedin', $param);
55 static::assertSame('rss', $param['target']);
56 })
57 ;
58
59 $result = $this->controller->rss($request, $response);
60
61 static::assertSame(200, $result->getStatusCode());
62 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
63 static::assertSame('feed.rss', (string) $result->getBody());
64 static::assertSame('data', $assignedVariables['content']);
65 }
66
67 /**
68 * Feed Controller - ATOM default behaviour
69 */
70 public function testDefaultAtomController(): void
71 {
72 $request = $this->createMock(Request::class);
73 $response = new Response();
74
75 $this->container->feedBuilder->expects(static::once())->method('setLocale');
76 $this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false);
77 $this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true);
78
79 // Save RainTPL assigned variables
80 $assignedVariables = [];
81 $this->assignTemplateVars($assignedVariables);
82
83 $this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']);
84
85 // Make sure that PluginManager hook is triggered
86 $this->container->pluginManager
87 ->expects(static::at(0))
88 ->method('executeHooks')
89 ->willReturnCallback(function (string $hook, array $data, array $param): void {
90 static::assertSame('render_feed', $hook);
91 static::assertSame('data', $data['content']);
92
93 static::assertArrayHasKey('loggedin', $param);
94 static::assertSame('atom', $param['target']);
95 })
96 ;
97
98 $result = $this->controller->atom($request, $response);
99
100 static::assertSame(200, $result->getStatusCode());
101 static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]);
102 static::assertSame('feed.atom', (string) $result->getBody());
103 static::assertSame('data', $assignedVariables['content']);
104 }
105
106 /**
107 * Feed Controller - ATOM with parameters
108 */
109 public function testAtomControllerWithParameters(): void
110 {
111 $request = $this->createMock(Request::class);
112 $request->method('getParams')->willReturn(['parameter' => 'value']);
113 $response = new Response();
114
115 // Save RainTPL assigned variables
116 $assignedVariables = [];
117 $this->assignTemplateVars($assignedVariables);
118
119 $this->container->feedBuilder
120 ->method('buildData')
121 ->with('atom', ['parameter' => 'value'])
122 ->willReturn(['content' => 'data'])
123 ;
124
125 // Make sure that PluginManager hook is triggered
126 $this->container->pluginManager
127 ->expects(static::at(0))
128 ->method('executeHooks')
129 ->willReturnCallback(function (string $hook, array $data, array $param): void {
130 static::assertSame('render_feed', $hook);
131 static::assertSame('data', $data['content']);
132
133 static::assertArrayHasKey('loggedin', $param);
134 static::assertSame('atom', $param['target']);
135 })
136 ;
137
138 $result = $this->controller->atom($request, $response);
139
140 static::assertSame(200, $result->getStatusCode());
141 static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]);
142 static::assertSame('feed.atom', (string) $result->getBody());
143 static::assertSame('data', $assignedVariables['content']);
144 }
145}
diff --git a/tests/front/controller/visitor/FrontControllerMockHelper.php b/tests/front/controller/visitor/FrontControllerMockHelper.php
new file mode 100644
index 00000000..e0bd4ecf
--- /dev/null
+++ b/tests/front/controller/visitor/FrontControllerMockHelper.php
@@ -0,0 +1,119 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\MockObject\MockObject;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Container\ShaarliTestContainer;
11use Shaarli\Formatter\BookmarkFormatter;
12use Shaarli\Formatter\BookmarkRawFormatter;
13use Shaarli\Formatter\FormatterFactory;
14use Shaarli\Plugin\PluginManager;
15use Shaarli\Render\PageBuilder;
16use Shaarli\Render\PageCacheManager;
17use Shaarli\Security\LoginManager;
18use Shaarli\Security\SessionManager;
19
20/**
21 * Trait FrontControllerMockHelper
22 *
23 * Helper trait used to initialize the ShaarliContainer and mock its services for controller tests.
24 *
25 * @property ShaarliTestContainer $container
26 * @package Shaarli\Front\Controller
27 */
28trait FrontControllerMockHelper
29{
30 /** @var ShaarliTestContainer */
31 protected $container;
32
33 /**
34 * Mock the container instance and initialize container's services used by tests
35 */
36 protected function createContainer(): void
37 {
38 $this->container = $this->createMock(ShaarliTestContainer::class);
39
40 $this->container->loginManager = $this->createMock(LoginManager::class);
41
42 // Config
43 $this->container->conf = $this->createMock(ConfigManager::class);
44 $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
45 return $default === null ? $parameter : $default;
46 });
47
48 // PageBuilder
49 $this->container->pageBuilder = $this->createMock(PageBuilder::class);
50 $this->container->pageBuilder
51 ->method('render')
52 ->willReturnCallback(function (string $template): string {
53 return $template;
54 })
55 ;
56
57 // Plugin Manager
58 $this->container->pluginManager = $this->createMock(PluginManager::class);
59
60 // BookmarkService
61 $this->container->bookmarkService = $this->createMock(BookmarkServiceInterface::class);
62
63 // Formatter
64 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
65 $this->container->formatterFactory
66 ->method('getFormatter')
67 ->willReturnCallback(function (): BookmarkFormatter {
68 return new BookmarkRawFormatter($this->container->conf, true);
69 })
70 ;
71
72 // CacheManager
73 $this->container->pageCacheManager = $this->createMock(PageCacheManager::class);
74
75 // SessionManager
76 $this->container->sessionManager = $this->createMock(SessionManager::class);
77
78 // $_SERVER
79 $this->container->environment = [
80 'SERVER_NAME' => 'shaarli',
81 'SERVER_PORT' => '80',
82 'REQUEST_URI' => '/daily-rss',
83 'REMOTE_ADDR' => '1.2.3.4',
84 ];
85
86 $this->container->basePath = '/subfolder';
87 }
88
89 /**
90 * Pass a reference of an array which will be populated by `pageBuilder->assign` calls during execution.
91 *
92 * @param mixed $variables Array reference to populate.
93 */
94 protected function assignTemplateVars(array &$variables): void
95 {
96 $this->container->pageBuilder
97 ->expects(static::atLeastOnce())
98 ->method('assign')
99 ->willReturnCallback(function ($key, $value) use (&$variables) {
100 $variables[$key] = $value;
101
102 return $this;
103 })
104 ;
105 }
106
107 protected static function generateString(int $length): string
108 {
109 // bin2hex(random_bytes) generates string twice as long as given parameter
110 $length = (int) ceil($length / 2);
111
112 return bin2hex(random_bytes($length));
113 }
114
115 /**
116 * Force to be used in PHPUnit context.
117 */
118 protected abstract function createMock($originalClassName): MockObject;
119}
diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php
new file mode 100644
index 00000000..3b855365
--- /dev/null
+++ b/tests/front/controller/visitor/InstallControllerTest.php
@@ -0,0 +1,262 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\AlreadyInstalledException;
10use Shaarli\Security\SessionManager;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class InstallControllerTest extends TestCase
15{
16 use FrontControllerMockHelper;
17
18 const MOCK_FILE = '.tmp';
19
20 /** @var InstallController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->container->conf = $this->createMock(ConfigManager::class);
28 $this->container->conf->method('getConfigFileExt')->willReturn(static::MOCK_FILE);
29 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
30 if ($key === 'resource.raintpl_tpl') {
31 return '.';
32 }
33
34 return $default ?? $key;
35 });
36
37 $this->controller = new InstallController($this->container);
38 }
39
40 protected function tearDown(): void
41 {
42 if (file_exists(static::MOCK_FILE)) {
43 unlink(static::MOCK_FILE);
44 }
45 }
46
47 /**
48 * Test displaying install page with valid session.
49 */
50 public function testInstallIndexWithValidSession(): void
51 {
52 $assignedVariables = [];
53 $this->assignTemplateVars($assignedVariables);
54
55 $request = $this->createMock(Request::class);
56 $response = new Response();
57
58 $this->container->sessionManager = $this->createMock(SessionManager::class);
59 $this->container->sessionManager
60 ->method('getSessionParameter')
61 ->willReturnCallback(function (string $key, $default) {
62 return $key === 'session_tested' ? 'Working' : $default;
63 })
64 ;
65
66 $result = $this->controller->index($request, $response);
67
68 static::assertSame(200, $result->getStatusCode());
69 static::assertSame('install', (string) $result->getBody());
70
71 static::assertIsArray($assignedVariables['continents']);
72 static::assertSame('Africa', $assignedVariables['continents'][0]);
73 static::assertSame('UTC', $assignedVariables['continents']['selected']);
74
75 static::assertIsArray($assignedVariables['cities']);
76 static::assertSame(['continent' => 'Africa', 'city' => 'Abidjan'], $assignedVariables['cities'][0]);
77 static::assertSame('UTC', $assignedVariables['continents']['selected']);
78
79 static::assertIsArray($assignedVariables['languages']);
80 static::assertSame('Automatic', $assignedVariables['languages']['auto']);
81 static::assertSame('French', $assignedVariables['languages']['fr']);
82 }
83
84 /**
85 * Instantiate the install controller with an existing config file: exception.
86 */
87 public function testInstallWithExistingConfigFile(): void
88 {
89 $this->expectException(AlreadyInstalledException::class);
90
91 touch(static::MOCK_FILE);
92
93 $this->controller = new InstallController($this->container);
94 }
95
96 /**
97 * Call controller without session yet defined, redirect to test session install page.
98 */
99 public function testInstallRedirectToSessionTest(): void
100 {
101 $request = $this->createMock(Request::class);
102 $response = new Response();
103
104 $this->container->sessionManager = $this->createMock(SessionManager::class);
105 $this->container->sessionManager
106 ->expects(static::once())
107 ->method('setSessionParameter')
108 ->with(InstallController::SESSION_TEST_KEY, InstallController::SESSION_TEST_VALUE)
109 ;
110
111 $result = $this->controller->index($request, $response);
112
113 static::assertSame(302, $result->getStatusCode());
114 static::assertSame('/subfolder/install/session-test', $result->getHeader('location')[0]);
115 }
116
117 /**
118 * Call controller in session test mode: valid session then redirect to install page.
119 */
120 public function testInstallSessionTestValid(): void
121 {
122 $request = $this->createMock(Request::class);
123 $response = new Response();
124
125 $this->container->sessionManager = $this->createMock(SessionManager::class);
126 $this->container->sessionManager
127 ->method('getSessionParameter')
128 ->with(InstallController::SESSION_TEST_KEY)
129 ->willReturn(InstallController::SESSION_TEST_VALUE)
130 ;
131
132 $result = $this->controller->sessionTest($request, $response);
133
134 static::assertSame(302, $result->getStatusCode());
135 static::assertSame('/subfolder/install', $result->getHeader('location')[0]);
136 }
137
138 /**
139 * Call controller in session test mode: invalid session then redirect to error page.
140 */
141 public function testInstallSessionTestError(): void
142 {
143 $assignedVars = [];
144 $this->assignTemplateVars($assignedVars);
145
146 $request = $this->createMock(Request::class);
147 $response = new Response();
148
149 $this->container->sessionManager = $this->createMock(SessionManager::class);
150 $this->container->sessionManager
151 ->method('getSessionParameter')
152 ->with(InstallController::SESSION_TEST_KEY)
153 ->willReturn('KO')
154 ;
155
156 $result = $this->controller->sessionTest($request, $response);
157
158 static::assertSame(200, $result->getStatusCode());
159 static::assertSame('error', (string) $result->getBody());
160 static::assertStringStartsWith(
161 '<pre>Sessions do not seem to work correctly on your server',
162 $assignedVars['message']
163 );
164 }
165
166 /**
167 * Test saving valid data from install form. Also initialize datastore.
168 */
169 public function testSaveInstallValid(): void
170 {
171 $providedParameters = [
172 'continent' => 'Europe',
173 'city' => 'Berlin',
174 'setlogin' => 'bob',
175 'setpassword' => 'password',
176 'title' => 'Shaarli',
177 'language' => 'fr',
178 'updateCheck' => true,
179 'enableApi' => true,
180 ];
181
182 $expectedSettings = [
183 'general.timezone' => 'Europe/Berlin',
184 'credentials.login' => 'bob',
185 'credentials.salt' => '_NOT_EMPTY',
186 'credentials.hash' => '_NOT_EMPTY',
187 'general.title' => 'Shaarli',
188 'translation.language' => 'en',
189 'updates.check_updates' => true,
190 'api.enabled' => true,
191 'api.secret' => '_NOT_EMPTY',
192 'general.header_link' => '/subfolder',
193 ];
194
195 $request = $this->createMock(Request::class);
196 $request->method('getParam')->willReturnCallback(function (string $key) use ($providedParameters) {
197 return $providedParameters[$key] ?? null;
198 });
199 $response = new Response();
200
201 $this->container->conf = $this->createMock(ConfigManager::class);
202 $this->container->conf
203 ->method('get')
204 ->willReturnCallback(function (string $key, $value) {
205 if ($key === 'credentials.login') {
206 return 'bob';
207 } elseif ($key === 'credentials.salt') {
208 return 'salt';
209 }
210
211 return $value;
212 })
213 ;
214 $this->container->conf
215 ->expects(static::exactly(count($expectedSettings)))
216 ->method('set')
217 ->willReturnCallback(function (string $key, $value) use ($expectedSettings) {
218 if ($expectedSettings[$key] ?? null === '_NOT_EMPTY') {
219 static::assertNotEmpty($value);
220 } else {
221 static::assertSame($expectedSettings[$key], $value);
222 }
223 })
224 ;
225 $this->container->conf->expects(static::once())->method('write');
226
227 $this->container->sessionManager
228 ->expects(static::once())
229 ->method('setSessionParameter')
230 ->with(SessionManager::KEY_SUCCESS_MESSAGES)
231 ;
232
233 $result = $this->controller->save($request, $response);
234
235 static::assertSame(302, $result->getStatusCode());
236 static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
237 }
238
239 /**
240 * Test default settings (timezone and title).
241 * Also check that bookmarks are not initialized if
242 */
243 public function testSaveInstallDefaultValues(): void
244 {
245 $confSettings = [];
246
247 $request = $this->createMock(Request::class);
248 $response = new Response();
249
250 $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
251 $confSettings[$key] = $value;
252 });
253
254 $result = $this->controller->save($request, $response);
255
256 static::assertSame(302, $result->getStatusCode());
257 static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
258
259 static::assertSame('UTC', $confSettings['general.timezone']);
260 static::assertSame('Shared bookmarks on http://shaarli', $confSettings['general.title']);
261 }
262}
diff --git a/tests/front/controller/visitor/LoginControllerTest.php b/tests/front/controller/visitor/LoginControllerTest.php
new file mode 100644
index 00000000..0a21f938
--- /dev/null
+++ b/tests/front/controller/visitor/LoginControllerTest.php
@@ -0,0 +1,404 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\LoginBannedException;
10use Shaarli\Front\Exception\WrongTokenException;
11use Shaarli\Render\TemplatePage;
12use Shaarli\Security\CookieManager;
13use Shaarli\Security\SessionManager;
14use Slim\Http\Request;
15use Slim\Http\Response;
16
17class LoginControllerTest extends TestCase
18{
19 use FrontControllerMockHelper;
20
21 /** @var LoginController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->container->cookieManager = $this->createMock(CookieManager::class);
29 $this->container->sessionManager->method('checkToken')->willReturn(true);
30
31 $this->controller = new LoginController($this->container);
32 }
33
34 /**
35 * Test displaying login form with valid parameters.
36 */
37 public function testValidControllerInvoke(): void
38 {
39 $request = $this->createMock(Request::class);
40 $request
41 ->expects(static::atLeastOnce())
42 ->method('getParam')
43 ->willReturnCallback(function (string $key) {
44 return 'returnurl' === $key ? '> referer' : null;
45 })
46 ;
47 $response = new Response();
48
49 $assignedVariables = [];
50 $this->container->pageBuilder
51 ->method('assign')
52 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
53 $assignedVariables[$key] = $value;
54
55 return $this;
56 })
57 ;
58
59 $this->container->loginManager->method('canLogin')->willReturn(true);
60
61 $result = $this->controller->index($request, $response);
62
63 static::assertInstanceOf(Response::class, $result);
64 static::assertSame(200, $result->getStatusCode());
65 static::assertSame(TemplatePage::LOGIN, (string) $result->getBody());
66
67 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
68 static::assertSame(true, $assignedVariables['remember_user_default']);
69 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
70 }
71
72 /**
73 * Test displaying login form with username defined in the request.
74 */
75 public function testValidControllerInvokeWithUserName(): void
76 {
77 $this->container->environment = ['HTTP_REFERER' => '> referer'];
78
79 $request = $this->createMock(Request::class);
80 $request
81 ->expects(static::atLeastOnce())
82 ->method('getParam')
83 ->willReturnCallback(function (string $key, $default) {
84 if ('login' === $key) {
85 return 'myUser>';
86 }
87
88 return $default;
89 })
90 ;
91 $response = new Response();
92
93 $assignedVariables = [];
94 $this->container->pageBuilder
95 ->method('assign')
96 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
97 $assignedVariables[$key] = $value;
98
99 return $this;
100 })
101 ;
102
103 $this->container->loginManager->expects(static::once())->method('canLogin')->willReturn(true);
104
105 $result = $this->controller->index($request, $response);
106
107 static::assertInstanceOf(Response::class, $result);
108 static::assertSame(200, $result->getStatusCode());
109 static::assertSame('loginform', (string) $result->getBody());
110
111 static::assertSame('myUser&gt;', $assignedVariables['username']);
112 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
113 static::assertSame(true, $assignedVariables['remember_user_default']);
114 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
115 }
116
117 /**
118 * Test displaying login page while being logged in.
119 */
120 public function testLoginControllerWhileLoggedIn(): void
121 {
122 $request = $this->createMock(Request::class);
123 $response = new Response();
124
125 $this->container->loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true);
126
127 $result = $this->controller->index($request, $response);
128
129 static::assertInstanceOf(Response::class, $result);
130 static::assertSame(302, $result->getStatusCode());
131 static::assertSame(['/subfolder/'], $result->getHeader('Location'));
132 }
133
134 /**
135 * Test displaying login page with open shaarli configured: redirect to homepage.
136 */
137 public function testLoginControllerOpenShaarli(): void
138 {
139 $request = $this->createMock(Request::class);
140 $response = new Response();
141
142 $conf = $this->createMock(ConfigManager::class);
143 $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
144 if ($parameter === 'security.open_shaarli') {
145 return true;
146 }
147 return $default;
148 });
149 $this->container->conf = $conf;
150
151 $result = $this->controller->index($request, $response);
152
153 static::assertInstanceOf(Response::class, $result);
154 static::assertSame(302, $result->getStatusCode());
155 static::assertSame(['/subfolder/'], $result->getHeader('Location'));
156 }
157
158 /**
159 * Test displaying login page while being banned.
160 */
161 public function testLoginControllerWhileBanned(): void
162 {
163 $request = $this->createMock(Request::class);
164 $response = new Response();
165
166 $this->container->loginManager->method('isLoggedIn')->willReturn(false);
167 $this->container->loginManager->method('canLogin')->willReturn(false);
168
169 $this->expectException(LoginBannedException::class);
170
171 $this->controller->index($request, $response);
172 }
173
174 /**
175 * Test processing login with valid parameters.
176 */
177 public function testProcessLoginWithValidParameters(): void
178 {
179 $parameters = [
180 'login' => 'bob',
181 'password' => 'pass',
182 ];
183 $request = $this->createMock(Request::class);
184 $request
185 ->expects(static::atLeastOnce())
186 ->method('getParam')
187 ->willReturnCallback(function (string $key) use ($parameters) {
188 return $parameters[$key] ?? null;
189 })
190 ;
191 $response = new Response();
192
193 $this->container->loginManager->method('canLogin')->willReturn(true);
194 $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
195 $this->container->loginManager
196 ->expects(static::once())
197 ->method('checkCredentials')
198 ->with('1.2.3.4', '1.2.3.4', 'bob', 'pass')
199 ->willReturn(true)
200 ;
201 $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
202
203 $this->container->sessionManager->expects(static::never())->method('extendSession');
204 $this->container->sessionManager->expects(static::once())->method('destroy');
205 $this->container->sessionManager
206 ->expects(static::once())
207 ->method('cookieParameters')
208 ->with(0, '/subfolder/', 'shaarli')
209 ;
210 $this->container->sessionManager->expects(static::once())->method('start');
211 $this->container->sessionManager->expects(static::once())->method('regenerateId')->with(true);
212
213 $result = $this->controller->login($request, $response);
214
215 static::assertSame(302, $result->getStatusCode());
216 static::assertSame('/subfolder/', $result->getHeader('location')[0]);
217 }
218
219 /**
220 * Test processing login with return URL.
221 */
222 public function testProcessLoginWithReturnUrl(): void
223 {
224 $parameters = [
225 'returnurl' => 'http://shaarli/subfolder/admin/shaare',
226 ];
227 $request = $this->createMock(Request::class);
228 $request
229 ->expects(static::atLeastOnce())
230 ->method('getParam')
231 ->willReturnCallback(function (string $key) use ($parameters) {
232 return $parameters[$key] ?? null;
233 })
234 ;
235 $response = new Response();
236
237 $this->container->loginManager->method('canLogin')->willReturn(true);
238 $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
239 $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(true);
240 $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
241
242 $result = $this->controller->login($request, $response);
243
244 static::assertSame(302, $result->getStatusCode());
245 static::assertSame('/subfolder/admin/shaare', $result->getHeader('location')[0]);
246 }
247
248 /**
249 * Test processing login with remember me session enabled.
250 */
251 public function testProcessLoginLongLastingSession(): void
252 {
253 $parameters = [
254 'longlastingsession' => true,
255 ];
256 $request = $this->createMock(Request::class);
257 $request
258 ->expects(static::atLeastOnce())
259 ->method('getParam')
260 ->willReturnCallback(function (string $key) use ($parameters) {
261 return $parameters[$key] ?? null;
262 })
263 ;
264 $response = new Response();
265
266 $this->container->loginManager->method('canLogin')->willReturn(true);
267 $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
268 $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(true);
269 $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
270
271 $this->container->sessionManager->expects(static::once())->method('destroy');
272 $this->container->sessionManager
273 ->expects(static::once())
274 ->method('cookieParameters')
275 ->with(42, '/subfolder/', 'shaarli')
276 ;
277 $this->container->sessionManager->expects(static::once())->method('start');
278 $this->container->sessionManager->expects(static::once())->method('regenerateId')->with(true);
279 $this->container->sessionManager->expects(static::once())->method('extendSession')->willReturn(42);
280
281 $this->container->cookieManager = $this->createMock(CookieManager::class);
282 $this->container->cookieManager
283 ->expects(static::once())
284 ->method('setCookieParameter')
285 ->willReturnCallback(function (string $name): CookieManager {
286 static::assertSame(CookieManager::STAY_SIGNED_IN, $name);
287
288 return $this->container->cookieManager;
289 })
290 ;
291
292 $result = $this->controller->login($request, $response);
293
294 static::assertSame(302, $result->getStatusCode());
295 static::assertSame('/subfolder/', $result->getHeader('location')[0]);
296 }
297
298 /**
299 * Test processing login with invalid credentials
300 */
301 public function testProcessLoginWrongCredentials(): void
302 {
303 $parameters = [
304 'returnurl' => 'http://shaarli/subfolder/admin/shaare',
305 ];
306 $request = $this->createMock(Request::class);
307 $request
308 ->expects(static::atLeastOnce())
309 ->method('getParam')
310 ->willReturnCallback(function (string $key) use ($parameters) {
311 return $parameters[$key] ?? null;
312 })
313 ;
314 $response = new Response();
315
316 $this->container->loginManager->method('canLogin')->willReturn(true);
317 $this->container->loginManager->expects(static::once())->method('handleFailedLogin');
318 $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(false);
319
320 $this->container->sessionManager
321 ->expects(static::once())
322 ->method('setSessionParameter')
323 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Wrong login/password.'])
324 ;
325
326 $result = $this->controller->login($request, $response);
327
328 static::assertSame(200, $result->getStatusCode());
329 static::assertSame(TemplatePage::LOGIN, (string) $result->getBody());
330 }
331
332 /**
333 * Test processing login with wrong token
334 */
335 public function testProcessLoginWrongToken(): void
336 {
337 $request = $this->createMock(Request::class);
338 $response = new Response();
339
340 $this->container->sessionManager = $this->createMock(SessionManager::class);
341 $this->container->sessionManager->method('checkToken')->willReturn(false);
342
343 $this->expectException(WrongTokenException::class);
344
345 $this->controller->login($request, $response);
346 }
347
348 /**
349 * Test processing login with wrong token
350 */
351 public function testProcessLoginAlreadyLoggedIn(): void
352 {
353 $request = $this->createMock(Request::class);
354 $response = new Response();
355
356 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
357 $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
358 $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
359
360 $result = $this->controller->login($request, $response);
361
362 static::assertSame(302, $result->getStatusCode());
363 static::assertSame('/subfolder/', $result->getHeader('location')[0]);
364 }
365
366 /**
367 * Test processing login with wrong token
368 */
369 public function testProcessLoginInOpenShaarli(): void
370 {
371 $request = $this->createMock(Request::class);
372 $response = new Response();
373
374 $this->container->conf = $this->createMock(ConfigManager::class);
375 $this->container->conf->method('get')->willReturnCallback(function (string $key, $value) {
376 return 'security.open_shaarli' === $key ? true : $value;
377 });
378
379 $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
380 $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
381
382 $result = $this->controller->login($request, $response);
383
384 static::assertSame(302, $result->getStatusCode());
385 static::assertSame('/subfolder/', $result->getHeader('location')[0]);
386 }
387
388 /**
389 * Test processing login while being banned
390 */
391 public function testProcessLoginWhileBanned(): void
392 {
393 $request = $this->createMock(Request::class);
394 $response = new Response();
395
396 $this->container->loginManager->method('canLogin')->willReturn(false);
397 $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
398 $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
399
400 $this->expectException(LoginBannedException::class);
401
402 $this->controller->login($request, $response);
403 }
404}
diff --git a/tests/front/controller/visitor/OpenSearchControllerTest.php b/tests/front/controller/visitor/OpenSearchControllerTest.php
new file mode 100644
index 00000000..5f9f5b12
--- /dev/null
+++ b/tests/front/controller/visitor/OpenSearchControllerTest.php
@@ -0,0 +1,44 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class OpenSearchControllerTest extends TestCase
12{
13 use FrontControllerMockHelper;
14
15 /** @var OpenSearchController */
16 protected $controller;
17
18 public function setUp(): void
19 {
20 $this->createContainer();
21
22 $this->controller = new OpenSearchController($this->container);
23 }
24
25 public function testOpenSearchController(): void
26 {
27 $request = $this->createMock(Request::class);
28 $response = new Response();
29
30 // Save RainTPL assigned variables
31 $assignedVariables = [];
32 $this->assignTemplateVars($assignedVariables);
33
34 $result = $this->controller->index($request, $response);
35
36 static::assertSame(200, $result->getStatusCode());
37 static::assertStringContainsString(
38 'application/opensearchdescription+xml',
39 $result->getHeader('Content-Type')[0]
40 );
41 static::assertSame('opensearch', (string) $result->getBody());
42 static::assertSame('http://shaarli', $assignedVariables['serverurl']);
43 }
44}
diff --git a/tests/front/controller/visitor/PictureWallControllerTest.php b/tests/front/controller/visitor/PictureWallControllerTest.php
new file mode 100644
index 00000000..3dc3f292
--- /dev/null
+++ b/tests/front/controller/visitor/PictureWallControllerTest.php
@@ -0,0 +1,121 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Front\Exception\ThumbnailsDisabledException;
11use Shaarli\Thumbnailer;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class PictureWallControllerTest extends TestCase
16{
17 use FrontControllerMockHelper;
18
19 /** @var PictureWallController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new PictureWallController($this->container);
27 }
28
29 public function testValidControllerInvokeDefault(): void
30 {
31 $request = $this->createMock(Request::class);
32 $request->expects(static::once())->method('getQueryParams')->willReturn([]);
33 $response = new Response();
34
35 // ConfigManager: thumbnails are enabled
36 $this->container->conf = $this->createMock(ConfigManager::class);
37 $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
38 if ($parameter === 'thumbnails.mode') {
39 return Thumbnailer::MODE_COMMON;
40 }
41
42 return $default;
43 });
44
45 // Save RainTPL assigned variables
46 $assignedVariables = [];
47 $this->assignTemplateVars($assignedVariables);
48
49 // Links dataset: 2 links with thumbnails
50 $this->container->bookmarkService
51 ->expects(static::once())
52 ->method('search')
53 ->willReturnCallback(function (array $parameters, ?string $visibility): array {
54 // Visibility is set through the container, not the call
55 static::assertNull($visibility);
56
57 // No query parameters
58 if (count($parameters) === 0) {
59 return [
60 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'),
61 (new Bookmark())->setId(2)->setUrl('http://url2.tld'),
62 (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'),
63 ];
64 }
65 })
66 ;
67
68 // Make sure that PluginManager hook is triggered
69 $this->container->pluginManager
70 ->expects(static::at(0))
71 ->method('executeHooks')
72 ->willReturnCallback(function (string $hook, array $data, array $param): array {
73 static::assertSame('render_picwall', $hook);
74 static::assertArrayHasKey('linksToDisplay', $data);
75 static::assertCount(2, $data['linksToDisplay']);
76 static::assertSame(1, $data['linksToDisplay'][0]['id']);
77 static::assertSame(3, $data['linksToDisplay'][1]['id']);
78 static::assertArrayHasKey('loggedin', $param);
79
80 return $data;
81 });
82
83 $result = $this->controller->index($request, $response);
84
85 static::assertSame(200, $result->getStatusCode());
86 static::assertSame('picwall', (string) $result->getBody());
87 static::assertSame('Picture wall - Shaarli', $assignedVariables['pagetitle']);
88 static::assertCount(2, $assignedVariables['linksToDisplay']);
89
90 $link = $assignedVariables['linksToDisplay'][0];
91
92 static::assertSame(1, $link['id']);
93 static::assertSame('http://url.tld', $link['url']);
94 static::assertSame('thumb1', $link['thumbnail']);
95
96 $link = $assignedVariables['linksToDisplay'][1];
97
98 static::assertSame(3, $link['id']);
99 static::assertSame('http://url3.tld', $link['url']);
100 static::assertSame('thumb2', $link['thumbnail']);
101 }
102
103 public function testControllerWithThumbnailsDisabled(): void
104 {
105 $this->expectException(ThumbnailsDisabledException::class);
106
107 $request = $this->createMock(Request::class);
108 $response = new Response();
109
110 // ConfigManager: thumbnails are disabled
111 $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
112 if ($parameter === 'thumbnails.mode') {
113 return Thumbnailer::MODE_NONE;
114 }
115
116 return $default;
117 });
118
119 $this->controller->index($request, $response);
120 }
121}
diff --git a/tests/front/controller/visitor/PublicSessionFilterControllerTest.php b/tests/front/controller/visitor/PublicSessionFilterControllerTest.php
new file mode 100644
index 00000000..06352750
--- /dev/null
+++ b/tests/front/controller/visitor/PublicSessionFilterControllerTest.php
@@ -0,0 +1,122 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Security\SessionManager;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class PublicSessionFilterControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var PublicSessionFilterController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new PublicSessionFilterController($this->container);
24 }
25
26 /**
27 * Link per page - Default call with valid parameter and a referer.
28 */
29 public function testLinksPerPage(): void
30 {
31 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
32
33 $request = $this->createMock(Request::class);
34 $request->method('getParam')->with('nb')->willReturn('8');
35 $response = new Response();
36
37 $this->container->sessionManager
38 ->expects(static::once())
39 ->method('setSessionParameter')
40 ->with(SessionManager::KEY_LINKS_PER_PAGE, 8)
41 ;
42
43 $result = $this->controller->linksPerPage($request, $response);
44
45 static::assertInstanceOf(Response::class, $result);
46 static::assertSame(302, $result->getStatusCode());
47 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
48 }
49
50 /**
51 * Link per page - Invalid value, should use default value (20)
52 */
53 public function testLinksPerPageNotValid(): void
54 {
55 $request = $this->createMock(Request::class);
56 $request->method('getParam')->with('nb')->willReturn('test');
57 $response = new Response();
58
59 $this->container->sessionManager
60 ->expects(static::once())
61 ->method('setSessionParameter')
62 ->with(SessionManager::KEY_LINKS_PER_PAGE, 20)
63 ;
64
65 $result = $this->controller->linksPerPage($request, $response);
66
67 static::assertInstanceOf(Response::class, $result);
68 static::assertSame(302, $result->getStatusCode());
69 static::assertSame(['/subfolder/'], $result->getHeader('location'));
70 }
71
72 /**
73 * Untagged only - valid call
74 */
75 public function testUntaggedOnly(): void
76 {
77 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
78
79 $request = $this->createMock(Request::class);
80 $response = new Response();
81
82 $this->container->sessionManager
83 ->expects(static::once())
84 ->method('setSessionParameter')
85 ->with(SessionManager::KEY_UNTAGGED_ONLY, true)
86 ;
87
88 $result = $this->controller->untaggedOnly($request, $response);
89
90 static::assertInstanceOf(Response::class, $result);
91 static::assertSame(302, $result->getStatusCode());
92 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
93 }
94
95 /**
96 * Untagged only - toggle off
97 */
98 public function testUntaggedOnlyToggleOff(): void
99 {
100 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
101
102 $request = $this->createMock(Request::class);
103 $response = new Response();
104
105 $this->container->sessionManager
106 ->method('getSessionParameter')
107 ->with(SessionManager::KEY_UNTAGGED_ONLY)
108 ->willReturn(true)
109 ;
110 $this->container->sessionManager
111 ->expects(static::once())
112 ->method('setSessionParameter')
113 ->with(SessionManager::KEY_UNTAGGED_ONLY, false)
114 ;
115
116 $result = $this->controller->untaggedOnly($request, $response);
117
118 static::assertInstanceOf(Response::class, $result);
119 static::assertSame(302, $result->getStatusCode());
120 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
121 }
122}
diff --git a/tests/front/controller/visitor/ShaarliVisitorControllerTest.php b/tests/front/controller/visitor/ShaarliVisitorControllerTest.php
new file mode 100644
index 00000000..316ce49c
--- /dev/null
+++ b/tests/front/controller/visitor/ShaarliVisitorControllerTest.php
@@ -0,0 +1,215 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkFilter;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ShaarliControllerTest
14 *
15 * This class is used to test default behavior of ShaarliVisitorController abstract class.
16 * It uses a dummy non abstract controller.
17 */
18class ShaarliVisitorControllerTest extends TestCase
19{
20 use FrontControllerMockHelper;
21
22 /** @var LoginController */
23 protected $controller;
24
25 /** @var mixed[] List of variable assigned to the template */
26 protected $assignedValues;
27
28 /** @var Request */
29 protected $request;
30
31 public function setUp(): void
32 {
33 $this->createContainer();
34
35 $this->controller = new class($this->container) extends ShaarliVisitorController
36 {
37 public function assignView(string $key, $value): ShaarliVisitorController
38 {
39 return parent::assignView($key, $value);
40 }
41
42 public function render(string $template): string
43 {
44 return parent::render($template);
45 }
46
47 public function redirectFromReferer(
48 Request $request,
49 Response $response,
50 array $loopTerms = [],
51 array $clearParams = [],
52 string $anchor = null
53 ): Response {
54 return parent::redirectFromReferer($request, $response, $loopTerms, $clearParams, $anchor);
55 }
56 };
57 $this->assignedValues = [];
58
59 $this->request = $this->createMock(Request::class);
60 }
61
62 public function testAssignView(): void
63 {
64 $this->assignTemplateVars($this->assignedValues);
65
66 $self = $this->controller->assignView('variableName', 'variableValue');
67
68 static::assertInstanceOf(ShaarliVisitorController::class, $self);
69 static::assertSame('variableValue', $this->assignedValues['variableName']);
70 }
71
72 public function testRender(): void
73 {
74 $this->assignTemplateVars($this->assignedValues);
75
76 $this->container->bookmarkService
77 ->method('count')
78 ->willReturnCallback(function (string $visibility): int {
79 return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10;
80 })
81 ;
82
83 $this->container->pluginManager
84 ->method('executeHooks')
85 ->willReturnCallback(function (string $hook, array &$data, array $params): array {
86 return $data[$hook] = $params;
87 });
88 $this->container->pluginManager->method('getErrors')->willReturn(['error']);
89
90 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
91
92 $render = $this->controller->render('templateName');
93
94 static::assertSame('templateName', $render);
95
96 static::assertSame(10, $this->assignedValues['linkcount']);
97 static::assertSame(5, $this->assignedValues['privateLinkcount']);
98 static::assertSame(['error'], $this->assignedValues['plugin_errors']);
99
100 static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']);
101 static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']);
102 static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']);
103 static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']);
104 static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
105 static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
106 }
107
108 /**
109 * Test redirectFromReferer() - Default behaviour
110 */
111 public function testRedirectFromRefererDefault(): void
112 {
113 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
114
115 $response = new Response();
116
117 $result = $this->controller->redirectFromReferer($this->request, $response);
118
119 static::assertSame(302, $result->getStatusCode());
120 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
121 }
122
123 /**
124 * Test redirectFromReferer() - With a loop term not matched in the referer
125 */
126 public function testRedirectFromRefererWithUnmatchedLoopTerm(): void
127 {
128 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
129
130 $response = new Response();
131
132 $result = $this->controller->redirectFromReferer($this->request, $response, ['nope']);
133
134 static::assertSame(302, $result->getStatusCode());
135 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
136 }
137
138 /**
139 * Test redirectFromReferer() - With a loop term matching the referer in its path -> redirect to default
140 */
141 public function testRedirectFromRefererWithMatchingLoopTermInPath(): void
142 {
143 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
144
145 $response = new Response();
146
147 $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'controller']);
148
149 static::assertSame(302, $result->getStatusCode());
150 static::assertSame(['/subfolder/'], $result->getHeader('location'));
151 }
152
153 /**
154 * Test redirectFromReferer() - With a loop term matching the referer in its query parameters -> redirect to default
155 */
156 public function testRedirectFromRefererWithMatchingLoopTermInQueryParam(): void
157 {
158 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
159
160 $response = new Response();
161
162 $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'other']);
163
164 static::assertSame(302, $result->getStatusCode());
165 static::assertSame(['/subfolder/'], $result->getHeader('location'));
166 }
167
168 /**
169 * Test redirectFromReferer() - With a loop term matching the referer in its query value
170 * -> we do not block redirection for query parameter values.
171 */
172 public function testRedirectFromRefererWithMatchingLoopTermInQueryValue(): void
173 {
174 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
175
176 $response = new Response();
177
178 $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'param']);
179
180 static::assertSame(302, $result->getStatusCode());
181 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
182 }
183
184 /**
185 * Test redirectFromReferer() - With a loop term matching the referer in its domain name
186 * -> we do not block redirection for shaarli's hosts
187 */
188 public function testRedirectFromRefererWithLoopTermInDomain(): void
189 {
190 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
191
192 $response = new Response();
193
194 $result = $this->controller->redirectFromReferer($this->request, $response, ['shaarli']);
195
196 static::assertSame(302, $result->getStatusCode());
197 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
198 }
199
200 /**
201 * Test redirectFromReferer() - With a loop term matching a query parameter AND clear this query param
202 * -> the param should be cleared before checking if it matches the redir loop terms
203 */
204 public function testRedirectFromRefererWithMatchingClearedParam(): void
205 {
206 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
207
208 $response = new Response();
209
210 $result = $this->controller->redirectFromReferer($this->request, $response, ['query'], ['query']);
211
212 static::assertSame(302, $result->getStatusCode());
213 static::assertSame(['/subfolder/controller?other=2'], $result->getHeader('location'));
214 }
215}
diff --git a/tests/front/controller/visitor/TagCloudControllerTest.php b/tests/front/controller/visitor/TagCloudControllerTest.php
new file mode 100644
index 00000000..9a6a4bc0
--- /dev/null
+++ b/tests/front/controller/visitor/TagCloudControllerTest.php
@@ -0,0 +1,369 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkFilter;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class TagCloudControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var TagCloudController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new TagCloudController($this->container);
24 }
25
26 /**
27 * Tag Cloud - default parameters
28 */
29 public function testValidCloudControllerInvokeDefault(): void
30 {
31 $allTags = [
32 'ghi' => 1,
33 'abc' => 3,
34 'def' => 12,
35 ];
36 $expectedOrder = ['abc', 'def', 'ghi'];
37
38 $request = $this->createMock(Request::class);
39 $response = new Response();
40
41 // Save RainTPL assigned variables
42 $assignedVariables = [];
43 $this->assignTemplateVars($assignedVariables);
44
45 $this->container->bookmarkService
46 ->expects(static::once())
47 ->method('bookmarksCountPerTag')
48 ->with([], null)
49 ->willReturnCallback(function () use ($allTags): array {
50 return $allTags;
51 })
52 ;
53
54 // Make sure that PluginManager hook is triggered
55 $this->container->pluginManager
56 ->expects(static::at(0))
57 ->method('executeHooks')
58 ->willReturnCallback(function (string $hook, array $data, array $param): array {
59 static::assertSame('render_tagcloud', $hook);
60 static::assertSame('', $data['search_tags']);
61 static::assertCount(3, $data['tags']);
62
63 static::assertArrayHasKey('loggedin', $param);
64
65 return $data;
66 })
67 ;
68
69 $result = $this->controller->cloud($request, $response);
70
71 static::assertSame(200, $result->getStatusCode());
72 static::assertSame('tag.cloud', (string) $result->getBody());
73 static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']);
74
75 static::assertSame('', $assignedVariables['search_tags']);
76 static::assertCount(3, $assignedVariables['tags']);
77 static::assertSame($expectedOrder, array_keys($assignedVariables['tags']));
78
79 foreach ($allTags as $tag => $count) {
80 static::assertArrayHasKey($tag, $assignedVariables['tags']);
81 static::assertSame($count, $assignedVariables['tags'][$tag]['count']);
82 static::assertGreaterThan(0, $assignedVariables['tags'][$tag]['size']);
83 static::assertLessThan(5, $assignedVariables['tags'][$tag]['size']);
84 }
85 }
86
87 /**
88 * Tag Cloud - Additional parameters:
89 * - logged in
90 * - visibility private
91 * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
92 */
93 public function testValidCloudControllerInvokeWithParameters(): void
94 {
95 $request = $this->createMock(Request::class);
96 $request
97 ->method('getQueryParam')
98 ->with()
99 ->willReturnCallback(function (string $key): ?string {
100 if ('searchtags' === $key) {
101 return 'ghi def';
102 }
103
104 return null;
105 })
106 ;
107 $response = new Response();
108
109 // Save RainTPL assigned variables
110 $assignedVariables = [];
111 $this->assignTemplateVars($assignedVariables);
112
113 $this->container->loginManager->method('isLoggedin')->willReturn(true);
114 $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private');
115
116 $this->container->bookmarkService
117 ->expects(static::once())
118 ->method('bookmarksCountPerTag')
119 ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE)
120 ->willReturnCallback(function (): array {
121 return ['abc' => 3];
122 })
123 ;
124
125 // Make sure that PluginManager hook is triggered
126 $this->container->pluginManager
127 ->expects(static::at(0))
128 ->method('executeHooks')
129 ->willReturnCallback(function (string $hook, array $data, array $param): array {
130 static::assertSame('render_tagcloud', $hook);
131 static::assertSame('ghi def', $data['search_tags']);
132 static::assertCount(1, $data['tags']);
133
134 static::assertArrayHasKey('loggedin', $param);
135
136 return $data;
137 })
138 ;
139
140 $result = $this->controller->cloud($request, $response);
141
142 static::assertSame(200, $result->getStatusCode());
143 static::assertSame('tag.cloud', (string) $result->getBody());
144 static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']);
145
146 static::assertSame('ghi def', $assignedVariables['search_tags']);
147 static::assertCount(1, $assignedVariables['tags']);
148
149 static::assertArrayHasKey('abc', $assignedVariables['tags']);
150 static::assertSame(3, $assignedVariables['tags']['abc']['count']);
151 static::assertGreaterThan(0, $assignedVariables['tags']['abc']['size']);
152 static::assertLessThan(5, $assignedVariables['tags']['abc']['size']);
153 }
154
155 /**
156 * Tag Cloud - empty
157 */
158 public function testEmptyCloud(): void
159 {
160 $request = $this->createMock(Request::class);
161 $response = new Response();
162
163 // Save RainTPL assigned variables
164 $assignedVariables = [];
165 $this->assignTemplateVars($assignedVariables);
166
167 $this->container->bookmarkService
168 ->expects(static::once())
169 ->method('bookmarksCountPerTag')
170 ->with([], null)
171 ->willReturnCallback(function (array $parameters, ?string $visibility): array {
172 return [];
173 })
174 ;
175
176 // Make sure that PluginManager hook is triggered
177 $this->container->pluginManager
178 ->expects(static::at(0))
179 ->method('executeHooks')
180 ->willReturnCallback(function (string $hook, array $data, array $param): array {
181 static::assertSame('render_tagcloud', $hook);
182 static::assertSame('', $data['search_tags']);
183 static::assertCount(0, $data['tags']);
184
185 static::assertArrayHasKey('loggedin', $param);
186
187 return $data;
188 })
189 ;
190
191 $result = $this->controller->cloud($request, $response);
192
193 static::assertSame(200, $result->getStatusCode());
194 static::assertSame('tag.cloud', (string) $result->getBody());
195 static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']);
196
197 static::assertSame('', $assignedVariables['search_tags']);
198 static::assertCount(0, $assignedVariables['tags']);
199 }
200
201 /**
202 * Tag List - Default sort is by usage DESC
203 */
204 public function testValidListControllerInvokeDefault(): void
205 {
206 $allTags = [
207 'def' => 12,
208 'abc' => 3,
209 'ghi' => 1,
210 ];
211
212 $request = $this->createMock(Request::class);
213 $response = new Response();
214
215 // Save RainTPL assigned variables
216 $assignedVariables = [];
217 $this->assignTemplateVars($assignedVariables);
218
219 $this->container->bookmarkService
220 ->expects(static::once())
221 ->method('bookmarksCountPerTag')
222 ->with([], null)
223 ->willReturnCallback(function () use ($allTags): array {
224 return $allTags;
225 })
226 ;
227
228 // Make sure that PluginManager hook is triggered
229 $this->container->pluginManager
230 ->expects(static::at(0))
231 ->method('executeHooks')
232 ->willReturnCallback(function (string $hook, array $data, array $param): array {
233 static::assertSame('render_taglist', $hook);
234 static::assertSame('', $data['search_tags']);
235 static::assertCount(3, $data['tags']);
236
237 static::assertArrayHasKey('loggedin', $param);
238
239 return $data;
240 })
241 ;
242
243 $result = $this->controller->list($request, $response);
244
245 static::assertSame(200, $result->getStatusCode());
246 static::assertSame('tag.list', (string) $result->getBody());
247 static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
248
249 static::assertSame('', $assignedVariables['search_tags']);
250 static::assertCount(3, $assignedVariables['tags']);
251
252 foreach ($allTags as $tag => $count) {
253 static::assertSame($count, $assignedVariables['tags'][$tag]);
254 }
255 }
256
257 /**
258 * Tag List - Additional parameters:
259 * - logged in
260 * - visibility private
261 * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
262 * - sort alphabetically
263 */
264 public function testValidListControllerInvokeWithParameters(): void
265 {
266 $request = $this->createMock(Request::class);
267 $request
268 ->method('getQueryParam')
269 ->with()
270 ->willReturnCallback(function (string $key): ?string {
271 if ('searchtags' === $key) {
272 return 'ghi def';
273 } elseif ('sort' === $key) {
274 return 'alpha';
275 }
276
277 return null;
278 })
279 ;
280 $response = new Response();
281
282 // Save RainTPL assigned variables
283 $assignedVariables = [];
284 $this->assignTemplateVars($assignedVariables);
285
286 $this->container->loginManager->method('isLoggedin')->willReturn(true);
287 $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private');
288
289 $this->container->bookmarkService
290 ->expects(static::once())
291 ->method('bookmarksCountPerTag')
292 ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE)
293 ->willReturnCallback(function (): array {
294 return ['abc' => 3];
295 })
296 ;
297
298 // Make sure that PluginManager hook is triggered
299 $this->container->pluginManager
300 ->expects(static::at(0))
301 ->method('executeHooks')
302 ->willReturnCallback(function (string $hook, array $data, array $param): array {
303 static::assertSame('render_taglist', $hook);
304 static::assertSame('ghi def', $data['search_tags']);
305 static::assertCount(1, $data['tags']);
306
307 static::assertArrayHasKey('loggedin', $param);
308
309 return $data;
310 })
311 ;
312
313 $result = $this->controller->list($request, $response);
314
315 static::assertSame(200, $result->getStatusCode());
316 static::assertSame('tag.list', (string) $result->getBody());
317 static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']);
318
319 static::assertSame('ghi def', $assignedVariables['search_tags']);
320 static::assertCount(1, $assignedVariables['tags']);
321 static::assertSame(3, $assignedVariables['tags']['abc']);
322 }
323
324 /**
325 * Tag List - empty
326 */
327 public function testEmptyList(): void
328 {
329 $request = $this->createMock(Request::class);
330 $response = new Response();
331
332 // Save RainTPL assigned variables
333 $assignedVariables = [];
334 $this->assignTemplateVars($assignedVariables);
335
336 $this->container->bookmarkService
337 ->expects(static::once())
338 ->method('bookmarksCountPerTag')
339 ->with([], null)
340 ->willReturnCallback(function (array $parameters, ?string $visibility): array {
341 return [];
342 })
343 ;
344
345 // Make sure that PluginManager hook is triggered
346 $this->container->pluginManager
347 ->expects(static::at(0))
348 ->method('executeHooks')
349 ->willReturnCallback(function (string $hook, array $data, array $param): array {
350 static::assertSame('render_taglist', $hook);
351 static::assertSame('', $data['search_tags']);
352 static::assertCount(0, $data['tags']);
353
354 static::assertArrayHasKey('loggedin', $param);
355
356 return $data;
357 })
358 ;
359
360 $result = $this->controller->list($request, $response);
361
362 static::assertSame(200, $result->getStatusCode());
363 static::assertSame('tag.list', (string) $result->getBody());
364 static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
365
366 static::assertSame('', $assignedVariables['search_tags']);
367 static::assertCount(0, $assignedVariables['tags']);
368 }
369}
diff --git a/tests/front/controller/visitor/TagControllerTest.php b/tests/front/controller/visitor/TagControllerTest.php
new file mode 100644
index 00000000..43076086
--- /dev/null
+++ b/tests/front/controller/visitor/TagControllerTest.php
@@ -0,0 +1,215 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class TagControllerTest extends TestCase
12{
13 use FrontControllerMockHelper;
14
15 /** @var TagController */ protected $controller;
16
17 public function setUp(): void
18 {
19 $this->createContainer();
20
21 $this->controller = new TagController($this->container);
22 }
23
24 public function testAddTagWithReferer(): void
25 {
26 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/'];
27
28 $request = $this->createMock(Request::class);
29 $response = new Response();
30
31 $tags = ['newTag' => 'abc'];
32
33 $result = $this->controller->addTag($request, $response, $tags);
34
35 static::assertInstanceOf(Response::class, $result);
36 static::assertSame(302, $result->getStatusCode());
37 static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location'));
38 }
39
40 public function testAddTagWithRefererAndExistingSearch(): void
41 {
42 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
43
44 $request = $this->createMock(Request::class);
45 $response = new Response();
46
47 $tags = ['newTag' => 'abc'];
48
49 $result = $this->controller->addTag($request, $response, $tags);
50
51 static::assertInstanceOf(Response::class, $result);
52 static::assertSame(302, $result->getStatusCode());
53 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
54 }
55
56 public function testAddTagWithoutRefererAndExistingSearch(): void
57 {
58 $request = $this->createMock(Request::class);
59 $response = new Response();
60
61 $tags = ['newTag' => 'abc'];
62
63 $result = $this->controller->addTag($request, $response, $tags);
64
65 static::assertInstanceOf(Response::class, $result);
66 static::assertSame(302, $result->getStatusCode());
67 static::assertSame(['/subfolder/?searchtags=abc'], $result->getHeader('location'));
68 }
69
70 public function testAddTagRemoveLegacyQueryParam(): void
71 {
72 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&addtag=abc'];
73
74 $request = $this->createMock(Request::class);
75 $response = new Response();
76
77 $tags = ['newTag' => 'abc'];
78
79 $result = $this->controller->addTag($request, $response, $tags);
80
81 static::assertInstanceOf(Response::class, $result);
82 static::assertSame(302, $result->getStatusCode());
83 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
84 }
85
86 public function testAddTagResetPagination(): void
87 {
88 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&page=12'];
89
90 $request = $this->createMock(Request::class);
91 $response = new Response();
92
93 $tags = ['newTag' => 'abc'];
94
95 $result = $this->controller->addTag($request, $response, $tags);
96
97 static::assertInstanceOf(Response::class, $result);
98 static::assertSame(302, $result->getStatusCode());
99 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
100 }
101
102 public function testAddTagWithRefererAndEmptySearch(): void
103 {
104 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags='];
105
106 $request = $this->createMock(Request::class);
107 $response = new Response();
108
109 $tags = ['newTag' => 'abc'];
110
111 $result = $this->controller->addTag($request, $response, $tags);
112
113 static::assertInstanceOf(Response::class, $result);
114 static::assertSame(302, $result->getStatusCode());
115 static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location'));
116 }
117
118 public function testAddTagWithoutNewTagWithReferer(): void
119 {
120 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
121
122 $request = $this->createMock(Request::class);
123 $response = new Response();
124
125 $result = $this->controller->addTag($request, $response, []);
126
127 static::assertInstanceOf(Response::class, $result);
128 static::assertSame(302, $result->getStatusCode());
129 static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location'));
130 }
131
132 public function testAddTagWithoutNewTagWithoutReferer(): void
133 {
134 $request = $this->createMock(Request::class);
135 $response = new Response();
136
137 $result = $this->controller->addTag($request, $response, []);
138
139 static::assertInstanceOf(Response::class, $result);
140 static::assertSame(302, $result->getStatusCode());
141 static::assertSame(['/subfolder/'], $result->getHeader('location'));
142 }
143
144 public function testRemoveTagWithoutMatchingTag(): void
145 {
146 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
147
148 $request = $this->createMock(Request::class);
149 $response = new Response();
150
151 $tags = ['tag' => 'abc'];
152
153 $result = $this->controller->removeTag($request, $response, $tags);
154
155 static::assertInstanceOf(Response::class, $result);
156 static::assertSame(302, $result->getStatusCode());
157 static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location'));
158 }
159
160 public function testRemoveTagWithoutTagsearch(): void
161 {
162 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/'];
163
164 $request = $this->createMock(Request::class);
165 $response = new Response();
166
167 $tags = ['tag' => 'abc'];
168
169 $result = $this->controller->removeTag($request, $response, $tags);
170
171 static::assertInstanceOf(Response::class, $result);
172 static::assertSame(302, $result->getStatusCode());
173 static::assertSame(['/controller/'], $result->getHeader('location'));
174 }
175
176 public function testRemoveTagWithoutReferer(): void
177 {
178 $request = $this->createMock(Request::class);
179 $response = new Response();
180
181 $tags = ['tag' => 'abc'];
182
183 $result = $this->controller->removeTag($request, $response, $tags);
184
185 static::assertInstanceOf(Response::class, $result);
186 static::assertSame(302, $result->getStatusCode());
187 static::assertSame(['/subfolder/'], $result->getHeader('location'));
188 }
189
190 public function testRemoveTagWithoutTag(): void
191 {
192 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtag=abc'];
193
194 $request = $this->createMock(Request::class);
195 $response = new Response();
196
197 $result = $this->controller->removeTag($request, $response, []);
198
199 static::assertInstanceOf(Response::class, $result);
200 static::assertSame(302, $result->getStatusCode());
201 static::assertSame(['/controller/?searchtag=abc'], $result->getHeader('location'));
202 }
203
204 public function testRemoveTagWithoutTagWithoutReferer(): void
205 {
206 $request = $this->createMock(Request::class);
207 $response = new Response();
208
209 $result = $this->controller->removeTag($request, $response, []);
210
211 static::assertInstanceOf(Response::class, $result);
212 static::assertSame(302, $result->getStatusCode());
213 static::assertSame(['/subfolder/'], $result->getHeader('location'));
214 }
215}
diff --git a/tests/http/HttpUtils/IndexUrlTest.php b/tests/http/HttpUtils/IndexUrlTest.php
index bcbe59cb..73d33cd4 100644
--- a/tests/http/HttpUtils/IndexUrlTest.php
+++ b/tests/http/HttpUtils/IndexUrlTest.php
@@ -71,4 +71,36 @@ class IndexUrlTest extends \PHPUnit\Framework\TestCase
71 ) 71 )
72 ); 72 );
73 } 73 }
74
75 /**
76 * The route is stored in REQUEST_URI
77 */
78 public function testPageUrlWithRoute()
79 {
80 $this->assertEquals(
81 'http://host.tld/picture-wall',
82 page_url(
83 array(
84 'HTTPS' => 'Off',
85 'SERVER_NAME' => 'host.tld',
86 'SERVER_PORT' => '80',
87 'SCRIPT_NAME' => '/index.php',
88 'REQUEST_URI' => '/picture-wall',
89 )
90 )
91 );
92
93 $this->assertEquals(
94 'http://host.tld/admin/picture-wall',
95 page_url(
96 array(
97 'HTTPS' => 'Off',
98 'SERVER_NAME' => 'host.tld',
99 'SERVER_PORT' => '80',
100 'SCRIPT_NAME' => '/admin/index.php',
101 'REQUEST_URI' => '/admin/picture-wall',
102 )
103 )
104 );
105 }
74} 106}
diff --git a/tests/legacy/LegacyControllerTest.php b/tests/legacy/LegacyControllerTest.php
new file mode 100644
index 00000000..759a5b2a
--- /dev/null
+++ b/tests/legacy/LegacyControllerTest.php
@@ -0,0 +1,99 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Legacy;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class LegacyControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var LegacyController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new LegacyController($this->container);
24 }
25
26 /**
27 * @dataProvider getProcessProvider
28 */
29 public function testProcess(string $legacyRoute, array $queryParameters, string $slimRoute, bool $isLoggedIn): void
30 {
31 $request = $this->createMock(Request::class);
32 $request->method('getQueryParams')->willReturn($queryParameters);
33 $request
34 ->method('getParam')
35 ->willReturnCallback(function (string $key) use ($queryParameters): ?string {
36 return $queryParameters[$key] ?? null;
37 })
38 ;
39 $response = new Response();
40
41 $this->container->loginManager->method('isLoggedIn')->willReturn($isLoggedIn);
42
43 $result = $this->controller->process($request, $response, $legacyRoute);
44
45 static::assertSame('/subfolder' . $slimRoute, $result->getHeader('location')[0]);
46 }
47
48 public function testProcessNotFound(): void
49 {
50 $request = $this->createMock(Request::class);
51 $response = new Response();
52
53 $this->expectException(UnknowLegacyRouteException::class);
54
55 $this->controller->process($request, $response, 'nope');
56 }
57
58 /**
59 * @return array[] Parameters:
60 * - string legacyRoute
61 * - array queryParameters
62 * - string slimRoute
63 * - bool isLoggedIn
64 */
65 public function getProcessProvider(): array
66 {
67 return [
68 ['post', [], '/admin/shaare', true],
69 ['post', [], '/login', false],
70 ['post', ['title' => 'test'], '/admin/shaare?title=test', true],
71 ['post', ['title' => 'test'], '/login?title=test', false],
72 ['addlink', [], '/admin/add-shaare', true],
73 ['addlink', [], '/login', false],
74 ['login', [], '/login', true],
75 ['login', [], '/login', false],
76 ['logout', [], '/admin/logout', true],
77 ['logout', [], '/admin/logout', false],
78 ['picwall', [], '/picture-wall', false],
79 ['picwall', [], '/picture-wall', true],
80 ['tagcloud', [], '/tags/cloud', false],
81 ['tagcloud', [], '/tags/cloud', true],
82 ['taglist', [], '/tags/list', false],
83 ['taglist', [], '/tags/list', true],
84 ['daily', [], '/daily', false],
85 ['daily', [], '/daily', true],
86 ['daily', ['day' => '123456789', 'discard' => '1'], '/daily?day=123456789', false],
87 ['rss', [], '/feed/rss', false],
88 ['rss', [], '/feed/rss', true],
89 ['rss', ['search' => 'filter123', 'other' => 'param'], '/feed/rss?search=filter123&other=param', false],
90 ['atom', [], '/feed/atom', false],
91 ['atom', [], '/feed/atom', true],
92 ['atom', ['search' => 'filter123', 'other' => 'param'], '/feed/atom?search=filter123&other=param', false],
93 ['opensearch', [], '/open-search', false],
94 ['opensearch', [], '/open-search', true],
95 ['dailyrss', [], '/daily-rss', false],
96 ['dailyrss', [], '/daily-rss', true],
97 ];
98 }
99}
diff --git a/tests/legacy/LegacyLinkDBTest.php b/tests/legacy/LegacyLinkDBTest.php
index 17b2b0e6..0884ad03 100644
--- a/tests/legacy/LegacyLinkDBTest.php
+++ b/tests/legacy/LegacyLinkDBTest.php
@@ -11,7 +11,6 @@ use ReflectionClass;
11use Shaarli; 11use Shaarli;
12use Shaarli\Bookmark\Bookmark; 12use Shaarli\Bookmark\Bookmark;
13 13
14require_once 'application/feed/Cache.php';
15require_once 'application/Utils.php'; 14require_once 'application/Utils.php';
16require_once 'tests/utils/ReferenceLinkDB.php'; 15require_once 'tests/utils/ReferenceLinkDB.php';
17 16
diff --git a/tests/RouterTest.php b/tests/legacy/LegacyRouterTest.php
index 0cd49bb8..c2019ca7 100644
--- a/tests/RouterTest.php
+++ b/tests/legacy/LegacyRouterTest.php
@@ -1,10 +1,13 @@
1<?php 1<?php
2namespace Shaarli; 2
3namespace Shaarli\Legacy;
4
5use PHPUnit\Framework\TestCase;
3 6
4/** 7/**
5 * Unit tests for Router 8 * Unit tests for Router
6 */ 9 */
7class RouterTest extends \PHPUnit\Framework\TestCase 10class LegacyRouterTest extends TestCase
8{ 11{
9 /** 12 /**
10 * Test findPage: login page output. 13 * Test findPage: login page output.
@@ -15,18 +18,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
15 public function testFindPageLoginValid() 18 public function testFindPageLoginValid()
16 { 19 {
17 $this->assertEquals( 20 $this->assertEquals(
18 Router::$PAGE_LOGIN, 21 LegacyRouter::$PAGE_LOGIN,
19 Router::findPage('do=login', array(), false) 22 LegacyRouter::findPage('do=login', array(), false)
20 ); 23 );
21 24
22 $this->assertEquals( 25 $this->assertEquals(
23 Router::$PAGE_LOGIN, 26 LegacyRouter::$PAGE_LOGIN,
24 Router::findPage('do=login', array(), 1) 27 LegacyRouter::findPage('do=login', array(), 1)
25 ); 28 );
26 29
27 $this->assertEquals( 30 $this->assertEquals(
28 Router::$PAGE_LOGIN, 31 LegacyRouter::$PAGE_LOGIN,
29 Router::findPage('do=login&stuff', array(), false) 32 LegacyRouter::findPage('do=login&stuff', array(), false)
30 ); 33 );
31 } 34 }
32 35
@@ -39,13 +42,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
39 public function testFindPageLoginInvalid() 42 public function testFindPageLoginInvalid()
40 { 43 {
41 $this->assertNotEquals( 44 $this->assertNotEquals(
42 Router::$PAGE_LOGIN, 45 LegacyRouter::$PAGE_LOGIN,
43 Router::findPage('do=login', array(), true) 46 LegacyRouter::findPage('do=login', array(), true)
44 ); 47 );
45 48
46 $this->assertNotEquals( 49 $this->assertNotEquals(
47 Router::$PAGE_LOGIN, 50 LegacyRouter::$PAGE_LOGIN,
48 Router::findPage('do=other', array(), false) 51 LegacyRouter::findPage('do=other', array(), false)
49 ); 52 );
50 } 53 }
51 54
@@ -58,13 +61,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
58 public function testFindPagePicwallValid() 61 public function testFindPagePicwallValid()
59 { 62 {
60 $this->assertEquals( 63 $this->assertEquals(
61 Router::$PAGE_PICWALL, 64 LegacyRouter::$PAGE_PICWALL,
62 Router::findPage('do=picwall', array(), false) 65 LegacyRouter::findPage('do=picwall', array(), false)
63 ); 66 );
64 67
65 $this->assertEquals( 68 $this->assertEquals(
66 Router::$PAGE_PICWALL, 69 LegacyRouter::$PAGE_PICWALL,
67 Router::findPage('do=picwall', array(), true) 70 LegacyRouter::findPage('do=picwall', array(), true)
68 ); 71 );
69 } 72 }
70 73
@@ -77,13 +80,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
77 public function testFindPagePicwallInvalid() 80 public function testFindPagePicwallInvalid()
78 { 81 {
79 $this->assertEquals( 82 $this->assertEquals(
80 Router::$PAGE_PICWALL, 83 LegacyRouter::$PAGE_PICWALL,
81 Router::findPage('do=picwall&stuff', array(), false) 84 LegacyRouter::findPage('do=picwall&stuff', array(), false)
82 ); 85 );
83 86
84 $this->assertNotEquals( 87 $this->assertNotEquals(
85 Router::$PAGE_PICWALL, 88 LegacyRouter::$PAGE_PICWALL,
86 Router::findPage('do=other', array(), false) 89 LegacyRouter::findPage('do=other', array(), false)
87 ); 90 );
88 } 91 }
89 92
@@ -96,18 +99,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
96 public function testFindPageTagcloudValid() 99 public function testFindPageTagcloudValid()
97 { 100 {
98 $this->assertEquals( 101 $this->assertEquals(
99 Router::$PAGE_TAGCLOUD, 102 LegacyRouter::$PAGE_TAGCLOUD,
100 Router::findPage('do=tagcloud', array(), false) 103 LegacyRouter::findPage('do=tagcloud', array(), false)
101 ); 104 );
102 105
103 $this->assertEquals( 106 $this->assertEquals(
104 Router::$PAGE_TAGCLOUD, 107 LegacyRouter::$PAGE_TAGCLOUD,
105 Router::findPage('do=tagcloud', array(), true) 108 LegacyRouter::findPage('do=tagcloud', array(), true)
106 ); 109 );
107 110
108 $this->assertEquals( 111 $this->assertEquals(
109 Router::$PAGE_TAGCLOUD, 112 LegacyRouter::$PAGE_TAGCLOUD,
110 Router::findPage('do=tagcloud&stuff', array(), false) 113 LegacyRouter::findPage('do=tagcloud&stuff', array(), false)
111 ); 114 );
112 } 115 }
113 116
@@ -120,8 +123,8 @@ class RouterTest extends \PHPUnit\Framework\TestCase
120 public function testFindPageTagcloudInvalid() 123 public function testFindPageTagcloudInvalid()
121 { 124 {
122 $this->assertNotEquals( 125 $this->assertNotEquals(
123 Router::$PAGE_TAGCLOUD, 126 LegacyRouter::$PAGE_TAGCLOUD,
124 Router::findPage('do=other', array(), false) 127 LegacyRouter::findPage('do=other', array(), false)
125 ); 128 );
126 } 129 }
127 130
@@ -134,23 +137,23 @@ class RouterTest extends \PHPUnit\Framework\TestCase
134 public function testFindPageLinklistValid() 137 public function testFindPageLinklistValid()
135 { 138 {
136 $this->assertEquals( 139 $this->assertEquals(
137 Router::$PAGE_LINKLIST, 140 LegacyRouter::$PAGE_LINKLIST,
138 Router::findPage('', array(), true) 141 LegacyRouter::findPage('', array(), true)
139 ); 142 );
140 143
141 $this->assertEquals( 144 $this->assertEquals(
142 Router::$PAGE_LINKLIST, 145 LegacyRouter::$PAGE_LINKLIST,
143 Router::findPage('whatever', array(), true) 146 LegacyRouter::findPage('whatever', array(), true)
144 ); 147 );
145 148
146 $this->assertEquals( 149 $this->assertEquals(
147 Router::$PAGE_LINKLIST, 150 LegacyRouter::$PAGE_LINKLIST,
148 Router::findPage('whatever', array(), false) 151 LegacyRouter::findPage('whatever', array(), false)
149 ); 152 );
150 153
151 $this->assertEquals( 154 $this->assertEquals(
152 Router::$PAGE_LINKLIST, 155 LegacyRouter::$PAGE_LINKLIST,
153 Router::findPage('do=tools', array(), false) 156 LegacyRouter::findPage('do=tools', array(), false)
154 ); 157 );
155 } 158 }
156 159
@@ -163,13 +166,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
163 public function testFindPageToolsValid() 166 public function testFindPageToolsValid()
164 { 167 {
165 $this->assertEquals( 168 $this->assertEquals(
166 Router::$PAGE_TOOLS, 169 LegacyRouter::$PAGE_TOOLS,
167 Router::findPage('do=tools', array(), true) 170 LegacyRouter::findPage('do=tools', array(), true)
168 ); 171 );
169 172
170 $this->assertEquals( 173 $this->assertEquals(
171 Router::$PAGE_TOOLS, 174 LegacyRouter::$PAGE_TOOLS,
172 Router::findPage('do=tools&stuff', array(), true) 175 LegacyRouter::findPage('do=tools&stuff', array(), true)
173 ); 176 );
174 } 177 }
175 178
@@ -182,18 +185,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
182 public function testFindPageToolsInvalid() 185 public function testFindPageToolsInvalid()
183 { 186 {
184 $this->assertNotEquals( 187 $this->assertNotEquals(
185 Router::$PAGE_TOOLS, 188 LegacyRouter::$PAGE_TOOLS,
186 Router::findPage('do=tools', array(), 1) 189 LegacyRouter::findPage('do=tools', array(), 1)
187 ); 190 );
188 191
189 $this->assertNotEquals( 192 $this->assertNotEquals(
190 Router::$PAGE_TOOLS, 193 LegacyRouter::$PAGE_TOOLS,
191 Router::findPage('do=tools', array(), false) 194 LegacyRouter::findPage('do=tools', array(), false)
192 ); 195 );
193 196
194 $this->assertNotEquals( 197 $this->assertNotEquals(
195 Router::$PAGE_TOOLS, 198 LegacyRouter::$PAGE_TOOLS,
196 Router::findPage('do=other', array(), true) 199 LegacyRouter::findPage('do=other', array(), true)
197 ); 200 );
198 } 201 }
199 202
@@ -206,12 +209,12 @@ class RouterTest extends \PHPUnit\Framework\TestCase
206 public function testFindPageChangepasswdValid() 209 public function testFindPageChangepasswdValid()
207 { 210 {
208 $this->assertEquals( 211 $this->assertEquals(
209 Router::$PAGE_CHANGEPASSWORD, 212 LegacyRouter::$PAGE_CHANGEPASSWORD,
210 Router::findPage('do=changepasswd', array(), true) 213 LegacyRouter::findPage('do=changepasswd', array(), true)
211 ); 214 );
212 $this->assertEquals( 215 $this->assertEquals(
213 Router::$PAGE_CHANGEPASSWORD, 216 LegacyRouter::$PAGE_CHANGEPASSWORD,
214 Router::findPage('do=changepasswd&stuff', array(), true) 217 LegacyRouter::findPage('do=changepasswd&stuff', array(), true)
215 ); 218 );
216 } 219 }
217 220
@@ -224,18 +227,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
224 public function testFindPageChangepasswdInvalid() 227 public function testFindPageChangepasswdInvalid()
225 { 228 {
226 $this->assertNotEquals( 229 $this->assertNotEquals(
227 Router::$PAGE_CHANGEPASSWORD, 230 LegacyRouter::$PAGE_CHANGEPASSWORD,
228 Router::findPage('do=changepasswd', array(), 1) 231 LegacyRouter::findPage('do=changepasswd', array(), 1)
229 ); 232 );
230 233
231 $this->assertNotEquals( 234 $this->assertNotEquals(
232 Router::$PAGE_CHANGEPASSWORD, 235 LegacyRouter::$PAGE_CHANGEPASSWORD,
233 Router::findPage('do=changepasswd', array(), false) 236 LegacyRouter::findPage('do=changepasswd', array(), false)
234 ); 237 );
235 238
236 $this->assertNotEquals( 239 $this->assertNotEquals(
237 Router::$PAGE_CHANGEPASSWORD, 240 LegacyRouter::$PAGE_CHANGEPASSWORD,
238 Router::findPage('do=other', array(), true) 241 LegacyRouter::findPage('do=other', array(), true)
239 ); 242 );
240 } 243 }
241 /** 244 /**
@@ -247,13 +250,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
247 public function testFindPageConfigureValid() 250 public function testFindPageConfigureValid()
248 { 251 {
249 $this->assertEquals( 252 $this->assertEquals(
250 Router::$PAGE_CONFIGURE, 253 LegacyRouter::$PAGE_CONFIGURE,
251 Router::findPage('do=configure', array(), true) 254 LegacyRouter::findPage('do=configure', array(), true)
252 ); 255 );
253 256
254 $this->assertEquals( 257 $this->assertEquals(
255 Router::$PAGE_CONFIGURE, 258 LegacyRouter::$PAGE_CONFIGURE,
256 Router::findPage('do=configure&stuff', array(), true) 259 LegacyRouter::findPage('do=configure&stuff', array(), true)
257 ); 260 );
258 } 261 }
259 262
@@ -266,18 +269,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
266 public function testFindPageConfigureInvalid() 269 public function testFindPageConfigureInvalid()
267 { 270 {
268 $this->assertNotEquals( 271 $this->assertNotEquals(
269 Router::$PAGE_CONFIGURE, 272 LegacyRouter::$PAGE_CONFIGURE,
270 Router::findPage('do=configure', array(), 1) 273 LegacyRouter::findPage('do=configure', array(), 1)
271 ); 274 );
272 275
273 $this->assertNotEquals( 276 $this->assertNotEquals(
274 Router::$PAGE_CONFIGURE, 277 LegacyRouter::$PAGE_CONFIGURE,
275 Router::findPage('do=configure', array(), false) 278 LegacyRouter::findPage('do=configure', array(), false)
276 ); 279 );
277 280
278 $this->assertNotEquals( 281 $this->assertNotEquals(
279 Router::$PAGE_CONFIGURE, 282 LegacyRouter::$PAGE_CONFIGURE,
280 Router::findPage('do=other', array(), true) 283 LegacyRouter::findPage('do=other', array(), true)
281 ); 284 );
282 } 285 }
283 286
@@ -290,13 +293,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
290 public function testFindPageChangetagValid() 293 public function testFindPageChangetagValid()
291 { 294 {
292 $this->assertEquals( 295 $this->assertEquals(
293 Router::$PAGE_CHANGETAG, 296 LegacyRouter::$PAGE_CHANGETAG,
294 Router::findPage('do=changetag', array(), true) 297 LegacyRouter::findPage('do=changetag', array(), true)
295 ); 298 );
296 299
297 $this->assertEquals( 300 $this->assertEquals(
298 Router::$PAGE_CHANGETAG, 301 LegacyRouter::$PAGE_CHANGETAG,
299 Router::findPage('do=changetag&stuff', array(), true) 302 LegacyRouter::findPage('do=changetag&stuff', array(), true)
300 ); 303 );
301 } 304 }
302 305
@@ -309,18 +312,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
309 public function testFindPageChangetagInvalid() 312 public function testFindPageChangetagInvalid()
310 { 313 {
311 $this->assertNotEquals( 314 $this->assertNotEquals(
312 Router::$PAGE_CHANGETAG, 315 LegacyRouter::$PAGE_CHANGETAG,
313 Router::findPage('do=changetag', array(), 1) 316 LegacyRouter::findPage('do=changetag', array(), 1)
314 ); 317 );
315 318
316 $this->assertNotEquals( 319 $this->assertNotEquals(
317 Router::$PAGE_CHANGETAG, 320 LegacyRouter::$PAGE_CHANGETAG,
318 Router::findPage('do=changetag', array(), false) 321 LegacyRouter::findPage('do=changetag', array(), false)
319 ); 322 );
320 323
321 $this->assertNotEquals( 324 $this->assertNotEquals(
322 Router::$PAGE_CHANGETAG, 325 LegacyRouter::$PAGE_CHANGETAG,
323 Router::findPage('do=other', array(), true) 326 LegacyRouter::findPage('do=other', array(), true)
324 ); 327 );
325 } 328 }
326 329
@@ -333,13 +336,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
333 public function testFindPageAddlinkValid() 336 public function testFindPageAddlinkValid()
334 { 337 {
335 $this->assertEquals( 338 $this->assertEquals(
336 Router::$PAGE_ADDLINK, 339 LegacyRouter::$PAGE_ADDLINK,
337 Router::findPage('do=addlink', array(), true) 340 LegacyRouter::findPage('do=addlink', array(), true)
338 ); 341 );
339 342
340 $this->assertEquals( 343 $this->assertEquals(
341 Router::$PAGE_ADDLINK, 344 LegacyRouter::$PAGE_ADDLINK,
342 Router::findPage('do=addlink&stuff', array(), true) 345 LegacyRouter::findPage('do=addlink&stuff', array(), true)
343 ); 346 );
344 } 347 }
345 348
@@ -352,18 +355,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
352 public function testFindPageAddlinkInvalid() 355 public function testFindPageAddlinkInvalid()
353 { 356 {
354 $this->assertNotEquals( 357 $this->assertNotEquals(
355 Router::$PAGE_ADDLINK, 358 LegacyRouter::$PAGE_ADDLINK,
356 Router::findPage('do=addlink', array(), 1) 359 LegacyRouter::findPage('do=addlink', array(), 1)
357 ); 360 );
358 361
359 $this->assertNotEquals( 362 $this->assertNotEquals(
360 Router::$PAGE_ADDLINK, 363 LegacyRouter::$PAGE_ADDLINK,
361 Router::findPage('do=addlink', array(), false) 364 LegacyRouter::findPage('do=addlink', array(), false)
362 ); 365 );
363 366
364 $this->assertNotEquals( 367 $this->assertNotEquals(
365 Router::$PAGE_ADDLINK, 368 LegacyRouter::$PAGE_ADDLINK,
366 Router::findPage('do=other', array(), true) 369 LegacyRouter::findPage('do=other', array(), true)
367 ); 370 );
368 } 371 }
369 372
@@ -376,13 +379,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
376 public function testFindPageExportValid() 379 public function testFindPageExportValid()
377 { 380 {
378 $this->assertEquals( 381 $this->assertEquals(
379 Router::$PAGE_EXPORT, 382 LegacyRouter::$PAGE_EXPORT,
380 Router::findPage('do=export', array(), true) 383 LegacyRouter::findPage('do=export', array(), true)
381 ); 384 );
382 385
383 $this->assertEquals( 386 $this->assertEquals(
384 Router::$PAGE_EXPORT, 387 LegacyRouter::$PAGE_EXPORT,
385 Router::findPage('do=export&stuff', array(), true) 388 LegacyRouter::findPage('do=export&stuff', array(), true)
386 ); 389 );
387 } 390 }
388 391
@@ -395,18 +398,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
395 public function testFindPageExportInvalid() 398 public function testFindPageExportInvalid()
396 { 399 {
397 $this->assertNotEquals( 400 $this->assertNotEquals(
398 Router::$PAGE_EXPORT, 401 LegacyRouter::$PAGE_EXPORT,
399 Router::findPage('do=export', array(), 1) 402 LegacyRouter::findPage('do=export', array(), 1)
400 ); 403 );
401 404
402 $this->assertNotEquals( 405 $this->assertNotEquals(
403 Router::$PAGE_EXPORT, 406 LegacyRouter::$PAGE_EXPORT,
404 Router::findPage('do=export', array(), false) 407 LegacyRouter::findPage('do=export', array(), false)
405 ); 408 );
406 409
407 $this->assertNotEquals( 410 $this->assertNotEquals(
408 Router::$PAGE_EXPORT, 411 LegacyRouter::$PAGE_EXPORT,
409 Router::findPage('do=other', array(), true) 412 LegacyRouter::findPage('do=other', array(), true)
410 ); 413 );
411 } 414 }
412 415
@@ -419,13 +422,13 @@ class RouterTest extends \PHPUnit\Framework\TestCase
419 public function testFindPageImportValid() 422 public function testFindPageImportValid()
420 { 423 {
421 $this->assertEquals( 424 $this->assertEquals(
422 Router::$PAGE_IMPORT, 425 LegacyRouter::$PAGE_IMPORT,
423 Router::findPage('do=import', array(), true) 426 LegacyRouter::findPage('do=import', array(), true)
424 ); 427 );
425 428
426 $this->assertEquals( 429 $this->assertEquals(
427 Router::$PAGE_IMPORT, 430 LegacyRouter::$PAGE_IMPORT,
428 Router::findPage('do=import&stuff', array(), true) 431 LegacyRouter::findPage('do=import&stuff', array(), true)
429 ); 432 );
430 } 433 }
431 434
@@ -438,18 +441,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
438 public function testFindPageImportInvalid() 441 public function testFindPageImportInvalid()
439 { 442 {
440 $this->assertNotEquals( 443 $this->assertNotEquals(
441 Router::$PAGE_IMPORT, 444 LegacyRouter::$PAGE_IMPORT,
442 Router::findPage('do=import', array(), 1) 445 LegacyRouter::findPage('do=import', array(), 1)
443 ); 446 );
444 447
445 $this->assertNotEquals( 448 $this->assertNotEquals(
446 Router::$PAGE_IMPORT, 449 LegacyRouter::$PAGE_IMPORT,
447 Router::findPage('do=import', array(), false) 450 LegacyRouter::findPage('do=import', array(), false)
448 ); 451 );
449 452
450 $this->assertNotEquals( 453 $this->assertNotEquals(
451 Router::$PAGE_IMPORT, 454 LegacyRouter::$PAGE_IMPORT,
452 Router::findPage('do=other', array(), true) 455 LegacyRouter::findPage('do=other', array(), true)
453 ); 456 );
454 } 457 }
455 458
@@ -462,24 +465,24 @@ class RouterTest extends \PHPUnit\Framework\TestCase
462 public function testFindPageEditlinkValid() 465 public function testFindPageEditlinkValid()
463 { 466 {
464 $this->assertEquals( 467 $this->assertEquals(
465 Router::$PAGE_EDITLINK, 468 LegacyRouter::$PAGE_EDITLINK,
466 Router::findPage('whatever', array('edit_link' => 1), true) 469 LegacyRouter::findPage('whatever', array('edit_link' => 1), true)
467 ); 470 );
468 471
469 $this->assertEquals( 472 $this->assertEquals(
470 Router::$PAGE_EDITLINK, 473 LegacyRouter::$PAGE_EDITLINK,
471 Router::findPage('', array('edit_link' => 1), true) 474 LegacyRouter::findPage('', array('edit_link' => 1), true)
472 ); 475 );
473 476
474 477
475 $this->assertEquals( 478 $this->assertEquals(
476 Router::$PAGE_EDITLINK, 479 LegacyRouter::$PAGE_EDITLINK,
477 Router::findPage('whatever', array('post' => 1), true) 480 LegacyRouter::findPage('whatever', array('post' => 1), true)
478 ); 481 );
479 482
480 $this->assertEquals( 483 $this->assertEquals(
481 Router::$PAGE_EDITLINK, 484 LegacyRouter::$PAGE_EDITLINK,
482 Router::findPage('whatever', array('post' => 1, 'edit_link' => 1), true) 485 LegacyRouter::findPage('whatever', array('post' => 1, 'edit_link' => 1), true)
483 ); 486 );
484 } 487 }
485 488
@@ -492,18 +495,18 @@ class RouterTest extends \PHPUnit\Framework\TestCase
492 public function testFindPageEditlinkInvalid() 495 public function testFindPageEditlinkInvalid()
493 { 496 {
494 $this->assertNotEquals( 497 $this->assertNotEquals(
495 Router::$PAGE_EDITLINK, 498 LegacyRouter::$PAGE_EDITLINK,
496 Router::findPage('whatever', array('edit_link' => 1), false) 499 LegacyRouter::findPage('whatever', array('edit_link' => 1), false)
497 ); 500 );
498 501
499 $this->assertNotEquals( 502 $this->assertNotEquals(
500 Router::$PAGE_EDITLINK, 503 LegacyRouter::$PAGE_EDITLINK,
501 Router::findPage('whatever', array('edit_link' => 1), 1) 504 LegacyRouter::findPage('whatever', array('edit_link' => 1), 1)
502 ); 505 );
503 506
504 $this->assertNotEquals( 507 $this->assertNotEquals(
505 Router::$PAGE_EDITLINK, 508 LegacyRouter::$PAGE_EDITLINK,
506 Router::findPage('whatever', array(), true) 509 LegacyRouter::findPage('whatever', array(), true)
507 ); 510 );
508 } 511 }
509} 512}
diff --git a/tests/netscape/BookmarkExportTest.php b/tests/netscape/BookmarkExportTest.php
index 6c948bba..509da51d 100644
--- a/tests/netscape/BookmarkExportTest.php
+++ b/tests/netscape/BookmarkExportTest.php
@@ -1,11 +1,12 @@
1<?php 1<?php
2
2namespace Shaarli\Netscape; 3namespace Shaarli\Netscape;
3 4
5use PHPUnit\Framework\TestCase;
4use Shaarli\Bookmark\BookmarkFileService; 6use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Bookmark\LinkDB;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
7use Shaarli\Formatter\FormatterFactory;
8use Shaarli\Formatter\BookmarkFormatter; 8use Shaarli\Formatter\BookmarkFormatter;
9use Shaarli\Formatter\FormatterFactory;
9use Shaarli\History; 10use Shaarli\History;
10 11
11require_once 'tests/utils/ReferenceLinkDB.php'; 12require_once 'tests/utils/ReferenceLinkDB.php';
@@ -13,7 +14,7 @@ require_once 'tests/utils/ReferenceLinkDB.php';
13/** 14/**
14 * Netscape bookmark export 15 * Netscape bookmark export
15 */ 16 */
16class BookmarkExportTest extends \PHPUnit\Framework\TestCase 17class BookmarkExportTest extends TestCase
17{ 18{
18 /** 19 /**
19 * @var string datastore to test write operations 20 * @var string datastore to test write operations
@@ -21,6 +22,11 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
21 protected static $testDatastore = 'sandbox/datastore.php'; 22 protected static $testDatastore = 'sandbox/datastore.php';
22 23
23 /** 24 /**
25 * @var ConfigManager instance.
26 */
27 protected static $conf;
28
29 /**
24 * @var \ReferenceLinkDB instance. 30 * @var \ReferenceLinkDB instance.
25 */ 31 */
26 protected static $refDb = null; 32 protected static $refDb = null;
@@ -36,18 +42,37 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
36 protected static $formatter; 42 protected static $formatter;
37 43
38 /** 44 /**
45 * @var History instance
46 */
47 protected static $history;
48
49 /**
50 * @var NetscapeBookmarkUtils
51 */
52 protected $netscapeBookmarkUtils;
53
54 /**
39 * Instantiate reference data 55 * Instantiate reference data
40 */ 56 */
41 public static function setUpBeforeClass() 57 public static function setUpBeforeClass()
42 { 58 {
43 $conf = new ConfigManager('tests/utils/config/configJson'); 59 static::$conf = new ConfigManager('tests/utils/config/configJson');
44 $conf->set('resource.datastore', self::$testDatastore); 60 static::$conf->set('resource.datastore', static::$testDatastore);
45 self::$refDb = new \ReferenceLinkDB(); 61 static::$refDb = new \ReferenceLinkDB();
46 self::$refDb->write(self::$testDatastore); 62 static::$refDb->write(static::$testDatastore);
47 $history = new History('sandbox/history.php'); 63 static::$history = new History('sandbox/history.php');
48 self::$bookmarkService = new BookmarkFileService($conf, $history, true); 64 static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, true);
49 $factory = new FormatterFactory($conf, true); 65 $factory = new FormatterFactory(static::$conf, true);
50 self::$formatter = $factory->getFormatter('raw'); 66 static::$formatter = $factory->getFormatter('raw');
67 }
68
69 public function setUp(): void
70 {
71 $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils(
72 static::$bookmarkService,
73 static::$conf,
74 static::$history
75 );
51 } 76 }
52 77
53 /** 78 /**
@@ -57,8 +82,7 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
57 */ 82 */
58 public function testFilterAndFormatInvalid() 83 public function testFilterAndFormatInvalid()
59 { 84 {
60 NetscapeBookmarkUtils::filterAndFormat( 85 $this->netscapeBookmarkUtils->filterAndFormat(
61 self::$bookmarkService,
62 self::$formatter, 86 self::$formatter,
63 'derp', 87 'derp',
64 false, 88 false,
@@ -71,8 +95,7 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
71 */ 95 */
72 public function testFilterAndFormatAll() 96 public function testFilterAndFormatAll()
73 { 97 {
74 $links = NetscapeBookmarkUtils::filterAndFormat( 98 $links = $this->netscapeBookmarkUtils->filterAndFormat(
75 self::$bookmarkService,
76 self::$formatter, 99 self::$formatter,
77 'all', 100 'all',
78 false, 101 false,
@@ -97,8 +120,7 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
97 */ 120 */
98 public function testFilterAndFormatPrivate() 121 public function testFilterAndFormatPrivate()
99 { 122 {
100 $links = NetscapeBookmarkUtils::filterAndFormat( 123 $links = $this->netscapeBookmarkUtils->filterAndFormat(
101 self::$bookmarkService,
102 self::$formatter, 124 self::$formatter,
103 'private', 125 'private',
104 false, 126 false,
@@ -123,8 +145,7 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
123 */ 145 */
124 public function testFilterAndFormatPublic() 146 public function testFilterAndFormatPublic()
125 { 147 {
126 $links = NetscapeBookmarkUtils::filterAndFormat( 148 $links = $this->netscapeBookmarkUtils->filterAndFormat(
127 self::$bookmarkService,
128 self::$formatter, 149 self::$formatter,
129 'public', 150 'public',
130 false, 151 false,
@@ -149,15 +170,14 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
149 */ 170 */
150 public function testFilterAndFormatDoNotPrependNoteUrl() 171 public function testFilterAndFormatDoNotPrependNoteUrl()
151 { 172 {
152 $links = NetscapeBookmarkUtils::filterAndFormat( 173 $links = $this->netscapeBookmarkUtils->filterAndFormat(
153 self::$bookmarkService,
154 self::$formatter, 174 self::$formatter,
155 'public', 175 'public',
156 false, 176 false,
157 '' 177 ''
158 ); 178 );
159 $this->assertEquals( 179 $this->assertEquals(
160 '?WDWyig', 180 '/shaare/WDWyig',
161 $links[2]['url'] 181 $links[2]['url']
162 ); 182 );
163 } 183 }
@@ -168,15 +188,14 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
168 public function testFilterAndFormatPrependNoteUrl() 188 public function testFilterAndFormatPrependNoteUrl()
169 { 189 {
170 $indexUrl = 'http://localhost:7469/shaarli/'; 190 $indexUrl = 'http://localhost:7469/shaarli/';
171 $links = NetscapeBookmarkUtils::filterAndFormat( 191 $links = $this->netscapeBookmarkUtils->filterAndFormat(
172 self::$bookmarkService,
173 self::$formatter, 192 self::$formatter,
174 'public', 193 'public',
175 true, 194 true,
176 $indexUrl 195 $indexUrl
177 ); 196 );
178 $this->assertEquals( 197 $this->assertEquals(
179 $indexUrl . '?WDWyig', 198 $indexUrl . 'shaare/WDWyig',
180 $links[2]['url'] 199 $links[2]['url']
181 ); 200 );
182 } 201 }
diff --git a/tests/netscape/BookmarkImportTest.php b/tests/netscape/BookmarkImportTest.php
index fef7f6d1..f678e26b 100644
--- a/tests/netscape/BookmarkImportTest.php
+++ b/tests/netscape/BookmarkImportTest.php
@@ -1,29 +1,31 @@
1<?php 1<?php
2
2namespace Shaarli\Netscape; 3namespace Shaarli\Netscape;
3 4
4use DateTime; 5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Psr\Http\Message\UploadedFileInterface;
5use Shaarli\Bookmark\Bookmark; 8use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFilter;
7use Shaarli\Bookmark\BookmarkFileService; 9use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\LinkDB; 10use Shaarli\Bookmark\BookmarkFilter;
9use Shaarli\Config\ConfigManager; 11use Shaarli\Config\ConfigManager;
10use Shaarli\History; 12use Shaarli\History;
13use Slim\Http\UploadedFile;
11 14
12/** 15/**
13 * Utility function to load a file's metadata in a $_FILES-like array 16 * Utility function to load a file's metadata in a $_FILES-like array
14 * 17 *
15 * @param string $filename Basename of the file 18 * @param string $filename Basename of the file
16 * 19 *
17 * @return array A $_FILES-like array 20 * @return UploadedFileInterface Upload file in PSR-7 compatible object
18 */ 21 */
19function file2array($filename) 22function file2array($filename)
20{ 23{
21 return array( 24 return new UploadedFile(
22 'filetoupload' => array( 25 __DIR__ . '/input/' . $filename,
23 'name' => $filename, 26 $filename,
24 'tmp_name' => __DIR__ . '/input/' . $filename, 27 null,
25 'size' => filesize(__DIR__ . '/input/' . $filename) 28 filesize(__DIR__ . '/input/' . $filename)
26 )
27 ); 29 );
28} 30}
29 31
@@ -31,7 +33,7 @@ function file2array($filename)
31/** 33/**
32 * Netscape bookmark import 34 * Netscape bookmark import
33 */ 35 */
34class BookmarkImportTest extends \PHPUnit\Framework\TestCase 36class BookmarkImportTest extends TestCase
35{ 37{
36 /** 38 /**
37 * @var string datastore to test write operations 39 * @var string datastore to test write operations
@@ -64,6 +66,11 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
64 protected $history; 66 protected $history;
65 67
66 /** 68 /**
69 * @var NetscapeBookmarkUtils
70 */
71 protected $netscapeBookmarkUtils;
72
73 /**
67 * @var string Save the current timezone. 74 * @var string Save the current timezone.
68 */ 75 */
69 protected static $defaultTimeZone; 76 protected static $defaultTimeZone;
@@ -91,6 +98,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
91 $this->conf->set('resource.datastore', self::$testDatastore); 98 $this->conf->set('resource.datastore', self::$testDatastore);
92 $this->history = new History(self::$historyFilePath); 99 $this->history = new History(self::$historyFilePath);
93 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 100 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
101 $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history);
94 } 102 }
95 103
96 /** 104 /**
@@ -115,7 +123,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
115 $this->assertEquals( 123 $this->assertEquals(
116 'File empty.htm (0 bytes) has an unknown file format.' 124 'File empty.htm (0 bytes) has an unknown file format.'
117 .' Nothing was imported.', 125 .' Nothing was imported.',
118 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) 126 $this->netscapeBookmarkUtils->import(null, $files)
119 ); 127 );
120 $this->assertEquals(0, $this->bookmarkService->count()); 128 $this->assertEquals(0, $this->bookmarkService->count());
121 } 129 }
@@ -128,7 +136,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
128 $files = file2array('no_doctype.htm'); 136 $files = file2array('no_doctype.htm');
129 $this->assertEquals( 137 $this->assertEquals(
130 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.', 138 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
131 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) 139 $this->netscapeBookmarkUtils->import(null, $files)
132 ); 140 );
133 $this->assertEquals(0, $this->bookmarkService->count()); 141 $this->assertEquals(0, $this->bookmarkService->count());
134 } 142 }
@@ -142,7 +150,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
142 $this->assertStringMatchesFormat( 150 $this->assertStringMatchesFormat(
143 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:' 151 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:'
144 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 152 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
145 NetscapeBookmarkUtils::import(null, $files, $this->bookmarkService, $this->conf, $this->history) 153 $this->netscapeBookmarkUtils->import(null, $files)
146 ); 154 );
147 $this->assertEquals(2, $this->bookmarkService->count()); 155 $this->assertEquals(2, $this->bookmarkService->count());
148 } 156 }
@@ -157,7 +165,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
157 $this->assertStringMatchesFormat( 165 $this->assertStringMatchesFormat(
158 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:' 166 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
159 .' 1 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 167 .' 1 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
160 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history) 168 $this->netscapeBookmarkUtils->import([], $files)
161 ); 169 );
162 $this->assertEquals(1, $this->bookmarkService->count()); 170 $this->assertEquals(1, $this->bookmarkService->count());
163 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 171 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -185,7 +193,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
185 $this->assertStringMatchesFormat( 193 $this->assertStringMatchesFormat(
186 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:' 194 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
187 .' 8 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 195 .' 8 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
188 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history) 196 $this->netscapeBookmarkUtils->import([], $files)
189 ); 197 );
190 $this->assertEquals(8, $this->bookmarkService->count()); 198 $this->assertEquals(8, $this->bookmarkService->count());
191 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 199 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -306,7 +314,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
306 $this->assertStringMatchesFormat( 314 $this->assertStringMatchesFormat(
307 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 315 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
308 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 316 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
309 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history) 317 $this->netscapeBookmarkUtils->import([], $files)
310 ); 318 );
311 319
312 $this->assertEquals(2, $this->bookmarkService->count()); 320 $this->assertEquals(2, $this->bookmarkService->count());
@@ -349,7 +357,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
349 $this->assertStringMatchesFormat( 357 $this->assertStringMatchesFormat(
350 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 358 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
351 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 359 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
352 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 360 $this->netscapeBookmarkUtils->import($post, $files)
353 ); 361 );
354 362
355 $this->assertEquals(2, $this->bookmarkService->count()); 363 $this->assertEquals(2, $this->bookmarkService->count());
@@ -392,7 +400,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
392 $this->assertStringMatchesFormat( 400 $this->assertStringMatchesFormat(
393 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 401 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
394 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 402 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
395 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 403 $this->netscapeBookmarkUtils->import($post, $files)
396 ); 404 );
397 $this->assertEquals(2, $this->bookmarkService->count()); 405 $this->assertEquals(2, $this->bookmarkService->count());
398 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 406 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -410,7 +418,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
410 $this->assertStringMatchesFormat( 418 $this->assertStringMatchesFormat(
411 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 419 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
412 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 420 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
413 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 421 $this->netscapeBookmarkUtils->import($post, $files)
414 ); 422 );
415 $this->assertEquals(2, $this->bookmarkService->count()); 423 $this->assertEquals(2, $this->bookmarkService->count());
416 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 424 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -430,7 +438,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
430 $this->assertStringMatchesFormat( 438 $this->assertStringMatchesFormat(
431 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 439 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
432 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 440 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
433 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 441 $this->netscapeBookmarkUtils->import($post, $files)
434 ); 442 );
435 $this->assertEquals(2, $this->bookmarkService->count()); 443 $this->assertEquals(2, $this->bookmarkService->count());
436 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 444 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -445,7 +453,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
445 $this->assertStringMatchesFormat( 453 $this->assertStringMatchesFormat(
446 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 454 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
447 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.', 455 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
448 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 456 $this->netscapeBookmarkUtils->import($post, $files)
449 ); 457 );
450 $this->assertEquals(2, $this->bookmarkService->count()); 458 $this->assertEquals(2, $this->bookmarkService->count());
451 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 459 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -465,7 +473,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
465 $this->assertStringMatchesFormat( 473 $this->assertStringMatchesFormat(
466 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 474 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
467 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 475 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
468 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 476 $this->netscapeBookmarkUtils->import($post, $files)
469 ); 477 );
470 $this->assertEquals(2, $this->bookmarkService->count()); 478 $this->assertEquals(2, $this->bookmarkService->count());
471 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 479 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -480,7 +488,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
480 $this->assertStringMatchesFormat( 488 $this->assertStringMatchesFormat(
481 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 489 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
482 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.', 490 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
483 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 491 $this->netscapeBookmarkUtils->import($post, $files)
484 ); 492 );
485 $this->assertEquals(2, $this->bookmarkService->count()); 493 $this->assertEquals(2, $this->bookmarkService->count());
486 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 494 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -498,7 +506,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
498 $this->assertStringMatchesFormat( 506 $this->assertStringMatchesFormat(
499 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 507 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
500 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 508 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
501 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 509 $this->netscapeBookmarkUtils->import($post, $files)
502 ); 510 );
503 $this->assertEquals(2, $this->bookmarkService->count()); 511 $this->assertEquals(2, $this->bookmarkService->count());
504 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 512 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -508,7 +516,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
508 $this->assertStringMatchesFormat( 516 $this->assertStringMatchesFormat(
509 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 517 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
510 .' 0 bookmarks imported, 0 bookmarks overwritten, 2 bookmarks skipped.', 518 .' 0 bookmarks imported, 0 bookmarks overwritten, 2 bookmarks skipped.',
511 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 519 $this->netscapeBookmarkUtils->import($post, $files)
512 ); 520 );
513 $this->assertEquals(2, $this->bookmarkService->count()); 521 $this->assertEquals(2, $this->bookmarkService->count());
514 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 522 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -527,7 +535,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
527 $this->assertStringMatchesFormat( 535 $this->assertStringMatchesFormat(
528 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 536 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
529 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 537 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
530 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 538 $this->netscapeBookmarkUtils->import($post, $files)
531 ); 539 );
532 $this->assertEquals(2, $this->bookmarkService->count()); 540 $this->assertEquals(2, $this->bookmarkService->count());
533 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 541 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -548,7 +556,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
548 $this->assertStringMatchesFormat( 556 $this->assertStringMatchesFormat(
549 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 557 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
550 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 558 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
551 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 559 $this->netscapeBookmarkUtils->import($post, $files)
552 ); 560 );
553 $this->assertEquals(2, $this->bookmarkService->count()); 561 $this->assertEquals(2, $this->bookmarkService->count());
554 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 562 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -573,7 +581,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
573 $this->assertStringMatchesFormat( 581 $this->assertStringMatchesFormat(
574 'File same_date.htm (453 bytes) was successfully processed in %d seconds:' 582 'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
575 .' 3 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 583 .' 3 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
576 NetscapeBookmarkUtils::import(array(), $files, $this->bookmarkService, $this->conf, $this->history) 584 $this->netscapeBookmarkUtils->import(array(), $files)
577 ); 585 );
578 $this->assertEquals(3, $this->bookmarkService->count()); 586 $this->assertEquals(3, $this->bookmarkService->count());
579 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 587 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -589,14 +597,14 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
589 'overwrite' => 'true', 597 'overwrite' => 'true',
590 ]; 598 ];
591 $files = file2array('netscape_basic.htm'); 599 $files = file2array('netscape_basic.htm');
592 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history); 600 $this->netscapeBookmarkUtils->import($post, $files);
593 $history = $this->history->getHistory(); 601 $history = $this->history->getHistory();
594 $this->assertEquals(1, count($history)); 602 $this->assertEquals(1, count($history));
595 $this->assertEquals(History::IMPORT, $history[0]['event']); 603 $this->assertEquals(History::IMPORT, $history[0]['event']);
596 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']); 604 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
597 605
598 // re-import as private, enable overwriting 606 // re-import as private, enable overwriting
599 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history); 607 $this->netscapeBookmarkUtils->import($post, $files);
600 $history = $this->history->getHistory(); 608 $history = $this->history->getHistory();
601 $this->assertEquals(2, count($history)); 609 $this->assertEquals(2, count($history));
602 $this->assertEquals(History::IMPORT, $history[0]['event']); 610 $this->assertEquals(History::IMPORT, $history[0]['event']);
diff --git a/tests/plugins/PluginAddlinkTest.php b/tests/plugins/PluginAddlinkTest.php
index d052f8b9..aa5c6988 100644
--- a/tests/plugins/PluginAddlinkTest.php
+++ b/tests/plugins/PluginAddlinkTest.php
@@ -2,7 +2,7 @@
2namespace Shaarli\Plugin\Addlink; 2namespace Shaarli\Plugin\Addlink;
3 3
4use Shaarli\Plugin\PluginManager; 4use Shaarli\Plugin\PluginManager;
5use Shaarli\Router; 5use Shaarli\Render\TemplatePage;
6 6
7require_once 'plugins/addlink_toolbar/addlink_toolbar.php'; 7require_once 'plugins/addlink_toolbar/addlink_toolbar.php';
8 8
@@ -26,8 +26,9 @@ class PluginAddlinkTest extends \PHPUnit\Framework\TestCase
26 { 26 {
27 $str = 'stuff'; 27 $str = 'stuff';
28 $data = array($str => $str); 28 $data = array($str => $str);
29 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 29 $data['_PAGE_'] = TemplatePage::LINKLIST;
30 $data['_LOGGEDIN_'] = true; 30 $data['_LOGGEDIN_'] = true;
31 $data['_BASE_PATH_'] = '/subfolder';
31 32
32 $data = hook_addlink_toolbar_render_header($data); 33 $data = hook_addlink_toolbar_render_header($data);
33 $this->assertEquals($str, $data[$str]); 34 $this->assertEquals($str, $data[$str]);
@@ -36,6 +37,8 @@ class PluginAddlinkTest extends \PHPUnit\Framework\TestCase
36 $data = array($str => $str); 37 $data = array($str => $str);
37 $data['_PAGE_'] = $str; 38 $data['_PAGE_'] = $str;
38 $data['_LOGGEDIN_'] = true; 39 $data['_LOGGEDIN_'] = true;
40 $data['_BASE_PATH_'] = '/subfolder';
41
39 $data = hook_addlink_toolbar_render_header($data); 42 $data = hook_addlink_toolbar_render_header($data);
40 $this->assertEquals($str, $data[$str]); 43 $this->assertEquals($str, $data[$str]);
41 $this->assertArrayNotHasKey('fields_toolbar', $data); 44 $this->assertArrayNotHasKey('fields_toolbar', $data);
@@ -48,8 +51,9 @@ class PluginAddlinkTest extends \PHPUnit\Framework\TestCase
48 { 51 {
49 $str = 'stuff'; 52 $str = 'stuff';
50 $data = array($str => $str); 53 $data = array($str => $str);
51 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 54 $data['_PAGE_'] = TemplatePage::LINKLIST;
52 $data['_LOGGEDIN_'] = false; 55 $data['_LOGGEDIN_'] = false;
56 $data['_BASE_PATH_'] = '/subfolder';
53 57
54 $data = hook_addlink_toolbar_render_header($data); 58 $data = hook_addlink_toolbar_render_header($data);
55 $this->assertEquals($str, $data[$str]); 59 $this->assertEquals($str, $data[$str]);
diff --git a/tests/plugins/PluginPlayvideosTest.php b/tests/plugins/PluginPlayvideosTest.php
index 51472617..b7b6ce53 100644
--- a/tests/plugins/PluginPlayvideosTest.php
+++ b/tests/plugins/PluginPlayvideosTest.php
@@ -6,7 +6,7 @@ namespace Shaarli\Plugin\Playvideos;
6 */ 6 */
7 7
8use Shaarli\Plugin\PluginManager; 8use Shaarli\Plugin\PluginManager;
9use Shaarli\Router; 9use Shaarli\Render\TemplatePage;
10 10
11require_once 'plugins/playvideos/playvideos.php'; 11require_once 'plugins/playvideos/playvideos.php';
12 12
@@ -31,7 +31,7 @@ class PluginPlayvideosTest extends \PHPUnit\Framework\TestCase
31 { 31 {
32 $str = 'stuff'; 32 $str = 'stuff';
33 $data = array($str => $str); 33 $data = array($str => $str);
34 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 34 $data['_PAGE_'] = TemplatePage::LINKLIST;
35 35
36 $data = hook_playvideos_render_header($data); 36 $data = hook_playvideos_render_header($data);
37 $this->assertEquals($str, $data[$str]); 37 $this->assertEquals($str, $data[$str]);
@@ -50,7 +50,7 @@ class PluginPlayvideosTest extends \PHPUnit\Framework\TestCase
50 { 50 {
51 $str = 'stuff'; 51 $str = 'stuff';
52 $data = array($str => $str); 52 $data = array($str => $str);
53 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 53 $data['_PAGE_'] = TemplatePage::LINKLIST;
54 54
55 $data = hook_playvideos_render_footer($data); 55 $data = hook_playvideos_render_footer($data);
56 $this->assertEquals($str, $data[$str]); 56 $this->assertEquals($str, $data[$str]);
diff --git a/tests/plugins/PluginPubsubhubbubTest.php b/tests/plugins/PluginPubsubhubbubTest.php
index a7bd8fc9..e66f484e 100644
--- a/tests/plugins/PluginPubsubhubbubTest.php
+++ b/tests/plugins/PluginPubsubhubbubTest.php
@@ -3,7 +3,7 @@ namespace Shaarli\Plugin\Pubsubhubbub;
3 3
4use Shaarli\Config\ConfigManager; 4use Shaarli\Config\ConfigManager;
5use Shaarli\Plugin\PluginManager; 5use Shaarli\Plugin\PluginManager;
6use Shaarli\Router; 6use Shaarli\Render\TemplatePage;
7 7
8require_once 'plugins/pubsubhubbub/pubsubhubbub.php'; 8require_once 'plugins/pubsubhubbub/pubsubhubbub.php';
9 9
@@ -34,7 +34,7 @@ class PluginPubsubhubbubTest extends \PHPUnit\Framework\TestCase
34 $hub = 'http://domain.hub'; 34 $hub = 'http://domain.hub';
35 $conf = new ConfigManager(self::$configFile); 35 $conf = new ConfigManager(self::$configFile);
36 $conf->set('plugins.PUBSUBHUB_URL', $hub); 36 $conf->set('plugins.PUBSUBHUB_URL', $hub);
37 $data['_PAGE_'] = Router::$PAGE_FEED_RSS; 37 $data['_PAGE_'] = TemplatePage::FEED_RSS;
38 38
39 $data = hook_pubsubhubbub_render_feed($data, $conf); 39 $data = hook_pubsubhubbub_render_feed($data, $conf);
40 $expected = '<atom:link rel="hub" href="'. $hub .'" />'; 40 $expected = '<atom:link rel="hub" href="'. $hub .'" />';
@@ -49,7 +49,7 @@ class PluginPubsubhubbubTest extends \PHPUnit\Framework\TestCase
49 $hub = 'http://domain.hub'; 49 $hub = 'http://domain.hub';
50 $conf = new ConfigManager(self::$configFile); 50 $conf = new ConfigManager(self::$configFile);
51 $conf->set('plugins.PUBSUBHUB_URL', $hub); 51 $conf->set('plugins.PUBSUBHUB_URL', $hub);
52 $data['_PAGE_'] = Router::$PAGE_FEED_ATOM; 52 $data['_PAGE_'] = TemplatePage::FEED_ATOM;
53 53
54 $data = hook_pubsubhubbub_render_feed($data, $conf); 54 $data = hook_pubsubhubbub_render_feed($data, $conf);
55 $expected = '<link rel="hub" href="'. $hub .'" />'; 55 $expected = '<link rel="hub" href="'. $hub .'" />';
diff --git a/tests/plugins/PluginQrcodeTest.php b/tests/plugins/PluginQrcodeTest.php
index 0c61e14a..c9f8c733 100644
--- a/tests/plugins/PluginQrcodeTest.php
+++ b/tests/plugins/PluginQrcodeTest.php
@@ -6,7 +6,7 @@ namespace Shaarli\Plugin\Qrcode;
6 */ 6 */
7 7
8use Shaarli\Plugin\PluginManager; 8use Shaarli\Plugin\PluginManager;
9use Shaarli\Router; 9use Shaarli\Render\TemplatePage;
10 10
11require_once 'plugins/qrcode/qrcode.php'; 11require_once 'plugins/qrcode/qrcode.php';
12 12
@@ -57,7 +57,7 @@ class PluginQrcodeTest extends \PHPUnit\Framework\TestCase
57 { 57 {
58 $str = 'stuff'; 58 $str = 'stuff';
59 $data = array($str => $str); 59 $data = array($str => $str);
60 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 60 $data['_PAGE_'] = TemplatePage::LINKLIST;
61 61
62 $data = hook_qrcode_render_footer($data); 62 $data = hook_qrcode_render_footer($data);
63 $this->assertEquals($str, $data[$str]); 63 $this->assertEquals($str, $data[$str]);
diff --git a/tests/plugins/resources/hashtags.md b/tests/plugins/resources/hashtags.md
deleted file mode 100644
index 46326de3..00000000
--- a/tests/plugins/resources/hashtags.md
+++ /dev/null
@@ -1,10 +0,0 @@
1[#lol](?addtag=lol)
2
3 #test
4
5`#test2`
6
7```
8bla #bli blo
9#bla
10```
diff --git a/tests/plugins/resources/hashtags.raw b/tests/plugins/resources/hashtags.raw
deleted file mode 100644
index 9d2dc98a..00000000
--- a/tests/plugins/resources/hashtags.raw
+++ /dev/null
@@ -1,10 +0,0 @@
1#lol
2
3 #test
4
5`#test2`
6
7```
8bla #bli blo
9#bla
10```
diff --git a/tests/plugins/resources/markdown.html b/tests/plugins/resources/markdown.html
deleted file mode 100644
index c3460bf7..00000000
--- a/tests/plugins/resources/markdown.html
+++ /dev/null
@@ -1,33 +0,0 @@
1<div class="markdown"><ul>
2<li>test:
3<ul>
4<li><a href="http://link.tld">zero</a></li>
5<li><a href="http://link.tld">two</a></li>
6<li><a href="http://link.tld">three</a></li>
7</ul></li>
8</ul>
9<ol>
10<li><a href="http://link.tld">zero</a>
11<ol start="2">
12<li><a href="http://link.tld">two</a></li>
13<li><a href="http://link.tld">three</a></li>
14<li><a href="http://link.tld">four</a></li>
15<li>foo <a href="?addtag=foobar">#foobar</a></li>
16</ol></li>
17</ol>
18<p><a href="?addtag=foobar">#foobar</a> foo <code>lol #foo</code> <a href="?addtag=bar">#bar</a></p>
19<p>fsdfs <a href="http://link.tld">http://link.tld</a> <a href="?addtag=foobar">#foobar</a> <code>http://link.tld</code></p>
20<pre><code>http://link.tld #foobar
21next #foo</code></pre>
22<p>Block:</p>
23<pre><code>lorem ipsum #foobar http://link.tld
24#foobar http://link.tld</code></pre>
25<p><a href="?123456">link</a><br />
26<img src="/img/train.png" alt="link" /><br />
27<a href="http://test.tld/path/?query=value#hash">link</a><br />
28<a href="http://test.tld/path/?query=value#hash">link</a><br />
29<a href="https://test.tld/path/?query=value#hash">link</a><br />
30<a href="ftp://test.tld/path/?query=value#hash">link</a><br />
31<a href="magnet:test.tld/path/?query=value#hash">link</a><br />
32<a href="http://alert(&#039;xss&#039;)">link</a><br />
33<a href="http://test.tld/path/?query=value#hash">link</a></p></div>
diff --git a/tests/plugins/resources/markdown.md b/tests/plugins/resources/markdown.md
deleted file mode 100644
index 9350a8c7..00000000
--- a/tests/plugins/resources/markdown.md
+++ /dev/null
@@ -1,34 +0,0 @@
1* test:
2 * [zero](http://link.tld)
3 + [two](http://link.tld)
4 - [three](http://link.tld)
5
61. [zero](http://link.tld)
7 2. [two](http://link.tld)
8 3. [three](http://link.tld)
9 4. [four](http://link.tld)
10 5. foo #foobar
11
12#foobar foo `lol #foo` #bar
13
14fsdfs http://link.tld #foobar `http://link.tld`
15
16 http://link.tld #foobar
17 next #foo
18
19Block:
20
21```
22lorem ipsum #foobar http://link.tld
23#foobar http://link.tld
24```
25
26[link](?123456)
27![link](/img/train.png)
28[link](test.tld/path/?query=value#hash)
29[link](http://test.tld/path/?query=value#hash)
30[link](https://test.tld/path/?query=value#hash)
31[link](ftp://test.tld/path/?query=value#hash)
32[link](magnet:test.tld/path/?query=value#hash)
33[link](javascript:alert('xss'))
34[link](other://test.tld/path/?query=value#hash)
diff --git a/tests/feed/CacheTest.php b/tests/render/PageCacheManagerTest.php
index c0a9f26f..c258f45f 100644
--- a/tests/feed/CacheTest.php
+++ b/tests/render/PageCacheManagerTest.php
@@ -1,18 +1,18 @@
1<?php 1<?php
2
2/** 3/**
3 * Cache tests 4 * Cache tests
4 */ 5 */
5namespace Shaarli\Feed;
6 6
7// required to access $_SESSION array 7namespace Shaarli\Render;
8session_start();
9 8
10require_once 'application/feed/Cache.php'; 9use PHPUnit\Framework\TestCase;
10use Shaarli\Security\SessionManager;
11 11
12/** 12/**
13 * Unitary tests for cached pages 13 * Unitary tests for cached pages
14 */ 14 */
15class CacheTest extends \PHPUnit\Framework\TestCase 15class PageCacheManagerTest extends TestCase
16{ 16{
17 // test cache directory 17 // test cache directory
18 protected static $testCacheDir = 'sandbox/dummycache'; 18 protected static $testCacheDir = 'sandbox/dummycache';
@@ -20,12 +20,19 @@ class CacheTest extends \PHPUnit\Framework\TestCase
20 // dummy cached file names / content 20 // dummy cached file names / content
21 protected static $pages = array('a', 'toto', 'd7b59c'); 21 protected static $pages = array('a', 'toto', 'd7b59c');
22 22
23 /** @var PageCacheManager */
24 protected $cacheManager;
25
26 /** @var SessionManager */
27 protected $sessionManager;
23 28
24 /** 29 /**
25 * Populate the cache with dummy files 30 * Populate the cache with dummy files
26 */ 31 */
27 public function setUp() 32 public function setUp()
28 { 33 {
34 $this->cacheManager = new PageCacheManager(static::$testCacheDir, true);
35
29 if (!is_dir(self::$testCacheDir)) { 36 if (!is_dir(self::$testCacheDir)) {
30 mkdir(self::$testCacheDir); 37 mkdir(self::$testCacheDir);
31 } else { 38 } else {
@@ -52,7 +59,7 @@ class CacheTest extends \PHPUnit\Framework\TestCase
52 */ 59 */
53 public function testPurgeCachedPages() 60 public function testPurgeCachedPages()
54 { 61 {
55 purgeCachedPages(self::$testCacheDir); 62 $this->cacheManager->purgeCachedPages();
56 foreach (self::$pages as $page) { 63 foreach (self::$pages as $page) {
57 $this->assertFileNotExists(self::$testCacheDir . '/' . $page . '.cache'); 64 $this->assertFileNotExists(self::$testCacheDir . '/' . $page . '.cache');
58 } 65 }
@@ -65,28 +72,14 @@ class CacheTest extends \PHPUnit\Framework\TestCase
65 */ 72 */
66 public function testPurgeCachedPagesMissingDir() 73 public function testPurgeCachedPagesMissingDir()
67 { 74 {
75 $this->cacheManager = new PageCacheManager(self::$testCacheDir . '_missing', true);
76
68 $oldlog = ini_get('error_log'); 77 $oldlog = ini_get('error_log');
69 ini_set('error_log', '/dev/null'); 78 ini_set('error_log', '/dev/null');
70 $this->assertEquals( 79 $this->assertEquals(
71 'Cannot purge sandbox/dummycache_missing: no directory', 80 'Cannot purge sandbox/dummycache_missing: no directory',
72 purgeCachedPages(self::$testCacheDir . '_missing') 81 $this->cacheManager->purgeCachedPages()
73 ); 82 );
74 ini_set('error_log', $oldlog); 83 ini_set('error_log', $oldlog);
75 } 84 }
76
77 /**
78 * Purge cached pages and session cache
79 */
80 public function testInvalidateCaches()
81 {
82 $this->assertArrayNotHasKey('tags', $_SESSION);
83 $_SESSION['tags'] = array('goodbye', 'cruel', 'world');
84
85 invalidateCaches(self::$testCacheDir);
86 foreach (self::$pages as $page) {
87 $this->assertFileNotExists(self::$testCacheDir . '/' . $page . '.cache');
88 }
89
90 $this->assertArrayNotHasKey('tags', $_SESSION);
91 }
92} 85}
diff --git a/tests/security/LoginManagerTest.php b/tests/security/LoginManagerTest.php
index 8fd1698c..f242be09 100644
--- a/tests/security/LoginManagerTest.php
+++ b/tests/security/LoginManagerTest.php
@@ -1,7 +1,6 @@
1<?php 1<?php
2namespace Shaarli\Security;
3 2
4require_once 'tests/utils/FakeConfigManager.php'; 3namespace Shaarli\Security;
5 4
6use PHPUnit\Framework\TestCase; 5use PHPUnit\Framework\TestCase;
7 6
@@ -58,6 +57,9 @@ class LoginManagerTest extends TestCase
58 /** @var string Salt used by hash functions */ 57 /** @var string Salt used by hash functions */
59 protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2'; 58 protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
60 59
60 /** @var CookieManager */
61 protected $cookieManager;
62
61 /** 63 /**
62 * Prepare or reset test resources 64 * Prepare or reset test resources
63 */ 65 */
@@ -84,8 +86,12 @@ class LoginManagerTest extends TestCase
84 $this->cookie = []; 86 $this->cookie = [];
85 $this->session = []; 87 $this->session = [];
86 88
87 $this->sessionManager = new SessionManager($this->session, $this->configManager); 89 $this->cookieManager = $this->createMock(CookieManager::class);
88 $this->loginManager = new LoginManager($this->configManager, $this->sessionManager); 90 $this->cookieManager->method('getCookieParameter')->willReturnCallback(function (string $key) {
91 return $this->cookie[$key] ?? null;
92 });
93 $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path');
94 $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager);
89 $this->server['REMOTE_ADDR'] = $this->ipAddr; 95 $this->server['REMOTE_ADDR'] = $this->ipAddr;
90 } 96 }
91 97
@@ -193,8 +199,8 @@ class LoginManagerTest extends TestCase
193 $configManager = new \FakeConfigManager([ 199 $configManager = new \FakeConfigManager([
194 'resource.ban_file' => $this->banFile, 200 'resource.ban_file' => $this->banFile,
195 ]); 201 ]);
196 $loginManager = new LoginManager($configManager, null); 202 $loginManager = new LoginManager($configManager, null, $this->cookieManager);
197 $loginManager->checkLoginState([], ''); 203 $loginManager->checkLoginState('');
198 204
199 $this->assertFalse($loginManager->isLoggedIn()); 205 $this->assertFalse($loginManager->isLoggedIn());
200 } 206 }
@@ -210,9 +216,9 @@ class LoginManagerTest extends TestCase
210 'expires_on' => time() + 100, 216 'expires_on' => time() + 100,
211 ]; 217 ];
212 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 218 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
213 $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope'; 219 $this->cookie[CookieManager::STAY_SIGNED_IN] = 'nope';
214 220
215 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 221 $this->loginManager->checkLoginState($this->clientIpAddress);
216 222
217 $this->assertTrue($this->loginManager->isLoggedIn()); 223 $this->assertTrue($this->loginManager->isLoggedIn());
218 $this->assertTrue(empty($this->session['username'])); 224 $this->assertTrue(empty($this->session['username']));
@@ -224,9 +230,9 @@ class LoginManagerTest extends TestCase
224 public function testCheckLoginStateStaySignedInWithValidToken() 230 public function testCheckLoginStateStaySignedInWithValidToken()
225 { 231 {
226 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 232 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
227 $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken(); 233 $this->cookie[CookieManager::STAY_SIGNED_IN] = $this->loginManager->getStaySignedInToken();
228 234
229 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 235 $this->loginManager->checkLoginState($this->clientIpAddress);
230 236
231 $this->assertTrue($this->loginManager->isLoggedIn()); 237 $this->assertTrue($this->loginManager->isLoggedIn());
232 $this->assertEquals($this->login, $this->session['username']); 238 $this->assertEquals($this->login, $this->session['username']);
@@ -241,7 +247,7 @@ class LoginManagerTest extends TestCase
241 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 247 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
242 $this->session['expires_on'] = time() - 100; 248 $this->session['expires_on'] = time() - 100;
243 249
244 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 250 $this->loginManager->checkLoginState($this->clientIpAddress);
245 251
246 $this->assertFalse($this->loginManager->isLoggedIn()); 252 $this->assertFalse($this->loginManager->isLoggedIn());
247 } 253 }
@@ -253,7 +259,7 @@ class LoginManagerTest extends TestCase
253 { 259 {
254 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 260 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
255 261
256 $this->loginManager->checkLoginState($this->cookie, '10.7.157.98'); 262 $this->loginManager->checkLoginState('10.7.157.98');
257 263
258 $this->assertFalse($this->loginManager->isLoggedIn()); 264 $this->assertFalse($this->loginManager->isLoggedIn());
259 } 265 }
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php
index f264505e..60695dcf 100644
--- a/tests/security/SessionManagerTest.php
+++ b/tests/security/SessionManagerTest.php
@@ -1,12 +1,8 @@
1<?php 1<?php
2require_once 'tests/utils/FakeConfigManager.php';
3 2
4// Initialize reference data _before_ PHPUnit starts a session 3namespace Shaarli\Security;
5require_once 'tests/utils/ReferenceSessionIdHashes.php';
6ReferenceSessionIdHashes::genAllHashes();
7 4
8use PHPUnit\Framework\TestCase; 5use PHPUnit\Framework\TestCase;
9use Shaarli\Security\SessionManager;
10 6
11/** 7/**
12 * Test coverage for SessionManager 8 * Test coverage for SessionManager
@@ -30,7 +26,7 @@ class SessionManagerTest extends TestCase
30 */ 26 */
31 public static function setUpBeforeClass() 27 public static function setUpBeforeClass()
32 { 28 {
33 self::$sidHashes = ReferenceSessionIdHashes::getHashes(); 29 self::$sidHashes = \ReferenceSessionIdHashes::getHashes();
34 } 30 }
35 31
36 /** 32 /**
@@ -38,13 +34,13 @@ class SessionManagerTest extends TestCase
38 */ 34 */
39 public function setUp() 35 public function setUp()
40 { 36 {
41 $this->conf = new FakeConfigManager([ 37 $this->conf = new \FakeConfigManager([
42 'credentials.login' => 'johndoe', 38 'credentials.login' => 'johndoe',
43 'credentials.salt' => 'salt', 39 'credentials.salt' => 'salt',
44 'security.session_protection_disabled' => false, 40 'security.session_protection_disabled' => false,
45 ]); 41 ]);
46 $this->session = []; 42 $this->session = [];
47 $this->sessionManager = new SessionManager($this->session, $this->conf); 43 $this->sessionManager = new SessionManager($this->session, $this->conf, 'session_path');
48 } 44 }
49 45
50 /** 46 /**
@@ -69,7 +65,7 @@ class SessionManagerTest extends TestCase
69 $token => 1, 65 $token => 1,
70 ], 66 ],
71 ]; 67 ];
72 $sessionManager = new SessionManager($session, $this->conf); 68 $sessionManager = new SessionManager($session, $this->conf, 'session_path');
73 69
74 // check and destroy the token 70 // check and destroy the token
75 $this->assertTrue($sessionManager->checkToken($token)); 71 $this->assertTrue($sessionManager->checkToken($token));
@@ -269,4 +265,61 @@ class SessionManagerTest extends TestCase
269 $this->session['ip'] = 'ip_id_one'; 265 $this->session['ip'] = 'ip_id_one';
270 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two')); 266 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two'));
271 } 267 }
268
269 /**
270 * Test creating an entry in the session array
271 */
272 public function testSetSessionParameterCreate(): void
273 {
274 $this->sessionManager->setSessionParameter('abc', 'def');
275
276 static::assertSame('def', $this->session['abc']);
277 }
278
279 /**
280 * Test updating an entry in the session array
281 */
282 public function testSetSessionParameterUpdate(): void
283 {
284 $this->session['abc'] = 'ghi';
285
286 $this->sessionManager->setSessionParameter('abc', 'def');
287
288 static::assertSame('def', $this->session['abc']);
289 }
290
291 /**
292 * Test updating an entry in the session array with null value
293 */
294 public function testSetSessionParameterUpdateNull(): void
295 {
296 $this->session['abc'] = 'ghi';
297
298 $this->sessionManager->setSessionParameter('abc', null);
299
300 static::assertArrayHasKey('abc', $this->session);
301 static::assertNull($this->session['abc']);
302 }
303
304 /**
305 * Test deleting an existing entry in the session array
306 */
307 public function testDeleteSessionParameter(): void
308 {
309 $this->session['abc'] = 'def';
310
311 $this->sessionManager->deleteSessionParameter('abc');
312
313 static::assertArrayNotHasKey('abc', $this->session);
314 }
315
316 /**
317 * Test deleting a non existent entry in the session array
318 */
319 public function testDeleteSessionParameterNotExisting(): void
320 {
321 $this->sessionManager->deleteSessionParameter('abc');
322
323 static::assertArrayNotHasKey('abc', $this->session);
324 }
272} 325}
diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php
index c689982b..a7dd70bf 100644
--- a/tests/updater/UpdaterTest.php
+++ b/tests/updater/UpdaterTest.php
@@ -2,17 +2,18 @@
2namespace Shaarli\Updater; 2namespace Shaarli\Updater;
3 3
4use Exception; 4use Exception;
5use PHPUnit\Framework\TestCase;
6use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\BookmarkServiceInterface;
5use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\History;
6 10
7require_once 'tests/updater/DummyUpdater.php';
8require_once 'tests/utils/ReferenceLinkDB.php';
9require_once 'inc/rain.tpl.class.php';
10 11
11/** 12/**
12 * Class UpdaterTest. 13 * Class UpdaterTest.
13 * Runs unit tests against the updater class. 14 * Runs unit tests against the updater class.
14 */ 15 */
15class UpdaterTest extends \PHPUnit\Framework\TestCase 16class UpdaterTest extends TestCase
16{ 17{
17 /** 18 /**
18 * @var string Path to test datastore. 19 * @var string Path to test datastore.
@@ -29,13 +30,27 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
29 */ 30 */
30 protected $conf; 31 protected $conf;
31 32
33 /** @var BookmarkServiceInterface */
34 protected $bookmarkService;
35
36 /** @var \ReferenceLinkDB */
37 protected $refDB;
38
39 /** @var Updater */
40 protected $updater;
41
32 /** 42 /**
33 * Executed before each test. 43 * Executed before each test.
34 */ 44 */
35 public function setUp() 45 public function setUp()
36 { 46 {
47 $this->refDB = new \ReferenceLinkDB();
48 $this->refDB->write(self::$testDatastore);
49
37 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); 50 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
38 $this->conf = new ConfigManager(self::$configFile); 51 $this->conf = new ConfigManager(self::$configFile);
52 $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true);
53 $this->updater = new Updater([], $this->bookmarkService, $this->conf, true);
39 } 54 }
40 55
41 /** 56 /**
@@ -167,4 +182,40 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
167 $updater = new DummyUpdater($updates, array(), $this->conf, true); 182 $updater = new DummyUpdater($updates, array(), $this->conf, true);
168 $updater->update(); 183 $updater->update();
169 } 184 }
185
186 public function testUpdateMethodRelativeHomeLinkRename(): void
187 {
188 $this->updater->setBasePath('/subfolder');
189 $this->conf->set('general.header_link', '?');
190
191 $this->updater->updateMethodRelativeHomeLink();
192
193 static::assertSame('/subfolder/', $this->conf->get('general.header_link'));
194 }
195
196 public function testUpdateMethodRelativeHomeLinkDoNotRename(): void
197 {
198 $this->conf->set('general.header_link', '~/my-blog');
199
200 $this->updater->updateMethodRelativeHomeLink();
201
202 static::assertSame('~/my-blog', $this->conf->get('general.header_link'));
203 }
204
205 public function testUpdateMethodMigrateExistingNotesUrl(): void
206 {
207 $this->updater->updateMethodMigrateExistingNotesUrl();
208
209 static::assertSame($this->refDB->getLinks()[0]->getUrl(), $this->bookmarkService->get(0)->getUrl());
210 static::assertSame($this->refDB->getLinks()[1]->getUrl(), $this->bookmarkService->get(1)->getUrl());
211 static::assertSame($this->refDB->getLinks()[4]->getUrl(), $this->bookmarkService->get(4)->getUrl());
212 static::assertSame($this->refDB->getLinks()[6]->getUrl(), $this->bookmarkService->get(6)->getUrl());
213 static::assertSame($this->refDB->getLinks()[7]->getUrl(), $this->bookmarkService->get(7)->getUrl());
214 static::assertSame($this->refDB->getLinks()[8]->getUrl(), $this->bookmarkService->get(8)->getUrl());
215 static::assertSame($this->refDB->getLinks()[9]->getUrl(), $this->bookmarkService->get(9)->getUrl());
216 static::assertSame('/shaare/WDWyig', $this->bookmarkService->get(42)->getUrl());
217 static::assertSame('/shaare/WDWyig', $this->bookmarkService->get(41)->getUrl());
218 static::assertSame('/shaare/0gCTjQ', $this->bookmarkService->get(10)->getUrl());
219 static::assertSame('/shaare/PCRizQ', $this->bookmarkService->get(11)->getUrl());
220 }
170} 221}
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index 0095f5a1..fc3cb109 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -30,7 +30,7 @@ class ReferenceLinkDB
30 $this->addLink( 30 $this->addLink(
31 11, 31 11,
32 'Pined older', 32 'Pined older',
33 '?PCRizQ', 33 '/shaare/PCRizQ',
34 'This is an older pinned link', 34 'This is an older pinned link',
35 0, 35 0,
36 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100309_101010'), 36 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100309_101010'),
@@ -43,7 +43,7 @@ class ReferenceLinkDB
43 $this->addLink( 43 $this->addLink(
44 10, 44 10,
45 'Pined', 45 'Pined',
46 '?0gCTjQ', 46 '/shaare/0gCTjQ',
47 'This is a pinned link', 47 'This is a pinned link',
48 0, 48 0,
49 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121207_152312'), 49 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121207_152312'),
@@ -56,7 +56,7 @@ class ReferenceLinkDB
56 $this->addLink( 56 $this->addLink(
57 41, 57 41,
58 'Link title: @website', 58 'Link title: @website',
59 '?WDWyig', 59 '/shaare/WDWyig',
60 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag', 60 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag',
61 0, 61 0,
62 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), 62 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'),
@@ -68,7 +68,7 @@ class ReferenceLinkDB
68 $this->addLink( 68 $this->addLink(
69 42, 69 42,
70 'Note: I have a big ID but an old date', 70 'Note: I have a big ID but an old date',
71 '?WDWyig', 71 '/shaare/WDWyig',
72 'Used to test bookmarks reordering.', 72 'Used to test bookmarks reordering.',
73 0, 73 0,
74 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'), 74 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
diff --git a/tpl/default/404.html b/tpl/default/404.html
index 09737b4b..7b696e4c 100644
--- a/tpl/default/404.html
+++ b/tpl/default/404.html
@@ -8,7 +8,7 @@
8 {include="page.header"} 8 {include="page.header"}
9<div id="pageError" class="page-error-container center"> 9<div id="pageError" class="page-error-container center">
10 <h2>{'Sorry, nothing to see here.'|t}</h2> 10 <h2>{'Sorry, nothing to see here.'|t}</h2>
11 <img src="img/sad_star.png" alt=""> 11 <img src="{$asset_path}/img/sad_star.png#" alt="">
12 <p>{$error_message}</p> 12 <p>{$error_message}</p>
13</div> 13</div>
14{include="page.footer"} 14{include="page.footer"}
diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html
index b4b4a0ec..67d3ebd1 100644
--- a/tpl/default/addlink.html
+++ b/tpl/default/addlink.html
@@ -9,7 +9,7 @@
9 <div class="pure-u-lg-1-3 pure-u-1-24"></div> 9 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> 10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
11 <h2 class="window-title">{"Shaare a new link"|t}</h2> 11 <h2 class="window-title">{"Shaare a new link"|t}</h2>
12 <form method="GET" action="#" name="addform" class="addform"> 12 <form method="GET" action="{$base_path}/admin/shaare" name="addform" class="addform">
13 <div> 13 <div>
14 <label for="shaare">{'URL or leave empty to post a note'|t}</label> 14 <label for="shaare">{'URL or leave empty to post a note'|t}</label>
15 <input type="text" name="post" id="shaare" class="autofocus"> 15 <input type="text" name="post" id="shaare" class="autofocus">
diff --git a/tpl/default/changepassword.html b/tpl/default/changepassword.html
index ab579433..736774f3 100644
--- a/tpl/default/changepassword.html
+++ b/tpl/default/changepassword.html
@@ -9,7 +9,7 @@
9 <div class="pure-u-lg-1-3 pure-u-1-24"></div> 9 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> 10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
11 <h2 class="window-title">{"Change password"|t}</h2> 11 <h2 class="window-title">{"Change password"|t}</h2>
12 <form method="POST" action="#" name="changepasswordform" id="changepasswordform"> 12 <form method="POST" action="{$base_path}/admin/password" name="changepasswordform" id="changepasswordform">
13 <div> 13 <div>
14 <input type="password" name="oldpassword" aria-label="{'Current password'|t}" placeholder="{'Current password'|t}" class="autofocus"> 14 <input type="password" name="oldpassword" aria-label="{'Current password'|t}" placeholder="{'Current password'|t}" class="autofocus">
15 </div> 15 </div>
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index ec6e0b46..16c55896 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -9,7 +9,7 @@
9 <div class="pure-u-lg-1-3 pure-u-1-24"></div> 9 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> 10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
11 <h2 class="window-title">{"Manage tags"|t}</h2> 11 <h2 class="window-title">{"Manage tags"|t}</h2>
12 <form method="POST" action="#" name="changetag" id="changetag"> 12 <form method="POST" action="{$base_path}/admin/tags" name="changetag" id="changetag">
13 <div> 13 <div>
14 <input type="text" name="fromtag" aria-label="{'Tag'|t}" placeholder="{'Tag'|t}" value="{$fromtag}" 14 <input type="text" name="fromtag" aria-label="{'Tag'|t}" placeholder="{'Tag'|t}" value="{$fromtag}"
15 list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1"> 15 list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1">
@@ -32,7 +32,7 @@
32 </div> 32 </div>
33 </form> 33 </form>
34 34
35 <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p> 35 <p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p>
36 </div> 36 </div>
37</div> 37</div>
38{include="page.footer"} 38{include="page.footer"}
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index 8b75900d..bb2564af 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -11,7 +11,7 @@
11{$ratioInput='7-12'} 11{$ratioInput='7-12'}
12{$ratioInputMobile='1-8'} 12{$ratioInputMobile='1-8'}
13 13
14<form method="POST" action="#" name="configform" id="configform"> 14<form method="POST" action="{$base_path}/admin/configure" name="configform" id="configform">
15 <div class="pure-g"> 15 <div class="pure-g">
16 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 16 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
17 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete"> 17 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
@@ -35,7 +35,7 @@
35 <div class="form-label"> 35 <div class="form-label">
36 <label for="titleLink"> 36 <label for="titleLink">
37 <span class="label-name">{'Home link'|t}</span><br> 37 <span class="label-name">{'Home link'|t}</span><br>
38 <span class="label-desc">{'Default value'|t}: ?</span> 38 <span class="label-desc">{'Default value'|t}: {$base_path}/</span>
39 </label> 39 </label>
40 </div> 40 </div>
41 </div> 41 </div>
@@ -289,7 +289,7 @@
289 {if="! $gd_enabled"} 289 {if="! $gd_enabled"}
290 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t} 290 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
291 {elseif="$thumbnails_enabled"} 291 {elseif="$thumbnails_enabled"}
292 <a href="?do=thumbs_update">{'Synchronize thumbnails'|t}</a> 292 <a href="{$base_path}/admin/thumbnails">{'Synchronize thumbnails'|t}</a>
293 {/if} 293 {/if}
294 </span> 294 </span>
295 </label> 295 </label>
diff --git a/tpl/default/daily.html b/tpl/default/daily.html
index 6b5103a4..3ab8053f 100644
--- a/tpl/default/daily.html
+++ b/tpl/default/daily.html
@@ -11,7 +11,7 @@
11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily"> 11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily">
12 <h2 class="window-title"> 12 <h2 class="window-title">
13 {'The Daily Shaarli'|t} 13 {'The Daily Shaarli'|t}
14 <a href="?do=dailyrss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a> 14 <a href="{$base_path}/daily-rss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a>
15 </h2> 15 </h2>
16 16
17 <div id="plugin_zone_start_daily" class="plugin_zone"> 17 <div id="plugin_zone_start_daily" class="plugin_zone">
@@ -25,7 +25,7 @@
25 <div class="pure-g"> 25 <div class="pure-g">
26 <div class="pure-u-lg-1-3 pure-u-1 center"> 26 <div class="pure-u-lg-1-3 pure-u-1 center">
27 {if="$previousday"} 27 {if="$previousday"}
28 <a href="?do=daily&amp;day={$previousday}"> 28 <a href="{$base_path}/daily?day={$previousday}">
29 <i class="fa fa-arrow-left"></i> 29 <i class="fa fa-arrow-left"></i>
30 {'Previous day'|t} 30 {'Previous day'|t}
31 </a> 31 </a>
@@ -36,7 +36,7 @@
36 </div> 36 </div>
37 <div class="pure-u-lg-1-3 pure-u-1 center"> 37 <div class="pure-u-lg-1-3 pure-u-1 center">
38 {if="$nextday"} 38 {if="$nextday"}
39 <a href="?do=daily&amp;day={$nextday}"> 39 <a href="{$base_path}/daily?day={$nextday}">
40 {'Next day'|t} 40 {'Next day'|t}
41 <i class="fa fa-arrow-right"></i> 41 <i class="fa fa-arrow-right"></i>
42 </a> 42 </a>
@@ -69,7 +69,7 @@
69 {$link=$value} 69 {$link=$value}
70 <div class="daily-entry"> 70 <div class="daily-entry">
71 <div class="daily-entry-title center"> 71 <div class="daily-entry-title center">
72 <a href="?{$link.shorturl}" title="{'Permalink'|t}"> 72 <a href="{$base_path}/?{$link.shorturl}" title="{'Permalink'|t}">
73 <i class="fa fa-link"></i> 73 <i class="fa fa-link"></i>
74 </a> 74 </a>
75 <a href="{$link.real_url}">{$link.title}</a> 75 <a href="{$link.real_url}">{$link.title}</a>
@@ -85,7 +85,7 @@
85 {if="$link.tags"} 85 {if="$link.tags"}
86 <div class="daily-entry-tags center"> 86 <div class="daily-entry-tags center">
87 {loop="link.taglist"} 87 {loop="link.taglist"}
88 <span class="label label-tag" title="Add tag"> 88 <span class="label label-tag">
89 {$value} 89 {$value}
90 </span> 90 </span>
91 {/loop} 91 {/loop}
@@ -116,7 +116,7 @@
116 </div> 116 </div>
117</div> 117</div>
118{include="page.footer"} 118{include="page.footer"}
119<script src="js/thumbnails.min.js?v={$version_hash}"></script> 119<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
120</body> 120</body>
121</html> 121</html>
122 122
diff --git a/tpl/default/dailyrss.html b/tpl/default/dailyrss.html
index f589b06e..d40d9496 100644
--- a/tpl/default/dailyrss.html
+++ b/tpl/default/dailyrss.html
@@ -1,16 +1,32 @@
1<item> 1<?xml version="1.0" encoding="UTF-8"?>
2 <title>{$title} - {function="strftime('%A %e %B %Y', $daydate)"}</title> 2<rss version="2.0">
3 <guid>{$absurl}</guid> 3 <channel>
4 <link>{$absurl}</link> 4 <title>Daily - {$title}</title>
5 <pubDate>{$rssdate}</pubDate> 5 <link>{$index_url}</link>
6 <description><![CDATA[ 6 <description>Daily shaared bookmarks</description>
7 {loop="links"} 7 <language>{$language}</language>
8 <h3><a href="{$value.url}">{$value.title}</a></h3> 8 <copyright>{$index_url}</copyright>
9 <small>{if="!$hide_timestamps"}{function="strftime('%c', $value.timestamp)"} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> 9 <generator>Shaarli</generator>
10 {$value.url}</small><br> 10
11 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br> 11 {loop="$days"}
12 {if="$value.description"}{$value.formatedDescription}{/if} 12 <item>
13 <br><br><hr> 13 <title>{$value.date_human} - {$title}</title>
14 {/loop} 14 <guid>{$value.absolute_url}</guid>
15 ]]></description> 15 <link>{$value.absolute_url}</link>
16</item> 16 <pubDate>{$value.date_rss}</pubDate>
17 <description><![CDATA[
18 {loop="$value.links"}
19 <h3><a href="{$value.url}">{$value.title}</a></h3>
20 <small>
21 {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br>
22 {$value.url}
23 </small><br>
24 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
25 {if="$value.description"}{$value.description}{/if}
26 <br><br><hr>
27 {/loop}
28 ]]></description>
29 </item>
30 {/loop}
31 </channel>
32</rss><!-- Cached version of {$page_url} -->
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html
index d16059a3..568545bd 100644
--- a/tpl/default/editlink.html
+++ b/tpl/default/editlink.html
@@ -7,7 +7,11 @@
7 {include="page.header"} 7 {include="page.header"}
8 <div id="editlinkform" class="edit-link-container" class="pure-g"> 8 <div id="editlinkform" class="edit-link-container" class="pure-g">
9 <div class="pure-u-lg-1-5 pure-u-1-24"></div> 9 <div class="pure-u-lg-1-5 pure-u-1-24"></div>
10 <form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"> 10 <form method="post"
11 name="linkform"
12 action="{$base_path}/admin/shaare"
13 class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"
14 >
11 <h2 class="window-title"> 15 <h2 class="window-title">
12 {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} 16 {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
13 </h2> 17 </h2>
@@ -69,7 +73,7 @@
69 <input type="submit" name="save_edit" class="" id="button-save-edit" 73 <input type="submit" name="save_edit" class="" id="button-save-edit"
70 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}"> 74 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}">
71 {if="!$link_is_new"} 75 {if="!$link_is_new"}
72 <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}" 76 <a href="{$base_path}/admin/shaare/delete?id={$link.id}&amp;token={$token}"
73 title="" name="delete_link" class="button button-red confirm-delete"> 77 title="" name="delete_link" class="button button-red confirm-delete">
74 {'Delete'|t} 78 {'Delete'|t}
75 </a> 79 </a>
@@ -77,6 +81,7 @@
77 </div> 81 </div>
78 82
79 <input type="hidden" name="token" value="{$token}"> 83 <input type="hidden" name="token" value="{$token}">
84 <input type="hidden" name="source" value="{$source}">
80 {if="$http_referer"} 85 {if="$http_referer"}
81 <input type="hidden" name="returnurl" value="{$http_referer}"> 86 <input type="hidden" name="returnurl" value="{$http_referer}">
82 {/if} 87 {/if}
diff --git a/tpl/default/error.html b/tpl/default/error.html
index ef1dfd73..c3e0c3c1 100644
--- a/tpl/default/error.html
+++ b/tpl/default/error.html
@@ -15,7 +15,7 @@
15 </pre> 15 </pre>
16 {/if} 16 {/if}
17 17
18 <img src="img/sad_star.png" alt=""> 18 <img src="{$asset_path}/img/sad_star.png#" alt="">
19</div> 19</div>
20{include="page.footer"} 20{include="page.footer"}
21</body> 21</body>
diff --git a/tpl/default/export.html b/tpl/default/export.html
index 99c01b11..c9c92943 100644
--- a/tpl/default/export.html
+++ b/tpl/default/export.html
@@ -6,14 +6,13 @@
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8 8
9<form method="GET" action="#" name="exportform" id="exportform"> 9<form method="POST" action="{$base_path}/admin/export" name="exportform" id="exportform">
10 <div class="pure-g"> 10 <div class="pure-g">
11 <div class="pure-u-lg-1-4 pure-u-1-24"></div> 11 <div class="pure-u-lg-1-4 pure-u-1-24"></div>
12 <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete"> 12 <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete">
13 <div> 13 <div>
14 <h2 class="window-title">{"Export Database"|t}</h2> 14 <h2 class="window-title">{"Export Database"|t}</h2>
15 </div> 15 </div>
16 <input type="hidden" name="do" value="export">
17 <input type="hidden" name="token" value="{$token}"> 16 <input type="hidden" name="token" value="{$token}">
18 17
19 <div class="pure-g"> 18 <div class="pure-g">
diff --git a/tpl/default/feed.atom.html b/tpl/default/feed.atom.html
index bcfa7012..dd58bd1e 100644
--- a/tpl/default/feed.atom.html
+++ b/tpl/default/feed.atom.html
@@ -6,6 +6,8 @@
6 <updated>{$last_update}</updated> 6 <updated>{$last_update}</updated>
7 {/if} 7 {/if}
8 <link rel="self" href="{$self_link}#" /> 8 <link rel="self" href="{$self_link}#" />
9 <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
10 title="Shaarli search - {$shaarlititle}" />
9 {loop="$plugins_feed_header"} 11 {loop="$plugins_feed_header"}
10 {$value} 12 {$value}
11 {/loop} 13 {/loop}
diff --git a/tpl/default/feed.rss.html b/tpl/default/feed.rss.html
index 66d9a869..85cec7f3 100644
--- a/tpl/default/feed.rss.html
+++ b/tpl/default/feed.rss.html
@@ -7,7 +7,9 @@
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>
10 <atom:link rel="self" href="{$self_link}" /> 10 <atom:link rel="self" href="{$self_link}" />
11 <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
12 title="Shaarli search - {$shaarlititle}" />
11 {loop="$plugins_feed_header"} 13 {loop="$plugins_feed_header"}
12 {$value} 14 {$value}
13 {/loop} 15 {/loop}
diff --git a/tpl/default/import.html b/tpl/default/import.html
index c41afcdb..156de71f 100644
--- a/tpl/default/import.html
+++ b/tpl/default/import.html
@@ -6,7 +6,7 @@
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8 8
9<form method="POST" action="?do=import" enctype="multipart/form-data" name="uploadform" id="uploadform"> 9<form method="POST" action="{$base_path}/admin/import" enctype="multipart/form-data" name="uploadform" id="uploadform">
10 <div class="pure-g"> 10 <div class="pure-g">
11 <div class="pure-u-lg-1-4 pure-u-1-24"></div> 11 <div class="pure-u-lg-1-4 pure-u-1-24"></div>
12 <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete"> 12 <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete">
diff --git a/tpl/default/includes.html b/tpl/default/includes.html
index 3820a4f7..227f9b52 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -3,21 +3,22 @@
3<meta name="format-detection" content="telephone=no" /> 3<meta name="format-detection" content="telephone=no" />
4<meta name="viewport" content="width=device-width, initial-scale=1"> 4<meta name="viewport" content="width=device-width, initial-scale=1">
5<meta name="referrer" content="same-origin"> 5<meta name="referrer" content="same-origin">
6<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" /> 6<link rel="alternate" type="application/atom+xml" href="{$feedurl}feed/atom?{$searchcrits}#" title="ATOM Feed" />
7<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" /> 7<link rel="alternate" type="application/rss+xml" href="{$feedurl}feed/rss?{$searchcrits}#" title="RSS Feed" />
8<link href="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="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="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="$formatter==='markdown'"}
12 <link type="text/css" rel="stylesheet" href="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="{$value}?v={$version_hash}#"/> 15 <link type="text/css" rel="stylesheet" href="{$base_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="data/user.css#" /> 18 <link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />
19{/if} 19{/if}
20<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/> 20<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
21 title="Shaarli search - {$shaarlititle}" />
21{if="! empty($links) && count($links) === 1"} 22{if="! empty($links) && count($links) === 1"}
22 {$link=reset($links)} 23 {$link=reset($links)}
23 <meta property="og:title" content="{$link.title}" /> 24 <meta property="og:title" content="{$link.title}" />
diff --git a/tpl/default/install.html b/tpl/default/install.html
index c6f501f0..a506a2eb 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -10,7 +10,7 @@
10{$ratioLabelMobile='7-8'} 10{$ratioLabelMobile='7-8'}
11{$ratioInputMobile='1-8'} 11{$ratioInputMobile='1-8'}
12 12
13<form method="POST" action="#" name="installform" id="installform"> 13<form method="POST" action="{$base_path}/install" name="installform" id="installform">
14<div class="pure-g"> 14<div class="pure-g">
15 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 15 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
16 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete"> 16 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index ffc236c7..c7617b22 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -94,7 +94,9 @@
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}">
97 <a href="?removetag={function="urlencode($value)"}" aria-label="{'Remove tag'|t}">{$value}<span class="remove"><i class="fa fa-times" aria-hidden="true"></i></span></a> 97 <a href="{$base_path}/remove-tag/{function="urlencode($value)"}" aria-label="{'Remove tag'|t}">
98 {$value}<span class="remove"><i class="fa fa-times" aria-hidden="true"></i></span>
99 </a>
98 </span> 100 </span>
99 {/loop} 101 {/loop}
100 {/if} 102 {/if}
@@ -138,7 +140,7 @@
138 <div class="thumbnail"> 140 <div class="thumbnail">
139 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 141 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
140 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1"> 142 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1">
141 <img data-src="{$value.thumbnail}#" class="b-lazy" 143 <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy"
142 src="" 144 src=""
143 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 145 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
144 </a> 146 </a>
@@ -181,7 +183,7 @@
181 {$tag_counter=count($value.taglist)} 183 {$tag_counter=count($value.taglist)}
182 {loop="value.taglist"} 184 {loop="value.taglist"}
183 <span class="label label-tag" title="{$strAddTag}"> 185 <span class="label label-tag" title="{$strAddTag}">
184 <a href="?addtag={$value|urlencode}">{$value}</a> 186 <a href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a>
185 </span> 187 </span>
186 {if="$tag_counter - 1 != $counter"}&middot;{/if} 188 {if="$tag_counter - 1 != $counter"}&middot;{/if}
187 {/loop} 189 {/loop}
@@ -196,16 +198,16 @@
196 <input type="checkbox" class="link-checkbox" value="{$value.id}"> 198 <input type="checkbox" class="link-checkbox" value="{$value.id}">
197 </span> 199 </span>
198 <span class="linklist-item-infos-controls-item ctrl-edit"> 200 <span class="linklist-item-infos-controls-item ctrl-edit">
199 <a href="?edit_link={$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a> 201 <a href="{$base_path}/admin/shaare/{$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a>
200 </span> 202 </span>
201 <span class="linklist-item-infos-controls-item ctrl-delete"> 203 <span class="linklist-item-infos-controls-item ctrl-delete">
202 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" aria-label="{$strDelete}" 204 <a href="{$base_path}/admin/shaare/delete?id={$value.id}&amp;token={$token}" aria-label="{$strDelete}"
203 title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete"> 205 title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
204 <i class="fa fa-trash" aria-hidden="true"></i> 206 <i class="fa fa-trash" aria-hidden="true"></i>
205 </a> 207 </a>
206 </span> 208 </span>
207 <span class="linklist-item-infos-controls-item ctrl-pin"> 209 <span class="linklist-item-infos-controls-item ctrl-pin">
208 <a href="?do=pin&amp;id={$value.id}&amp;token={$token}" 210 <a href="{$base_path}/admin/shaare/{$value.id}/pin?token={$token}"
209 title="{$strToggleSticky}" aria-label="{$strToggleSticky}" class="pin-link {if="$value.sticky"}pinned-link{/if} pure-u-0 pure-u-lg-visible"> 211 title="{$strToggleSticky}" aria-label="{$strToggleSticky}" class="pin-link {if="$value.sticky"}pinned-link{/if} pure-u-0 pure-u-lg-visible">
210 <i class="fa fa-thumb-tack" aria-hidden="true"></i> 212 <i class="fa fa-thumb-tack" aria-hidden="true"></i>
211 </a> 213 </a>
@@ -222,7 +224,7 @@
222 </div> 224 </div>
223 {/if} 225 {/if}
224 {/if} 226 {/if}
225 <a href="?{$value.shorturl}" title="{$strPermalink}"> 227 <a href="{$base_path}/shaare/{$value.shorturl}" title="{$strPermalink}">
226 {if="!$hide_timestamps || $is_logged_in"} 228 {if="!$hide_timestamps || $is_logged_in"}
227 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink} 229 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
228 <span class="linkdate" title="{$updated}"> 230 <span class="linkdate" title="{$updated}">
@@ -265,12 +267,12 @@
265 {/if} 267 {/if}
266 {if="$is_logged_in"} 268 {if="$is_logged_in"}
267 &middot; 269 &middot;
268 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" aria-label="{$strDelete}" 270 <a href="{$base_path}/admin/shaare/delete?id={$value.id}&amp;token={$token}" aria-label="{$strDelete}"
269 title="{$strDelete}" class="delete-link confirm-delete"> 271 title="{$strDelete}" class="delete-link confirm-delete">
270 <i class="fa fa-trash" aria-hidden="true"></i> 272 <i class="fa fa-trash" aria-hidden="true"></i>
271 </a> 273 </a>
272 &middot; 274 &middot;
273 <a href="?edit_link={$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a> 275 <a href="{$base_path}/admin/shaare/{$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a>
274 {/if} 276 {/if}
275 </div> 277 </div>
276 </div> 278 </div>
@@ -295,6 +297,6 @@
295</div> 297</div>
296 298
297{include="page.footer"} 299{include="page.footer"}
298<script src="js/thumbnails.min.js?v={$version_hash}"></script> 300<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
299</body> 301</body>
300</html> 302</html>
diff --git a/tpl/default/linklist.paging.html b/tpl/default/linklist.paging.html
index 68947f92..7b320eaf 100644
--- a/tpl/default/linklist.paging.html
+++ b/tpl/default/linklist.paging.html
@@ -6,14 +6,14 @@
6 {'Filters'|t} 6 {'Filters'|t}
7 </span> 7 </span>
8 {if="$is_logged_in"} 8 {if="$is_logged_in"}
9 <a href="?visibility=private" aria-label="{'Only display private links'|t}" title="{'Only display private links'|t}" 9 <a href="{$base_path}/admin/visibility/private" aria-label="{'Only display private links'|t}" title="{'Only display private links'|t}"
10 class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}" 10 class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}"
11 ><i class="fa fa-user-secret" aria-hidden="true"></i></a> 11 ><i class="fa fa-user-secret" aria-hidden="true"></i></a>
12 <a href="?visibility=public" aria-label="{'Only display public links'|t}" title="{'Only display public links'|t}" 12 <a href="{$base_path}/admin/visibility/public" aria-label="{'Only display public links'|t}" title="{'Only display public links'|t}"
13 class="{if="$visibility==='public'"}filter-on{else}filter-off{/if}" 13 class="{if="$visibility==='public'"}filter-on{else}filter-off{/if}"
14 ><i class="fa fa-globe" aria-hidden="true"></i></a> 14 ><i class="fa fa-globe" aria-hidden="true"></i></a>
15 {/if} 15 {/if}
16 <a href="?untaggedonly" aria-label="{'Filter untagged links'|t}" title="{'Filter untagged links'|t}" 16 <a href="{$base_path}/untagged-only" aria-label="{'Filter untagged links'|t}" title="{'Filter untagged links'|t}"
17 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if} 17 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
18 ><i class="fa fa-tag" aria-hidden="true"></i></a> 18 ><i class="fa fa-tag" aria-hidden="true"></i></a>
19 <a href="#" aria-label="{'Select all'|t}" title="{'Select all'|t}" 19 <a href="#" aria-label="{'Select all'|t}" title="{'Select all'|t}"
@@ -53,11 +53,11 @@
53 53
54 <div class="linksperpage pure-u-1-3"> 54 <div class="linksperpage pure-u-1-3">
55 <div class="pure-u-0 pure-u-lg-visible">{'Links per page'|t}</div> 55 <div class="pure-u-0 pure-u-lg-visible">{'Links per page'|t}</div>
56 <a href="?linksperpage=20">20</a> 56 <a href="{$base_path}/links-per-page?nb=20">20</a>
57 <a href="?linksperpage=50">50</a> 57 <a href="{$base_path}/links-per-page?nb=50">50</a>
58 <a href="?linksperpage=100">100</a> 58 <a href="{$base_path}/links-per-page?nb=100">100</a>
59 <form method="GET" class="pure-u-0 pure-u-lg-visible"> 59 <form method="GET" class="pure-u-0 pure-u-lg-visible" action="{$base_path}/links-per-page">
60 <input type="text" name="linksperpage" placeholder="133"> 60 <input type="text" name="nb" placeholder="133">
61 </form> 61 </form>
62 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" aria-label="{'Fold all'|t}" title="{'Fold all'|t}"> 62 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" aria-label="{'Fold all'|t}" title="{'Fold all'|t}">
63 <i class="fa fa-chevron-up" aria-hidden="true"></i> 63 <i class="fa fa-chevron-up" aria-hidden="true"></i>
diff --git a/tpl/default/opensearch.html b/tpl/default/opensearch.html
index 3fcc30b7..1c7f279b 100644
--- a/tpl/default/opensearch.html
+++ b/tpl/default/opensearch.html
@@ -3,8 +3,8 @@
3 <ShortName>Shaarli search - {$pagetitle}</ShortName> 3 <ShortName>Shaarli search - {$pagetitle}</ShortName>
4 <Description>Shaarli search - {$pagetitle}</Description> 4 <Description>Shaarli search - {$pagetitle}</Description>
5 <Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" /> 5 <Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" />
6 <Url type="application/atom+xml" template="{$serverurl}?do=atom&amp;searchterm={searchTerms}"/> 6 <Url type="application/atom+xml" template="{$serverurl}feed/atom?searchterm={searchTerms}"/>
7 <Url type="application/rss+xml" template="{$serverurl}?do=rss&amp;searchterm={searchTerms}"/> 7 <Url type="application/rss+xml" template="{$serverurl}feed/rss?searchterm={searchTerms}"/>
8 <InputEncoding>UTF-8</InputEncoding> 8 <InputEncoding>UTF-8</InputEncoding>
9 <Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer> 9 <Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer>
10 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE 10 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index 0899826b..51bdb2f0 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="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a> 13 <a href="{$base_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}
@@ -25,7 +25,7 @@
25{/loop} 25{/loop}
26 26
27{loop="$plugins_footer.js_files"} 27{loop="$plugins_footer.js_files"}
28 <script src="{$value}#"></script> 28 <script src="{$base_path}/{$value}#"></script>
29{/loop} 29{/loop}
30 30
31<div id="js-translations" class="hidden"> 31<div id="js-translations" class="hidden">
@@ -39,4 +39,5 @@
39 </span> 39 </span>
40</div> 40</div>
41 41
42<script src="js/shaarli.min.js?v={$version_hash}"></script> 42<input type="hidden" name="js_base_path" value="{$base_path}" />
43<script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script>
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html
index 82f8ebf1..a71464c7 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -21,24 +21,24 @@
21 </li> 21 </li>
22 {if="$is_logged_in || $openshaarli"} 22 {if="$is_logged_in || $openshaarli"}
23 <li class="pure-menu-item"> 23 <li class="pure-menu-item">
24 <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare"> 24 <a href="{$base_path}/admin/add-shaare" class="pure-menu-link" id="shaarli-menu-shaare">
25 <i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t} 25 <i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t}
26 </a> 26 </a>
27 </li> 27 </li>
28 <li class="pure-menu-item" id="shaarli-menu-tools"> 28 <li class="pure-menu-item" id="shaarli-menu-tools">
29 <a href="?do=tools" class="pure-menu-link">{'Tools'|t}</a> 29 <a href="{$base_path}/admin/tools" class="pure-menu-link">{'Tools'|t}</a>
30 </li> 30 </li>
31 {/if} 31 {/if}
32 <li class="pure-menu-item" id="shaarli-menu-tags"> 32 <li class="pure-menu-item" id="shaarli-menu-tags">
33 <a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a> 33 <a href="{$base_path}/tags/cloud" class="pure-menu-link">{'Tag cloud'|t}</a>
34 </li> 34 </li>
35 {if="$thumbnails_enabled"} 35 {if="$thumbnails_enabled"}
36 <li class="pure-menu-item" id="shaarli-menu-picwall"> 36 <li class="pure-menu-item" id="shaarli-menu-picwall">
37 <a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a> 37 <a href="{$base_path}/picture-wall?{function="ltrim($searchcrits, '&')"}" class="pure-menu-link">{'Picture wall'|t}</a>
38 </li> 38 </li>
39 {/if} 39 {/if}
40 <li class="pure-menu-item" id="shaarli-menu-daily"> 40 <li class="pure-menu-item" id="shaarli-menu-daily">
41 <a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a> 41 <a href="{$base_path}/daily" class="pure-menu-link">{'Daily'|t}</a>
42 </li> 42 </li>
43 {loop="$plugins_header.buttons_toolbar"} 43 {loop="$plugins_header.buttons_toolbar"}
44 <li class="pure-menu-item shaarli-menu-plugin"> 44 <li class="pure-menu-item shaarli-menu-plugin">
@@ -52,15 +52,15 @@
52 </li> 52 </li>
53 {/loop} 53 {/loop}
54 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss"> 54 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
55 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a> 55 <a href="{$base_path}/feed/{$feed_type}?{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
56 </li> 56 </li>
57 {if="$is_logged_in"} 57 {if="$is_logged_in"}
58 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout"> 58 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
59 <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a> 59 <a href="{$base_path}/admin/logout" class="pure-menu-link">{'Logout'|t}</a>
60 </li> 60 </li>
61 {else} 61 {else}
62 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login"> 62 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login">
63 <a href="/login" class="pure-menu-link">{'Login'|t}</a> 63 <a href="{$base_path}/login" class="pure-menu-link">{'Login'|t}</a>
64 </li> 64 </li>
65 {/if} 65 {/if}
66 </ul> 66 </ul>
@@ -74,13 +74,13 @@
74 </a> 74 </a>
75 </li> 75 </li>
76 <li class="pure-menu-item" id="shaarli-menu-desktop-rss"> 76 <li class="pure-menu-item" id="shaarli-menu-desktop-rss">
77 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}" aria-label="{'RSS Feed'|t}"> 77 <a href="{$base_path}/feed/{$feed_type}?{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}" aria-label="{'RSS Feed'|t}">
78 <i class="fa fa-rss" aria-hidden="true"></i> 78 <i class="fa fa-rss" aria-hidden="true"></i>
79 </a> 79 </a>
80 </li> 80 </li>
81 {if="!$is_logged_in"} 81 {if="!$is_logged_in"}
82 <li class="pure-menu-item" id="shaarli-menu-desktop-login"> 82 <li class="pure-menu-item" id="shaarli-menu-desktop-login">
83 <a href="/login" class="pure-menu-link" 83 <a href="{$base_path}/login" class="pure-menu-link"
84 data-open-id="header-login-form" 84 data-open-id="header-login-form"
85 id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}"> 85 id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}">
86 <i class="fa fa-user" aria-hidden="true"></i> 86 <i class="fa fa-user" aria-hidden="true"></i>
@@ -88,7 +88,7 @@
88 </li> 88 </li>
89 {else} 89 {else}
90 <li class="pure-menu-item" id="shaarli-menu-desktop-logout"> 90 <li class="pure-menu-item" id="shaarli-menu-desktop-logout">
91 <a href="?do=logout" class="pure-menu-link" aria-label="{'Logout'|t}" title="{'Logout'|t}"> 91 <a href="{$base_path}/admin/logout" class="pure-menu-link" aria-label="{'Logout'|t}" title="{'Logout'|t}">
92 <i class="fa fa-sign-out" aria-hidden="true"></i> 92 <i class="fa fa-sign-out" aria-hidden="true"></i>
93 </a> 93 </a>
94 </li> 94 </li>
@@ -101,7 +101,7 @@
101 101
102<main id="content" class="container" role="main"> 102<main id="content" class="container" role="main">
103 <div id="search" class="subheader-form searchform-block header-search"> 103 <div id="search" class="subheader-form searchform-block header-search">
104 <form method="GET" class="pure-form searchform" name="searchform"> 104 <form method="GET" class="pure-form searchform" name="searchform" action="{$base_path}/">
105 <input type="text" id="searchform_value" name="searchterm" aria-label="{'Search text'|t}" placeholder="{'Search text'|t}" 105 <input type="text" id="searchform_value" name="searchterm" aria-label="{'Search text'|t}" placeholder="{'Search text'|t}"
106 {if="!empty($search_term)"} 106 {if="!empty($search_term)"}
107 value="{$search_term}" 107 value="{$search_term}"
@@ -184,8 +184,22 @@
184 </div> 184 </div>
185{/if} 185{/if}
186 186
187{if="!empty($global_warnings) && $is_logged_in"} 187{if="!empty($global_errors)"}
188 <div class="pure-g pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert"> 188 <div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
189 <div class="pure-u-2-24"></div>
190 <div class="pure-u-20-24">
191 {loop="$global_errors"}
192 <p>{$value}</p>
193 {/loop}
194 </div>
195 <div class="pure-u-2-24">
196 <i class="fa fa-times pure-alert-close"></i>
197 </div>
198 </div>
199{/if}
200
201{if="!empty($global_warnings)"}
202 <div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
189 <div class="pure-u-2-24"></div> 203 <div class="pure-u-2-24"></div>
190 <div class="pure-u-20-24"> 204 <div class="pure-u-20-24">
191 {loop="global_warnings"} 205 {loop="global_warnings"}
@@ -198,4 +212,18 @@
198 </div> 212 </div>
199{/if} 213{/if}
200 214
215{if="!empty($global_successes)"}
216 <div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert">
217 <div class="pure-u-2-24"></div>
218 <div class="pure-u-20-24">
219 {loop="$global_successes"}
220 <p>{$value}</p>
221 {/loop}
222 </div>
223 <div class="pure-u-2-24">
224 <i class="fa fa-times pure-alert-close"></i>
225 </div>
226 </div>
227{/if}
228
201 <div class="clear"></div> 229 <div class="clear"></div>
diff --git a/tpl/default/picwall.html b/tpl/default/picwall.html
index 73359949..b7a56c89 100644
--- a/tpl/default/picwall.html
+++ b/tpl/default/picwall.html
@@ -5,61 +5,55 @@
5</head> 5</head>
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8{if="!$thumbnails_enabled"} 8
9<div class="pure-g pure-alert pure-alert-warning page-single-alert"> 9{if="count($linksToDisplay)===0 && $is_logged_in"}
10 <div class="pure-u-1 center"> 10 <div class="pure-g pure-alert pure-alert-warning page-single-alert">
11 {'Picture wall unavailable (thumbnails are disabled).'|t} 11 <div class="pure-u-1 center">
12 </div> 12 {'There is no cached thumbnail.'|t}
13</div> 13 <a href="{$base_path}/admin/thumbnails">{'Try to synchronize them.'|t}</a>
14{else}
15 {if="count($linksToDisplay)===0 && $is_logged_in"}
16 <div class="pure-g pure-alert pure-alert-warning page-single-alert">
17 <div class="pure-u-1 center">
18 {'There is no cached thumbnail. Try to <a href="?do=thumbs_update">synchronize them</a>.'|t}
19 </div>
20 </div> 14 </div>
21 {/if} 15 </div>
16{/if}
22 17
23 <div class="pure-g"> 18<div class="pure-g">
24 <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>
25 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor"> 20 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
26 {$countPics=count($linksToDisplay)} 21 {$countPics=count($linksToDisplay)}
27 <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2> 22 <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
28 23
29 <div id="plugin_zone_start_picwall" class="plugin_zone"> 24 <div id="plugin_zone_start_picwall" class="plugin_zone">
30 {loop="$plugin_start_zone"} 25 {loop="$plugin_start_zone"}
31 {$value} 26 {$value}
32 {/loop} 27 {/loop}
33 </div> 28 </div>
34 29
35 <div id="picwall-container" class="picwall-container" role="list"> 30 <div id="picwall-container" class="picwall-container" role="list">
36 {loop="$linksToDisplay"} 31 {loop="$linksToDisplay"}
37 <div class="picwall-pictureframe" role="listitem"> 32 <div class="picwall-pictureframe" role="listitem">
38 {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}
39 <img data-src="{$value.thumbnail}#" class="b-lazy" 34 <img data-src="{$value.thumbnail}#" class="b-lazy"
40 src="" 35 src=""
41 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 36 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
42 <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>
43 {loop="$value.picwall_plugin"} 38 {loop="$value.picwall_plugin"}
44 {$value} 39 {$value}
45 {/loop} 40 {/loop}
46 </div> 41 </div>
47 {/loop} 42 {/loop}
48 <div class="clear"></div> 43 <div class="clear"></div>
49 </div> 44 </div>
50 45
51 <div id="plugin_zone_end_picwall" class="plugin_zone"> 46 <div id="plugin_zone_end_picwall" class="plugin_zone">
52 {loop="$plugin_end_zone"} 47 {loop="$plugin_end_zone"}
53 {$value} 48 {$value}
54 {/loop} 49 {/loop}
55 </div>
56 </div> 50 </div>
57 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
58 </div> 51 </div>
59{/if} 52 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
53</div>
60 54
61{include="page.footer"} 55{include="page.footer"}
62<script src="js/thumbnails.min.js?v={$version_hash}"></script> 56<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
63</body> 57</body>
64</html> 58</html>
65 59
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index 4bfaa934..05d13556 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -16,7 +16,7 @@
16 <div class="clear"></div> 16 <div class="clear"></div>
17</noscript> 17</noscript>
18 18
19<form method="POST" action="?do=save_pluginadmin" name="pluginform" id="pluginform" class="pluginform-container"> 19<form method="POST" action="{$base_path}/admin/plugins" name="pluginform" id="pluginform" class="pluginform-container">
20 <div class="pure-g"> 20 <div class="pure-g">
21 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 21 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
22 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete"> 22 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
@@ -127,7 +127,7 @@
127 <input type="hidden" name="token" value="{$token}"> 127 <input type="hidden" name="token" value="{$token}">
128</form> 128</form>
129 129
130<form action="?do=save_pluginadmin" method="POST"> 130<form action="{$base_path}/admin/plugins" method="POST">
131 <div class="pure-g"> 131 <div class="pure-g">
132 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 132 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
133 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-light"> 133 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-light">
@@ -173,10 +173,11 @@
173 </section> 173 </section>
174 </div> 174 </div>
175 </div> 175 </div>
176 <input type="hidden" name="token" value="{$token}">
176</form> 177</form>
177 178
178{include="page.footer"} 179{include="page.footer"}
179<script src="js/pluginsadmin.min.js?v={$version_hash}"></script> 180<script src="{$asset_path}/js/pluginsadmin.min.js?v={$version_hash}#"></script>
180 181
181</body> 182</body>
182</html> 183</html>
diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html
index 7839fcca..024882ec 100644
--- a/tpl/default/tag.cloud.html
+++ b/tpl/default/tag.cloud.html
@@ -15,7 +15,7 @@
15 <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2> 15 <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
16 {if="!empty($search_tags)"} 16 {if="!empty($search_tags)"}
17 <p class="center"> 17 <p class="center">
18 <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli"> 18 <a href="{$base_path}/?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
19 {'List all links with those tags'|t} 19 {'List all links with those tags'|t}
20 </a> 20 </a>
21 </p> 21 </p>
@@ -48,8 +48,8 @@
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="?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a 51 <a href="{$base_path}/?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a
52 ><a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a> 52 ><a href="{$base_path}/add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
53 {loop="$value.tag_plugin"} 53 {loop="$value.tag_plugin"}
54 {$value} 54 {$value}
55 {/loop} 55 {/loop}
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html
index d5777465..99ae44d2 100644
--- a/tpl/default/tag.list.html
+++ b/tpl/default/tag.list.html
@@ -15,7 +15,7 @@
15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2> 15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
16 {if="!empty($search_tags)"} 16 {if="!empty($search_tags)"}
17 <p class="center"> 17 <p class="center">
18 <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli"> 18 <a href="{$base_path}/?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
19 {'List all links with those tags'|t} 19 {'List all links with those tags'|t}
20 </a> 20 </a>
21 </p> 21 </p>
@@ -51,13 +51,13 @@
51 <div class="pure-u-1"> 51 <div class="pure-u-1">
52 {if="$is_logged_in===true"} 52 {if="$is_logged_in===true"}
53 <a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>&nbsp;&nbsp; 53 <a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>&nbsp;&nbsp;
54 <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag" aria-label="{'Rename tag'|t}"> 54 <a href="{$base_path}/admin/tags?fromtag={$key|urlencode}" class="rename-tag" aria-label="{'Rename tag'|t}">
55 <i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i> 55 <i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i>
56 </a> 56 </a>
57 {/if} 57 {/if}
58 58
59 <a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a> 59 <a href="{$base_path}/add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
60 <a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a> 60 <a href="{$base_path}/?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a>
61 61
62 {loop="$value.tag_plugin"} 62 {loop="$value.tag_plugin"}
63 {$value} 63 {$value}
diff --git a/tpl/default/tag.sort.html b/tpl/default/tag.sort.html
index d24c9f64..8718b188 100644
--- a/tpl/default/tag.sort.html
+++ b/tpl/default/tag.sort.html
@@ -1,8 +1,8 @@
1<div class="pure-g"> 1<div class="pure-g">
2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort"> 2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
3 {'Sort by:'|t} 3 {'Sort by:'|t}
4 <a href="?do=tagcloud">{'Cloud'|t}</a> &middot; 4 <a href="{$base_path}/tags/cloud">{'Cloud'|t}</a> &middot;
5 <a href="?do=taglist&sort=usage">{'Most used'|t}</a> &middot; 5 <a href="{$base_path}/tags/list?sort=usage">{'Most used'|t}</a> &middot;
6 <a href="?do=taglist&sort=alpha">{'Alphabetical'|t}</a> 6 <a href="{$base_path}/tags/list?sort=alpha">{'Alphabetical'|t}</a>
7 </div> 7 </div>
8</div> \ No newline at end of file 8</div>
diff --git a/tpl/default/thumbnails.html b/tpl/default/thumbnails.html
index 5f9bef08..504644ca 100644
--- a/tpl/default/thumbnails.html
+++ b/tpl/default/thumbnails.html
@@ -43,6 +43,6 @@
43</div> 43</div>
44 44
45{include="page.footer"} 45{include="page.footer"}
46<script src="js/thumbnails_update.min.js?v={$version_hash}"></script> 46<script src="{$asset_path}/js/thumbnails_update.min.js?v={$version_hash}#"></script>
47</body> 47</body>
48</html> 48</html>
diff --git a/tpl/default/tools.html b/tpl/default/tools.html
index 20d0c893..2cb08e38 100644
--- a/tpl/default/tools.html
+++ b/tpl/default/tools.html
@@ -11,35 +11,35 @@
11 <div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light"> 11 <div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light">
12 <h2 class="window-title">{'Settings'|t}</h2> 12 <h2 class="window-title">{'Settings'|t}</h2>
13 <div class="tools-item"> 13 <div class="tools-item">
14 <a href="?do=configure" title="{'Change Shaarli settings: title, timezone, etc.'|t}"> 14 <a href="{$base_path}/admin/configure" title="{'Change Shaarli settings: title, timezone, etc.'|t}">
15 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Configure your Shaarli'|t}</span> 15 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Configure your Shaarli'|t}</span>
16 </a> 16 </a>
17 </div> 17 </div>
18 <div class="tools-item"> 18 <div class="tools-item">
19 <a href="?do=pluginadmin" title="{'Enable, disable and configure plugins'|t}"> 19 <a href="{$base_path}/admin/plugins" title="{'Enable, disable and configure plugins'|t}">
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 {if="!$openshaarli"} 23 {if="!$openshaarli"}
24 <div class="tools-item"> 24 <div class="tools-item">
25 <a href="?do=changepasswd" title="{'Change your password'|t}"> 25 <a href="{$base_path}/admin/password" title="{'Change your password'|t}">
26 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Change password'|t}</span> 26 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Change password'|t}</span>
27 </a> 27 </a>
28 </div> 28 </div>
29 {/if} 29 {/if}
30 <div class="tools-item"> 30 <div class="tools-item">
31 <a href="?do=changetag" title="{'Rename or delete a tag in all links'|t}"> 31 <a href="{$base_path}/admin/tags" title="{'Rename or delete a tag in all links'|t}">
32 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Manage tags'|t}</span> 32 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Manage tags'|t}</span>
33 </a> 33 </a>
34 </div> 34 </div>
35 <div class="tools-item"> 35 <div class="tools-item">
36 <a href="?do=import" 36 <a href="{$base_path}/admin/import"
37 title="{'Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, delicious...)'|t}"> 37 title="{'Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, delicious...)'|t}">
38 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Import links'|t}</span> 38 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Import links'|t}</span>
39 </a> 39 </a>
40 </div> 40 </div>
41 <div class="tools-item"> 41 <div class="tools-item">
42 <a href="?do=export" 42 <a href="{$base_path}/admin/export"
43 title="{'Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)'|t}"> 43 title="{'Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)'|t}">
44 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Export database'|t}</span> 44 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Export database'|t}</span>
45 </a> 45 </a>
@@ -47,7 +47,7 @@
47 47
48 {if="$thumbnails_enabled"} 48 {if="$thumbnails_enabled"}
49 <div class="tools-item"> 49 <div class="tools-item">
50 <a href="?do=thumbs_update" title="{'Synchronize all link thumbnails'|t}"> 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> 51 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
52 </a> 52 </a>
53 </div> 53 </div>
@@ -86,7 +86,7 @@
86 alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}'); 86 alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}');
87 } 87 }
88 window.open( 88 window.open(
89 '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+ 89 '{$pageabsaddr}admin/shaare?post='%20+%20encodeURIComponent(url)+
90 '&amp;title='%20+%20encodeURIComponent(title)+ 90 '&amp;title='%20+%20encodeURIComponent(title)+
91 '&amp;description='%20+%20encodeURIComponent(desc)+ 91 '&amp;description='%20+%20encodeURIComponent(desc)+
92 '&amp;source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1' 92 '&amp;source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1'
diff --git a/tpl/vintage/404.html b/tpl/vintage/404.html
index 53e98e2e..0fef0f08 100644
--- a/tpl/vintage/404.html
+++ b/tpl/vintage/404.html
@@ -10,7 +10,7 @@
10<div class="error-container"> 10<div class="error-container">
11 <h1>404 Not found <small>Oh crap!</small></h1> 11 <h1>404 Not found <small>Oh crap!</small></h1>
12 <p>{$error_message}</p> 12 <p>{$error_message}</p>
13 <p>Would you mind <a href="?">clicking here</a>?</p> 13 <p>Would you mind <a href="{$base_path}/">clicking here</a>?</p>
14</div> 14</div>
15{include="page.footer"} 15{include="page.footer"}
16</body> 16</body>
diff --git a/tpl/vintage/addlink.html b/tpl/vintage/addlink.html
index da50f45e..ade08c7c 100644
--- a/tpl/vintage/addlink.html
+++ b/tpl/vintage/addlink.html
@@ -5,7 +5,7 @@
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="headerform"> 7 <div id="headerform">
8 <form method="GET" action="" name="addform" class="addform"> 8 <form method="GET" action="{$base_path}/admin/shaare" name="addform" class="addform">
9 <input type="text" name="post" class="linkurl"> 9 <input type="text" name="post" class="linkurl">
10 <input type="submit" value="Add link" class="bigbutton"> 10 <input type="submit" value="Add link" class="bigbutton">
11 </form> 11 </form>
diff --git a/tpl/vintage/changepassword.html b/tpl/vintage/changepassword.html
index c40daf9d..7e37b9a3 100644
--- a/tpl/vintage/changepassword.html
+++ b/tpl/vintage/changepassword.html
@@ -4,7 +4,7 @@
4<body onload="document.changepasswordform.oldpassword.focus();"> 4<body onload="document.changepasswordform.oldpassword.focus();">
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <form method="POST" action="#" name="changepasswordform" id="changepasswordform"> 7 <form method="POST" action="{$base_path}/admin/password" name="changepasswordform" id="changepasswordform">
8 Old password: <input type="password" name="oldpassword">&nbsp; &nbsp; 8 Old password: <input type="password" name="oldpassword">&nbsp; &nbsp;
9 New password: <input type="password" name="setpassword"> 9 New password: <input type="password" name="setpassword">
10 <input type="hidden" name="token" value="{$token}"> 10 <input type="hidden" name="token" value="{$token}">
@@ -12,4 +12,4 @@
12</div> 12</div>
13{include="page.footer"} 13{include="page.footer"}
14</body> 14</body>
15</html> \ No newline at end of file 15</html>
diff --git a/tpl/vintage/changetag.html b/tpl/vintage/changetag.html
index 670a8dd7..6ef60252 100644
--- a/tpl/vintage/changetag.html
+++ b/tpl/vintage/changetag.html
@@ -5,7 +5,7 @@
5<body onload="document.changetag.fromtag.focus();"> 5<body onload="document.changetag.fromtag.focus();">
6<div id="pageheader"> 6<div id="pageheader">
7 {include="page.header"} 7 {include="page.header"}
8 <form method="POST" action="" name="changetag" id="changetag"> 8 <form method="POST" action="{$base_path}/admin/tags" name="changetag" id="changetag">
9 <input type="hidden" name="token" value="{$token}"> 9 <input type="hidden" name="token" value="{$token}">
10 <div> 10 <div>
11 <label for="fromtag">Tag:</label> 11 <label for="fromtag">Tag:</label>
diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html
index 53b0cad2..ba4f3f71 100644
--- a/tpl/vintage/configure.html
+++ b/tpl/vintage/configure.html
@@ -4,7 +4,7 @@
4<body onload="document.configform.title.focus();"> 4<body onload="document.configform.title.focus();">
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <form method="POST" action="#" name="configform" id="configform"> 7 <form method="POST" action="{$base_path}/admin/configure" name="configform" id="configform">
8 <input type="hidden" name="token" value="{$token}"> 8 <input type="hidden" name="token" value="{$token}">
9 <table id="configuration_table"> 9 <table id="configuration_table">
10 10
@@ -16,7 +16,7 @@
16 <tr> 16 <tr>
17 <td><b>Home link:</b></td> 17 <td><b>Home link:</b></td>
18 <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label 18 <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
19 for="titleLink">(default value is: ?)</label></td> 19 for="titleLink">(default value is: {$base_path}/)</label></td>
20 </tr> 20 </tr>
21 21
22 <tr> 22 <tr>
@@ -159,7 +159,7 @@
159 {if="! $gd_enabled"} 159 {if="! $gd_enabled"}
160 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t} 160 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
161 {elseif="$thumbnails_enabled"} 161 {elseif="$thumbnails_enabled"}
162 <a href="?do=thumbs_update">{'Synchonize thumbnails'|t}</a> 162 <a href="{$base_path}/admin/thumbnails">{'Synchonize thumbnails'|t}</a>
163 {/if} 163 {/if}
164 </label> 164 </label>
165 </td> 165 </td>
diff --git a/tpl/vintage/daily.html b/tpl/vintage/daily.html
index 00f18e26..74f6cdc7 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="?do=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&amp;day={$previousday}"><b>&lt;</b>Previous day</a>{else}<b>&lt;</b>Previous day{/if}
18 - 18 -
19 {if="$nextday"}<a href="?do=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&amp;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"}
@@ -24,13 +24,13 @@
24 {/loop} 24 {/loop}
25 25
26 <br> 26 <br>
27 <a href="?do=dailyrss" title="1 RSS entry per day"><img src="img/feed-icon-14x14.png" alt="rss_feed">Daily RSS Feed</a> 27 <a href="{$base_path}/daily-rss" title="1 RSS entry per day"><img src="{$asset_path}/img/feed-icon-14x14.png#" alt="rss_feed">Daily RSS Feed</a>
28 </div> 28 </div>
29 29
30 <div class="dailyTitle"> 30 <div class="dailyTitle">
31 <img src="img/floral_left.png" width="51" height="50" class="nomobile" alt="floral_left"> 31 <img src="{$asset_path}/img/floral_left.png#" width="51" height="50" class="nomobile" alt="floral_left">
32 The Daily Shaarli 32 The Daily Shaarli
33 <img src="img/floral_right.png" width="51" height="50" class="nomobile" alt="floral_right"> 33 <img src="{$asset_path}/img/floral_right.png#" width="51" height="50" class="nomobile" alt="floral_right">
34 </div> 34 </div>
35 35
36 <div class="dailyDate"> 36 <div class="dailyDate">
@@ -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="?{$value.shorturl}"> 55 <a href="{$base_path}/?{$value.shorturl}">
56 <img src="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="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a> 61 <a href="{$base_path}/?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
62 </div> 62 </div>
63 {/if} 63 {/if}
64 {if="$link.tags"} 64 {if="$link.tags"}
@@ -101,9 +101,9 @@
101 {$value} 101 {$value}
102 {/loop} 102 {/loop}
103 </div> 103 </div>
104 <div id="closing"><img src="img/squiggle_closing.png" width="66" height="61" alt="-"></div> 104 <div id="closing"><img src="{$asset_path}/img/squiggle_closing.png#" width="66" height="61" alt="-"></div>
105</div> 105</div>
106{include="page.footer"} 106{include="page.footer"}
107<script src="js/thumbnails.min.js?v={$version_hash}"></script> 107<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
108</body> 108</body>
109</html> 109</html>
diff --git a/tpl/vintage/dailyrss.html b/tpl/vintage/dailyrss.html
index f589b06e..ff19bbfb 100644
--- a/tpl/vintage/dailyrss.html
+++ b/tpl/vintage/dailyrss.html
@@ -1,16 +1,32 @@
1<item> 1<?xml version="1.0" encoding="UTF-8"?>
2 <title>{$title} - {function="strftime('%A %e %B %Y', $daydate)"}</title> 2<rss version="2.0">
3 <guid>{$absurl}</guid> 3 <channel>
4 <link>{$absurl}</link> 4 <title>Daily - {$title}</title>
5 <pubDate>{$rssdate}</pubDate> 5 <link>{$index_url}</link>
6 <description><![CDATA[ 6 <description>Daily shaared bookmarks</description>
7 {loop="links"} 7 <language>{$language}</language>
8 <h3><a href="{$value.url}">{$value.title}</a></h3> 8 <copyright>{$index_url}</copyright>
9 <small>{if="!$hide_timestamps"}{function="strftime('%c', $value.timestamp)"} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> 9 <generator>Shaarli</generator>
10 {$value.url}</small><br> 10
11 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br> 11 {loop="$days"}
12 {if="$value.description"}{$value.formatedDescription}{/if} 12 <item>
13 <br><br><hr> 13 <title>{$value.date_human} - {$title}</title>
14 <guid>{$value.absolute_url}</guid>
15 <link>{$value.absolute_url}</link>
16 <pubDate>{$value.date_rss}</pubDate>
17 <description><![CDATA[
18 {loop="$value.links"}
19 <h3><a href="{$value.url}">{$value.title}</a></h3>
20 <small>
21 {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br>
22 {$value.url}
23 </small><br>
24 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
25 {if="$value.description"}{$value.description}{/if}
26 <br><br><hr>
14 {/loop} 27 {/loop}
15 ]]></description> 28 ]]></description>
16</item> 29 </item>
30 {/loop}
31 </channel>
32</rss><!-- Cached version of {$page_url} -->
diff --git a/tpl/vintage/editlink.html b/tpl/vintage/editlink.html
index 6f7a330f..c3671b1f 100644
--- a/tpl/vintage/editlink.html
+++ b/tpl/vintage/editlink.html
@@ -1,20 +1,16 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3<head>{include="includes"} 3<head>{include="includes"}
4 <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
5</head> 4</head>
6<body 5<body
7{if="$link.title==''"}onload="document.linkform.lf_title.focus();" 6{if="$link.title==''"}onload="document.linkform.lf_title.focus();"
8{elseif="$link.description==''"}onload="document.linkform.lf_description.focus();" 7{elseif="$link.description==''"}onload="document.linkform.lf_description.focus();"
9{else}onload="document.linkform.lf_tags.focus();"{/if} > 8{else}onload="document.linkform.lf_tags.focus();"{/if} >
10<div id="pageheader"> 9<div id="pageheader">
11 {if="$source !== 'firefoxsocialapi'"}
12 {include="page.header"} 10 {include="page.header"}
13 {else}
14 <div id="shaarli_title"><a href="{$titleLink}">{$shaarlititle}</a></div> 11 <div id="shaarli_title"><a href="{$titleLink}">{$shaarlititle}</a></div>
15 {/if}
16 <div id="editlinkform"> 12 <div id="editlinkform">
17 <form method="post" name="linkform"> 13 <form method="post" name="linkform" action="{$base_path}/admin/shaare">
18 <input type="hidden" name="lf_linkdate" value="{$link.linkdate}"> 14 <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
19 {if="isset($link.id)"} 15 {if="isset($link.id)"}
20 <input type="hidden" name="lf_id" value="{$link.id}"> 16 <input type="hidden" name="lf_id" value="{$link.id}">
@@ -48,19 +44,18 @@
48 {/if} 44 {/if}
49 <input type="submit" value="Save" name="save_edit" class="bigbutton"> 45 <input type="submit" value="Save" name="save_edit" class="bigbutton">
50 {if="!$link_is_new && isset($link.id)"} 46 {if="!$link_is_new && isset($link.id)"}
51 <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}" 47 <a href="{$base_path}/admin/shaare/delete?id={$link.id}&amp;token={$token}"
52 name="delete_link" class="bigbutton" 48 name="delete_link" class="bigbutton"
53 onClick="return confirmDeleteLink();"> 49 onClick="return confirmDeleteLink();">
54 {'Delete'|t} 50 {'Delete'|t}
55 </a> 51 </a>
56 {/if} 52 {/if}
57 <input type="hidden" name="token" value="{$token}"> 53 <input type="hidden" name="token" value="{$token}">
54 <input type="hidden" name="source" value="{$source}">
58 {if="$http_referer"}<input type="hidden" name="returnurl" value="{$http_referer}">{/if} 55 {if="$http_referer"}<input type="hidden" name="returnurl" value="{$http_referer}">{/if}
59 </form> 56 </form>
60 </div> 57 </div>
61</div> 58</div>
62{if="$source !== 'firefoxsocialapi'"}
63{include="page.footer"} 59{include="page.footer"}
64{/if}
65</body> 60</body>
66</html> 61</html>
diff --git a/tpl/vintage/error.html b/tpl/vintage/error.html
index b6e62be0..64f54cd2 100644
--- a/tpl/vintage/error.html
+++ b/tpl/vintage/error.html
@@ -18,7 +18,7 @@
18 </pre> 18 </pre>
19 {/if} 19 {/if}
20 20
21 <p>Would you mind <a href="?">clicking here</a>?</p> 21 <p>Would you mind <a href="{$base_path}/">clicking here</a>?</p>
22</div> 22</div>
23{include="page.footer"} 23{include="page.footer"}
24</body> 24</body>
diff --git a/tpl/vintage/export.html b/tpl/vintage/export.html
index 67c3d05f..c30e3b0a 100644
--- a/tpl/vintage/export.html
+++ b/tpl/vintage/export.html
@@ -5,12 +5,13 @@
5 <div id="pageheader"> 5 <div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="toolsdiv"> 7 <div id="toolsdiv">
8 <form method="GET"> 8 <form method="POST" action="{$base_path}/admin/export">
9 <input type="hidden" name="do" value="export">
10 Selection:<br> 9 Selection:<br>
11 <input type="radio" name="selection" value="all" checked="true"> All<br> 10 <input type="radio" name="selection" value="all" checked="true"> All<br>
12 <input type="radio" name="selection" value="private"> Private<br> 11 <input type="radio" name="selection" value="private"> Private<br>
13 <input type="radio" name="selection" value="public"> Public<br> 12 <input type="radio" name="selection" value="public"> Public<br>
13 <input type="hidden" name="token" value="{$token}">
14
14 <br> 15 <br>
15 <input type="checkbox" name="prepend_note_url" id="prepend_note_url"> 16 <input type="checkbox" name="prepend_note_url" id="prepend_note_url">
16 <label for="prepend_note_url"> 17 <label for="prepend_note_url">
diff --git a/tpl/vintage/feed.atom.html b/tpl/vintage/feed.atom.html
index 0621cb9e..5919bb49 100644
--- a/tpl/vintage/feed.atom.html
+++ b/tpl/vintage/feed.atom.html
@@ -6,8 +6,8 @@
6 <updated>{$last_update}</updated> 6 <updated>{$last_update}</updated>
7 {/if} 7 {/if}
8 <link rel="self" href="{$self_link}#" /> 8 <link rel="self" href="{$self_link}#" />
9 <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}?do=opensearch#" 9 <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
10 title="Shaarli search - {$shaarlititle}" /> 10 title="Shaarli search - {$shaarlititle}" />
11 {loop="$feed_plugins_header"} 11 {loop="$feed_plugins_header"}
12 {$value} 12 {$value}
13 {/loop} 13 {/loop}
diff --git a/tpl/vintage/feed.rss.html b/tpl/vintage/feed.rss.html
index ee3fef88..4be8202f 100644
--- a/tpl/vintage/feed.rss.html
+++ b/tpl/vintage/feed.rss.html
@@ -8,7 +8,7 @@
8 <copyright>{$index_url}</copyright> 8 <copyright>{$index_url}</copyright>
9 <generator>Shaarli</generator> 9 <generator>Shaarli</generator>
10 <atom:link rel="self" href="{$self_link}" /> 10 <atom:link rel="self" href="{$self_link}" />
11 <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}?do=opensearch#" 11 <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
12 title="Shaarli search - {$shaarlititle}" /> 12 title="Shaarli search - {$shaarlititle}" />
13 {loop="$feed_plugins_header"} 13 {loop="$feed_plugins_header"}
14 {$value} 14 {$value}
diff --git a/tpl/vintage/import.html b/tpl/vintage/import.html
index bb9e4a56..7d6eac76 100644
--- a/tpl/vintage/import.html
+++ b/tpl/vintage/import.html
@@ -6,7 +6,7 @@
6 {include="page.header"} 6 {include="page.header"}
7 <div id="uploaddiv"> 7 <div id="uploaddiv">
8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}). 8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}).
9 <form method="POST" action="?do=import" enctype="multipart/form-data" 9 <form method="POST" action="{$base_path}/admin/import" enctype="multipart/form-data"
10 name="uploadform" id="uploadform"> 10 name="uploadform" id="uploadform">
11 <input type="hidden" name="token" value="{$token}"> 11 <input type="hidden" name="token" value="{$token}">
12 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 12 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
diff --git a/tpl/vintage/includes.html b/tpl/vintage/includes.html
index 8d273c44..eac05701 100644
--- a/tpl/vintage/includes.html
+++ b/tpl/vintage/includes.html
@@ -3,18 +3,19 @@
3<meta name="format-detection" content="telephone=no" /> 3<meta name="format-detection" content="telephone=no" />
4<meta name="viewport" content="width=device-width,initial-scale=1.0" /> 4<meta name="viewport" content="width=device-width,initial-scale=1.0" />
5<meta name="referrer" content="same-origin"> 5<meta name="referrer" content="same-origin">
6<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" /> 6<link rel="alternate" type="application/rss+xml" href="{$feedurl}feed/rss?{$searchcrits}#" title="RSS Feed" />
7<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=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="img/favicon.ico" rel="shortcut icon" type="image/x-icon" />
9<link type="text/css" rel="stylesheet" href="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="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="{$value}#"/> 14<link type="text/css" rel="stylesheet" href="{$base_path}/{$value}#"/>
15{/loop} 15{/loop}
16{if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="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="?do=opensearch#" title="Shaarli search - {$shaarlititle|htmlspecialchars}"/> 17<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
18 title="Shaarli search - {$shaarlititle|htmlspecialchars}" />
18{if="! empty($links) && count($links) === 1"} 19{if="! empty($links) && count($links) === 1"}
19 {$link=reset($links)} 20 {$link=reset($links)}
20 <meta property="og:title" content="{$link.title}" /> 21 <meta property="og:title" content="{$link.title}" />
diff --git a/tpl/vintage/install.html b/tpl/vintage/install.html
index aca890d6..8c10b2cb 100644
--- a/tpl/vintage/install.html
+++ b/tpl/vintage/install.html
@@ -5,7 +5,7 @@
5<div id="install"> 5<div id="install">
6 <h1>Shaarli</h1> 6 <h1>Shaarli</h1>
7 It looks like it's the first time you run Shaarli. Please configure it:<br> 7 It looks like it's the first time you run Shaarli. Please configure it:<br>
8 <form method="POST" action="#" name="installform" id="installform"> 8 <form method="POST" action="{$base_path}/install" name="installform" id="installform">
9 <table> 9 <table>
10 <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr> 10 <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr>
11 <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr> 11 <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr>
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html
index dcb14e90..00896eb5 100644
--- a/tpl/vintage/linklist.html
+++ b/tpl/vintage/linklist.html
@@ -1,7 +1,6 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3<head> 3<head>
4 <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
5 {include="includes"} 4 {include="includes"}
6</head> 5</head>
7<body> 6<body>
@@ -66,12 +65,12 @@
66 tagged 65 tagged
67 {loop="$exploded_tags"} 66 {loop="$exploded_tags"}
68 <span class="linktag" title="Remove tag"> 67 <span class="linktag" title="Remove tag">
69 <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a> 68 <a href="{$base_path}/remove-tag/{function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
70 </span> 69 </span>
71 {/loop} 70 {/loop}
72 {elseif="$search_tags === false"} 71 {elseif="$search_tags === false"}
73 <span class="linktag" title="Remove tag"> 72 <span class="linktag" title="Remove tag">
74 <a href="?">untagged <span class="remove">x</span></a> 73 <a href="{$base_path}/">untagged <span class="remove">x</span></a>
75 </span> 74 </span>
76 {/if} 75 {/if}
77 </div> 76 </div>
@@ -84,7 +83,7 @@
84 <div class="thumbnail"> 83 <div class="thumbnail">
85 <a href="{$value.real_url}"> 84 <a href="{$value.real_url}">
86 {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}
87 <img data-src="{$value.thumbnail}#" class="b-lazy" 86 <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy"
88 src="" 87 src=""
89 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 88 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
90 </a> 89 </a>
@@ -93,17 +92,16 @@
93 <div class="linkcontainer"> 92 <div class="linkcontainer">
94 {if="$is_logged_in"} 93 {if="$is_logged_in"}
95 <div class="linkeditbuttons"> 94 <div class="linkeditbuttons">
96 <form method="GET" class="buttoneditform"> 95 <a href="{$base_path}/admin/shaare/{$value.id}" title="Edit" class="button_edit">
97 <input type="hidden" name="edit_link" value="{$value.id}"> 96 <img src="{$asset_path}/img/edit_icon.png#">
98 <input type="image" alt="Edit" src="img/edit_icon.png" title="Edit" class="button_edit"> 97 </a>
99 </form><br> 98 <br>
100 <form method="GET" class="buttoneditform"> 99 <a href="{$base_path}/admin/shaare/delete?id={$value.id}&amp;token={$token}" label="Delete"
101 <input type="hidden" name="lf_linkdate" value="{$value.id}"> 100 onClick="return confirmDeleteLink();"
102 <input type="hidden" name="token" value="{$token}"> 101 class="button_delete"
103 <input type="hidden" name="delete_link"> 102 >
104 <input type="image" alt="Delete" src="img/delete_icon.png" title="Delete" 103 <img src="{$asset_path}/img/delete_icon.png#">
105 class="button_delete" onClick="return confirmDeleteLink();"> 104 </a>
106 </form>
107 </div> 105 </div>
108 {/if} 106 {/if}
109 <span class="linktitle"> 107 <span class="linktitle">
@@ -114,7 +112,7 @@
114 {if="!$hide_timestamps || $is_logged_in"} 112 {if="!$hide_timestamps || $is_logged_in"}
115 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} 113 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
116 <span class="linkdate" title="Permalink"> 114 <span class="linkdate" title="Permalink">
117 <a href="?{$value.shorturl}"> 115 <a href="{$base_path}/shaare/{$value.shorturl}">
118 <span title="{$updated}"> 116 <span title="{$updated}">
119 {$value.created|format_date} 117 {$value.created|format_date}
120 {if="$value.updated_timestamp"}*{/if} 118 {if="$value.updated_timestamp"}*{/if}
@@ -123,7 +121,7 @@
123 </a> - 121 </a> -
124 </span> 122 </span>
125 {else} 123 {else}
126 <span class="linkdate" title="Short link here"><a href="?{$value.shorturl}">permalink</a> - </span> 124 <span class="linkdate" title="Short link here"><a href="{$base_path}/shaare/{$value.shorturl}">permalink</a> - </span>
127 {/if} 125 {/if}
128 126
129 {loop="$value.link_plugin"} 127 {loop="$value.link_plugin"}
@@ -133,7 +131,7 @@
133 <a href="{$value.real_url}"><span class="linkurl" title="Short link">{$value.url}</span></a><br> 131 <a href="{$value.real_url}"><span class="linkurl" title="Short link">{$value.url}</span></a><br>
134 {if="$value.tags"} 132 {if="$value.tags"}
135 <div class="linktaglist"> 133 <div class="linktaglist">
136 {loop="$value.taglist"}<span class="linktag" title="Add tag"><a href="?addtag={$value|urlencode}">{$value}</a></span> {/loop} 134 {loop="$value.taglist"}<span class="linktag" title="Add tag"><a href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a></span> {/loop}
137 </div> 135 </div>
138 {/if} 136 {/if}
139 137
@@ -154,7 +152,7 @@
154</div> 152</div>
155 153
156 {include="page.footer"} 154 {include="page.footer"}
157<script src="js/thumbnails.min.js"></script> 155<script src="{$asset_path}/js/thumbnails.min.js#"></script>
158 156
159</body> 157</body>
160</html> 158</html>
diff --git a/tpl/vintage/linklist.paging.html b/tpl/vintage/linklist.paging.html
index 35149a6b..b9396df6 100644
--- a/tpl/vintage/linklist.paging.html
+++ b/tpl/vintage/linklist.paging.html
@@ -1,11 +1,11 @@
1<div class="paging"> 1<div class="paging">
2{if="$is_logged_in"} 2{if="$is_logged_in"}
3 <div class="paging_privatelinks"> 3 <div class="paging_privatelinks">
4 <a href="?visibility=private"> 4 <a href="{$base_path}/admin/isibility/private">
5 {if="$visibility=='private'"} 5 {if="$visibility=='private'"}
6 <img src="img/private_16x16_active.png" width="16" height="16" title="Click to see all links" alt="Click to see all links"> 6 <img src="{$asset_path}/img/private_16x16_active.png#" width="16" height="16" title="Click to see all links" alt="Click to see all links">
7 {else} 7 {else}
8 <img src="img/private_16x16.png" width="16" height="16" title="Click to see only private links" alt="Click to see only private links"> 8 <img src="{$asset_path}/img/private_16x16.png#" width="16" height="16" title="Click to see only private links" alt="Click to see only private links">
9 {/if} 9 {/if}
10 </a> 10 </a>
11 11
@@ -23,8 +23,13 @@
23 </div> 23 </div>
24 {/loop} 24 {/loop}
25 <div class="paging_linksperpage"> 25 <div class="paging_linksperpage">
26 Links per page: <a href="?linksperpage=20">20</a> <a href="?linksperpage=50">50</a> <a href="?linksperpage=100">100</a> 26 Links per page:
27 <form method="GET" class="linksperpage"><input type="text" name="linksperpage" size="2"></form> 27 <a href="{$base_path}/links-per-page?nb=20">20</a>
28 <a href="{$base_path}/links-per-page?nb=50">50</a>
29 <a href="{$base_path}/links-per-page?nb=100">100</a>
30 <form method="GET" class="linksperpage" action="{$base_path}/links-per-page">
31 <input type="text" name="nb" size="2">
32 </form>
28 </div> 33 </div>
29 {if="$previous_page_url"} <a href="{$previous_page_url}" class="paging_older">&#x25C4;Older</a> {/if} 34 {if="$previous_page_url"} <a href="{$previous_page_url}" class="paging_older">&#x25C4;Older</a> {/if}
30 <div class="paging_current">page {$page_current} / {$page_max} </div> 35 <div class="paging_current">page {$page_current} / {$page_max} </div>
diff --git a/tpl/vintage/loginform.html b/tpl/vintage/loginform.html
index a3792066..6aa20ab1 100644
--- a/tpl/vintage/loginform.html
+++ b/tpl/vintage/loginform.html
@@ -11,7 +11,7 @@
11 {include="page.header"} 11 {include="page.header"}
12 12
13 <div id="headerform"> 13 <div id="headerform">
14 <form method="post" name="loginform"> 14 <form method="post" name="loginform" action="{$base_path}/login">
15 <label for="login">Login: <input type="text" id="login" name="login" tabindex="1" 15 <label for="login">Login: <input type="text" id="login" name="login" tabindex="1"
16 {if="!empty($username)"}value="{$username}"{/if}> 16 {if="!empty($username)"}value="{$username}"{/if}>
17 </label> 17 </label>
diff --git a/tpl/vintage/opensearch.html b/tpl/vintage/opensearch.html
index 3fcc30b7..1c7f279b 100644
--- a/tpl/vintage/opensearch.html
+++ b/tpl/vintage/opensearch.html
@@ -3,8 +3,8 @@
3 <ShortName>Shaarli search - {$pagetitle}</ShortName> 3 <ShortName>Shaarli search - {$pagetitle}</ShortName>
4 <Description>Shaarli search - {$pagetitle}</Description> 4 <Description>Shaarli search - {$pagetitle}</Description>
5 <Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" /> 5 <Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" />
6 <Url type="application/atom+xml" template="{$serverurl}?do=atom&amp;searchterm={searchTerms}"/> 6 <Url type="application/atom+xml" template="{$serverurl}feed/atom?searchterm={searchTerms}"/>
7 <Url type="application/rss+xml" template="{$serverurl}?do=rss&amp;searchterm={searchTerms}"/> 7 <Url type="application/rss+xml" template="{$serverurl}feed/rss?searchterm={searchTerms}"/>
8 <InputEncoding>UTF-8</InputEncoding> 8 <InputEncoding>UTF-8</InputEncoding>
9 <Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer> 9 <Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer>
10 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE 10 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE
diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html
index a3380841..0fe4c736 100644
--- a/tpl/vintage/page.footer.html
+++ b/tpl/vintage/page.footer.html
@@ -23,12 +23,14 @@
23</div> 23</div>
24{/if} 24{/if}
25 25
26<script src="js/shaarli.min.js"></script> 26<script src="{$asset_path}/js/shaarli.min.js#"></script>
27 27
28{if="$is_logged_in"} 28{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> 29<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} 30{/if}
31 31
32{loop="$plugins_footer.js_files"} 32{loop="$plugins_footer.js_files"}
33 <script src="{$value}#"></script> 33 <script src="{$base_path}/{$value}#"></script>
34{/loop} 34{/loop}
35
36<input type="hidden" name="js_base_path" value="{$base_path}" />
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html
index a37926d2..0a33523b 100644
--- a/tpl/vintage/page.header.html
+++ b/tpl/vintage/page.header.html
@@ -18,22 +18,22 @@
18{else} 18{else}
19<li><a href="{$titleLink}" class="nomobile">Home</a></li> 19<li><a href="{$titleLink}" class="nomobile">Home</a></li>
20 {if="$is_logged_in"} 20 {if="$is_logged_in"}
21 <li><a href="?do=logout">Logout</a></li> 21 <li><a href="{$base_path}/admin/logout">Logout</a></li>
22 <li><a href="?do=tools">Tools</a></li> 22 <li><a href="{$base_path}/admin/tools">Tools</a></li>
23 <li><a href="?do=addlink">Add link</a></li> 23 <li><a href="{$base_path}/admin/add-shaare">Add link</a></li>
24 {elseif="$openshaarli"} 24 {elseif="$openshaarli"}
25 <li><a href="?do=tools">Tools</a></li> 25 <li><a href="{$base_path}/admin/tools">Tools</a></li>
26 <li><a href="?do=addlink">Add link</a></li> 26 <li><a href="{$base_path}/admin/add-shaare">Add link</a></li>
27 {else} 27 {else}
28 <li><a href="/login">Login</a></li> 28 <li><a href="{$base_path}/login">Login</a></li>
29 {/if} 29 {/if}
30 <li><a href="{$feedurl}?do=rss{$searchcrits}" class="nomobile">RSS Feed</a></li> 30 <li><a href="{$feedurl}/feed/rss?{$searchcrits}" class="nomobile">RSS Feed</a></li>
31 {if="$showatom"} 31 {if="$showatom"}
32 <li><a href="{$feedurl}?do=atom{$searchcrits}" class="nomobile">ATOM Feed</a></li> 32 <li><a href="{$feedurl}/feed/atom?{$searchcrits}" class="nomobile">ATOM Feed</a></li>
33 {/if} 33 {/if}
34 <li><a href="?do=tagcloud">Tag cloud</a></li> 34 <li><a href="{$base_path}/tags/cloud">Tag cloud</a></li>
35 <li><a href="?do=picwall{$searchcrits}">Picture wall</a></li> 35 <li><a href="{$base_path}/picture-wall{function="ltrim($searchcrits, '&')"}">Picture wall</a></li>
36 <li><a href="?do=daily">Daily</a></li> 36 <li><a href="{$base_path}/daily">Daily</a></li>
37 {loop="$plugins_header.buttons_toolbar"} 37 {loop="$plugins_header.buttons_toolbar"}
38 <li><a 38 <li><a
39 {loop="$value.attr"} 39 {loop="$value.attr"}
diff --git a/tpl/vintage/picwall.html b/tpl/vintage/picwall.html
index b3a16791..da3aa36c 100644
--- a/tpl/vintage/picwall.html
+++ b/tpl/vintage/picwall.html
@@ -38,6 +38,6 @@
38 38
39{include="page.footer"} 39{include="page.footer"}
40 40
41<script src="js/thumbnails.min.js"></script> 41<script src="{$asset_path}/js/thumbnails.min.js#"></script>
42</body> 42</body>
43</html> 43</html>
diff --git a/tpl/vintage/pluginsadmin.html b/tpl/vintage/pluginsadmin.html
index 63b45cac..d0972cd1 100644
--- a/tpl/vintage/pluginsadmin.html
+++ b/tpl/vintage/pluginsadmin.html
@@ -16,7 +16,7 @@
16</noscript> 16</noscript>
17 17
18<div id="pluginsadmin"> 18<div id="pluginsadmin">
19 <form action="?do=save_pluginadmin" method="POST"> 19 <form action="{$base_path}/admin/plugins" method="POST">
20 <section id="enabled_plugins"> 20 <section id="enabled_plugins">
21 <h1>Enabled Plugins</h1> 21 <h1>Enabled Plugins</h1>
22 22
@@ -86,9 +86,10 @@
86 <input type="submit" value="Save"/> 86 <input type="submit" value="Save"/>
87 </div> 87 </div>
88 </section> 88 </section>
89 <input type="hidden" name="token" value="{$token}">
89 </form> 90 </form>
90 91
91 <form action="?do=save_pluginadmin" method="POST"> 92 <form action="{$base_path}/admin/plugins" method="POST">
92 <section id="plugin_parameters"> 93 <section id="plugin_parameters">
93 <h1>Enabled Plugin Parameters</h1> 94 <h1>Enabled Plugin Parameters</h1>
94 95
@@ -124,6 +125,7 @@
124 </div> 125 </div>
125 </div> 126 </div>
126 </section> 127 </section>
128 <input type="hidden" name="token" value="{$token}">
127 </form> 129 </form>
128 130
129</div> 131</div>
diff --git a/tpl/vintage/tag.cloud.html b/tpl/vintage/tag.cloud.html
index d93bf4f9..5d21f239 100644
--- a/tpl/vintage/tag.cloud.html
+++ b/tpl/vintage/tag.cloud.html
@@ -12,8 +12,8 @@
12 12
13 <div id="cloudtag"> 13 <div id="cloudtag">
14 {loop="$tags"} 14 {loop="$tags"}
15 <a href="?addtag={$key|urlencode}" class="count">{$value.count}</a><a 15 <a href="{$base_path}/add-tag/{$key|urlencode}" class="count">{$value.count}</a><a
16 href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a> 16 href="{$base_path}/?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a>
17 {loop="$value.tag_plugin"} 17 {loop="$value.tag_plugin"}
18 {$value} 18 {$value}
19 {/loop} 19 {/loop}
diff --git a/tpl/vintage/thumbnails.html b/tpl/vintage/thumbnails.html
index 5cad845b..18f296f7 100644
--- a/tpl/vintage/thumbnails.html
+++ b/tpl/vintage/thumbnails.html
@@ -23,6 +23,6 @@
23<input type="hidden" name="ids" value="{function="implode(',', $ids)"}" /> 23<input type="hidden" name="ids" value="{function="implode(',', $ids)"}" />
24 24
25{include="page.footer"} 25{include="page.footer"}
26<script src="js/thumbnails_update.min.js?v={$version_hash}"></script> 26<script src="{$asset_path}/js/thumbnails_update.min.js?v={$version_hash}#"></script>
27</body> 27</body>
28</html> 28</html>
diff --git a/tpl/vintage/tools.html b/tpl/vintage/tools.html
index 1cef726e..1125bba9 100644
--- a/tpl/vintage/tools.html
+++ b/tpl/vintage/tools.html
@@ -5,17 +5,17 @@
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="toolsdiv"> 7 <div id="toolsdiv">
8 <a href="?do=configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a> 8 <a href="{$base_path}/admin/configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
9 <br><br> 9 <br><br>
10 <a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a> 10 <a href="{$base_path}/admin/plugins"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
11 <br><br> 11 <br><br>
12 {if="!$openshaarli"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a> 12 {if="!$openshaarli"}<a href="{$base_path}/admin/password"><b>Change password</b><span>: Change your password.</span></a>
13 <br><br>{/if} 13 <br><br>{/if}
14 <a href="?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a> 14 <a href="{$base_path}/admin/tags"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
15 <br><br> 15 <br><br>
16 <a href="?do=import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a> 16 <a href="{$base_path}/admin/import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a>
17 <br><br> 17 <br><br>
18 <a href="?do=export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a> 18 <a href="{$base_path}/admin/export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a>
19 <br><br> 19 <br><br>
20 <a class="smallbutton" 20 <a class="smallbutton"
21 onclick="return alertBookmarklet();" 21 onclick="return alertBookmarklet();"
@@ -24,7 +24,7 @@
24 var%20url%20=%20location.href; 24 var%20url%20=%20location.href;
25 var%20title%20=%20document.title%20||%20url; 25 var%20title%20=%20document.title%20||%20url;
26 window.open( 26 window.open(
27 '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+ 27 '{$pageabsaddr}admin/shaare?post='%20+%20encodeURIComponent(url)+
28 '&amp;title='%20+%20encodeURIComponent(title)+ 28 '&amp;title='%20+%20encodeURIComponent(title)+
29 '&amp;description='%20+%20encodeURIComponent(document.getSelection())+ 29 '&amp;description='%20+%20encodeURIComponent(document.getSelection())+
30 '&amp;source=bookmarklet','_blank','menubar=no,height=390,width=600,toolbar=no,scrollbars=no,status=no,dialog=1' 30 '&amp;source=bookmarklet','_blank','menubar=no,height=390,width=600,toolbar=no,scrollbars=no,status=no,dialog=1'