aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Dockerfile2
-rw-r--r--Dockerfile.armhf8
-rw-r--r--application/bookmark/BookmarkIO.php22
-rw-r--r--application/container/ContainerBuilder.php17
-rw-r--r--application/feed/CachedPage.php45
-rw-r--r--application/front/controller/admin/ServerController.php7
-rw-r--r--application/front/controller/visitor/DailyController.php7
-rw-r--r--application/front/controller/visitor/InstallController.php7
-rw-r--r--application/helper/ApplicationUtils.php16
-rw-r--r--application/helper/DailyPageHelper.php84
-rw-r--r--application/plugin/PluginManager.php64
-rw-r--r--application/plugin/exception/PluginInvalidRouteException.php26
-rw-r--r--application/render/PageCacheManager.php14
-rw-r--r--doc/md/REST-API.md2
-rw-r--r--doc/md/dev/Plugin-system.md25
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po186
-rw-r--r--index.php22
-rw-r--r--phpcs.xml1
-rw-r--r--plugins/demo_plugin/DemoPluginController.php24
-rw-r--r--plugins/demo_plugin/demo_plugin.php19
-rw-r--r--tests/PluginManagerTest.php39
-rw-r--r--tests/container/ContainerBuilderTest.php5
-rw-r--r--tests/feed/CachedPageTest.php57
-rw-r--r--tests/helper/DailyPageHelperTest.php115
-rw-r--r--tests/plugins/test/test.php16
-rw-r--r--tests/plugins/test_route_invalid/test_route_invalid.php12
-rw-r--r--tpl/default/pluginscontent.html13
-rw-r--r--yarn.lock6
28 files changed, 673 insertions, 188 deletions
diff --git a/Dockerfile b/Dockerfile
index f6120b71..79d33130 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,7 +26,7 @@ RUN cd shaarli \
26 26
27# Stage 4: 27# Stage 4:
28# - Shaarli image 28# - Shaarli image
29FROM alpine:3.8 29FROM alpine:3.12
30LABEL maintainer="Shaarli Community" 30LABEL maintainer="Shaarli Community"
31 31
32RUN apk --update --no-cache add \ 32RUN apk --update --no-cache add \
diff --git a/Dockerfile.armhf b/Dockerfile.armhf
index 5bbf6680..471f2397 100644
--- a/Dockerfile.armhf
+++ b/Dockerfile.armhf
@@ -1,7 +1,7 @@
1# Stage 1: 1# Stage 1:
2# - Copy Shaarli sources 2# - Copy Shaarli sources
3# - Build documentation 3# - Build documentation
4FROM arm32v6/alpine:3.8 as docs 4FROM arm32v6/alpine:3.10 as docs
5ADD . /usr/src/app/shaarli 5ADD . /usr/src/app/shaarli
6RUN apk --update --no-cache add py2-pip \ 6RUN apk --update --no-cache add py2-pip \
7 && cd /usr/src/app/shaarli \ 7 && cd /usr/src/app/shaarli \
@@ -10,7 +10,7 @@ RUN apk --update --no-cache add py2-pip \
10 10
11# Stage 2: 11# Stage 2:
12# - Resolve PHP dependencies with Composer 12# - Resolve PHP dependencies with Composer
13FROM arm32v6/alpine:3.8 as composer 13FROM arm32v6/alpine:3.10 as composer
14COPY --from=docs /usr/src/app/shaarli /app/shaarli 14COPY --from=docs /usr/src/app/shaarli /app/shaarli
15RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \ 15RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \
16 && cd /app/shaarli \ 16 && cd /app/shaarli \
@@ -18,7 +18,7 @@ RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer
18 18
19# Stage 3: 19# Stage 3:
20# - Frontend dependencies 20# - Frontend dependencies
21FROM arm32v6/alpine:3.8 as node 21FROM arm32v6/alpine:3.10 as node
22COPY --from=composer /app/shaarli /shaarli 22COPY --from=composer /app/shaarli /shaarli
23RUN apk --update --no-cache add yarn nodejs-current python2 build-base \ 23RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
24 && cd /shaarli \ 24 && cd /shaarli \
@@ -28,7 +28,7 @@ RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
28 28
29# Stage 4: 29# Stage 4:
30# - Shaarli image 30# - Shaarli image
31FROM arm32v6/alpine:3.8 31FROM arm32v6/alpine:3.10
32LABEL maintainer="Shaarli Community" 32LABEL maintainer="Shaarli Community"
33 33
34RUN apk --update --no-cache add \ 34RUN apk --update --no-cache add \
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php
index c78dbe41..8439d470 100644
--- a/application/bookmark/BookmarkIO.php
+++ b/application/bookmark/BookmarkIO.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
6 6
7use malkusch\lock\exception\LockAcquireException;
7use malkusch\lock\mutex\Mutex; 8use malkusch\lock\mutex\Mutex;
8use malkusch\lock\mutex\NoMutex; 9use malkusch\lock\mutex\NoMutex;
9use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; 10use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
@@ -80,7 +81,7 @@ class BookmarkIO
80 } 81 }
81 82
82 $content = null; 83 $content = null;
83 $this->mutex->synchronized(function () use (&$content) { 84 $this->synchronized(function () use (&$content) {
84 $content = file_get_contents($this->datastore); 85 $content = file_get_contents($this->datastore);
85 }); 86 });
86 87
@@ -119,11 +120,28 @@ class BookmarkIO
119 120
120 $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix; 121 $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix;
121 122
122 $this->mutex->synchronized(function () use ($data) { 123 $this->synchronized(function () use ($data) {
123 file_put_contents( 124 file_put_contents(
124 $this->datastore, 125 $this->datastore,
125 $data 126 $data
126 ); 127 );
127 }); 128 });
128 } 129 }
130
131 /**
132 * Wrapper applying mutex to provided function.
133 * If the lock can't be acquired (e.g. some shared hosting provider), we execute the function without mutex.
134 *
135 * @see https://github.com/shaarli/Shaarli/issues/1650
136 *
137 * @param callable $function
138 */
139 protected function synchronized(callable $function): void
140 {
141 try {
142 $this->mutex->synchronized($function);
143 } catch (LockAcquireException $exception) {
144 $function();
145 }
146 }
129} 147}
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php
index f0234eca..6d69a880 100644
--- a/application/container/ContainerBuilder.php
+++ b/application/container/ContainerBuilder.php
@@ -50,6 +50,9 @@ class ContainerBuilder
50 /** @var LoginManager */ 50 /** @var LoginManager */
51 protected $login; 51 protected $login;
52 52
53 /** @var PluginManager */
54 protected $pluginManager;
55
53 /** @var LoggerInterface */ 56 /** @var LoggerInterface */
54 protected $logger; 57 protected $logger;
55 58
@@ -61,12 +64,14 @@ class ContainerBuilder
61 SessionManager $session, 64 SessionManager $session,
62 CookieManager $cookieManager, 65 CookieManager $cookieManager,
63 LoginManager $login, 66 LoginManager $login,
67 PluginManager $pluginManager,
64 LoggerInterface $logger 68 LoggerInterface $logger
65 ) { 69 ) {
66 $this->conf = $conf; 70 $this->conf = $conf;
67 $this->session = $session; 71 $this->session = $session;
68 $this->login = $login; 72 $this->login = $login;
69 $this->cookieManager = $cookieManager; 73 $this->cookieManager = $cookieManager;
74 $this->pluginManager = $pluginManager;
70 $this->logger = $logger; 75 $this->logger = $logger;
71 } 76 }
72 77
@@ -78,12 +83,10 @@ class ContainerBuilder
78 $container['sessionManager'] = $this->session; 83 $container['sessionManager'] = $this->session;
79 $container['cookieManager'] = $this->cookieManager; 84 $container['cookieManager'] = $this->cookieManager;
80 $container['loginManager'] = $this->login; 85 $container['loginManager'] = $this->login;
86 $container['pluginManager'] = $this->pluginManager;
81 $container['logger'] = $this->logger; 87 $container['logger'] = $this->logger;
82 $container['basePath'] = $this->basePath; 88 $container['basePath'] = $this->basePath;
83 89
84 $container['plugins'] = function (ShaarliContainer $container): PluginManager {
85 return new PluginManager($container->conf);
86 };
87 90
88 $container['history'] = function (ShaarliContainer $container): History { 91 $container['history'] = function (ShaarliContainer $container): History {
89 return new History($container->conf->get('resource.history')); 92 return new History($container->conf->get('resource.history'));
@@ -113,14 +116,6 @@ class ContainerBuilder
113 ); 116 );
114 }; 117 };
115 118
116 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
117 $pluginManager = new PluginManager($container->conf);
118
119 $pluginManager->load($container->conf->get('general.enabled_plugins'));
120
121 return $pluginManager;
122 };
123
124 $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { 119 $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
125 return new FormatterFactory( 120 return new FormatterFactory(
126 $container->conf, 121 $container->conf,
diff --git a/application/feed/CachedPage.php b/application/feed/CachedPage.php
index d809bdd9..c23c200f 100644
--- a/application/feed/CachedPage.php
+++ b/application/feed/CachedPage.php
@@ -1,34 +1,43 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Feed; 5namespace Shaarli\Feed;
4 6
7use DatePeriod;
8
5/** 9/**
6 * Simple cache system, mainly for the RSS/ATOM feeds 10 * Simple cache system, mainly for the RSS/ATOM feeds
7 */ 11 */
8class CachedPage 12class CachedPage
9{ 13{
10 // Directory containing page caches 14 /** Directory containing page caches */
11 private $cacheDir; 15 protected $cacheDir;
16
17 /** Should this URL be cached (boolean)? */
18 protected $shouldBeCached;
12 19
13 // Should this URL be cached (boolean)? 20 /** Name of the cache file for this URL */
14 private $shouldBeCached; 21 protected $filename;
15 22
16 // Name of the cache file for this URL 23 /** @var DatePeriod|null Optionally specify a period of time for cache validity */
17 private $filename; 24 protected $validityPeriod;
18 25
19 /** 26 /**
20 * Creates a new CachedPage 27 * Creates a new CachedPage
21 * 28 *
22 * @param string $cacheDir page cache directory 29 * @param string $cacheDir page cache directory
23 * @param string $url page URL 30 * @param string $url page URL
24 * @param bool $shouldBeCached whether this page needs to be cached 31 * @param bool $shouldBeCached whether this page needs to be cached
32 * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
25 */ 33 */
26 public function __construct($cacheDir, $url, $shouldBeCached) 34 public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod)
27 { 35 {
28 // TODO: check write access to the cache directory 36 // TODO: check write access to the cache directory
29 $this->cacheDir = $cacheDir; 37 $this->cacheDir = $cacheDir;
30 $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; 38 $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
31 $this->shouldBeCached = $shouldBeCached; 39 $this->shouldBeCached = $shouldBeCached;
40 $this->validityPeriod = $validityPeriod;
32 } 41 }
33 42
34 /** 43 /**
@@ -41,10 +50,20 @@ class CachedPage
41 if (!$this->shouldBeCached) { 50 if (!$this->shouldBeCached) {
42 return null; 51 return null;
43 } 52 }
44 if (is_file($this->filename)) { 53 if (!is_file($this->filename)) {
45 return file_get_contents($this->filename); 54 return null;
55 }
56 if ($this->validityPeriod !== null) {
57 $cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename));
58 if (
59 $cacheDate < $this->validityPeriod->getStartDate()
60 || $cacheDate > $this->validityPeriod->getEndDate()
61 ) {
62 return null;
63 }
46 } 64 }
47 return null; 65
66 return file_get_contents($this->filename);
48 } 67 }
49 68
50 /** 69 /**
diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php
index fabeaf2f..4b74f4a9 100644
--- a/application/front/controller/admin/ServerController.php
+++ b/application/front/controller/admin/ServerController.php
@@ -39,11 +39,16 @@ class ServerController extends ShaarliAdminController
39 $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; 39 $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion;
40 $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); 40 $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
41 41
42 $permissions = array_merge(
43 ApplicationUtils::checkResourcePermissions($this->container->conf),
44 ApplicationUtils::checkDatastoreMutex()
45 );
46
42 $this->assignView('php_version', PHP_VERSION); 47 $this->assignView('php_version', PHP_VERSION);
43 $this->assignView('php_eol', format_date($phpEol, false)); 48 $this->assignView('php_eol', format_date($phpEol, false));
44 $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); 49 $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
45 $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); 50 $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
46 $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); 51 $this->assignView('permissions', $permissions);
47 $this->assignView('release_url', $releaseUrl); 52 $this->assignView('release_url', $releaseUrl);
48 $this->assignView('latest_version', $latestVersion); 53 $this->assignView('latest_version', $latestVersion);
49 $this->assignView('current_version', $currentVersion); 54 $this->assignView('current_version', $currentVersion);
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php
index 846cfe22..29492a5f 100644
--- a/application/front/controller/visitor/DailyController.php
+++ b/application/front/controller/visitor/DailyController.php
@@ -86,9 +86,11 @@ class DailyController extends ShaarliVisitorController
86 public function rss(Request $request, Response $response): Response 86 public function rss(Request $request, Response $response): Response
87 { 87 {
88 $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); 88 $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
89 $type = DailyPageHelper::extractRequestedType($request);
90 $cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type);
89 91
90 $pageUrl = page_url($this->container->environment); 92 $pageUrl = page_url($this->container->environment);
91 $cache = $this->container->pageCacheManager->getCachePage($pageUrl); 93 $cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration);
92 94
93 $cached = $cache->cachedVersion(); 95 $cached = $cache->cachedVersion();
94 if (!empty($cached)) { 96 if (!empty($cached)) {
@@ -96,7 +98,6 @@ class DailyController extends ShaarliVisitorController
96 } 98 }
97 99
98 $days = []; 100 $days = [];
99 $type = DailyPageHelper::extractRequestedType($request);
100 $format = DailyPageHelper::getFormatByType($type); 101 $format = DailyPageHelper::getFormatByType($type);
101 $length = DailyPageHelper::getRssLengthByType($type); 102 $length = DailyPageHelper::getRssLengthByType($type);
102 foreach ($this->container->bookmarkService->search() as $bookmark) { 103 foreach ($this->container->bookmarkService->search() as $bookmark) {
@@ -131,7 +132,7 @@ class DailyController extends ShaarliVisitorController
131 $dataPerDay[$day] = [ 132 $dataPerDay[$day] = [
132 'date' => $endDateTime, 133 'date' => $endDateTime,
133 'date_rss' => $endDateTime->format(DateTime::RSS), 134 'date_rss' => $endDateTime->format(DateTime::RSS),
134 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), 135 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime, false),
135 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day, 136 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day,
136 'links' => [], 137 'links' => [],
137 ]; 138 ];
diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php
index bf965929..418d4a49 100644
--- a/application/front/controller/visitor/InstallController.php
+++ b/application/front/controller/visitor/InstallController.php
@@ -56,11 +56,16 @@ class InstallController extends ShaarliVisitorController
56 56
57 $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); 57 $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
58 58
59 $permissions = array_merge(
60 ApplicationUtils::checkResourcePermissions($this->container->conf),
61 ApplicationUtils::checkDatastoreMutex()
62 );
63
59 $this->assignView('php_version', PHP_VERSION); 64 $this->assignView('php_version', PHP_VERSION);
60 $this->assignView('php_eol', format_date($phpEol, false)); 65 $this->assignView('php_eol', format_date($phpEol, false));
61 $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); 66 $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
62 $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); 67 $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
63 $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); 68 $this->assignView('permissions', $permissions);
64 69
65 $this->assignView('pagetitle', t('Install Shaarli')); 70 $this->assignView('pagetitle', t('Install Shaarli'));
66 71
diff --git a/application/helper/ApplicationUtils.php b/application/helper/ApplicationUtils.php
index 212dd8e2..a6c03aae 100644
--- a/application/helper/ApplicationUtils.php
+++ b/application/helper/ApplicationUtils.php
@@ -3,6 +3,8 @@
3namespace Shaarli\Helper; 3namespace Shaarli\Helper;
4 4
5use Exception; 5use Exception;
6use malkusch\lock\exception\LockAcquireException;
7use malkusch\lock\mutex\FlockMutex;
6use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
7 9
8/** 10/**
@@ -252,6 +254,20 @@ class ApplicationUtils
252 return $errors; 254 return $errors;
253 } 255 }
254 256
257 public static function checkDatastoreMutex(): array
258 {
259 $mutex = new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2);
260 try {
261 $mutex->synchronized(function () {
262 return true;
263 });
264 } catch (LockAcquireException $e) {
265 $errors[] = t('Lock can not be acquired on the datastore. You might encounter concurrent access issues.');
266 }
267
268 return $errors ?? [];
269 }
270
255 /** 271 /**
256 * Returns a salted hash representing the current Shaarli version. 272 * Returns a salted hash representing the current Shaarli version.
257 * 273 *
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php
index 5fabc907..05f95812 100644
--- a/application/helper/DailyPageHelper.php
+++ b/application/helper/DailyPageHelper.php
@@ -4,6 +4,9 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Helper; 5namespace Shaarli\Helper;
6 6
7use DatePeriod;
8use DateTimeImmutable;
9use Exception;
7use Shaarli\Bookmark\Bookmark; 10use Shaarli\Bookmark\Bookmark;
8use Slim\Http\Request; 11use Slim\Http\Request;
9 12
@@ -40,31 +43,31 @@ class DailyPageHelper
40 * @param string|null $requestedDate Input string extracted from the request 43 * @param string|null $requestedDate Input string extracted from the request
41 * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date) 44 * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
42 * 45 *
43 * @return \DateTimeImmutable from input or latest bookmark. 46 * @return DateTimeImmutable from input or latest bookmark.
44 * 47 *
45 * @throws \Exception Type not supported. 48 * @throws Exception Type not supported.
46 */ 49 */
47 public static function extractRequestedDateTime( 50 public static function extractRequestedDateTime(
48 string $type, 51 string $type,
49 ?string $requestedDate, 52 ?string $requestedDate,
50 Bookmark $latestBookmark = null 53 Bookmark $latestBookmark = null
51 ): \DateTimeImmutable { 54 ): DateTimeImmutable {
52 $format = static::getFormatByType($type); 55 $format = static::getFormatByType($type);
53 if (empty($requestedDate)) { 56 if (empty($requestedDate)) {
54 return $latestBookmark instanceof Bookmark 57 return $latestBookmark instanceof Bookmark
55 ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) 58 ? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
56 : new \DateTimeImmutable() 59 : new DateTimeImmutable()
57 ; 60 ;
58 } 61 }
59 62
60 // W is not supported by createFromFormat... 63 // W is not supported by createFromFormat...
61 if ($type === static::WEEK) { 64 if ($type === static::WEEK) {
62 return (new \DateTimeImmutable()) 65 return (new DateTimeImmutable())
63 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2)) 66 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
64 ; 67 ;
65 } 68 }
66 69
67 return \DateTimeImmutable::createFromFormat($format, $requestedDate); 70 return DateTimeImmutable::createFromFormat($format, $requestedDate);
68 } 71 }
69 72
70 /** 73 /**
@@ -80,7 +83,7 @@ class DailyPageHelper
80 * 83 *
81 * @see https://www.php.net/manual/en/datetime.format.php 84 * @see https://www.php.net/manual/en/datetime.format.php
82 * 85 *
83 * @throws \Exception Type not supported. 86 * @throws Exception Type not supported.
84 */ 87 */
85 public static function getFormatByType(string $type): string 88 public static function getFormatByType(string $type): string
86 { 89 {
@@ -92,7 +95,7 @@ class DailyPageHelper
92 case static::DAY: 95 case static::DAY:
93 return 'Ymd'; 96 return 'Ymd';
94 default: 97 default:
95 throw new \Exception('Unsupported daily format type'); 98 throw new Exception('Unsupported daily format type');
96 } 99 }
97 } 100 }
98 101
@@ -102,14 +105,14 @@ class DailyPageHelper
102 * and we don't want to alter original datetime. 105 * and we don't want to alter original datetime.
103 * 106 *
104 * @param string $type month/week/day 107 * @param string $type month/week/day
105 * @param \DateTimeImmutable $requested DateTime extracted from request input 108 * @param DateTimeImmutable $requested DateTime extracted from request input
106 * (should come from extractRequestedDateTime) 109 * (should come from extractRequestedDateTime)
107 * 110 *
108 * @return \DateTimeInterface First DateTime of the time period 111 * @return \DateTimeInterface First DateTime of the time period
109 * 112 *
110 * @throws \Exception Type not supported. 113 * @throws Exception Type not supported.
111 */ 114 */
112 public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface 115 public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
113 { 116 {
114 switch ($type) { 117 switch ($type) {
115 case static::MONTH: 118 case static::MONTH:
@@ -119,7 +122,7 @@ class DailyPageHelper
119 case static::DAY: 122 case static::DAY:
120 return $requested->modify('Today midnight'); 123 return $requested->modify('Today midnight');
121 default: 124 default:
122 throw new \Exception('Unsupported daily format type'); 125 throw new Exception('Unsupported daily format type');
123 } 126 }
124 } 127 }
125 128
@@ -129,14 +132,14 @@ class DailyPageHelper
129 * and we don't want to alter original datetime. 132 * and we don't want to alter original datetime.
130 * 133 *
131 * @param string $type month/week/day 134 * @param string $type month/week/day
132 * @param \DateTimeImmutable $requested DateTime extracted from request input 135 * @param DateTimeImmutable $requested DateTime extracted from request input
133 * (should come from extractRequestedDateTime) 136 * (should come from extractRequestedDateTime)
134 * 137 *
135 * @return \DateTimeInterface Last DateTime of the time period 138 * @return \DateTimeInterface Last DateTime of the time period
136 * 139 *
137 * @throws \Exception Type not supported. 140 * @throws Exception Type not supported.
138 */ 141 */
139 public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface 142 public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
140 { 143 {
141 switch ($type) { 144 switch ($type) {
142 case static::MONTH: 145 case static::MONTH:
@@ -146,7 +149,7 @@ class DailyPageHelper
146 case static::DAY: 149 case static::DAY:
147 return $requested->modify('Today 23:59:59'); 150 return $requested->modify('Today 23:59:59');
148 default: 151 default:
149 throw new \Exception('Unsupported daily format type'); 152 throw new Exception('Unsupported daily format type');
150 } 153 }
151 } 154 }
152 155
@@ -154,16 +157,20 @@ class DailyPageHelper
154 * Get localized description of the time period depending on given datetime and type. 157 * Get localized description of the time period depending on given datetime and type.
155 * Example: for a month period, it returns `October, 2020`. 158 * Example: for a month period, it returns `October, 2020`.
156 * 159 *
157 * @param string $type month/week/day 160 * @param string $type month/week/day
158 * @param \DateTimeImmutable $requested DateTime extracted from request input 161 * @param \DateTimeImmutable $requested DateTime extracted from request input
159 * (should come from extractRequestedDateTime) 162 * (should come from extractRequestedDateTime)
163 * @param bool $includeRelative Include relative date description (today, yesterday, etc.)
160 * 164 *
161 * @return string Localized time period description 165 * @return string Localized time period description
162 * 166 *
163 * @throws \Exception Type not supported. 167 * @throws Exception Type not supported.
164 */ 168 */
165 public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string 169 public static function getDescriptionByType(
166 { 170 string $type,
171 \DateTimeImmutable $requested,
172 bool $includeRelative = true
173 ): string {
167 switch ($type) { 174 switch ($type) {
168 case static::MONTH: 175 case static::MONTH:
169 return $requested->format('F') . ', ' . $requested->format('Y'); 176 return $requested->format('F') . ', ' . $requested->format('Y');
@@ -172,14 +179,14 @@ class DailyPageHelper
172 return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')'; 179 return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
173 case static::DAY: 180 case static::DAY:
174 $out = ''; 181 $out = '';
175 if ($requested->format('Ymd') === date('Ymd')) { 182 if ($includeRelative && $requested->format('Ymd') === date('Ymd')) {
176 $out = t('Today') . ' - '; 183 $out = t('Today') . ' - ';
177 } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) { 184 } elseif ($includeRelative && $requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
178 $out = t('Yesterday') . ' - '; 185 $out = t('Yesterday') . ' - ';
179 } 186 }
180 return $out . format_date($requested, false); 187 return $out . format_date($requested, false);
181 default: 188 default:
182 throw new \Exception('Unsupported daily format type'); 189 throw new Exception('Unsupported daily format type');
183 } 190 }
184 } 191 }
185 192
@@ -190,7 +197,7 @@ class DailyPageHelper
190 * 197 *
191 * @return int number of elements 198 * @return int number of elements
192 * 199 *
193 * @throws \Exception Type not supported. 200 * @throws Exception Type not supported.
194 */ 201 */
195 public static function getRssLengthByType(string $type): int 202 public static function getRssLengthByType(string $type): int
196 { 203 {
@@ -202,7 +209,28 @@ class DailyPageHelper
202 case static::DAY: 209 case static::DAY:
203 return 30; // ~1 month 210 return 30; // ~1 month
204 default: 211 default:
205 throw new \Exception('Unsupported daily format type'); 212 throw new Exception('Unsupported daily format type');
206 } 213 }
207 } 214 }
215
216 /**
217 * Get the number of items to display in the RSS feed depending on the given type.
218 *
219 * @param string $type month/week/day
220 * @param ?DateTimeImmutable $requested Currently only used for UT
221 *
222 * @return DatePeriod number of elements
223 *
224 * @throws Exception Type not supported.
225 */
226 public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod
227 {
228 $requested = $requested ?? new DateTimeImmutable();
229
230 return new DatePeriod(
231 static::getStartDateTimeByType($type, $requested),
232 new \DateInterval('P1D'),
233 static::getEndDateTimeByType($type, $requested)
234 );
235 }
208} 236}
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php
index 3ea55728..7fc0cb04 100644
--- a/application/plugin/PluginManager.php
+++ b/application/plugin/PluginManager.php
@@ -4,6 +4,7 @@ namespace Shaarli\Plugin;
4 4
5use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
6use Shaarli\Plugin\Exception\PluginFileNotFoundException; 6use Shaarli\Plugin\Exception\PluginFileNotFoundException;
7use Shaarli\Plugin\Exception\PluginInvalidRouteException;
7 8
8/** 9/**
9 * Class PluginManager 10 * Class PluginManager
@@ -26,6 +27,14 @@ class PluginManager
26 */ 27 */
27 private $loadedPlugins = []; 28 private $loadedPlugins = [];
28 29
30 /** @var array List of registered routes. Contains keys:
31 * - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE
32 * - `route` (path): without prefix, e.g. `/up/{variable}`
33 * It will be later prefixed by `/plugin/<plugin name>/`.
34 * - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`.
35 */
36 protected $registeredRoutes = [];
37
29 /** 38 /**
30 * @var ConfigManager Configuration Manager instance. 39 * @var ConfigManager Configuration Manager instance.
31 */ 40 */
@@ -86,6 +95,9 @@ class PluginManager
86 $this->loadPlugin($dirs[$index], $plugin); 95 $this->loadPlugin($dirs[$index], $plugin);
87 } catch (PluginFileNotFoundException $e) { 96 } catch (PluginFileNotFoundException $e) {
88 error_log($e->getMessage()); 97 error_log($e->getMessage());
98 } catch (\Throwable $e) {
99 $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
100 $this->errors = array_unique(array_merge($this->errors, [$error]));
89 } 101 }
90 } 102 }
91 } 103 }
@@ -166,6 +178,22 @@ class PluginManager
166 } 178 }
167 } 179 }
168 180
181 $registerRouteFunction = $pluginName . '_register_routes';
182 $routes = null;
183 if (function_exists($registerRouteFunction)) {
184 $routes = call_user_func($registerRouteFunction);
185 }
186
187 if ($routes !== null) {
188 foreach ($routes as $route) {
189 if (static::validateRouteRegistration($route)) {
190 $this->registeredRoutes[$pluginName][] = $route;
191 } else {
192 throw new PluginInvalidRouteException($pluginName);
193 }
194 }
195 }
196
169 $this->loadedPlugins[] = $pluginName; 197 $this->loadedPlugins[] = $pluginName;
170 } 198 }
171 199
@@ -238,6 +266,14 @@ class PluginManager
238 } 266 }
239 267
240 /** 268 /**
269 * @return array List of registered custom routes by plugins.
270 */
271 public function getRegisteredRoutes(): array
272 {
273 return $this->registeredRoutes;
274 }
275
276 /**
241 * Return the list of encountered errors. 277 * Return the list of encountered errors.
242 * 278 *
243 * @return array List of errors (empty array if none exists). 279 * @return array List of errors (empty array if none exists).
@@ -246,4 +282,32 @@ class PluginManager
246 { 282 {
247 return $this->errors; 283 return $this->errors;
248 } 284 }
285
286 /**
287 * Checks whether provided input is valid to register a new route.
288 * It must contain keys `method`, `route`, `callable` (all strings).
289 *
290 * @param string[] $input
291 *
292 * @return bool
293 */
294 protected static function validateRouteRegistration(array $input): bool
295 {
296 if (
297 !array_key_exists('method', $input)
298 || !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
299 ) {
300 return false;
301 }
302
303 if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) {
304 return false;
305 }
306
307 if (!array_key_exists('callable', $input)) {
308 return false;
309 }
310
311 return true;
312 }
249} 313}
diff --git a/application/plugin/exception/PluginInvalidRouteException.php b/application/plugin/exception/PluginInvalidRouteException.php
new file mode 100644
index 00000000..6ba9bc43
--- /dev/null
+++ b/application/plugin/exception/PluginInvalidRouteException.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Plugin\Exception;
6
7use Exception;
8
9/**
10 * Class PluginFileNotFoundException
11 *
12 * Raise when plugin files can't be found.
13 */
14class PluginInvalidRouteException extends Exception
15{
16 /**
17 * Construct exception with plugin name.
18 * Generate message.
19 *
20 * @param string $pluginName name of the plugin not found
21 */
22 public function __construct()
23 {
24 $this->message = 'trying to register invalid route.';
25 }
26}
diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php
index 97805c35..fe74bf27 100644
--- a/application/render/PageCacheManager.php
+++ b/application/render/PageCacheManager.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Render; 3namespace Shaarli\Render;
4 4
5use DatePeriod;
5use Shaarli\Feed\CachedPage; 6use Shaarli\Feed\CachedPage;
6 7
7/** 8/**
@@ -49,12 +50,21 @@ class PageCacheManager
49 $this->purgeCachedPages(); 50 $this->purgeCachedPages();
50 } 51 }
51 52
52 public function getCachePage(string $pageUrl): CachedPage 53 /**
54 * Get CachedPage instance for provided URL.
55 *
56 * @param string $pageUrl
57 * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
58 *
59 * @return CachedPage
60 */
61 public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage
53 { 62 {
54 return new CachedPage( 63 return new CachedPage(
55 $this->pageCacheDir, 64 $this->pageCacheDir,
56 $pageUrl, 65 $pageUrl,
57 false === $this->isLoggedIn 66 false === $this->isLoggedIn,
67 $validityPeriod
58 ); 68 );
59 } 69 }
60} 70}
diff --git a/doc/md/REST-API.md b/doc/md/REST-API.md
index 01071d8e..2a36ea29 100644
--- a/doc/md/REST-API.md
+++ b/doc/md/REST-API.md
@@ -73,7 +73,7 @@ var_dump(getInfo($baseUrl, $secret));
73### Authentication 73### Authentication
74 74
75- All requests to Shaarli's API must include a **JWT token** to verify their authenticity. 75- All requests to Shaarli's API must include a **JWT token** to verify their authenticity.
76- This token must be included as an HTTP header called `Authentication: Bearer <jwt token>`. 76- This token must be included as an HTTP header called `Authorization: Bearer <jwt token>`.
77- JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64: 77- JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64:
78 78
79``` 79```
diff --git a/doc/md/dev/Plugin-system.md b/doc/md/dev/Plugin-system.md
index f09fadc2..79654011 100644
--- a/doc/md/dev/Plugin-system.md
+++ b/doc/md/dev/Plugin-system.md
@@ -139,6 +139,31 @@ Each file contain two keys:
139 139
140> Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file. 140> Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file.
141 141
142### Register plugin's routes
143
144Shaarli lets you register custom Slim routes for your plugin.
145
146To register a route, the plugin must include a function called `function <plugin_name>_register_routes(): array`.
147
148This method must return an array of routes, each entry must contain the following keys:
149
150 - `method`: HTTP method, `GET/POST/PUT/PATCH/DELETE`
151 - `route` (path): without prefix, e.g. `/up/{variable}`
152 It will be later prefixed by `/plugin/<plugin name>/`.
153 - `callable` string, function name or FQN class's method to execute, e.g. `demo_plugin_custom_controller`.
154
155Callable functions or methods must have `Slim\Http\Request` and `Slim\Http\Response` parameters
156and return a `Slim\Http\Response`. We recommend creating a dedicated class and extend either
157`ShaarliVisitorController` or `ShaarliAdminController` to use helper functions they provide.
158
159A dedicated plugin template is available for rendering content: `pluginscontent.html` using `content` placeholder.
160
161> **Warning**: plugins are not able to use RainTPL template engine for their content due to technical restrictions.
162> RainTPL does not allow to register multiple template folders, so all HTML rendering must be done within plugin
163> custom controller.
164
165Check out the `demo_plugin` for a live example: `GET <shaarli_url>/plugin/demo_plugin/custom`.
166
142### Understanding relative paths 167### Understanding relative paths
143 168
144Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder. 169Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder.
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
index 26dede4e..01492af4 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -1,8 +1,8 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: Shaarli\n" 3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2020-11-09 14:39+0100\n" 4"POT-Creation-Date: 2020-11-24 13:13+0100\n"
5"PO-Revision-Date: 2020-11-09 14:42+0100\n" 5"PO-Revision-Date: 2020-11-24 13:14+0100\n"
6"Last-Translator: \n" 6"Last-Translator: \n"
7"Language-Team: Shaarli\n" 7"Language-Team: Shaarli\n"
8"Language: fr_FR\n" 8"Language: fr_FR\n"
@@ -20,31 +20,31 @@ msgstr ""
20"X-Poedit-SearchPath-3: init.php\n" 20"X-Poedit-SearchPath-3: init.php\n"
21"X-Poedit-SearchPath-4: plugins\n" 21"X-Poedit-SearchPath-4: plugins\n"
22 22
23#: application/History.php:180 23#: application/History.php:181
24msgid "History file isn't readable or writable" 24msgid "History file isn't readable or writable"
25msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" 25msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
26 26
27#: application/History.php:191 27#: application/History.php:192
28msgid "Could not parse history file" 28msgid "Could not parse history file"
29msgstr "Format incorrect pour le fichier d'historique" 29msgstr "Format incorrect pour le fichier d'historique"
30 30
31#: application/Languages.php:181 31#: application/Languages.php:184
32msgid "Automatic" 32msgid "Automatic"
33msgstr "Automatique" 33msgstr "Automatique"
34 34
35#: application/Languages.php:182 35#: application/Languages.php:185
36msgid "German" 36msgid "German"
37msgstr "Allemand" 37msgstr "Allemand"
38 38
39#: application/Languages.php:183 39#: application/Languages.php:186
40msgid "English" 40msgid "English"
41msgstr "Anglais" 41msgstr "Anglais"
42 42
43#: application/Languages.php:184 43#: application/Languages.php:187
44msgid "French" 44msgid "French"
45msgstr "Français" 45msgstr "Français"
46 46
47#: application/Languages.php:185 47#: application/Languages.php:188
48msgid "Japanese" 48msgid "Japanese"
49msgstr "Japonais" 49msgstr "Japonais"
50 50
@@ -56,46 +56,46 @@ msgstr ""
56"l'extension php-gd doit être chargée pour utiliser les miniatures. Les " 56"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
57"miniatures sont désormais désactivées. Rechargez la page." 57"miniatures sont désormais désactivées. Rechargez la page."
58 58
59#: application/Utils.php:402 59#: application/Utils.php:405
60msgid "Setting not set" 60msgid "Setting not set"
61msgstr "Paramètre non défini" 61msgstr "Paramètre non défini"
62 62
63#: application/Utils.php:409 63#: application/Utils.php:412
64msgid "Unlimited" 64msgid "Unlimited"
65msgstr "Illimité" 65msgstr "Illimité"
66 66
67#: application/Utils.php:412 67#: application/Utils.php:415
68msgid "B" 68msgid "B"
69msgstr "o" 69msgstr "o"
70 70
71#: application/Utils.php:412 71#: application/Utils.php:415
72msgid "kiB" 72msgid "kiB"
73msgstr "ko" 73msgstr "ko"
74 74
75#: application/Utils.php:412 75#: application/Utils.php:415
76msgid "MiB" 76msgid "MiB"
77msgstr "Mo" 77msgstr "Mo"
78 78
79#: application/Utils.php:412 79#: application/Utils.php:415
80msgid "GiB" 80msgid "GiB"
81msgstr "Go" 81msgstr "Go"
82 82
83#: application/bookmark/BookmarkFileService.php:183 83#: application/bookmark/BookmarkFileService.php:185
84#: application/bookmark/BookmarkFileService.php:205 84#: application/bookmark/BookmarkFileService.php:207
85#: application/bookmark/BookmarkFileService.php:227 85#: application/bookmark/BookmarkFileService.php:229
86#: application/bookmark/BookmarkFileService.php:241 86#: application/bookmark/BookmarkFileService.php:243
87msgid "You're not authorized to alter the datastore" 87msgid "You're not authorized to alter the datastore"
88msgstr "Vous n'êtes pas autorisé à modifier les données" 88msgstr "Vous n'êtes pas autorisé à modifier les données"
89 89
90#: application/bookmark/BookmarkFileService.php:208 90#: application/bookmark/BookmarkFileService.php:210
91msgid "This bookmarks already exists" 91msgid "This bookmarks already exists"
92msgstr "Ce marque-page existe déjà" 92msgstr "Ce marque-page existe déjà"
93 93
94#: application/bookmark/BookmarkInitializer.php:39 94#: application/bookmark/BookmarkInitializer.php:42
95msgid "(private bookmark with thumbnail demo)" 95msgid "(private bookmark with thumbnail demo)"
96msgstr "(marque page privé avec une miniature)" 96msgstr "(marque page privé avec une miniature)"
97 97
98#: application/bookmark/BookmarkInitializer.php:42 98#: application/bookmark/BookmarkInitializer.php:45
99msgid "" 99msgid ""
100"Shaarli will automatically pick up the thumbnail for links to a variety of " 100"Shaarli will automatically pick up the thumbnail for links to a variety of "
101"websites.\n" 101"websites.\n"
@@ -118,11 +118,11 @@ msgstr ""
118"\n" 118"\n"
119"Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n" 119"Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n"
120 120
121#: application/bookmark/BookmarkInitializer.php:55 121#: application/bookmark/BookmarkInitializer.php:58
122msgid "Note: Shaare descriptions" 122msgid "Note: Shaare descriptions"
123msgstr "Note : Description des Shaares" 123msgstr "Note : Description des Shaares"
124 124
125#: application/bookmark/BookmarkInitializer.php:57 125#: application/bookmark/BookmarkInitializer.php:60
126msgid "" 126msgid ""
127"Adding a shaare without entering a URL creates a text-only \"note\" post " 127"Adding a shaare without entering a URL creates a text-only \"note\" post "
128"such as this one.\n" 128"such as this one.\n"
@@ -186,7 +186,7 @@ msgstr ""
186"| Citron | Fruit | Jaune | 30 |\n" 186"| Citron | Fruit | Jaune | 30 |\n"
187"| Carotte | Légume | Orange | 14 |\n" 187"| Carotte | Légume | Orange | 14 |\n"
188 188
189#: application/bookmark/BookmarkInitializer.php:91 189#: application/bookmark/BookmarkInitializer.php:94
190#: application/legacy/LegacyLinkDB.php:246 190#: application/legacy/LegacyLinkDB.php:246
191#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 191#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
192#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 192#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
@@ -198,7 +198,7 @@ msgstr ""
198"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " 198"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
199"données" 199"données"
200 200
201#: application/bookmark/BookmarkInitializer.php:94 201#: application/bookmark/BookmarkInitializer.php:97
202msgid "" 202msgid ""
203"Welcome to Shaarli!\n" 203"Welcome to Shaarli!\n"
204"\n" 204"\n"
@@ -247,11 +247,11 @@ msgstr ""
247"issues) si vous avez une suggestion ou si vous rencontrez un problème.\n" 247"issues) si vous avez une suggestion ou si vous rencontrez un problème.\n"
248" \n" 248" \n"
249 249
250#: application/bookmark/exception/BookmarkNotFoundException.php:13 250#: application/bookmark/exception/BookmarkNotFoundException.php:14
251msgid "The link you are trying to reach does not exist or has been deleted." 251msgid "The link you are trying to reach does not exist or has been deleted."
252msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." 252msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
253 253
254#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129 254#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:131
255msgid "" 255msgid ""
256"Shaarli could not create the config file. Please make sure Shaarli has the " 256"Shaarli could not create the config file. Please make sure Shaarli has the "
257"right to write in the folder is it installed in." 257"right to write in the folder is it installed in."
@@ -259,12 +259,12 @@ msgstr ""
259"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " 259"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
260"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." 260"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
261 261
262#: application/config/ConfigManager.php:136 262#: application/config/ConfigManager.php:137
263#: application/config/ConfigManager.php:163 263#: application/config/ConfigManager.php:164
264msgid "Invalid setting key parameter. String expected, got: " 264msgid "Invalid setting key parameter. String expected, got: "
265msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " 265msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
266 266
267#: application/config/exception/MissingFieldConfigException.php:21 267#: application/config/exception/MissingFieldConfigException.php:20
268#, php-format 268#, php-format
269msgid "Configuration value is required for %s" 269msgid "Configuration value is required for %s"
270msgstr "Le paramètre %s est obligatoire" 270msgstr "Le paramètre %s est obligatoire"
@@ -274,48 +274,48 @@ msgid "An error occurred while trying to save plugins loading order."
274msgstr "" 274msgstr ""
275"Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions." 275"Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions."
276 276
277#: application/config/exception/UnauthorizedConfigException.php:16 277#: application/config/exception/UnauthorizedConfigException.php:15
278msgid "You are not authorized to alter config." 278msgid "You are not authorized to alter config."
279msgstr "Vous n'êtes pas autorisé à modifier la configuration." 279msgstr "Vous n'êtes pas autorisé à modifier la configuration."
280 280
281#: application/exceptions/IOException.php:22 281#: application/exceptions/IOException.php:23
282msgid "Error accessing" 282msgid "Error accessing"
283msgstr "Une erreur s'est produite en accédant à" 283msgstr "Une erreur s'est produite en accédant à"
284 284
285#: application/feed/FeedBuilder.php:179 285#: application/feed/FeedBuilder.php:180
286msgid "Direct link" 286msgid "Direct link"
287msgstr "Liens directs" 287msgstr "Liens directs"
288 288
289#: application/feed/FeedBuilder.php:181 289#: application/feed/FeedBuilder.php:182
290#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 290#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
291#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 291#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
292#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 292#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
293msgid "Permalink" 293msgid "Permalink"
294msgstr "Permalien" 294msgstr "Permalien"
295 295
296#: application/front/controller/admin/ConfigureController.php:54 296#: application/front/controller/admin/ConfigureController.php:56
297#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 297#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
298msgid "Configure" 298msgid "Configure"
299msgstr "Configurer" 299msgstr "Configurer"
300 300
301#: application/front/controller/admin/ConfigureController.php:102 301#: application/front/controller/admin/ConfigureController.php:106
302#: application/legacy/LegacyUpdater.php:537 302#: application/legacy/LegacyUpdater.php:539
303msgid "You have enabled or changed thumbnails mode." 303msgid "You have enabled or changed thumbnails mode."
304msgstr "Vous avez activé ou changé le mode de miniatures." 304msgstr "Vous avez activé ou changé le mode de miniatures."
305 305
306#: application/front/controller/admin/ConfigureController.php:103 306#: application/front/controller/admin/ConfigureController.php:108
307#: application/front/controller/admin/ServerController.php:75 307#: application/front/controller/admin/ServerController.php:76
308#: application/legacy/LegacyUpdater.php:538 308#: application/legacy/LegacyUpdater.php:540
309msgid "Please synchronize them." 309msgid "Please synchronize them."
310msgstr "Merci de les synchroniser." 310msgstr "Merci de les synchroniser."
311 311
312#: application/front/controller/admin/ConfigureController.php:113 312#: application/front/controller/admin/ConfigureController.php:119
313#: application/front/controller/visitor/InstallController.php:146 313#: application/front/controller/visitor/InstallController.php:149
314msgid "Error while writing config file after configuration update." 314msgid "Error while writing config file after configuration update."
315msgstr "" 315msgstr ""
316"Une erreur s'est produite lors de la sauvegarde du fichier de configuration." 316"Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
317 317
318#: application/front/controller/admin/ConfigureController.php:122 318#: application/front/controller/admin/ConfigureController.php:128
319msgid "Configuration was saved." 319msgid "Configuration was saved."
320msgstr "La configuration a été sauvegardée." 320msgstr "La configuration a été sauvegardée."
321 321
@@ -433,7 +433,7 @@ msgstr "Administration serveur"
433msgid "Thumbnails cache has been cleared." 433msgid "Thumbnails cache has been cleared."
434msgstr "Le cache des miniatures a été vidé." 434msgstr "Le cache des miniatures a été vidé."
435 435
436#: application/front/controller/admin/ServerController.php:83 436#: application/front/controller/admin/ServerController.php:85
437msgid "Shaarli's cache folder has been cleared!" 437msgid "Shaarli's cache folder has been cleared!"
438msgstr "Le dossier de cache de Shaarli a été vidé !" 438msgstr "Le dossier de cache de Shaarli a été vidé !"
439 439
@@ -459,18 +459,18 @@ msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
459msgid "Invalid visibility provided." 459msgid "Invalid visibility provided."
460msgstr "Visibilité du lien non valide." 460msgstr "Visibilité du lien non valide."
461 461
462#: application/front/controller/admin/ShaarePublishController.php:171 462#: application/front/controller/admin/ShaarePublishController.php:173
463#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 463#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
464msgid "Edit" 464msgid "Edit"
465msgstr "Modifier" 465msgstr "Modifier"
466 466
467#: application/front/controller/admin/ShaarePublishController.php:174 467#: application/front/controller/admin/ShaarePublishController.php:176
468#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 468#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
469#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 469#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
470msgid "Shaare" 470msgid "Shaare"
471msgstr "Shaare" 471msgstr "Shaare"
472 472
473#: application/front/controller/admin/ShaarePublishController.php:205 473#: application/front/controller/admin/ShaarePublishController.php:208
474msgid "Note: " 474msgid "Note: "
475msgstr "Note : " 475msgstr "Note : "
476 476
@@ -485,7 +485,7 @@ msgstr "Mise à jour des miniatures"
485msgid "Tools" 485msgid "Tools"
486msgstr "Outils" 486msgstr "Outils"
487 487
488#: application/front/controller/visitor/BookmarkListController.php:120 488#: application/front/controller/visitor/BookmarkListController.php:121
489msgid "Search: " 489msgid "Search: "
490msgstr "Recherche : " 490msgstr "Recherche : "
491 491
@@ -535,12 +535,12 @@ msgstr "Une erreur inattendue s'est produite."
535msgid "Requested page could not be found." 535msgid "Requested page could not be found."
536msgstr "La page demandée n'a pas pu être trouvée." 536msgstr "La page demandée n'a pas pu être trouvée."
537 537
538#: application/front/controller/visitor/InstallController.php:64 538#: application/front/controller/visitor/InstallController.php:65
539#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 539#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
540msgid "Install Shaarli" 540msgid "Install Shaarli"
541msgstr "Installation de Shaarli" 541msgstr "Installation de Shaarli"
542 542
543#: application/front/controller/visitor/InstallController.php:83 543#: application/front/controller/visitor/InstallController.php:85
544#, php-format 544#, php-format
545msgid "" 545msgid ""
546"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " 546"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@@ -559,14 +559,14 @@ msgstr ""
559"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " 559"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
560"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" 560"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
561 561
562#: application/front/controller/visitor/InstallController.php:154 562#: application/front/controller/visitor/InstallController.php:157
563msgid "" 563msgid ""
564"Shaarli is now configured. Please login and start shaaring your bookmarks!" 564"Shaarli is now configured. Please login and start shaaring your bookmarks!"
565msgstr "" 565msgstr ""
566"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " 566"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
567"shaare vos liens !" 567"shaare vos liens !"
568 568
569#: application/front/controller/visitor/InstallController.php:168 569#: application/front/controller/visitor/InstallController.php:171
570msgid "Insufficient permissions:" 570msgid "Insufficient permissions:"
571msgstr "Permissions insuffisantes :" 571msgstr "Permissions insuffisantes :"
572 572
@@ -580,7 +580,7 @@ msgstr "Permissions insuffisantes :"
580msgid "Login" 580msgid "Login"
581msgstr "Connexion" 581msgstr "Connexion"
582 582
583#: application/front/controller/visitor/LoginController.php:77 583#: application/front/controller/visitor/LoginController.php:78
584msgid "Wrong login/password." 584msgid "Wrong login/password."
585msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." 585msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
586 586
@@ -620,7 +620,7 @@ msgstr ""
620msgid "Wrong token." 620msgid "Wrong token."
621msgstr "Jeton invalide." 621msgstr "Jeton invalide."
622 622
623#: application/helper/ApplicationUtils.php:162 623#: application/helper/ApplicationUtils.php:165
624#, php-format 624#, php-format
625msgid "" 625msgid ""
626"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " 626"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
@@ -631,52 +631,60 @@ msgstr ""
631"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " 631"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
632"connues et devrait être mise à jour au plus tôt." 632"connues et devrait être mise à jour au plus tôt."
633 633
634#: application/helper/ApplicationUtils.php:195 634#: application/helper/ApplicationUtils.php:200
635#: application/helper/ApplicationUtils.php:215 635#: application/helper/ApplicationUtils.php:220
636msgid "directory is not readable" 636msgid "directory is not readable"
637msgstr "le répertoire n'est pas accessible en lecture" 637msgstr "le répertoire n'est pas accessible en lecture"
638 638
639#: application/helper/ApplicationUtils.php:218 639#: application/helper/ApplicationUtils.php:223
640msgid "directory is not writable" 640msgid "directory is not writable"
641msgstr "le répertoire n'est pas accessible en écriture" 641msgstr "le répertoire n'est pas accessible en écriture"
642 642
643#: application/helper/ApplicationUtils.php:240 643#: application/helper/ApplicationUtils.php:247
644msgid "file is not readable" 644msgid "file is not readable"
645msgstr "le fichier n'est pas accessible en lecture" 645msgstr "le fichier n'est pas accessible en lecture"
646 646
647#: application/helper/ApplicationUtils.php:243 647#: application/helper/ApplicationUtils.php:250
648msgid "file is not writable" 648msgid "file is not writable"
649msgstr "le fichier n'est pas accessible en écriture" 649msgstr "le fichier n'est pas accessible en écriture"
650 650
651#: application/helper/ApplicationUtils.php:277 651#: application/helper/ApplicationUtils.php:260
652msgid ""
653"Lock can not be acquired on the datastore. You might encounter concurrent "
654"access issues."
655msgstr ""
656"Le fichier datastore ne peut pas être verrouillé. Vous pourriez rencontrer "
657"des problèmes d'accès concurrents."
658
659#: application/helper/ApplicationUtils.php:293
652msgid "Configuration parsing" 660msgid "Configuration parsing"
653msgstr "Chargement de la configuration" 661msgstr "Chargement de la configuration"
654 662
655#: application/helper/ApplicationUtils.php:278 663#: application/helper/ApplicationUtils.php:294
656msgid "Slim Framework (routing, etc.)" 664msgid "Slim Framework (routing, etc.)"
657msgstr "Slim Framwork (routage, etc.)" 665msgstr "Slim Framwork (routage, etc.)"
658 666
659#: application/helper/ApplicationUtils.php:279 667#: application/helper/ApplicationUtils.php:295
660msgid "Multibyte (Unicode) string support" 668msgid "Multibyte (Unicode) string support"
661msgstr "Support des chaînes de caractère multibytes (Unicode)" 669msgstr "Support des chaînes de caractère multibytes (Unicode)"
662 670
663#: application/helper/ApplicationUtils.php:280 671#: application/helper/ApplicationUtils.php:296
664msgid "Required to use thumbnails" 672msgid "Required to use thumbnails"
665msgstr "Obligatoire pour utiliser les miniatures" 673msgstr "Obligatoire pour utiliser les miniatures"
666 674
667#: application/helper/ApplicationUtils.php:281 675#: application/helper/ApplicationUtils.php:297
668msgid "Localized text sorting (e.g. e->è->f)" 676msgid "Localized text sorting (e.g. e->è->f)"
669msgstr "Tri des textes traduits (ex : e->è->f)" 677msgstr "Tri des textes traduits (ex : e->è->f)"
670 678
671#: application/helper/ApplicationUtils.php:282 679#: application/helper/ApplicationUtils.php:298
672msgid "Better retrieval of bookmark metadata and thumbnail" 680msgid "Better retrieval of bookmark metadata and thumbnail"
673msgstr "Meilleure récupération des meta-données des marque-pages et minatures" 681msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
674 682
675#: application/helper/ApplicationUtils.php:283 683#: application/helper/ApplicationUtils.php:299
676msgid "Use the translation system in gettext mode" 684msgid "Use the translation system in gettext mode"
677msgstr "Utiliser le système de traduction en mode gettext" 685msgstr "Utiliser le système de traduction en mode gettext"
678 686
679#: application/helper/ApplicationUtils.php:284 687#: application/helper/ApplicationUtils.php:300
680msgid "Login using LDAP server" 688msgid "Login using LDAP server"
681msgstr "Authentification via un serveur LDAP" 689msgstr "Authentification via un serveur LDAP"
682 690
@@ -750,7 +758,7 @@ msgstr ""
750msgid "Couldn't retrieve updater class methods." 758msgid "Couldn't retrieve updater class methods."
751msgstr "Impossible de récupérer les méthodes de la classe Updater." 759msgstr "Impossible de récupérer les méthodes de la classe Updater."
752 760
753#: application/legacy/LegacyUpdater.php:538 761#: application/legacy/LegacyUpdater.php:540
754msgid "<a href=\"./admin/thumbnails\">" 762msgid "<a href=\"./admin/thumbnails\">"
755msgstr "<a href=\"./admin/thumbnails\">" 763msgstr "<a href=\"./admin/thumbnails\">"
756 764
@@ -776,11 +784,11 @@ msgstr ""
776"a été importé avec succès en %d secondes : %d liens importés, %d liens " 784"a été importé avec succès en %d secondes : %d liens importés, %d liens "
777"écrasés, %d liens ignorés." 785"écrasés, %d liens ignorés."
778 786
779#: application/plugin/PluginManager.php:124 787#: application/plugin/PluginManager.php:125
780msgid " [plugin incompatibility]: " 788msgid " [plugin incompatibility]: "
781msgstr " [incompatibilité de l'extension] : " 789msgstr " [incompatibilité de l'extension] : "
782 790
783#: application/plugin/exception/PluginFileNotFoundException.php:21 791#: application/plugin/exception/PluginFileNotFoundException.php:22
784#, php-format 792#, php-format
785msgid "Plugin \"%s\" files not found." 793msgid "Plugin \"%s\" files not found."
786msgstr "Les fichiers de l'extension \"%s\" sont introuvables." 794msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
@@ -794,7 +802,7 @@ msgstr "Impossible de purger %s : le répertoire n'existe pas"
794msgid "An error occurred while running the update " 802msgid "An error occurred while running the update "
795msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " 803msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
796 804
797#: index.php:80 805#: index.php:81
798msgid "Shared bookmarks on " 806msgid "Shared bookmarks on "
799msgstr "Liens partagés sur " 807msgstr "Liens partagés sur "
800 808
@@ -811,11 +819,11 @@ msgstr "Shaare"
811msgid "Adds the addlink input on the linklist page." 819msgid "Adds the addlink input on the linklist page."
812msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." 820msgstr "Ajoute le formulaire d'ajout de liens sur la page principale."
813 821
814#: plugins/archiveorg/archiveorg.php:28 822#: plugins/archiveorg/archiveorg.php:29
815msgid "View on archive.org" 823msgid "View on archive.org"
816msgstr "Voir sur archive.org" 824msgstr "Voir sur archive.org"
817 825
818#: plugins/archiveorg/archiveorg.php:41 826#: plugins/archiveorg/archiveorg.php:42
819msgid "For each link, add an Archive.org icon." 827msgid "For each link, add an Archive.org icon."
820msgstr "Pour chaque lien, ajoute une icône pour Archive.org." 828msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
821 829
@@ -845,7 +853,7 @@ msgstr "Couleur de fond (gris léger)"
845msgid "Dark main color (e.g. visited links)" 853msgid "Dark main color (e.g. visited links)"
846msgstr "Couleur principale sombre (ex : les liens visités)" 854msgstr "Couleur principale sombre (ex : les liens visités)"
847 855
848#: plugins/demo_plugin/demo_plugin.php:477 856#: plugins/demo_plugin/demo_plugin.php:478
849msgid "" 857msgid ""
850"A demo plugin covering all use cases for template designers and plugin " 858"A demo plugin covering all use cases for template designers and plugin "
851"developers." 859"developers."
@@ -853,11 +861,11 @@ msgstr ""
853"Une extension de démonstration couvrant tous les cas d'utilisation pour les " 861"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
854"designers de thèmes et les développeurs d'extensions." 862"designers de thèmes et les développeurs d'extensions."
855 863
856#: plugins/demo_plugin/demo_plugin.php:478 864#: plugins/demo_plugin/demo_plugin.php:479
857msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." 865msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
858msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé." 866msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé."
859 867
860#: plugins/demo_plugin/demo_plugin.php:479 868#: plugins/demo_plugin/demo_plugin.php:480
861msgid "Other demo parameter" 869msgid "Other demo parameter"
862msgstr "Un autre paramètre de démo" 870msgstr "Un autre paramètre de démo"
863 871
@@ -879,7 +887,7 @@ msgstr ""
879msgid "Isso server URL (without 'http://')" 887msgid "Isso server URL (without 'http://')"
880msgstr "URL du serveur Isso (sans 'http://')" 888msgstr "URL du serveur Isso (sans 'http://')"
881 889
882#: plugins/piwik/piwik.php:23 890#: plugins/piwik/piwik.php:24
883msgid "" 891msgid ""
884"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " 892"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
885"administration page." 893"administration page."
@@ -887,27 +895,27 @@ msgstr ""
887"Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et " 895"Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et "
888"PIWIK_SITEID dans la page d'administration des extensions." 896"PIWIK_SITEID dans la page d'administration des extensions."
889 897
890#: plugins/piwik/piwik.php:72 898#: plugins/piwik/piwik.php:73
891msgid "A plugin that adds Piwik tracking code to Shaarli pages." 899msgid "A plugin that adds Piwik tracking code to Shaarli pages."
892msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli." 900msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli."
893 901
894#: plugins/piwik/piwik.php:73 902#: plugins/piwik/piwik.php:74
895msgid "Piwik URL" 903msgid "Piwik URL"
896msgstr "URL de Piwik" 904msgstr "URL de Piwik"
897 905
898#: plugins/piwik/piwik.php:74 906#: plugins/piwik/piwik.php:75
899msgid "Piwik site ID" 907msgid "Piwik site ID"
900msgstr "Site ID de Piwik" 908msgstr "Site ID de Piwik"
901 909
902#: plugins/playvideos/playvideos.php:25 910#: plugins/playvideos/playvideos.php:26
903msgid "Video player" 911msgid "Video player"
904msgstr "Lecteur vidéo" 912msgstr "Lecteur vidéo"
905 913
906#: plugins/playvideos/playvideos.php:28 914#: plugins/playvideos/playvideos.php:29
907msgid "Play Videos" 915msgid "Play Videos"
908msgstr "Jouer les vidéos" 916msgstr "Jouer les vidéos"
909 917
910#: plugins/playvideos/playvideos.php:59 918#: plugins/playvideos/playvideos.php:60
911msgid "Add a button in the toolbar allowing to watch all videos." 919msgid "Add a button in the toolbar allowing to watch all videos."
912msgstr "" 920msgstr ""
913"Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos." 921"Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos."
@@ -935,11 +943,11 @@ msgstr "Mauvaise réponse du hub %s"
935msgid "Enable PubSubHubbub feed publishing." 943msgid "Enable PubSubHubbub feed publishing."
936msgstr "Active la publication de flux vers PubSubHubbub." 944msgstr "Active la publication de flux vers PubSubHubbub."
937 945
938#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:71 946#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72
939msgid "For each link, add a QRCode icon." 947msgid "For each link, add a QRCode icon."
940msgstr "Pour chaque lien, ajouter une icône de QRCode." 948msgstr "Pour chaque lien, ajouter une icône de QRCode."
941 949
942#: plugins/wallabag/wallabag.php:21 950#: plugins/wallabag/wallabag.php:22
943msgid "" 951msgid ""
944"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " 952"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
945"plugin administration page." 953"plugin administration page."
@@ -947,15 +955,15 @@ msgstr ""
947"Erreur de l'extension Wallabag : Merci de définir le paramètre « " 955"Erreur de l'extension Wallabag : Merci de définir le paramètre « "
948"WALLABAG_URL » dans la page d'administration des extensions." 956"WALLABAG_URL » dans la page d'administration des extensions."
949 957
950#: plugins/wallabag/wallabag.php:48 958#: plugins/wallabag/wallabag.php:49
951msgid "Save to wallabag" 959msgid "Save to wallabag"
952msgstr "Sauvegarder dans Wallabag" 960msgstr "Sauvegarder dans Wallabag"
953 961
954#: plugins/wallabag/wallabag.php:72 962#: plugins/wallabag/wallabag.php:73
955msgid "Wallabag API URL" 963msgid "Wallabag API URL"
956msgstr "URL de l'API Wallabag" 964msgstr "URL de l'API Wallabag"
957 965
958#: plugins/wallabag/wallabag.php:73 966#: plugins/wallabag/wallabag.php:74
959msgid "Wallabag API version (1 or 2)" 967msgid "Wallabag API version (1 or 2)"
960msgstr "Version de l'API Wallabag (1 ou 2)" 968msgstr "Version de l'API Wallabag (1 ou 2)"
961 969
diff --git a/index.php b/index.php
index 1eb7659a..862c53ef 100644
--- a/index.php
+++ b/index.php
@@ -31,6 +31,7 @@ use Psr\Log\LogLevel;
31use Shaarli\Config\ConfigManager; 31use Shaarli\Config\ConfigManager;
32use Shaarli\Container\ContainerBuilder; 32use Shaarli\Container\ContainerBuilder;
33use Shaarli\Languages; 33use Shaarli\Languages;
34use Shaarli\Plugin\PluginManager;
34use Shaarli\Security\BanManager; 35use Shaarli\Security\BanManager;
35use Shaarli\Security\CookieManager; 36use Shaarli\Security\CookieManager;
36use Shaarli\Security\LoginManager; 37use Shaarli\Security\LoginManager;
@@ -87,7 +88,17 @@ date_default_timezone_set($conf->get('general.timezone', 'UTC'));
87 88
88$loginManager->checkLoginState(client_ip_id($_SERVER)); 89$loginManager->checkLoginState(client_ip_id($_SERVER));
89 90
90$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger); 91$pluginManager = new PluginManager($conf);
92$pluginManager->load($conf->get('general.enabled_plugins', []));
93
94$containerBuilder = new ContainerBuilder(
95 $conf,
96 $sessionManager,
97 $cookieManager,
98 $loginManager,
99 $pluginManager,
100 $logger
101);
91$container = $containerBuilder->build(); 102$container = $containerBuilder->build();
92$app = new App($container); 103$app = new App($container);
93 104
@@ -154,6 +165,15 @@ $app->group('/admin', function () {
154 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); 165 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
155})->add('\Shaarli\Front\ShaarliAdminMiddleware'); 166})->add('\Shaarli\Front\ShaarliAdminMiddleware');
156 167
168$app->group('/plugin', function () use ($pluginManager) {
169 foreach ($pluginManager->getRegisteredRoutes() as $pluginName => $routes) {
170 $this->group('/' . $pluginName, function () use ($routes) {
171 foreach ($routes as $route) {
172 $this->{strtolower($route['method'])}('/' . ltrim($route['route'], '/'), $route['callable']);
173 }
174 });
175 }
176})->add('\Shaarli\Front\ShaarliMiddleware');
157 177
158// REST API routes 178// REST API routes
159$app->group('/api/v1', function () { 179$app->group('/api/v1', function () {
diff --git a/phpcs.xml b/phpcs.xml
index c559e35d..9bdc8720 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -18,5 +18,6 @@
18 <rule ref="PSR1.Files.SideEffects.FoundWithSymbols"> 18 <rule ref="PSR1.Files.SideEffects.FoundWithSymbols">
19 <!-- index.php bootstraps everything, so yes mixed symbols with side effects --> 19 <!-- index.php bootstraps everything, so yes mixed symbols with side effects -->
20 <exclude-pattern>index.php</exclude-pattern> 20 <exclude-pattern>index.php</exclude-pattern>
21 <exclude-pattern>plugins/*</exclude-pattern>
21 </rule> 22 </rule>
22</ruleset> 23</ruleset>
diff --git a/plugins/demo_plugin/DemoPluginController.php b/plugins/demo_plugin/DemoPluginController.php
new file mode 100644
index 00000000..b8ace9c8
--- /dev/null
+++ b/plugins/demo_plugin/DemoPluginController.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\DemoPlugin;
6
7use Shaarli\Front\Controller\Admin\ShaarliAdminController;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class DemoPluginController extends ShaarliAdminController
12{
13 public function index(Request $request, Response $response): Response
14 {
15 $this->assignView(
16 'content',
17 '<div class="center">' .
18 'This is a demo page. I have access to Shaarli container, so I\'m free to do whatever I want here.' .
19 '</div>'
20 );
21
22 return $response->write($this->render('pluginscontent'));
23 }
24}
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php
index 22d27b68..15cfc2c5 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -7,6 +7,8 @@
7 * Can be used by plugin developers to make their own plugin. 7 * Can be used by plugin developers to make their own plugin.
8 */ 8 */
9 9
10require_once __DIR__ . '/DemoPluginController.php';
11
10/* 12/*
11 * RENDER HEADER, INCLUDES, FOOTER 13 * RENDER HEADER, INCLUDES, FOOTER
12 * 14 *
@@ -60,6 +62,17 @@ function demo_plugin_init($conf)
60 return $errors; 62 return $errors;
61} 63}
62 64
65function demo_plugin_register_routes(): array
66{
67 return [
68 [
69 'method' => 'GET',
70 'route' => '/custom',
71 'callable' => 'Shaarli\DemoPlugin\DemoPluginController:index',
72 ],
73 ];
74}
75
63/** 76/**
64 * Hook render_header. 77 * Hook render_header.
65 * Executed on every page render. 78 * Executed on every page render.
@@ -304,7 +317,11 @@ function hook_demo_plugin_render_editlink($data)
304function hook_demo_plugin_render_tools($data) 317function hook_demo_plugin_render_tools($data)
305{ 318{
306 // field_plugin 319 // field_plugin
307 $data['tools_plugin'][] = 'tools_plugin'; 320 $data['tools_plugin'][] = '<div class="tools-item">
321 <a href="' . $data['_BASE_PATH_'] . '/plugin/demo_plugin/custom">
322 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Demo Plugin Custom Route</span>
323 </a>
324 </div>';
308 325
309 return $data; 326 return $data;
310} 327}
diff --git a/tests/PluginManagerTest.php b/tests/PluginManagerTest.php
index efef5e87..8947f679 100644
--- a/tests/PluginManagerTest.php
+++ b/tests/PluginManagerTest.php
@@ -120,4 +120,43 @@ class PluginManagerTest extends \Shaarli\TestCase
120 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']); 120 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
121 $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']); 121 $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
122 } 122 }
123
124 /**
125 * Test plugin custom routes - note that there is no check on callable functions
126 */
127 public function testRegisteredRoutes(): void
128 {
129 PluginManager::$PLUGINS_PATH = self::$pluginPath;
130 $this->pluginManager->load([self::$pluginName]);
131
132 $expectedParameters = [
133 [
134 'method' => 'GET',
135 'route' => '/test',
136 'callable' => 'getFunction',
137 ],
138 [
139 'method' => 'POST',
140 'route' => '/custom',
141 'callable' => 'postFunction',
142 ],
143 ];
144 $meta = $this->pluginManager->getRegisteredRoutes();
145 static::assertSame($expectedParameters, $meta[self::$pluginName]);
146 }
147
148 /**
149 * Test plugin custom routes with invalid route
150 */
151 public function testRegisteredRoutesInvalid(): void
152 {
153 $plugin = 'test_route_invalid';
154 $this->pluginManager->load([$plugin]);
155
156 $meta = $this->pluginManager->getRegisteredRoutes();
157 static::assertSame([], $meta);
158
159 $errors = $this->pluginManager->getErrors();
160 static::assertSame(['test_route_invalid [plugin incompatibility]: trying to register invalid route.'], $errors);
161 }
123} 162}
diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php
index 3d43c344..04d4ef01 100644
--- a/tests/container/ContainerBuilderTest.php
+++ b/tests/container/ContainerBuilderTest.php
@@ -43,11 +43,15 @@ class ContainerBuilderTest extends TestCase
43 /** @var CookieManager */ 43 /** @var CookieManager */
44 protected $cookieManager; 44 protected $cookieManager;
45 45
46 /** @var PluginManager */
47 protected $pluginManager;
48
46 public function setUp(): void 49 public function setUp(): void
47 { 50 {
48 $this->conf = new ConfigManager('tests/utils/config/configJson'); 51 $this->conf = new ConfigManager('tests/utils/config/configJson');
49 $this->sessionManager = $this->createMock(SessionManager::class); 52 $this->sessionManager = $this->createMock(SessionManager::class);
50 $this->cookieManager = $this->createMock(CookieManager::class); 53 $this->cookieManager = $this->createMock(CookieManager::class);
54 $this->pluginManager = $this->createMock(PluginManager::class);
51 55
52 $this->loginManager = $this->createMock(LoginManager::class); 56 $this->loginManager = $this->createMock(LoginManager::class);
53 $this->loginManager->method('isLoggedIn')->willReturn(true); 57 $this->loginManager->method('isLoggedIn')->willReturn(true);
@@ -57,6 +61,7 @@ class ContainerBuilderTest extends TestCase
57 $this->sessionManager, 61 $this->sessionManager,
58 $this->cookieManager, 62 $this->cookieManager,
59 $this->loginManager, 63 $this->loginManager,
64 $this->pluginManager,
60 $this->createMock(LoggerInterface::class) 65 $this->createMock(LoggerInterface::class)
61 ); 66 );
62 } 67 }
diff --git a/tests/feed/CachedPageTest.php b/tests/feed/CachedPageTest.php
index 904db9dc..1decfaf3 100644
--- a/tests/feed/CachedPageTest.php
+++ b/tests/feed/CachedPageTest.php
@@ -40,10 +40,10 @@ class CachedPageTest extends \Shaarli\TestCase
40 */ 40 */
41 public function testConstruct() 41 public function testConstruct()
42 { 42 {
43 new CachedPage(self::$testCacheDir, '', true); 43 new CachedPage(self::$testCacheDir, '', true, null);
44 new CachedPage(self::$testCacheDir, '', false); 44 new CachedPage(self::$testCacheDir, '', false, null);
45 new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true); 45 new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true, null);
46 new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false); 46 new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false, null);
47 $this->addToAssertionCount(1); 47 $this->addToAssertionCount(1);
48 } 48 }
49 49
@@ -52,7 +52,7 @@ class CachedPageTest extends \Shaarli\TestCase
52 */ 52 */
53 public function testCache() 53 public function testCache()
54 { 54 {
55 $page = new CachedPage(self::$testCacheDir, self::$url, true); 55 $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
56 56
57 $this->assertFileNotExists(self::$filename); 57 $this->assertFileNotExists(self::$filename);
58 $page->cache('<p>Some content</p>'); 58 $page->cache('<p>Some content</p>');
@@ -68,7 +68,7 @@ class CachedPageTest extends \Shaarli\TestCase
68 */ 68 */
69 public function testShouldNotCache() 69 public function testShouldNotCache()
70 { 70 {
71 $page = new CachedPage(self::$testCacheDir, self::$url, false); 71 $page = new CachedPage(self::$testCacheDir, self::$url, false, null);
72 72
73 $this->assertFileNotExists(self::$filename); 73 $this->assertFileNotExists(self::$filename);
74 $page->cache('<p>Some content</p>'); 74 $page->cache('<p>Some content</p>');
@@ -80,7 +80,7 @@ class CachedPageTest extends \Shaarli\TestCase
80 */ 80 */
81 public function testCachedVersion() 81 public function testCachedVersion()
82 { 82 {
83 $page = new CachedPage(self::$testCacheDir, self::$url, true); 83 $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
84 84
85 $this->assertFileNotExists(self::$filename); 85 $this->assertFileNotExists(self::$filename);
86 $page->cache('<p>Some content</p>'); 86 $page->cache('<p>Some content</p>');
@@ -96,7 +96,7 @@ class CachedPageTest extends \Shaarli\TestCase
96 */ 96 */
97 public function testCachedVersionNoFile() 97 public function testCachedVersionNoFile()
98 { 98 {
99 $page = new CachedPage(self::$testCacheDir, self::$url, true); 99 $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
100 100
101 $this->assertFileNotExists(self::$filename); 101 $this->assertFileNotExists(self::$filename);
102 $this->assertEquals( 102 $this->assertEquals(
@@ -110,7 +110,7 @@ class CachedPageTest extends \Shaarli\TestCase
110 */ 110 */
111 public function testNoCachedVersion() 111 public function testNoCachedVersion()
112 { 112 {
113 $page = new CachedPage(self::$testCacheDir, self::$url, false); 113 $page = new CachedPage(self::$testCacheDir, self::$url, false, null);
114 114
115 $this->assertFileNotExists(self::$filename); 115 $this->assertFileNotExists(self::$filename);
116 $this->assertEquals( 116 $this->assertEquals(
@@ -118,4 +118,43 @@ class CachedPageTest extends \Shaarli\TestCase
118 $page->cachedVersion() 118 $page->cachedVersion()
119 ); 119 );
120 } 120 }
121
122 /**
123 * Return a page's cached content within date period
124 */
125 public function testCachedVersionInDatePeriod()
126 {
127 $period = new \DatePeriod(
128 new \DateTime('yesterday'),
129 new \DateInterval('P1D'),
130 new \DateTime('tomorrow')
131 );
132 $page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
133
134 $this->assertFileNotExists(self::$filename);
135 $page->cache('<p>Some content</p>');
136 $this->assertFileExists(self::$filename);
137 $this->assertEquals(
138 '<p>Some content</p>',
139 $page->cachedVersion()
140 );
141 }
142
143 /**
144 * Return a page's cached content outside of date period
145 */
146 public function testCachedVersionNotInDatePeriod()
147 {
148 $period = new \DatePeriod(
149 new \DateTime('yesterday noon'),
150 new \DateInterval('P1D'),
151 new \DateTime('yesterday midnight')
152 );
153 $page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
154
155 $this->assertFileNotExists(self::$filename);
156 $page->cache('<p>Some content</p>');
157 $this->assertFileExists(self::$filename);
158 $this->assertNull($page->cachedVersion());
159 }
121} 160}
diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php
index 5255b7b1..2d745800 100644
--- a/tests/helper/DailyPageHelperTest.php
+++ b/tests/helper/DailyPageHelperTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Helper; 5namespace Shaarli\Helper;
6 6
7use DateTimeImmutable;
8use DateTimeInterface;
7use Shaarli\Bookmark\Bookmark; 9use Shaarli\Bookmark\Bookmark;
8use Shaarli\TestCase; 10use Shaarli\TestCase;
9use Slim\Http\Request; 11use Slim\Http\Request;
@@ -32,7 +34,7 @@ class DailyPageHelperTest extends TestCase
32 string $type, 34 string $type,
33 string $input, 35 string $input,
34 ?Bookmark $bookmark, 36 ?Bookmark $bookmark,
35 \DateTimeInterface $expectedDateTime, 37 DateTimeInterface $expectedDateTime,
36 string $compareFormat = 'Ymd' 38 string $compareFormat = 'Ymd'
37 ): void { 39 ): void {
38 $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark); 40 $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
@@ -71,8 +73,8 @@ class DailyPageHelperTest extends TestCase
71 */ 73 */
72 public function testGetStartDatesByType( 74 public function testGetStartDatesByType(
73 string $type, 75 string $type,
74 \DateTimeImmutable $dateTime, 76 DateTimeImmutable $dateTime,
75 \DateTimeInterface $expectedDateTime 77 DateTimeInterface $expectedDateTime
76 ): void { 78 ): void {
77 $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime); 79 $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
78 80
@@ -84,7 +86,7 @@ class DailyPageHelperTest extends TestCase
84 $this->expectException(\Exception::class); 86 $this->expectException(\Exception::class);
85 $this->expectExceptionMessage('Unsupported daily format type'); 87 $this->expectExceptionMessage('Unsupported daily format type');
86 88
87 DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable()); 89 DailyPageHelper::getStartDateTimeByType('nope', new DateTimeImmutable());
88 } 90 }
89 91
90 /** 92 /**
@@ -92,8 +94,8 @@ class DailyPageHelperTest extends TestCase
92 */ 94 */
93 public function testGetEndDatesByType( 95 public function testGetEndDatesByType(
94 string $type, 96 string $type,
95 \DateTimeImmutable $dateTime, 97 DateTimeImmutable $dateTime,
96 \DateTimeInterface $expectedDateTime 98 DateTimeInterface $expectedDateTime
97 ): void { 99 ): void {
98 $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime); 100 $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
99 101
@@ -105,7 +107,7 @@ class DailyPageHelperTest extends TestCase
105 $this->expectException(\Exception::class); 107 $this->expectException(\Exception::class);
106 $this->expectExceptionMessage('Unsupported daily format type'); 108 $this->expectExceptionMessage('Unsupported daily format type');
107 109
108 DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable()); 110 DailyPageHelper::getEndDateTimeByType('nope', new DateTimeImmutable());
109 } 111 }
110 112
111 /** 113 /**
@@ -113,7 +115,7 @@ class DailyPageHelperTest extends TestCase
113 */ 115 */
114 public function testGeDescriptionsByType( 116 public function testGeDescriptionsByType(
115 string $type, 117 string $type,
116 \DateTimeImmutable $dateTime, 118 DateTimeImmutable $dateTime,
117 string $expectedDescription 119 string $expectedDescription
118 ): void { 120 ): void {
119 $description = DailyPageHelper::getDescriptionByType($type, $dateTime); 121 $description = DailyPageHelper::getDescriptionByType($type, $dateTime);
@@ -121,12 +123,25 @@ class DailyPageHelperTest extends TestCase
121 static::assertEquals($expectedDescription, $description); 123 static::assertEquals($expectedDescription, $description);
122 } 124 }
123 125
126 /**
127 * @dataProvider getDescriptionsByTypeNotIncludeRelative
128 */
129 public function testGeDescriptionsByTypeNotIncludeRelative(
130 string $type,
131 \DateTimeImmutable $dateTime,
132 string $expectedDescription
133 ): void {
134 $description = DailyPageHelper::getDescriptionByType($type, $dateTime, false);
135
136 static::assertEquals($expectedDescription, $description);
137 }
138
124 public function getDescriptionByTypeExceptionUnknownType(): void 139 public function getDescriptionByTypeExceptionUnknownType(): void
125 { 140 {
126 $this->expectException(\Exception::class); 141 $this->expectException(\Exception::class);
127 $this->expectExceptionMessage('Unsupported daily format type'); 142 $this->expectExceptionMessage('Unsupported daily format type');
128 143
129 DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable()); 144 DailyPageHelper::getDescriptionByType('nope', new DateTimeImmutable());
130 } 145 }
131 146
132 /** 147 /**
@@ -147,6 +162,29 @@ class DailyPageHelperTest extends TestCase
147 } 162 }
148 163
149 /** 164 /**
165 * @dataProvider getCacheDatePeriodByType
166 */
167 public function testGetCacheDatePeriodByType(
168 string $type,
169 DateTimeImmutable $requested,
170 DateTimeInterface $start,
171 DateTimeInterface $end
172 ): void {
173 $period = DailyPageHelper::getCacheDatePeriodByType($type, $requested);
174
175 static::assertEquals($start, $period->getStartDate());
176 static::assertEquals($end, $period->getEndDate());
177 }
178
179 public function testGetCacheDatePeriodByTypeExceptionUnknownType(): void
180 {
181 $this->expectException(\Exception::class);
182 $this->expectExceptionMessage('Unsupported daily format type');
183
184 DailyPageHelper::getCacheDatePeriodByType('nope');
185 }
186
187 /**
150 * Data provider for testExtractRequestedType() test method. 188 * Data provider for testExtractRequestedType() test method.
151 */ 189 */
152 public function getRequestedTypes(): array 190 public function getRequestedTypes(): array
@@ -216,9 +254,9 @@ class DailyPageHelperTest extends TestCase
216 public function getStartDatesByType(): array 254 public function getStartDatesByType(): array
217 { 255 {
218 return [ 256 return [
219 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')], 257 [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
220 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')], 258 [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
221 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')], 259 [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
222 ]; 260 ];
223 } 261 }
224 262
@@ -228,9 +266,9 @@ class DailyPageHelperTest extends TestCase
228 public function getEndDatesByType(): array 266 public function getEndDatesByType(): array
229 { 267 {
230 return [ 268 return [
231 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')], 269 [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
232 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')], 270 [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
233 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')], 271 [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
234 ]; 272 ];
235 } 273 }
236 274
@@ -240,8 +278,22 @@ class DailyPageHelperTest extends TestCase
240 public function getDescriptionsByType(): array 278 public function getDescriptionsByType(): array
241 { 279 {
242 return [ 280 return [
243 [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')], 281 [DailyPageHelper::DAY, $date = new DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
244 [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')], 282 [DailyPageHelper::DAY, $date = new DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
283 [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
284 [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
285 [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
286 ];
287 }
288
289 /**
290 * Data provider for testGeDescriptionsByTypeNotIncludeRelative() test method.
291 */
292 public function getDescriptionsByTypeNotIncludeRelative(): array
293 {
294 return [
295 [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), $date->format('F j, Y')],
296 [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), $date->format('F j, Y')],
245 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], 297 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
246 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], 298 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
247 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], 299 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
@@ -249,7 +301,7 @@ class DailyPageHelperTest extends TestCase
249 } 301 }
250 302
251 /** 303 /**
252 * Data provider for testGetDescriptionsByType() test method. 304 * Data provider for testGetRssLengthsByType() test method.
253 */ 305 */
254 public function getRssLengthsByType(): array 306 public function getRssLengthsByType(): array
255 { 307 {
@@ -259,4 +311,31 @@ class DailyPageHelperTest extends TestCase
259 [DailyPageHelper::MONTH], 311 [DailyPageHelper::MONTH],
260 ]; 312 ];
261 } 313 }
314
315 /**
316 * Data provider for testGetCacheDatePeriodByType() test method.
317 */
318 public function getCacheDatePeriodByType(): array
319 {
320 return [
321 [
322 DailyPageHelper::DAY,
323 new DateTimeImmutable('2020-10-09 04:05:06'),
324 new \DateTime('2020-10-09 00:00:00'),
325 new \DateTime('2020-10-09 23:59:59'),
326 ],
327 [
328 DailyPageHelper::WEEK,
329 new DateTimeImmutable('2020-10-09 04:05:06'),
330 new \DateTime('2020-10-05 00:00:00'),
331 new \DateTime('2020-10-11 23:59:59'),
332 ],
333 [
334 DailyPageHelper::MONTH,
335 new DateTimeImmutable('2020-10-09 04:05:06'),
336 new \DateTime('2020-10-01 00:00:00'),
337 new \DateTime('2020-10-31 23:59:59'),
338 ],
339 ];
340 }
262} 341}
diff --git a/tests/plugins/test/test.php b/tests/plugins/test/test.php
index 03be4f4e..34cd339e 100644
--- a/tests/plugins/test/test.php
+++ b/tests/plugins/test/test.php
@@ -27,3 +27,19 @@ function hook_test_error()
27{ 27{
28 new Unknown(); 28 new Unknown();
29} 29}
30
31function test_register_routes(): array
32{
33 return [
34 [
35 'method' => 'GET',
36 'route' => '/test',
37 'callable' => 'getFunction',
38 ],
39 [
40 'method' => 'POST',
41 'route' => '/custom',
42 'callable' => 'postFunction',
43 ],
44 ];
45}
diff --git a/tests/plugins/test_route_invalid/test_route_invalid.php b/tests/plugins/test_route_invalid/test_route_invalid.php
new file mode 100644
index 00000000..0c5a5101
--- /dev/null
+++ b/tests/plugins/test_route_invalid/test_route_invalid.php
@@ -0,0 +1,12 @@
1<?php
2
3function test_route_invalid_register_routes(): array
4{
5 return [
6 [
7 'method' => 'GET',
8 'route' => 'not a route',
9 'callable' => 'getFunction',
10 ],
11 ];
12}
diff --git a/tpl/default/pluginscontent.html b/tpl/default/pluginscontent.html
new file mode 100644
index 00000000..1e4f6b80
--- /dev/null
+++ b/tpl/default/pluginscontent.html
@@ -0,0 +1,13 @@
1<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head>
4 {include="includes"}
5</head>
6<body>
7 {include="page.header"}
8
9 {$content}
10
11 {include="page.footer"}
12</body>
13</html>
diff --git a/yarn.lock b/yarn.lock
index 55bd9827..97fb0fad 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3052,9 +3052,9 @@ inherits@2.0.3:
3052 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 3052 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
3053 3053
3054ini@^1.3.4, ini@^1.3.5: 3054ini@^1.3.4, ini@^1.3.5:
3055 version "1.3.5" 3055 version "1.3.7"
3056 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" 3056 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
3057 integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== 3057 integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
3058 3058
3059interpret@^1.4.0: 3059interpret@^1.4.0:
3060 version "1.4.0" 3060 version "1.4.0"