diff options
Diffstat (limited to 'application')
-rw-r--r-- | application/Languages.php | 1 | ||||
-rw-r--r-- | application/bookmark/BookmarkIO.php | 22 | ||||
-rw-r--r-- | application/container/ContainerBuilder.php | 17 | ||||
-rw-r--r-- | application/front/controller/admin/ServerController.php | 7 | ||||
-rw-r--r-- | application/front/controller/visitor/InstallController.php | 7 | ||||
-rw-r--r-- | application/helper/ApplicationUtils.php | 16 | ||||
-rw-r--r-- | application/plugin/PluginManager.php | 64 | ||||
-rw-r--r-- | application/plugin/exception/PluginInvalidRouteException.php | 26 |
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 | ||
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/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 @@ | |||
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/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 | } | ||