aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/Languages.php1
-rw-r--r--application/bookmark/BookmarkIO.php22
-rw-r--r--application/container/ContainerBuilder.php17
-rw-r--r--application/front/controller/admin/ServerController.php7
-rw-r--r--application/front/controller/visitor/InstallController.php7
-rw-r--r--application/helper/ApplicationUtils.php16
-rw-r--r--application/plugin/PluginManager.php64
-rw-r--r--application/plugin/exception/PluginInvalidRouteException.php26
8 files changed, 145 insertions, 15 deletions
diff --git a/application/Languages.php b/application/Languages.php
index 60e91631..7177db2c 100644
--- a/application/Languages.php
+++ b/application/Languages.php
@@ -186,6 +186,7 @@ class Languages
186 'en' => t('English'), 186 'en' => t('English'),
187 'fr' => t('French'), 187 'fr' => t('French'),
188 'jp' => t('Japanese'), 188 'jp' => t('Japanese'),
189 'ru' => t('Russian'),
189 ]; 190 ];
190 } 191 }
191} 192}
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/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/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/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}