# Stage 4:
# - Shaarli image
-FROM alpine:3.8
+FROM alpine:3.12
LABEL maintainer="Shaarli Community"
RUN apk --update --no-cache add \
# 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 \
# 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 \
# 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 \
# Stage 4:
# - Shaarli image
-FROM arm32v6/alpine:3.8
+FROM arm32v6/alpine:3.10
LABEL maintainer="Shaarli Community"
RUN apk --update --no-cache add \
namespace Shaarli\Bookmark;
+use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\mutex\Mutex;
use malkusch\lock\mutex\NoMutex;
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
}
$content = null;
- $this->mutex->synchronized(function () use (&$content) {
+ $this->synchronized(function () use (&$content) {
$content = file_get_contents($this->datastore);
});
$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();
+ }
+ }
}
$properties = implode('|', $propertiesKey);
// We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
$orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
+ // Support quotes in double quoted content, and the other way around
+ $content = 'content=(["\'])((?:(?!\1).)*)\1';
// Try to retrieve OpenGraph tag.
- $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#';
+ $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*' . $content . '.*?>#';
// If the attributes are not in the order property => content (e.g. Github)
// New regex to keep this readable... more or less.
- $ogRegexReverse = '#<meta[^>]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
+ $ogRegexReverse = '#<meta[^>]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
if (
preg_match($ogRegex, $html, $matches) > 0
/** @var LoginManager */
protected $login;
+ /** @var PluginManager */
+ protected $pluginManager;
+
/** @var LoggerInterface */
protected $logger;
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;
}
$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'));
);
};
- $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,
<?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;
}
/**
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);
}
/**
$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);
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)) {
}
$days = [];
- $type = DailyPageHelper::extractRequestedType($request);
$format = DailyPageHelper::getFormatByType($type);
$length = DailyPageHelper::getRssLengthByType($type);
foreach ($this->container->bookmarkService->search() as $bookmark) {
$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' => [],
];
$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'));
namespace Shaarli\Helper;
use Exception;
+use malkusch\lock\exception\LockAcquireException;
+use malkusch\lock\mutex\FlockMutex;
use Shaarli\Config\ConfigManager;
/**
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.
*
namespace Shaarli\Helper;
+use DatePeriod;
+use DateTimeImmutable;
+use Exception;
use Shaarli\Bookmark\Bookmark;
use Slim\Http\Request;
* @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);
}
/**
*
* @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
{
case static::DAY:
return 'Ymd';
default:
- throw new \Exception('Unsupported daily format type');
+ throw new Exception('Unsupported daily format type');
}
}
* 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:
case static::DAY:
return $requested->modify('Today midnight');
default:
- throw new \Exception('Unsupported daily format type');
+ throw new Exception('Unsupported daily format type');
}
}
* 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:
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');
}
}
* 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');
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');
}
}
*
* @return int number of elements
*
- * @throws \Exception Type not supported.
+ * @throws Exception Type not supported.
*/
public static function getRssLengthByType(string $type): int
{
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)
+ );
+ }
}
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\Exception\PluginFileNotFoundException;
+use Shaarli\Plugin\Exception\PluginInvalidRouteException;
/**
* 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.
*/
$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]));
}
}
}
}
}
+ $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;
}
return $metaData;
}
+ /**
+ * @return array List of registered custom routes by plugins.
+ */
+ public function getRegisteredRoutes(): array
+ {
+ return $this->registeredRoutes;
+ }
+
/**
* Return the list of encountered errors.
*
{
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;
+ }
}
--- /dev/null
+<?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.';
+ }
+}
namespace Shaarli\Render;
+use DatePeriod;
use Shaarli\Feed\CachedPage;
/**
$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
);
}
}
### 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:
```
> 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.
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"
"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"
"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"
"\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"
"| 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
"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"
"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."
"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"
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."
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é !"
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 : "
msgid "Tools"
msgstr "Outils"
-#: application/front/controller/visitor/BookmarkListController.php:120
+#: application/front/controller/visitor/BookmarkListController.php:121
msgid "Search: "
msgstr "Recherche : "
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 "
"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 :"
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)."
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 "
"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"
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\">"
"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."
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 "
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."
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."
"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"
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."
"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."
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."
"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)"
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;
$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);
$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 () {
<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>
--- /dev/null
+<?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'));
+ }
+}
* Can be used by plugin developers to make their own plugin.
*/
+require_once __DIR__ . '/DemoPluginController.php';
+
/*
* RENDER HEADER, INCLUDES, FOOTER
*
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.
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;
}
$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);
+ }
}
$this->assertFalse(html_extract_tag('description', $html));
}
+ public function testHtmlExtractDescriptionFromGoogleRealCase(): void
+ {
+ $html = 'id="gsr"><meta content="Fêtes de fin d\'année" property="twitter:title"><meta '.
+ 'content="Bonnes fêtes de fin d\'année ! #GoogleDoodle" property="twitter:description">'.
+ '<meta content="Bonnes fêtes de fin d\'année ! #GoogleDoodle" property="og:description">'.
+ '<meta content="summary_large_image" property="twitter:card"><meta co'
+ ;
+ $this->assertSame('Bonnes fêtes de fin d\'année ! #GoogleDoodle', html_extract_tag('description', $html));
+ }
+
/**
* Test the header callback with valid value
*/
/** @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);
$this->sessionManager,
$this->cookieManager,
$this->loginManager,
+ $this->pluginManager,
$this->createMock(LoggerInterface::class)
);
}
*/
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);
}
*/
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>');
*/
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>');
*/
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>');
*/
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(
*/
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(
$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());
+ }
}
namespace Shaarli\Helper;
+use DateTimeImmutable;
+use DateTimeInterface;
use Shaarli\Bookmark\Bookmark;
use Shaarli\TestCase;
use Slim\Http\Request;
string $type,
string $input,
?Bookmark $bookmark,
- \DateTimeInterface $expectedDateTime,
+ DateTimeInterface $expectedDateTime,
string $compareFormat = 'Ymd'
): void {
$dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
*/
public function testGetStartDatesByType(
string $type,
- \DateTimeImmutable $dateTime,
- \DateTimeInterface $expectedDateTime
+ DateTimeImmutable $dateTime,
+ DateTimeInterface $expectedDateTime
): void {
$startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
- DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
+ DailyPageHelper::getStartDateTimeByType('nope', new DateTimeImmutable());
}
/**
*/
public function testGetEndDatesByType(
string $type,
- \DateTimeImmutable $dateTime,
- \DateTimeInterface $expectedDateTime
+ DateTimeImmutable $dateTime,
+ DateTimeInterface $expectedDateTime
): void {
$endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
- DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
+ DailyPageHelper::getEndDateTimeByType('nope', new DateTimeImmutable());
}
/**
*/
public function testGeDescriptionsByType(
string $type,
- \DateTimeImmutable $dateTime,
+ DateTimeImmutable $dateTime,
string $expectedDescription
): void {
$description = DailyPageHelper::getDescriptionByType($type, $dateTime);
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());
}
/**
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.
*/
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')],
];
}
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')],
];
}
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'],
}
/**
- * Data provider for testGetDescriptionsByType() test method.
+ * Data provider for testGetRssLengthsByType() test method.
*/
public function getRssLengthsByType(): array
{
[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'),
+ ],
+ ];
+ }
}
{
new Unknown();
}
+
+function test_register_routes(): array
+{
+ return [
+ [
+ 'method' => 'GET',
+ 'route' => '/test',
+ 'callable' => 'getFunction',
+ ],
+ [
+ 'method' => 'POST',
+ 'route' => '/custom',
+ 'callable' => 'postFunction',
+ ],
+ ];
+}
--- /dev/null
+<?php
+
+function test_route_invalid_register_routes(): array
+{
+ return [
+ [
+ 'method' => 'GET',
+ 'route' => 'not a route',
+ 'callable' => 'getFunction',
+ ],
+ ];
+}
--- /dev/null
+<!DOCTYPE html>
+<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
+<head>
+ {include="includes"}
+</head>
+<body>
+ {include="page.header"}
+
+ {$content}
+
+ {include="page.footer"}
+</body>
+</html>
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"