]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge pull request #1665 from ArthurHoaro/fix/metadata-regexes-2
authorArthurHoaro <arthur@hoa.ro>
Tue, 29 Dec 2020 10:43:39 +0000 (11:43 +0100)
committerGitHub <noreply@github.com>
Tue, 29 Dec 2020 10:43:39 +0000 (11:43 +0100)
Fix metadata extract regex (2)

28 files changed:
Dockerfile
Dockerfile.armhf
application/bookmark/BookmarkIO.php
application/container/ContainerBuilder.php
application/feed/CachedPage.php
application/front/controller/admin/ServerController.php
application/front/controller/visitor/DailyController.php
application/front/controller/visitor/InstallController.php
application/helper/ApplicationUtils.php
application/helper/DailyPageHelper.php
application/plugin/PluginManager.php
application/plugin/exception/PluginInvalidRouteException.php [new file with mode: 0644]
application/render/PageCacheManager.php
doc/md/REST-API.md
doc/md/dev/Plugin-system.md
inc/languages/fr/LC_MESSAGES/shaarli.po
index.php
phpcs.xml
plugins/demo_plugin/DemoPluginController.php [new file with mode: 0644]
plugins/demo_plugin/demo_plugin.php
tests/PluginManagerTest.php
tests/container/ContainerBuilderTest.php
tests/feed/CachedPageTest.php
tests/helper/DailyPageHelperTest.php
tests/plugins/test/test.php
tests/plugins/test_route_invalid/test_route_invalid.php [new file with mode: 0644]
tpl/default/pluginscontent.html [new file with mode: 0644]
yarn.lock

index f6120b71f2b1d4507cd46dcaf7ebf09934d98139..79d3313095b89f8904ba37992dedd30ee74aa229 100644 (file)
@@ -26,7 +26,7 @@ RUN cd shaarli \
 
 # Stage 4:
 # - Shaarli image
-FROM alpine:3.8
+FROM alpine:3.12
 LABEL maintainer="Shaarli Community"
 
 RUN apk --update --no-cache add \
index 5bbf668049d7ed31f203d799b29f64685d538e43..471f239743651420a1afc012c2412b6b9f0b1554 100644 (file)
@@ -1,7 +1,7 @@
 # Stage 1:
 # - Copy Shaarli sources
 # - Build documentation
-FROM arm32v6/alpine:3.8 as docs
+FROM arm32v6/alpine:3.10 as docs
 ADD . /usr/src/app/shaarli
 RUN apk --update --no-cache add py2-pip \
     && cd /usr/src/app/shaarli \
@@ -10,7 +10,7 @@ RUN apk --update --no-cache add py2-pip \
 
 # Stage 2:
 # - Resolve PHP dependencies with Composer
-FROM arm32v6/alpine:3.8 as composer
+FROM arm32v6/alpine:3.10 as composer
 COPY --from=docs /usr/src/app/shaarli /app/shaarli
 RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \
     && cd /app/shaarli \
@@ -18,7 +18,7 @@ RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer
 
 # Stage 3:
 # - Frontend dependencies
-FROM arm32v6/alpine:3.8 as node
+FROM arm32v6/alpine:3.10 as node
 COPY --from=composer /app/shaarli /shaarli
 RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
     && cd /shaarli \
@@ -28,7 +28,7 @@ RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
 
 # Stage 4:
 # - Shaarli image
-FROM arm32v6/alpine:3.8
+FROM arm32v6/alpine:3.10
 LABEL maintainer="Shaarli Community"
 
 RUN apk --update --no-cache add \
index c78dbe41fe7dc8af6c3cd8a96ea2986dea45a914..8439d470da21fdff3d0b7da95230de0594e5a341 100644 (file)
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace Shaarli\Bookmark;
 
+use malkusch\lock\exception\LockAcquireException;
 use malkusch\lock\mutex\Mutex;
 use malkusch\lock\mutex\NoMutex;
 use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
@@ -80,7 +81,7 @@ class BookmarkIO
         }
 
         $content = null;
-        $this->mutex->synchronized(function () use (&$content) {
+        $this->synchronized(function () use (&$content) {
             $content = file_get_contents($this->datastore);
         });
 
@@ -119,11 +120,28 @@ class BookmarkIO
 
         $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix;
 
-        $this->mutex->synchronized(function () use ($data) {
+        $this->synchronized(function () use ($data) {
             file_put_contents(
                 $this->datastore,
                 $data
             );
         });
     }
+
+    /**
+     * Wrapper applying mutex to provided function.
+     * If the lock can't be acquired (e.g. some shared hosting provider), we execute the function without mutex.
+     *
+     * @see https://github.com/shaarli/Shaarli/issues/1650
+     *
+     * @param callable $function
+     */
+    protected function synchronized(callable $function): void
+    {
+        try {
+            $this->mutex->synchronized($function);
+        } catch (LockAcquireException $exception) {
+            $function();
+        }
+    }
 }
index f0234eca2f92a4bcca74ea99b915c18d4d2e64f4..6d69a880f4fb0694e762b42a38df432c445bca4a 100644 (file)
@@ -50,6 +50,9 @@ class ContainerBuilder
     /** @var LoginManager */
     protected $login;
 
+    /** @var PluginManager */
+    protected $pluginManager;
+
     /** @var LoggerInterface */
     protected $logger;
 
@@ -61,12 +64,14 @@ class ContainerBuilder
         SessionManager $session,
         CookieManager $cookieManager,
         LoginManager $login,
+        PluginManager $pluginManager,
         LoggerInterface $logger
     ) {
         $this->conf = $conf;
         $this->session = $session;
         $this->login = $login;
         $this->cookieManager = $cookieManager;
+        $this->pluginManager = $pluginManager;
         $this->logger = $logger;
     }
 
@@ -78,12 +83,10 @@ class ContainerBuilder
         $container['sessionManager'] = $this->session;
         $container['cookieManager'] = $this->cookieManager;
         $container['loginManager'] = $this->login;
+        $container['pluginManager'] = $this->pluginManager;
         $container['logger'] = $this->logger;
         $container['basePath'] = $this->basePath;
 
-        $container['plugins'] = function (ShaarliContainer $container): PluginManager {
-            return new PluginManager($container->conf);
-        };
 
         $container['history'] = function (ShaarliContainer $container): History {
             return new History($container->conf->get('resource.history'));
@@ -113,14 +116,6 @@ class ContainerBuilder
             );
         };
 
-        $container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
-            $pluginManager = new PluginManager($container->conf);
-
-            $pluginManager->load($container->conf->get('general.enabled_plugins'));
-
-            return $pluginManager;
-        };
-
         $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
             return new FormatterFactory(
                 $container->conf,
index d809bdd962ca901c54536309d17068652ff5f3bd..c23c200f3370869b952cc681daf98cfc456c4666 100644 (file)
@@ -1,34 +1,43 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Shaarli\Feed;
 
+use DatePeriod;
+
 /**
  * Simple cache system, mainly for the RSS/ATOM feeds
  */
 class CachedPage
 {
-    // Directory containing page caches
-    private $cacheDir;
+    /** Directory containing page caches */
+    protected $cacheDir;
+
+    /** Should this URL be cached (boolean)? */
+    protected $shouldBeCached;
 
-    // Should this URL be cached (boolean)?
-    private $shouldBeCached;
+    /** Name of the cache file for this URL */
+    protected $filename;
 
-    // Name of the cache file for this URL
-    private $filename;
+    /** @var DatePeriod|null Optionally specify a period of time for cache validity */
+    protected $validityPeriod;
 
     /**
      * Creates a new CachedPage
      *
-     * @param string $cacheDir       page cache directory
-     * @param string $url            page URL
-     * @param bool   $shouldBeCached whether this page needs to be cached
+     * @param string      $cacheDir       page cache directory
+     * @param string      $url            page URL
+     * @param bool        $shouldBeCached whether this page needs to be cached
+     * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
      */
-    public function __construct($cacheDir, $url, $shouldBeCached)
+    public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod)
     {
         // TODO: check write access to the cache directory
         $this->cacheDir = $cacheDir;
         $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
         $this->shouldBeCached = $shouldBeCached;
+        $this->validityPeriod = $validityPeriod;
     }
 
     /**
@@ -41,10 +50,20 @@ class CachedPage
         if (!$this->shouldBeCached) {
             return null;
         }
-        if (is_file($this->filename)) {
-            return file_get_contents($this->filename);
+        if (!is_file($this->filename)) {
+            return null;
+        }
+        if ($this->validityPeriod !== null) {
+            $cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename));
+            if (
+                $cacheDate < $this->validityPeriod->getStartDate()
+                || $cacheDate > $this->validityPeriod->getEndDate()
+            ) {
+                return null;
+            }
         }
-        return null;
+
+        return file_get_contents($this->filename);
     }
 
     /**
index fabeaf2f26b09643709bbf1a23f156361fa6bb75..4b74f4a94f4549385f1fa3f50e9082375ee66a12 100644 (file)
@@ -39,11 +39,16 @@ class ServerController extends ShaarliAdminController
         $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion;
         $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
 
+        $permissions = array_merge(
+            ApplicationUtils::checkResourcePermissions($this->container->conf),
+            ApplicationUtils::checkDatastoreMutex()
+        );
+
         $this->assignView('php_version', PHP_VERSION);
         $this->assignView('php_eol', format_date($phpEol, false));
         $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
         $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
-        $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
+        $this->assignView('permissions', $permissions);
         $this->assignView('release_url', $releaseUrl);
         $this->assignView('latest_version', $latestVersion);
         $this->assignView('current_version', $currentVersion);
index 846cfe22a81c310469391e63e84dc549991792c3..29492a5f3c1f3acd50d65547d9d3543dbf7e7a8a 100644 (file)
@@ -86,9 +86,11 @@ class DailyController extends ShaarliVisitorController
     public function rss(Request $request, Response $response): Response
     {
         $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
+        $type = DailyPageHelper::extractRequestedType($request);
+        $cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type);
 
         $pageUrl = page_url($this->container->environment);
-        $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
+        $cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration);
 
         $cached = $cache->cachedVersion();
         if (!empty($cached)) {
@@ -96,7 +98,6 @@ class DailyController extends ShaarliVisitorController
         }
 
         $days = [];
-        $type = DailyPageHelper::extractRequestedType($request);
         $format = DailyPageHelper::getFormatByType($type);
         $length = DailyPageHelper::getRssLengthByType($type);
         foreach ($this->container->bookmarkService->search() as $bookmark) {
@@ -131,7 +132,7 @@ class DailyController extends ShaarliVisitorController
             $dataPerDay[$day] = [
                 'date' => $endDateTime,
                 'date_rss' => $endDateTime->format(DateTime::RSS),
-                'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime),
+                'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime, false),
                 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day,
                 'links' => [],
             ];
index bf96592949370026d888ae96f3e155dad483a8b3..418d4a49cdfffbb3a44e0712f4d08d9460677f74 100644 (file)
@@ -56,11 +56,16 @@ class InstallController extends ShaarliVisitorController
 
         $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
 
+        $permissions = array_merge(
+            ApplicationUtils::checkResourcePermissions($this->container->conf),
+            ApplicationUtils::checkDatastoreMutex()
+        );
+
         $this->assignView('php_version', PHP_VERSION);
         $this->assignView('php_eol', format_date($phpEol, false));
         $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
         $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
-        $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
+        $this->assignView('permissions', $permissions);
 
         $this->assignView('pagetitle', t('Install Shaarli'));
 
index 212dd8e2dc7578aa6da6a3f8d403fff319ae3f42..a6c03aaeae899daebb46bb37884dc3422694b6d0 100644 (file)
@@ -3,6 +3,8 @@
 namespace Shaarli\Helper;
 
 use Exception;
+use malkusch\lock\exception\LockAcquireException;
+use malkusch\lock\mutex\FlockMutex;
 use Shaarli\Config\ConfigManager;
 
 /**
@@ -252,6 +254,20 @@ class ApplicationUtils
         return $errors;
     }
 
+    public static function checkDatastoreMutex(): array
+    {
+        $mutex = new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2);
+        try {
+            $mutex->synchronized(function () {
+                return true;
+            });
+        } catch (LockAcquireException $e) {
+            $errors[] = t('Lock can not be acquired on the datastore. You might encounter concurrent access issues.');
+        }
+
+        return $errors ?? [];
+    }
+
     /**
      * Returns a salted hash representing the current Shaarli version.
      *
index 5fabc90786dc172a5bbf5b178695c8a89a92e861..05f95812a279825b5b31372d4e4761d424189f5e 100644 (file)
@@ -4,6 +4,9 @@ declare(strict_types=1);
 
 namespace Shaarli\Helper;
 
+use DatePeriod;
+use DateTimeImmutable;
+use Exception;
 use Shaarli\Bookmark\Bookmark;
 use Slim\Http\Request;
 
@@ -40,31 +43,31 @@ class DailyPageHelper
      * @param string|null   $requestedDate  Input string extracted from the request
      * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
      *
-     * @return \DateTimeImmutable from input or latest bookmark.
+     * @return DateTimeImmutable from input or latest bookmark.
      *
-     * @throws \Exception Type not supported.
+     * @throws Exception Type not supported.
      */
     public static function extractRequestedDateTime(
         string $type,
         ?string $requestedDate,
         Bookmark $latestBookmark = null
-    ): \DateTimeImmutable {
+    ): DateTimeImmutable {
         $format = static::getFormatByType($type);
         if (empty($requestedDate)) {
             return $latestBookmark instanceof Bookmark
-                ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
-                : new \DateTimeImmutable()
+                ? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
+                : new DateTimeImmutable()
             ;
         }
 
         // W is not supported by createFromFormat...
         if ($type === static::WEEK) {
-            return (new \DateTimeImmutable())
+            return (new DateTimeImmutable())
                 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
             ;
         }
 
-        return \DateTimeImmutable::createFromFormat($format, $requestedDate);
+        return DateTimeImmutable::createFromFormat($format, $requestedDate);
     }
 
     /**
@@ -80,7 +83,7 @@ class DailyPageHelper
      *
      * @see https://www.php.net/manual/en/datetime.format.php
      *
-     * @throws \Exception Type not supported.
+     * @throws Exception Type not supported.
      */
     public static function getFormatByType(string $type): string
     {
@@ -92,7 +95,7 @@ class DailyPageHelper
             case static::DAY:
                 return 'Ymd';
             default:
-                throw new \Exception('Unsupported daily format type');
+                throw new Exception('Unsupported daily format type');
         }
     }
 
@@ -102,14 +105,14 @@ class DailyPageHelper
      *       and we don't want to alter original datetime.
      *
      * @param string             $type      month/week/day
-     * @param \DateTimeImmutable $requested DateTime extracted from request input
+     * @param DateTimeImmutable $requested DateTime extracted from request input
      *                                      (should come from extractRequestedDateTime)
      *
      * @return \DateTimeInterface First DateTime of the time period
      *
-     * @throws \Exception Type not supported.
+     * @throws Exception Type not supported.
      */
-    public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
+    public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
     {
         switch ($type) {
             case static::MONTH:
@@ -119,7 +122,7 @@ class DailyPageHelper
             case static::DAY:
                 return $requested->modify('Today midnight');
             default:
-                throw new \Exception('Unsupported daily format type');
+                throw new Exception('Unsupported daily format type');
         }
     }
 
@@ -129,14 +132,14 @@ class DailyPageHelper
      *       and we don't want to alter original datetime.
      *
      * @param string             $type      month/week/day
-     * @param \DateTimeImmutable $requested DateTime extracted from request input
+     * @param DateTimeImmutable $requested DateTime extracted from request input
      *                                      (should come from extractRequestedDateTime)
      *
      * @return \DateTimeInterface Last DateTime of the time period
      *
-     * @throws \Exception Type not supported.
+     * @throws Exception Type not supported.
      */
-    public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
+    public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
     {
         switch ($type) {
             case static::MONTH:
@@ -146,7 +149,7 @@ class DailyPageHelper
             case static::DAY:
                 return $requested->modify('Today 23:59:59');
             default:
-                throw new \Exception('Unsupported daily format type');
+                throw new Exception('Unsupported daily format type');
         }
     }
 
@@ -154,16 +157,20 @@ class DailyPageHelper
      * Get localized description of the time period depending on given datetime and type.
      * Example: for a month period, it returns `October, 2020`.
      *
-     * @param string             $type      month/week/day
-     * @param \DateTimeImmutable $requested DateTime extracted from request input
-     *                                      (should come from extractRequestedDateTime)
+     * @param string             $type            month/week/day
+     * @param \DateTimeImmutable $requested       DateTime extracted from request input
+     *                                            (should come from extractRequestedDateTime)
+     * @param bool               $includeRelative Include relative date description (today, yesterday, etc.)
      *
      * @return string Localized time period description
      *
-     * @throws \Exception Type not supported.
+     * @throws Exception Type not supported.
      */
-    public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string
-    {
+    public static function getDescriptionByType(
+        string $type,
+        \DateTimeImmutable $requested,
+        bool $includeRelative = true
+    ): string {
         switch ($type) {
             case static::MONTH:
                 return $requested->format('F') . ', ' . $requested->format('Y');
@@ -172,14 +179,14 @@ class DailyPageHelper
                 return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
             case static::DAY:
                 $out = '';
-                if ($requested->format('Ymd') === date('Ymd')) {
+                if ($includeRelative && $requested->format('Ymd') === date('Ymd')) {
                     $out = t('Today') . ' - ';
-                } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
+                } elseif ($includeRelative && $requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
                     $out = t('Yesterday') . ' - ';
                 }
                 return $out . format_date($requested, false);
             default:
-                throw new \Exception('Unsupported daily format type');
+                throw new Exception('Unsupported daily format type');
         }
     }
 
@@ -190,7 +197,7 @@ class DailyPageHelper
      *
      * @return int number of elements
      *
-     * @throws \Exception Type not supported.
+     * @throws Exception Type not supported.
      */
     public static function getRssLengthByType(string $type): int
     {
@@ -202,7 +209,28 @@ class DailyPageHelper
             case static::DAY:
                 return 30; // ~1 month
             default:
-                throw new \Exception('Unsupported daily format type');
+                throw new Exception('Unsupported daily format type');
         }
     }
+
+    /**
+     * Get the number of items to display in the RSS feed depending on the given type.
+     *
+     * @param string             $type      month/week/day
+     * @param ?DateTimeImmutable $requested Currently only used for UT
+     *
+     * @return DatePeriod number of elements
+     *
+     * @throws Exception Type not supported.
+     */
+    public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod
+    {
+        $requested = $requested ?? new DateTimeImmutable();
+
+        return new DatePeriod(
+            static::getStartDateTimeByType($type, $requested),
+            new \DateInterval('P1D'),
+            static::getEndDateTimeByType($type, $requested)
+        );
+    }
 }
index 3ea55728cc4c9b6af22d88da6f92e533b270069c..7fc0cb047db70d1ca33d8f72529b57ff598b579e 100644 (file)
@@ -4,6 +4,7 @@ namespace Shaarli\Plugin;
 
 use Shaarli\Config\ConfigManager;
 use Shaarli\Plugin\Exception\PluginFileNotFoundException;
+use Shaarli\Plugin\Exception\PluginInvalidRouteException;
 
 /**
  * Class PluginManager
@@ -26,6 +27,14 @@ class PluginManager
      */
     private $loadedPlugins = [];
 
+    /** @var array List of registered routes. Contains keys:
+     *               - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE
+     *               - `route` (path): without prefix, e.g. `/up/{variable}`
+     *                 It will be later prefixed by `/plugin/<plugin name>/`.
+     *               - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`.
+     */
+    protected $registeredRoutes = [];
+
     /**
      * @var ConfigManager Configuration Manager instance.
      */
@@ -86,6 +95,9 @@ class PluginManager
                 $this->loadPlugin($dirs[$index], $plugin);
             } catch (PluginFileNotFoundException $e) {
                 error_log($e->getMessage());
+            } catch (\Throwable $e) {
+                $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
+                $this->errors = array_unique(array_merge($this->errors, [$error]));
             }
         }
     }
@@ -166,6 +178,22 @@ class PluginManager
             }
         }
 
+        $registerRouteFunction = $pluginName . '_register_routes';
+        $routes = null;
+        if (function_exists($registerRouteFunction)) {
+            $routes = call_user_func($registerRouteFunction);
+        }
+
+        if ($routes !== null) {
+            foreach ($routes as $route) {
+                if (static::validateRouteRegistration($route)) {
+                    $this->registeredRoutes[$pluginName][] = $route;
+                } else {
+                    throw new PluginInvalidRouteException($pluginName);
+                }
+            }
+        }
+
         $this->loadedPlugins[] = $pluginName;
     }
 
@@ -237,6 +265,14 @@ class PluginManager
         return $metaData;
     }
 
+    /**
+     * @return array List of registered custom routes by plugins.
+     */
+    public function getRegisteredRoutes(): array
+    {
+        return $this->registeredRoutes;
+    }
+
     /**
      * Return the list of encountered errors.
      *
@@ -246,4 +282,32 @@ class PluginManager
     {
         return $this->errors;
     }
+
+    /**
+     * Checks whether provided input is valid to register a new route.
+     * It must contain keys `method`, `route`, `callable` (all strings).
+     *
+     * @param string[] $input
+     *
+     * @return bool
+     */
+    protected static function validateRouteRegistration(array $input): bool
+    {
+        if (
+            !array_key_exists('method', $input)
+            || !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
+        ) {
+            return false;
+        }
+
+        if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) {
+            return false;
+        }
+
+        if (!array_key_exists('callable', $input)) {
+            return false;
+        }
+
+        return true;
+    }
 }
diff --git a/application/plugin/exception/PluginInvalidRouteException.php b/application/plugin/exception/PluginInvalidRouteException.php
new file mode 100644 (file)
index 0000000..6ba9bc4
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Plugin\Exception;
+
+use Exception;
+
+/**
+ * Class PluginFileNotFoundException
+ *
+ * Raise when plugin files can't be found.
+ */
+class PluginInvalidRouteException extends Exception
+{
+    /**
+     * Construct exception with plugin name.
+     * Generate message.
+     *
+     * @param string $pluginName name of the plugin not found
+     */
+    public function __construct()
+    {
+        $this->message = 'trying to register invalid route.';
+    }
+}
index 97805c3524605bae07b30cb06b4c935517c8126c..fe74bf271bb08448f3f4a605f701fb3b8af0ec24 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Shaarli\Render;
 
+use DatePeriod;
 use Shaarli\Feed\CachedPage;
 
 /**
@@ -49,12 +50,21 @@ class PageCacheManager
         $this->purgeCachedPages();
     }
 
-    public function getCachePage(string $pageUrl): CachedPage
+    /**
+     * Get CachedPage instance for provided URL.
+     *
+     * @param string      $pageUrl
+     * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
+     *
+     * @return CachedPage
+     */
+    public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage
     {
         return new CachedPage(
             $this->pageCacheDir,
             $pageUrl,
-            false === $this->isLoggedIn
+            false === $this->isLoggedIn,
+            $validityPeriod
         );
     }
 }
index 01071d8e550775d7836d99a6129ca5871a7a3d27..2a36ea29d1c99ea334029d520d388816f4d9ec84 100644 (file)
@@ -73,7 +73,7 @@ var_dump(getInfo($baseUrl, $secret));
 ### Authentication
 
 - All requests to Shaarli's API must include a **JWT token** to verify their authenticity.
-- This token must be included as an HTTP header called `Authentication: Bearer <jwt token>`.
+- This token must be included as an HTTP header called `Authorization: Bearer <jwt token>`.
 - JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64:
 
 ```
index f09fadc2925db027873cd2d788ac6a239a6ffa68..79654011b49f910aeb380a805736f812fd6e2490 100644 (file)
@@ -139,6 +139,31 @@ Each file contain two keys:
 
 > Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file.
 
+### Register plugin's routes
+
+Shaarli lets you register custom Slim routes for your plugin.
+
+To register a route, the plugin must include a function called `function <plugin_name>_register_routes(): array`.
+
+This method must return an array of routes, each entry must contain the following keys:
+
+  - `method`: HTTP method, `GET/POST/PUT/PATCH/DELETE`
+  - `route` (path): without prefix, e.g. `/up/{variable}`
+     It will be later prefixed by `/plugin/<plugin name>/`.
+  - `callable` string, function name or FQN class's method to execute, e.g. `demo_plugin_custom_controller`.
+
+Callable functions or methods must have `Slim\Http\Request` and `Slim\Http\Response` parameters
+and return a `Slim\Http\Response`. We recommend creating a dedicated class and extend either
+`ShaarliVisitorController` or `ShaarliAdminController` to use helper functions they provide.
+
+A dedicated plugin template is available for rendering content: `pluginscontent.html` using `content` placeholder.
+
+> **Warning**: plugins are not able to use RainTPL template engine for their content due to technical restrictions.
+> RainTPL does not allow to register multiple template folders, so all HTML rendering must be done within plugin
+> custom controller.
+
+Check out the `demo_plugin` for a live example: `GET <shaarli_url>/plugin/demo_plugin/custom`.
+
 ### Understanding relative paths
 
 Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder.
index 26dede4e29e780577ee692ed8766f609808cd277..01492af4640e7e1a6ce34255d7235edc019e14c8 100644 (file)
@@ -1,8 +1,8 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: Shaarli\n"
-"POT-Creation-Date: 2020-11-09 14:39+0100\n"
-"PO-Revision-Date: 2020-11-09 14:42+0100\n"
+"POT-Creation-Date: 2020-11-24 13:13+0100\n"
+"PO-Revision-Date: 2020-11-24 13:14+0100\n"
 "Last-Translator: \n"
 "Language-Team: Shaarli\n"
 "Language: fr_FR\n"
@@ -20,31 +20,31 @@ msgstr ""
 "X-Poedit-SearchPath-3: init.php\n"
 "X-Poedit-SearchPath-4: plugins\n"
 
-#: application/History.php:180
+#: application/History.php:181
 msgid "History file isn't readable or writable"
 msgstr "Le fichier d'historique n'est pas accessible en lecture ou en Ã©criture"
 
-#: application/History.php:191
+#: application/History.php:192
 msgid "Could not parse history file"
 msgstr "Format incorrect pour le fichier d'historique"
 
-#: application/Languages.php:181
+#: application/Languages.php:184
 msgid "Automatic"
 msgstr "Automatique"
 
-#: application/Languages.php:182
+#: application/Languages.php:185
 msgid "German"
 msgstr "Allemand"
 
-#: application/Languages.php:183
+#: application/Languages.php:186
 msgid "English"
 msgstr "Anglais"
 
-#: application/Languages.php:184
+#: application/Languages.php:187
 msgid "French"
 msgstr "Français"
 
-#: application/Languages.php:185
+#: application/Languages.php:188
 msgid "Japanese"
 msgstr "Japonais"
 
@@ -56,46 +56,46 @@ msgstr ""
 "l'extension php-gd doit Ãªtre chargée pour utiliser les miniatures. Les "
 "miniatures sont désormais désactivées. Rechargez la page."
 
-#: application/Utils.php:402
+#: application/Utils.php:405
 msgid "Setting not set"
 msgstr "Paramètre non défini"
 
-#: application/Utils.php:409
+#: application/Utils.php:412
 msgid "Unlimited"
 msgstr "Illimité"
 
-#: application/Utils.php:412
+#: application/Utils.php:415
 msgid "B"
 msgstr "o"
 
-#: application/Utils.php:412
+#: application/Utils.php:415
 msgid "kiB"
 msgstr "ko"
 
-#: application/Utils.php:412
+#: application/Utils.php:415
 msgid "MiB"
 msgstr "Mo"
 
-#: application/Utils.php:412
+#: application/Utils.php:415
 msgid "GiB"
 msgstr "Go"
 
-#: application/bookmark/BookmarkFileService.php:183
-#: application/bookmark/BookmarkFileService.php:205
-#: application/bookmark/BookmarkFileService.php:227
-#: application/bookmark/BookmarkFileService.php:241
+#: application/bookmark/BookmarkFileService.php:185
+#: application/bookmark/BookmarkFileService.php:207
+#: application/bookmark/BookmarkFileService.php:229
+#: application/bookmark/BookmarkFileService.php:243
 msgid "You're not authorized to alter the datastore"
 msgstr "Vous n'êtes pas autorisé Ã  modifier les données"
 
-#: application/bookmark/BookmarkFileService.php:208
+#: application/bookmark/BookmarkFileService.php:210
 msgid "This bookmarks already exists"
 msgstr "Ce marque-page existe déjà"
 
-#: application/bookmark/BookmarkInitializer.php:39
+#: application/bookmark/BookmarkInitializer.php:42
 msgid "(private bookmark with thumbnail demo)"
 msgstr "(marque page privé avec une miniature)"
 
-#: application/bookmark/BookmarkInitializer.php:42
+#: application/bookmark/BookmarkInitializer.php:45
 msgid ""
 "Shaarli will automatically pick up the thumbnail for links to a variety of "
 "websites.\n"
@@ -118,11 +118,11 @@ msgstr ""
 "\n"
 "Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n"
 
-#: application/bookmark/BookmarkInitializer.php:55
+#: application/bookmark/BookmarkInitializer.php:58
 msgid "Note: Shaare descriptions"
 msgstr "Note : Description des Shaares"
 
-#: application/bookmark/BookmarkInitializer.php:57
+#: application/bookmark/BookmarkInitializer.php:60
 msgid ""
 "Adding a shaare without entering a URL creates a text-only \"note\" post "
 "such as this one.\n"
@@ -186,7 +186,7 @@ msgstr ""
 "| Citron   | Fruit     | Jaune | 30    |\n"
 "| Carotte  | Légume | Orange    | 14    |\n"
 
-#: application/bookmark/BookmarkInitializer.php:91
+#: application/bookmark/BookmarkInitializer.php:94
 #: application/legacy/LegacyLinkDB.php:246
 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
@@ -198,7 +198,7 @@ msgstr ""
 "Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
 "données"
 
-#: application/bookmark/BookmarkInitializer.php:94
+#: application/bookmark/BookmarkInitializer.php:97
 msgid ""
 "Welcome to Shaarli!\n"
 "\n"
@@ -247,11 +247,11 @@ msgstr ""
 "issues) si vous avez une suggestion ou si vous rencontrez un problème.\n"
 " \n"
 
-#: application/bookmark/exception/BookmarkNotFoundException.php:13
+#: application/bookmark/exception/BookmarkNotFoundException.php:14
 msgid "The link you are trying to reach does not exist or has been deleted."
 msgstr "Le lien que vous essayez de consulter n'existe pas ou a Ã©té supprimé."
 
-#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129
+#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:131
 msgid ""
 "Shaarli could not create the config file. Please make sure Shaarli has the "
 "right to write in the folder is it installed in."
@@ -259,12 +259,12 @@ msgstr ""
 "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
 "Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
 
-#: application/config/ConfigManager.php:136
-#: application/config/ConfigManager.php:163
+#: application/config/ConfigManager.php:137
+#: application/config/ConfigManager.php:164
 msgid "Invalid setting key parameter. String expected, got: "
 msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
 
-#: application/config/exception/MissingFieldConfigException.php:21
+#: application/config/exception/MissingFieldConfigException.php:20
 #, php-format
 msgid "Configuration value is required for %s"
 msgstr "Le paramètre %s est obligatoire"
@@ -274,48 +274,48 @@ msgid "An error occurred while trying to save plugins loading order."
 msgstr ""
 "Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions."
 
-#: application/config/exception/UnauthorizedConfigException.php:16
+#: application/config/exception/UnauthorizedConfigException.php:15
 msgid "You are not authorized to alter config."
 msgstr "Vous n'êtes pas autorisé Ã  modifier la configuration."
 
-#: application/exceptions/IOException.php:22
+#: application/exceptions/IOException.php:23
 msgid "Error accessing"
 msgstr "Une erreur s'est produite en accédant Ã "
 
-#: application/feed/FeedBuilder.php:179
+#: application/feed/FeedBuilder.php:180
 msgid "Direct link"
 msgstr "Liens directs"
 
-#: application/feed/FeedBuilder.php:181
+#: application/feed/FeedBuilder.php:182
 #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
 #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
 msgid "Permalink"
 msgstr "Permalien"
 
-#: application/front/controller/admin/ConfigureController.php:54
+#: application/front/controller/admin/ConfigureController.php:56
 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
 msgid "Configure"
 msgstr "Configurer"
 
-#: application/front/controller/admin/ConfigureController.php:102
-#: application/legacy/LegacyUpdater.php:537
+#: application/front/controller/admin/ConfigureController.php:106
+#: application/legacy/LegacyUpdater.php:539
 msgid "You have enabled or changed thumbnails mode."
 msgstr "Vous avez activé ou changé le mode de miniatures."
 
-#: application/front/controller/admin/ConfigureController.php:103
-#: application/front/controller/admin/ServerController.php:75
-#: application/legacy/LegacyUpdater.php:538
+#: application/front/controller/admin/ConfigureController.php:108
+#: application/front/controller/admin/ServerController.php:76
+#: application/legacy/LegacyUpdater.php:540
 msgid "Please synchronize them."
 msgstr "Merci de les synchroniser."
 
-#: application/front/controller/admin/ConfigureController.php:113
-#: application/front/controller/visitor/InstallController.php:146
+#: application/front/controller/admin/ConfigureController.php:119
+#: application/front/controller/visitor/InstallController.php:149
 msgid "Error while writing config file after configuration update."
 msgstr ""
 "Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
 
-#: application/front/controller/admin/ConfigureController.php:122
+#: application/front/controller/admin/ConfigureController.php:128
 msgid "Configuration was saved."
 msgstr "La configuration a Ã©té sauvegardée."
 
@@ -433,7 +433,7 @@ msgstr "Administration serveur"
 msgid "Thumbnails cache has been cleared."
 msgstr "Le cache des miniatures a Ã©té vidé."
 
-#: application/front/controller/admin/ServerController.php:83
+#: application/front/controller/admin/ServerController.php:85
 msgid "Shaarli's cache folder has been cleared!"
 msgstr "Le dossier de cache de Shaarli a Ã©té vidé !"
 
@@ -459,18 +459,18 @@ msgstr "Le lien avec l'identifiant %s n'a pas pu Ãªtre trouvé."
 msgid "Invalid visibility provided."
 msgstr "Visibilité du lien non valide."
 
-#: application/front/controller/admin/ShaarePublishController.php:171
+#: application/front/controller/admin/ShaarePublishController.php:173
 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
 msgid "Edit"
 msgstr "Modifier"
 
-#: application/front/controller/admin/ShaarePublishController.php:174
+#: application/front/controller/admin/ShaarePublishController.php:176
 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
 msgid "Shaare"
 msgstr "Shaare"
 
-#: application/front/controller/admin/ShaarePublishController.php:205
+#: application/front/controller/admin/ShaarePublishController.php:208
 msgid "Note: "
 msgstr "Note : "
 
@@ -485,7 +485,7 @@ msgstr "Mise Ã  jour des miniatures"
 msgid "Tools"
 msgstr "Outils"
 
-#: application/front/controller/visitor/BookmarkListController.php:120
+#: application/front/controller/visitor/BookmarkListController.php:121
 msgid "Search: "
 msgstr "Recherche : "
 
@@ -535,12 +535,12 @@ msgstr "Une erreur inattendue s'est produite."
 msgid "Requested page could not be found."
 msgstr "La page demandée n'a pas pu Ãªtre trouvée."
 
-#: application/front/controller/visitor/InstallController.php:64
+#: application/front/controller/visitor/InstallController.php:65
 #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
 msgid "Install Shaarli"
 msgstr "Installation de Shaarli"
 
-#: application/front/controller/visitor/InstallController.php:83
+#: application/front/controller/visitor/InstallController.php:85
 #, php-format
 msgid ""
 "<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@@ -559,14 +559,14 @@ msgstr ""
 "des cookies. Nous vous recommandons d'accéder Ã  votre serveur depuis son "
 "adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
 
-#: application/front/controller/visitor/InstallController.php:154
+#: application/front/controller/visitor/InstallController.php:157
 msgid ""
 "Shaarli is now configured. Please login and start shaaring your bookmarks!"
 msgstr ""
 "Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez Ã  "
 "shaare vos liens !"
 
-#: application/front/controller/visitor/InstallController.php:168
+#: application/front/controller/visitor/InstallController.php:171
 msgid "Insufficient permissions:"
 msgstr "Permissions insuffisantes :"
 
@@ -580,7 +580,7 @@ msgstr "Permissions insuffisantes :"
 msgid "Login"
 msgstr "Connexion"
 
-#: application/front/controller/visitor/LoginController.php:77
+#: application/front/controller/visitor/LoginController.php:78
 msgid "Wrong login/password."
 msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
 
@@ -620,7 +620,7 @@ msgstr ""
 msgid "Wrong token."
 msgstr "Jeton invalide."
 
-#: application/helper/ApplicationUtils.php:162
+#: application/helper/ApplicationUtils.php:165
 #, php-format
 msgid ""
 "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
@@ -631,52 +631,60 @@ msgstr ""
 "peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
 "connues et devrait Ãªtre mise Ã  jour au plus tôt."
 
-#: application/helper/ApplicationUtils.php:195
-#: application/helper/ApplicationUtils.php:215
+#: application/helper/ApplicationUtils.php:200
+#: application/helper/ApplicationUtils.php:220
 msgid "directory is not readable"
 msgstr "le répertoire n'est pas accessible en lecture"
 
-#: application/helper/ApplicationUtils.php:218
+#: application/helper/ApplicationUtils.php:223
 msgid "directory is not writable"
 msgstr "le répertoire n'est pas accessible en Ã©criture"
 
-#: application/helper/ApplicationUtils.php:240
+#: application/helper/ApplicationUtils.php:247
 msgid "file is not readable"
 msgstr "le fichier n'est pas accessible en lecture"
 
-#: application/helper/ApplicationUtils.php:243
+#: application/helper/ApplicationUtils.php:250
 msgid "file is not writable"
 msgstr "le fichier n'est pas accessible en Ã©criture"
 
-#: application/helper/ApplicationUtils.php:277
+#: application/helper/ApplicationUtils.php:260
+msgid ""
+"Lock can not be acquired on the datastore. You might encounter concurrent "
+"access issues."
+msgstr ""
+"Le fichier datastore ne peut pas Ãªtre verrouillé. Vous pourriez rencontrer "
+"des problèmes d'accès concurrents."
+
+#: application/helper/ApplicationUtils.php:293
 msgid "Configuration parsing"
 msgstr "Chargement de la configuration"
 
-#: application/helper/ApplicationUtils.php:278
+#: application/helper/ApplicationUtils.php:294
 msgid "Slim Framework (routing, etc.)"
 msgstr "Slim Framwork (routage, etc.)"
 
-#: application/helper/ApplicationUtils.php:279
+#: application/helper/ApplicationUtils.php:295
 msgid "Multibyte (Unicode) string support"
 msgstr "Support des chaînes de caractère multibytes (Unicode)"
 
-#: application/helper/ApplicationUtils.php:280
+#: application/helper/ApplicationUtils.php:296
 msgid "Required to use thumbnails"
 msgstr "Obligatoire pour utiliser les miniatures"
 
-#: application/helper/ApplicationUtils.php:281
+#: application/helper/ApplicationUtils.php:297
 msgid "Localized text sorting (e.g. e->è->f)"
 msgstr "Tri des textes traduits (ex : e->è->f)"
 
-#: application/helper/ApplicationUtils.php:282
+#: application/helper/ApplicationUtils.php:298
 msgid "Better retrieval of bookmark metadata and thumbnail"
 msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
 
-#: application/helper/ApplicationUtils.php:283
+#: application/helper/ApplicationUtils.php:299
 msgid "Use the translation system in gettext mode"
 msgstr "Utiliser le système de traduction en mode gettext"
 
-#: application/helper/ApplicationUtils.php:284
+#: application/helper/ApplicationUtils.php:300
 msgid "Login using LDAP server"
 msgstr "Authentification via un serveur LDAP"
 
@@ -750,7 +758,7 @@ msgstr ""
 msgid "Couldn't retrieve updater class methods."
 msgstr "Impossible de récupérer les méthodes de la classe Updater."
 
-#: application/legacy/LegacyUpdater.php:538
+#: application/legacy/LegacyUpdater.php:540
 msgid "<a href=\"./admin/thumbnails\">"
 msgstr "<a href=\"./admin/thumbnails\">"
 
@@ -776,11 +784,11 @@ msgstr ""
 "a Ã©té importé avec succès en %d secondes : %d liens importés, %d liens "
 "écrasés, %d liens ignorés."
 
-#: application/plugin/PluginManager.php:124
+#: application/plugin/PluginManager.php:125
 msgid " [plugin incompatibility]: "
 msgstr " [incompatibilité de l'extension] : "
 
-#: application/plugin/exception/PluginFileNotFoundException.php:21
+#: application/plugin/exception/PluginFileNotFoundException.php:22
 #, php-format
 msgid "Plugin \"%s\" files not found."
 msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
@@ -794,7 +802,7 @@ msgstr "Impossible de purger %s : le répertoire n'existe pas"
 msgid "An error occurred while running the update "
 msgstr "Une erreur s'est produite lors de l'exécution de la mise Ã  jour "
 
-#: index.php:80
+#: index.php:81
 msgid "Shared bookmarks on "
 msgstr "Liens partagés sur "
 
@@ -811,11 +819,11 @@ msgstr "Shaare"
 msgid "Adds the addlink input on the linklist page."
 msgstr "Ajoute le formulaire d'ajout de liens sur la page principale."
 
-#: plugins/archiveorg/archiveorg.php:28
+#: plugins/archiveorg/archiveorg.php:29
 msgid "View on archive.org"
 msgstr "Voir sur archive.org"
 
-#: plugins/archiveorg/archiveorg.php:41
+#: plugins/archiveorg/archiveorg.php:42
 msgid "For each link, add an Archive.org icon."
 msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
 
@@ -845,7 +853,7 @@ msgstr "Couleur de fond (gris léger)"
 msgid "Dark main color (e.g. visited links)"
 msgstr "Couleur principale sombre (ex : les liens visités)"
 
-#: plugins/demo_plugin/demo_plugin.php:477
+#: plugins/demo_plugin/demo_plugin.php:478
 msgid ""
 "A demo plugin covering all use cases for template designers and plugin "
 "developers."
@@ -853,11 +861,11 @@ msgstr ""
 "Une extension de démonstration couvrant tous les cas d'utilisation pour les "
 "designers de thèmes et les développeurs d'extensions."
 
-#: plugins/demo_plugin/demo_plugin.php:478
+#: plugins/demo_plugin/demo_plugin.php:479
 msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
 msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé."
 
-#: plugins/demo_plugin/demo_plugin.php:479
+#: plugins/demo_plugin/demo_plugin.php:480
 msgid "Other demo parameter"
 msgstr "Un autre paramètre de démo"
 
@@ -879,7 +887,7 @@ msgstr ""
 msgid "Isso server URL (without 'http://')"
 msgstr "URL du serveur Isso (sans 'http://')"
 
-#: plugins/piwik/piwik.php:23
+#: plugins/piwik/piwik.php:24
 msgid ""
 "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
 "administration page."
@@ -887,27 +895,27 @@ msgstr ""
 "Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et "
 "PIWIK_SITEID dans la page d'administration des extensions."
 
-#: plugins/piwik/piwik.php:72
+#: plugins/piwik/piwik.php:73
 msgid "A plugin that adds Piwik tracking code to Shaarli pages."
 msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli."
 
-#: plugins/piwik/piwik.php:73
+#: plugins/piwik/piwik.php:74
 msgid "Piwik URL"
 msgstr "URL de Piwik"
 
-#: plugins/piwik/piwik.php:74
+#: plugins/piwik/piwik.php:75
 msgid "Piwik site ID"
 msgstr "Site ID de Piwik"
 
-#: plugins/playvideos/playvideos.php:25
+#: plugins/playvideos/playvideos.php:26
 msgid "Video player"
 msgstr "Lecteur vidéo"
 
-#: plugins/playvideos/playvideos.php:28
+#: plugins/playvideos/playvideos.php:29
 msgid "Play Videos"
 msgstr "Jouer les vidéos"
 
-#: plugins/playvideos/playvideos.php:59
+#: plugins/playvideos/playvideos.php:60
 msgid "Add a button in the toolbar allowing to watch all videos."
 msgstr ""
 "Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos."
@@ -935,11 +943,11 @@ msgstr "Mauvaise réponse du hub %s"
 msgid "Enable PubSubHubbub feed publishing."
 msgstr "Active la publication de flux vers PubSubHubbub."
 
-#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:71
+#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72
 msgid "For each link, add a QRCode icon."
 msgstr "Pour chaque lien, ajouter une icône de QRCode."
 
-#: plugins/wallabag/wallabag.php:21
+#: plugins/wallabag/wallabag.php:22
 msgid ""
 "Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
 "plugin administration page."
@@ -947,15 +955,15 @@ msgstr ""
 "Erreur de l'extension Wallabag : Merci de définir le paramètre Â« "
 "WALLABAG_URL Â» dans la page d'administration des extensions."
 
-#: plugins/wallabag/wallabag.php:48
+#: plugins/wallabag/wallabag.php:49
 msgid "Save to wallabag"
 msgstr "Sauvegarder dans Wallabag"
 
-#: plugins/wallabag/wallabag.php:72
+#: plugins/wallabag/wallabag.php:73
 msgid "Wallabag API URL"
 msgstr "URL de l'API Wallabag"
 
-#: plugins/wallabag/wallabag.php:73
+#: plugins/wallabag/wallabag.php:74
 msgid "Wallabag API version (1 or 2)"
 msgstr "Version de l'API Wallabag (1 ou 2)"
 
index 1eb7659af859a2a785a5f913c6be5b6b5f80c966..862c53efa5d6716ba6faef19b0adf073266a5587 100644 (file)
--- a/index.php
+++ b/index.php
@@ -31,6 +31,7 @@ use Psr\Log\LogLevel;
 use Shaarli\Config\ConfigManager;
 use Shaarli\Container\ContainerBuilder;
 use Shaarli\Languages;
+use Shaarli\Plugin\PluginManager;
 use Shaarli\Security\BanManager;
 use Shaarli\Security\CookieManager;
 use Shaarli\Security\LoginManager;
@@ -87,7 +88,17 @@ date_default_timezone_set($conf->get('general.timezone', 'UTC'));
 
 $loginManager->checkLoginState(client_ip_id($_SERVER));
 
-$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger);
+$pluginManager = new PluginManager($conf);
+$pluginManager->load($conf->get('general.enabled_plugins', []));
+
+$containerBuilder = new ContainerBuilder(
+    $conf,
+    $sessionManager,
+    $cookieManager,
+    $loginManager,
+    $pluginManager,
+    $logger
+);
 $container = $containerBuilder->build();
 $app = new App($container);
 
@@ -154,6 +165,15 @@ $app->group('/admin', function () {
     $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
 })->add('\Shaarli\Front\ShaarliAdminMiddleware');
 
+$app->group('/plugin', function () use ($pluginManager) {
+    foreach ($pluginManager->getRegisteredRoutes() as $pluginName => $routes) {
+        $this->group('/' . $pluginName, function () use ($routes) {
+            foreach ($routes as $route) {
+                $this->{strtolower($route['method'])}('/' . ltrim($route['route'], '/'), $route['callable']);
+            }
+        });
+    }
+})->add('\Shaarli\Front\ShaarliMiddleware');
 
 // REST API routes
 $app->group('/api/v1', function () {
index c559e35da97bb4d8ec7afb229c7845fab983767f..9bdc872092a0d3aff71bb1652db2148f3cc1790f 100644 (file)
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -18,5 +18,6 @@
   <rule ref="PSR1.Files.SideEffects.FoundWithSymbols">
     <!--  index.php bootstraps everything, so yes mixed symbols with side effects  -->
     <exclude-pattern>index.php</exclude-pattern>
+    <exclude-pattern>plugins/*</exclude-pattern>
   </rule>
 </ruleset>
diff --git a/plugins/demo_plugin/DemoPluginController.php b/plugins/demo_plugin/DemoPluginController.php
new file mode 100644 (file)
index 0000000..b8ace9c
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\DemoPlugin;
+
+use Shaarli\Front\Controller\Admin\ShaarliAdminController;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DemoPluginController extends ShaarliAdminController
+{
+    public function index(Request $request, Response $response): Response
+    {
+        $this->assignView(
+            'content',
+            '<div class="center">' .
+                'This is a demo page. I have access to Shaarli container, so I\'m free to do whatever I want here.' .
+            '</div>'
+        );
+
+        return $response->write($this->render('pluginscontent'));
+    }
+}
index 22d27b6827f306ad3a3230ffd8361cd543a5bd6b..15cfc2c51cd4889cc0d38f821e0ab856806a4167 100644 (file)
@@ -7,6 +7,8 @@
  * Can be used by plugin developers to make their own plugin.
  */
 
+require_once __DIR__ . '/DemoPluginController.php';
+
 /*
  * RENDER HEADER, INCLUDES, FOOTER
  *
@@ -60,6 +62,17 @@ function demo_plugin_init($conf)
     return $errors;
 }
 
+function demo_plugin_register_routes(): array
+{
+    return [
+        [
+            'method' => 'GET',
+            'route' => '/custom',
+            'callable' => 'Shaarli\DemoPlugin\DemoPluginController:index',
+        ],
+    ];
+}
+
 /**
  * Hook render_header.
  * Executed on every page render.
@@ -304,7 +317,11 @@ function hook_demo_plugin_render_editlink($data)
 function hook_demo_plugin_render_tools($data)
 {
     // field_plugin
-    $data['tools_plugin'][] = 'tools_plugin';
+    $data['tools_plugin'][] = '<div class="tools-item">
+        <a href="' . $data['_BASE_PATH_'] . '/plugin/demo_plugin/custom">
+          <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Demo Plugin Custom Route</span>
+        </a>
+      </div>';
 
     return $data;
 }
index efef5e8746ed2b165d4902a1877a550cc007b462..8947f6791831cec961c9c84a73e644a7c197ee7f 100644 (file)
@@ -120,4 +120,43 @@ class PluginManagerTest extends \Shaarli\TestCase
         $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
         $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
     }
+
+    /**
+     * Test plugin custom routes - note that there is no check on callable functions
+     */
+    public function testRegisteredRoutes(): void
+    {
+        PluginManager::$PLUGINS_PATH = self::$pluginPath;
+        $this->pluginManager->load([self::$pluginName]);
+
+        $expectedParameters = [
+            [
+                'method' => 'GET',
+                'route' => '/test',
+                'callable' => 'getFunction',
+            ],
+            [
+                'method' => 'POST',
+                'route' => '/custom',
+                'callable' => 'postFunction',
+            ],
+        ];
+        $meta = $this->pluginManager->getRegisteredRoutes();
+        static::assertSame($expectedParameters, $meta[self::$pluginName]);
+    }
+
+    /**
+     * Test plugin custom routes with invalid route
+     */
+    public function testRegisteredRoutesInvalid(): void
+    {
+        $plugin = 'test_route_invalid';
+        $this->pluginManager->load([$plugin]);
+
+        $meta = $this->pluginManager->getRegisteredRoutes();
+        static::assertSame([], $meta);
+
+        $errors = $this->pluginManager->getErrors();
+        static::assertSame(['test_route_invalid [plugin incompatibility]: trying to register invalid route.'], $errors);
+    }
 }
index 3d43c34470d098dd69a18e5a78c874187c0ca83d..04d4ef014878c75d18da09e841b20451d410a508 100644 (file)
@@ -43,11 +43,15 @@ class ContainerBuilderTest extends TestCase
     /** @var CookieManager */
     protected $cookieManager;
 
+    /** @var PluginManager */
+    protected $pluginManager;
+
     public function setUp(): void
     {
         $this->conf = new ConfigManager('tests/utils/config/configJson');
         $this->sessionManager = $this->createMock(SessionManager::class);
         $this->cookieManager = $this->createMock(CookieManager::class);
+        $this->pluginManager = $this->createMock(PluginManager::class);
 
         $this->loginManager = $this->createMock(LoginManager::class);
         $this->loginManager->method('isLoggedIn')->willReturn(true);
@@ -57,6 +61,7 @@ class ContainerBuilderTest extends TestCase
             $this->sessionManager,
             $this->cookieManager,
             $this->loginManager,
+            $this->pluginManager,
             $this->createMock(LoggerInterface::class)
         );
     }
index 904db9dc2251a4f23da5ecbf57df59c1b85e5d72..1decfaf3b5c70fd00e31dd1200bcae62f5b892e2 100644 (file)
@@ -40,10 +40,10 @@ class CachedPageTest extends \Shaarli\TestCase
      */
     public function testConstruct()
     {
-        new CachedPage(self::$testCacheDir, '', true);
-        new CachedPage(self::$testCacheDir, '', false);
-        new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true);
-        new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false);
+        new CachedPage(self::$testCacheDir, '', true, null);
+        new CachedPage(self::$testCacheDir, '', false, null);
+        new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true, null);
+        new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false, null);
         $this->addToAssertionCount(1);
     }
 
@@ -52,7 +52,7 @@ class CachedPageTest extends \Shaarli\TestCase
      */
     public function testCache()
     {
-        $page = new CachedPage(self::$testCacheDir, self::$url, true);
+        $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
 
         $this->assertFileNotExists(self::$filename);
         $page->cache('<p>Some content</p>');
@@ -68,7 +68,7 @@ class CachedPageTest extends \Shaarli\TestCase
      */
     public function testShouldNotCache()
     {
-        $page = new CachedPage(self::$testCacheDir, self::$url, false);
+        $page = new CachedPage(self::$testCacheDir, self::$url, false, null);
 
         $this->assertFileNotExists(self::$filename);
         $page->cache('<p>Some content</p>');
@@ -80,7 +80,7 @@ class CachedPageTest extends \Shaarli\TestCase
      */
     public function testCachedVersion()
     {
-        $page = new CachedPage(self::$testCacheDir, self::$url, true);
+        $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
 
         $this->assertFileNotExists(self::$filename);
         $page->cache('<p>Some content</p>');
@@ -96,7 +96,7 @@ class CachedPageTest extends \Shaarli\TestCase
      */
     public function testCachedVersionNoFile()
     {
-        $page = new CachedPage(self::$testCacheDir, self::$url, true);
+        $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
 
         $this->assertFileNotExists(self::$filename);
         $this->assertEquals(
@@ -110,7 +110,7 @@ class CachedPageTest extends \Shaarli\TestCase
      */
     public function testNoCachedVersion()
     {
-        $page = new CachedPage(self::$testCacheDir, self::$url, false);
+        $page = new CachedPage(self::$testCacheDir, self::$url, false, null);
 
         $this->assertFileNotExists(self::$filename);
         $this->assertEquals(
@@ -118,4 +118,43 @@ class CachedPageTest extends \Shaarli\TestCase
             $page->cachedVersion()
         );
     }
+
+    /**
+     * Return a page's cached content within date period
+     */
+    public function testCachedVersionInDatePeriod()
+    {
+        $period = new \DatePeriod(
+            new \DateTime('yesterday'),
+            new \DateInterval('P1D'),
+            new \DateTime('tomorrow')
+        );
+        $page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
+
+        $this->assertFileNotExists(self::$filename);
+        $page->cache('<p>Some content</p>');
+        $this->assertFileExists(self::$filename);
+        $this->assertEquals(
+            '<p>Some content</p>',
+            $page->cachedVersion()
+        );
+    }
+
+    /**
+     * Return a page's cached content outside of date period
+     */
+    public function testCachedVersionNotInDatePeriod()
+    {
+        $period = new \DatePeriod(
+            new \DateTime('yesterday noon'),
+            new \DateInterval('P1D'),
+            new \DateTime('yesterday midnight')
+        );
+        $page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
+
+        $this->assertFileNotExists(self::$filename);
+        $page->cache('<p>Some content</p>');
+        $this->assertFileExists(self::$filename);
+        $this->assertNull($page->cachedVersion());
+    }
 }
index 5255b7b16db55e0afa388e14c319bf02612eede3..2d7458004ade968e2b678a8dbcd9e32e4513ded0 100644 (file)
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Shaarli\Helper;
 
+use DateTimeImmutable;
+use DateTimeInterface;
 use Shaarli\Bookmark\Bookmark;
 use Shaarli\TestCase;
 use Slim\Http\Request;
@@ -32,7 +34,7 @@ class DailyPageHelperTest extends TestCase
         string $type,
         string $input,
         ?Bookmark $bookmark,
-        \DateTimeInterface $expectedDateTime,
+        DateTimeInterface $expectedDateTime,
         string $compareFormat = 'Ymd'
     ): void {
         $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
@@ -71,8 +73,8 @@ class DailyPageHelperTest extends TestCase
      */
     public function testGetStartDatesByType(
         string $type,
-        \DateTimeImmutable $dateTime,
-        \DateTimeInterface $expectedDateTime
+        DateTimeImmutable $dateTime,
+        DateTimeInterface $expectedDateTime
     ): void {
         $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
 
@@ -84,7 +86,7 @@ class DailyPageHelperTest extends TestCase
         $this->expectException(\Exception::class);
         $this->expectExceptionMessage('Unsupported daily format type');
 
-        DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
+        DailyPageHelper::getStartDateTimeByType('nope', new DateTimeImmutable());
     }
 
     /**
@@ -92,8 +94,8 @@ class DailyPageHelperTest extends TestCase
      */
     public function testGetEndDatesByType(
         string $type,
-        \DateTimeImmutable $dateTime,
-        \DateTimeInterface $expectedDateTime
+        DateTimeImmutable $dateTime,
+        DateTimeInterface $expectedDateTime
     ): void {
         $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
 
@@ -105,7 +107,7 @@ class DailyPageHelperTest extends TestCase
         $this->expectException(\Exception::class);
         $this->expectExceptionMessage('Unsupported daily format type');
 
-        DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
+        DailyPageHelper::getEndDateTimeByType('nope', new DateTimeImmutable());
     }
 
     /**
@@ -113,7 +115,7 @@ class DailyPageHelperTest extends TestCase
      */
     public function testGeDescriptionsByType(
         string $type,
-        \DateTimeImmutable $dateTime,
+        DateTimeImmutable $dateTime,
         string $expectedDescription
     ): void {
         $description = DailyPageHelper::getDescriptionByType($type, $dateTime);
@@ -121,12 +123,25 @@ class DailyPageHelperTest extends TestCase
         static::assertEquals($expectedDescription, $description);
     }
 
+    /**
+     * @dataProvider getDescriptionsByTypeNotIncludeRelative
+     */
+    public function testGeDescriptionsByTypeNotIncludeRelative(
+        string $type,
+        \DateTimeImmutable $dateTime,
+        string $expectedDescription
+    ): void {
+        $description = DailyPageHelper::getDescriptionByType($type, $dateTime, false);
+
+        static::assertEquals($expectedDescription, $description);
+    }
+
     public function getDescriptionByTypeExceptionUnknownType(): void
     {
         $this->expectException(\Exception::class);
         $this->expectExceptionMessage('Unsupported daily format type');
 
-        DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable());
+        DailyPageHelper::getDescriptionByType('nope', new DateTimeImmutable());
     }
 
     /**
@@ -146,6 +161,29 @@ class DailyPageHelperTest extends TestCase
         DailyPageHelper::getRssLengthByType('nope');
     }
 
+    /**
+     * @dataProvider getCacheDatePeriodByType
+     */
+    public function testGetCacheDatePeriodByType(
+        string $type,
+        DateTimeImmutable $requested,
+        DateTimeInterface $start,
+        DateTimeInterface $end
+    ): void {
+        $period = DailyPageHelper::getCacheDatePeriodByType($type, $requested);
+
+        static::assertEquals($start, $period->getStartDate());
+        static::assertEquals($end, $period->getEndDate());
+    }
+
+    public function testGetCacheDatePeriodByTypeExceptionUnknownType(): void
+    {
+        $this->expectException(\Exception::class);
+        $this->expectExceptionMessage('Unsupported daily format type');
+
+        DailyPageHelper::getCacheDatePeriodByType('nope');
+    }
+
     /**
      * Data provider for testExtractRequestedType() test method.
      */
@@ -216,9 +254,9 @@ class DailyPageHelperTest extends TestCase
     public function getStartDatesByType(): array
     {
         return [
-            [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
-            [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
-            [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
+            [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
+            [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
+            [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
         ];
     }
 
@@ -228,9 +266,9 @@ class DailyPageHelperTest extends TestCase
     public function getEndDatesByType(): array
     {
         return [
-            [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
-            [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
-            [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
+            [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
+            [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
+            [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
         ];
     }
 
@@ -240,8 +278,22 @@ class DailyPageHelperTest extends TestCase
     public function getDescriptionsByType(): array
     {
         return [
-            [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
-            [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
+            [DailyPageHelper::DAY, $date = new DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
+            [DailyPageHelper::DAY, $date = new DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
+            [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
+            [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
+            [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
+        ];
+    }
+
+    /**
+     * Data provider for testGeDescriptionsByTypeNotIncludeRelative() test method.
+     */
+    public function getDescriptionsByTypeNotIncludeRelative(): array
+    {
+        return [
+            [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), $date->format('F j, Y')],
+            [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), $date->format('F j, Y')],
             [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
             [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
             [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
@@ -249,7 +301,7 @@ class DailyPageHelperTest extends TestCase
     }
 
     /**
-     * Data provider for testGetDescriptionsByType() test method.
+     * Data provider for testGetRssLengthsByType() test method.
      */
     public function getRssLengthsByType(): array
     {
@@ -259,4 +311,31 @@ class DailyPageHelperTest extends TestCase
             [DailyPageHelper::MONTH],
         ];
     }
+
+    /**
+     * Data provider for testGetCacheDatePeriodByType() test method.
+     */
+    public function getCacheDatePeriodByType(): array
+    {
+        return [
+            [
+                DailyPageHelper::DAY,
+                new DateTimeImmutable('2020-10-09 04:05:06'),
+                new \DateTime('2020-10-09 00:00:00'),
+                new \DateTime('2020-10-09 23:59:59'),
+            ],
+            [
+                DailyPageHelper::WEEK,
+                new DateTimeImmutable('2020-10-09 04:05:06'),
+                new \DateTime('2020-10-05 00:00:00'),
+                new \DateTime('2020-10-11 23:59:59'),
+            ],
+            [
+                DailyPageHelper::MONTH,
+                new DateTimeImmutable('2020-10-09 04:05:06'),
+                new \DateTime('2020-10-01 00:00:00'),
+                new \DateTime('2020-10-31 23:59:59'),
+            ],
+        ];
+    }
 }
index 03be4f4e8c997bd9eb875ad1f42a65bf4d294eb7..34cd339e1a8cb84409f689d3a48f67dbb5124744 100644 (file)
@@ -27,3 +27,19 @@ function hook_test_error()
 {
     new Unknown();
 }
+
+function test_register_routes(): array
+{
+    return [
+        [
+            'method' => 'GET',
+            'route' => '/test',
+            'callable' => 'getFunction',
+        ],
+        [
+            'method' => 'POST',
+            'route' => '/custom',
+            'callable' => 'postFunction',
+        ],
+    ];
+}
diff --git a/tests/plugins/test_route_invalid/test_route_invalid.php b/tests/plugins/test_route_invalid/test_route_invalid.php
new file mode 100644 (file)
index 0000000..0c5a510
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+function test_route_invalid_register_routes(): array
+{
+    return [
+        [
+            'method' => 'GET',
+            'route' => 'not a route',
+            'callable' => 'getFunction',
+        ],
+    ];
+}
diff --git a/tpl/default/pluginscontent.html b/tpl/default/pluginscontent.html
new file mode 100644 (file)
index 0000000..1e4f6b8
--- /dev/null
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
+<head>
+  {include="includes"}
+</head>
+<body>
+  {include="page.header"}
+
+  {$content}
+
+  {include="page.footer"}
+</body>
+</html>
index 55bd9827843a20d21bd96e425c69e53af52fa976..97fb0fad140aa6cb5d95525193156e9ff4834f46 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -3052,9 +3052,9 @@ inherits@2.0.3:
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
 ini@^1.3.4, ini@^1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-  integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
+  integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
 
 interpret@^1.4.0:
   version "1.4.0"