aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/bookmark/BookmarkIO.php22
-rw-r--r--application/bookmark/LinkUtils.php6
-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
12 files changed, 252 insertions, 63 deletions
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/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index d65e97ed..0ab2d213 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -68,11 +68,13 @@ function html_extract_tag($tag, $html)
68 $properties = implode('|', $propertiesKey); 68 $properties = implode('|', $propertiesKey);
69 // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' 69 // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
70 $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; 70 $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
71 // Support quotes in double quoted content, and the other way around
72 $content = 'content=(["\'])((?:(?!\1).)*)\1';
71 // Try to retrieve OpenGraph tag. 73 // Try to retrieve OpenGraph tag.
72 $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; 74 $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*' . $content . '.*?>#';
73 // If the attributes are not in the order property => content (e.g. Github) 75 // If the attributes are not in the order property => content (e.g. Github)
74 // New regex to keep this readable... more or less. 76 // New regex to keep this readable... more or less.
75 $ogRegexReverse = '#<meta[^>]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; 77 $ogRegexReverse = '#<meta[^>]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
76 78
77 if ( 79 if (
78 preg_match($ogRegex, $html, $matches) > 0 80 preg_match($ogRegex, $html, $matches) > 0
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}