diff options
Diffstat (limited to 'application')
-rw-r--r-- | application/bookmark/BookmarkIO.php | 22 | ||||
-rw-r--r-- | application/bookmark/LinkUtils.php | 6 | ||||
-rw-r--r-- | application/container/ContainerBuilder.php | 17 | ||||
-rw-r--r-- | application/feed/CachedPage.php | 45 | ||||
-rw-r--r-- | application/front/controller/admin/ServerController.php | 7 | ||||
-rw-r--r-- | application/front/controller/visitor/DailyController.php | 7 | ||||
-rw-r--r-- | application/front/controller/visitor/InstallController.php | 7 | ||||
-rw-r--r-- | application/helper/ApplicationUtils.php | 16 | ||||
-rw-r--r-- | application/helper/DailyPageHelper.php | 84 | ||||
-rw-r--r-- | application/plugin/PluginManager.php | 64 | ||||
-rw-r--r-- | application/plugin/exception/PluginInvalidRouteException.php | 26 | ||||
-rw-r--r-- | application/render/PageCacheManager.php | 14 |
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 | ||
5 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
6 | 6 | ||
7 | use malkusch\lock\exception\LockAcquireException; | ||
7 | use malkusch\lock\mutex\Mutex; | 8 | use malkusch\lock\mutex\Mutex; |
8 | use malkusch\lock\mutex\NoMutex; | 9 | use malkusch\lock\mutex\NoMutex; |
9 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; | 10 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; |
@@ -80,7 +81,7 @@ class BookmarkIO | |||
80 | } | 81 | } |
81 | 82 | ||
82 | $content = null; | 83 | $content = null; |
83 | $this->mutex->synchronized(function () use (&$content) { | 84 | $this->synchronized(function () use (&$content) { |
84 | $content = file_get_contents($this->datastore); | 85 | $content = file_get_contents($this->datastore); |
85 | }); | 86 | }); |
86 | 87 | ||
@@ -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 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Feed; | 5 | namespace Shaarli\Feed; |
4 | 6 | ||
7 | use DatePeriod; | ||
8 | |||
5 | /** | 9 | /** |
6 | * Simple cache system, mainly for the RSS/ATOM feeds | 10 | * Simple cache system, mainly for the RSS/ATOM feeds |
7 | */ | 11 | */ |
8 | class CachedPage | 12 | class CachedPage |
9 | { | 13 | { |
10 | // Directory containing page caches | 14 | /** Directory containing page caches */ |
11 | private $cacheDir; | 15 | protected $cacheDir; |
16 | |||
17 | /** Should this URL be cached (boolean)? */ | ||
18 | protected $shouldBeCached; | ||
12 | 19 | ||
13 | // Should this URL be cached (boolean)? | 20 | /** Name of the cache file for this URL */ |
14 | private $shouldBeCached; | 21 | protected $filename; |
15 | 22 | ||
16 | // Name of the cache file for this URL | 23 | /** @var DatePeriod|null Optionally specify a period of time for cache validity */ |
17 | private $filename; | 24 | protected $validityPeriod; |
18 | 25 | ||
19 | /** | 26 | /** |
20 | * Creates a new CachedPage | 27 | * Creates a new CachedPage |
21 | * | 28 | * |
22 | * @param string $cacheDir page cache directory | 29 | * @param string $cacheDir page cache directory |
23 | * @param string $url page URL | 30 | * @param string $url page URL |
24 | * @param bool $shouldBeCached whether this page needs to be cached | 31 | * @param bool $shouldBeCached whether this page needs to be cached |
32 | * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache | ||
25 | */ | 33 | */ |
26 | public function __construct($cacheDir, $url, $shouldBeCached) | 34 | public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod) |
27 | { | 35 | { |
28 | // TODO: check write access to the cache directory | 36 | // TODO: check write access to the cache directory |
29 | $this->cacheDir = $cacheDir; | 37 | $this->cacheDir = $cacheDir; |
30 | $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; | 38 | $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; |
31 | $this->shouldBeCached = $shouldBeCached; | 39 | $this->shouldBeCached = $shouldBeCached; |
40 | $this->validityPeriod = $validityPeriod; | ||
32 | } | 41 | } |
33 | 42 | ||
34 | /** | 43 | /** |
@@ -41,10 +50,20 @@ class CachedPage | |||
41 | if (!$this->shouldBeCached) { | 50 | if (!$this->shouldBeCached) { |
42 | return null; | 51 | return null; |
43 | } | 52 | } |
44 | if (is_file($this->filename)) { | 53 | if (!is_file($this->filename)) { |
45 | return file_get_contents($this->filename); | 54 | return null; |
55 | } | ||
56 | if ($this->validityPeriod !== null) { | ||
57 | $cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename)); | ||
58 | if ( | ||
59 | $cacheDate < $this->validityPeriod->getStartDate() | ||
60 | || $cacheDate > $this->validityPeriod->getEndDate() | ||
61 | ) { | ||
62 | return null; | ||
63 | } | ||
46 | } | 64 | } |
47 | return null; | 65 | |
66 | return file_get_contents($this->filename); | ||
48 | } | 67 | } |
49 | 68 | ||
50 | /** | 69 | /** |
diff --git a/application/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 @@ | |||
3 | namespace Shaarli\Helper; | 3 | namespace Shaarli\Helper; |
4 | 4 | ||
5 | use Exception; | 5 | use Exception; |
6 | use malkusch\lock\exception\LockAcquireException; | ||
7 | use malkusch\lock\mutex\FlockMutex; | ||
6 | use Shaarli\Config\ConfigManager; | 8 | use 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 | ||
5 | namespace Shaarli\Helper; | 5 | namespace Shaarli\Helper; |
6 | 6 | ||
7 | use DatePeriod; | ||
8 | use DateTimeImmutable; | ||
9 | use Exception; | ||
7 | use Shaarli\Bookmark\Bookmark; | 10 | use Shaarli\Bookmark\Bookmark; |
8 | use Slim\Http\Request; | 11 | use 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 | ||
5 | use Shaarli\Config\ConfigManager; | 5 | use Shaarli\Config\ConfigManager; |
6 | use Shaarli\Plugin\Exception\PluginFileNotFoundException; | 6 | use Shaarli\Plugin\Exception\PluginFileNotFoundException; |
7 | use 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Plugin\Exception; | ||
6 | |||
7 | use Exception; | ||
8 | |||
9 | /** | ||
10 | * Class PluginFileNotFoundException | ||
11 | * | ||
12 | * Raise when plugin files can't be found. | ||
13 | */ | ||
14 | class PluginInvalidRouteException extends Exception | ||
15 | { | ||
16 | /** | ||
17 | * Construct exception with plugin name. | ||
18 | * Generate message. | ||
19 | * | ||
20 | * @param string $pluginName name of the plugin not found | ||
21 | */ | ||
22 | public function __construct() | ||
23 | { | ||
24 | $this->message = 'trying to register invalid route.'; | ||
25 | } | ||
26 | } | ||
diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php index 97805c35..fe74bf27 100644 --- a/application/render/PageCacheManager.php +++ b/application/render/PageCacheManager.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Render; | 3 | namespace Shaarli\Render; |
4 | 4 | ||
5 | use DatePeriod; | ||
5 | use Shaarli\Feed\CachedPage; | 6 | use Shaarli\Feed\CachedPage; |
6 | 7 | ||
7 | /** | 8 | /** |
@@ -49,12 +50,21 @@ class PageCacheManager | |||
49 | $this->purgeCachedPages(); | 50 | $this->purgeCachedPages(); |
50 | } | 51 | } |
51 | 52 | ||
52 | public function getCachePage(string $pageUrl): CachedPage | 53 | /** |
54 | * Get CachedPage instance for provided URL. | ||
55 | * | ||
56 | * @param string $pageUrl | ||
57 | * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache | ||
58 | * | ||
59 | * @return CachedPage | ||
60 | */ | ||
61 | public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage | ||
53 | { | 62 | { |
54 | return new CachedPage( | 63 | return new CachedPage( |
55 | $this->pageCacheDir, | 64 | $this->pageCacheDir, |
56 | $pageUrl, | 65 | $pageUrl, |
57 | false === $this->isLoggedIn | 66 | false === $this->isLoggedIn, |
67 | $validityPeriod | ||
58 | ); | 68 | ); |
59 | } | 69 | } |
60 | } | 70 | } |