From ca5e98da4867f720dc863dac55cd1fa2360068e7 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 20 Oct 2020 10:39:58 +0200 Subject: [PATCH 01/50] Composer: explicitly import katzgrau/klogger (already included in netscape-bookmark-parser) --- composer.json | 1 + composer.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c0855e47..64f0025e 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "erusev/parsedown": "^1.6", "erusev/parsedown-extra": "^0.8.1", "gettext/gettext": "^4.4", + "katzgrau/klogger": "^1.2", "malkusch/lock": "^2.1", "pubsubhubbub/publisher": "dev-master", "shaarli/netscape-bookmark-parser": "^2.1", diff --git a/composer.lock b/composer.lock index c379d8e7..3c89036f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "932b191006135ff8be495aa0b4ba7e09", + "content-hash": "61360efbb2e1ba4c4fe00ce1f7a78ec5", "packages": [ { "name": "arthurhoaro/web-thumbnailer", From b38a1b0209f546d4824a0db81a34c4e30fcdebaf Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 20 Oct 2020 11:47:07 +0200 Subject: [PATCH 02/50] Use PSR-3 logger for login attempts Fixes #1122 --- application/Utils.php | 24 ++++--- application/container/ContainerBuilder.php | 10 ++- application/container/ShaarliContainer.php | 2 + .../controller/visitor/LoginController.php | 1 - application/render/PageBuilder.php | 29 +++++--- application/security/BanManager.php | 28 ++++---- application/security/LoginManager.php | 69 ++++++++----------- index.php | 19 ++++- tests/UtilsTest.php | 36 +++------- tests/container/ContainerBuilderTest.php | 5 +- .../visitor/LoginControllerTest.php | 2 +- tests/security/BanManagerTest.php | 3 +- tests/security/LoginManagerTest.php | 51 ++++++++++---- tests/security/SessionManagerTest.php | 5 +- tests/utils/FakeConfigManager.php | 10 ++- 15 files changed, 170 insertions(+), 124 deletions(-) diff --git a/application/Utils.php b/application/Utils.php index bcfda65c..7a9d2645 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -4,21 +4,23 @@ */ /** - * Logs a message to a text file + * Format log using provided data. * - * The log format is compatible with fail2ban. + * @param string $message the message to log + * @param string|null $clientIp the client's remote IPv4/IPv6 address * - * @param string $logFile where to write the logs - * @param string $clientIp the client's remote IPv4/IPv6 address - * @param string $message the message to log + * @return string Formatted message to log */ -function logm($logFile, $clientIp, $message) +function format_log(string $message, string $clientIp = null): string { - file_put_contents( - $logFile, - date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL, - FILE_APPEND - ); + $out = $message; + + if (!empty($clientIp)) { + // Note: we keep the first dash to avoid breaking fail2ban configs + $out = '- ' . $clientIp . ' - ' . $out; + } + + return $out; } /** diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index fd94a1c3..d84418ad 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Container; use malkusch\lock\mutex\FlockMutex; +use Psr\Log\LoggerInterface; use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; @@ -49,6 +50,9 @@ class ContainerBuilder /** @var LoginManager */ protected $login; + /** @var LoggerInterface */ + protected $logger; + /** @var string|null */ protected $basePath = null; @@ -56,12 +60,14 @@ class ContainerBuilder ConfigManager $conf, SessionManager $session, CookieManager $cookieManager, - LoginManager $login + LoginManager $login, + LoggerInterface $logger ) { $this->conf = $conf; $this->session = $session; $this->login = $login; $this->cookieManager = $cookieManager; + $this->logger = $logger; } public function build(): ShaarliContainer @@ -72,6 +78,7 @@ class ContainerBuilder $container['sessionManager'] = $this->session; $container['cookieManager'] = $this->cookieManager; $container['loginManager'] = $this->login; + $container['logger'] = $this->logger; $container['basePath'] = $this->basePath; $container['plugins'] = function (ShaarliContainer $container): PluginManager { @@ -99,6 +106,7 @@ class ContainerBuilder return new PageBuilder( $container->conf, $container->sessionManager->getSession(), + $container->logger, $container->bookmarkService, $container->sessionManager->generateToken(), $container->loginManager->isLoggedIn() diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 3a7c238f..3e5bd252 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Container; +use Psr\Log\LoggerInterface; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; use Shaarli\Feed\FeedBuilder; @@ -36,6 +37,7 @@ use Slim\Container; * @property History $history * @property HttpAccess $httpAccess * @property LoginManager $loginManager + * @property LoggerInterface $logger * @property MetadataRetriever $metadataRetriever * @property NetscapeBookmarkUtils $netscapeBookmarkUtils * @property callable $notFoundHandler Overrides default Slim exception display diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index 121ba40b..f5038fe3 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -65,7 +65,6 @@ class LoginController extends ShaarliVisitorController } if (!$this->container->loginManager->checkCredentials( - $this->container->environment['REMOTE_ADDR'], client_ip_id($this->container->environment), $request->getParam('login'), $request->getParam('password') diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 2d6d2dbe..512bb79e 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -3,7 +3,7 @@ namespace Shaarli\Render; use Exception; -use exceptions\MissingBasePathException; +use Psr\Log\LoggerInterface; use RainTPL; use Shaarli\ApplicationUtils; use Shaarli\Bookmark\BookmarkServiceInterface; @@ -35,6 +35,9 @@ class PageBuilder */ protected $session; + /** @var LoggerInterface */ + protected $logger; + /** * @var BookmarkServiceInterface $bookmarkService instance. */ @@ -54,17 +57,25 @@ class PageBuilder * PageBuilder constructor. * $tpl is initialized at false for lazy loading. * - * @param ConfigManager $conf Configuration Manager instance (reference). - * @param array $session $_SESSION array - * @param BookmarkServiceInterface $linkDB instance. - * @param string $token Session token - * @param bool $isLoggedIn + * @param ConfigManager $conf Configuration Manager instance (reference). + * @param array $session $_SESSION array + * @param LoggerInterface $logger + * @param null $linkDB instance. + * @param null $token Session token + * @param bool $isLoggedIn */ - public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) - { + public function __construct( + ConfigManager &$conf, + array $session, + LoggerInterface $logger, + $linkDB = null, + $token = null, + $isLoggedIn = false + ) { $this->tpl = false; $this->conf = $conf; $this->session = $session; + $this->logger = $logger; $this->bookmarkService = $linkDB; $this->token = $token; $this->isLoggedIn = $isLoggedIn; @@ -98,7 +109,7 @@ class PageBuilder $this->tpl->assign('newVersion', escape($version)); $this->tpl->assign('versionError', ''); } catch (Exception $exc) { - logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); + $this->logger->error(format_log('Error: ' . $exc->getMessage(), client_ip_id($_SERVER))); $this->tpl->assign('newVersion', ''); $this->tpl->assign('versionError', escape($exc->getMessage())); } diff --git a/application/security/BanManager.php b/application/security/BanManager.php index 68190c54..f72c8b7b 100644 --- a/application/security/BanManager.php +++ b/application/security/BanManager.php @@ -3,6 +3,7 @@ namespace Shaarli\Security; +use Psr\Log\LoggerInterface; use Shaarli\FileUtils; /** @@ -28,8 +29,8 @@ class BanManager /** @var string Path to the file containing IP bans and failures */ protected $banFile; - /** @var string Path to the log file, used to log bans */ - protected $logFile; + /** @var LoggerInterface Path to the log file, used to log bans */ + protected $logger; /** @var array List of IP with their associated number of failed attempts */ protected $failures = []; @@ -40,18 +41,19 @@ class BanManager /** * BanManager constructor. * - * @param array $trustedProxies List of allowed proxies IP - * @param int $nbAttempts Number of allowed failed attempt before the ban - * @param int $banDuration Ban duration in seconds - * @param string $banFile Path to the file containing IP bans and failures - * @param string $logFile Path to the log file, used to log bans + * @param array $trustedProxies List of allowed proxies IP + * @param int $nbAttempts Number of allowed failed attempt before the ban + * @param int $banDuration Ban duration in seconds + * @param string $banFile Path to the file containing IP bans and failures + * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory */ - public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) { + public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger) { $this->trustedProxies = $trustedProxies; $this->nbAttempts = $nbAttempts; $this->banDuration = $banDuration; $this->banFile = $banFile; - $this->logFile = $logFile; + $this->logger = $logger; + $this->readBanFile(); } @@ -78,11 +80,7 @@ class BanManager if ($this->failures[$ip] >= $this->nbAttempts) { $this->bans[$ip] = time() + $this->banDuration; - logm( - $this->logFile, - $server['REMOTE_ADDR'], - 'IP address banned from login: '. $ip - ); + $this->logger->info(format_log('IP address banned from login: '. $ip, $ip)); } $this->writeBanFile(); } @@ -138,7 +136,7 @@ class BanManager unset($this->failures[$ip]); } unset($this->bans[$ip]); - logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); + $this->logger->info(format_log('Ban lifted for: '. $ip, $ip)); $this->writeBanFile(); return false; diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 65048f10..426e785e 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -2,6 +2,7 @@ namespace Shaarli\Security; use Exception; +use Psr\Log\LoggerInterface; use Shaarli\Config\ConfigManager; /** @@ -31,26 +32,30 @@ class LoginManager protected $staySignedInToken = ''; /** @var CookieManager */ protected $cookieManager; + /** @var LoggerInterface */ + protected $logger; /** * Constructor * - * @param ConfigManager $configManager Configuration Manager instance + * @param ConfigManager $configManager Configuration Manager instance * @param SessionManager $sessionManager SessionManager instance - * @param CookieManager $cookieManager CookieManager instance + * @param CookieManager $cookieManager CookieManager instance + * @param BanManager $banManager + * @param LoggerInterface $logger Used to log login attempts */ - public function __construct($configManager, $sessionManager, $cookieManager) - { + public function __construct( + ConfigManager $configManager, + SessionManager $sessionManager, + CookieManager $cookieManager, + BanManager $banManager, + LoggerInterface $logger + ) { $this->configManager = $configManager; $this->sessionManager = $sessionManager; $this->cookieManager = $cookieManager; - $this->banManager = new BanManager( - $this->configManager->get('security.trusted_proxies', []), - $this->configManager->get('security.ban_after'), - $this->configManager->get('security.ban_duration'), - $this->configManager->get('resource.ban_file', 'data/ipbans.php'), - $this->configManager->get('resource.log') - ); + $this->banManager = $banManager; + $this->logger = $logger; if ($this->configManager->get('security.open_shaarli') === true) { $this->openShaarli = true; @@ -129,48 +134,34 @@ class LoginManager /** * Check user credentials are valid * - * @param string $remoteIp Remote client IP address * @param string $clientIpId Client IP address identifier * @param string $login Username * @param string $password Password * * @return bool true if the provided credentials are valid, false otherwise */ - public function checkCredentials($remoteIp, $clientIpId, $login, $password) + public function checkCredentials($clientIpId, $login, $password) { - // Check login matches config - if ($login !== $this->configManager->get('credentials.login')) { - return false; - } - // Check credentials try { $useLdapLogin = !empty($this->configManager->get('ldap.host')); - if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) - || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) + if ($login === $this->configManager->get('credentials.login') + && ( + (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) + || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) + ) ) { - $this->sessionManager->storeLoginInfo($clientIpId); - logm( - $this->configManager->get('resource.log'), - $remoteIp, - 'Login successful' - ); - return true; + $this->sessionManager->storeLoginInfo($clientIpId); + $this->logger->info(format_log('Login successful', $clientIpId)); + + return true; } - } - catch(Exception $exception) { - logm( - $this->configManager->get('resource.log'), - $remoteIp, - 'Exception while checking credentials: ' . $exception - ); + } catch(Exception $exception) { + $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId)); } - logm( - $this->configManager->get('resource.log'), - $remoteIp, - 'Login failed for user ' . $login - ); + $this->logger->info(format_log('Login failed for user ' . $login, $clientIpId)); + return false; } diff --git a/index.php b/index.php index 220847f5..ea6e8501 100644 --- a/index.php +++ b/index.php @@ -25,9 +25,12 @@ require_once 'application/Utils.php'; require_once __DIR__ . '/init.php'; +use Katzgrau\KLogger\Logger; +use Psr\Log\LogLevel; use Shaarli\Config\ConfigManager; use Shaarli\Container\ContainerBuilder; use Shaarli\Languages; +use Shaarli\Security\BanManager; use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; @@ -48,10 +51,22 @@ if ($conf->get('dev.debug', false)) { }); } +$logger = new Logger( + dirname($conf->get('resource.log')), + !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, + ['filename' => basename($conf->get('resource.log'))] +); $sessionManager = new SessionManager($_SESSION, $conf, session_save_path()); $sessionManager->initialize(); $cookieManager = new CookieManager($_COOKIE); -$loginManager = new LoginManager($conf, $sessionManager, $cookieManager); +$banManager = new BanManager( + $conf->get('security.trusted_proxies', []), + $conf->get('security.ban_after'), + $conf->get('security.ban_duration'), + $conf->get('resource.ban_file', 'data/ipbans.php'), + $logger +); +$loginManager = new LoginManager($conf, $sessionManager, $cookieManager, $banManager, $logger); $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); // Sniff browser language and set date format accordingly. @@ -71,7 +86,7 @@ date_default_timezone_set($conf->get('general.timezone', 'UTC')); $loginManager->checkLoginState(client_ip_id($_SERVER)); -$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager); +$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger); $container = $containerBuilder->build(); $app = new App($container); diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 6e787d7f..59dca75f 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -63,41 +63,25 @@ class UtilsTest extends \Shaarli\TestCase } /** - * Log a message to a file - IPv4 client address + * Format a log a message - IPv4 client address */ - public function testLogmIp4() + public function testFormatLogIp4() { - $logMessage = 'IPv4 client connected'; - logm(self::$testLogFile, '127.0.0.1', $logMessage); - list($date, $ip, $message) = $this->getLastLogEntry(); + $message = 'IPv4 client connected'; + $log = format_log($message, '127.0.0.1'); - $this->assertInstanceOf( - 'DateTime', - DateTime::createFromFormat(self::$dateFormat, $date) - ); - $this->assertTrue( - filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false - ); - $this->assertEquals($logMessage, $message); + static::assertSame('- 127.0.0.1 - IPv4 client connected', $log); } /** - * Log a message to a file - IPv6 client address + * Format a log a message - IPv6 client address */ - public function testLogmIp6() + public function testFormatLogIp6() { - $logMessage = 'IPv6 client connected'; - logm(self::$testLogFile, '2001:db8::ff00:42:8329', $logMessage); - list($date, $ip, $message) = $this->getLastLogEntry(); + $message = 'IPv6 client connected'; + $log = format_log($message, '2001:db8::ff00:42:8329'); - $this->assertInstanceOf( - 'DateTime', - DateTime::createFromFormat(self::$dateFormat, $date) - ); - $this->assertTrue( - filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false - ); - $this->assertEquals($logMessage, $message); + static::assertSame('- 2001:db8::ff00:42:8329 - IPv6 client connected', $log); } /** diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php index 3dadc0b9..3d43c344 100644 --- a/tests/container/ContainerBuilderTest.php +++ b/tests/container/ContainerBuilderTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Container; +use Psr\Log\LoggerInterface; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; use Shaarli\Feed\FeedBuilder; @@ -55,7 +56,8 @@ class ContainerBuilderTest extends TestCase $this->conf, $this->sessionManager, $this->cookieManager, - $this->loginManager + $this->loginManager, + $this->createMock(LoggerInterface::class) ); } @@ -73,6 +75,7 @@ class ContainerBuilderTest extends TestCase static::assertInstanceOf(History::class, $container->history); static::assertInstanceOf(HttpAccess::class, $container->httpAccess); static::assertInstanceOf(LoginManager::class, $container->loginManager); + static::assertInstanceOf(LoggerInterface::class, $container->logger); static::assertInstanceOf(MetadataRetriever::class, $container->metadataRetriever); static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils); static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); diff --git a/tests/front/controller/visitor/LoginControllerTest.php b/tests/front/controller/visitor/LoginControllerTest.php index 1312ccb7..00d9eab3 100644 --- a/tests/front/controller/visitor/LoginControllerTest.php +++ b/tests/front/controller/visitor/LoginControllerTest.php @@ -195,7 +195,7 @@ class LoginControllerTest extends TestCase $this->container->loginManager ->expects(static::once()) ->method('checkCredentials') - ->with('1.2.3.4', '1.2.3.4', 'bob', 'pass') + ->with('1.2.3.4', 'bob', 'pass') ->willReturn(true) ; $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8))); diff --git a/tests/security/BanManagerTest.php b/tests/security/BanManagerTest.php index 698d3d10..22aa8666 100644 --- a/tests/security/BanManagerTest.php +++ b/tests/security/BanManagerTest.php @@ -3,6 +3,7 @@ namespace Shaarli\Security; +use Psr\Log\LoggerInterface; use Shaarli\FileUtils; use Shaarli\TestCase; @@ -387,7 +388,7 @@ class BanManagerTest extends TestCase 3, 1800, $this->banFile, - $this->logFile + $this->createMock(LoggerInterface::class) ); } } diff --git a/tests/security/LoginManagerTest.php b/tests/security/LoginManagerTest.php index d302983d..f7609fc6 100644 --- a/tests/security/LoginManagerTest.php +++ b/tests/security/LoginManagerTest.php @@ -2,6 +2,8 @@ namespace Shaarli\Security; +use Psr\Log\LoggerInterface; +use Shaarli\FakeConfigManager; use Shaarli\TestCase; /** @@ -9,7 +11,7 @@ use Shaarli\TestCase; */ class LoginManagerTest extends TestCase { - /** @var \FakeConfigManager Configuration Manager instance */ + /** @var FakeConfigManager Configuration Manager instance */ protected $configManager = null; /** @var LoginManager Login Manager instance */ @@ -60,6 +62,9 @@ class LoginManagerTest extends TestCase /** @var CookieManager */ protected $cookieManager; + /** @var BanManager */ + protected $banManager; + /** * Prepare or reset test resources */ @@ -71,7 +76,7 @@ class LoginManagerTest extends TestCase $this->passwordHash = sha1($this->password . $this->login . $this->salt); - $this->configManager = new \FakeConfigManager([ + $this->configManager = new FakeConfigManager([ 'credentials.login' => $this->login, 'credentials.hash' => $this->passwordHash, 'credentials.salt' => $this->salt, @@ -91,18 +96,29 @@ class LoginManagerTest extends TestCase return $this->cookie[$key] ?? null; }); $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path'); - $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager); + $this->banManager = $this->createMock(BanManager::class); + $this->loginManager = new LoginManager( + $this->configManager, + $this->sessionManager, + $this->cookieManager, + $this->banManager, + $this->createMock(LoggerInterface::class) + ); $this->server['REMOTE_ADDR'] = $this->ipAddr; } /** * Record a failed login attempt */ - public function testHandleFailedLogin() + public function testHandleFailedLogin(): void { + $this->banManager->expects(static::exactly(2))->method('handleFailedAttempt'); + $this->banManager->method('isBanned')->willReturn(true); + $this->loginManager->handleFailedLogin($this->server); $this->loginManager->handleFailedLogin($this->server); - $this->assertFalse($this->loginManager->canLogin($this->server)); + + static::assertFalse($this->loginManager->canLogin($this->server)); } /** @@ -114,8 +130,13 @@ class LoginManagerTest extends TestCase 'REMOTE_ADDR' => $this->trustedProxy, 'HTTP_X_FORWARDED_FOR' => $this->ipAddr, ]; + + $this->banManager->expects(static::exactly(2))->method('handleFailedAttempt'); + $this->banManager->method('isBanned')->willReturn(true); + $this->loginManager->handleFailedLogin($server); $this->loginManager->handleFailedLogin($server); + $this->assertFalse($this->loginManager->canLogin($server)); } @@ -196,10 +217,16 @@ class LoginManagerTest extends TestCase */ public function testCheckLoginStateNotConfigured() { - $configManager = new \FakeConfigManager([ + $configManager = new FakeConfigManager([ 'resource.ban_file' => $this->banFile, ]); - $loginManager = new LoginManager($configManager, null, $this->cookieManager); + $loginManager = new LoginManager( + $configManager, + $this->sessionManager, + $this->cookieManager, + $this->banManager, + $this->createMock(LoggerInterface::class) + ); $loginManager->checkLoginState(''); $this->assertFalse($loginManager->isLoggedIn()); @@ -270,7 +297,7 @@ class LoginManagerTest extends TestCase public function testCheckCredentialsWrongLogin() { $this->assertFalse( - $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password) + $this->loginManager->checkCredentials('', 'b4dl0g1n', $this->password) ); } @@ -280,7 +307,7 @@ class LoginManagerTest extends TestCase public function testCheckCredentialsWrongPassword() { $this->assertFalse( - $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd') + $this->loginManager->checkCredentials('', $this->login, 'b4dp455wd') ); } @@ -290,7 +317,7 @@ class LoginManagerTest extends TestCase public function testCheckCredentialsWrongLoginAndPassword() { $this->assertFalse( - $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd') + $this->loginManager->checkCredentials('', 'b4dl0g1n', 'b4dp455wd') ); } @@ -300,7 +327,7 @@ class LoginManagerTest extends TestCase public function testCheckCredentialsGoodLoginAndPassword() { $this->assertTrue( - $this->loginManager->checkCredentials('', '', $this->login, $this->password) + $this->loginManager->checkCredentials('', $this->login, $this->password) ); } @@ -311,7 +338,7 @@ class LoginManagerTest extends TestCase { $this->configManager->set('ldap.host', 'dummy'); $this->assertFalse( - $this->loginManager->checkCredentials('', '', $this->login, $this->password) + $this->loginManager->checkCredentials('', $this->login, $this->password) ); } diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php index 3f9c3ef5..6830d714 100644 --- a/tests/security/SessionManagerTest.php +++ b/tests/security/SessionManagerTest.php @@ -2,6 +2,7 @@ namespace Shaarli\Security; +use Shaarli\FakeConfigManager; use Shaarli\TestCase; /** @@ -12,7 +13,7 @@ class SessionManagerTest extends TestCase /** @var array Session ID hashes */ protected static $sidHashes = null; - /** @var \FakeConfigManager ConfigManager substitute for testing */ + /** @var FakeConfigManager ConfigManager substitute for testing */ protected $conf = null; /** @var array $_SESSION array for testing */ @@ -34,7 +35,7 @@ class SessionManagerTest extends TestCase */ protected function setUp(): void { - $this->conf = new \FakeConfigManager([ + $this->conf = new FakeConfigManager([ 'credentials.login' => 'johndoe', 'credentials.salt' => 'salt', 'security.session_protection_disabled' => false, diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php index 360b34a9..014c2af0 100644 --- a/tests/utils/FakeConfigManager.php +++ b/tests/utils/FakeConfigManager.php @@ -1,9 +1,13 @@ values[$key] = $value; } @@ -35,7 +39,7 @@ class FakeConfigManager * * @return mixed The value if set, else the name of the key */ - public function get($key) + public function get($key, $default = '') { if (isset($this->values[$key])) { return $this->values[$key]; From 0cf76ccb4736473a958d9fd36ed914e2d25d594a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 21 Oct 2020 13:12:15 +0200 Subject: [PATCH 03/50] Feature: add a Server administration page It contains mostly read only information about the current Shaarli instance, PHP version, extensions, file and folder permissions, etc. Also action buttons to clear the cache or sync thumbnails. Part of the content of this page is also displayed on the install page, to check server requirement before installing Shaarli config file. Fixes #40 Fixes #185 --- application/ApplicationUtils.php | 93 +++++- application/FileUtils.php | 56 ++++ .../controller/admin/ServerController.php | 87 ++++++ .../visitor/BookmarkListController.php | 26 +- .../controller/visitor/InstallController.php | 12 +- assets/default/scss/shaarli.scss | 56 +++- inc/languages/fr/LC_MESSAGES/shaarli.po | 294 +++++++++++++----- index.php | 2 + tests/ApplicationUtilsTest.php | 62 ++++ tests/FileUtilsTest.php | 88 +++++- .../controller/admin/ServerControllerTest.php | 184 +++++++++++ .../visitor/InstallControllerTest.php | 9 + tpl/default/install.html | 10 + tpl/default/server.html | 129 ++++++++ tpl/default/server.requirements.html | 68 ++++ tpl/default/tools.html | 14 +- 16 files changed, 1086 insertions(+), 104 deletions(-) create mode 100644 application/front/controller/admin/ServerController.php create mode 100644 tests/front/controller/admin/ServerControllerTest.php create mode 100644 tpl/default/server.html create mode 100644 tpl/default/server.requirements.html diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 3aa21829..bd1c7cf3 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php @@ -14,8 +14,9 @@ class ApplicationUtils */ public static $VERSION_FILE = 'shaarli_version.php'; - private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; - private static $GIT_BRANCHES = array('latest', 'stable'); + public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli'; + public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; + public static $GIT_BRANCHES = array('latest', 'stable'); private static $VERSION_START_TAG = ''; @@ -125,7 +126,7 @@ class ApplicationUtils // Late Static Binding allows overriding within tests // See http://php.net/manual/en/language.oop5.late-static-bindings.php $latestVersion = static::getVersion( - self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE + self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE ); if (!$latestVersion) { @@ -171,35 +172,45 @@ class ApplicationUtils /** * Checks Shaarli has the proper access permissions to its resources * - * @param ConfigManager $conf Configuration Manager instance. + * @param ConfigManager $conf Configuration Manager instance. + * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template. + * Currently we only need to be able to read the theme and write in raintpl cache. * * @return array A list of the detected configuration issues */ - public static function checkResourcePermissions($conf) + public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array { - $errors = array(); + $errors = []; $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); // Check script and template directories are readable - foreach (array( + foreach ([ 'application', 'inc', 'plugins', $rainTplDir, $rainTplDir . '/' . $conf->get('resource.theme'), - ) as $path) { + ] as $path) { if (!is_readable(realpath($path))) { $errors[] = '"' . $path . '" ' . t('directory is not readable'); } } // Check cache and data directories are readable and writable - foreach (array( - $conf->get('resource.thumbnails_cache'), - $conf->get('resource.data_dir'), - $conf->get('resource.page_cache'), - $conf->get('resource.raintpl_tmp'), - ) as $path) { + if ($minimalMode) { + $folders = [ + $conf->get('resource.raintpl_tmp'), + ]; + } else { + $folders = [ + $conf->get('resource.thumbnails_cache'), + $conf->get('resource.data_dir'), + $conf->get('resource.page_cache'), + $conf->get('resource.raintpl_tmp'), + ]; + } + + foreach ($folders as $path) { if (!is_readable(realpath($path))) { $errors[] = '"' . $path . '" ' . t('directory is not readable'); } @@ -208,6 +219,10 @@ class ApplicationUtils } } + if ($minimalMode) { + return $errors; + } + // Check configuration files are readable and writable foreach (array( $conf->getConfigFileExt(), @@ -246,4 +261,54 @@ class ApplicationUtils { return hash_hmac('sha256', $currentVersion, $salt); } + + /** + * Get a list of PHP extensions used by Shaarli. + * + * @return array[] List of extension with following keys: + * - name: extension name + * - required: whether the extension is required to use Shaarli + * - desc: short description of extension usage in Shaarli + * - loaded: whether the extension is properly loaded or not + */ + public static function getPhpExtensionsRequirement(): array + { + $extensions = [ + ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')], + ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')], + ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')], + ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')], + ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')], + ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')], + ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')], + ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')], + ]; + + foreach ($extensions as &$extension) { + $extension['loaded'] = extension_loaded($extension['name']); + } + + return $extensions; + } + + /** + * Return the EOL date of given PHP version. If the version is unknown, + * we return today + 2 years. + * + * @param string $fullVersion PHP version, e.g. 7.4.7 + * + * @return string Date format: YYYY-MM-DD + */ + public static function getPhpEol(string $fullVersion): string + { + preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches); + + return [ + '7.1' => '2019-12-01', + '7.2' => '2020-11-30', + '7.3' => '2021-12-06', + '7.4' => '2022-11-28', + '8.0' => '2023-12-01', + ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d'); + } } diff --git a/application/FileUtils.php b/application/FileUtils.php index 30560bfc..3f940751 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php @@ -81,4 +81,60 @@ class FileUtils ) ); } + + /** + * Recursively deletes a folder content, and deletes itself optionally. + * If an excluded file is found, folders won't be deleted. + * + * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory. + * + * @param string $path + * @param bool $selfDelete Delete the provided folder if true, only its content if false. + * @param array $exclude + */ + public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool + { + $skipped = false; + + if (!is_dir($path)) { + throw new IOException(t('Provided path is not a directory.')); + } + + if (!static::isPathInShaarliFolder($path)) { + throw new IOException(t('Trying to delete a folder outside of Shaarli path.')); + } + + foreach (new \DirectoryIterator($path) as $file) { + if($file->isDot()) { + continue; + } + + if (in_array($file->getBasename(), $exclude, true)) { + $skipped = true; + continue; + } + + if ($file->isFile()) { + unlink($file->getPathname()); + } elseif($file->isDir()) { + $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped; + } + } + + if ($selfDelete && !$skipped) { + rmdir($path); + } + + return $skipped; + } + + /** + * Checks that the given path is inside Shaarli directory. + */ + public static function isPathInShaarliFolder(string $path): bool + { + $rootDirectory = dirname(dirname(__FILE__)); + + return strpos(realpath($path), $rootDirectory) !== false; + } } diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php new file mode 100644 index 00000000..85654a43 --- /dev/null +++ b/application/front/controller/admin/ServerController.php @@ -0,0 +1,87 @@ +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('release_url', ApplicationUtils::$GITHUB_URL . '/releases/tag/' . $latestVersion); + $this->assignView('latest_version', $latestVersion); + $this->assignView('current_version', $currentVersion); + $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode')); + $this->assignView('index_url', index_url($this->container->environment)); + $this->assignView('client_ip', client_ip_id($this->container->environment)); + $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', [])); + + $this->assignView( + 'pagetitle', + t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('server')); + } + + /** + * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails). + */ + public function clearCache(Request $request, Response $response): Response + { + $exclude = ['.htaccess']; + + if ($request->getQueryParam('type') === static::CACHE_THUMB) { + $folders = [$this->container->conf->get('resource.thumbnails_cache')]; + + $this->saveWarningMessage( + t('Thumbnails cache has been cleared.') . ' ' . + '' . t('Please synchronize them.') .'' + ); + } else { + $folders = [ + $this->container->conf->get('resource.page_cache'), + $this->container->conf->get('resource.raintpl_tmp'), + ]; + + $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!')); + } + + // Make sure that we don't delete root cache folder + $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders)))); + foreach ($folders as $folder) { + FileUtils::clearFolder($folder, false, $exclude); + } + + return $this->redirect($response, '/admin/server'); + } +} diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index a8019ead..5267c8f5 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -169,16 +169,24 @@ class BookmarkListController extends ShaarliVisitorController */ protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool { - // Logged in, not async retrieval, thumbnails enabled, and thumbnail should be updated - if ($this->container->loginManager->isLoggedIn() - && true !== $this->container->conf->get('general.enable_async_metadata', true) - && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE - && $bookmark->shouldUpdateThumbnail() - ) { - $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); - $this->container->bookmarkService->set($bookmark, $writeDatastore); + if (false === $this->container->loginManager->isLoggedIn()) { + return false; + } - return true; + // If thumbnail should be updated, we reset it to null + if ($bookmark->shouldUpdateThumbnail()) { + $bookmark->setThumbnail(null); + + // Requires an update, not async retrieval, thumbnails enabled + if ($bookmark->shouldUpdateThumbnail() + && true !== $this->container->conf->get('general.enable_async_metadata', true) + && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + ) { + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + $this->container->bookmarkService->set($bookmark, $writeDatastore); + + return true; + } } return false; diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 7cb32777..564a5777 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -53,6 +53,16 @@ class InstallController extends ShaarliVisitorController $this->assignView('cities', $cities); $this->assignView('languages', Languages::getAvailableLanguages()); + $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); + + $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('pagetitle', t('Install Shaarli')); + return $response->write($this->render('install')); } @@ -150,7 +160,7 @@ class InstallController extends ShaarliVisitorController protected function checkPermissions(): bool { // Ensure Shaarli has proper access to its resources - $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); + $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true); if (empty($errors)) { return true; } diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index 286ac83b..7dc61903 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss @@ -1047,7 +1047,7 @@ body, } table { - margin: auto; + margin: 10px auto 25px auto; width: 90%; .order { @@ -1696,6 +1696,60 @@ form { } } +// SERVER PAGE + +.server-tables-page, +.server-tables { + .window-subtitle { + &::before { + display: block; + margin: 8px auto; + background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color)); + width: 50%; + height: 1px; + content: ''; + } + } + + .server-row { + p { + height: 25px; + padding: 0 10px; + } + } + + .server-label { + text-align: right; + font-weight: bold; + } + + i { + &.fa-color-green { + color: $main-green; + } + + &.fa-color-orange { + color: $orange; + } + + &.fa-color-red { + color: $red; + } + } + + @media screen and (max-width: 64em) { + .server-label { + text-align: center; + } + + .server-row { + p { + text-align: center; + } + } + } +} + // Print rules @media print { .shaarli-menu { diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index f7baedfb..db6bfa3e 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-10-16 20:01+0200\n" -"PO-Revision-Date: 2020-10-16 20:02+0200\n" +"POT-Creation-Date: 2020-10-21 15:00+0200\n" +"PO-Revision-Date: 2020-10-21 15:06+0200\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -20,7 +20,7 @@ msgstr "" "X-Poedit-SearchPath-3: init.php\n" "X-Poedit-SearchPath-4: plugins\n" -#: application/ApplicationUtils.php:161 +#: application/ApplicationUtils.php:162 #, php-format msgid "" "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " @@ -31,22 +31,62 @@ 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/ApplicationUtils.php:192 application/ApplicationUtils.php:204 +#: application/ApplicationUtils.php:195 application/ApplicationUtils.php:215 msgid "directory is not readable" msgstr "le répertoire n'est pas accessible en lecture" -#: application/ApplicationUtils.php:207 +#: application/ApplicationUtils.php:218 msgid "directory is not writable" msgstr "le répertoire n'est pas accessible en écriture" -#: application/ApplicationUtils.php:225 +#: application/ApplicationUtils.php:240 msgid "file is not readable" msgstr "le fichier n'est pas accessible en lecture" -#: application/ApplicationUtils.php:228 +#: application/ApplicationUtils.php:243 msgid "file is not writable" msgstr "le fichier n'est pas accessible en écriture" +#: application/ApplicationUtils.php:277 +msgid "Configuration parsing" +msgstr "Chargement de la configuration" + +#: application/ApplicationUtils.php:278 +msgid "Slim Framework (routing, etc.)" +msgstr "Slim Framwork (routage, etc.)" + +#: application/ApplicationUtils.php:279 +msgid "Multibyte (Unicode) string support" +msgstr "Support des chaînes de caractère multibytes (Unicode)" + +#: application/ApplicationUtils.php:280 +msgid "Required to use thumbnails" +msgstr "Obligatoire pour utiliser les miniatures" + +#: application/ApplicationUtils.php:281 +msgid "Localized text sorting (e.g. e->è->f)" +msgstr "Tri des textes traduits (ex : e->è->f)" + +#: application/ApplicationUtils.php:282 +msgid "Better retrieval of bookmark metadata and thumbnail" +msgstr "Meilleure récupération des meta-données des marque-pages et minatures" + +#: application/ApplicationUtils.php:283 +msgid "Use the translation system in gettext mode" +msgstr "Utiliser le système de traduction en mode gettext" + +#: application/ApplicationUtils.php:284 +msgid "Login using LDAP server" +msgstr "Authentification via un serveur LDAP" + +#: application/FileUtils.php:100 +msgid "Provided path is not a directory." +msgstr "Le chemin fourni n'est pas un dossier." + +#: application/FileUtils.php:104 +msgid "Trying to delete a folder outside of Shaarli path." +msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli." + #: application/History.php:179 msgid "History file isn't readable or writable" msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" @@ -330,12 +370,13 @@ 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:68 #: application/legacy/LegacyUpdater.php:538 msgid "Please synchronize them." msgstr "Merci de les synchroniser." #: application/front/controller/admin/ConfigureController.php:113 -#: application/front/controller/visitor/InstallController.php:136 +#: application/front/controller/visitor/InstallController.php:146 msgid "Error while writing config file after configuration update." msgstr "" "Une erreur s'est produite lors de la sauvegarde du fichier de configuration." @@ -377,33 +418,33 @@ msgstr "" msgid "Shaare a new link" msgstr "Partager un nouveau lien" -#: application/front/controller/admin/ManageShaareController.php:78 +#: application/front/controller/admin/ManageShaareController.php:64 msgid "Note: " msgstr "Note : " -#: application/front/controller/admin/ManageShaareController.php:109 -#: application/front/controller/admin/ManageShaareController.php:206 -#: application/front/controller/admin/ManageShaareController.php:275 -#: application/front/controller/admin/ManageShaareController.php:315 +#: application/front/controller/admin/ManageShaareController.php:95 +#: application/front/controller/admin/ManageShaareController.php:193 +#: application/front/controller/admin/ManageShaareController.php:262 +#: application/front/controller/admin/ManageShaareController.php:302 #, php-format msgid "Bookmark with identifier %s could not be found." msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." -#: application/front/controller/admin/ManageShaareController.php:194 -#: application/front/controller/admin/ManageShaareController.php:252 +#: application/front/controller/admin/ManageShaareController.php:181 +#: application/front/controller/admin/ManageShaareController.php:239 msgid "Invalid bookmark ID provided." msgstr "ID du lien non valide." -#: application/front/controller/admin/ManageShaareController.php:260 +#: application/front/controller/admin/ManageShaareController.php:247 msgid "Invalid visibility provided." msgstr "Visibilité du lien non valide." -#: application/front/controller/admin/ManageShaareController.php:363 +#: application/front/controller/admin/ManageShaareController.php:352 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 msgid "Edit" msgstr "Modifier" -#: application/front/controller/admin/ManageShaareController.php:366 +#: application/front/controller/admin/ManageShaareController.php:355 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 msgid "Shaare" @@ -411,7 +452,7 @@ msgstr "Shaare" #: application/front/controller/admin/ManageTagController.php:29 #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 msgid "Manage tags" msgstr "Gérer les tags" @@ -435,7 +476,7 @@ msgstr[1] "Le tag a été renommé dans %d liens." #: application/front/controller/admin/PasswordController.php:28 #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 msgid "Change password" msgstr "Modifier le mot de passe" @@ -467,6 +508,20 @@ msgstr "" "Une erreur s'est produite lors de la sauvegarde de la configuration des " "plugins : " +#: application/front/controller/admin/ServerController.php:50 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Server administration" +msgstr "Administration serveur" + +#: application/front/controller/admin/ServerController.php:67 +msgid "Thumbnails cache has been cleared." +msgstr "Le cache des miniatures a été vidé." + +#: application/front/controller/admin/ServerController.php:76 +msgid "Shaarli's cache folder has been cleared!" +msgstr "Le dossier de cache de Shaarli a été vidé !" + #: application/front/controller/admin/ThumbnailsController.php:37 #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 msgid "Thumbnails update" @@ -502,9 +557,14 @@ msgstr "Une erreur inattendue s'est produite." #: application/front/controller/visitor/ErrorNotFoundController.php:25 msgid "Requested page could not be found." -msgstr "" +msgstr "La page demandée n'a pas pu être trouvée." -#: application/front/controller/visitor/InstallController.php:73 +#: application/front/controller/visitor/InstallController.php:64 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Install Shaarli" +msgstr "Installation de Shaarli" + +#: application/front/controller/visitor/InstallController.php:83 #, php-format msgid "" "
Sessions do not seem to work correctly on your server.
Make sure the " @@ -523,14 +583,14 @@ msgstr "" "des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " "adresse IP ou un Fully Qualified Domain Name.
" -#: application/front/controller/visitor/InstallController.php:144 +#: application/front/controller/visitor/InstallController.php:154 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:158 +#: application/front/controller/visitor/InstallController.php:168 msgid "Insufficient permissions:" msgstr "Permissions insuffisantes :" @@ -1016,25 +1076,28 @@ msgstr "" "miniatures." #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 msgid "Synchronize thumbnails" msgstr "Synchroniser les miniatures" #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 msgid "All" msgstr "Tous" #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 msgid "Only common media hosts" msgstr "Seulement les hébergeurs de média connus" #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 msgid "None" msgstr "Aucune" #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 msgid "Save" @@ -1060,27 +1123,27 @@ msgstr "Tous les liens d'un jour sur une page." msgid "Next day" msgstr "Jour suivant" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 msgid "Edit Shaare" msgstr "Modifier le Shaare" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 msgid "New Shaare" msgstr "Nouveau Shaare" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 msgid "Created:" msgstr "Création :" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 msgid "URL" msgstr "URL" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 msgid "Title" msgstr "Titre" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 @@ -1088,33 +1151,33 @@ msgstr "Titre" msgid "Description" msgstr "Description" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 msgid "Tags" msgstr "Tags" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 msgid "Private" msgstr "Privé" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 msgid "Description will be rendered with" msgstr "La description sera générée avec" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 msgid "Markdown syntax documentation" msgstr "Documentation sur la syntaxe Markdown" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 msgid "Markdown syntax" msgstr "la syntaxe Markdown" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 msgid "Apply Changes" msgstr "Appliquer les changements" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 @@ -1179,10 +1242,6 @@ msgstr "Les doublons s'appuient sur les URL" msgid "Add default tags" msgstr "Ajouter des tags par défaut" -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 -msgid "Install Shaarli" -msgstr "Installation de Shaarli" - #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 msgid "It looks like it's the first time you run Shaarli. Please configure it." msgstr "" @@ -1215,6 +1274,10 @@ msgstr "Mes liens" msgid "Install" msgstr "Installer" +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190 +msgid "Server requirements" +msgstr "Pré-requis serveur" + #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 msgid "shaare" @@ -1511,6 +1574,100 @@ msgstr "Configuration des extensions" msgid "No parameter available." msgstr "Aucun paramètre disponible." +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "General" +msgstr "Général" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +msgid "Index URL" +msgstr "URL de l'index" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Base path" +msgstr "Chemin de base" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Client IP" +msgstr "IP du client" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "Trusted reverse proxies" +msgstr "Reverse proxies de confiance" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "N/A" +msgstr "N/A" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 +msgid "Visit releases page on Github" +msgstr "Visiter la page des releases sur Github" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +msgid "Synchronize all link thumbnails" +msgstr "Synchroniser toutes les miniatures" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2 +msgid "Permissions" +msgstr "Permissions" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8 +msgid "There are permissions that need to be fixed." +msgstr "Il y a des permissions qui doivent être corrigées." + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23 +msgid "All read/write permissions are properly set." +msgstr "Toutes les permissions de lecture/écriture sont définies correctement." + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32 +msgid "Running PHP" +msgstr "Fonctionnant avec PHP" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36 +msgid "End of life: " +msgstr "Fin de vie : " + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "Extension" +msgstr "Extension" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49 +msgid "Usage" +msgstr "Utilisation" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50 +msgid "Status" +msgstr "Statut" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66 +msgid "Loaded" +msgstr "Chargé" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 +msgid "Required" +msgstr "Obligatoire" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 +msgid "Optional" +msgstr "Optionnel" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70 +msgid "Not loaded" +msgstr "Non chargé" + #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 msgid "tags" @@ -1561,15 +1718,19 @@ msgstr "Configurer Shaarli" msgid "Enable, disable and configure plugins" msgstr "Activer, désactiver et configurer les extensions" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 +msgid "Check instance's server configuration" +msgstr "Vérifier la configuration serveur de l'instance" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 msgid "Change your password" msgstr "Modifier le mot de passe" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 msgid "Rename or delete a tag in all links" msgstr "Renommer ou supprimer un tag dans tous les liens" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 msgid "" "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " "delicious...)" @@ -1577,11 +1738,11 @@ msgstr "" "Importer des marques pages au format Netscape HTML (comme exportés depuis " "Firefox, Chrome, Opera, delicious...)" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 msgid "Import links" msgstr "Importer des liens" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 msgid "" "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " "Opera, delicious...)" @@ -1589,15 +1750,11 @@ msgstr "" "Exporter les marques pages au format Netscape HTML (comme exportés depuis " "Firefox, Chrome, Opera, delicious...)" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54 msgid "Export database" msgstr "Exporter les données" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55 -msgid "Synchronize all link thumbnails" -msgstr "Synchroniser toutes les miniatures" - -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 msgid "" "Drag one of these button to your bookmarks toolbar or right-click it and " "\"Bookmark This Link\"" @@ -1605,13 +1762,13 @@ msgstr "" "Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " "dessus et « Ajouter aux favoris »" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 msgid "then click on the bookmarklet in any page you want to share." msgstr "" "puis cliquer sur le marque-page depuis un site que vous souhaitez partager." -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 msgid "" "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " "Link" @@ -1619,40 +1776,40 @@ msgstr "" "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " "Ajouter aux favoris »" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 msgid "then click ✚Shaare link button in any page you want to share" msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 msgid "The selected text is too long, it will be truncated." msgstr "Le texte sélectionné est trop long, il sera tronqué." -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 msgid "Shaare link" msgstr "Shaare" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 msgid "" "Then click ✚Add Note button anytime to start composing a private Note (text " "post) to your Shaarli" msgstr "" "Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 msgid "Add Note" msgstr "Ajouter une Note" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132 msgid "3rd party" msgstr "Applications tierces" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140 msgid "plugin" msgstr "extension" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 msgid "" "Drag this link to your bookmarks toolbar, or right-click it and choose " "Bookmark This Link" @@ -1660,9 +1817,6 @@ msgstr "" "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " "Ajouter aux favoris »" -#~ msgid "Provided data is invalid" -#~ msgstr "Les informations fournies ne sont pas valides" - #~ msgid "Rename" #~ msgstr "Renommer" diff --git a/index.php b/index.php index 220847f5..d0c5ac60 100644 --- a/index.php +++ b/index.php @@ -128,6 +128,8 @@ $app->group('/admin', function () { $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index'); $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save'); $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken'); + $this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index'); + $this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache'); $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index'); $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle'); $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); diff --git a/tests/ApplicationUtilsTest.php b/tests/ApplicationUtilsTest.php index a232b351..ac46cbf1 100644 --- a/tests/ApplicationUtilsTest.php +++ b/tests/ApplicationUtilsTest.php @@ -339,6 +339,35 @@ class ApplicationUtilsTest extends \Shaarli\TestCase ); } + /** + * Checks resource permissions in minimal mode. + */ + public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void + { + $conf = new ConfigManager(''); + $conf->set('resource.thumbnails_cache', 'null/cache'); + $conf->set('resource.config', 'null/data/config.php'); + $conf->set('resource.data_dir', 'null/data'); + $conf->set('resource.datastore', 'null/data/store.php'); + $conf->set('resource.ban_file', 'null/data/ipbans.php'); + $conf->set('resource.log', 'null/data/log.txt'); + $conf->set('resource.page_cache', 'null/pagecache'); + $conf->set('resource.raintpl_tmp', 'null/tmp'); + $conf->set('resource.raintpl_tpl', 'null/tpl'); + $conf->set('resource.raintpl_theme', 'null/tpl/default'); + $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt'); + + static::assertSame( + [ + '"null/tpl" directory is not readable', + '"null/tpl/default" directory is not readable', + '"null/tmp" directory is not readable', + '"null/tmp" directory is not writable' + ], + ApplicationUtils::checkResourcePermissions($conf, true) + ); + } + /** * Check update with 'dev' as curent version (master branch). * It should always return false. @@ -349,4 +378,37 @@ class ApplicationUtilsTest extends \Shaarli\TestCase ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) ); } + + /** + * Basic test of getPhpExtensionsRequirement() + */ + public function testGetPhpExtensionsRequirementSimple(): void + { + static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement()); + static::assertSame([ + 'name' => 'json', + 'required' => true, + 'desc' => 'Configuration parsing', + 'loaded' => true, + ], ApplicationUtils::getPhpExtensionsRequirement()[0]); + } + + /** + * Test getPhpEol with a known version: 7.4 -> 2022 + */ + public function testGetKnownPhpEol(): void + { + static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7')); + } + + /** + * Test getPhpEol with an unknown version: 7.4 -> 2022 + */ + public function testGetUnknownPhpEol(): void + { + static::assertSame( + (((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'), + ApplicationUtils::getPhpEol('7.51.34') + ); + } } diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php index 9163bdf1..3384504a 100644 --- a/tests/FileUtilsTest.php +++ b/tests/FileUtilsTest.php @@ -3,25 +3,48 @@ namespace Shaarli; use Exception; +use Shaarli\Exceptions\IOException; /** * Class FileUtilsTest * * Test file utility class. */ -class FileUtilsTest extends \Shaarli\TestCase +class FileUtilsTest extends TestCase { /** * @var string Test file path. */ protected static $file = 'sandbox/flat.db'; + protected function setUp(): void + { + @mkdir('sandbox'); + mkdir('sandbox/folder2'); + touch('sandbox/file1'); + touch('sandbox/file2'); + mkdir('sandbox/folder1'); + touch('sandbox/folder1/file1'); + touch('sandbox/folder1/file2'); + mkdir('sandbox/folder3'); + mkdir('/tmp/shaarli-to-delete'); + } + /** * Delete test file after every test. */ protected function tearDown(): void { @unlink(self::$file); + + @unlink('sandbox/folder1/file1'); + @unlink('sandbox/folder1/file2'); + @rmdir('sandbox/folder1'); + @unlink('sandbox/file1'); + @unlink('sandbox/file2'); + @rmdir('sandbox/folder2'); + @rmdir('sandbox/folder3'); + @rmdir('/tmp/shaarli-to-delete'); } /** @@ -107,4 +130,67 @@ class FileUtilsTest extends \Shaarli\TestCase $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); } + + /** + * Test clearFolder with self delete and excluded files + */ + public function testClearFolderSelfDeleteWithExclusion(): void + { + FileUtils::clearFolder('sandbox', true, ['file2']); + + static::assertFileExists('sandbox/folder1/file2'); + static::assertFileExists('sandbox/folder1'); + static::assertFileExists('sandbox/file2'); + static::assertFileExists('sandbox'); + + static::assertFileNotExists('sandbox/folder1/file1'); + static::assertFileNotExists('sandbox/file1'); + static::assertFileNotExists('sandbox/folder3'); + } + + /** + * Test clearFolder with self delete and excluded files + */ + public function testClearFolderSelfDeleteWithoutExclusion(): void + { + FileUtils::clearFolder('sandbox', true); + + static::assertFileNotExists('sandbox'); + } + + /** + * Test clearFolder with self delete and excluded files + */ + public function testClearFolderNoSelfDeleteWithoutExclusion(): void + { + FileUtils::clearFolder('sandbox', false); + + static::assertFileExists('sandbox'); + + // 2 because '.' and '..' + static::assertCount(2, new \DirectoryIterator('sandbox')); + } + + /** + * Test clearFolder on a file instead of a folder + */ + public function testClearFolderOnANonDirectory(): void + { + $this->expectException(IOException::class); + $this->expectExceptionMessage('Provided path is not a directory.'); + + FileUtils::clearFolder('sandbox/file1', false); + } + + /** + * Test clearFolder on a file instead of a folder + */ + public function testClearFolderOutsideOfShaarliDirectory(): void + { + $this->expectException(IOException::class); + $this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.'); + + + FileUtils::clearFolder('/tmp/shaarli-to-delete', true); + } } diff --git a/tests/front/controller/admin/ServerControllerTest.php b/tests/front/controller/admin/ServerControllerTest.php new file mode 100644 index 00000000..355cce7d --- /dev/null +++ b/tests/front/controller/admin/ServerControllerTest.php @@ -0,0 +1,184 @@ +createContainer(); + + $this->controller = new ServerController($this->container); + + // initialize dummy cache + @mkdir('sandbox/'); + foreach (['pagecache', 'tmp', 'cache'] as $folder) { + @mkdir('sandbox/' . $folder); + @touch('sandbox/' . $folder . '/.htaccess'); + @touch('sandbox/' . $folder . '/1'); + @touch('sandbox/' . $folder . '/2'); + } + } + + public function tearDown(): void + { + foreach (['pagecache', 'tmp', 'cache'] as $folder) { + @unlink('sandbox/' . $folder . '/.htaccess'); + @unlink('sandbox/' . $folder . '/1'); + @unlink('sandbox/' . $folder . '/2'); + @rmdir('sandbox/' . $folder); + } + } + + /** + * Test default display of server administration page. + */ + public function testIndex(): void + { + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('server', (string) $result->getBody()); + + static::assertSame(PHP_VERSION, $assignedVariables['php_version']); + static::assertArrayHasKey('php_has_reached_eol', $assignedVariables); + static::assertArrayHasKey('php_eol', $assignedVariables); + static::assertArrayHasKey('php_extensions', $assignedVariables); + static::assertArrayHasKey('permissions', $assignedVariables); + static::assertEmpty($assignedVariables['permissions']); + + static::assertRegExp( + '#https://github\.com/shaarli/Shaarli/releases/tag/v\d+\.\d+\.\d+#', + $assignedVariables['release_url'] + ); + static::assertRegExp('#v\d+\.\d+\.\d+#', $assignedVariables['latest_version']); + static::assertRegExp('#(v\d+\.\d+\.\d+|dev)#', $assignedVariables['current_version']); + static::assertArrayHasKey('index_url', $assignedVariables); + static::assertArrayHasKey('client_ip', $assignedVariables); + static::assertArrayHasKey('trusted_proxies', $assignedVariables); + + static::assertSame('Server administration - Shaarli', $assignedVariables['pagetitle']); + } + + /** + * Test clearing the main cache + */ + public function testClearMainCache(): void + { + $this->container->conf = $this->createMock(ConfigManager::class); + $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { + if ($key === 'resource.page_cache') { + return 'sandbox/pagecache'; + } elseif ($key === 'resource.raintpl_tmp') { + return 'sandbox/tmp'; + } elseif ($key === 'resource.thumbnails_cache') { + return 'sandbox/cache'; + } else { + return $default; + } + }); + + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['Shaarli\'s cache folder has been cleared!']) + ; + + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->with('type')->willReturn('main'); + $response = new Response(); + + $result = $this->controller->clearCache($request, $response); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location')); + + static::assertFileNotExists('sandbox/pagecache/1'); + static::assertFileNotExists('sandbox/pagecache/2'); + static::assertFileNotExists('sandbox/tmp/1'); + static::assertFileNotExists('sandbox/tmp/2'); + + static::assertFileExists('sandbox/pagecache/.htaccess'); + static::assertFileExists('sandbox/tmp/.htaccess'); + static::assertFileExists('sandbox/cache'); + static::assertFileExists('sandbox/cache/.htaccess'); + static::assertFileExists('sandbox/cache/1'); + static::assertFileExists('sandbox/cache/2'); + } + + /** + * Test clearing thumbnails cache + */ + public function testClearThumbnailsCache(): void + { + $this->container->conf = $this->createMock(ConfigManager::class); + $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { + if ($key === 'resource.page_cache') { + return 'sandbox/pagecache'; + } elseif ($key === 'resource.raintpl_tmp') { + return 'sandbox/tmp'; + } elseif ($key === 'resource.thumbnails_cache') { + return 'sandbox/cache'; + } else { + return $default; + } + }); + + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->willReturnCallback(function (string $key, array $value): SessionManager { + static::assertSame(SessionManager::KEY_WARNING_MESSAGES, $key); + static::assertCount(1, $value); + static::assertStringStartsWith('Thumbnails cache has been cleared.', $value[0]); + + return $this->container->sessionManager; + }); + ; + + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->with('type')->willReturn('thumbnails'); + $response = new Response(); + + $result = $this->controller->clearCache($request, $response); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location')); + + static::assertFileNotExists('sandbox/cache/1'); + static::assertFileNotExists('sandbox/cache/2'); + + static::assertFileExists('sandbox/cache/.htaccess'); + static::assertFileExists('sandbox/pagecache'); + static::assertFileExists('sandbox/pagecache/.htaccess'); + static::assertFileExists('sandbox/pagecache/1'); + static::assertFileExists('sandbox/pagecache/2'); + static::assertFileExists('sandbox/tmp'); + static::assertFileExists('sandbox/tmp/.htaccess'); + static::assertFileExists('sandbox/tmp/1'); + static::assertFileExists('sandbox/tmp/2'); + } +} diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php index 345ad544..2105ed77 100644 --- a/tests/front/controller/visitor/InstallControllerTest.php +++ b/tests/front/controller/visitor/InstallControllerTest.php @@ -79,6 +79,15 @@ class InstallControllerTest extends TestCase static::assertIsArray($assignedVariables['languages']); static::assertSame('Automatic', $assignedVariables['languages']['auto']); static::assertSame('French', $assignedVariables['languages']['fr']); + + static::assertSame(PHP_VERSION, $assignedVariables['php_version']); + static::assertArrayHasKey('php_has_reached_eol', $assignedVariables); + static::assertArrayHasKey('php_eol', $assignedVariables); + static::assertArrayHasKey('php_extensions', $assignedVariables); + static::assertArrayHasKey('permissions', $assignedVariables); + static::assertEmpty($assignedVariables['permissions']); + + static::assertSame('Install Shaarli', $assignedVariables['pagetitle']); } /** diff --git a/tpl/default/install.html b/tpl/default/install.html index a506a2eb..4f98d49d 100644 --- a/tpl/default/install.html +++ b/tpl/default/install.html @@ -163,6 +163,16 @@ + +
+
+
+

{'Server requirements'|t}

+ + {include="server.requirements"} +
+
+ {include="page.footer"} diff --git a/tpl/default/server.html b/tpl/default/server.html new file mode 100644 index 00000000..de1c8b53 --- /dev/null +++ b/tpl/default/server.html @@ -0,0 +1,129 @@ + + + + {include="includes"} + + +{include="page.header"} + +
+
+
+

{'Server administration'|t}

+ +

{'General'|t}

+ +
+
+

{'Index URL'|t}

+
+ +
+
+
+

{'Base path'|t}

+
+
+

{$base_path}

+
+
+
+
+

{'Client IP'|t}

+
+
+

{$client_ip}

+
+
+
+
+

{'Trusted reverse proxies'|t}

+
+
+ {if="count($trusted_proxies) > 0"} +

+ {loop="$trusted_proxies"} + {$value}
+ {/loop} +

+ {else} +

{'N/A'|t}

+ {/if} +
+
+ + {include="server.requirements"} + +

Version

+ +
+
+

Current version

+
+
+

{$current_version}

+
+
+ +
+
+

Latest release

+
+ +
+ +

Thumbnails

+ +
+
+

Thumbnails status

+
+
+

+ {if="$thumbnails_mode==='all'"} + {'All'|t} + {elseif="$thumbnails_mode==='common'"} + {'Only common media hosts'|t} + {else} + {'None'|t} + {/if} +

+
+
+ + {if="$thumbnails_mode!=='none'"} + + {/if} + +

Cache

+ + + + +
+
+ +{include="page.footer"} + + + diff --git a/tpl/default/server.requirements.html b/tpl/default/server.requirements.html new file mode 100644 index 00000000..85def9b7 --- /dev/null +++ b/tpl/default/server.requirements.html @@ -0,0 +1,68 @@ +
+

{'Permissions'|t}

+ + {if="count($permissions) > 0"} +

+ + {'There are permissions that need to be fixed.'|t} +

+ +

+ {loop="$permissions"} +

{$value}
+ {/loop} +

+ {else} +

+ + {'All read/write permissions are properly set.'|t} +

+ {/if} + +

PHP

+ +

+ {'Running PHP'|t} {$php_version} + {if="$php_has_reached_eol"} +
+ {'End of life: '|t} {$php_eol} + {else} +
+ {/if} +

+ + + + + + + + + + + + {loop="$php_extensions"} + + + + + + + {/loop} + +
{'Extension'|t}{'Usage'|t}{'Status'|t}{'Loaded'|t}
{$value.name}{$value.desc}{$value.required ? t('Required') : t('Optional')} + {if="$value.loaded"} + {$classLoaded="fa-color-green"} + {$strLoaded=t('Loaded')} + {else} + {$strLoaded=t('Not loaded')} + {if="$value.required"} + {$classLoaded="fa-color-red"} + {else} + {$classLoaded="fa-color-orange"} + {/if} + {/if} + + +
+
diff --git a/tpl/default/tools.html b/tpl/default/tools.html index 2cb08e38..2df73598 100644 --- a/tpl/default/tools.html +++ b/tpl/default/tools.html @@ -20,6 +20,12 @@ {'Plugin administration'|t} + {if="!$openshaarli"} - {if="$thumbnails_enabled"} - - {/if} - {loop="$tools_plugin"}
{$value} From 42a72c02fa4b6a5eb9d26a7a3a990e497fc10df3 Mon Sep 17 00:00:00 2001 From: Ganesh Kandu Date: Tue, 27 Oct 2020 17:42:35 +0530 Subject: [PATCH 04/50] Replaced PHP_EOL to "\n" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit i was getting error ``` An error occurred while parsing JSON configuration file (data/config.json.php): error code #4 ➜ Syntax error Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as jsonlint.com. ``` after debug i found ```php $data = str_replace(self::getPhpHeaders(), '', $data); $data = str_replace(self::getPhpSuffix(), '', $data); ``` doesn't removing php header and php suffix cause of this issue was PHP_EOL represents the endline character for the current system. if my ```config.json.php``` was encoded with unix ( LF ) and php running on windows windows encoding ( CR LF ) is not same as unix encoding ( LF ) so ```str_replace``` doesn't replace strin then it causes issue. --- application/config/ConfigJson.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php index c0c0dab9..eaa4ee3f 100644 --- a/application/config/ConfigJson.php +++ b/application/config/ConfigJson.php @@ -73,7 +73,7 @@ class ConfigJson implements ConfigIO */ public static function getPhpHeaders() { - return ''; + return "\n" . '*/ ?>'; } } From e69e3fef7bbdc7299ae01aa0e0258395d2e49818 Mon Sep 17 00:00:00 2001 From: Ganesh Kandu Date: Tue, 27 Oct 2020 18:08:14 +0530 Subject: [PATCH 05/50] Removed PHP_EOL just replace "*/ ?>" and "'; + return '*/ ?>'; } } From 9c04921a8c28c18ef757f2d43ba35e7e2a7f1a4b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 20:17:08 +0200 Subject: [PATCH 06/50] Feature: Share private bookmarks using a URL containing a private key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a share link next to « Permalink » in linklist (using share icon from fork awesome) - This link generates a private key associated to the bookmark - Accessing the bookmark while logged out with the proper key will display it Fixes #475 --- application/bookmark/BookmarkFileService.php | 7 +- .../bookmark/BookmarkServiceInterface.php | 5 +- .../admin/ManageShaareController.php | 26 ++++ .../visitor/BookmarkListController.php | 4 +- inc/languages/fr/LC_MESSAGES/shaarli.po | 40 ++--- index.php | 1 + tests/bookmark/BookmarkFileServiceTest.php | 31 ++++ .../SharePrivateTest.php | 139 ++++++++++++++++++ .../visitor/BookmarkListControllerTest.php | 31 ++++ tpl/default/linklist.html | 7 + 10 files changed, 268 insertions(+), 23 deletions(-) create mode 100644 tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index eb7899bf..14b3d620 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -97,12 +97,15 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function findByHash(string $hash): Bookmark + public function findByHash(string $hash, string $privateKey = null): Bookmark { $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); // PHP 7.3 introduced array_key_first() to avoid this hack $first = reset($bookmark); - if (! $this->isLoggedIn && $first->isPrivate()) { + if (!$this->isLoggedIn + && $first->isPrivate() + && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) + ) { throw new Exception('Not authorized'); } diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 37a54d03..9fa61533 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -20,13 +20,14 @@ interface BookmarkServiceInterface /** * Find a bookmark by hash * - * @param string $hash + * @param string $hash Bookmark's hash + * @param string|null $privateKey Optional key used to access private links while logged out * * @return Bookmark * * @throws \Exception */ - public function findByHash(string $hash): Bookmark; + public function findByHash(string $hash, string $privateKey = null); /** * @param $url diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index 908ebae3..e490f85a 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -320,6 +320,32 @@ class ManageShaareController extends ShaarliAdminController return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); } + /** + * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL. + */ + public function sharePrivate(Request $request, Response $response, array $args): Response + { + $this->checkToken($request); + + $hash = $args['hash'] ?? ''; + $bookmark = $this->container->bookmarkService->findByHash($hash); + + if ($bookmark->isPrivate() !== true) { + return $this->redirect($response, '/shaare/' . $hash); + } + + if (empty($bookmark->getAdditionalContentEntry('private_key'))) { + $privateKey = bin2hex(random_bytes(16)); + $bookmark->addAdditionalContentEntry('private_key', $privateKey); + $this->container->bookmarkService->set($bookmark); + } + + return $this->redirect( + $response, + '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key') + ); + } + /** * Helper function used to display the shaare form whether it's a new or existing bookmark. * diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index 5267c8f5..78c474c9 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -137,8 +137,10 @@ class BookmarkListController extends ShaarliVisitorController */ public function permalink(Request $request, Response $response, array $args): Response { + $privateKey = $request->getParam('key'); + try { - $bookmark = $this->container->bookmarkService->findByHash($args['hash']); + $bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey); } catch (BookmarkNotFoundException $e) { $this->assignView('error_message', $e->getMessage()); diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index db6bfa3e..3f14d22c 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-10-21 15:00+0200\n" -"PO-Revision-Date: 2020-10-21 15:06+0200\n" +"POT-Creation-Date: 2020-10-27 19:32+0100\n" +"PO-Revision-Date: 2020-10-27 19:32+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -123,38 +123,38 @@ 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:383 +#: application/Utils.php:385 msgid "Setting not set" msgstr "Paramètre non défini" -#: application/Utils.php:390 +#: application/Utils.php:392 msgid "Unlimited" msgstr "Illimité" -#: application/Utils.php:393 +#: application/Utils.php:395 msgid "B" msgstr "o" -#: application/Utils.php:393 +#: application/Utils.php:395 msgid "kiB" msgstr "ko" -#: application/Utils.php:393 +#: application/Utils.php:395 msgid "MiB" msgstr "Mo" -#: application/Utils.php:393 +#: application/Utils.php:395 msgid "GiB" msgstr "Go" -#: application/bookmark/BookmarkFileService.php:180 -#: application/bookmark/BookmarkFileService.php:202 -#: application/bookmark/BookmarkFileService.php:224 -#: application/bookmark/BookmarkFileService.php:238 +#: application/bookmark/BookmarkFileService.php:183 +#: application/bookmark/BookmarkFileService.php:205 +#: application/bookmark/BookmarkFileService.php:227 +#: application/bookmark/BookmarkFileService.php:241 msgid "You're not authorized to alter the datastore" msgstr "Vous n'êtes pas autorisé à modifier les données" -#: application/bookmark/BookmarkFileService.php:205 +#: application/bookmark/BookmarkFileService.php:208 msgid "This bookmarks already exists" msgstr "Ce marque-page existe déjà." @@ -439,12 +439,12 @@ msgstr "ID du lien non valide." msgid "Invalid visibility provided." msgstr "Visibilité du lien non valide." -#: application/front/controller/admin/ManageShaareController.php:352 +#: application/front/controller/admin/ManageShaareController.php:378 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 msgid "Edit" msgstr "Modifier" -#: application/front/controller/admin/ManageShaareController.php:355 +#: application/front/controller/admin/ManageShaareController.php:381 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 msgid "Shaare" @@ -551,7 +551,7 @@ msgstr "Hier" msgid "Daily" msgstr "Quotidien" -#: application/front/controller/visitor/ErrorController.php:36 +#: application/front/controller/visitor/ErrorController.php:33 msgid "An unexpected error occurred." msgstr "Une erreur inattendue s'est produite." @@ -604,7 +604,7 @@ msgstr "Permissions insuffisantes :" msgid "Login" msgstr "Connexion" -#: application/front/controller/visitor/LoginController.php:78 +#: application/front/controller/visitor/LoginController.php:77 msgid "Wrong login/password." msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." @@ -738,7 +738,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:65 +#: index.php:80 msgid "Shared bookmarks on " msgstr "Liens partagés sur " @@ -1376,6 +1376,10 @@ msgstr "Changer statut épinglé" msgid "Sticky" msgstr "Épinglé" +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 +msgid "Share a private link" +msgstr "Partager un lien privé" + #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 msgid "Filters" diff --git a/index.php b/index.php index a46e32c9..0ed52bad 100644 --- a/index.php +++ b/index.php @@ -128,6 +128,7 @@ $app->group('/admin', function () { $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare'); $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm'); $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm'); + $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ManageShaareController:sharePrivate'); $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php index daafd250..47970117 100644 --- a/tests/bookmark/BookmarkFileServiceTest.php +++ b/tests/bookmark/BookmarkFileServiceTest.php @@ -897,6 +897,37 @@ class BookmarkFileServiceTest extends TestCase $this->publicLinkDB->findByHash(''); } + /** + * Test filterHash() on a private bookmark while logged out. + */ + public function testFilterHashPrivateWhileLoggedOut() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Not authorized'); + + $hash = smallHash('20141125_084734' . 6); + + $this->publicLinkDB->findByHash($hash); + } + + /** + * Test filterHash() with private key. + */ + public function testFilterHashWithPrivateKey() + { + $hash = smallHash('20141125_084734' . 6); + $privateKey = 'this is usually auto generated'; + + $bookmark = $this->privateLinkDB->findByHash($hash); + $bookmark->addAdditionalContentEntry('private_key', $privateKey); + $this->privateLinkDB->save(); + + $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); + $bookmark = $this->privateLinkDB->findByHash($hash, $privateKey); + + static::assertSame(6, $bookmark->getId()); + } + /** * Test linksCountPerTag all tags without filter. * Equal occurrences should be sorted alphabetically. diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php b/tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php new file mode 100644 index 00000000..1e7877c7 --- /dev/null +++ b/tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php @@ -0,0 +1,139 @@ +createContainer(); + + $this->container->httpAccess = $this->createMock(HttpAccess::class); + $this->controller = new ManageShaareController($this->container); + } + + /** + * Test shaare private with a private bookmark which does not have a key yet. + */ + public function testSharePrivateWithNewPrivateBookmark(): void + { + $hash = 'abcdcef'; + $request = $this->createMock(Request::class); + $response = new Response(); + + $bookmark = (new Bookmark()) + ->setId(123) + ->setUrl('http://domain.tld') + ->setTitle('Title 123') + ->setPrivate(true) + ; + + $this->container->bookmarkService + ->expects(static::once()) + ->method('findByHash') + ->with($hash) + ->willReturn($bookmark) + ; + $this->container->bookmarkService + ->expects(static::once()) + ->method('set') + ->with($bookmark, true) + ->willReturnCallback(function (Bookmark $bookmark): Bookmark { + static::assertSame(32, strlen($bookmark->getAdditionalContentEntry('private_key'))); + + return $bookmark; + }) + ; + + $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]); + + static::assertSame(302, $result->getStatusCode()); + static::assertRegExp('#/subfolder/shaare/' . $hash . '\?key=\w{32}#', $result->getHeaderLine('Location')); + } + + /** + * Test shaare private with a private bookmark which does already have a key. + */ + public function testSharePrivateWithExistingPrivateBookmark(): void + { + $hash = 'abcdcef'; + $existingKey = 'this is a private key'; + $request = $this->createMock(Request::class); + $response = new Response(); + + $bookmark = (new Bookmark()) + ->setId(123) + ->setUrl('http://domain.tld') + ->setTitle('Title 123') + ->setPrivate(true) + ->addAdditionalContentEntry('private_key', $existingKey) + ; + + $this->container->bookmarkService + ->expects(static::once()) + ->method('findByHash') + ->with($hash) + ->willReturn($bookmark) + ; + $this->container->bookmarkService + ->expects(static::never()) + ->method('set') + ; + + $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame('/subfolder/shaare/' . $hash . '?key=' . $existingKey, $result->getHeaderLine('Location')); + } + + /** + * Test shaare private with a public bookmark. + */ + public function testSharePrivateWithPublicBookmark(): void + { + $hash = 'abcdcef'; + $request = $this->createMock(Request::class); + $response = new Response(); + + $bookmark = (new Bookmark()) + ->setId(123) + ->setUrl('http://domain.tld') + ->setTitle('Title 123') + ->setPrivate(false) + ; + + $this->container->bookmarkService + ->expects(static::once()) + ->method('findByHash') + ->with($hash) + ->willReturn($bookmark) + ; + $this->container->bookmarkService + ->expects(static::never()) + ->method('set') + ; + + $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame('/subfolder/shaare/' . $hash, $result->getHeaderLine('Location')); + } +} diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php index 5ca92507..5cbc8c73 100644 --- a/tests/front/controller/visitor/BookmarkListControllerTest.php +++ b/tests/front/controller/visitor/BookmarkListControllerTest.php @@ -291,6 +291,37 @@ class BookmarkListControllerTest extends TestCase ); } + /** + * Test GET /shaare/{hash}?key={key} - Find a link by hash using a private link. + */ + public function testPermalinkWithPrivateKey(): void + { + $hash = 'abcdef'; + $privateKey = 'this is a private key'; + + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $request = $this->createMock(Request::class); + $request->method('getParam')->willReturnCallback(function (string $key, $default = null) use ($privateKey) { + return $key === 'key' ? $privateKey : $default; + }); + $response = new Response(); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('findByHash') + ->with($hash, $privateKey) + ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld')) + ; + + $result = $this->controller->permalink($request, $response, ['hash' => $hash]); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('linklist', (string) $result->getBody()); + static::assertCount(1, $assignedVariables['links']); + } + /** * Test getting link list with thumbnail updates. * -> 2 thumbnails update, only 1 datastore write diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index 48cd9aad..e1115d49 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html @@ -129,6 +129,7 @@ {$strAddTag=t('Add tag')} {$strToggleSticky=t('Toggle sticky')} {$strSticky=t('Sticky')} + {$strShaarePrivate=t('Share a private link')} {ignore}End of translations{/ignore} {loop="links"}
@@ -241,6 +242,12 @@ {$strPermalinkLc} + {if="$is_logged_in && $value.private"} + + + + {/if} +
{if="isset($value.link_plugin)"} · From c2cd15dac2bfaebe6d32f7649fbdedc07400fa08 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 13:34:59 +0200 Subject: [PATCH 07/50] Move utils classes to Shaarli\Helper namespace and folder --- application/History.php | 1 + application/front/controller/visitor/InstallController.php | 2 +- application/{ => helper}/ApplicationUtils.php | 2 +- application/{ => helper}/FileUtils.php | 2 +- application/legacy/LegacyLinkDB.php | 2 +- application/legacy/LegacyUpdater.php | 2 +- application/render/PageBuilder.php | 2 +- application/security/BanManager.php | 2 +- composer.json | 1 + init.php | 2 +- tests/{ => helper}/ApplicationUtilsTest.php | 3 ++- tests/{ => helper}/FileUtilsTest.php | 2 +- tests/security/BanManagerTest.php | 2 +- tests/utils/FakeApplicationUtils.php | 2 ++ tests/utils/ReferenceHistory.php | 2 +- 15 files changed, 17 insertions(+), 12 deletions(-) rename application/{ => helper}/ApplicationUtils.php (99%) rename application/{ => helper}/FileUtils.php (99%) rename tests/{ => helper}/ApplicationUtilsTest.php (99%) rename tests/{ => helper}/FileUtilsTest.php (99%) diff --git a/application/History.php b/application/History.php index 4fd2f294..bd5c1bf7 100644 --- a/application/History.php +++ b/application/History.php @@ -4,6 +4,7 @@ namespace Shaarli; use DateTime; use Exception; use Shaarli\Bookmark\Bookmark; +use Shaarli\Helper\FileUtils; /** * Class History diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 564a5777..22329294 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; -use Shaarli\ApplicationUtils; use Shaarli\Container\ShaarliContainer; use Shaarli\Front\Exception\AlreadyInstalledException; use Shaarli\Front\Exception\ResourcePermissionException; +use Shaarli\Helper\ApplicationUtils; use Shaarli\Languages; use Shaarli\Security\SessionManager; use Slim\Http\Request; diff --git a/application/ApplicationUtils.php b/application/helper/ApplicationUtils.php similarity index 99% rename from application/ApplicationUtils.php rename to application/helper/ApplicationUtils.php index bd1c7cf3..4b34e114 100644 --- a/application/ApplicationUtils.php +++ b/application/helper/ApplicationUtils.php @@ -1,5 +1,5 @@ Date: Fri, 16 Oct 2020 11:50:53 +0200 Subject: [PATCH 08/50] Feature: add weekly and monthly view/RSS feed for daily page - Heavy refactoring of DailyController - Add a banner like in tag cloud to display monthly and weekly links - Translations: t() now supports variables with optional first letter uppercase Fixes #160 --- application/Utils.php | 33 +- application/bookmark/BookmarkFileService.php | 40 +- .../bookmark/BookmarkServiceInterface.php | 27 +- .../controller/visitor/DailyController.php | 103 +++-- application/helper/DailyPageHelper.php | 208 +++++++++ inc/languages/fr/LC_MESSAGES/shaarli.po | 250 ++++++----- tests/bookmark/BookmarkFileServiceTest.php | 124 ++++-- .../visitor/DailyControllerTest.php | 410 ++++++++++++++---- tests/helper/DailyPageHelperTest.php | 262 +++++++++++ tpl/default/daily.html | 32 +- tpl/default/dailyrss.html | 11 +- 11 files changed, 1186 insertions(+), 314 deletions(-) create mode 100644 application/helper/DailyPageHelper.php create mode 100644 tests/helper/DailyPageHelperTest.php diff --git a/application/Utils.php b/application/Utils.php index bc1c9f5d..db046893 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -326,6 +326,23 @@ function format_date($date, $time = true, $intl = true) return $formatter->format($date); } +/** + * Format the date month according to the locale. + * + * @param DateTimeInterface $date to format. + * + * @return bool|string Formatted date, or false if the input is invalid. + */ +function format_month(DateTimeInterface $date) +{ + if (! $date instanceof DateTimeInterface) { + return false; + } + + return strftime('%B', $date->getTimestamp()); +} + + /** * Check if the input is an integer, no matter its real type. * @@ -454,16 +471,20 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) * Wrapper function for translation which match the API * of gettext()/_() and ngettext(). * - * @param string $text Text to translate. - * @param string $nText The plural message ID. - * @param int $nb The number of items for plural forms. - * @param string $domain The domain where the translation is stored (default: shaarli). + * @param string $text Text to translate. + * @param string $nText The plural message ID. + * @param int $nb The number of items for plural forms. + * @param string $domain The domain where the translation is stored (default: shaarli). + * @param array $variables Associative array of variables to replace in translated text. + * @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables. * * @return string Text translated. */ -function t($text, $nText = '', $nb = 1, $domain = 'shaarli') +function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false) { - return dn__($domain, $text, $nText, $nb); + $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; }; + + return $postFunction(dn__($domain, $text, $nText, $nb, $variables)); } /** diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 14b3d620..0df2f47f 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -343,26 +343,42 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function days(): array - { - $bookmarkDays = []; - foreach ($this->search() as $bookmark) { - $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; - } - $bookmarkDays = array_keys($bookmarkDays); - sort($bookmarkDays); + public function findByDate( + \DateTimeInterface $from, + \DateTimeInterface $to, + ?\DateTimeInterface &$previous, + ?\DateTimeInterface &$next + ): array { + $out = []; + $previous = null; + $next = null; - return array_map('strval', $bookmarkDays); + foreach ($this->search([], null, false, false, true) as $bookmark) { + if ($to < $bookmark->getCreated()) { + $next = $bookmark->getCreated(); + } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { + $out[] = $bookmark; + } else { + if ($previous !== null) { + break; + } + $previous = $bookmark->getCreated(); + } + } + + return $out; } /** * @inheritDoc */ - public function filterDay(string $request) + public function getLatest(): ?Bookmark { - $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; + foreach ($this->search([], null, false, false, true) as $bookmark) { + return $bookmark; + } - return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); + return null; } /** diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 9fa61533..08cdbb4e 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -156,22 +156,29 @@ interface BookmarkServiceInterface public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; /** - * Returns the list of days containing articles (oldest first) + * Return a list of bookmark matching provided period of time. + * It also update directly previous and next date outside of given period found in the datastore. * - * @return array containing days (in format YYYYMMDD). + * @param \DateTimeInterface $from Starting date. + * @param \DateTimeInterface $to Ending date. + * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from. + * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to. + * + * @return array List of bookmarks matching provided period of time. */ - public function days(): array; + public function findByDate( + \DateTimeInterface $from, + \DateTimeInterface $to, + ?\DateTimeInterface &$previous, + ?\DateTimeInterface &$next + ): array; /** - * Returns the list of articles for a given day. + * Returns the latest bookmark by creation date. * - * @param string $request day to filter. Format: YYYYMMDD. - * - * @return Bookmark[] list of shaare found. - * - * @throws BookmarkNotFoundException + * @return Bookmark|null Found Bookmark or null if the datastore is empty. */ - public function filterDay(string $request); + public function getLatest(): ?Bookmark; /** * Creates the default database after a fresh install. diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 07617cf1..728bc2d8 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; use DateTime; -use DateTimeImmutable; use Shaarli\Bookmark\Bookmark; +use Shaarli\Helper\DailyPageHelper; use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController */ public function index(Request $request, Response $response): Response { - $day = $request->getQueryParam('day') ?? date('Ymd'); + $type = DailyPageHelper::extractRequestedType($request); + $format = DailyPageHelper::getFormatByType($type); + $latestBookmark = $this->container->bookmarkService->getLatest(); + $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark); + $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime); + $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime); + $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime); - $availableDates = $this->container->bookmarkService->days(); - $nbAvailableDates = count($availableDates); - $index = array_search($day, $availableDates); - - if ($index === false) { - // no bookmarks for day, but at least one day with bookmarks - $day = $availableDates[$nbAvailableDates - 1] ?? $day; - $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; - } else { - $previousDay = $availableDates[$index - 1] ?? ''; - $nextDay = $availableDates[$index + 1] ?? ''; - } - - if ($day === date('Ymd')) { - $this->assignView('dayDesc', t('Today')); - } elseif ($day === date('Ymd', strtotime('-1 days'))) { - $this->assignView('dayDesc', t('Yesterday')); - } - - try { - $linksToDisplay = $this->container->bookmarkService->filterDay($day); - } catch (\Exception $exc) { - $linksToDisplay = []; - } + $linksToDisplay = $this->container->bookmarkService->findByDate( + $start, + $end, + $previousDay, + $nextDay + ); $formatter = $this->container->formatterFactory->getFormatter(); $formatter->addContextData('base_path', $this->container->basePath); @@ -63,13 +51,15 @@ class DailyController extends ShaarliVisitorController $linksToDisplay[$key]['description'] = $bookmark->getDescription(); } - $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); $data = [ 'linksToDisplay' => $linksToDisplay, - 'day' => $dayDate->getTimestamp(), - 'dayDate' => $dayDate, - 'previousday' => $previousDay ?? '', - 'nextday' => $nextDay ?? '', + 'dayDate' => $start, + 'day' => $start->getTimestamp(), + 'previousday' => $previousDay ? $previousDay->format($format) : '', + 'nextday' => $nextDay ? $nextDay->format($format) : '', + 'dayDesc' => $dailyDesc, + 'type' => $type, + 'localizedType' => $this->translateType($type), ]; // Hooks are called before column construction so that plugins don't have to deal with columns. @@ -82,7 +72,7 @@ class DailyController extends ShaarliVisitorController $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); $this->assignView( 'pagetitle', - t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle + $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle ); return $response->write($this->render(TemplatePage::DAILY)); @@ -106,11 +96,14 @@ class DailyController extends ShaarliVisitorController } $days = []; + $type = DailyPageHelper::extractRequestedType($request); + $format = DailyPageHelper::getFormatByType($type); + $length = DailyPageHelper::getRssLengthByType($type); foreach ($this->container->bookmarkService->search() as $bookmark) { - $day = $bookmark->getCreated()->format('Ymd'); + $day = $bookmark->getCreated()->format($format); // Stop iterating after DAILY_RSS_NB_DAYS entries - if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { + if (count($days) === $length && !isset($days[$day])) { break; } @@ -127,12 +120,19 @@ class DailyController extends ShaarliVisitorController /** @var Bookmark[] $bookmarks */ foreach ($days as $day => $bookmarks) { - $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); + $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day); + $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime); + + // We only want the RSS entry to be published when the period is over. + if (new DateTime() < $endDateTime) { + continue; + } + $dataPerDay[$day] = [ - 'date' => $dayDatetime, - 'date_rss' => $dayDatetime->format(DateTime::RSS), - 'date_human' => format_date($dayDatetime, false, true), - 'absolute_url' => $indexUrl . 'daily?day=' . $day, + 'date' => $endDateTime, + 'date_rss' => $endDateTime->format(DateTime::RSS), + 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), + 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day, 'links' => [], ]; @@ -141,16 +141,20 @@ class DailyController extends ShaarliVisitorController // Make permalink URL absolute if ($bookmark->isNote()) { - $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); + $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl(); } } } - $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); - $this->assignView('index_url', $indexUrl); - $this->assignView('page_url', $pageUrl); - $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); - $this->assignView('days', $dataPerDay); + $this->assignAllView([ + 'title' => $this->container->conf->get('general.title', 'Shaarli'), + 'index_url' => $indexUrl, + 'page_url' => $pageUrl, + 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false), + 'days' => $dataPerDay, + 'type' => $type, + 'localizedType' => $this->translateType($type), + ]); $rssContent = $this->render(TemplatePage::DAILY_RSS); @@ -189,4 +193,13 @@ class DailyController extends ShaarliVisitorController return $columns; } + + protected function translateType($type): string + { + return [ + t('day') => t('Daily'), + t('week') => t('Weekly'), + t('month') => t('Monthly'), + ][t($type)] ?? t('Daily'); + } } diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php new file mode 100644 index 00000000..5fabc907 --- /dev/null +++ b/application/helper/DailyPageHelper.php @@ -0,0 +1,208 @@ +getQueryParam(static::MONTH) !== null) { + return static::MONTH; + } elseif ($request->getQueryParam(static::WEEK) !== null) { + return static::WEEK; + } + + return static::DAY; + } + + /** + * Extracts a DateTimeImmutable from provided HTTP request. + * If no parameter is provided, we rely on the creation date of the latest provided created bookmark. + * If the datastore is empty or no bookmark is provided, we use the current date. + * + * @param string $type month/week/day + * @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. + * + * @throws \Exception Type not supported. + */ + public static function extractRequestedDateTime( + string $type, + ?string $requestedDate, + Bookmark $latestBookmark = null + ): \DateTimeImmutable { + $format = static::getFormatByType($type); + if (empty($requestedDate)) { + return $latestBookmark instanceof Bookmark + ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) + : new \DateTimeImmutable() + ; + } + + // W is not supported by createFromFormat... + if ($type === static::WEEK) { + return (new \DateTimeImmutable()) + ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2)) + ; + } + + return \DateTimeImmutable::createFromFormat($format, $requestedDate); + } + + /** + * Get the DateTime format used by provided type + * Examples: + * - day: 20201016 () + * - week: 202041 () + * - month: 202010 () + * + * @param string $type month/week/day + * + * @return string DateTime compatible format + * + * @see https://www.php.net/manual/en/datetime.format.php + * + * @throws \Exception Type not supported. + */ + public static function getFormatByType(string $type): string + { + switch ($type) { + case static::MONTH: + return 'Ym'; + case static::WEEK: + return 'YW'; + case static::DAY: + return 'Ymd'; + default: + throw new \Exception('Unsupported daily format type'); + } + } + + /** + * Get the first DateTime of the time period depending on given datetime and type. + * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax + * and we don't want to alter original datetime. + * + * @param string $type month/week/day + * @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. + */ + public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface + { + switch ($type) { + case static::MONTH: + return $requested->modify('first day of this month midnight'); + case static::WEEK: + return $requested->modify('Monday this week midnight'); + case static::DAY: + return $requested->modify('Today midnight'); + default: + throw new \Exception('Unsupported daily format type'); + } + } + + /** + * Get the last DateTime of the time period depending on given datetime and type. + * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax + * and we don't want to alter original datetime. + * + * @param string $type month/week/day + * @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. + */ + public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface + { + switch ($type) { + case static::MONTH: + return $requested->modify('last day of this month 23:59:59'); + case static::WEEK: + return $requested->modify('Sunday this week 23:59:59'); + case static::DAY: + return $requested->modify('Today 23:59:59'); + default: + 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) + * + * @return string Localized time period description + * + * @throws \Exception Type not supported. + */ + public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string + { + switch ($type) { + case static::MONTH: + return $requested->format('F') . ', ' . $requested->format('Y'); + case static::WEEK: + $requested = $requested->modify('Monday this week'); + return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')'; + case static::DAY: + $out = ''; + if ($requested->format('Ymd') === date('Ymd')) { + $out = t('Today') . ' - '; + } elseif ($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'); + } + } + + /** + * Get the number of items to display in the RSS feed depending on the given type. + * + * @param string $type month/week/day + * + * @return int number of elements + * + * @throws \Exception Type not supported. + */ + public static function getRssLengthByType(string $type): int + { + switch ($type) { + case static::MONTH: + return 12; // 1 year + case static::WEEK: + return 26; // ~6 months + case static::DAY: + return 30; // ~1 month + default: + throw new \Exception('Unsupported daily format type'); + } + } +} diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 3f14d22c..6d4ff0bd 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-10-27 19:32+0100\n" -"PO-Revision-Date: 2020-10-27 19:32+0100\n" +"POT-Creation-Date: 2020-10-27 19:44+0100\n" +"PO-Revision-Date: 2020-10-27 19:44+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -20,78 +20,11 @@ msgstr "" "X-Poedit-SearchPath-3: init.php\n" "X-Poedit-SearchPath-4: plugins\n" -#: application/ApplicationUtils.php:162 -#, php-format -msgid "" -"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " -"cannot run. Your PHP version has known security vulnerabilities and should " -"be updated as soon as possible." -msgstr "" -"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne " -"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/ApplicationUtils.php:195 application/ApplicationUtils.php:215 -msgid "directory is not readable" -msgstr "le répertoire n'est pas accessible en lecture" - -#: application/ApplicationUtils.php:218 -msgid "directory is not writable" -msgstr "le répertoire n'est pas accessible en écriture" - -#: application/ApplicationUtils.php:240 -msgid "file is not readable" -msgstr "le fichier n'est pas accessible en lecture" - -#: application/ApplicationUtils.php:243 -msgid "file is not writable" -msgstr "le fichier n'est pas accessible en écriture" - -#: application/ApplicationUtils.php:277 -msgid "Configuration parsing" -msgstr "Chargement de la configuration" - -#: application/ApplicationUtils.php:278 -msgid "Slim Framework (routing, etc.)" -msgstr "Slim Framwork (routage, etc.)" - -#: application/ApplicationUtils.php:279 -msgid "Multibyte (Unicode) string support" -msgstr "Support des chaînes de caractère multibytes (Unicode)" - -#: application/ApplicationUtils.php:280 -msgid "Required to use thumbnails" -msgstr "Obligatoire pour utiliser les miniatures" - -#: application/ApplicationUtils.php:281 -msgid "Localized text sorting (e.g. e->è->f)" -msgstr "Tri des textes traduits (ex : e->è->f)" - -#: application/ApplicationUtils.php:282 -msgid "Better retrieval of bookmark metadata and thumbnail" -msgstr "Meilleure récupération des meta-données des marque-pages et minatures" - -#: application/ApplicationUtils.php:283 -msgid "Use the translation system in gettext mode" -msgstr "Utiliser le système de traduction en mode gettext" - -#: application/ApplicationUtils.php:284 -msgid "Login using LDAP server" -msgstr "Authentification via un serveur LDAP" - -#: application/FileUtils.php:100 -msgid "Provided path is not a directory." -msgstr "Le chemin fourni n'est pas un dossier." - -#: application/FileUtils.php:104 -msgid "Trying to delete a folder outside of Shaarli path." -msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli." - -#: application/History.php:179 +#: application/History.php:180 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:190 +#: application/History.php:191 msgid "Could not parse history file" msgstr "Format incorrect pour le fichier d'historique" @@ -123,27 +56,27 @@ 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:385 +#: application/Utils.php:402 msgid "Setting not set" msgstr "Paramètre non défini" -#: application/Utils.php:392 +#: application/Utils.php:409 msgid "Unlimited" msgstr "Illimité" -#: application/Utils.php:395 +#: application/Utils.php:412 msgid "B" msgstr "o" -#: application/Utils.php:395 +#: application/Utils.php:412 msgid "kiB" msgstr "ko" -#: application/Utils.php:395 +#: application/Utils.php:412 msgid "MiB" msgstr "Mo" -#: application/Utils.php:395 +#: application/Utils.php:412 msgid "GiB" msgstr "Go" @@ -156,7 +89,7 @@ msgstr "Vous n'êtes pas autorisé à modifier les données" #: application/bookmark/BookmarkFileService.php:208 msgid "This bookmarks already exists" -msgstr "Ce marque-page existe déjà." +msgstr "Ce marque-page existe déjà" #: application/bookmark/BookmarkInitializer.php:39 msgid "(private bookmark with thumbnail demo)" @@ -354,7 +287,8 @@ msgid "Direct link" msgstr "Liens directs" #: application/feed/FeedBuilder.php:181 -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 +#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 msgid "Permalink" msgstr "Permalien" @@ -537,20 +471,36 @@ msgstr "Outils" msgid "Search: " msgstr "Recherche : " -#: application/front/controller/visitor/DailyController.php:45 -msgid "Today" -msgstr "Aujourd'hui" +#: application/front/controller/visitor/DailyController.php:200 +msgid "day" +msgstr "jour" -#: application/front/controller/visitor/DailyController.php:47 -msgid "Yesterday" -msgstr "Hier" - -#: application/front/controller/visitor/DailyController.php:85 +#: application/front/controller/visitor/DailyController.php:200 +#: application/front/controller/visitor/DailyController.php:203 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 msgid "Daily" msgstr "Quotidien" +#: application/front/controller/visitor/DailyController.php:201 +msgid "week" +msgstr "semaine" + +#: application/front/controller/visitor/DailyController.php:201 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Weekly" +msgstr "Hebdomadaire" + +#: application/front/controller/visitor/DailyController.php:202 +msgid "month" +msgstr "mois" + +#: application/front/controller/visitor/DailyController.php:202 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "Monthly" +msgstr "Mensuel" + #: application/front/controller/visitor/ErrorController.php:33 msgid "An unexpected error occurred." msgstr "Une erreur inattendue s'est produite." @@ -616,7 +566,7 @@ msgstr "Mur d'images" #: application/front/controller/visitor/TagCloudController.php:88 msgid "Tag " -msgstr "Tag" +msgstr "Tag " #: application/front/exceptions/AlreadyInstalledException.php:11 msgid "Shaarli has already been installed. Login to edit the configuration." @@ -644,6 +594,86 @@ msgstr "" msgid "Wrong token." msgstr "Jeton invalide." +#: application/helper/ApplicationUtils.php:162 +#, php-format +msgid "" +"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " +"cannot run. Your PHP version has known security vulnerabilities and should " +"be updated as soon as possible." +msgstr "" +"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne " +"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 +msgid "directory is not readable" +msgstr "le répertoire n'est pas accessible en lecture" + +#: application/helper/ApplicationUtils.php:218 +msgid "directory is not writable" +msgstr "le répertoire n'est pas accessible en écriture" + +#: application/helper/ApplicationUtils.php:240 +msgid "file is not readable" +msgstr "le fichier n'est pas accessible en lecture" + +#: application/helper/ApplicationUtils.php:243 +msgid "file is not writable" +msgstr "le fichier n'est pas accessible en écriture" + +#: application/helper/ApplicationUtils.php:277 +msgid "Configuration parsing" +msgstr "Chargement de la configuration" + +#: application/helper/ApplicationUtils.php:278 +msgid "Slim Framework (routing, etc.)" +msgstr "Slim Framwork (routage, etc.)" + +#: application/helper/ApplicationUtils.php:279 +msgid "Multibyte (Unicode) string support" +msgstr "Support des chaînes de caractère multibytes (Unicode)" + +#: application/helper/ApplicationUtils.php:280 +msgid "Required to use thumbnails" +msgstr "Obligatoire pour utiliser les miniatures" + +#: application/helper/ApplicationUtils.php:281 +msgid "Localized text sorting (e.g. e->è->f)" +msgstr "Tri des textes traduits (ex : e->è->f)" + +#: application/helper/ApplicationUtils.php:282 +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 +msgid "Use the translation system in gettext mode" +msgstr "Utiliser le système de traduction en mode gettext" + +#: application/helper/ApplicationUtils.php:284 +msgid "Login using LDAP server" +msgstr "Authentification via un serveur LDAP" + +#: application/helper/DailyPageHelper.php:172 +msgid "Week" +msgstr "Semaine" + +#: application/helper/DailyPageHelper.php:176 +msgid "Today" +msgstr "Aujourd'hui" + +#: application/helper/DailyPageHelper.php:178 +msgid "Yesterday" +msgstr "Hier" + +#: application/helper/FileUtils.php:100 +msgid "Provided path is not a directory." +msgstr "Le chemin fourni n'est pas un dossier." + +#: application/helper/FileUtils.php:104 +msgid "Trying to delete a folder outside of Shaarli path." +msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli." + #: application/legacy/LegacyLinkDB.php:131 msgid "You are not authorized to add a link." msgstr "Vous n'êtes pas autorisé à ajouter un lien." @@ -1103,25 +1133,30 @@ msgstr "Aucune" msgid "Save" msgstr "Enregistrer" -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -msgid "The Daily Shaarli" -msgstr "Le Quotidien Shaarli" +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +msgid "1 RSS entry per :type" +msgid_plural "" +msgstr[0] "1 entrée RSS par :type" +msgstr[1] "" -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 -msgid "1 RSS entry per day" -msgstr "1 entrée RSS par jour" +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +msgid "Previous :type" +msgid_plural "" +msgstr[0] ":type précédent" +msgstr[1] "Jour précédent" -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 -msgid "Previous day" -msgstr "Jour précédent" +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +msgid "All links of one :type in a single page." +msgid_plural "" +msgstr[0] "Tous les liens d'un :type sur une page." +msgstr[1] "Tous les liens d'un jour sur une page." -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 -msgid "All links of one day in a single page." -msgstr "Tous les liens d'un jour sur une page." - -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 -msgid "Next day" -msgstr "Jour suivant" +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +msgid "Next :type" +msgid_plural "" +msgstr[0] ":type suivant" +msgstr[1] "" #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 msgid "Edit Shaare" @@ -1821,8 +1856,11 @@ msgstr "" "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " "Ajouter aux favoris »" -#~ msgid "Rename" -#~ msgstr "Renommer" +#~ msgid "Display:" +#~ msgstr "Afficher :" + +#~ msgid "The Daily Shaarli" +#~ msgstr "Le Quotidien Shaarli" #, fuzzy #~| msgid "Selection" diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php index 47970117..8e0ff8dd 100644 --- a/tests/bookmark/BookmarkFileServiceTest.php +++ b/tests/bookmark/BookmarkFileServiceTest.php @@ -685,22 +685,6 @@ class BookmarkFileServiceTest extends TestCase $this->assertEquals(0, $linkDB->count()); } - /** - * List the days for which bookmarks have been posted - */ - public function testDays() - { - $this->assertSame( - ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'], - $this->publicLinkDB->days() - ); - - $this->assertSame( - ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'], - $this->privateLinkDB->days() - ); - } - /** * The URL corresponds to an existing entry in the DB */ @@ -1074,33 +1058,105 @@ class BookmarkFileServiceTest extends TestCase } /** - * Test filterDay while logged in + * Test find by dates in the middle of the datastore (sorted by dates) with a single bookmark as a result. */ - public function testFilterDayLoggedIn(): void + public function testFilterByDateMidTimePeriodSingleBookmark(): void { - $bookmarks = $this->privateLinkDB->filterDay('20121206'); - $expectedIds = [4, 9, 1, 0]; + $bookmarks = $this->privateLinkDB->findByDate( + DateTime::createFromFormat('Ymd_His', '20121206_150000'), + DateTime::createFromFormat('Ymd_His', '20121206_160000'), + $before, + $after + ); - static::assertCount(4, $bookmarks); - foreach ($bookmarks as $bookmark) { - $i = ($i ?? -1) + 1; - static::assertSame($expectedIds[$i], $bookmark->getId()); - } + static::assertCount(1, $bookmarks); + + static::assertSame(9, $bookmarks[0]->getId()); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after); } /** - * Test filterDay while logged out + * Test find by dates in the middle of the datastore (sorted by dates) with a multiple bookmarks as a result. */ - public function testFilterDayLoggedOut(): void + public function testFilterByDateMidTimePeriodMultipleBookmarks(): void { - $bookmarks = $this->publicLinkDB->filterDay('20121206'); - $expectedIds = [4, 9, 1]; + $bookmarks = $this->privateLinkDB->findByDate( + DateTime::createFromFormat('Ymd_His', '20121206_150000'), + DateTime::createFromFormat('Ymd_His', '20121206_180000'), + $before, + $after + ); - static::assertCount(3, $bookmarks); - foreach ($bookmarks as $bookmark) { - $i = ($i ?? -1) + 1; - static::assertSame($expectedIds[$i], $bookmark->getId()); - } + static::assertCount(2, $bookmarks); + + static::assertSame(1, $bookmarks[0]->getId()); + static::assertSame(9, $bookmarks[1]->getId()); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_182539'), $after); + } + + /** + * Test find by dates at the end of the datastore (sorted by dates). + */ + public function testFilterByDateLastTimePeriod(): void + { + $after = new DateTime(); + $bookmarks = $this->privateLinkDB->findByDate( + DateTime::createFromFormat('Ymd_His', '20150310_114640'), + DateTime::createFromFormat('Ymd_His', '20450101_010101'), + $before, + $after + ); + + static::assertCount(1, $bookmarks); + + static::assertSame(41, $bookmarks[0]->getId()); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20150310_114633'), $before); + static::assertNull($after); + } + + /** + * Test find by dates at the beginning of the datastore (sorted by dates). + */ + public function testFilterByDateFirstTimePeriod(): void + { + $before = new DateTime(); + $bookmarks = $this->privateLinkDB->findByDate( + DateTime::createFromFormat('Ymd_His', '20000101_101010'), + DateTime::createFromFormat('Ymd_His', '20100309_110000'), + $before, + $after + ); + + static::assertCount(1, $bookmarks); + + static::assertSame(11, $bookmarks[0]->getId()); + static::assertNull($before); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20100310_101010'), $after); + } + + /** + * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead. + */ + public function testGetLatestWithSticky(): void + { + $bookmark = $this->publicLinkDB->getLatest(); + + static::assertSame(41, $bookmark->getId()); + } + + /** + * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead. + */ + public function testGetLatestEmptyDatastore(): void + { + unlink($this->conf->get('resource.datastore')); + $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); + + $bookmark = $this->publicLinkDB->getLatest(); + + static::assertNull($bookmark); } /** diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php index fc78bc13..758e7219 100644 --- a/tests/front/controller/visitor/DailyControllerTest.php +++ b/tests/front/controller/visitor/DailyControllerTest.php @@ -28,52 +28,49 @@ class DailyControllerTest extends TestCase public function testValidIndexControllerInvokeDefault(): void { $currentDay = new \DateTimeImmutable('2020-05-13'); + $previousDate = new \DateTime('2 days ago 00:00:00'); + $nextDate = new \DateTime('today 00:00:00'); $request = $this->createMock(Request::class); - $request->method('getQueryParam')->willReturn($currentDay->format('Ymd')); + $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { + return $key === 'day' ? $currentDay->format('Ymd') : null; + }); $response = new Response(); // Save RainTPL assigned variables $assignedVariables = []; $this->assignTemplateVars($assignedVariables); - // Links dataset: 2 links with thumbnails $this->container->bookmarkService ->expects(static::once()) - ->method('days') - ->willReturnCallback(function () use ($currentDay): array { - return [ - '20200510', - $currentDay->format('Ymd'), - '20200516', - ]; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { - return [ - (new Bookmark()) - ->setId(1) - ->setUrl('http://url.tld') - ->setTitle(static::generateString(50)) - ->setDescription(static::generateString(500)) - , - (new Bookmark()) - ->setId(2) - ->setUrl('http://url2.tld') - ->setTitle(static::generateString(50)) - ->setDescription(static::generateString(500)) - , - (new Bookmark()) - ->setId(3) - ->setUrl('http://url3.tld') - ->setTitle(static::generateString(50)) - ->setDescription(static::generateString(500)) - , - ]; - }) + ->method('findByDate') + ->willReturnCallback( + function ($from, $to, &$previous, &$next) use ($currentDay, $previousDate, $nextDate): array { + $previous = $previousDate; + $next = $nextDate; + + return [ + (new Bookmark()) + ->setId(1) + ->setUrl('http://url.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + (new Bookmark()) + ->setId(2) + ->setUrl('http://url2.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + (new Bookmark()) + ->setId(3) + ->setUrl('http://url3.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + ]; + } + ) ; // Make sure that PluginManager hook is triggered @@ -81,20 +78,22 @@ class DailyControllerTest extends TestCase ->expects(static::atLeastOnce()) ->method('executeHooks') ->withConsecutive(['render_daily']) - ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { - if ('render_daily' === $hook) { - static::assertArrayHasKey('linksToDisplay', $data); - static::assertCount(3, $data['linksToDisplay']); - static::assertSame(1, $data['linksToDisplay'][0]['id']); - static::assertSame($currentDay->getTimestamp(), $data['day']); - static::assertSame('20200510', $data['previousday']); - static::assertSame('20200516', $data['nextday']); + ->willReturnCallback( + function (string $hook, array $data, array $param) use ($currentDay, $previousDate, $nextDate): array { + if ('render_daily' === $hook) { + static::assertArrayHasKey('linksToDisplay', $data); + static::assertCount(3, $data['linksToDisplay']); + static::assertSame(1, $data['linksToDisplay'][0]['id']); + static::assertSame($currentDay->getTimestamp(), $data['day']); + static::assertSame($previousDate->format('Ymd'), $data['previousday']); + static::assertSame($nextDate->format('Ymd'), $data['nextday']); - static::assertArrayHasKey('loggedin', $param); + static::assertArrayHasKey('loggedin', $param); + } + + return $data; } - - return $data; - }) + ) ; $result = $this->controller->index($request, $response); @@ -107,6 +106,11 @@ class DailyControllerTest extends TestCase ); static::assertEquals($currentDay, $assignedVariables['dayDate']); static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); + static::assertSame($previousDate->format('Ymd'), $assignedVariables['previousday']); + static::assertSame($nextDate->format('Ymd'), $assignedVariables['nextday']); + static::assertSame('day', $assignedVariables['type']); + static::assertSame('May 13, 2020', $assignedVariables['dayDesc']); + static::assertSame('Daily', $assignedVariables['localizedType']); static::assertCount(3, $assignedVariables['linksToDisplay']); $link = $assignedVariables['linksToDisplay'][0]; @@ -171,26 +175,19 @@ class DailyControllerTest extends TestCase $currentDay = new \DateTimeImmutable('2020-05-13'); $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { + return $key === 'day' ? $currentDay->format('Ymd') : null; + }); $response = new Response(); // Save RainTPL assigned variables $assignedVariables = []; $this->assignTemplateVars($assignedVariables); - // Links dataset: 2 links with thumbnails $this->container->bookmarkService ->expects(static::once()) - ->method('days') + ->method('findByDate') ->willReturnCallback(function () use ($currentDay): array { - return [ - $currentDay->format($currentDay->format('Ymd')), - ]; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { return [ (new Bookmark()) ->setId(1) @@ -250,20 +247,10 @@ class DailyControllerTest extends TestCase $assignedVariables = []; $this->assignTemplateVars($assignedVariables); - // Links dataset: 2 links with thumbnails $this->container->bookmarkService ->expects(static::once()) - ->method('days') + ->method('findByDate') ->willReturnCallback(function () use ($currentDay): array { - return [ - $currentDay->format($currentDay->format('Ymd')), - ]; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { return [ (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), (new Bookmark()) @@ -320,14 +307,7 @@ class DailyControllerTest extends TestCase // Links dataset: 2 links with thumbnails $this->container->bookmarkService ->expects(static::once()) - ->method('days') - ->willReturnCallback(function (): array { - return []; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') + ->method('findByDate') ->willReturnCallback(function (): array { return []; }) @@ -347,7 +327,7 @@ class DailyControllerTest extends TestCase static::assertSame(200, $result->getStatusCode()); static::assertSame('daily', (string) $result->getBody()); static::assertCount(0, $assignedVariables['linksToDisplay']); - static::assertSame('Today', $assignedVariables['dayDesc']); + static::assertSame('Today - ' . (new \DateTime())->format('F d, Y'), $assignedVariables['dayDesc']); static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); } @@ -361,6 +341,7 @@ class DailyControllerTest extends TestCase new \DateTimeImmutable('2020-05-17'), new \DateTimeImmutable('2020-05-15'), new \DateTimeImmutable('2020-05-13'), + new \DateTimeImmutable('+1 month'), ]; $request = $this->createMock(Request::class); @@ -371,6 +352,7 @@ class DailyControllerTest extends TestCase (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), + (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'), ]); $this->container->pageCacheManager @@ -397,13 +379,14 @@ class DailyControllerTest extends TestCase static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']); static::assertFalse($assignedVariables['hide_timestamps']); - static::assertCount(2, $assignedVariables['days']); + static::assertCount(3, $assignedVariables['days']); $day = $assignedVariables['days'][$dates[0]->format('Ymd')]; + $date = $dates[0]->setTime(23, 59, 59); - static::assertEquals($dates[0], $day['date']); - static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']); - static::assertSame(format_date($dates[0], false), $day['date_human']); + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame(format_date($date, false), $day['date_human']); static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); static::assertCount(1, $day['links']); static::assertSame(1, $day['links'][0]['id']); @@ -411,10 +394,11 @@ class DailyControllerTest extends TestCase static::assertEquals($dates[0], $day['links'][0]['created']); $day = $assignedVariables['days'][$dates[1]->format('Ymd')]; + $date = $dates[1]->setTime(23, 59, 59); - static::assertEquals($dates[1], $day['date']); - static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']); - static::assertSame(format_date($dates[1], false), $day['date_human']); + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame(format_date($date, false), $day['date_human']); static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); static::assertCount(2, $day['links']); @@ -424,6 +408,18 @@ class DailyControllerTest extends TestCase static::assertSame(3, $day['links'][1]['id']); static::assertSame('http://domain.tld/3', $day['links'][1]['url']); static::assertEquals($dates[1], $day['links'][1]['created']); + + $day = $assignedVariables['days'][$dates[2]->format('Ymd')]; + $date = $dates[2]->setTime(23, 59, 59); + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame(format_date($date, false), $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?day='. $dates[2]->format('Ymd'), $day['absolute_url']); + static::assertCount(1, $day['links']); + static::assertSame(4, $day['links'][0]['id']); + static::assertSame('http://domain.tld/4', $day['links'][0]['url']); + static::assertEquals($dates[2], $day['links'][0]['created']); } /** @@ -475,4 +471,246 @@ class DailyControllerTest extends TestCase static::assertFalse($assignedVariables['hide_timestamps']); static::assertCount(0, $assignedVariables['days']); } + + /** + * Test simple display index with week parameter + */ + public function testSimpleIndexWeekly(): void + { + $currentDay = new \DateTimeImmutable('2020-05-13'); + $expectedDay = new \DateTimeImmutable('2020-05-11'); + + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { + return $key === 'week' ? $currentDay->format('YW') : null; + }); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('findByDate') + ->willReturnCallback( + function (): array { + return [ + (new Bookmark()) + ->setId(1) + ->setUrl('http://url.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + (new Bookmark()) + ->setId(2) + ->setUrl('http://url2.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + ]; + } + ) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('daily', (string) $result->getBody()); + static::assertSame( + 'Weekly - Week 20 (May 11, 2020) - Shaarli', + $assignedVariables['pagetitle'] + ); + + static::assertCount(2, $assignedVariables['linksToDisplay']); + static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']); + static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); + static::assertSame('', $assignedVariables['previousday']); + static::assertSame('', $assignedVariables['nextday']); + static::assertSame('Week 20 (May 11, 2020)', $assignedVariables['dayDesc']); + static::assertSame('week', $assignedVariables['type']); + static::assertSame('Weekly', $assignedVariables['localizedType']); + } + + /** + * Test simple display index with month parameter + */ + public function testSimpleIndexMonthly(): void + { + $currentDay = new \DateTimeImmutable('2020-05-13'); + $expectedDay = new \DateTimeImmutable('2020-05-01'); + + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { + return $key === 'month' ? $currentDay->format('Ym') : null; + }); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('findByDate') + ->willReturnCallback( + function (): array { + return [ + (new Bookmark()) + ->setId(1) + ->setUrl('http://url.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + (new Bookmark()) + ->setId(2) + ->setUrl('http://url2.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + ]; + } + ) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('daily', (string) $result->getBody()); + static::assertSame( + 'Monthly - May, 2020 - Shaarli', + $assignedVariables['pagetitle'] + ); + + static::assertCount(2, $assignedVariables['linksToDisplay']); + static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']); + static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); + static::assertSame('', $assignedVariables['previousday']); + static::assertSame('', $assignedVariables['nextday']); + static::assertSame('May, 2020', $assignedVariables['dayDesc']); + static::assertSame('month', $assignedVariables['type']); + static::assertSame('Monthly', $assignedVariables['localizedType']); + } + + /** + * Test simple display RSS with week parameter + */ + public function testSimpleRssWeekly(): void + { + $dates = [ + new \DateTimeImmutable('2020-05-19'), + new \DateTimeImmutable('2020-05-13'), + ]; + $expectedDates = [ + new \DateTimeImmutable('2020-05-24 23:59:59'), + new \DateTimeImmutable('2020-05-17 23:59:59'), + ]; + + $this->container->environment['QUERY_STRING'] = 'week'; + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string { + return $key === 'week' ? '' : null; + }); + $response = new Response(); + + $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ + (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), + (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), + (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), + ]); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->rss($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); + static::assertSame('dailyrss', (string) $result->getBody()); + static::assertSame('Shaarli', $assignedVariables['title']); + static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); + static::assertSame('http://shaarli/subfolder/daily-rss?week', $assignedVariables['page_url']); + static::assertFalse($assignedVariables['hide_timestamps']); + static::assertCount(2, $assignedVariables['days']); + + $day = $assignedVariables['days'][$dates[0]->format('YW')]; + $date = $expectedDates[0]; + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame('Week 21 (May 18, 2020)', $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?week='. $dates[0]->format('YW'), $day['absolute_url']); + static::assertCount(1, $day['links']); + + $day = $assignedVariables['days'][$dates[1]->format('YW')]; + $date = $expectedDates[1]; + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame('Week 20 (May 11, 2020)', $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?week='. $dates[1]->format('YW'), $day['absolute_url']); + static::assertCount(2, $day['links']); + } + + /** + * Test simple display RSS with month parameter + */ + public function testSimpleRssMonthly(): void + { + $dates = [ + new \DateTimeImmutable('2020-05-19'), + new \DateTimeImmutable('2020-04-13'), + ]; + $expectedDates = [ + new \DateTimeImmutable('2020-05-31 23:59:59'), + new \DateTimeImmutable('2020-04-30 23:59:59'), + ]; + + $this->container->environment['QUERY_STRING'] = 'month'; + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string { + return $key === 'month' ? '' : null; + }); + $response = new Response(); + + $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ + (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), + (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), + (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), + ]); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->rss($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); + static::assertSame('dailyrss', (string) $result->getBody()); + static::assertSame('Shaarli', $assignedVariables['title']); + static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); + static::assertSame('http://shaarli/subfolder/daily-rss?month', $assignedVariables['page_url']); + static::assertFalse($assignedVariables['hide_timestamps']); + static::assertCount(2, $assignedVariables['days']); + + $day = $assignedVariables['days'][$dates[0]->format('Ym')]; + $date = $expectedDates[0]; + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame('May, 2020', $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?month='. $dates[0]->format('Ym'), $day['absolute_url']); + static::assertCount(1, $day['links']); + + $day = $assignedVariables['days'][$dates[1]->format('Ym')]; + $date = $expectedDates[1]; + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame('April, 2020', $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?month='. $dates[1]->format('Ym'), $day['absolute_url']); + static::assertCount(2, $day['links']); + } } diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php new file mode 100644 index 00000000..e0378491 --- /dev/null +++ b/tests/helper/DailyPageHelperTest.php @@ -0,0 +1,262 @@ +createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string { + return $queryParams[$key] ?? null; + }); + + $type = DailyPageHelper::extractRequestedType($request); + + static::assertSame($type, $expectedType); + } + + /** + * @dataProvider getRequestedDateTimes + */ + public function testExtractRequestedDateTime( + string $type, + string $input, + ?Bookmark $bookmark, + \DateTimeInterface $expectedDateTime, + string $compareFormat = 'Ymd' + ): void { + $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark); + + static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat)); + } + + public function testExtractRequestedDateTimeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::extractRequestedDateTime('nope', null, null); + } + + /** + * @dataProvider getFormatsByType + */ + public function testGetFormatByType(string $type, string $expectedFormat): void + { + $format = DailyPageHelper::getFormatByType($type); + + static::assertSame($expectedFormat, $format); + } + + public function testGetFormatByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getFormatByType('nope'); + } + + /** + * @dataProvider getStartDatesByType + */ + public function testGetStartDatesByType( + string $type, + \DateTimeImmutable $dateTime, + \DateTimeInterface $expectedDateTime + ): void { + $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime); + + static::assertEquals($expectedDateTime, $startDateTime); + } + + public function testGetStartDatesByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable()); + } + + /** + * @dataProvider getEndDatesByType + */ + public function testGetEndDatesByType( + string $type, + \DateTimeImmutable $dateTime, + \DateTimeInterface $expectedDateTime + ): void { + $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime); + + static::assertEquals($expectedDateTime, $endDateTime); + } + + public function testGetEndDatesByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable()); + } + + /** + * @dataProvider getDescriptionsByType + */ + public function testGeDescriptionsByType( + string $type, + \DateTimeImmutable $dateTime, + string $expectedDescription + ): void { + $description = DailyPageHelper::getDescriptionByType($type, $dateTime); + + static::assertEquals($expectedDescription, $description); + } + + public function getDescriptionByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable()); + } + + /** + * @dataProvider getRssLengthsByType + */ + public function testGeRssLengthsByType(string $type): void { + $length = DailyPageHelper::getRssLengthByType($type); + + static::assertIsInt($length); + } + + public function testGeRssLengthsByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getRssLengthByType('nope'); + } + + /** + * Data provider for testExtractRequestedType() test method. + */ + public function getRequestedTypes(): array + { + return [ + [['month' => null], DailyPageHelper::DAY], + [['month' => ''], DailyPageHelper::MONTH], + [['month' => 'content'], DailyPageHelper::MONTH], + [['week' => null], DailyPageHelper::DAY], + [['week' => ''], DailyPageHelper::WEEK], + [['week' => 'content'], DailyPageHelper::WEEK], + [['day' => null], DailyPageHelper::DAY], + [['day' => ''], DailyPageHelper::DAY], + [['day' => 'content'], DailyPageHelper::DAY], + ]; + } + + /** + * Data provider for testExtractRequestedDateTime() test method. + */ + public function getRequestedDateTimes(): array + { + return [ + [DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')], + [ + DailyPageHelper::DAY, + '', + (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), + $date, + ], + [DailyPageHelper::DAY, '', null, new \DateTime()], + [DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')], + [ + DailyPageHelper::WEEK, + '', + (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), + new \DateTime('2020-10-13'), + ], + [DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'], + [DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'], + [ + DailyPageHelper::MONTH, + '', + (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), + new \DateTime('2020-10-13'), + 'Ym' + ], + [DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'], + ]; + } + + /** + * Data provider for testGetFormatByType() test method. + */ + public function getFormatsByType(): array + { + return [ + [DailyPageHelper::DAY, 'Ymd'], + [DailyPageHelper::WEEK, 'YW'], + [DailyPageHelper::MONTH, 'Ym'], + ]; + } + + /** + * Data provider for testGetStartDatesByType() 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')], + ]; + } + + /** + * Data provider for testGetEndDatesByType() test method. + */ + 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')], + ]; + } + + /** + * Data provider for testGetDescriptionsByType() test method. + */ + public function getDescriptionsByType(): array + { + return [ + [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F d, Y')], + [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F d, 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. + */ + public function getRssLengthsByType(): array + { + return [ + [DailyPageHelper::DAY], + [DailyPageHelper::WEEK], + [DailyPageHelper::MONTH], + ]; + } +} diff --git a/tpl/default/daily.html b/tpl/default/daily.html index 3749bffb..5e038c39 100644 --- a/tpl/default/daily.html +++ b/tpl/default/daily.html @@ -6,12 +6,25 @@ {include="page.header"} + + +

- {'The Daily Shaarli'|t} - + {$localizedType} Shaarli + t($type)])"}" + > + +

@@ -25,19 +38,19 @@
- {'All links of one day in a single page.'|t} + {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}

- {if="!empty($dayDesc)"} - {$dayDesc} - - {/if} - {function="format_date($dayDate, false)"} + {$dayDesc}

diff --git a/tpl/default/dailyrss.html b/tpl/default/dailyrss.html index d40d9496..871a3ba7 100644 --- a/tpl/default/dailyrss.html +++ b/tpl/default/dailyrss.html @@ -1,9 +1,9 @@ - Daily - {$title} + {$localizedType} - {$title} {$index_url} - Daily shaared bookmarks + {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"} {$language} {$index_url} Shaarli @@ -18,12 +18,15 @@ {loop="$value.links"}

{$value.title}

- {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}
+ {if="!$hide_timestamps"}{$value.created|format_date} — {/if} + {'Permalink'|t} + {if="$value.tags"} — {$value.tags}{/if} +
{$value.url}

{if="$value.thumbnail"}thumbnail{/if}
{if="$value.description"}{$value.description}{/if} -


+

{/loop} ]]> From 54afb1d6f65f727b20b66582bb63a42c421eea4d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 27 Oct 2020 19:55:29 +0100 Subject: [PATCH 09/50] Fix rebase issue --- application/front/controller/admin/ServerController.php | 4 ++-- application/helper/FileUtils.php | 2 +- tests/helper/FileUtilsTest.php | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index 85654a43..bfc99422 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; -use Shaarli\ApplicationUtils; -use Shaarli\FileUtils; +use Shaarli\Helper\ApplicationUtils; +use Shaarli\Helper\FileUtils; use Slim\Http\Request; use Slim\Http\Response; diff --git a/application/helper/FileUtils.php b/application/helper/FileUtils.php index 2d50d850..2eac0793 100644 --- a/application/helper/FileUtils.php +++ b/application/helper/FileUtils.php @@ -133,7 +133,7 @@ class FileUtils */ public static function isPathInShaarliFolder(string $path): bool { - $rootDirectory = dirname(dirname(__FILE__)); + $rootDirectory = dirname(dirname(dirname(__FILE__))); return strpos(realpath($path), $rootDirectory) !== false; } diff --git a/tests/helper/FileUtilsTest.php b/tests/helper/FileUtilsTest.php index 948e46d1..8035f79c 100644 --- a/tests/helper/FileUtilsTest.php +++ b/tests/helper/FileUtilsTest.php @@ -4,6 +4,7 @@ namespace Shaarli\Helper; use Exception; use Shaarli\Exceptions\IOException; +use Shaarli\TestCase; /** * Class FileUtilsTest From 5d8de7587d67b5c3e5d1fed8562d9b87ecde80c1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 10 Oct 2020 17:40:26 +0200 Subject: [PATCH 10/50] Feature: bulk creation of bookmarks This changes creates a new form in addlink page allowing to create multiple bookmarks at once more easily. It focuses on re-using as much existing code and template component as possible. These changes includes: - a new form in addlink (hidden behind a button by default), containing a text area for URL, and tags/private status to apply to created links - this form displays a new template called editlink.batch, itself including editlink template multiple times - User interation in this new templates are handle by a new JS script (shaare-batch.js) making AJAX requests, and therefore does not need page reloading - ManageShaareController has been split into 3 distinct controllers: + ShaareAdd: displays addlink template + ShaareManage: various operation applied on existing shaares (change visibility, pin, deletion, etc.) + ShaarePublish: handles creation/edit forms and saving Shaare's form - Updated translations Fixes #137 --- .../admin/ManageShaareController.php | 386 ------------------ .../controller/admin/ShaareAddController.php | 34 ++ .../admin/ShaareManageController.php | 202 +++++++++ .../admin/ShaarePublishController.php | 222 ++++++++++ application/render/TemplatePage.php | 1 + assets/common/js/metadata.js | 50 +-- assets/common/js/shaare-batch.js | 107 +++++ assets/default/js/base.js | 21 + assets/default/scss/shaarli.scss | 49 +++ inc/languages/fr/LC_MESSAGES/shaarli.po | 109 +++-- index.php | 17 +- .../AddShaareTest.php | 47 --- .../admin/ShaareAddControllerTest.php | 97 +++++ .../ChangeVisibilityBookmarkTest.php | 8 +- .../DeleteBookmarkTest.php | 8 +- .../PinBookmarkTest.php | 8 +- .../SharePrivateTest.php | 8 +- .../DisplayCreateBatchFormTest.php | 62 +++ .../DisplayCreateFormTest.php | 8 +- .../DisplayEditFormTest.php | 8 +- .../SaveBookmarkTest.php | 8 +- tpl/default/addlink.html | 56 +++ tpl/default/editlink.batch.html | 23 ++ tpl/default/editlink.html | 15 + webpack.config.js | 1 + 25 files changed, 1028 insertions(+), 527 deletions(-) delete mode 100644 application/front/controller/admin/ManageShaareController.php create mode 100644 application/front/controller/admin/ShaareAddController.php create mode 100644 application/front/controller/admin/ShaareManageController.php create mode 100644 application/front/controller/admin/ShaarePublishController.php create mode 100644 assets/common/js/shaare-batch.js delete mode 100644 tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php create mode 100644 tests/front/controller/admin/ShaareAddControllerTest.php rename tests/front/controller/admin/{ManageShaareControllerTest => ShaareManageControllerTest}/ChangeVisibilityBookmarkTest.php (98%) rename tests/front/controller/admin/{ManageShaareControllerTest => ShaareManageControllerTest}/DeleteBookmarkTest.php (98%) rename tests/front/controller/admin/{ManageShaareControllerTest => ShaareManageControllerTest}/PinBookmarkTest.php (95%) rename tests/front/controller/admin/{ManageShaareControllerTest => ShaareManageControllerTest}/SharePrivateTest.php (94%) create mode 100644 tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php rename tests/front/controller/admin/{ManageShaareControllerTest => ShaarePublishControllerTest}/DisplayCreateFormTest.php (98%) rename tests/front/controller/admin/{ManageShaareControllerTest => ShaarePublishControllerTest}/DisplayEditFormTest.php (95%) rename tests/front/controller/admin/{ManageShaareControllerTest => ShaarePublishControllerTest}/SaveBookmarkTest.php (98%) create mode 100644 tpl/default/editlink.batch.html diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php deleted file mode 100644 index e490f85a..00000000 --- a/application/front/controller/admin/ManageShaareController.php +++ /dev/null @@ -1,386 +0,0 @@ -assignView( - 'pagetitle', - t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - return $response->write($this->render(TemplatePage::ADDLINK)); - } - - /** - * GET /admin/shaare - Displays the bookmark form for creation. - * Note that if the URL is found in existing bookmarks, then it will be in edit mode. - */ - public function displayCreateForm(Request $request, Response $response): Response - { - $url = cleanup_url($request->getParam('post')); - - $linkIsNew = false; - // Check if URL is not already in database (in this case, we will edit the existing link) - $bookmark = $this->container->bookmarkService->findByUrl($url); - if (null === $bookmark) { - $linkIsNew = true; - // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). - $title = $request->getParam('title'); - $description = $request->getParam('description'); - $tags = $request->getParam('tags'); - $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); - - // If this is an HTTP(S) link, we try go get the page to extract - // the title (otherwise we will to straight to the edit form.) - if (true !== $this->container->conf->get('general.enable_async_metadata', true) - && empty($title) - && strpos(get_url_scheme($url) ?: '', 'http') !== false - ) { - $metadata = $this->container->metadataRetriever->retrieve($url); - } - - if (empty($url)) { - $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); - } - - $link = [ - 'title' => $title ?? $metadata['title'] ?? '', - 'url' => $url ?? '', - 'description' => $description ?? $metadata['description'] ?? '', - 'tags' => $tags ?? $metadata['tags'] ?? '', - 'private' => $private, - ]; - } else { - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $link = $formatter->format($bookmark); - } - - return $this->displayForm($link, $linkIsNew, $request, $response); - } - - /** - * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. - */ - public function displayEditForm(Request $request, Response $response, array $args): Response - { - $id = $args['id'] ?? ''; - try { - if (false === ctype_digit($id)) { - throw new BookmarkNotFoundException(); - } - $bookmark = $this->container->bookmarkService->get((int) $id); // Read database - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(sprintf( - t('Bookmark with identifier %s could not be found.'), - $id - )); - - return $this->redirect($response, '/'); - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $link = $formatter->format($bookmark); - - return $this->displayForm($link, false, $request, $response); - } - - /** - * POST /admin/shaare - */ - public function save(Request $request, Response $response): Response - { - $this->checkToken($request); - - // lf_id should only be present if the link exists. - $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; - if (null !== $id && true === $this->container->bookmarkService->exists($id)) { - // Edit - $bookmark = $this->container->bookmarkService->get($id); - } else { - // New link - $bookmark = new Bookmark(); - } - - $bookmark->setTitle($request->getParam('lf_title')); - $bookmark->setDescription($request->getParam('lf_description')); - $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); - $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); - $bookmark->setTagsString($request->getParam('lf_tags')); - - if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE - && true !== $this->container->conf->get('general.enable_async_metadata', true) - && $bookmark->shouldUpdateThumbnail() - ) { - $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); - } - $this->container->bookmarkService->addOrSet($bookmark, false); - - // To preserve backward compatibility with 3rd parties, plugins still use arrays - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $data = $formatter->format($bookmark); - $this->executePageHooks('save_link', $data); - - $bookmark->fromArray($data); - $this->container->bookmarkService->set($bookmark); - - // If we are called from the bookmarklet, we must close the popup: - if ($request->getParam('source') === 'bookmarklet') { - return $response->write(''); - } - - if (!empty($request->getParam('returnurl'))) { - $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); - } - - return $this->redirectFromReferer( - $request, - $response, - ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], - $bookmark->getShortUrl() - ); - } - - /** - * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). - */ - public function deleteBookmark(Request $request, Response $response): Response - { - $this->checkToken($request); - - $ids = escape(trim($request->getParam('id') ?? '')); - if (empty($ids) || strpos($ids, ' ') !== false) { - // multiple, space-separated ids provided - $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); - } else { - $ids = [$ids]; - } - - // assert at least one id is given - if (0 === count($ids)) { - $this->saveErrorMessage(t('Invalid bookmark ID provided.')); - - return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $count = 0; - foreach ($ids as $id) { - try { - $bookmark = $this->container->bookmarkService->get((int) $id); - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(sprintf( - t('Bookmark with identifier %s could not be found.'), - $id - )); - - continue; - } - - $data = $formatter->format($bookmark); - $this->executePageHooks('delete_link', $data); - $this->container->bookmarkService->remove($bookmark, false); - ++ $count; - } - - if ($count > 0) { - $this->container->bookmarkService->save(); - } - - // If we are called from the bookmarklet, we must close the popup: - if ($request->getParam('source') === 'bookmarklet') { - return $response->write(''); - } - - // Don't redirect to where we were previously because the datastore has changed. - return $this->redirect($response, '/'); - } - - /** - * GET /admin/shaare/visibility - * - * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). - */ - public function changeVisibility(Request $request, Response $response): Response - { - $this->checkToken($request); - - $ids = trim(escape($request->getParam('id') ?? '')); - if (empty($ids) || strpos($ids, ' ') !== false) { - // multiple, space-separated ids provided - $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); - } else { - // only a single id provided - $ids = [$ids]; - } - - // assert at least one id is given - if (0 === count($ids)) { - $this->saveErrorMessage(t('Invalid bookmark ID provided.')); - - return $this->redirectFromReferer($request, $response, [], ['change_visibility']); - } - - // assert that the visibility is valid - $visibility = $request->getParam('newVisibility'); - if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { - $this->saveErrorMessage(t('Invalid visibility provided.')); - - return $this->redirectFromReferer($request, $response, [], ['change_visibility']); - } else { - $isPrivate = $visibility === 'private'; - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $count = 0; - - foreach ($ids as $id) { - try { - $bookmark = $this->container->bookmarkService->get((int) $id); - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(sprintf( - t('Bookmark with identifier %s could not be found.'), - $id - )); - - continue; - } - - $bookmark->setPrivate($isPrivate); - - // To preserve backward compatibility with 3rd parties, plugins still use arrays - $data = $formatter->format($bookmark); - $this->executePageHooks('save_link', $data); - $bookmark->fromArray($data); - - $this->container->bookmarkService->set($bookmark, false); - ++$count; - } - - if ($count > 0) { - $this->container->bookmarkService->save(); - } - - return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); - } - - /** - * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. - */ - public function pinBookmark(Request $request, Response $response, array $args): Response - { - $this->checkToken($request); - - $id = $args['id'] ?? ''; - try { - if (false === ctype_digit($id)) { - throw new BookmarkNotFoundException(); - } - $bookmark = $this->container->bookmarkService->get((int) $id); // Read database - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(sprintf( - t('Bookmark with identifier %s could not be found.'), - $id - )); - - return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - - $bookmark->setSticky(!$bookmark->isSticky()); - - // To preserve backward compatibility with 3rd parties, plugins still use arrays - $data = $formatter->format($bookmark); - $this->executePageHooks('save_link', $data); - $bookmark->fromArray($data); - - $this->container->bookmarkService->set($bookmark); - - return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); - } - - /** - * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL. - */ - public function sharePrivate(Request $request, Response $response, array $args): Response - { - $this->checkToken($request); - - $hash = $args['hash'] ?? ''; - $bookmark = $this->container->bookmarkService->findByHash($hash); - - if ($bookmark->isPrivate() !== true) { - return $this->redirect($response, '/shaare/' . $hash); - } - - if (empty($bookmark->getAdditionalContentEntry('private_key'))) { - $privateKey = bin2hex(random_bytes(16)); - $bookmark->addAdditionalContentEntry('private_key', $privateKey); - $this->container->bookmarkService->set($bookmark); - } - - return $this->redirect( - $response, - '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key') - ); - } - - /** - * Helper function used to display the shaare form whether it's a new or existing bookmark. - * - * @param array $link data used in template, either from parameters or from the data store - */ - protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response - { - $tags = $this->container->bookmarkService->bookmarksCountPerTag(); - if ($this->container->conf->get('formatter') === 'markdown') { - $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; - } - - $data = escape([ - 'link' => $link, - 'link_is_new' => $isNew, - 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', - 'source' => $request->getParam('source') ?? '', - 'tags' => $tags, - 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), - 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), - 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), - ]); - - $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); - - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } - - $editLabel = false === $isNew ? t('Edit') .' ' : ''; - $this->assignView( - 'pagetitle', - $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - return $response->write($this->render(TemplatePage::EDIT_LINK)); - } -} diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php new file mode 100644 index 00000000..8dc386b2 --- /dev/null +++ b/application/front/controller/admin/ShaareAddController.php @@ -0,0 +1,34 @@ +container->bookmarkService->bookmarksCountPerTag(); + if ($this->container->conf->get('formatter') === 'markdown') { + $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + + $this->assignView( + 'pagetitle', + t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + $this->assignView('tags', $tags); + $this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false)); + $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true)); + + return $response->write($this->render(TemplatePage::ADDLINK)); + } +} diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php new file mode 100644 index 00000000..7ceb8d8a --- /dev/null +++ b/application/front/controller/admin/ShaareManageController.php @@ -0,0 +1,202 @@ +checkToken($request); + + $ids = escape(trim($request->getParam('id') ?? '')); + if (empty($ids) || strpos($ids, ' ') !== false) { + // multiple, space-separated ids provided + $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); + } else { + $ids = [$ids]; + } + + // assert at least one id is given + if (0 === count($ids)) { + $this->saveErrorMessage(t('Invalid bookmark ID provided.')); + + return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $count = 0; + foreach ($ids as $id) { + try { + $bookmark = $this->container->bookmarkService->get((int) $id); + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + continue; + } + + $data = $formatter->format($bookmark); + $this->executePageHooks('delete_link', $data); + $this->container->bookmarkService->remove($bookmark, false); + ++ $count; + } + + if ($count > 0) { + $this->container->bookmarkService->save(); + } + + // If we are called from the bookmarklet, we must close the popup: + if ($request->getParam('source') === 'bookmarklet') { + return $response->write(''); + } + + // Don't redirect to where we were previously because the datastore has changed. + return $this->redirect($response, '/'); + } + + /** + * GET /admin/shaare/visibility + * + * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). + */ + public function changeVisibility(Request $request, Response $response): Response + { + $this->checkToken($request); + + $ids = trim(escape($request->getParam('id') ?? '')); + if (empty($ids) || strpos($ids, ' ') !== false) { + // multiple, space-separated ids provided + $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); + } else { + // only a single id provided + $ids = [$ids]; + } + + // assert at least one id is given + if (0 === count($ids)) { + $this->saveErrorMessage(t('Invalid bookmark ID provided.')); + + return $this->redirectFromReferer($request, $response, [], ['change_visibility']); + } + + // assert that the visibility is valid + $visibility = $request->getParam('newVisibility'); + if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { + $this->saveErrorMessage(t('Invalid visibility provided.')); + + return $this->redirectFromReferer($request, $response, [], ['change_visibility']); + } else { + $isPrivate = $visibility === 'private'; + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $count = 0; + + foreach ($ids as $id) { + try { + $bookmark = $this->container->bookmarkService->get((int) $id); + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + continue; + } + + $bookmark->setPrivate($isPrivate); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $data = $formatter->format($bookmark); + $this->executePageHooks('save_link', $data); + $bookmark->fromArray($data); + + $this->container->bookmarkService->set($bookmark, false); + ++$count; + } + + if ($count > 0) { + $this->container->bookmarkService->save(); + } + + return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); + } + + /** + * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. + */ + public function pinBookmark(Request $request, Response $response, array $args): Response + { + $this->checkToken($request); + + $id = $args['id'] ?? ''; + try { + if (false === ctype_digit($id)) { + throw new BookmarkNotFoundException(); + } + $bookmark = $this->container->bookmarkService->get((int) $id); // Read database + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + + $bookmark->setSticky(!$bookmark->isSticky()); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $data = $formatter->format($bookmark); + $this->executePageHooks('save_link', $data); + $bookmark->fromArray($data); + + $this->container->bookmarkService->set($bookmark); + + return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); + } + + /** + * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL. + */ + public function sharePrivate(Request $request, Response $response, array $args): Response + { + $this->checkToken($request); + + $hash = $args['hash'] ?? ''; + $bookmark = $this->container->bookmarkService->findByHash($hash); + + if ($bookmark->isPrivate() !== true) { + return $this->redirect($response, '/shaare/' . $hash); + } + + if (empty($bookmark->getAdditionalContentEntry('private_key'))) { + $privateKey = bin2hex(random_bytes(16)); + $bookmark->addAdditionalContentEntry('private_key', $privateKey); + $this->container->bookmarkService->set($bookmark); + } + + return $this->redirect( + $response, + '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key') + ); + } +} diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php new file mode 100644 index 00000000..608f79cf --- /dev/null +++ b/application/front/controller/admin/ShaarePublishController.php @@ -0,0 +1,222 @@ +getParam('post')); + $link = $this->buildLinkDataFromUrl($request, $url); + + return $this->displayForm($link, $link['linkIsNew'], $request, $response); + } + + /** + * POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page. + */ + public function displayCreateBatchForms(Request $request, Response $response): Response + { + $urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls'))); + + $links = []; + foreach ($urls as $url) { + $link = $this->buildLinkDataFromUrl($request, $url); + $data = $this->buildFormData($link, $link['linkIsNew'], $request); + $data['token'] = $this->container->sessionManager->generateToken(); + $data['source'] = 'batch'; + + $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); + + $links[] = $data; + } + + $this->assignView('links', $links); + $this->assignView('batch_mode', true); + $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true)); + + return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH)); + } + + /** + * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. + */ + public function displayEditForm(Request $request, Response $response, array $args): Response + { + $id = $args['id'] ?? ''; + try { + if (false === ctype_digit($id)) { + throw new BookmarkNotFoundException(); + } + $bookmark = $this->container->bookmarkService->get((int) $id); // Read database + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + return $this->redirect($response, '/'); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + + return $this->displayForm($link, false, $request, $response); + } + + /** + * POST /admin/shaare + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + // lf_id should only be present if the link exists. + $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; + if (null !== $id && true === $this->container->bookmarkService->exists($id)) { + // Edit + $bookmark = $this->container->bookmarkService->get($id); + } else { + // New link + $bookmark = new Bookmark(); + } + + $bookmark->setTitle($request->getParam('lf_title')); + $bookmark->setDescription($request->getParam('lf_description')); + $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); + $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); + $bookmark->setTagsString($request->getParam('lf_tags')); + + if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + && true !== $this->container->conf->get('general.enable_async_metadata', true) + && $bookmark->shouldUpdateThumbnail() + ) { + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + } + $this->container->bookmarkService->addOrSet($bookmark, false); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $data = $formatter->format($bookmark); + $this->executePageHooks('save_link', $data); + + $bookmark->fromArray($data); + $this->container->bookmarkService->set($bookmark); + + // If we are called from the bookmarklet, we must close the popup: + if ($request->getParam('source') === 'bookmarklet') { + return $response->write(''); + } elseif ($request->getParam('source') === 'batch') { + return $response; + } + + if (!empty($request->getParam('returnurl'))) { + $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); + } + + return $this->redirectFromReferer( + $request, + $response, + ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], + $bookmark->getShortUrl() + ); + } + + /** + * Helper function used to display the shaare form whether it's a new or existing bookmark. + * + * @param array $link data used in template, either from parameters or from the data store + */ + protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response + { + $data = $this->buildFormData($link, $isNew, $request); + + $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); + + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $editLabel = false === $isNew ? t('Edit') .' ' : ''; + $this->assignView( + 'pagetitle', + $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render(TemplatePage::EDIT_LINK)); + } + + protected function buildLinkDataFromUrl(Request $request, string $url): array + { + // Check if URL is not already in database (in this case, we will edit the existing link) + $bookmark = $this->container->bookmarkService->findByUrl($url); + if (null === $bookmark) { + // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). + $title = $request->getParam('title'); + $description = $request->getParam('description'); + $tags = $request->getParam('tags'); + $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + + // If this is an HTTP(S) link, we try go get the page to extract + // the title (otherwise we will to straight to the edit form.) + if (true !== $this->container->conf->get('general.enable_async_metadata', true) + && empty($title) + && strpos(get_url_scheme($url) ?: '', 'http') !== false + ) { + $metadata = $this->container->metadataRetriever->retrieve($url); + } + + if (empty($url)) { + $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); + } + + return [ + 'title' => $title ?? $metadata['title'] ?? '', + 'url' => $url ?? '', + 'description' => $description ?? $metadata['description'] ?? '', + 'tags' => $tags ?? $metadata['tags'] ?? '', + 'private' => $private, + 'linkIsNew' => true, + ]; + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + $link['linkIsNew'] = false; + + return $link; + } + + protected function buildFormData(array $link, bool $isNew, Request $request): array + { + $tags = $this->container->bookmarkService->bookmarksCountPerTag(); + if ($this->container->conf->get('formatter') === 'markdown') { + $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + + return escape([ + 'link' => $link, + 'link_is_new' => $isNew, + 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', + 'source' => $request->getParam('source') ?? '', + 'tags' => $tags, + 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), + 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), + 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), + ]); + } +} diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php index 8af8228a..03b424f3 100644 --- a/application/render/TemplatePage.php +++ b/application/render/TemplatePage.php @@ -14,6 +14,7 @@ interface TemplatePage public const DAILY = 'daily'; public const DAILY_RSS = 'dailyrss'; public const EDIT_LINK = 'editlink'; + public const EDIT_LINK_BATCH = 'editlink.batch'; public const ERROR = 'error'; public const EXPORT = 'export'; public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks'; diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js index 2b013364..d5a28a35 100644 --- a/assets/common/js/metadata.js +++ b/assets/common/js/metadata.js @@ -56,37 +56,41 @@ function updateThumb(basePath, divElement, id) { (() => { const basePath = document.querySelector('input[name="js_base_path"]').value; - const loaders = document.querySelectorAll('.loading-input'); /* * METADATA FOR EDIT BOOKMARK PAGE */ - const inputTitle = document.querySelector('input[name="lf_title"]'); - if (inputTitle != null) { - if (inputTitle.value.length > 0) { - clearLoaders(loaders); - return; - } + const inputTitles = document.querySelectorAll('input[name="lf_title"]'); + if (inputTitles != null) { + [...inputTitles].forEach((inputTitle) => { + const form = inputTitle.closest('form[name="linkform"]'); + const loaders = form.querySelectorAll('.loading-input'); - const url = document.querySelector('input[name="lf_url"]').value; + if (inputTitle.value.length > 0) { + clearLoaders(loaders); + return; + } - const xhr = new XMLHttpRequest(); - xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onload = () => { - const result = JSON.parse(xhr.response); - Object.keys(result).forEach((key) => { - if (result[key] !== null && result[key].length) { - const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); - if (element != null && element.value.length === 0) { - element.value = he.decode(result[key]); + const url = form.querySelector('input[name="lf_url"]').value; + + const xhr = new XMLHttpRequest(); + xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.onload = () => { + const result = JSON.parse(xhr.response); + Object.keys(result).forEach((key) => { + if (result[key] !== null && result[key].length) { + const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); + if (element != null && element.value.length === 0) { + element.value = he.decode(result[key]); + } } - } - }); - clearLoaders(loaders); - }; + }); + clearLoaders(loaders); + }; - xhr.send(); + xhr.send(); + }); } /* diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js new file mode 100644 index 00000000..9f612993 --- /dev/null +++ b/assets/common/js/shaare-batch.js @@ -0,0 +1,107 @@ +const sendBookmarkForm = (basePath, formElement) => { + const inputs = formElement + .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]'); + + const formData = new FormData(); + [...inputs].forEach((input) => { + formData.append(input.getAttribute('name'), input.value); + }); + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${basePath}/admin/shaare`); + xhr.onload = () => { + if (xhr.status !== 200) { + alert(`An error occurred. Return code: ${xhr.status}`); + reject(); + } else { + formElement.remove(); + resolve(); + } + }; + xhr.send(formData); + }); +}; + +const sendBookmarkDelete = (buttonElement, formElement) => ( + new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', buttonElement.href); + xhr.onload = () => { + if (xhr.status !== 200) { + alert(`An error occurred. Return code: ${xhr.status}`); + reject(); + } else { + formElement.remove(); + resolve(); + } + }; + xhr.send(); + }) +); + +const redirectIfEmptyBatch = (basePath, formElements, path) => { + if (formElements == null || formElements.length === 0) { + window.location.href = `${basePath}${path}`; + } +}; + +(() => { + const basePath = document.querySelector('input[name="js_base_path"]').value; + const getForms = () => document.querySelectorAll('form[name="linkform"]'); + + const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]'); + if (cancelButtons != null) { + [...cancelButtons].forEach((cancelButton) => { + cancelButton.addEventListener('click', (e) => { + e.preventDefault(); + e.target.closest('form[name="linkform"]').remove(); + redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare'); + }); + }); + } + + const saveButtons = document.querySelectorAll('[name="save_edit"]'); + if (saveButtons != null) { + [...saveButtons].forEach((saveButton) => { + saveButton.addEventListener('click', (e) => { + e.preventDefault(); + + const formElement = e.target.closest('form[name="linkform"]'); + sendBookmarkForm(basePath, formElement) + .then(() => redirectIfEmptyBatch(basePath, getForms(), '/')); + }); + }); + } + + const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]'); + if (saveAllButtons != null) { + [...saveAllButtons].forEach((saveAllButton) => { + saveAllButton.addEventListener('click', (e) => { + e.preventDefault(); + + const promises = []; + [...getForms()].forEach((formElement) => { + promises.push(sendBookmarkForm(basePath, formElement)); + }); + + Promise.all(promises).then(() => { + window.location.href = basePath || '/'; + }); + }); + }); + } + + const deleteButtons = document.querySelectorAll('[name="delete_link"]'); + if (deleteButtons != null) { + [...deleteButtons].forEach((deleteButton) => { + deleteButton.addEventListener('click', (e) => { + e.preventDefault(); + + const formElement = e.target.closest('form[name="linkform"]'); + sendBookmarkDelete(e.target, formElement) + .then(() => redirectIfEmptyBatch(basePath, getForms(), '/')); + }); + }); + } +})(); diff --git a/assets/default/js/base.js b/assets/default/js/base.js index 7f6b9637..9161b4fc 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js @@ -634,4 +634,25 @@ function init(description) { }); }); } + + const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block'); + if (bulkCreationButton != null) { + const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => { + if (bulkCreationButton.classList.contains('pure-u-0')) { + showMoreBlockElement.classList.remove('pure-u-0'); + formElement.classList.add('pure-u-0'); + } else { + showMoreBlockElement.classList.add('pure-u-0'); + formElement.classList.remove('pure-u-0'); + } + }; + + const bulkCreationForm = document.querySelector('.addlink-batch-form-block'); + + toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm); + bulkCreationButton.querySelector('a').addEventListener('click', (e) => { + e.preventDefault(); + toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm); + }); + } })(); diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index 7dc61903..7c85dee8 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss @@ -1023,6 +1023,10 @@ body, &.button-red { background: $red; } + + &.button-grey { + background: $light-grey; + } } .submit-buttons { @@ -1083,6 +1087,11 @@ body, position: absolute; right: 5%; } + + &.button-grey { + position: absolute; + left: 5%; + } } } } @@ -1750,6 +1759,46 @@ form { } } +// Batch creation +input[name='save_edit_batch'] { + @extend %page-form-button; +} + +.addlink-batch-show-more { + display: flex; + align-items: center; + margin: 20px 0 8px; + + a { + color: var(--main-color); + text-decoration: none; + } + + &::before, + &::after { + content: ""; + flex-grow: 1; + background: rgba(0, 0, 0, 0.35); + height: 1px; + font-size: 0; + line-height: 0; + } + + &::before { + margin: 0 16px 0 0; + } + + &::after { + margin: 0 0 0 16px; + } +} + +.addlink-batch-form-block { + .pure-alert { + margin: 25px 0 0 0; + } +} + // Print rules @media print { .shaarli-menu { diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 6d4ff0bd..60ea7a97 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -347,43 +347,16 @@ msgstr "" "le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " "légères." -#: application/front/controller/admin/ManageShaareController.php:29 -#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 -msgid "Shaare a new link" -msgstr "Partager un nouveau lien" - #: application/front/controller/admin/ManageShaareController.php:64 -msgid "Note: " -msgstr "Note : " - #: application/front/controller/admin/ManageShaareController.php:95 #: application/front/controller/admin/ManageShaareController.php:193 #: application/front/controller/admin/ManageShaareController.php:262 #: application/front/controller/admin/ManageShaareController.php:302 -#, php-format -msgid "Bookmark with identifier %s could not be found." -msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." - #: application/front/controller/admin/ManageShaareController.php:181 #: application/front/controller/admin/ManageShaareController.php:239 -msgid "Invalid bookmark ID provided." -msgstr "ID du lien non valide." - #: application/front/controller/admin/ManageShaareController.php:247 -msgid "Invalid visibility provided." -msgstr "Visibilité du lien non valide." - #: application/front/controller/admin/ManageShaareController.php:378 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 -msgid "Edit" -msgstr "Modifier" - #: application/front/controller/admin/ManageShaareController.php:381 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 -msgid "Shaare" -msgstr "Shaare" - #: application/front/controller/admin/ManageTagController.php:29 #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 @@ -456,6 +429,29 @@ msgstr "Le cache des miniatures a été vidé." msgid "Shaarli's cache folder has been cleared!" msgstr "Le dossier de cache de Shaarli a été vidé !" +#, php-format +msgid "Bookmark with identifier %s could not be found." +msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." + +#: application/front/controller/admin/ShaareManageController.php:101 +msgid "Invalid visibility provided." +msgstr "Visibilité du lien non valide." + +#: application/front/controller/admin/ShaarePublishController.php:154 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 +msgid "Edit" +msgstr "Modifier" + +#: application/front/controller/admin/ShaarePublishController.php:157 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 +msgid "Shaare" +msgstr "Shaare" + +#: application/front/controller/admin/ShaarePublishController.php:184 +msgid "Note: " +msgstr "Note : " + #: application/front/controller/admin/ThumbnailsController.php:37 #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 msgid "Thumbnails update" @@ -941,6 +937,48 @@ msgstr "Désolé, il y a rien à voir ici." msgid "URL or leave empty to post a note" msgstr "URL ou laisser vide pour créer une note" +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "BULK CREATION" +msgstr "CRÉATION DE MASSE" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "Metadata asynchronous retrieval is disabled." +msgstr "La récupération asynchrone des meta-données est désactivée." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "" +"We recommend that you enable the setting general > " +"enable_async_metadata in your configuration file to use bulk link " +"creation." +msgstr "" +"Nous recommandons d'activer le paramètre general > " +"enable_async_metadata dans votre fichier de configuration pour utiliser " +"la création de masse." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +msgid "Shaare multiple new links" +msgstr "Partagez plusieurs nouveaux liens" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 +msgid "Add one URL per line to create multiple bookmarks." +msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Tags" +msgstr "Tags" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Private" +msgstr "Privé" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +msgid "Add links" +msgstr "Ajouter des liens" + #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 msgid "Current password" msgstr "Mot de passe actuel" @@ -1187,15 +1225,7 @@ msgid "Description" msgstr "Description" #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 -msgid "Tags" -msgstr "Tags" - #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 -#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 -msgid "Private" -msgstr "Privé" - #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 msgid "Description will be rendered with" msgstr "La description sera générée avec" @@ -1209,9 +1239,18 @@ msgid "Markdown syntax" msgstr "la syntaxe Markdown" #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "Cancel" +msgstr "Annuler" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 msgid "Apply Changes" msgstr "Appliquer les changements" +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Save all" +msgstr "Tout enregistrer" + #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 diff --git a/index.php b/index.php index 0ed52bad..4b5602ac 100644 --- a/index.php +++ b/index.php @@ -125,14 +125,15 @@ $app->group('/admin', function () { $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save'); $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index'); $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save'); - $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare'); - $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm'); - $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm'); - $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ManageShaareController:sharePrivate'); - $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); - $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); - $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); - $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); + $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare'); + $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm'); + $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm'); + $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate'); + $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms'); + $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save'); + $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark'); + $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility'); + $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark'); $this->patch( '/shaare/{id:[0-9]+}/update-thumbnail', '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate' diff --git a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php deleted file mode 100644 index 0f27ec2f..00000000 --- a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php +++ /dev/null @@ -1,47 +0,0 @@ -createContainer(); - - $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); - } - - /** - * Test displaying add link page - */ - public function testAddShaare(): void - { - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->addShaare($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('addlink', (string) $result->getBody()); - - static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']); - } -} diff --git a/tests/front/controller/admin/ShaareAddControllerTest.php b/tests/front/controller/admin/ShaareAddControllerTest.php new file mode 100644 index 00000000..a27ebe64 --- /dev/null +++ b/tests/front/controller/admin/ShaareAddControllerTest.php @@ -0,0 +1,97 @@ +createContainer(); + + $this->container->httpAccess = $this->createMock(HttpAccess::class); + $this->controller = new ShaareAddController($this->container); + } + + /** + * Test displaying add link page + */ + public function testAddShaare(): void + { + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $expectedTags = [ + 'tag1' => 32, + 'tag2' => 24, + 'tag3' => 1, + ]; + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->willReturn($expectedTags) + ; + $expectedTags = array_merge($expectedTags, [BookmarkMarkdownFormatter::NO_MD_TAG => 1]); + + $this->container->conf = $this->createMock(ConfigManager::class); + $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { + return $key === 'formatter' ? 'markdown' : $default; + }); + + $result = $this->controller->addShaare($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('addlink', (string) $result->getBody()); + + static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']); + static::assertFalse($assignedVariables['default_private_links']); + static::assertTrue($assignedVariables['async_metadata']); + static::assertSame($expectedTags, $assignedVariables['tags']); + } + + /** + * Test displaying add link page + */ + public function testAddShaareWithoutMd(): void + { + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $expectedTags = [ + 'tag1' => 32, + 'tag2' => 24, + 'tag3' => 1, + ]; + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->willReturn($expectedTags) + ; + + $result = $this->controller->addShaare($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('addlink', (string) $result->getBody()); + + static::assertSame($expectedTags, $assignedVariables['tags']); + } +} diff --git a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php similarity index 98% rename from tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php rename to tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php index 096d0774..28b1c023 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; @@ -10,7 +10,7 @@ use Shaarli\Formatter\BookmarkFormatter; use Shaarli\Formatter\BookmarkRawFormatter; use Shaarli\Formatter\FormatterFactory; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaareManageController; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; use Shaarli\TestCase; @@ -21,7 +21,7 @@ class ChangeVisibilityBookmarkTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaareManageController */ protected $controller; public function setUp(): void @@ -29,7 +29,7 @@ class ChangeVisibilityBookmarkTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaareManageController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php similarity index 98% rename from tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php rename to tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php index 83bbee7c..770a16d7 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Formatter\BookmarkFormatter; use Shaarli\Formatter\FormatterFactory; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaareManageController; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; use Shaarli\TestCase; @@ -20,7 +20,7 @@ class DeleteBookmarkTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaareManageController */ protected $controller; public function setUp(): void @@ -28,7 +28,7 @@ class DeleteBookmarkTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaareManageController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php similarity index 95% rename from tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php rename to tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php index 50ce7df1..b89206ce 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaareManageController; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; use Shaarli\TestCase; @@ -18,7 +18,7 @@ class PinBookmarkTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaareManageController */ protected $controller; public function setUp(): void @@ -26,7 +26,7 @@ class PinBookmarkTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaareManageController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php similarity index 94% rename from tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php rename to tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php index 1e7877c7..ae61dfb7 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaareManageController; use Shaarli\Http\HttpAccess; use Shaarli\TestCase; use Slim\Http\Request; @@ -19,7 +19,7 @@ class SharePrivateTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaareManageController */ protected $controller; public function setUp(): void @@ -27,7 +27,7 @@ class SharePrivateTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaareManageController($this->container); } /** diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php new file mode 100644 index 00000000..34547120 --- /dev/null +++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php @@ -0,0 +1,62 @@ +createContainer(); + + $this->container->httpAccess = $this->createMock(HttpAccess::class); + $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class); + $this->controller = new ShaarePublishController($this->container); + } + + /** + * TODO + */ + public function testDisplayCreateFormBatch(): void + { + $urls = [ + 'https://domain1.tld/url1', + 'https://domain2.tld/url2', + 'https://domain3.tld/url3', + ]; + + $request = $this->createMock(Request::class); + $request->method('getParam')->willReturnCallback(function (string $key) use ($urls): ?string { + return $key === 'urls' ? implode(PHP_EOL, $urls) : null; + }); + $response = new Response(); + + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->displayCreateBatchForms($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('editlink.batch', (string) $result->getBody()); + + static::assertTrue($assignedVariables['batch_mode']); + static::assertCount(3, $assignedVariables['links']); + static::assertSame($urls[0], $assignedVariables['links'][0]['link']['url']); + static::assertSame($urls[1], $assignedVariables['links'][1]['link']['url']); + static::assertSame($urls[2], $assignedVariables['links'][2]['link']['url']); + } +} diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php similarity index 98% rename from tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php rename to tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php index eafa54eb..f20b1def 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php +++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Config\ConfigManager; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaarePublishController; use Shaarli\Http\HttpAccess; use Shaarli\Http\MetadataRetriever; use Shaarli\TestCase; @@ -18,7 +18,7 @@ class DisplayCreateFormTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaarePublishController */ protected $controller; public function setUp(): void @@ -27,7 +27,7 @@ class DisplayCreateFormTest extends TestCase $this->container->httpAccess = $this->createMock(HttpAccess::class); $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaarePublishController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php similarity index 95% rename from tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php rename to tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php index 2dc3f41c..da393e49 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php +++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaarePublishController; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; use Shaarli\TestCase; @@ -18,7 +18,7 @@ class DisplayEditFormTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaarePublishController */ protected $controller; public function setUp(): void @@ -26,7 +26,7 @@ class DisplayEditFormTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaarePublishController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php similarity index 98% rename from tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php rename to tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php index 1adeef5a..b6a861bc 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php +++ b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Config\ConfigManager; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaarePublishController; use Shaarli\Front\Exception\WrongTokenException; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; @@ -20,7 +20,7 @@ class SaveBookmarkTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaarePublishController */ protected $controller; public function setUp(): void @@ -28,7 +28,7 @@ class SaveBookmarkTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaarePublishController($this->container); } /** diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html index 67d3ebd1..7d4bc9e6 100644 --- a/tpl/default/addlink.html +++ b/tpl/default/addlink.html @@ -20,6 +20,62 @@
+ + + + + {include="page.footer"} diff --git a/tpl/default/editlink.batch.html b/tpl/default/editlink.batch.html new file mode 100644 index 00000000..71985c1a --- /dev/null +++ b/tpl/default/editlink.batch.html @@ -0,0 +1,23 @@ + + + + {include="includes"} + + +{include="page.header"} + +
+ +
+ +{loop="$links"} + {include="editlink"} +{/loop} + +
+ +
+ +{include="page.footer"} +{if="$async_metadata"}{/if} + diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html index 7ab7e1fe..980b2b8e 100644 --- a/tpl/default/editlink.html +++ b/tpl/default/editlink.html @@ -1,3 +1,4 @@ +{if="empty($batch_mode)"} @@ -5,6 +6,10 @@ {include="page.header"} +{else} + {ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore} + {function="extract($value) ? '' : ''"} +{/if} + +{if="empty($batch_mode)"} {include="page.footer"} {if="$link_is_new && $async_metadata"}{/if} +{/if} diff --git a/webpack.config.js b/webpack.config.js index 8e3d1470..a4aa633e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,6 +18,7 @@ module.exports = [ { mode: 'production', entry: { + shaare_batch: './assets/common/js/shaare-batch.js', thumbnails: './assets/common/js/thumbnails.js', thumbnails_update: './assets/common/js/thumbnails-update.js', metadata: './assets/common/js/metadata.js', From 25e90d8d75382721ff7473fa1686090fcfeb46ff Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 11 Oct 2020 13:34:38 +0200 Subject: [PATCH 11/50] Bulk creation: fix private status based on the first form --- .../front/controller/admin/ShaarePublishController.php | 6 +++++- assets/default/js/base.js | 8 ++++++++ tpl/default/addlink.html | 4 ++-- tpl/default/editlink.html | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 608f79cf..fd680ea0 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -169,7 +169,11 @@ class ShaarePublishController extends ShaarliAdminController $title = $request->getParam('title'); $description = $request->getParam('description'); $tags = $request->getParam('tags'); - $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + if ($request->getParam('private') !== null) { + $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + } else { + $private = $this->container->conf->get('privacy.default_private_links', false); + } // If this is an HTTP(S) link, we try go get the page to extract // the title (otherwise we will to straight to the edit form.) diff --git a/assets/default/js/base.js b/assets/default/js/base.js index 9161b4fc..4163577d 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js @@ -654,5 +654,13 @@ function init(description) { e.preventDefault(); toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm); }); + + // Force to send falsy value if the checkbox is not checked. + const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]'); + const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]'); + privateButton.addEventListener('click', () => { + privateHiddenButton.disabled = !privateHiddenButton.disabled; + }); + privateHiddenButton.disabled = privateButton.checked; } })(); diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html index 7d4bc9e6..4aac7ff1 100644 --- a/tpl/default/addlink.html +++ b/tpl/default/addlink.html @@ -62,8 +62,8 @@
- + +  
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html index 980b2b8e..83e541fd 100644 --- a/tpl/default/editlink.html +++ b/tpl/default/editlink.html @@ -65,7 +65,7 @@
  From c609944cb906a2f5002cd86a808aa36d8deb2afd Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 23 Oct 2020 12:29:52 +0200 Subject: [PATCH 12/50] Bulk creation: improve performances using memoization Reduced additional processing time per links from ~40ms to ~5ms --- .../admin/ShaarePublishController.php | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index fd680ea0..65fdcdee 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller\Admin; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; +use Shaarli\Formatter\BookmarkFormatter; use Shaarli\Formatter\BookmarkMarkdownFormatter; use Shaarli\Render\TemplatePage; use Shaarli\Thumbnailer; @@ -14,6 +15,16 @@ use Slim\Http\Response; class ShaarePublishController extends ShaarliAdminController { + /** + * @var BookmarkFormatter[] Statically cached instances of formatters + */ + protected $formatters = []; + + /** + * @var array Statically cached bookmark's tags counts + */ + protected $tags; + /** * GET /admin/shaare - Displays the bookmark form for creation. * Note that if the URL is found in existing bookmarks, then it will be in edit mode. @@ -72,7 +83,7 @@ class ShaarePublishController extends ShaarliAdminController return $this->redirect($response, '/'); } - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $link = $formatter->format($bookmark); return $this->displayForm($link, false, $request, $response); @@ -110,7 +121,7 @@ class ShaarePublishController extends ShaarliAdminController $this->container->bookmarkService->addOrSet($bookmark, false); // To preserve backward compatibility with 3rd parties, plugins still use arrays - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $data = $formatter->format($bookmark); $this->executePageHooks('save_link', $data); @@ -198,7 +209,7 @@ class ShaarePublishController extends ShaarliAdminController ]; } - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $link = $formatter->format($bookmark); $link['linkIsNew'] = false; @@ -207,20 +218,43 @@ class ShaarePublishController extends ShaarliAdminController protected function buildFormData(array $link, bool $isNew, Request $request): array { - $tags = $this->container->bookmarkService->bookmarksCountPerTag(); - if ($this->container->conf->get('formatter') === 'markdown') { - $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; - } - return escape([ 'link' => $link, 'link_is_new' => $isNew, 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', 'source' => $request->getParam('source') ?? '', - 'tags' => $tags, + 'tags' => $this->getTags(), 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), ]); } + + /** + * Memoize formatterFactory->getFormatter() calls. + */ + protected function getFormatter(string $type): BookmarkFormatter + { + if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) { + $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type); + } + + return $this->formatters[$type]; + } + + /** + * Memoize bookmarkService->bookmarksCountPerTag() calls. + */ + protected function getTags(): array + { + if ($this->tags === null) { + $this->tags = $this->container->bookmarkService->bookmarksCountPerTag(); + + if ($this->container->conf->get('formatter') === 'markdown') { + $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + } + + return $this->tags; + } } From 6a716758871885e5bd045d1981f890dbf3343b1d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 23 Oct 2020 12:53:49 +0200 Subject: [PATCH 13/50] Bulk creation: displays a progress bar when saving all displayed forms --- assets/common/js/shaare-batch.js | 22 ++++++++++++++++++---- assets/default/scss/shaarli.scss | 23 +++++++++++++++++++++++ tpl/default/editlink.batch.html | 9 +++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js index 9f612993..557325ee 100644 --- a/assets/common/js/shaare-batch.js +++ b/assets/common/js/shaare-batch.js @@ -15,7 +15,7 @@ const sendBookmarkForm = (basePath, formElement) => { alert(`An error occurred. Return code: ${xhr.status}`); reject(); } else { - formElement.remove(); + formElement.closest('.edit-link-container').remove(); resolve(); } }; @@ -32,7 +32,7 @@ const sendBookmarkDelete = (buttonElement, formElement) => ( alert(`An error occurred. Return code: ${xhr.status}`); reject(); } else { - formElement.remove(); + formElement.closest('.edit-link-container').remove(); resolve(); } }; @@ -80,9 +80,23 @@ const redirectIfEmptyBatch = (basePath, formElements, path) => { saveAllButton.addEventListener('click', (e) => { e.preventDefault(); + const forms = [...getForms()]; + const nbForm = forms.length; + let current = 0; + const progressBar = document.querySelector('.progressbar > div'); + const progressBarCurrent = document.querySelector('.progressbar-current'); + + document.querySelector('.dark-layer').style.display = 'block'; + document.querySelector('.progressbar-max').innerHTML = nbForm; + progressBarCurrent.innerHTML = current; + const promises = []; - [...getForms()].forEach((formElement) => { - promises.push(sendBookmarkForm(basePath, formElement)); + forms.forEach((formElement) => { + promises.push(sendBookmarkForm(basePath, formElement).then(() => { + current += 1; + progressBar.style.width = `${(current * 100) / nbForm}%`; + progressBarCurrent.innerHTML = current; + })); }); Promise.all(promises).then(() => { diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index 7c85dee8..a7f091e9 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss @@ -1793,6 +1793,29 @@ input[name='save_edit_batch'] { } } +.dark-layer { + display: none; + position: fixed; + height: 100%; + width: 100%; + z-index: 998; + background-color: rgba(0, 0, 0, .75); + color: #fff; + + .screen-center { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + min-height: 100vh; + } + + .progressbar { + width: 33%; + } +} + .addlink-batch-form-block { .pure-alert { margin: 25px 0 0 0; diff --git a/tpl/default/editlink.batch.html b/tpl/default/editlink.batch.html index 71985c1a..b1f8e5bd 100644 --- a/tpl/default/editlink.batch.html +++ b/tpl/default/editlink.batch.html @@ -4,6 +4,15 @@ {include="includes"} +
+
+
/
+
+
+
+
+
+ {include="page.header"}
From 34c8f558e595d4f90e46e3753c8455b0b515771a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 23 Oct 2020 13:28:02 +0200 Subject: [PATCH 14/50] Bulk creation: ignore blank lines --- application/front/controller/admin/ShaarePublishController.php | 3 +++ .../ShaarePublishControllerTest/DisplayCreateBatchFormTest.php | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 65fdcdee..ddcffdc7 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -46,6 +46,9 @@ class ShaarePublishController extends ShaarliAdminController $links = []; foreach ($urls as $url) { + if (empty($url)) { + continue; + } $link = $this->buildLinkDataFromUrl($request, $url); $data = $this->buildFormData($link, $link['linkIsNew'], $request); $data['token'] = $this->container->sessionManager->generateToken(); diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php index 34547120..ce8e112b 100644 --- a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php +++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php @@ -36,6 +36,7 @@ class DisplayCreateBatchFormTest extends TestCase $urls = [ 'https://domain1.tld/url1', 'https://domain2.tld/url2', + ' ', 'https://domain3.tld/url3', ]; @@ -57,6 +58,6 @@ class DisplayCreateBatchFormTest extends TestCase static::assertCount(3, $assignedVariables['links']); static::assertSame($urls[0], $assignedVariables['links'][0]['link']['url']); static::assertSame($urls[1], $assignedVariables['links'][1]['link']['url']); - static::assertSame($urls[2], $assignedVariables['links'][2]['link']['url']); + static::assertSame($urls[3], $assignedVariables['links'][2]['link']['url']); } } From 358cb20bcba3cb7b0ce2a3000fb7026465a10386 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 24 Oct 2020 16:25:06 +0200 Subject: [PATCH 15/50] Plugin wallabag: minor improvements - hide the wallabag icon for logged out users - set API V2 as default parameter - fix URL encoding issue with special chars Fixes #1147 --- plugins/wallabag/wallabag.php | 5 ++-- tests/plugins/PluginWallabagTest.php | 35 +++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php index d0df3501..8cd3f4ad 100644 --- a/plugins/wallabag/wallabag.php +++ b/plugins/wallabag/wallabag.php @@ -22,6 +22,7 @@ function wallabag_init($conf) 'Please define the "WALLABAG_URL" setting in the plugin administration page.'); return array($error); } + $conf->setEmpty('plugins.WALLABAG_URL', '2'); } /** @@ -35,7 +36,7 @@ function wallabag_init($conf) function hook_wallabag_render_linklist($data, $conf) { $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); - if (empty($wallabagUrl)) { + if (empty($wallabagUrl) || !$data['_LOGGEDIN_']) { return $data; } @@ -51,7 +52,7 @@ function hook_wallabag_render_linklist($data, $conf) $wallabag = sprintf( $wallabagHtml, $wallabagInstance->getWallabagUrl(), - urlencode($value['url']), + urlencode(unescape($value['url'])), $path, $linkTitle ); diff --git a/tests/plugins/PluginWallabagTest.php b/tests/plugins/PluginWallabagTest.php index 36317215..9a402fb7 100644 --- a/tests/plugins/PluginWallabagTest.php +++ b/tests/plugins/PluginWallabagTest.php @@ -49,14 +49,15 @@ class PluginWallabagTest extends \Shaarli\TestCase $conf = new ConfigManager(''); $conf->set('plugins.WALLABAG_URL', 'value'); $str = 'http://randomstr.com/test'; - $data = array( + $data = [ 'title' => $str, - 'links' => array( - array( + 'links' => [ + [ 'url' => $str, - ) - ) - ); + ] + ], + '_LOGGEDIN_' => true, + ]; $data = hook_wallabag_render_linklist($data, $conf); $link = $data['links'][0]; @@ -69,4 +70,26 @@ class PluginWallabagTest extends \Shaarli\TestCase $this->assertNotFalse(strpos($link['link_plugin'][0], urlencode($str))); $this->assertNotFalse(strpos($link['link_plugin'][0], $conf->get('plugins.WALLABAG_URL'))); } + + /** + * Test render_linklist hook while logged out: no change. + */ + public function testWallabagLinklistLoggedOut(): void + { + $conf = new ConfigManager(''); + $str = 'http://randomstr.com/test'; + $data = [ + 'title' => $str, + 'links' => [ + [ + 'url' => $str, + ] + ], + '_LOGGEDIN_' => false, + ]; + + $result = hook_wallabag_render_linklist($data, $conf); + + static::assertSame($data, $result); + } } From ff9686066e017cc975eeab7da804e7194223ef8c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 28 Oct 2020 12:25:52 +0100 Subject: [PATCH 16/50] Include php-simplexml in Docker image Composer 2.0 is now blocking everything if requirements are not met --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index e2ff71fd..f6120b71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,7 @@ RUN apk --update --no-cache add \ php7-openssl \ php7-session \ php7-xml \ + php7-simplexml \ php7-zlib \ s6 From 156061d445fd23d033a52f84954484a3349c988a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 28 Oct 2020 12:54:52 +0100 Subject: [PATCH 17/50] Raise 404 error instead of 500 if permalink access is denied --- application/bookmark/BookmarkFileService.php | 2 +- tests/bookmark/BookmarkFileServiceTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 0df2f47f..3ea98a45 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -106,7 +106,7 @@ class BookmarkFileService implements BookmarkServiceInterface && $first->isPrivate() && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) ) { - throw new Exception('Not authorized'); + throw new BookmarkNotFoundException(); } return $first; diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php index 8e0ff8dd..f619aff3 100644 --- a/tests/bookmark/BookmarkFileServiceTest.php +++ b/tests/bookmark/BookmarkFileServiceTest.php @@ -886,8 +886,8 @@ class BookmarkFileServiceTest extends TestCase */ public function testFilterHashPrivateWhileLoggedOut() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Not authorized'); + $this->expectException(BookmarkNotFoundException::class); + $this->expectExceptionMessage('The link you are trying to reach does not exist or has been deleted'); $hash = smallHash('20141125_084734' . 6); From d3f6d525253eb7bb041d9436cbf213c10524a85c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 28 Oct 2020 14:02:08 +0100 Subject: [PATCH 18/50] Fix compatiliby issue on login with PHP 7.1 session_set_cookie_params does not return any value in PHP 7.1 --- application/render/PageBuilder.php | 2 +- application/security/SessionManager.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 25e0e284..c2fae705 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -160,7 +160,7 @@ class PageBuilder $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); - $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE']); + $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20); // To be removed with a proper theme configuration. $this->tpl->assign('conf', $this->conf); diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 36df8c1c..96bf193c 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -293,9 +293,12 @@ class SessionManager return session_start(); } - public function cookieParameters(int $lifeTime, string $path, string $domain): bool + /** + * Be careful, return type of session_set_cookie_params() changed between PHP 7.1 and 7.2. + */ + public function cookieParameters(int $lifeTime, string $path, string $domain): void { - return session_set_cookie_params($lifeTime, $path, $domain); + session_set_cookie_params($lifeTime, $path, $domain); } public function regenerateId(bool $deleteOldSession = false): bool From 114a43b20e9a1f83647d4f0f7a001e80a76c75ce Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 28 Oct 2020 14:13:50 +0100 Subject: [PATCH 19/50] Remove unnecessary escape of referer Fixes #1611 --- application/front/controller/admin/ShaarePublishController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index ddcffdc7..18afc2d1 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -139,7 +139,7 @@ class ShaarePublishController extends ShaarliAdminController } if (!empty($request->getParam('returnurl'))) { - $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); + $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); } return $this->redirectFromReferer( From b37ca790729125fa0df956220a4062f1d34c57e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Carr?= Date: Wed, 28 Oct 2020 19:57:40 -0700 Subject: [PATCH 20/50] postLink: change relative path to absolute path --- application/api/controllers/Links.php | 2 +- tests/api/controllers/links/PostLinkTest.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 29247950..16fc8688 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -130,7 +130,7 @@ class Links extends ApiController $this->bookmarkService->add($bookmark); $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment'])); - $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]); + $redirect = $this->ci->router->pathFor('getLink', ['id' => $bookmark->getId()]); return $response->withAddedHeader('Location', $redirect) ->withJson($out, 201, $this->jsonStyle); } diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php index b2dd09eb..969b9fd9 100644 --- a/tests/api/controllers/links/PostLinkTest.php +++ b/tests/api/controllers/links/PostLinkTest.php @@ -90,8 +90,8 @@ class PostLinkTest extends TestCase $mock = $this->createMock(Router::class); $mock->expects($this->any()) - ->method('relativePathFor') - ->willReturn('api/v1/bookmarks/1'); + ->method('pathFor') + ->willReturn('/api/v1/bookmarks/1'); // affect @property-read... seems to work $this->controller->getCi()->router = $mock; @@ -126,7 +126,7 @@ class PostLinkTest extends TestCase $response = $this->controller->postLink($request, new Response()); $this->assertEquals(201, $response->getStatusCode()); - $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); + $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); $data = json_decode((string) $response->getBody(), true); $this->assertEquals(self::NB_FIELDS_LINK, count($data)); $this->assertEquals(43, $data['id']); @@ -171,7 +171,7 @@ class PostLinkTest extends TestCase $response = $this->controller->postLink($request, new Response()); $this->assertEquals(201, $response->getStatusCode()); - $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); + $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); $data = json_decode((string) $response->getBody(), true); $this->assertEquals(self::NB_FIELDS_LINK, count($data)); $this->assertEquals(43, $data['id']); From b862705947ff14f54023ba6d1cbead3435d0f234 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 2 Nov 2020 19:22:50 +0100 Subject: [PATCH 21/50] UT: fix formatting issue when the current day has a single digit --- tests/front/controller/visitor/DailyControllerTest.php | 2 +- tests/helper/DailyPageHelperTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php index 758e7219..70fbce54 100644 --- a/tests/front/controller/visitor/DailyControllerTest.php +++ b/tests/front/controller/visitor/DailyControllerTest.php @@ -327,7 +327,7 @@ class DailyControllerTest extends TestCase static::assertSame(200, $result->getStatusCode()); static::assertSame('daily', (string) $result->getBody()); static::assertCount(0, $assignedVariables['linksToDisplay']); - static::assertSame('Today - ' . (new \DateTime())->format('F d, Y'), $assignedVariables['dayDesc']); + static::assertSame('Today - ' . (new \DateTime())->format('F j, Y'), $assignedVariables['dayDesc']); static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); } diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php index e0378491..5255b7b1 100644 --- a/tests/helper/DailyPageHelperTest.php +++ b/tests/helper/DailyPageHelperTest.php @@ -240,8 +240,8 @@ class DailyPageHelperTest extends TestCase public function getDescriptionsByType(): array { return [ - [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F d, Y')], - [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F d, 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'], From 1a94978e44d0cf93f84ed96413b2f6d06f716685 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 3 Nov 2020 11:58:02 +0100 Subject: [PATCH 22/50] Fix French translation 2 missing key + 1 wrong translation Fixes #1571 --- inc/languages/fr/LC_MESSAGES/shaarli.po | 80 +++++++++++++------------ 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 60ea7a97..d5b01227 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-10-27 19:44+0100\n" -"PO-Revision-Date: 2020-10-27 19:44+0100\n" +"POT-Creation-Date: 2020-11-03 11:51+0100\n" +"PO-Revision-Date: 2020-11-03 11:55+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -347,16 +347,6 @@ msgstr "" "le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " "légères." -#: application/front/controller/admin/ManageShaareController.php:64 -#: application/front/controller/admin/ManageShaareController.php:95 -#: application/front/controller/admin/ManageShaareController.php:193 -#: application/front/controller/admin/ManageShaareController.php:262 -#: application/front/controller/admin/ManageShaareController.php:302 -#: application/front/controller/admin/ManageShaareController.php:181 -#: application/front/controller/admin/ManageShaareController.php:239 -#: application/front/controller/admin/ManageShaareController.php:247 -#: application/front/controller/admin/ManageShaareController.php:378 -#: application/front/controller/admin/ManageShaareController.php:381 #: application/front/controller/admin/ManageTagController.php:29 #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 @@ -429,6 +419,20 @@ msgstr "Le cache des miniatures a été vidé." msgid "Shaarli's cache folder has been cleared!" msgstr "Le dossier de cache de Shaarli a été vidé !" +#: application/front/controller/admin/ShaareAddController.php:26 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +msgid "Shaare a new link" +msgstr "Partagez un nouveau lien" + +#: application/front/controller/admin/ShaareManageController.php:35 +#: application/front/controller/admin/ShaareManageController.php:93 +msgid "Invalid bookmark ID provided." +msgstr "L'ID du marque-page fourni n'est pas valide." + +#: application/front/controller/admin/ShaareManageController.php:47 +#: application/front/controller/admin/ShaareManageController.php:116 +#: application/front/controller/admin/ShaareManageController.php:156 +#: application/front/controller/admin/ShaarePublishController.php:82 #, php-format msgid "Bookmark with identifier %s could not be found." msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." @@ -437,18 +441,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:154 +#: application/front/controller/admin/ShaarePublishController.php:168 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 msgid "Edit" msgstr "Modifier" -#: application/front/controller/admin/ShaarePublishController.php:157 +#: application/front/controller/admin/ShaarePublishController.php:171 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 msgid "Shaare" msgstr "Shaare" -#: application/front/controller/admin/ShaarePublishController.php:184 +#: application/front/controller/admin/ShaarePublishController.php:202 msgid "Note: " msgstr "Note : " @@ -905,7 +909,7 @@ 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:70 +#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:71 msgid "For each link, add a QRCode icon." msgstr "Pour chaque lien, ajouter une icône de QRCode." @@ -917,15 +921,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:47 +#: plugins/wallabag/wallabag.php:48 msgid "Save to wallabag" msgstr "Sauvegarder dans Wallabag" -#: plugins/wallabag/wallabag.php:71 +#: plugins/wallabag/wallabag.php:72 msgid "Wallabag API URL" msgstr "URL de l'API Wallabag" -#: plugins/wallabag/wallabag.php:72 +#: plugins/wallabag/wallabag.php:73 msgid "Wallabag API version (1 or 2)" msgstr "Version de l'API Wallabag (1 ou 2)" @@ -1165,7 +1169,7 @@ msgid "None" msgstr "Aucune" #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 msgid "Save" @@ -1196,27 +1200,27 @@ msgid_plural "" msgstr[0] ":type suivant" msgstr[1] "" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 msgid "Edit Shaare" msgstr "Modifier le Shaare" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 msgid "New Shaare" msgstr "Nouveau Shaare" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 msgid "Created:" msgstr "Création :" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 msgid "URL" msgstr "URL" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 msgid "Title" msgstr "Titre" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 @@ -1224,21 +1228,19 @@ msgstr "Titre" msgid "Description" msgstr "Description" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 msgid "Description will be rendered with" msgstr "La description sera générée avec" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 msgid "Markdown syntax documentation" msgstr "Documentation sur la syntaxe Markdown" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 msgid "Markdown syntax" msgstr "la syntaxe Markdown" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115 msgid "Cancel" msgstr "Annuler" @@ -1246,12 +1248,7 @@ msgstr "Annuler" msgid "Apply Changes" msgstr "Appliquer les changements" -#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 -#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 -msgid "Save all" -msgstr "Tout enregistrer" - -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 @@ -1259,6 +1256,11 @@ msgstr "Tout enregistrer" msgid "Delete" msgstr "Supprimer" +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +msgid "Save all" +msgstr "Tout enregistrer" + #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 msgid "Export Database" msgstr "Exporter les données" @@ -1472,7 +1474,7 @@ msgstr "Afficher uniquement les liens publics" #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 msgid "Filter untagged links" -msgstr "Filtrer par liens privés" +msgstr "Filtrer par liens sans tag" #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 From 740b32b520e6b1723512c6f9b78cef6575b1725b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 3 Nov 2020 12:38:38 +0100 Subject: [PATCH 23/50] Default formatter: add a setting to disable auto-linkification + update documentation + single parameter for both URL and hashtags Fixes #1094 --- application/bookmark/LinkUtils.php | 11 +++++++--- .../formatter/BookmarkDefaultFormatter.php | 7 ++++++- doc/md/Shaarli-configuration.md | 16 +++++++++++++++ .../BookmarkDefaultFormatterTest.php | 20 +++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index faf5dbfd..17c37979 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -138,12 +138,17 @@ function space2nbsp($text) * * @param string $description shaare's description. * @param string $indexUrl URL to Shaarli's index. - + * @param bool $autolink Turn on/off automatic linkifications of URLs and hashtags + * * @return string formatted description. */ -function format_description($description, $indexUrl = '') +function format_description($description, $indexUrl = '', $autolink = true) { - return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl))); + if ($autolink) { + $description = hashtag_autolink(text2clickable($description), $indexUrl); + } + + return nl2br(space2nbsp($description)); } /** diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index d58a5e39..149a3eb9 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -46,8 +46,13 @@ class BookmarkDefaultFormatter extends BookmarkFormatter $bookmark->getDescription() ?? '', $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] ); + $description = format_description( + escape($description), + $indexUrl, + $this->conf->get('formatter_settings.autolink', true) + ); - return $this->replaceTokens(format_description(escape($description), $indexUrl)); + return $this->replaceTokens($description); } /** diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md index dbfc3da9..99084728 100644 --- a/doc/md/Shaarli-configuration.md +++ b/doc/md/Shaarli-configuration.md @@ -164,6 +164,22 @@ _These settings should not be edited_ - **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy. - **allowed_protocols**: List of allowed protocols in shaare URLs or markdown-rendered descriptions. Useful if you want to store `javascript:` links (bookmarklets) in Shaarli (default: `["ftp", "ftps", "magnet"]`). +### Formatter + +Single string value. Default available: + + - `default`: supports line breaks, URL and hashtag auto-links. + - `markdown`: supports [Markdown](https://daringfireball.net/projects/markdown/syntax). + - `markdownExtra`: adds [extra](https://michelf.ca/projects/php-markdown/extra/) flavor to Markdown. + +### Formatter Settings + +Additional settings applied to formatters. + +#### default + + - **autolink**: boolean to enable or disable automatic linkification of URL and hashtags. + ### Resources - **data_dir**: Data directory. diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php index 3fc6f8dc..4fcc5dd1 100644 --- a/tests/formatter/BookmarkDefaultFormatterTest.php +++ b/tests/formatter/BookmarkDefaultFormatterTest.php @@ -289,4 +289,24 @@ class BookmarkDefaultFormatterTest extends TestCase $link['taglist_html'] ); } + + /** + * Test default formatting with formatter_settings.autolink set to false: + * URLs and hashtags should not be transformed + */ + public function testFormatDescriptionWithoutLinkification(): void + { + $this->conf->set('formatter_settings.autolink', false); + $this->formatter = new BookmarkDefaultFormatter($this->conf, false); + + $bookmark = new Bookmark(); + $bookmark->setDescription('Hi!' . PHP_EOL . 'https://thisisaurl.tld #hashtag'); + + $link = $this->formatter->format($bookmark); + + static::assertSame( + 'Hi!
' . PHP_EOL . 'https://thisisaurl.tld  #hashtag', + $link['description'] + ); + } } From 330ac859fb13a3a15875f185a611bfaa6c5f5587 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 5 Nov 2020 16:14:22 +0100 Subject: [PATCH 24/50] Fix: redirect to referrer after bookmark deletion Except if the referer points to a permalink (which has been deleted). Fixes #1622 --- .../front/controller/admin/ShaareManageController.php | 4 ++-- .../admin/ShaareManageControllerTest/DeleteBookmarkTest.php | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php index 7ceb8d8a..2ed298f5 100644 --- a/application/front/controller/admin/ShaareManageController.php +++ b/application/front/controller/admin/ShaareManageController.php @@ -66,8 +66,8 @@ class ShaareManageController extends ShaarliAdminController return $response->write(''); } - // Don't redirect to where we were previously because the datastore has changed. - return $this->redirect($response, '/'); + // Don't redirect to permalink after deletion. + return $this->redirectFromReferer($request, $response, ['shaare/']); } /** diff --git a/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php index 770a16d7..a276d988 100644 --- a/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php @@ -38,6 +38,8 @@ class DeleteBookmarkTest extends TestCase { $parameters = ['id' => '123']; + $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/shaare/abcdef'; + $request = $this->createMock(Request::class); $request ->method('getParam') @@ -90,6 +92,8 @@ class DeleteBookmarkTest extends TestCase { $parameters = ['id' => '123 456 789']; + $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/?searchtags=abcdef'; + $request = $this->createMock(Request::class); $request ->method('getParam') @@ -152,7 +156,7 @@ class DeleteBookmarkTest extends TestCase $result = $this->controller->deleteBookmark($request, $response); static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/'], $result->getHeader('location')); + static::assertSame(['/subfolder/?searchtags=abcdef'], $result->getHeader('location')); } /** From 5f987a64d88e0c1bb0da8bde9050e3409879cbda Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 5 Nov 2020 16:32:15 +0100 Subject: [PATCH 25/50] Fix confirm popup before bookmark deletion Regression introduced by #1596 Fixes #1623 --- assets/default/js/base.js | 3 ++- inc/languages/fr/LC_MESSAGES/shaarli.po | 21 +++++++++++++-------- tpl/default/changetag.html | 3 ++- tpl/default/page.footer.html | 5 +++-- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/assets/default/js/base.js b/assets/default/js/base.js index 4163577d..66badfb2 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js @@ -294,7 +294,8 @@ function init(description) { const deleteLinks = document.querySelectorAll('.confirm-delete'); [...deleteLinks].forEach((deleteLink) => { deleteLink.addEventListener('click', (event) => { - if (!confirm(document.getElementById('translation-delete-tag').innerHTML)) { + const type = event.currentTarget.getAttribute('data-type') || 'link'; + if (!confirm(document.getElementById(`translation-delete-${type}`).innerHTML)) { event.preventDefault(); } }); diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index d5b01227..4c363fa8 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-11-03 11:51+0100\n" -"PO-Revision-Date: 2020-11-03 11:55+0100\n" +"POT-Creation-Date: 2020-11-05 16:47+0100\n" +"PO-Revision-Date: 2020-11-05 16:48+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -189,9 +189,9 @@ msgstr "" #: application/bookmark/BookmarkInitializer.php:91 #: application/legacy/LegacyLinkDB.php:246 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:50 msgid "" "The personal, minimalist, super-fast, database free, bookmarking service" msgstr "" @@ -1017,11 +1017,11 @@ msgstr "Renommer le tag" msgid "Delete tag" msgstr "Supprimer le tag" -#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 msgid "You can also edit tags in the" msgstr "Vous pouvez aussi modifier les tags dans la" -#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 msgid "tag list" msgstr "liste des tags" @@ -1502,9 +1502,9 @@ msgid "Remember me" msgstr "Rester connecté" #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:50 msgid "by the Shaarli community" msgstr "par la communauté Shaarli" @@ -1525,6 +1525,11 @@ msgstr "Déplier tout" #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:47 +msgid "Are you sure you want to delete this link?" +msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 msgid "Are you sure you want to delete this tag?" msgstr "Êtes-vous sûr de vouloir supprimer ce tag ?" diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html index 89d08e2c..a5fbd31e 100644 --- a/tpl/default/changetag.html +++ b/tpl/default/changetag.html @@ -28,7 +28,8 @@
- +
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html index c153def0..964ffff1 100644 --- a/tpl/default/page.footer.html +++ b/tpl/default/page.footer.html @@ -28,12 +28,13 @@ {/loop} - + +
+
+
+

{"Change tags separator"|t}

+
+

+ {'Your current tag separator is'|t} {$tags_separator}{if="!empty($tags_separator_desc)"} ({$tags_separator_desc}){/if}. +

+
+ +
+ +
+ +
+

+ {'Note that hashtags won\'t fully work with a non-whitespace separator.'|t} +

+
+
+
{include="page.footer"} diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index e1115d49..7208a3b6 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html @@ -90,7 +90,7 @@ {'for'|t} {$search_term} {/if} {if="!empty($search_tags)"} - {$exploded_tags=explode(' ', $search_tags)} + {$exploded_tags=tags_str2array($search_tags, $tags_separator)} {'tagged'|t} {loop="$exploded_tags"} diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html index 964ffff1..58ca18c5 100644 --- a/tpl/default/page.footer.html +++ b/tpl/default/page.footer.html @@ -18,8 +18,6 @@
- - {loop="$plugins_footer.endofpage"} {$value} {/loop} @@ -41,4 +39,7 @@
+ + + diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html index c067e1d4..01b50b02 100644 --- a/tpl/default/tag.cloud.html +++ b/tpl/default/tag.cloud.html @@ -48,7 +48,7 @@
{loop="tags"} - {$key}{$key}{$value.count} {loop="$value.tag_plugin"} {$value} From 67339338af74a53780cc05232461ec30ffb05ad9 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 3 Nov 2020 13:35:27 +0100 Subject: [PATCH 28/50] Bump shaarli/netscape-bookmark-parser dependency version --- composer.json | 2 +- composer.lock | 65 +++++++++++++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 94492586..138319ca 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "katzgrau/klogger": "^1.2", "malkusch/lock": "^2.1", "pubsubhubbub/publisher": "dev-master", - "shaarli/netscape-bookmark-parser": "^2.1", + "shaarli/netscape-bookmark-parser": "^3.0", "slim/slim": "^3.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 3c89036f..0023df88 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "61360efbb2e1ba4c4fe00ce1f7a78ec5", + "content-hash": "83852dec81e299a117a81206a5091472", "packages": [ { "name": "arthurhoaro/web-thumbnailer", @@ -786,24 +786,25 @@ }, { "name": "shaarli/netscape-bookmark-parser", - "version": "v2.2.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/shaarli/netscape-bookmark-parser.git", - "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df" + "reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/432a010af2bb1832d6fbc4763e6b0100b980a1df", - "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df", + "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/d2321f30413944b2d0a9844bf8cc588c71ae6305", + "reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305", "shasum": "" }, "require": { "katzgrau/klogger": "~1.0", - "php": ">=5.6" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^5.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "autoload": { @@ -839,9 +840,9 @@ ], "support": { "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues", - "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v2.2.0" + "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v3.0.1" }, - "time": "2020-06-06T15:53:53+00:00" + "time": "2020-11-03T12:27:58+00:00" }, { "name": "slim/slim", @@ -1713,12 +1714,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "ba5d234b3a1559321b816b64aafc2ce6728799ff" + "reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ba5d234b3a1559321b816b64aafc2ce6728799ff", - "reference": "ba5d234b3a1559321b816b64aafc2ce6728799ff", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/065a018d3b5c2c84a53db3347cca4e1b7fa362a6", + "reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6", "shasum": "" }, "conflict": { @@ -1734,7 +1735,7 @@ "bagisto/bagisto": "<0.1.5", "barrelstrength/sprout-base-email": "<1.2.7", "barrelstrength/sprout-forms": "<3.9", - "baserproject/basercms": ">=4,<=4.3.6", + "baserproject/basercms": ">=4,<=4.3.6|>=4.4,<4.4.1", "bolt/bolt": "<3.7.1", "brightlocal/phpwhois": "<=4.2.5", "buddypress/buddypress": "<5.1.2", @@ -1818,6 +1819,7 @@ "magento/magento1ee": ">=1,<1.14.4.3", "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", "marcwillmann/turn": "<0.3.3", + "mediawiki/core": ">=1.31,<1.31.9|>=1.32,<1.32.4|>=1.33,<1.33.3|>=1.34,<1.34.3|>=1.34.99,<1.35", "mittwald/typo3_forum": "<1.2.1", "monolog/monolog": ">=1.8,<1.12", "namshi/jose": "<2.2", @@ -1832,7 +1834,8 @@ "onelogin/php-saml": "<2.10.4", "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5", "openid/php-openid": "<2.3", - "openmage/magento-lts": "<19.4.6|>=20,<20.0.2", + "openmage/magento-lts": "<19.4.8|>=20,<20.0.4", + "orchid/platform": ">=9,<9.4.4", "oro/crm": ">=1.7,<1.7.4", "oro/platform": ">=1.7,<1.7.4", "padraic/humbug_get_contents": "<1.1.2", @@ -1867,8 +1870,8 @@ "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", "sensiolabs/connect": "<4.2.3", "serluck/phpwhois": "<=4.2.6", - "shopware/core": "<=6.3.1", - "shopware/platform": "<=6.3.1", + "shopware/core": "<=6.3.2", + "shopware/platform": "<=6.3.2", "shopware/shopware": "<5.3.7", "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1", "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2", @@ -1901,7 +1904,7 @@ "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", - "sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5", + "sylius/sylius": "<1.6.9|>=1.7,<1.7.9|>=1.8,<1.8.3", "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", "symbiote/silverstripe-versionedfiles": "<=2.0.3", "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", @@ -2018,7 +2021,7 @@ "type": "tidelift" } ], - "time": "2020-10-08T21:02:27+00:00" + "time": "2020-11-01T20:01:47+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -2632,16 +2635,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.6", + "version": "3.5.8", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", "shasum": "" }, "require": { @@ -2684,24 +2687,24 @@ "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2020-08-10T04:50:15+00:00" + "time": "2020-10-23T02:01:07+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-ctype": "For best performance" @@ -2709,7 +2712,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2747,7 +2750,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.18.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" }, "funding": [ { @@ -2763,7 +2766,7 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "theseer/tokenizer", From df9aac5b6406bf192f2e4e0987e25d0de88480df Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 5 Nov 2020 18:16:52 +0100 Subject: [PATCH 29/50] Tags separator: vintage theme compatibility --- assets/default/js/base.js | 2 +- assets/vintage/js/base.js | 47 +++++++++++++++++++++--------------- tpl/vintage/includes.html | 4 +-- tpl/vintage/linklist.html | 2 +- tpl/vintage/page.footer.html | 6 +++-- 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/assets/default/js/base.js b/assets/default/js/base.js index e7bf4909..37069d69 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js @@ -217,7 +217,7 @@ function init(description) { (() => { const basePath = document.querySelector('input[name="js_base_path"]').value; const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]'); - const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || '\s' : '\s'; + const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' '; /** * Handle responsive menu. diff --git a/assets/vintage/js/base.js b/assets/vintage/js/base.js index 66830b59..15b664ce 100644 --- a/assets/vintage/js/base.js +++ b/assets/vintage/js/base.js @@ -2,29 +2,36 @@ import Awesomplete from 'awesomplete'; import 'awesomplete/awesomplete.css'; (() => { - const awp = Awesomplete.$; const autocompleteFields = document.querySelectorAll('input[data-multiple]'); - [...autocompleteFields].forEach((autocompleteField) => { - const awesomplete = new Awesomplete(awp(autocompleteField)); - awesomplete.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]); - awesomplete.replace = (text) => { - const before = awesomplete.input.value.match(/^.+ \s*|/)[0]; - awesomplete.input.value = `${before}${text} `; - }; - awesomplete.minChars = 1; + const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]'); + const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || " " : " "; - autocompleteField.addEventListener('input', () => { - const proposedTags = autocompleteField.getAttribute('data-list').replace(/,/g, '').split(' '); - const reg = /(\w+) /g; - let match; - while ((match = reg.exec(autocompleteField.value)) !== null) { - const id = proposedTags.indexOf(match[1]); - if (id !== -1) { - proposedTags.splice(id, 1); + [...autocompleteFields].forEach((autocompleteField) => { + const awesome = new Awesomplete(Awesomplete.$(autocompleteField)); + + // Tags are separated by separator + awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS( + text, + input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]) + ; + // Insert new selected tag in the input + awesome.replace = (text) => { + const before = awesome.input.value.match(new RegExp(`^.+${tagsSeparator}+|`))[0]; + awesome.input.value = `${before}${text}${tagsSeparator}`; + }; + // Highlight found items + awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]); + // Don't display already selected items + const reg = new RegExp(`/(\w+)${tagsSeparator}/g`); + let match; + awesome.data = (item, input) => { + while ((match = reg.exec(input))) { + if (item === match[1]) { + return ''; } } - - awesomplete.list = proposedTags; - }); + return item; + }; + awesome.minChars = 1; }); })(); diff --git a/tpl/vintage/includes.html b/tpl/vintage/includes.html index eac05701..2ce9da42 100644 --- a/tpl/vintage/includes.html +++ b/tpl/vintage/includes.html @@ -5,13 +5,13 @@ - + {if="$formatter==='markdown'"} {/if} {loop="$plugins_includes.css_files"} - + {/loop} {if="is_file('data/user.css')"}{/if} {$search_term} {/if} {if="!empty($search_tags)"} - {$exploded_tags=explode(' ', $search_tags)} + {$exploded_tags=tags_str2array($search_tags, $tags_separator)} tagged {loop="$exploded_tags"} diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html index 0fe4c736..be709aeb 100644 --- a/tpl/vintage/page.footer.html +++ b/tpl/vintage/page.footer.html @@ -23,8 +23,6 @@
{/if} - - {if="$is_logged_in"} {/if} @@ -34,3 +32,7 @@ {/loop} + + + + From 8a1ce1da15fdbae99b24700b06f2008c7a657603 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 5 Nov 2020 19:08:38 +0100 Subject: [PATCH 30/50] ESLint --- assets/default/js/base.js | 3 ++- assets/vintage/js/base.js | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/assets/default/js/base.js b/assets/default/js/base.js index 37069d69..dd532bb7 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js @@ -55,7 +55,8 @@ function createAwesompleteInstance(element, separator, tags = []) { // Highlight found items awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${separator}]*$`))[0]); // Don't display already selected items - const reg = new RegExp(`/(\w+)${separator}/g`); + // WARNING: pseudo classes does not seem to work with string litterals... + const reg = new RegExp(`([^${separator}]+)${separator}`, 'g'); let match; awesome.data = (item, input) => { while ((match = reg.exec(input))) { diff --git a/assets/vintage/js/base.js b/assets/vintage/js/base.js index 15b664ce..55f1c37d 100644 --- a/assets/vintage/js/base.js +++ b/assets/vintage/js/base.js @@ -4,7 +4,7 @@ import 'awesomplete/awesomplete.css'; (() => { const autocompleteFields = document.querySelectorAll('input[data-multiple]'); const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]'); - const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || " " : " "; + const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' '; [...autocompleteFields].forEach((autocompleteField) => { const awesome = new Awesomplete(Awesomplete.$(autocompleteField)); @@ -12,8 +12,8 @@ import 'awesomplete/awesomplete.css'; // Tags are separated by separator awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS( text, - input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]) - ; + input.match(new RegExp(`[^${tagsSeparator}]*$`))[0], + ); // Insert new selected tag in the input awesome.replace = (text) => { const before = awesome.input.value.match(new RegExp(`^.+${tagsSeparator}+|`))[0]; @@ -21,8 +21,10 @@ import 'awesomplete/awesomplete.css'; }; // Highlight found items awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]); + // Don't display already selected items - const reg = new RegExp(`/(\w+)${tagsSeparator}/g`); + // WARNING: pseudo classes does not seem to work with string litterals... + const reg = new RegExp(`([^${tagsSeparator}]+)${tagsSeparator}`, 'g'); let match; awesome.data = (item, input) => { while ((match = reg.exec(input))) { From cfdd2094407e61f371c02117c8c66916a6d1d807 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 5 Nov 2020 19:45:41 +0100 Subject: [PATCH 31/50] Display error details even with dev.debug set to false It makes more sense to display the error even if it's unexpected. Only for logged in users. Fixes #1606 --- .../controller/visitor/ErrorController.php | 11 +++++-- assets/default/scss/shaarli.scss | 6 +++- inc/languages/fr/LC_MESSAGES/shaarli.po | 14 +++++++-- .../visitor/ErrorControllerTest.php | 29 ++++++++++++++++++- tpl/default/error.html | 8 +++-- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php index 8da11172..428e8254 100644 --- a/application/front/controller/visitor/ErrorController.php +++ b/application/front/controller/visitor/ErrorController.php @@ -26,8 +26,14 @@ class ErrorController extends ShaarliVisitorController $response = $response->withStatus($throwable->getCode()); } else { // Internal error (any other Throwable) - if ($this->container->conf->get('dev.debug', false)) { - $this->assignView('message', $throwable->getMessage()); + if ($this->container->conf->get('dev.debug', false) || $this->container->loginManager->isLoggedIn()) { + $this->assignView('message', t('Error: ') . $throwable->getMessage()); + $this->assignView( + 'text', + '' + . t('Please report it on Github.') + . '' + ); $this->assignView('stacktrace', exception2text($throwable)); } else { $this->assignView('message', t('An unexpected error occurred.')); @@ -36,7 +42,6 @@ class ErrorController extends ShaarliVisitorController $response = $response->withStatus(500); } - return $response->write($this->render('error')); } } diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index a7f091e9..3404ce12 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss @@ -1266,11 +1266,15 @@ form { margin: 70px 0 25px; } + a { + color: var(--main-color); + } + pre { margin: 0 20%; padding: 20px 0; text-align: left; - line-height: .7em; + line-height: 1em; } } diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 4c363fa8..51bef6c7 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-11-05 16:47+0100\n" -"PO-Revision-Date: 2020-11-05 16:48+0100\n" +"POT-Creation-Date: 2020-11-05 19:43+0100\n" +"PO-Revision-Date: 2020-11-05 19:44+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -501,7 +501,15 @@ msgstr "mois" msgid "Monthly" msgstr "Mensuel" -#: application/front/controller/visitor/ErrorController.php:33 +#: application/front/controller/visitor/ErrorController.php:30 +msgid "Error: " +msgstr "Erreur : " + +#: application/front/controller/visitor/ErrorController.php:34 +msgid "Please report it on Github." +msgstr "Merci de la rapporter sur Github." + +#: application/front/controller/visitor/ErrorController.php:39 msgid "An unexpected error occurred." msgstr "Une erreur inattendue s'est produite." diff --git a/tests/front/controller/visitor/ErrorControllerTest.php b/tests/front/controller/visitor/ErrorControllerTest.php index 75408cf4..e18a6fa2 100644 --- a/tests/front/controller/visitor/ErrorControllerTest.php +++ b/tests/front/controller/visitor/ErrorControllerTest.php @@ -50,7 +50,31 @@ class ErrorControllerTest extends TestCase } /** - * Test displaying error with any exception (no debug): only display an error occurred with HTTP 500. + * Test displaying error with any exception (no debug) while logged in: + * display full error details + */ + public function testDisplayAnyExceptionErrorNoDebugLoggedIn(): void + { + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->loginManager->method('isLoggedIn')->willReturn(true); + + $result = ($this->controller)($request, $response, new \Exception('abc')); + + static::assertSame(500, $result->getStatusCode()); + static::assertSame('Error: abc', $assignedVariables['message']); + static::assertContainsPolyfill('Please report it on Github', $assignedVariables['text']); + static::assertArrayHasKey('stacktrace', $assignedVariables); + } + + /** + * Test displaying error with any exception (no debug) while logged out: + * display standard error without detail */ public function testDisplayAnyExceptionErrorNoDebug(): void { @@ -61,10 +85,13 @@ class ErrorControllerTest extends TestCase $assignedVariables = []; $this->assignTemplateVars($assignedVariables); + $this->container->loginManager->method('isLoggedIn')->willReturn(false); + $result = ($this->controller)($request, $response, new \Exception('abc')); static::assertSame(500, $result->getStatusCode()); static::assertSame('An unexpected error occurred.', $assignedVariables['message']); + static::assertArrayNotHasKey('text', $assignedVariables); static::assertArrayNotHasKey('stacktrace', $assignedVariables); } } diff --git a/tpl/default/error.html b/tpl/default/error.html index c3e0c3c1..34f9707d 100644 --- a/tpl/default/error.html +++ b/tpl/default/error.html @@ -9,13 +9,17 @@

{$message}

+ + + {if="!empty($text)"} +

{$text}

+ {/if} + {if="!empty($stacktrace)"}
         {$stacktrace}
       
{/if} - -
{include="page.footer"} From ce901a58289c72bf7f4dc3515a2be70562cd618b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 7 Nov 2020 14:27:49 +0100 Subject: [PATCH 32/50] Reviewed nginx configuration Both in documentation and Docker image. For security purpose, it no longer allow to access static files through the main nginx *location*. Static files are served if their extension matches the whitelist. As a side effect, we no longer need specific restrictions, and therefore it fixes the nginx part of #1608. --- .docker/nginx.conf | 45 ++++++++++------------------------ .dockerignore | 11 +++++++++ doc/md/Server-configuration.md | 23 +++-------------- 3 files changed, 27 insertions(+), 52 deletions(-) diff --git a/.docker/nginx.conf b/.docker/nginx.conf index 023f52c1..30810a87 100644 --- a/.docker/nginx.conf +++ b/.docker/nginx.conf @@ -17,27 +17,13 @@ http { index index.html index.php; server { - listen 80; - root /var/www/shaarli; + listen 80; + root /var/www/shaarli; access_log /var/log/nginx/shaarli.access.log; error_log /var/log/nginx/shaarli.error.log; - location ~ /\. { - # deny access to dotfiles - access_log off; - log_not_found off; - deny all; - } - - location ~ ~$ { - # deny access to temp editor files, e.g. "script.php~" - access_log off; - log_not_found off; - deny all; - } - - location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { + location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$ { # cache static assets expires max; add_header Pragma public; @@ -49,30 +35,25 @@ http { alias /var/www/shaarli/images/favicon.ico; } - location / { - # Slim - rewrite URLs - try_files $uri /index.php$is_args$args; + location /doc/html/ { + default_type "text/html"; + try_files $uri $uri/ $uri.html =404; } - location ~ (index)\.php$ { + location / { + # Slim - rewrite URLs & do NOT serve static files through this location + try_files _ /index.php$is_args$args; + } + + location ~ index\.php$ { # Slim - split URL path into (script_filename, path_info) try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_split_path_info ^(index.php)(/.+)$; # filter and proxy PHP requests to PHP-FPM fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_index index.php; include fastcgi.conf; } - - location ~ /doc/ { - default_type "text/html"; - try_files $uri $uri/ $uri.html =404; - } - - location ~ \.php$ { - # deny access to all other PHP scripts - deny all; - } } } diff --git a/.dockerignore b/.dockerignore index 96fd31c5..19fd87a5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,16 @@ .dev .git .github +.gitattributes +.gitignore +.travis.yml tests +# Docker related resources are not needed inside the container +.dockerignore +Dockerfile +Dockerfile.armhf + # Docker Compose resources docker-compose.yml @@ -13,6 +21,9 @@ data/* pagecache/* tmp/* +# Shaarli's docs are created during the build +doc/html/ + # Eclipse project files .settings .buildpath diff --git a/doc/md/Server-configuration.md b/doc/md/Server-configuration.md index 4e74d80b..5b8aff53 100644 --- a/doc/md/Server-configuration.md +++ b/doc/md/Server-configuration.md @@ -296,7 +296,7 @@ server { location / { # default index file when no file URI is requested index index.php; - try_files $uri /index.php$is_args$args; + try_files _ /index.php$is_args$args; } location ~ (index)\.php$ { @@ -309,23 +309,7 @@ server { include fastcgi.conf; } - location ~ \.php$ { - # deny access to all other PHP scripts - # disable this if you host other PHP applications on the same virtualhost - deny all; - } - - location ~ /\. { - # deny access to dotfiles - deny all; - } - - location ~ ~$ { - # deny access to temp editor files, e.g. "script.php~" - deny all; - } - - location ~ /doc/ { + location ~ /doc/html/ { default_type "text/html"; try_files $uri $uri/ $uri.html =404; } @@ -336,13 +320,12 @@ server { } # allow client-side caching of static files - location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { + location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$ { expires max; add_header Cache-Control "public, must-revalidate, proxy-revalidate"; # HTTP 1.0 compatibility add_header Pragma public; } - } ``` From 9952de2fe0d7e6b2c45d551ae523ddd796653c7d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 8 Nov 2020 11:58:17 +0100 Subject: [PATCH 33/50] Replace vimeo link in demo bookmarks due to IP ban on the demo instance Fixes #1148 --- application/bookmark/BookmarkInitializer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 04b996f3..98dd3f1c 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -36,8 +36,8 @@ class BookmarkInitializer public function initialize(): void { $bookmark = new Bookmark(); - $bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)')); - $bookmark->setUrl('https://vimeo.com/153493904'); + $bookmark->setTitle('Calm Jazz Music - YouTube ' . t('(private bookmark with thumbnail demo)')); + $bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c'); $bookmark->setDescription(t( 'Shaarli will automatically pick up the thumbnail for links to a variety of websites. From 8a9796014ce6c842095a9d031c8cbf40da761e0f Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 8 Nov 2020 13:13:13 +0100 Subject: [PATCH 34/50] Reviewed Apache configuration (in documentation) For security purpose, block access to any static file not matching the list of allowed extensions. It allows us to remove the specific retriction on dotfiles, and fix Apache part of #1608. --- .htaccess | 2 +- doc/md/Server-configuration.md | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.htaccess b/.htaccess index 25fcfb03..9d1522df 100644 --- a/.htaccess +++ b/.htaccess @@ -13,7 +13,7 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] # Alternative (if the 2 lines above don't work) # SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0 -# REST API +# Slim URL Redirection # Ionos Hosting needs RewriteBase / # RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f diff --git a/doc/md/Server-configuration.md b/doc/md/Server-configuration.md index 4e74d80b..66db8c57 100644 --- a/doc/md/Server-configuration.md +++ b/doc/md/Server-configuration.md @@ -193,19 +193,24 @@ sudo nano /etc/apache2/sites-available/shaarli.mydomain.org.conf Require all granted - - # Prevent accessing dotfiles - RedirectMatch 404 ".*" - + # BE CAREFUL: directives order matter! - + + Require all denied + + + + Require all granted + + + # allow client-side caching of static files Header set Cache-Control "max-age=2628000, public, must-revalidate, proxy-revalidate" - + + # serve the Shaarli favicon from its custom location Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico - ``` From 00d3dd91ef42df13eeafbcc54dcebe3238e322c6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 8 Nov 2020 13:54:39 +0100 Subject: [PATCH 35/50] Fix an issue truncating extracted metadata content Previous regex forced the selection to stop at either the first single or double quote found, regardless of the opening quote. Using '\1', we're sure to wait for the proper quote before stopping the capture. --- application/bookmark/LinkUtils.php | 8 ++++---- tests/bookmark/LinkUtilsTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 17c37979..a74fda57 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -68,16 +68,16 @@ function html_extract_tag($tag, $html) $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 . '[^\'"]*?[\'"]'; - // Try to retrieve OpenGraph image. - $ogRegex = '#]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=["\'](.*?)["\'].*?>#'; + // Try to retrieve OpenGraph tag. + $ogRegex = '#]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; // If the attributes are not in the order property => content (e.g. Github) // New regex to keep this readable... more or less. - $ogRegexReverse = '#]+content=["\'](.*?)["\'][^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; + $ogRegexReverse = '#]+content=(["\'])([^\1]*?)\1[^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; if (preg_match($ogRegex, $html, $matches) > 0 || preg_match($ogRegexReverse, $html, $matches) > 0 ) { - return $matches[1]; + return $matches[2]; } return false; diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php index 3321242f..9bddf84b 100644 --- a/tests/bookmark/LinkUtilsTest.php +++ b/tests/bookmark/LinkUtilsTest.php @@ -168,6 +168,36 @@ class LinkUtilsTest extends TestCase $this->assertEquals($description, html_extract_tag('description', $html)); } + /** + * Test html_extract_tag() with double quoted content containing single quote, and the opposite. + */ + public function testHtmlExtractExistentNameTagWithMixedQuotes(): void + { + $description = 'Bob and Alice share M&M\'s.'; + + $html = ''; + $this->assertEquals($description, html_extract_tag('description', $html)); + + $html = ''; + $this->assertEquals($description, html_extract_tag('description', $html)); + + $html = ''; + $this->assertEquals($description, html_extract_tag('description', $html)); + + $description = 'Bob and Alice share "cookies".'; + + $html = ''; + $this->assertEquals($description, html_extract_tag('description', $html)); + + $html = ''; + $this->assertEquals($description, html_extract_tag('description', $html)); + + $html = ''; + $this->assertEquals($description, html_extract_tag('description', $html)); + } + /** * Test html_extract_tag() when the tag Date: Tue, 22 Sep 2020 20:16:23 +0200 Subject: [PATCH 36/50] Coding style: switch PHPCS to PSR12 Also temporarily ignore test code (one step at a time). Reference: https://www.php-fig.org/psr/psr-12/ Related to #95 --- Makefile | 4 ---- phpcs.xml | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 7415887a..181b61c4 100644 --- a/Makefile +++ b/Makefile @@ -27,10 +27,6 @@ PHPCS := $(BIN)/phpcs code_sniffer: @$(PHPCS) -### - errors filtered by coding standard: PEAR, PSR1, PSR2, Zend... -PHPCS_%: - @$(PHPCS) --report-full --report-width=200 --standard=$* - ### - errors by Git author code_sniffer_blame: @$(PHPCS) --report-gitblame diff --git a/phpcs.xml b/phpcs.xml index 29b95d56..349dc566 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -5,13 +5,13 @@ index.php application plugins - tests + */*.css */*.js - - + + From b7c50a58dedc00a6d34793fc3393aaabf808ab94 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 9 Nov 2020 10:36:13 +0100 Subject: [PATCH 37/50] Docker-compose: fix SSL certificate + add parameter for Docker tag Use envvar SHAARLI_VIRTUAL_HOST for Traefik's docker.domain parameter instead of localhost (I'm not sure if did work at some point). Add an environment variable to choose which Docker tag to use instead of using master by default. Fixes #1632 --- doc/md/Docker.md | 7 +++++-- docker-compose.yml | 9 +++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/md/Docker.md b/doc/md/Docker.md index c152fe92..fc406c00 100644 --- a/doc/md/Docker.md +++ b/doc/md/Docker.md @@ -1,3 +1,4 @@ + # Docker [Docker](https://docs.docker.com/get-started/overview/) is an open platform for developing, shipping, and running applications @@ -113,9 +114,11 @@ $ mkdir shaarli && cd shaarli # Download the latest version of Shaarli's docker-compose.yml $ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/latest/docker-compose.yml -o docker-compose.yml # Create the .env file and fill in your VPS and domain information -# (replace and with your actual information) +# (replace , and with your actual information) $ echo 'SHAARLI_VIRTUAL_HOST=shaarli.mydomain.org' > .env $ echo 'SHAARLI_LETSENCRYPT_EMAIL=admin@mydomain.org' >> .env +# Available Docker tags can be found at https://hub.docker.com/r/shaarli/shaarli/tags +$ echo 'SHAARLI_DOCKER_TAG=latest' >> .env # Pull the Docker images $ docker-compose pull # Run! @@ -224,4 +227,4 @@ $ docker system prune - [docker pull](https://docs.docker.com/engine/reference/commandline/pull/) - [docker run](https://docs.docker.com/engine/reference/commandline/run/) - [docker-compose logs](https://docs.docker.com/compose/reference/logs/) -- Træfik: [Getting Started](https://docs.traefik.io/), [Docker backend](https://docs.traefik.io/configuration/backends/docker/), [Let's Encrypt](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/), [Docker image](https://hub.docker.com/_/traefik/) \ No newline at end of file +- Træfik: [Getting Started](https://docs.traefik.io/), [Docker backend](https://docs.traefik.io/configuration/backends/docker/), [Let's Encrypt](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/), [Docker image](https://hub.docker.com/_/traefik/) diff --git a/docker-compose.yml b/docker-compose.yml index a3de4b1c..4ebae447 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,12 +2,13 @@ # Shaarli - Docker Compose example configuration # # See: -# - https://shaarli.readthedocs.io/en/master/docker/shaarli-images/ -# - https://shaarli.readthedocs.io/en/master/guides/install-shaarli-with-debian9-and-docker/ +# - https://shaarli.readthedocs.io/en/master/Docker/#docker-compose # # Environment variables: # - SHAARLI_VIRTUAL_HOST Fully Qualified Domain Name for the Shaarli instance # - SHAARLI_LETSENCRYPT_EMAIL Contact email for certificate renewal +# - SHAARLI_DOCKER_TAG Shaarli docker tag to use +# See: https://hub.docker.com/r/shaarli/shaarli/tags version: '3' networks: @@ -20,7 +21,7 @@ volumes: services: shaarli: - image: shaarli/shaarli:master + image: shaarli/shaarli:${SHAARLI_DOCKER_TAG} build: ./ networks: - http-proxy @@ -40,7 +41,7 @@ services: - "--entrypoints=Name:https Address::443 TLS" - "--retry" - "--docker" - - "--docker.domain=docker.localhost" + - "--docker.domain=${SHAARLI_VIRTUAL_HOST}" - "--docker.exposedbydefault=true" - "--docker.watch=true" - "--acme" From 53054b2bf6a919fd4ff9b44b6ad1986f21f488b6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 22 Sep 2020 20:25:47 +0200 Subject: [PATCH 38/50] Apply PHP Code Beautifier on source code for linter automatic fixes --- application/History.php | 1 + application/Languages.php | 13 ++-- application/Thumbnailer.php | 5 +- application/TimeZone.php | 7 +- application/Utils.php | 14 ++-- application/api/ApiMiddleware.php | 4 +- application/api/ApiUtils.php | 6 +- .../api/controllers/HistoryController.php | 1 - application/api/controllers/Info.php | 4 +- application/api/controllers/Links.php | 6 +- .../exceptions/ApiAuthorizationException.php | 2 +- application/api/exceptions/ApiException.php | 2 +- application/bookmark/Bookmark.php | 5 +- application/bookmark/BookmarkArray.php | 6 +- application/bookmark/BookmarkFileService.php | 18 +++-- application/bookmark/BookmarkFilter.php | 12 +-- application/bookmark/BookmarkIO.php | 4 +- application/bookmark/BookmarkInitializer.php | 6 +- application/bookmark/LinkUtils.php | 11 +-- .../exception/BookmarkNotFoundException.php | 1 + .../exception/EmptyDataStoreException.php | 6 +- .../exception/InvalidBookmarkException.php | 14 ++-- .../NotWritableDataStoreException.php | 4 +- application/config/ConfigIO.php | 1 + application/config/ConfigManager.php | 13 ++-- application/config/ConfigPhp.php | 28 +++---- application/config/ConfigPlugin.php | 8 +- .../exception/MissingFieldConfigException.php | 1 - .../exception/UnauthorizedConfigException.php | 1 - application/exceptions/IOException.php | 1 + application/feed/FeedBuilder.php | 9 ++- .../formatter/BookmarkMarkdownFormatter.php | 12 +-- .../formatter/BookmarkRawFormatter.php | 4 +- application/formatter/FormatterFactory.php | 2 +- application/front/ShaarliMiddleware.php | 6 +- .../controller/admin/ConfigureController.php | 7 +- .../controller/admin/ExportController.php | 4 +- .../controller/admin/ImportController.php | 4 +- .../controller/admin/ManageTagController.php | 4 +- .../controller/admin/PasswordController.php | 4 +- .../controller/admin/PluginsController.php | 4 +- .../controller/admin/ServerController.php | 2 +- .../admin/SessionFilterController.php | 2 - .../controller/admin/ShaareAddController.php | 2 +- .../admin/ShaareManageController.php | 2 +- .../admin/ShaarePublishController.php | 13 ++-- .../controller/admin/ThumbnailsController.php | 2 +- .../controller/admin/ToolsController.php | 2 +- .../visitor/BookmarkListController.php | 8 +- .../controller/visitor/DailyController.php | 2 +- .../controller/visitor/FeedController.php | 2 +- .../controller/visitor/InstallController.php | 25 ++++--- .../controller/visitor/LoginController.php | 8 +- .../visitor/PictureWallController.php | 2 +- .../visitor/ShaarliVisitorController.php | 5 +- .../controller/visitor/TagCloudController.php | 4 +- .../controller/visitor/TagController.php | 8 +- application/helper/ApplicationUtils.php | 47 ++++++------ application/helper/FileUtils.php | 4 +- application/http/HttpUtils.php | 73 +++++++++++-------- application/http/Url.php | 10 +-- application/http/UrlUtils.php | 11 +-- application/legacy/LegacyController.php | 2 +- application/legacy/LegacyLinkDB.php | 18 ++--- application/legacy/LegacyLinkFilter.php | 18 ++--- application/legacy/LegacyUpdater.php | 12 +-- .../netscape/NetscapeBookmarkUtils.php | 4 +- application/plugin/PluginManager.php | 13 ++-- .../exception/PluginFileNotFoundException.php | 1 + application/render/ThemeUtils.php | 4 +- application/security/BanManager.php | 8 +- application/security/LoginManager.php | 16 ++-- application/security/SessionManager.php | 3 +- application/updater/Updater.php | 6 +- application/updater/UpdaterUtils.php | 4 +- index.php | 7 +- plugins/addlink_toolbar/addlink_toolbar.php | 20 ++--- plugins/archiveorg/archiveorg.php | 1 + plugins/default_colors/default_colors.php | 12 +-- plugins/demo_plugin/demo_plugin.php | 45 ++++++------ plugins/isso/isso.php | 8 +- plugins/piwik/piwik.php | 3 +- plugins/playvideos/playvideos.php | 11 +-- plugins/pubsubhubbub/pubsubhubbub.php | 14 ++-- plugins/qrcode/qrcode.php | 1 + plugins/wallabag/WallabagInstance.php | 9 ++- plugins/wallabag/wallabag.php | 5 +- 87 files changed, 408 insertions(+), 336 deletions(-) diff --git a/application/History.php b/application/History.php index bd5c1bf7..1be955c5 100644 --- a/application/History.php +++ b/application/History.php @@ -1,4 +1,5 @@ language = $confLanguage; } - if (! extension_loaded('gettext') + if ( + ! extension_loaded('gettext') || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php']) ) { $this->initPhpTranslator(); @@ -98,7 +99,7 @@ class Languages $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages'); // Default extension translation from the current theme - $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language'; + $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $this->conf->get('theme') . '/language'; if (is_dir($themeTransFolder)) { $this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false); } @@ -121,7 +122,7 @@ class Languages $translations = new Translations(); // Core translations try { - $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po'); + $translations = $translations->addFromPoFile('inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po'); $translations->setDomain('shaarli'); $this->translator->loadTranslations($translations); } catch (\InvalidArgumentException $e) { @@ -129,11 +130,11 @@ class Languages // Default extension translation from the current theme $theme = $this->conf->get('theme'); - $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language'; + $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $theme . '/language'; if (is_dir($themeTransFolder)) { try { $translations = Translations::fromPoFile( - $themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po' + $themeTransFolder . '/' . $this->language . '/LC_MESSAGES/' . $theme . '.po' ); $translations->setDomain($theme); $this->translator->loadTranslations($translations); @@ -149,7 +150,7 @@ class Languages try { $extension = Translations::fromPoFile( - $translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po' + $translationPath . $this->language . '/LC_MESSAGES/' . $domain . '.po' ); $extension->setDomain($domain); $this->translator->loadTranslations($extension); diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php index 5aec23c8..30354310 100644 --- a/application/Thumbnailer.php +++ b/application/Thumbnailer.php @@ -60,7 +60,7 @@ class Thumbnailer // TODO: create a proper error handling system able to catch exceptions... die(t( 'php-gd extension must be loaded to use thumbnails. ' - .'Thumbnails are now disabled. Please reload the page.' + . 'Thumbnails are now disabled. Please reload the page.' )); } @@ -81,7 +81,8 @@ class Thumbnailer */ public function get($url) { - if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON + if ( + $this->conf->get('thumbnails.mode') === self::MODE_COMMON && ! $this->isCommonMediaOrImage($url) ) { return false; diff --git a/application/TimeZone.php b/application/TimeZone.php index c1869ef8..a420eb96 100644 --- a/application/TimeZone.php +++ b/application/TimeZone.php @@ -1,4 +1,5 @@ $continent, 'city' => $city]; $continents[$continent] = true; } @@ -85,7 +86,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '') function isTimeZoneValid($continent, $city) { return in_array( - $continent.'/'.$city, + $continent . '/' . $city, timezone_identifiers_list() ); } diff --git a/application/Utils.php b/application/Utils.php index db046893..4c2d6701 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -1,4 +1,5 @@ $value) { $out[escape($key)] = escape($value); } @@ -163,7 +164,7 @@ function checkDateFormat($format, $string) * * @return string $referer - final referer. */ -function generateLocation($referer, $host, $loopTerms = array()) +function generateLocation($referer, $host, $loopTerms = []) { $finalReferer = './?'; @@ -196,7 +197,7 @@ function generateLocation($referer, $host, $loopTerms = array()) function autoLocale($headerLocale) { // Default if browser does not send HTTP_ACCEPT_LANGUAGE - $locales = array('en_US', 'en_US.utf8', 'en_US.UTF-8'); + $locales = ['en_US', 'en_US.utf8', 'en_US.UTF-8']; if (! empty($headerLocale)) { if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) { $attempts = []; @@ -376,7 +377,7 @@ function return_bytes($val) return $val; } $val = trim($val); - $last = strtolower($val[strlen($val)-1]); + $last = strtolower($val[strlen($val) - 1]); $val = intval(substr($val, 0, -1)); switch ($last) { case 'g': @@ -482,7 +483,9 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) */ function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false) { - $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; }; + $postFunction = $fixCase ? 'ucfirst' : function ($input) { + return $input; + }; return $postFunction(dn__($domain, $text, $nText, $nb, $variables)); } @@ -494,4 +497,3 @@ function exception2text(Throwable $e): string { return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString(); } - diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php index adc8b266..9fb88358 100644 --- a/application/api/ApiMiddleware.php +++ b/application/api/ApiMiddleware.php @@ -1,4 +1,5 @@ hasHeader('Authorization') + if ( + !$request->hasHeader('Authorization') && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION']) ) { throw new ApiAuthorizationException('JWT token not provided'); diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index eb1ca9bc..05a2840a 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -1,4 +1,5 @@ iat) + if ( + empty($payload->iat) || $payload->iat > time() || time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION ) { diff --git a/application/api/controllers/HistoryController.php b/application/api/controllers/HistoryController.php index 505647a9..d83a3a25 100644 --- a/application/api/controllers/HistoryController.php +++ b/application/api/controllers/HistoryController.php @@ -1,6 +1,5 @@ $this->bookmarkService->count(), 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE), - 'settings' => array( + 'settings' => [ 'title' => $this->conf->get('general.title', 'Shaarli'), 'header_link' => $this->conf->get('general.header_link', '?'), 'timezone' => $this->conf->get('general.timezone', 'UTC'), 'enabled_plugins' => $this->conf->get('general.enabled_plugins', []), 'default_private_links' => $this->conf->get('privacy.default_private_links', false), - ), + ], ]; return $response->withJson($info, 200, $this->jsonStyle); diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 6bf529e4..c379b962 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -119,7 +119,8 @@ class Links extends ApiController $data = (array) ($request->getParsedBody() ?? []); $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); // duplicate by URL, return 409 Conflict - if (! empty($bookmark->getUrl()) + if ( + ! empty($bookmark->getUrl()) && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) ) { return $response->withJson( @@ -159,7 +160,8 @@ class Links extends ApiController $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); // duplicate URL on a different link, return 409 Conflict - if (! empty($requestBookmark->getUrl()) + if ( + ! empty($requestBookmark->getUrl()) && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) && $dup->getId() != $id ) { diff --git a/application/api/exceptions/ApiAuthorizationException.php b/application/api/exceptions/ApiAuthorizationException.php index 0e3f4776..c77e9eea 100644 --- a/application/api/exceptions/ApiAuthorizationException.php +++ b/application/api/exceptions/ApiAuthorizationException.php @@ -28,7 +28,7 @@ class ApiAuthorizationException extends ApiException */ public function setMessage($message) { - $original = $this->debug === true ? ': '. $this->getMessage() : ''; + $original = $this->debug === true ? ': ' . $this->getMessage() : ''; $this->message = $message . $original; } } diff --git a/application/api/exceptions/ApiException.php b/application/api/exceptions/ApiException.php index d6b66323..7deafb96 100644 --- a/application/api/exceptions/ApiException.php +++ b/application/api/exceptions/ApiException.php @@ -44,7 +44,7 @@ abstract class ApiException extends \Exception } return [ 'message' => $this->getMessage(), - 'stacktrace' => get_class($this) .': '. $this->getTraceAsString() + 'stacktrace' => get_class($this) . ': ' . $this->getTraceAsString() ]; } diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index 8aaeb9d8..b592722f 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -106,7 +106,8 @@ class Bookmark */ public function validate(): void { - if ($this->id === null + if ( + $this->id === null || ! is_int($this->id) || empty($this->shortUrl) || empty($this->created) @@ -114,7 +115,7 @@ class Bookmark throw new InvalidBookmarkException($this); } if (empty($this->url)) { - $this->url = '/shaare/'. $this->shortUrl; + $this->url = '/shaare/' . $this->shortUrl; } if (empty($this->title)) { $this->title = $this->url; diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php index 67bb3b73..b9328116 100644 --- a/application/bookmark/BookmarkArray.php +++ b/application/bookmark/BookmarkArray.php @@ -72,7 +72,8 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess */ public function offsetSet($offset, $value) { - if (! $value instanceof Bookmark + if ( + ! $value instanceof Bookmark || $value->getId() === null || empty($value->getUrl()) || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId()) || $offset !== null && $offset !== $value->getId() @@ -222,7 +223,8 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess */ public function getByUrl(string $url): ?Bookmark { - if (! empty($url) + if ( + ! empty($url) && isset($this->urls[$url]) && isset($this->bookmarks[$this->urls[$url]]) ) { diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 85efeea6..66248cc2 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -69,7 +69,7 @@ class BookmarkFileService implements BookmarkServiceInterface } else { try { $this->bookmarks = $this->bookmarksIO->read(); - } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { + } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) { $this->bookmarks = new BookmarkArray(); if ($this->isLoggedIn) { @@ -85,7 +85,7 @@ class BookmarkFileService implements BookmarkServiceInterface if (! $this->bookmarks instanceof BookmarkArray) { $this->migrate(); exit( - 'Your data store has been migrated, please reload the page.'. PHP_EOL . + 'Your data store has been migrated, please reload the page.' . PHP_EOL . 'If this message keeps showing up, please delete data/updates.txt file.' ); } @@ -102,7 +102,8 @@ class BookmarkFileService implements BookmarkServiceInterface $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); // PHP 7.3 introduced array_key_first() to avoid this hack $first = reset($bookmark); - if (!$this->isLoggedIn + if ( + !$this->isLoggedIn && $first->isPrivate() && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) ) { @@ -165,7 +166,8 @@ class BookmarkFileService implements BookmarkServiceInterface } $bookmark = $this->bookmarks[$id]; - if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') + if ( + ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') ) { throw new Exception('Unauthorized'); @@ -265,7 +267,8 @@ class BookmarkFileService implements BookmarkServiceInterface } $bookmark = $this->bookmarks[$id]; - if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') + if ( + ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') ) { return false; @@ -307,7 +310,8 @@ class BookmarkFileService implements BookmarkServiceInterface $caseMapping = []; foreach ($bookmarks as $bookmark) { foreach ($bookmark->getTags() as $tag) { - if (empty($tag) + if ( + empty($tag) || (! $this->isLoggedIn && startsWith($tag, '.')) || $tag === BookmarkMarkdownFormatter::NO_MD_TAG || in_array($tag, $filteringTags, true) @@ -356,7 +360,7 @@ class BookmarkFileService implements BookmarkServiceInterface foreach ($this->search([], null, false, false, true) as $bookmark) { if ($to < $bookmark->getCreated()) { $next = $bookmark->getCreated(); - } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { + } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { $out[] = $bookmark; } else { if ($previous !== null) { diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index 5d8733dc..db83c51c 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php @@ -150,7 +150,7 @@ class BookmarkFilter return $this->bookmarks; } - $out = array(); + $out = []; foreach ($this->bookmarks as $key => $value) { if ($value->isPrivate() && $visibility === 'private') { $out[$key] = $value; @@ -395,7 +395,7 @@ class BookmarkFilter $search = $link->getTagsString($tagsSeparator); if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { // description given and at least one possible tag found - $descTags = array(); + $descTags = []; // find all tags in the form of #tag in the description preg_match_all( '/(?getTagsString($this->conf->get('general.tags_separator', ' ')); - $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') .'\\'; + $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') . '\\'; + $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') . '\\'; + $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') . '\\'; + $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') . '\\'; $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; $nextField = $lengths['title']['end'] + 1; diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index f40fa476..c78dbe41 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php @@ -112,12 +112,12 @@ class BookmarkIO if (is_file($this->datastore) && !is_writeable($this->datastore)) { // The datastore exists but is not writeable throw new NotWritableDataStoreException($this->datastore); - } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { + } elseif (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { // The datastore does not exist and its parent directory is not writeable throw new NotWritableDataStoreException(dirname($this->datastore)); } - $data = self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix; + $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix; $this->mutex->synchronized(function () use ($data) { file_put_contents( diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 98dd3f1c..2240f58c 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -39,7 +39,7 @@ class BookmarkInitializer $bookmark->setTitle('Calm Jazz Music - YouTube ' . t('(private bookmark with thumbnail demo)')); $bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c'); $bookmark->setDescription(t( -'Shaarli will automatically pick up the thumbnail for links to a variety of websites. + 'Shaarli will automatically pick up the thumbnail for links to a variety of websites. Explore your new Shaarli instance by trying out controls and menus. Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli. @@ -54,7 +54,7 @@ Now you can edit or delete the default shaares. $bookmark = new Bookmark(); $bookmark->setTitle(t('Note: Shaare descriptions')); $bookmark->setDescription(t( -'Adding a shaare without entering a URL creates a text-only "note" post such as this one. + 'Adding a shaare without entering a URL creates a text-only "note" post such as this one. This note is private, so you are the only one able to see it while logged in. You can use this to keep notes, post articles, code snippets, and much more. @@ -91,7 +91,7 @@ Markdown also supports tables: 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service') ); $bookmark->setDescription(t( -'Welcome to Shaarli! + 'Welcome to Shaarli! Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately. You can add a description to your bookmarks, such as this one, and tag them. diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index cf97e3b0..d65e97ed 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -67,14 +67,15 @@ function html_extract_tag($tag, $html) $propertiesKey = ['property', 'name', 'itemprop']; $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 . '[^\'"]*?[\'"]'; + $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; // Try to retrieve OpenGraph tag. - $ogRegex = '#]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; + $ogRegex = '#]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; // If the attributes are not in the order property => content (e.g. Github) // New regex to keep this readable... more or less. - $ogRegexReverse = '#]+content=(["\'])([^\1]*?)\1[^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; + $ogRegexReverse = '#]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; - if (preg_match($ogRegex, $html, $matches) > 0 + if ( + preg_match($ogRegex, $html, $matches) > 0 || preg_match($ogRegexReverse, $html, $matches) > 0 ) { return $matches[2]; @@ -116,7 +117,7 @@ function hashtag_autolink($description, $indexUrl = '') * \p{Mn} - any non marking space (accents, umlauts, etc) */ $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; - $replacement = '$1#$2'; + $replacement = '$1#$2'; return preg_replace($regex, $replacement, $description); } diff --git a/application/bookmark/exception/BookmarkNotFoundException.php b/application/bookmark/exception/BookmarkNotFoundException.php index 827a3d35..a91d1efa 100644 --- a/application/bookmark/exception/BookmarkNotFoundException.php +++ b/application/bookmark/exception/BookmarkNotFoundException.php @@ -1,4 +1,5 @@ message = 'This bookmark is not valid'. PHP_EOL; - $this->message .= ' - ID: '. $bookmark->getId() . PHP_EOL; - $this->message .= ' - Title: '. $bookmark->getTitle() . PHP_EOL; - $this->message .= ' - Url: '. $bookmark->getUrl() . PHP_EOL; - $this->message .= ' - ShortUrl: '. $bookmark->getShortUrl() . PHP_EOL; - $this->message .= ' - Created: '. $created . PHP_EOL; + $this->message = 'This bookmark is not valid' . PHP_EOL; + $this->message .= ' - ID: ' . $bookmark->getId() . PHP_EOL; + $this->message .= ' - Title: ' . $bookmark->getTitle() . PHP_EOL; + $this->message .= ' - Url: ' . $bookmark->getUrl() . PHP_EOL; + $this->message .= ' - ShortUrl: ' . $bookmark->getShortUrl() . PHP_EOL; + $this->message .= ' - Created: ' . $created . PHP_EOL; } else { - $this->message = 'The provided data is not a bookmark'. PHP_EOL; + $this->message = 'The provided data is not a bookmark' . PHP_EOL; $this->message .= var_export($bookmark, true); } } diff --git a/application/bookmark/exception/NotWritableDataStoreException.php b/application/bookmark/exception/NotWritableDataStoreException.php index 95f34b50..df91f3bc 100644 --- a/application/bookmark/exception/NotWritableDataStoreException.php +++ b/application/bookmark/exception/NotWritableDataStoreException.php @@ -1,9 +1,7 @@ message = 'Couldn\'t load data from the data store file "'. $dataStore .'". '. + $this->message = 'Couldn\'t load data from the data store file "' . $dataStore . '". ' . 'Your data might be corrupted, or your file isn\'t readable.'; } } diff --git a/application/config/ConfigIO.php b/application/config/ConfigIO.php index 3efe5b6f..a623bc8b 100644 --- a/application/config/ConfigIO.php +++ b/application/config/ConfigIO.php @@ -1,4 +1,5 @@ getConfigFileExt()) && !$isLoggedIn) { @@ -392,7 +393,7 @@ class ConfigManager $this->setEmpty('translation.mode', 'php'); $this->setEmpty('translation.extensions', []); - $this->setEmpty('plugins', array()); + $this->setEmpty('plugins', []); $this->setEmpty('formatter', 'markdown'); } diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php index cad34594..53d6a7a3 100644 --- a/application/config/ConfigPhp.php +++ b/application/config/ConfigPhp.php @@ -1,4 +1,5 @@ legacy key. */ - public static $LEGACY_KEYS_MAPPING = array( + public static $LEGACY_KEYS_MAPPING = [ 'credentials.login' => 'login', 'credentials.hash' => 'hash', 'credentials.salt' => 'salt', @@ -68,7 +69,7 @@ class ConfigPhp implements ConfigIO 'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS', 'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS', 'security.open_shaarli' => 'config.OPEN_SHAARLI', - ); + ]; /** * @inheritdoc @@ -76,12 +77,12 @@ class ConfigPhp implements ConfigIO public function read($filepath) { if (! file_exists($filepath) || ! is_readable($filepath)) { - return array(); + return []; } include $filepath; - $out = array(); + $out = []; foreach (self::$ROOT_KEYS as $key) { $out[$key] = isset($GLOBALS[$key]) ? $GLOBALS[$key] : ''; } @@ -95,7 +96,7 @@ class ConfigPhp implements ConfigIO */ public function write($filepath, $conf) { - $configStr = ' $value) { $configStr .= '$GLOBALS[\'config\'][\'' . $key - .'\'] = ' - .var_export($conf['config'][$key], true).';' + . '\'] = ' + . var_export($conf['config'][$key], true) . ';' . PHP_EOL; } @@ -115,18 +116,19 @@ class ConfigPhp implements ConfigIO foreach ($conf['plugins'] as $key => $value) { $configStr .= '$GLOBALS[\'plugins\'][\'' . $key - .'\'] = ' - .var_export($conf['plugins'][$key], true).';' + . '\'] = ' + . var_export($conf['plugins'][$key], true) . ';' . PHP_EOL; } } - if (!file_put_contents($filepath, $configStr) + if ( + !file_put_contents($filepath, $configStr) || strcmp(file_get_contents($filepath), $configStr) != 0 ) { throw new \Shaarli\Exceptions\IOException( $filepath, - t('Shaarli could not create the config file. '. + t('Shaarli could not create the config file. ' . 'Please make sure Shaarli has the right to write in the folder is it installed in.') ); } diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php index ea8dfbda..6cadef12 100644 --- a/application/config/ConfigPlugin.php +++ b/application/config/ConfigPlugin.php @@ -39,8 +39,8 @@ function save_plugin_config($formData) throw new PluginConfigOrderException(); } - $plugins = array(); - $newEnabledPlugins = array(); + $plugins = []; + $newEnabledPlugins = []; foreach ($formData as $key => $data) { if (startsWith($key, 'order')) { continue; @@ -62,7 +62,7 @@ function save_plugin_config($formData) throw new PluginConfigOrderException(); } - $finalPlugins = array(); + $finalPlugins = []; // Make plugins order continuous. foreach ($plugins as $plugin) { $finalPlugins[] = $plugin; @@ -81,7 +81,7 @@ function save_plugin_config($formData) */ function validate_plugin_order($formData) { - $orders = array(); + $orders = []; foreach ($formData as $key => $value) { // No duplicate order allowed. if (in_array($value, $orders, true)) { diff --git a/application/config/exception/MissingFieldConfigException.php b/application/config/exception/MissingFieldConfigException.php index 9e0a9359..a5f4356a 100644 --- a/application/config/exception/MissingFieldConfigException.php +++ b/application/config/exception/MissingFieldConfigException.php @@ -1,6 +1,5 @@ getNbLinks(count($linksToDisplay), $userInput); // Can't use array_keys() because $link is a LinkDB instance and not a real array. - $keys = array(); + $keys = []; foreach ($linksToDisplay as $key => $value) { $keys[] = $key; } $pageaddr = escape(index_url($this->serverInfo)); $this->formatter->addContextData('index_url', $pageaddr); - $linkDisplayed = array(); + $linkDisplayed = []; for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr); } @@ -176,9 +177,9 @@ class FeedBuilder $data = $this->formatter->format($link); $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl']; if ($this->usePermalinks === true) { - $permalink = ''. t('Direct link') .''; + $permalink = '' . t('Direct link') . ''; } else { - $permalink = ''. t('Permalink') .''; + $permalink = '' . t('Permalink') . ''; } $data['description'] .= PHP_EOL . PHP_EOL . '
— ' . $permalink; diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index f7714be9..052333ca 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php @@ -71,7 +71,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter $processedDescription = $this->replaceTokens($processedDescription); if (!empty($processedDescription)) { - $processedDescription = '
'. $processedDescription . '
'; + $processedDescription = '
' . $processedDescription . '
'; } return $processedDescription; @@ -110,7 +110,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter function ($match) use ($allowedProtocols, $indexUrl) { $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; $link .= whitelist_protocols($match[1], $allowedProtocols); - return ']('. $link.')'; + return '](' . $link . ')'; }, $description ); @@ -137,7 +137,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter * \p{Mn} - any non marking space (accents, umlauts, etc) */ $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; - $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)'; + $replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)'; $descriptionLines = explode(PHP_EOL, $description); $descriptionOut = ''; @@ -178,17 +178,17 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter */ protected function sanitizeHtml($description) { - $escapeTags = array( + $escapeTags = [ 'script', 'style', 'link', 'iframe', 'frameset', 'frame', - ); + ]; foreach ($escapeTags as $tag) { $description = preg_replace_callback( - '#<\s*'. $tag .'[^>]*>(.*]*>)?#is', + '#<\s*' . $tag . '[^>]*>(.*]*>)?#is', function ($match) { return escape($match[0]); }, diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php index bc372273..4ff07cdf 100644 --- a/application/formatter/BookmarkRawFormatter.php +++ b/application/formatter/BookmarkRawFormatter.php @@ -10,4 +10,6 @@ namespace Shaarli\Formatter; * * @package Shaarli\Formatter */ -class BookmarkRawFormatter extends BookmarkFormatter {} +class BookmarkRawFormatter extends BookmarkFormatter +{ +} diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php index a029579f..bb865aed 100644 --- a/application/formatter/FormatterFactory.php +++ b/application/formatter/FormatterFactory.php @@ -41,7 +41,7 @@ class FormatterFactory public function getFormatter(string $type = null): BookmarkFormatter { $type = $type ? $type : $this->conf->get('formatter', 'default'); - $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; + $className = '\\Shaarli\\Formatter\\Bookmark' . ucfirst($type) . 'Formatter'; if (!class_exists($className)) { $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter'; } diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index d1aa1399..164217f4 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -42,7 +42,8 @@ class ShaarliMiddleware $this->initBasePath($request); try { - if (!is_file($this->container->conf->getConfigFileExt()) + if ( + !is_file($this->container->conf->getConfigFileExt()) && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true) ) { return $response->withRedirect($this->container->basePath . '/install'); @@ -86,7 +87,8 @@ class ShaarliMiddleware */ protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool { - if (// if the user isn't logged in + if ( +// if the user isn't logged in !$this->container->loginManager->isLoggedIn() // and Shaarli doesn't have public content... && $this->container->conf->get('privacy.hide_public_links') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 0ed7ad81..eb26ef21 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -51,7 +51,7 @@ class ConfigureController extends ShaarliAdminController $this->assignView('languages', Languages::getAvailableLanguages()); $this->assignView('gd_enabled', extension_loaded('gd')); $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); - $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('pagetitle', t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); return $response->write($this->render(TemplatePage::CONFIGURE)); } @@ -95,12 +95,13 @@ class ConfigureController extends ShaarliAdminController } $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; - if ($thumbnailsMode !== Thumbnailer::MODE_NONE + if ( + $thumbnailsMode !== Thumbnailer::MODE_NONE && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) ) { $this->saveWarningMessage( t('You have enabled or changed thumbnails mode.') . - '' . t('Please synchronize them.') .'' + '' . t('Please synchronize them.') . '' ); } $this->container->conf->set('thumbnails.mode', $thumbnailsMode); diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php index 2be957fa..f01d7e9b 100644 --- a/application/front/controller/admin/ExportController.php +++ b/application/front/controller/admin/ExportController.php @@ -23,7 +23,7 @@ class ExportController extends ShaarliAdminController */ public function index(Request $request, Response $response): Response { - $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('pagetitle', t('Export') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); return $response->write($this->render(TemplatePage::EXPORT)); } @@ -68,7 +68,7 @@ class ExportController extends ShaarliAdminController $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); $response = $response->withHeader( 'Content-disposition', - 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' + 'attachment; filename=bookmarks_' . $selection . '_' . $now->format(Bookmark::LINK_DATE_FORMAT) . '.html' ); $this->assignView('date', $now->format(DateTime::RFC822)); diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php index 758d5ef9..c2ad6a09 100644 --- a/application/front/controller/admin/ImportController.php +++ b/application/front/controller/admin/ImportController.php @@ -38,7 +38,7 @@ class ImportController extends ShaarliAdminController true ) ); - $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('pagetitle', t('Import') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); return $response->write($this->render(TemplatePage::IMPORT)); } @@ -64,7 +64,7 @@ class ImportController extends ShaarliAdminController $msg = sprintf( t( 'The file you are trying to upload is probably bigger than what this webserver can accept' - .' (%s). Please upload in smaller chunks.' + . ' (%s). Please upload in smaller chunks.' ), get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) ); diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index 22fb461c..8675a0c5 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php @@ -32,7 +32,7 @@ class ManageTagController extends ShaarliAdminController $this->assignView('tags_separator', $separator); $this->assignView( 'pagetitle', - t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Manage tags') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::CHANGE_TAG)); @@ -87,7 +87,7 @@ class ManageTagController extends ShaarliAdminController $this->saveSuccessMessage($alert); - $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag); + $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags=' . urlencode($toTag); return $this->redirect($response, $redirect); } diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php index 5ec0d24b..4aaf1f82 100644 --- a/application/front/controller/admin/PasswordController.php +++ b/application/front/controller/admin/PasswordController.php @@ -25,7 +25,7 @@ class PasswordController extends ShaarliAdminController $this->assignView( 'pagetitle', - t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Change password') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); } @@ -78,7 +78,7 @@ class PasswordController extends ShaarliAdminController // Save new password // Salt renders rainbow-tables attacks useless. - $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); + $this->container->conf->set('credentials.salt', sha1(uniqid('', true) . '_' . mt_rand())); $this->container->conf->set( 'credentials.hash', sha1( diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index 8e059681..ae47c1af 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php @@ -42,7 +42,7 @@ class PluginsController extends ShaarliAdminController $this->assignView('disabledPlugins', $disabledPlugins); $this->assignView( 'pagetitle', - t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Plugin Administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); @@ -64,7 +64,7 @@ class PluginsController extends ShaarliAdminController unset($parameters['parameters_form']); unset($parameters['token']); foreach ($parameters as $param => $value) { - $this->container->conf->set('plugins.'. $param, escape($value)); + $this->container->conf->set('plugins.' . $param, escape($value)); } } else { $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index bfc99422..80997940 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -65,7 +65,7 @@ class ServerController extends ShaarliAdminController $this->saveWarningMessage( t('Thumbnails cache has been cleared.') . ' ' . - '' . t('Please synchronize them.') .'' + '' . t('Please synchronize them.') . '' ); } else { $folders = [ diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php index d9a7a2e0..0917b6d2 100644 --- a/application/front/controller/admin/SessionFilterController.php +++ b/application/front/controller/admin/SessionFilterController.php @@ -45,6 +45,4 @@ class SessionFilterController extends ShaarliAdminController return $this->redirectFromReferer($request, $response, ['visibility']); } - - } diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php index 8dc386b2..ab8e7f40 100644 --- a/application/front/controller/admin/ShaareAddController.php +++ b/application/front/controller/admin/ShaareAddController.php @@ -23,7 +23,7 @@ class ShaareAddController extends ShaarliAdminController $this->assignView( 'pagetitle', - t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Shaare a new link') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); $this->assignView('tags', $tags); $this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false)); diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php index 0b143172..35837baa 100644 --- a/application/front/controller/admin/ShaareManageController.php +++ b/application/front/controller/admin/ShaareManageController.php @@ -54,7 +54,7 @@ class ShaareManageController extends ShaarliAdminController $data = $formatter->format($bookmark); $this->executePageHooks('delete_link', $data); $this->container->bookmarkService->remove($bookmark, false); - ++ $count; + ++$count; } if ($count > 0) { diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 625a5680..4cbfcdc5 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -118,7 +118,8 @@ class ShaarePublishController extends ShaarliAdminController $this->container->conf->get('general.tags_separator', ' ') ); - if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + if ( + $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE && true !== $this->container->conf->get('general.enable_async_metadata', true) && $bookmark->shouldUpdateThumbnail() ) { @@ -148,7 +149,8 @@ class ShaarePublishController extends ShaarliAdminController return $this->redirectFromReferer( $request, $response, - ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], + ['/admin/add-shaare', '/admin/shaare'], + ['addlink', 'post', 'edit_link'], $bookmark->getShortUrl() ); } @@ -168,10 +170,10 @@ class ShaarePublishController extends ShaarliAdminController $this->assignView($key, $value); } - $editLabel = false === $isNew ? t('Edit') .' ' : ''; + $editLabel = false === $isNew ? t('Edit') . ' ' : ''; $this->assignView( 'pagetitle', - $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') + $editLabel . t('Shaare') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::EDIT_LINK)); @@ -194,7 +196,8 @@ class ShaarePublishController extends ShaarliAdminController // If this is an HTTP(S) link, we try go get the page to extract // the title (otherwise we will to straight to the edit form.) - if (true !== $this->container->conf->get('general.enable_async_metadata', true) + if ( + true !== $this->container->conf->get('general.enable_async_metadata', true) && empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false ) { diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php index 4dc09d38..94d97d4b 100644 --- a/application/front/controller/admin/ThumbnailsController.php +++ b/application/front/controller/admin/ThumbnailsController.php @@ -34,7 +34,7 @@ class ThumbnailsController extends ShaarliAdminController $this->assignView('ids', $ids); $this->assignView( 'pagetitle', - t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Thumbnails update') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::THUMBNAILS)); diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index a87f20d2..560e5e3e 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php @@ -28,7 +28,7 @@ class ToolsController extends ShaarliAdminController $this->assignView($key, $value); } - $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('pagetitle', t('Tools') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); return $response->write($this->render(TemplatePage::TOOLS)); } diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index cc3837ce..fe8231be 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -35,7 +35,8 @@ class BookmarkListController extends ShaarliVisitorController $formatter->addContextData('base_path', $this->container->basePath); $searchTags = normalize_spaces($request->getParam('searchtags') ?? ''); - $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));; + $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? '')); + ; // Filter bookmarks according search parameters. $visibility = $this->container->sessionManager->getSessionParameter('visibility'); @@ -160,7 +161,7 @@ class BookmarkListController extends ShaarliVisitorController $data = array_merge( $this->initializeTemplateVars(), [ - 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'), + 'pagetitle' => $bookmark->getTitle() . ' - ' . $this->container->conf->get('general.title', 'Shaarli'), 'links' => [$formatter->format($bookmark)], ] ); @@ -185,7 +186,8 @@ class BookmarkListController extends ShaarliVisitorController $bookmark->setThumbnail(null); // Requires an update, not async retrieval, thumbnails enabled - if ($bookmark->shouldUpdateThumbnail() + if ( + $bookmark->shouldUpdateThumbnail() && true !== $this->container->conf->get('general.enable_async_metadata', true) && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE ) { diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 728bc2d8..846cfe22 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -132,7 +132,7 @@ class DailyController extends ShaarliVisitorController 'date' => $endDateTime, 'date_rss' => $endDateTime->format(DateTime::RSS), 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), - 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day, + 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day, 'links' => [], ]; diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php index 8d8b546a..edc7ef43 100644 --- a/application/front/controller/visitor/FeedController.php +++ b/application/front/controller/visitor/FeedController.php @@ -27,7 +27,7 @@ class FeedController extends ShaarliVisitorController protected function processRequest(string $feedType, Request $request, Response $response): Response { - $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8'); + $response = $response->withHeader('Content-Type', 'application/' . $feedType . '+xml; charset=utf-8'); $pageUrl = page_url($this->container->environment); $cache = $this->container->pageCacheManager->getCachePage($pageUrl); diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 22329294..bf965929 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -39,7 +39,8 @@ class InstallController extends ShaarliVisitorController // Before installation, we'll make sure that permissions are set properly, and sessions are working. $this->checkPermissions(); - if (static::SESSION_TEST_VALUE + if ( + static::SESSION_TEST_VALUE !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) ) { $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); @@ -75,17 +76,18 @@ class InstallController extends ShaarliVisitorController // This part makes sure sessions works correctly. // (Because on some hosts, session.save_path may not be set correctly, // or we may not have write access to it.) - if (static::SESSION_TEST_VALUE + if ( + static::SESSION_TEST_VALUE !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) ) { // Step 2: Check if data in session is correct. $msg = t( - '
Sessions do not seem to work correctly on your server.
'. - 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. - 'and that you have write access to it.
'. - 'It currently points to %s.
'. - 'On some browsers, accessing your server via a hostname like \'localhost\' '. - 'or any custom hostname without a dot causes cookie storage to fail. '. + '
Sessions do not seem to work correctly on your server.
' . + 'Make sure the variable "session.save_path" is set correctly in your PHP config, ' . + 'and that you have write access to it.
' . + 'It currently points to %s.
' . + 'On some browsers, accessing your server via a hostname like \'localhost\' ' . + 'or any custom hostname without a dot causes cookie storage to fail. ' . 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
' ); $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); @@ -104,7 +106,8 @@ class InstallController extends ShaarliVisitorController public function save(Request $request, Response $response): Response { $timezone = 'UTC'; - if (!empty($request->getParam('continent')) + if ( + !empty($request->getParam('continent')) && !empty($request->getParam('city')) && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) ) { @@ -114,7 +117,7 @@ class InstallController extends ShaarliVisitorController $login = $request->getParam('setlogin'); $this->container->conf->set('credentials.login', $login); - $salt = sha1(uniqid('', true) .'_'. mt_rand()); + $salt = sha1(uniqid('', true) . '_' . mt_rand()); $this->container->conf->set('credentials.salt', $salt); $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); @@ -123,7 +126,7 @@ class InstallController extends ShaarliVisitorController } else { $this->container->conf->set( 'general.title', - 'Shared bookmarks on '.escape(index_url($this->container->environment)) + 'Shared bookmarks on ' . escape(index_url($this->container->environment)) ); } diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index f5038fe3..4b881535 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -43,7 +43,7 @@ class LoginController extends ShaarliVisitorController $this ->assignView('returnurl', escape($returnUrl)) ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) - ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) + ->assignView('pagetitle', t('Login') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')) ; return $response->write($this->render(TemplatePage::LOGIN)); @@ -64,7 +64,8 @@ class LoginController extends ShaarliVisitorController return $this->redirect($response, '/'); } - if (!$this->container->loginManager->checkCredentials( + if ( + !$this->container->loginManager->checkCredentials( client_ip_id($this->container->environment), $request->getParam('login'), $request->getParam('password') @@ -101,7 +102,8 @@ class LoginController extends ShaarliVisitorController */ protected function checkLoginState(): bool { - if ($this->container->loginManager->isLoggedIn() + if ( + $this->container->loginManager->isLoggedIn() || $this->container->conf->get('security.open_shaarli', false) ) { throw new CantLoginException(); diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php index 3c57f8dd..23553ee6 100644 --- a/application/front/controller/visitor/PictureWallController.php +++ b/application/front/controller/visitor/PictureWallController.php @@ -26,7 +26,7 @@ class PictureWallController extends ShaarliVisitorController $this->assignView( 'pagetitle', - t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Picture wall') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); // Optionally filter the results: diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index 54f9fe03..ae946c59 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -144,7 +144,8 @@ abstract class ShaarliVisitorController if (null !== $referer) { $currentUrl = parse_url($referer); // If the referer is not related to Shaarli instance, redirect to default - if (isset($currentUrl['host']) + if ( + isset($currentUrl['host']) && strpos(index_url($this->container->environment), $currentUrl['host']) === false ) { return $response->withRedirect($defaultPath); @@ -173,7 +174,7 @@ abstract class ShaarliVisitorController } } - $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; + $queryString = count($params) > 0 ? '?' . http_build_query($params) : ''; $anchor = $anchor ? '#' . $anchor : ''; return $response->withRedirect($path . $queryString . $anchor); diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php index 560cad08..46d62779 100644 --- a/application/front/controller/visitor/TagCloudController.php +++ b/application/front/controller/visitor/TagCloudController.php @@ -84,10 +84,10 @@ class TagCloudController extends ShaarliVisitorController $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type); $this->assignAllView($data); - $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) .' - ' : ''; + $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) . ' - ' : ''; $this->assignView( 'pagetitle', - $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') + $searchTags . t('Tag ' . $type) . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render('tag.' . $type)); diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php index 7a3377a7..3aa58542 100644 --- a/application/front/controller/visitor/TagController.php +++ b/application/front/controller/visitor/TagController.php @@ -27,7 +27,7 @@ class TagController extends ShaarliVisitorController // In case browser does not send HTTP_REFERER, we search a single tag if (null === $referer) { if (null !== $newTag) { - return $this->redirect($response, '/?searchtags='. urlencode($newTag)); + return $this->redirect($response, '/?searchtags=' . urlencode($newTag)); } return $this->redirect($response, '/'); @@ -37,7 +37,7 @@ class TagController extends ShaarliVisitorController parse_str($currentUrl['query'] ?? '', $params); if (null === $newTag) { - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); } // Prevent redirection loop @@ -68,7 +68,7 @@ class TagController extends ShaarliVisitorController // We also remove page (keeping the same page has no sense, since the results are different) unset($params['page']); - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); } /** @@ -90,7 +90,7 @@ class TagController extends ShaarliVisitorController parse_str($currentUrl['query'] ?? '', $params); if (null === $tagToRemove) { - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); } // Prevent redirection loop diff --git a/application/helper/ApplicationUtils.php b/application/helper/ApplicationUtils.php index 4b34e114..212dd8e2 100644 --- a/application/helper/ApplicationUtils.php +++ b/application/helper/ApplicationUtils.php @@ -1,4 +1,5 @@ '; @@ -64,8 +65,8 @@ class ApplicationUtils } return str_replace( - array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), - array('', '', ''), + [self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL], + ['', '', ''], $data ); } @@ -184,13 +185,15 @@ class ApplicationUtils $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); // Check script and template directories are readable - foreach ([ - 'application', - 'inc', - 'plugins', - $rainTplDir, - $rainTplDir . '/' . $conf->get('resource.theme'), - ] as $path) { + foreach ( + [ + 'application', + 'inc', + 'plugins', + $rainTplDir, + $rainTplDir . '/' . $conf->get('resource.theme'), + ] as $path + ) { if (!is_readable(realpath($path))) { $errors[] = '"' . $path . '" ' . t('directory is not readable'); } @@ -203,10 +206,10 @@ class ApplicationUtils ]; } else { $folders = [ - $conf->get('resource.thumbnails_cache'), - $conf->get('resource.data_dir'), - $conf->get('resource.page_cache'), - $conf->get('resource.raintpl_tmp'), + $conf->get('resource.thumbnails_cache'), + $conf->get('resource.data_dir'), + $conf->get('resource.page_cache'), + $conf->get('resource.raintpl_tmp'), ]; } @@ -224,13 +227,15 @@ class ApplicationUtils } // Check configuration files are readable and writable - foreach (array( - $conf->getConfigFileExt(), - $conf->get('resource.datastore'), - $conf->get('resource.ban_file'), - $conf->get('resource.log'), - $conf->get('resource.update_check'), - ) as $path) { + foreach ( + [ + $conf->getConfigFileExt(), + $conf->get('resource.datastore'), + $conf->get('resource.ban_file'), + $conf->get('resource.log'), + $conf->get('resource.update_check'), + ] as $path + ) { if (!is_file(realpath($path))) { # the file may not exist yet continue; diff --git a/application/helper/FileUtils.php b/application/helper/FileUtils.php index 2eac0793..e8a2168c 100644 --- a/application/helper/FileUtils.php +++ b/application/helper/FileUtils.php @@ -105,7 +105,7 @@ class FileUtils } foreach (new \DirectoryIterator($path) as $file) { - if($file->isDot()) { + if ($file->isDot()) { continue; } @@ -116,7 +116,7 @@ class FileUtils if ($file->isFile()) { unlink($file->getPathname()); - } elseif($file->isDir()) { + } elseif ($file->isDir()) { $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped; } } diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php index ed1002b0..4bde1d5b 100644 --- a/application/http/HttpUtils.php +++ b/application/http/HttpUtils.php @@ -48,7 +48,7 @@ function get_http_response( $cleanUrl = $urlObj->idnToAscii(); if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) { - return array(array(0 => 'Invalid HTTP UrlUtils'), false); + return [[0 => 'Invalid HTTP UrlUtils'], false]; } $userAgent = @@ -71,7 +71,7 @@ function get_http_response( $ch = curl_init($cleanUrl); if ($ch === false) { - return array(array(0 => 'curl_init() error'), false); + return [[0 => 'curl_init() error'], false]; } // General cURL settings @@ -82,7 +82,7 @@ function get_http_response( curl_setopt( $ch, CURLOPT_HTTPHEADER, - array('Accept-Language: ' . $acceptLanguage) + ['Accept-Language: ' . $acceptLanguage] ); curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -90,7 +90,7 @@ function get_http_response( curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); // Max download size management - curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16); + curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024 * 16); curl_setopt($ch, CURLOPT_NOPROGRESS, false); if (is_callable($curlHeaderFunction)) { curl_setopt($ch, CURLOPT_HEADERFUNCTION, $curlHeaderFunction); @@ -122,9 +122,9 @@ function get_http_response( * Removing this would require updating * GetHttpUrlTest::testGetInvalidRemoteUrl() */ - return array(false, false); + return [false, false]; } - return array(array(0 => 'curl_exec() error: ' . $errorStr), false); + return [[0 => 'curl_exec() error: ' . $errorStr], false]; } // Formatting output like the fallback method @@ -135,7 +135,7 @@ function get_http_response( $rawHeadersLastRedir = end($rawHeadersArrayRedirs); $content = substr($response, $headSize); - $headers = array(); + $headers = []; foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) { if (empty($line) || ctype_space($line)) { continue; @@ -146,7 +146,7 @@ function get_http_response( $value = $splitLine[1]; if (array_key_exists($key, $headers)) { if (!is_array($headers[$key])) { - $headers[$key] = array(0 => $headers[$key]); + $headers[$key] = [0 => $headers[$key]]; } $headers[$key][] = $value; } else { @@ -157,7 +157,7 @@ function get_http_response( } } - return array($headers, $content); + return [$headers, $content]; } /** @@ -188,15 +188,15 @@ function get_http_response_fallback( $acceptLanguage, $maxRedr ) { - $options = array( - 'http' => array( + $options = [ + 'http' => [ 'method' => 'GET', 'timeout' => $timeout, 'user_agent' => $userAgent, 'header' => "Accept: */*\r\n" . 'Accept-Language: ' . $acceptLanguage - ) - ); + ] + ]; stream_context_set_default($options); list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); @@ -207,7 +207,7 @@ function get_http_response_fallback( } if (! $headers) { - return array($headers, false); + return [$headers, false]; } try { @@ -215,10 +215,10 @@ function get_http_response_fallback( $context = stream_context_create($options); $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes); } catch (Exception $exc) { - return array(array(0 => 'HTTP Error'), $exc->getMessage()); + return [[0 => 'HTTP Error'], $exc->getMessage()]; } - return array($headers, $content); + return [$headers, $content]; } /** @@ -237,10 +237,12 @@ function get_redirected_headers($url, $redirectionLimit = 3) } // Headers found, redirection found, and limit not reached. - if ($redirectionLimit-- > 0 + if ( + $redirectionLimit-- > 0 && !empty($headers) && (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false) - && !empty($headers['Location'])) { + && !empty($headers['Location']) + ) { $redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location']; if ($redirection != $url) { $redirection = getAbsoluteUrl($url, $redirection); @@ -248,7 +250,7 @@ function get_redirected_headers($url, $redirectionLimit = 3) } } - return array($headers, $url); + return [$headers, $url]; } /** @@ -270,7 +272,7 @@ function getAbsoluteUrl($originalUrl, $newUrl) } $parts = parse_url($originalUrl); - $final = $parts['scheme'] .'://'. $parts['host']; + $final = $parts['scheme'] . '://' . $parts['host']; $final .= (!empty($parts['port'])) ? $parts['port'] : ''; $final .= '/'; if ($newUrl[0] != '/') { @@ -323,7 +325,8 @@ function server_url($server) $scheme = 'https'; } - if (($scheme == 'http' && $port != '80') + if ( + ($scheme == 'http' && $port != '80') || ($scheme == 'https' && $port != '443') ) { $port = ':' . $port; @@ -344,22 +347,26 @@ function server_url($server) $host = $server['SERVER_NAME']; } - return $scheme.'://'.$host.$port; + return $scheme . '://' . $host . $port; } // SSL detection - if ((! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on') - || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')) { + if ( + (! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on') + || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443') + ) { $scheme = 'https'; } // Do not append standard port values - if (($scheme == 'http' && $server['SERVER_PORT'] != '80') - || ($scheme == 'https' && $server['SERVER_PORT'] != '443')) { - $port = ':'.$server['SERVER_PORT']; + if ( + ($scheme == 'http' && $server['SERVER_PORT'] != '80') + || ($scheme == 'https' && $server['SERVER_PORT'] != '443') + ) { + $port = ':' . $server['SERVER_PORT']; } - return $scheme.'://'.$server['SERVER_NAME'].$port; + return $scheme . '://' . $server['SERVER_NAME'] . $port; } /** @@ -567,7 +574,10 @@ function get_curl_download_callback( * * @return int|bool length of $data or false if we need to stop the download */ - return function ($ch, $data) use ( + return function ( + $ch, + $data + ) use ( $retrieveDescription, $tagsSeparator, &$charset, @@ -601,7 +611,7 @@ function get_curl_download_callback( $foundChunk = $currentChunk; // Keywords use the format tag1, tag2 multiple words, tag // So we split the result with `,`, then if a tag contains the separator we replace it by `-`. - $keywords = tags_array2str(array_map(function(string $keyword) use ($tagsSeparator): string { + $keywords = tags_array2str(array_map(function (string $keyword) use ($tagsSeparator): string { return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-'); }, tags_str2array($keywords, ',')), $tagsSeparator); } @@ -611,7 +621,8 @@ function get_curl_download_callback( // If we already found either the title, description or keywords, // it's highly unlikely that we'll found the other metas further than // in the same chunk of data or the next one. So we also stop the download after that. - if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null + if ( + (!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null && (! $retrieveDescription || $foundChunk < $currentChunk || (!empty($title) && !empty($description) && !empty($keywords)) diff --git a/application/http/Url.php b/application/http/Url.php index 90444a2f..fe87088f 100644 --- a/application/http/Url.php +++ b/application/http/Url.php @@ -17,7 +17,7 @@ namespace Shaarli\Http; */ class Url { - private static $annoyingQueryParams = array( + private static $annoyingQueryParams = [ // Facebook 'action_object_map=', 'action_ref_map=', @@ -37,15 +37,15 @@ class Url // Other 'campaign_' - ); + ]; - private static $annoyingFragments = array( + private static $annoyingFragments = [ // ATInternet 'xtor=RSS-', // Misc. 'tk.rss_all' - ); + ]; /* * URL parts represented as an array @@ -120,7 +120,7 @@ class Url foreach (self::$annoyingQueryParams as $annoying) { foreach ($queryParams as $param) { if (startsWith($param, $annoying)) { - $queryParams = array_diff($queryParams, array($param)); + $queryParams = array_diff($queryParams, [$param]); continue; } } diff --git a/application/http/UrlUtils.php b/application/http/UrlUtils.php index e8d1a283..de5b7db1 100644 --- a/application/http/UrlUtils.php +++ b/application/http/UrlUtils.php @@ -1,4 +1,5 @@ container->loginManager->isLoggedIn()) { $parameters = $buildParameters($request->getQueryParams(), true); - return $this->redirect($response, '/login?returnurl='. $this->getBasePath() . $route . $parameters); + return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route . $parameters); } $parameters = $buildParameters($request->getQueryParams(), false); diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php index 5c02a21b..442b833c 100644 --- a/application/legacy/LegacyLinkDB.php +++ b/application/legacy/LegacyLinkDB.php @@ -240,8 +240,8 @@ class LegacyLinkDB implements Iterator, Countable, ArrayAccess } // Create a dummy database for example - $this->links = array(); - $link = array( + $this->links = []; + $link = [ 'id' => 1, 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'), 'url' => 'https://shaarli.readthedocs.io', @@ -257,11 +257,11 @@ You use the community supported version of the original Shaarli project, by Seba 'created' => new DateTime(), 'tags' => 'opensource software', 'sticky' => false, - ); + ]; $link['shorturl'] = link_small_hash($link['created'], $link['id']); $this->links[1] = $link; - $link = array( + $link = [ 'id' => 0, 'title' => t('My secret stuff... - Pastebin.com'), 'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', @@ -270,7 +270,7 @@ You use the community supported version of the original Shaarli project, by Seba 'created' => new DateTime('1 minute ago'), 'tags' => 'secretstuff', 'sticky' => false, - ); + ]; $link['shorturl'] = link_small_hash($link['created'], $link['id']); $this->links[0] = $link; @@ -285,7 +285,7 @@ You use the community supported version of the original Shaarli project, by Seba { // Public bookmarks are hidden and user not logged in => nothing to show if ($this->hidePublicLinks && !$this->loggedIn) { - $this->links = array(); + $this->links = []; return; } @@ -293,7 +293,7 @@ You use the community supported version of the original Shaarli project, by Seba $this->ids = []; $this->links = FileUtils::readFlatDB($this->datastore, []); - $toremove = array(); + $toremove = []; foreach ($this->links as $key => &$link) { if (!$this->loggedIn && $link['private'] != 0) { // Transition for not upgraded databases. @@ -414,7 +414,7 @@ You use the community supported version of the original Shaarli project, by Seba * @return array filtered bookmarks, all bookmarks if no suitable filter was provided. */ public function filterSearch( - $filterRequest = array(), + $filterRequest = [], $casesensitive = false, $visibility = 'all', $untaggedonly = false @@ -512,7 +512,7 @@ You use the community supported version of the original Shaarli project, by Seba */ public function days() { - $linkDays = array(); + $linkDays = []; foreach ($this->links as $link) { $linkDays[$link['created']->format('Ymd')] = 0; } diff --git a/application/legacy/LegacyLinkFilter.php b/application/legacy/LegacyLinkFilter.php index 7cf93d60..e6d186c4 100644 --- a/application/legacy/LegacyLinkFilter.php +++ b/application/legacy/LegacyLinkFilter.php @@ -120,7 +120,7 @@ class LegacyLinkFilter return $this->links; } - $out = array(); + $out = []; foreach ($this->links as $key => $value) { if ($value['private'] && $visibility === 'private') { $out[$key] = $value; @@ -143,7 +143,7 @@ class LegacyLinkFilter */ private function filterSmallHash($smallHash) { - $filtered = array(); + $filtered = []; foreach ($this->links as $key => $l) { if ($smallHash == $l['shorturl']) { // Yes, this is ugly and slow @@ -186,7 +186,7 @@ class LegacyLinkFilter return $this->noFilter($visibility); } - $filtered = array(); + $filtered = []; $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); $exactRegex = '/"([^"]+)"/'; // Retrieve exact search terms. @@ -198,8 +198,8 @@ class LegacyLinkFilter $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); // Filter excluding terms and update andSearch. - $excludeSearch = array(); - $andSearch = array(); + $excludeSearch = []; + $andSearch = []; foreach ($explodedSearchAnd as $needle) { if ($needle[0] == '-' && strlen($needle) > 1) { $excludeSearch[] = substr($needle, 1); @@ -208,7 +208,7 @@ class LegacyLinkFilter } } - $keys = array('title', 'description', 'url', 'tags'); + $keys = ['title', 'description', 'url', 'tags']; // Iterate over every stored link. foreach ($this->links as $id => $link) { @@ -336,7 +336,7 @@ class LegacyLinkFilter } // create resulting array - $filtered = array(); + $filtered = []; // iterate over each link foreach ($this->links as $key => $link) { @@ -352,7 +352,7 @@ class LegacyLinkFilter $search = $link['tags']; // build search string, start with tags of current link if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) { // description given and at least one possible tag found - $descTags = array(); + $descTags = []; // find all tags in the form of #tag in the description preg_match_all( '/(?links as $key => $l) { if ($l['created']->format('Ymd') == $day) { $filtered[$key] = $l; diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index ed949b1e..9bda54b8 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php @@ -93,7 +93,7 @@ class LegacyUpdater */ public function update() { - $updatesRan = array(); + $updatesRan = []; // If the user isn't logged in, exit without updating. if ($this->isLoggedIn !== true) { @@ -106,7 +106,8 @@ class LegacyUpdater foreach ($this->methods as $method) { // Not an update method or already done, pass. - if (!startsWith($method->getName(), 'updateMethod') + if ( + !startsWith($method->getName(), 'updateMethod') || in_array($method->getName(), $this->doneUpdates) ) { continue; @@ -189,7 +190,7 @@ class LegacyUpdater } // Set sub config keys (config and plugins) - $subConfig = array('config', 'plugins'); + $subConfig = ['config', 'plugins']; foreach ($subConfig as $sub) { foreach ($oldConfig[$sub] as $key => $value) { if (isset($legacyMap[$sub . '.' . $key])) { @@ -259,7 +260,7 @@ class LegacyUpdater $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php'; copy($this->conf->get('resource.datastore'), $save); - $links = array(); + $links = []; foreach ($this->linkDB as $offset => $value) { $links[] = $value; unset($this->linkDB[$offset]); @@ -498,7 +499,8 @@ class LegacyUpdater */ public function updateMethodDownloadSizeAndTimeoutConf() { - if ($this->conf->exists('general.download_max_size') + if ( + $this->conf->exists('general.download_max_size') && $this->conf->exists('general.download_timeout') ) { return true; diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index 6ca728b7..2d97b4c8 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php @@ -59,11 +59,11 @@ class NetscapeBookmarkUtils $indexUrl ) { // see tpl/export.html for possible values - if (!in_array($selection, array('all', 'public', 'private'))) { + if (!in_array($selection, ['all', 'public', 'private'])) { throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"'); } - $bookmarkLinks = array(); + $bookmarkLinks = []; foreach ($this->bookmarkService->search([], $selection) as $bookmark) { $link = $formatter->format($bookmark); $link['taglist'] = implode(',', $bookmark->getTags()); diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index da66dea3..3ea55728 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php @@ -1,4 +1,5 @@ conf = $conf; - $this->errors = array(); + $this->errors = []; } /** @@ -98,7 +99,7 @@ class PluginManager * * @return void */ - public function executeHooks($hook, &$data, $params = array()) + public function executeHooks($hook, &$data, $params = []) { $metadataParameters = [ 'target' => '_PAGE_', @@ -196,7 +197,7 @@ class PluginManager */ public function getPluginsMeta() { - $metaData = array(); + $metaData = []; $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK); // Browse all plugin directories. @@ -217,9 +218,9 @@ class PluginManager if (isset($metaData[$plugin]['parameters'])) { $params = explode(';', $metaData[$plugin]['parameters']); } else { - $params = array(); + $params = []; } - $metaData[$plugin]['parameters'] = array(); + $metaData[$plugin]['parameters'] = []; foreach ($params as $param) { if (empty($param)) { continue; diff --git a/application/plugin/exception/PluginFileNotFoundException.php b/application/plugin/exception/PluginFileNotFoundException.php index e5386f02..21ac6604 100644 --- a/application/plugin/exception/PluginFileNotFoundException.php +++ b/application/plugin/exception/PluginFileNotFoundException.php @@ -1,4 +1,5 @@ trustedProxies = $trustedProxies; $this->nbAttempts = $nbAttempts; $this->banDuration = $banDuration; @@ -80,7 +80,7 @@ class BanManager if ($this->failures[$ip] >= $this->nbAttempts) { $this->bans[$ip] = time() + $this->banDuration; - $this->logger->info(format_log('IP address banned from login: '. $ip, $ip)); + $this->logger->info(format_log('IP address banned from login: ' . $ip, $ip)); } $this->writeBanFile(); } @@ -136,7 +136,7 @@ class BanManager unset($this->failures[$ip]); } unset($this->bans[$ip]); - $this->logger->info(format_log('Ban lifted for: '. $ip, $ip)); + $this->logger->info(format_log('Ban lifted for: ' . $ip, $ip)); $this->writeBanFile(); return false; diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 426e785e..b795b80e 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -1,4 +1,5 @@ sessionManager->storeLoginInfo($clientIpId); - } elseif ($this->sessionManager->hasSessionExpired() + } elseif ( + $this->sessionManager->hasSessionExpired() || $this->sessionManager->hasClientIpChanged($clientIpId) ) { $this->sessionManager->logout(); @@ -145,7 +147,8 @@ class LoginManager // Check credentials try { $useLdapLogin = !empty($this->configManager->get('ldap.host')); - if ($login === $this->configManager->get('credentials.login') + if ( + $login === $this->configManager->get('credentials.login') && ( (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) @@ -156,7 +159,7 @@ class LoginManager return true; } - } catch(Exception $exception) { + } catch (Exception $exception) { $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId)); } @@ -174,7 +177,8 @@ class LoginManager * * @return bool true if the provided credentials are valid, false otherwise */ - public function checkCredentialsFromLocalConfig($login, $password) { + public function checkCredentialsFromLocalConfig($login, $password) + { $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); return $login == $this->configManager->get('credentials.login') @@ -193,14 +197,14 @@ class LoginManager */ public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null) { - $connect = $connect ?? function($host) { + $connect = $connect ?? function ($host) { $resource = ldap_connect($host); ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3); return $resource; }; - $bind = $bind ?? function($handle, $dn, $password) { + $bind = $bind ?? function ($handle, $dn, $password) { return ldap_bind($handle, $dn, $password); }; diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 96bf193c..f957b91a 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -1,4 +1,5 @@ conf->get('credentials.salt')); + $token = sha1(uniqid('', true) . '_' . mt_rand() . $this->conf->get('credentials.salt')); $this->session['tokens'][$token] = 1; return $token; } diff --git a/application/updater/Updater.php b/application/updater/Updater.php index 88a7bc7b..3451cf36 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -88,7 +88,8 @@ class Updater foreach ($this->methods as $method) { // Not an update method or already done, pass. - if (! startsWith($method->getName(), 'updateMethod') + if ( + ! startsWith($method->getName(), 'updateMethod') || in_array($method->getName(), $this->doneUpdates) ) { continue; @@ -152,7 +153,8 @@ class Updater $updated = false; foreach ($this->bookmarkService->search() as $bookmark) { - if ($bookmark->isNote() + if ( + $bookmark->isNote() && startsWith($bookmark->getUrl(), '?') && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) ) { diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php index 828a49fc..908bdc39 100644 --- a/application/updater/UpdaterUtils.php +++ b/application/updater/UpdaterUtils.php @@ -19,7 +19,7 @@ class UpdaterUtils return explode(';', $content); } } - return array(); + return []; } /** @@ -38,7 +38,7 @@ class UpdaterUtils $res = file_put_contents($updatesFilepath, implode(';', $updates)); if ($res === false) { - throw new \Exception('Unable to write updates in '. $updatesFilepath . '.'); + throw new \Exception('Unable to write updates in ' . $updatesFilepath . '.'); } } } diff --git a/index.php b/index.php index 8fe86236..1eb7659a 100644 --- a/index.php +++ b/index.php @@ -1,4 +1,5 @@ setEmpty('general.timezone', date_default_timezone_get()); -$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER))); +$conf->setEmpty('general.title', t('Shared bookmarks on ') . escape(index_url($_SERVER))); -RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory +RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl') . '/' . $conf->get('resource.theme') . '/'; // template directory RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory date_default_timezone_set($conf->get('general.timezone', 'UTC')); @@ -177,6 +178,6 @@ try { } catch (Throwable $e) { die(nl2br( 'An unexpected error happened, and the error template could not be displayed.' . PHP_EOL . PHP_EOL . - exception2text($e) + exception2text($e) )); } diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php index ab6ed6de..80b1dd95 100644 --- a/plugins/addlink_toolbar/addlink_toolbar.php +++ b/plugins/addlink_toolbar/addlink_toolbar.php @@ -17,26 +17,26 @@ use Shaarli\Render\TemplatePage; function hook_addlink_toolbar_render_header($data) { if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) { - $form = array( - 'attr' => array( + $form = [ + 'attr' => [ 'method' => 'GET', 'action' => $data['_BASE_PATH_'] . '/admin/shaare', 'name' => 'addform', 'class' => 'addform', - ), - 'inputs' => array( - array( + ], + 'inputs' => [ + [ 'type' => 'text', 'name' => 'post', 'placeholder' => t('URI'), - ), - array( + ], + [ 'type' => 'submit', 'value' => t('Add link'), 'class' => 'bigbutton', - ), - ), - ); + ], + ], + ]; $data['fields_toolbar'][] = $form; } diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php index ed271532..88f2b653 100644 --- a/plugins/archiveorg/archiveorg.php +++ b/plugins/archiveorg/archiveorg.php @@ -1,4 +1,5 @@ get('plugins.'. $placeholder, '')); + $value = trim($conf->get('plugins.' . $placeholder, '')); if (strlen($value) > 0) { $params[$placeholder] = $value; } } if (empty($params)) { - $error = t('Default colors plugin error: '. + $error = t('Default colors plugin error: ' . 'This plugin is active and no custom color is configured.'); return [$error]; } @@ -56,7 +56,7 @@ function default_colors_init($conf) function hook_default_colors_render_includes($data) { $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css'; - if (file_exists($file )) { + if (file_exists($file)) { $data['css_files'][] = $file ; } @@ -75,7 +75,7 @@ function default_colors_generate_css_file($params): void $content = ''; foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) { $content .= !empty($params[$rule]) - ? default_colors_format_css_rule($params, $rule) .';'. PHP_EOL + ? default_colors_format_css_rule($params, $rule) . ';' . PHP_EOL : ''; } @@ -99,8 +99,8 @@ function default_colors_format_css_rule($data, $parameter) } $key = str_replace('DEFAULT_COLORS_', '', $parameter); - $key = str_replace('_', '-', strtolower($key)) .'-color'; - return ' --'. $key .': '. $data[$parameter]; + $key = str_replace('_', '-', strtolower($key)) . '-color'; + return ' --' . $key . ': ' . $data[$parameter]; } diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php index defb01f7..22d27b68 100644 --- a/plugins/demo_plugin/demo_plugin.php +++ b/plugins/demo_plugin/demo_plugin.php @@ -1,4 +1,5 @@ array ( + $button = [ + 'attr' => [ 'href' => '#', 'class' => 'mybutton', 'title' => 'hover me', - ), + ], 'html' => 'DEMO buttons toolbar', - ); + ]; $data['buttons_toolbar'][] = $button; } @@ -115,29 +116,29 @@ function hook_demo_plugin_render_header($data) * * */ - $form = array( - 'attr' => array( + $form = [ + 'attr' => [ 'method' => 'GET', 'action' => $data['_BASE_PATH_'] . '/', 'class' => 'addform', - ), - 'inputs' => array( - array( + ], + 'inputs' => [ + [ 'type' => 'text', 'name' => 'demo', 'placeholder' => 'demo', - ) - ) - ); + ] + ] + ]; $data['fields_toolbar'][] = $form; } // Another button always displayed - $button = array( - 'attr' => array( + $button = [ + 'attr' => [ 'href' => '#', - ), + ], 'html' => 'Demo', - ); + ]; $data['buttons_toolbar'][] = $button; return $data; @@ -187,7 +188,7 @@ function hook_demo_plugin_render_includes($data) function hook_demo_plugin_render_footer($data) { // Footer text - $data['text'][] = '
'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); + $data['text'][] = '
' . demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); // Free elements at the end of the page. $data['endofpage'][] = '' . @@ -229,13 +230,13 @@ function hook_demo_plugin_render_linklist($data) * and a mandatory `html` key, which contains its value. * It's also recommended to add key 'on' or 'off' for theme rendering. */ - $action = array( - 'attr' => array( + $action = [ + 'attr' => [ 'href' => '?up', 'title' => 'Uppercase!', - ), + ], 'html' => '←', - ); + ]; if (isset($_GET['up'])) { // Manipulate link data @@ -275,7 +276,7 @@ function hook_demo_plugin_render_linklist($data) function hook_demo_plugin_render_editlink($data) { // Load HTML into a string - $html = file_get_contents(PluginManager::$PLUGINS_PATH .'/demo_plugin/field.html'); + $html = file_get_contents(PluginManager::$PLUGINS_PATH . '/demo_plugin/field.html'); // Replace value in HTML if it exists in $data if (!empty($data['link']['stuff'])) { diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php index d4632163..a5450989 100644 --- a/plugins/isso/isso.php +++ b/plugins/isso/isso.php @@ -19,9 +19,9 @@ function isso_init($conf) { $issoUrl = $conf->get('plugins.ISSO_SERVER'); if (empty($issoUrl)) { - $error = t('Isso plugin error: '. + $error = t('Isso plugin error: ' . 'Please define the "ISSO_SERVER" setting in the plugin administration page.'); - return array($error); + return [$error]; } } @@ -49,12 +49,12 @@ function hook_isso_render_linklist($data, $conf) $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']); $data['plugin_end_zone'][] = $isso; } else { - $button = ''; + $button = ''; // For the default theme we use a FontAwesome icon which is better than an image if ($conf->get('resource.theme') === 'default') { $button .= ''; } else { - $button .= ' array( + $playvideo = [ + 'attr' => [ 'href' => '#', 'title' => t('Video player'), 'id' => 'playvideos', - ), - 'html' => '► '. t('Play Videos') - ); + ], + 'html' => '► ' . t('Play Videos') + ]; $data['buttons_toolbar'][] = $playvideo; } diff --git a/plugins/pubsubhubbub/pubsubhubbub.php b/plugins/pubsubhubbub/pubsubhubbub.php index 8fe6799c..299b84fb 100644 --- a/plugins/pubsubhubbub/pubsubhubbub.php +++ b/plugins/pubsubhubbub/pubsubhubbub.php @@ -42,7 +42,7 @@ function pubsubhubbub_init($conf) function hook_pubsubhubbub_render_feed($data, $conf) { $feedType = $data['_PAGE_'] == TemplatePage::FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM; - $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/pubsubhubbub/hub.'. $feedType .'.xml'); + $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/pubsubhubbub/hub.' . $feedType . '.xml'); $data['feed_plugins_header'][] = sprintf($template, $conf->get('plugins.PUBSUBHUB_URL')); return $data; @@ -59,10 +59,10 @@ function hook_pubsubhubbub_render_feed($data, $conf) */ function hook_pubsubhubbub_save_link($data, $conf) { - $feeds = array( - index_url($_SERVER) .'feed/atom', - index_url($_SERVER) .'feed/rss', - ); + $feeds = [ + index_url($_SERVER) . 'feed/atom', + index_url($_SERVER) . 'feed/rss', + ]; $httpPost = function_exists('curl_version') ? false : 'nocurl_http_post'; try { @@ -87,11 +87,11 @@ function hook_pubsubhubbub_save_link($data, $conf) */ function nocurl_http_post($url, $postString) { - $params = array('http' => array( + $params = ['http' => [ 'method' => 'POST', 'content' => $postString, 'user_agent' => 'PubSubHubbub-Publisher-PHP/1.0', - )); + ]]; $context = stream_context_create($params); $fp = @fopen($url, 'rb', false, $context); diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php index 24fd18ba..2ae10476 100644 --- a/plugins/qrcode/qrcode.php +++ b/plugins/qrcode/qrcode.php @@ -1,4 +1,5 @@ '1.x', 2 => '2.x', - ); + ]; /** * @var array Static reference to WB endpoint according to the API version. * - key: version name. * - value: endpoint. */ - private static $wallabagEndpoints = array( + private static $wallabagEndpoints = [ '1.x' => '?plainurl=', '2.x' => 'bookmarklet?url=', - ); + ]; /** * @var string Wallabag user instance URL. diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php index 8cd3f4ad..f2003cb9 100644 --- a/plugins/wallabag/wallabag.php +++ b/plugins/wallabag/wallabag.php @@ -1,4 +1,5 @@ get('plugins.WALLABAG_URL'); if (empty($wallabagUrl)) { - $error = t('Wallabag plugin error: '. + $error = t('Wallabag plugin error: ' . 'Please define the "WALLABAG_URL" setting in the plugin administration page.'); - return array($error); + return [$error]; } $conf->setEmpty('plugins.WALLABAG_URL', '2'); } From b99e00f7cd5f7e2090f44cd97bfb426db55340c2 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 8 Nov 2020 15:02:45 +0100 Subject: [PATCH 39/50] Manually fix remaining PHPCS errors --- application/History.php | 10 +++++----- application/Languages.php | 6 ++++-- application/Thumbnailer.php | 8 ++++---- application/Utils.php | 2 ++ application/bookmark/Bookmark.php | 2 +- application/bookmark/BookmarkFileService.php | 4 ++-- application/bookmark/BookmarkInitializer.php | 3 +++ application/container/ContainerBuilder.php | 2 +- .../formatter/BookmarkDefaultFormatter.php | 4 ++-- .../formatter/BookmarkMarkdownFormatter.php | 2 +- .../controller/admin/ConfigureController.php | 9 +++++++-- .../front/controller/admin/ServerController.php | 4 +++- application/legacy/LegacyLinkDB.php | 2 +- application/updater/Updater.php | 4 ++-- application/updater/UpdaterUtils.php | 4 ++-- phpcs.xml | 5 +++++ tests/legacy/LegacyUpdaterTest.php | 16 ++++++++-------- tests/updater/UpdaterTest.php | 16 ++++++++-------- 18 files changed, 61 insertions(+), 42 deletions(-) diff --git a/application/History.php b/application/History.php index 1be955c5..d230f39d 100644 --- a/application/History.php +++ b/application/History.php @@ -32,27 +32,27 @@ class History /** * @var string Action key: a new link has been created. */ - const CREATED = 'CREATED'; + public const CREATED = 'CREATED'; /** * @var string Action key: a link has been updated. */ - const UPDATED = 'UPDATED'; + public const UPDATED = 'UPDATED'; /** * @var string Action key: a link has been deleted. */ - const DELETED = 'DELETED'; + public const DELETED = 'DELETED'; /** * @var string Action key: settings have been updated. */ - const SETTINGS = 'SETTINGS'; + public const SETTINGS = 'SETTINGS'; /** * @var string Action key: a bulk import has been processed. */ - const IMPORT = 'IMPORT'; + public const IMPORT = 'IMPORT'; /** * @var string History file path. diff --git a/application/Languages.php b/application/Languages.php index 8d0e13c8..60e91631 100644 --- a/application/Languages.php +++ b/application/Languages.php @@ -41,7 +41,7 @@ class Languages /** * Core translations domain */ - const DEFAULT_DOMAIN = 'shaarli'; + public const DEFAULT_DOMAIN = 'shaarli'; /** * @var TranslatorInterface @@ -122,7 +122,9 @@ class Languages $translations = new Translations(); // Core translations try { - $translations = $translations->addFromPoFile('inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po'); + $translations = $translations->addFromPoFile( + 'inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po' + ); $translations->setDomain('shaarli'); $this->translator->loadTranslations($translations); } catch (\InvalidArgumentException $e) { diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php index 30354310..c4ff8d7a 100644 --- a/application/Thumbnailer.php +++ b/application/Thumbnailer.php @@ -13,7 +13,7 @@ use WebThumbnailer\WebThumbnailer; */ class Thumbnailer { - const COMMON_MEDIA_DOMAINS = [ + protected const COMMON_MEDIA_DOMAINS = [ 'imgur.com', 'flickr.com', 'youtube.com', @@ -31,9 +31,9 @@ class Thumbnailer 'deviantart.com', ]; - const MODE_ALL = 'all'; - const MODE_COMMON = 'common'; - const MODE_NONE = 'none'; + public const MODE_ALL = 'all'; + public const MODE_COMMON = 'common'; + public const MODE_NONE = 'none'; /** * @var WebThumbnailer instance. diff --git a/application/Utils.php b/application/Utils.php index 4c2d6701..952378ab 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -382,8 +382,10 @@ function return_bytes($val) switch ($last) { case 'g': $val *= 1024; + // do no break in order 1024^2 for each unit case 'm': $val *= 1024; + // do no break in order 1024^2 for each unit case 'k': $val *= 1024; } diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index b592722f..4238ef25 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -19,7 +19,7 @@ use Shaarli\Bookmark\Exception\InvalidBookmarkException; class Bookmark { /** @var string Date format used in string (former ID format) */ - const LINK_DATE_FORMAT = 'Ymd_His'; + public const LINK_DATE_FORMAT = 'Ymd_His'; /** @var int Bookmark ID */ protected $id; diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 66248cc2..6666a251 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -409,14 +409,14 @@ class BookmarkFileService implements BookmarkServiceInterface false ); $updater = new LegacyUpdater( - UpdaterUtils::read_updates_file($this->conf->get('resource.updates')), + UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')), $bookmarkDb, $this->conf, true ); $newUpdates = $updater->update(); if (! empty($newUpdates)) { - UpdaterUtils::write_updates_file( + UpdaterUtils::writeUpdatesFile( $this->conf->get('resource.updates'), $updater->getDoneUpdates() ); diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 2240f58c..8ab5c441 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -13,6 +13,9 @@ namespace Shaarli\Bookmark; * To prevent data corruption, it does not overwrite existing bookmarks, * even though there should not be any. * + * We disable this because otherwise it creates indentation issues, and heredoc is not supported by PHP gettext. + * @phpcs:disable Generic.Files.LineLength.TooLong + * * @package Shaarli\Bookmark */ class BookmarkInitializer diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index d84418ad..f0234eca 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -158,7 +158,7 @@ class ContainerBuilder $container['updater'] = function (ShaarliContainer $container): Updater { return new Updater( - UpdaterUtils::read_updates_file($container->conf->get('resource.updates')), + UpdaterUtils::readUpdatesFile($container->conf->get('resource.updates')), $container->bookmarkService, $container->conf, $container->loginManager->isLoggedIn() diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 51bea0f1..7e0afafc 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -12,8 +12,8 @@ namespace Shaarli\Formatter; */ class BookmarkDefaultFormatter extends BookmarkFormatter { - const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; - const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; + protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; + protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; /** * @inheritdoc diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index 052333ca..ee4e8dca 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php @@ -16,7 +16,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter /** * When this tag is present in a bookmark, its description should not be processed with Markdown */ - const NO_MD_TAG = 'nomarkdown'; + public const NO_MD_TAG = 'nomarkdown'; /** @var \Parsedown instance */ protected $parsedown; diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index eb26ef21..dc421661 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -51,7 +51,10 @@ class ConfigureController extends ShaarliAdminController $this->assignView('languages', Languages::getAvailableLanguages()); $this->assignView('gd_enabled', extension_loaded('gd')); $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); - $this->assignView('pagetitle', t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView( + 'pagetitle', + t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') + ); return $response->write($this->render(TemplatePage::CONFIGURE)); } @@ -101,7 +104,9 @@ class ConfigureController extends ShaarliAdminController ) { $this->saveWarningMessage( t('You have enabled or changed thumbnails mode.') . - '' . t('Please synchronize them.') . '' + '' . + t('Please synchronize them.') . + '' ); } $this->container->conf->set('thumbnails.mode', $thumbnailsMode); diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index 80997940..575a2f9d 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -65,7 +65,9 @@ class ServerController extends ShaarliAdminController $this->saveWarningMessage( t('Thumbnails cache has been cleared.') . ' ' . - '' . t('Please synchronize them.') . '' + '' . + t('Please synchronize them.') . + '' ); } else { $folders = [ diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php index 442b833c..d3beafe0 100644 --- a/application/legacy/LegacyLinkDB.php +++ b/application/legacy/LegacyLinkDB.php @@ -62,7 +62,7 @@ class LegacyLinkDB implements Iterator, Countable, ArrayAccess private $datastore; // Link date storage format - const LINK_DATE_FORMAT = 'Ymd_His'; + public const LINK_DATE_FORMAT = 'Ymd_His'; // List of bookmarks (associative array) // - key: link date (e.g. "20110823_124546"), diff --git a/application/updater/Updater.php b/application/updater/Updater.php index 3451cf36..4f557d0f 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -122,12 +122,12 @@ class Updater public function readUpdates(string $updatesFilepath): array { - return UpdaterUtils::read_updates_file($updatesFilepath); + return UpdaterUtils::readUpdatesFile($updatesFilepath); } public function writeUpdates(string $updatesFilepath, array $updates): void { - UpdaterUtils::write_updates_file($updatesFilepath, $updates); + UpdaterUtils::writeUpdatesFile($updatesFilepath, $updates); } /** diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php index 908bdc39..206f826e 100644 --- a/application/updater/UpdaterUtils.php +++ b/application/updater/UpdaterUtils.php @@ -11,7 +11,7 @@ class UpdaterUtils * * @return array Already done update methods. */ - public static function read_updates_file($updatesFilepath) + public static function readUpdatesFile($updatesFilepath) { if (! empty($updatesFilepath) && is_file($updatesFilepath)) { $content = file_get_contents($updatesFilepath); @@ -30,7 +30,7 @@ class UpdaterUtils * * @throws \Exception Couldn't write version number. */ - public static function write_updates_file($updatesFilepath, $updates) + public static function writeUpdatesFile($updatesFilepath, $updates) { if (empty($updatesFilepath)) { throw new \Exception('Updates file path is not set, can\'t write updates.'); diff --git a/phpcs.xml b/phpcs.xml index 349dc566..c559e35d 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -14,4 +14,9 @@ + + + + index.php + diff --git a/tests/legacy/LegacyUpdaterTest.php b/tests/legacy/LegacyUpdaterTest.php index f7391b86..395dd4b7 100644 --- a/tests/legacy/LegacyUpdaterTest.php +++ b/tests/legacy/LegacyUpdaterTest.php @@ -51,10 +51,10 @@ class LegacyUpdaterTest extends \Shaarli\TestCase */ public function testReadEmptyUpdatesFile() { - $this->assertEquals(array(), UpdaterUtils::read_updates_file('')); + $this->assertEquals(array(), UpdaterUtils::readUpdatesFile('')); $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; touch($updatesFile); - $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile)); + $this->assertEquals(array(), UpdaterUtils::readUpdatesFile($updatesFile)); unlink($updatesFile); } @@ -66,14 +66,14 @@ class LegacyUpdaterTest extends \Shaarli\TestCase $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; $updatesMethods = array('m1', 'm2', 'm3'); - UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); - $readMethods = UpdaterUtils::read_updates_file($updatesFile); + UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); + $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); $this->assertEquals($readMethods, $updatesMethods); // Update $updatesMethods[] = 'm4'; - UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); - $readMethods = UpdaterUtils::read_updates_file($updatesFile); + UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); + $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); $this->assertEquals($readMethods, $updatesMethods); unlink($updatesFile); } @@ -86,7 +86,7 @@ class LegacyUpdaterTest extends \Shaarli\TestCase $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); - UpdaterUtils::write_updates_file('', array('test')); + UpdaterUtils::writeUpdatesFile('', array('test')); } /** @@ -101,7 +101,7 @@ class LegacyUpdaterTest extends \Shaarli\TestCase touch($updatesFile); chmod($updatesFile, 0444); try { - @UpdaterUtils::write_updates_file($updatesFile, array('test')); + @UpdaterUtils::writeUpdatesFile($updatesFile, array('test')); } catch (Exception $e) { unlink($updatesFile); throw $e; diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php index 47332544..cadd8265 100644 --- a/tests/updater/UpdaterTest.php +++ b/tests/updater/UpdaterTest.php @@ -60,10 +60,10 @@ class UpdaterTest extends TestCase */ public function testReadEmptyUpdatesFile() { - $this->assertEquals(array(), UpdaterUtils::read_updates_file('')); + $this->assertEquals(array(), UpdaterUtils::readUpdatesFile('')); $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; touch($updatesFile); - $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile)); + $this->assertEquals(array(), UpdaterUtils::readUpdatesFile($updatesFile)); unlink($updatesFile); } @@ -75,14 +75,14 @@ class UpdaterTest extends TestCase $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; $updatesMethods = array('m1', 'm2', 'm3'); - UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); - $readMethods = UpdaterUtils::read_updates_file($updatesFile); + UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); + $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); $this->assertEquals($readMethods, $updatesMethods); // Update $updatesMethods[] = 'm4'; - UpdaterUtils::write_updates_file($updatesFile, $updatesMethods); - $readMethods = UpdaterUtils::read_updates_file($updatesFile); + UpdaterUtils::writeUpdatesFile($updatesFile, $updatesMethods); + $readMethods = UpdaterUtils::readUpdatesFile($updatesFile); $this->assertEquals($readMethods, $updatesMethods); unlink($updatesFile); } @@ -95,7 +95,7 @@ class UpdaterTest extends TestCase $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/'); - UpdaterUtils::write_updates_file('', array('test')); + UpdaterUtils::writeUpdatesFile('', array('test')); } /** @@ -110,7 +110,7 @@ class UpdaterTest extends TestCase touch($updatesFile); chmod($updatesFile, 0444); try { - @UpdaterUtils::write_updates_file($updatesFile, array('test')); + @UpdaterUtils::writeUpdatesFile($updatesFile, array('test')); } catch (Exception $e) { unlink($updatesFile); throw $e; From 5c856a69231eb1505df2ae1bf67199fd83f05a5e Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 9 Nov 2020 10:56:02 +0100 Subject: [PATCH 40/50] Run PHPCS during Travis CI checks + disable xdebug --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index d7460947..422bf835 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,6 +49,10 @@ cache: directories: - $HOME/.composer/cache +before_install: + # Disable xdebug: it significantly speed up tests and linter, and we don't use coverage yet + - phpenv config-rm xdebug.ini || echo 'No xdebug config.' + install: # install/update composer and php dependencies - composer config --unset platform && composer config platform.php $TRAVIS_PHP_VERSION @@ -60,4 +64,5 @@ before_script: script: - make clean - make check_permissions + - make code_sniffer - make all_tests From 2f4df753041088d788d1923692a7d530167a6840 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 9 Nov 2020 12:17:40 +0100 Subject: [PATCH 41/50] Update Static Analysis documentation --- doc/md/dev/Development.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/md/dev/Development.md b/doc/md/dev/Development.md index 5c085e03..c42e8ffe 100644 --- a/doc/md/dev/Development.md +++ b/doc/md/dev/Development.md @@ -6,7 +6,7 @@ Please read [Contributing to Shaarli](https://github.com/shaarli/Shaarli/tree/ma - [Unit tests](Unit-tests) -- Javascript linting - Shaarli uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). +- Javascript linting - Shaarli uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). Run `make eslint` to check JS style. - [GnuPG signature](GnuPG-signature) for tags/releases @@ -51,12 +51,12 @@ PHP (managed through [`composer.json`](https://github.com/shaarli/Shaarli/blob/m ## Link structure -Every link available through the `LinkDB` object is represented as an array +Every link available through the `LinkDB` object is represented as an array containing the following fields: * `id` (integer): Unique identifier. * `title` (string): Title of the link. - * `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.). + * `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.). Can be absolute or relative for Notes. * `real_url` (string): Real destination URL, can be redirected, encoded, etc. * `shorturl` (string): Permalink small hash. @@ -66,7 +66,7 @@ containing the following fields: * `thumbnail` (string|boolean): relative path of the thumbnail cache file, or false if there isn't any. * `created` (DateTime): link creation date time. * `updated` (DateTime): last modification date time. - + Small hashes are used to make a link to an entry in Shaarli. They are unique: the date of the item (eg. `20110923_150523`) is hashed with CRC32, then converted to base64 and some characters are replaced. They are always 6 characters longs and use only `A-Z a-z 0-9 - _` and `@`. @@ -163,11 +163,13 @@ See [`.travis.yml`](https://github.com/shaarli/Shaarli/blob/master/.travis.yml). ## Static analysis -Patches should try to stick to the [PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially: +Patches should try to stick to the [PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), and must follow: - [PSR-1](http://www.php-fig.org/psr/psr-1/) - Basic Coding Standard - [PSR-2](http://www.php-fig.org/psr/psr-2/) - Coding Style Guide +- [PSR-12](http://www.php-fig.org/psr/psr-12/) - Extended Coding Style Guide +These are enforced on pull requests using our Continuous Integration tools. **Work in progress:** Static analysis is currently being discussed here: in [#95 - Fix coding style (static analysis)](https://github.com/shaarli/Shaarli/issues/95), [#130 - Continuous Integration tools & features](https://github.com/shaarli/Shaarli/issues/130) From 1e49a65a2a930390cf00114cc30d8516626331c2 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 9 Nov 2020 12:36:04 +0100 Subject: [PATCH 42/50] Vintage theme: support async metadata retrieval --- assets/vintage/css/shaarli.css | 51 ++++++++++++++++++++++++++++++++++ tpl/vintage/editlink.html | 32 ++++++++++++++++----- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css index 1688dce0..5b02ab5b 100644 --- a/assets/vintage/css/shaarli.css +++ b/assets/vintage/css/shaarli.css @@ -1248,3 +1248,54 @@ ul.errors { width: 0%; height: 10px; } + +.loading-input { + position: relative; +} + +@keyframes around { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.loading-input .icon-container { + position: absolute; + right: 60px; + top: calc(50% - 10px); +} + +.loading-input .loader { + position: relative; + height: 20px; + width: 20px; + display: inline-block; + animation: around 5.4s infinite; +} + +.loading-input .loader::after, +.loading-input .loader::before { + content: ""; + background: #eee; + position: absolute; + display: inline-block; + width: 100%; + height: 100%; + border-width: 2px; + border-color: #333 #333 transparent transparent; + border-style: solid; + border-radius: 20px; + box-sizing: border-box; + top: 0; + left: 0; + animation: around 0.7s ease-in-out infinite; +} + +.loading-input .loader::after { + animation: around 0.7s ease-in-out 0.1s infinite; + background: transparent; +} diff --git a/tpl/vintage/editlink.html b/tpl/vintage/editlink.html index eb8807b5..343418bc 100644 --- a/tpl/vintage/editlink.html +++ b/tpl/vintage/editlink.html @@ -6,6 +6,7 @@ {if="$link.title==''"}onload="document.linkform.lf_title.focus();" {elseif="$link.description==''"}onload="document.linkform.lf_description.focus();" {else}onload="document.linkform.lf_tags.focus();"{/if} > +{$asyncLoadClass=$link_is_new && $async_metadata && empty($link.title) ? 'loading-input' : ''} {include="page.footer"} - +{if="$link_is_new && $async_metadata"}{/if} From 336f15e8baeaba0c1605985cdd7f05980cf80696 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 9 Nov 2020 12:46:24 +0100 Subject: [PATCH 43/50] Vintage theme: display global messages --- assets/vintage/css/shaarli.css | 10 ++++++++++ tpl/vintage/page.header.html | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css index 5b02ab5b..33e178af 100644 --- a/assets/vintage/css/shaarli.css +++ b/assets/vintage/css/shaarli.css @@ -1122,6 +1122,16 @@ ul.errors { float: left; } +ul.warnings { + color: orange; + float: left; +} + +ul.successes { + color: green; + float: left; +} + #pluginsadmin { width: 80%; padding: 20px 0 0 20px; diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html index 0a33523b..64d7f656 100644 --- a/tpl/vintage/page.header.html +++ b/tpl/vintage/page.header.html @@ -54,6 +54,30 @@ {/if} +{if="!empty($global_errors)"} +
    + {loop="$global_errors"} +
  • {$value}
  • + {/loop} +
+{/if} + +{if="!empty($global_warnings)"} +
    + {loop="$global_warnings"} +
  • {$value}
  • + {/loop} +
+{/if} + +{if="!empty($global_successes)"} +
    + {loop="$global_successes"} +
  • {$value}
  • + {/loop} +
+{/if} +
From 85c09fe3799db606a3d117c370bc63965e4a8554 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 9 Nov 2020 12:46:43 +0100 Subject: [PATCH 44/50] Vintage theme: fix routes in daily page --- tpl/vintage/daily.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tpl/vintage/daily.html b/tpl/vintage/daily.html index 74f6cdc7..28ba9f90 100644 --- a/tpl/vintage/daily.html +++ b/tpl/vintage/daily.html @@ -14,9 +14,9 @@
All links of one day
in a single page.
- {if="$previousday"} <Previous day{else}<Previous day{/if} + {if="$previousday"} <Previous day{else}<Previous day{/if} - - {if="$nextday"}Next day>{else}Next day>{/if} + {if="$nextday"}Next day>{else}Next day>{/if}
{loop="$daily_about_plugin"} @@ -52,13 +52,13 @@ {$link=$value}
{if="!$hide_timestamps || $is_logged_in"} {/if} {if="$link.tags"} From 80c8889bfe5151a23066188e6c74c3c1e8575e61 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 9 Nov 2020 14:37:45 +0100 Subject: [PATCH 45/50] Server admin: do not retrieve latest version without update_check If the setting 'updates.check_updates' is disabled, do not retrieve the latest version on server administration page. Additionally, updated default values for - updates.check_updates from false to true - updates.check_updates_branch from stable to latest --- application/config/ConfigManager.php | 4 +- .../controller/admin/ServerController.php | 15 ++- inc/languages/fr/LC_MESSAGES/shaarli.po | 113 ++++++++++++------ 3 files changed, 88 insertions(+), 44 deletions(-) diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index a035baae..3260d7c0 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -370,8 +370,8 @@ class ConfigManager $this->setEmpty('general.enable_async_metadata', true); $this->setEmpty('general.tags_separator', ' '); - $this->setEmpty('updates.check_updates', false); - $this->setEmpty('updates.check_updates_branch', 'stable'); + $this->setEmpty('updates.check_updates', true); + $this->setEmpty('updates.check_updates_branch', 'latest'); $this->setEmpty('updates.check_updates_interval', 86400); $this->setEmpty('feed.rss_permalinks', true); diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index bfc99422..780151dd 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -25,9 +25,16 @@ class ServerController extends ShaarliAdminController */ public function index(Request $request, Response $response): Response { - $latestVersion = 'v' . ApplicationUtils::getVersion( - ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE - ); + $releaseUrl = ApplicationUtils::$GITHUB_URL . '/releases/'; + if ($this->container->conf->get('updates.check_updates', true)) { + $latestVersion = 'v' . ApplicationUtils::getVersion( + ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE + ); + $releaseUrl .= 'tag/' . $latestVersion; + } else { + $latestVersion = t('Check disabled'); + } + $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php'); $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); @@ -37,7 +44,7 @@ class ServerController extends ShaarliAdminController $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('release_url', ApplicationUtils::$GITHUB_URL . '/releases/tag/' . $latestVersion); + $this->assignView('release_url', $releaseUrl); $this->assignView('latest_version', $latestVersion); $this->assignView('current_version', $currentVersion); $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode')); diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 51bef6c7..26dede4e 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-11-05 19:43+0100\n" -"PO-Revision-Date: 2020-11-05 19:44+0100\n" +"POT-Creation-Date: 2020-11-09 14:39+0100\n" +"PO-Revision-Date: 2020-11-09 14:42+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -189,9 +189,9 @@ msgstr "" #: application/bookmark/BookmarkInitializer.php:91 #: application/legacy/LegacyLinkDB.php:246 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:50 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 msgid "" "The personal, minimalist, super-fast, database free, bookmarking service" msgstr "" @@ -304,7 +304,7 @@ 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:68 +#: application/front/controller/admin/ServerController.php:75 #: application/legacy/LegacyUpdater.php:538 msgid "Please synchronize them." msgstr "Merci de les synchroniser." @@ -347,30 +347,44 @@ msgstr "" "le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " "légères." -#: application/front/controller/admin/ManageTagController.php:29 +#: application/front/controller/admin/ManageTagController.php:30 +msgid "whitespace" +msgstr "espace" + +#: application/front/controller/admin/ManageTagController.php:35 #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 msgid "Manage tags" msgstr "Gérer les tags" -#: application/front/controller/admin/ManageTagController.php:48 +#: application/front/controller/admin/ManageTagController.php:54 msgid "Invalid tags provided." msgstr "Les tags fournis ne sont pas valides." -#: application/front/controller/admin/ManageTagController.php:72 +#: application/front/controller/admin/ManageTagController.php:78 #, php-format msgid "The tag was removed from %d bookmark." msgid_plural "The tag was removed from %d bookmarks." msgstr[0] "Le tag a été supprimé du %d lien." msgstr[1] "Le tag a été supprimé de %d liens." -#: application/front/controller/admin/ManageTagController.php:77 +#: application/front/controller/admin/ManageTagController.php:83 #, php-format msgid "The tag was renamed in %d bookmark." msgid_plural "The tag was renamed in %d bookmarks." msgstr[0] "Le tag a été renommé dans %d lien." msgstr[1] "Le tag a été renommé dans %d liens." +#: application/front/controller/admin/ManageTagController.php:105 +msgid "Tags separator must be a single character." +msgstr "Un séparateur de tags doit contenir un seul caractère." + +#: application/front/controller/admin/ManageTagController.php:111 +msgid "These characters are reserved and can't be used as tags separator: " +msgstr "" +"Ces caractères sont réservés et ne peuvent être utilisés comme des " +"séparateurs de tags : " + #: application/front/controller/admin/PasswordController.php:28 #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 @@ -405,17 +419,21 @@ msgstr "" "Une erreur s'est produite lors de la sauvegarde de la configuration des " "plugins : " -#: application/front/controller/admin/ServerController.php:50 +#: application/front/controller/admin/ServerController.php:35 +msgid "Check disabled" +msgstr "Vérification désactivée" + +#: application/front/controller/admin/ServerController.php:57 #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 msgid "Server administration" msgstr "Administration serveur" -#: application/front/controller/admin/ServerController.php:67 +#: application/front/controller/admin/ServerController.php:74 msgid "Thumbnails cache has been cleared." msgstr "Le cache des miniatures a été vidé." -#: application/front/controller/admin/ServerController.php:76 +#: application/front/controller/admin/ServerController.php:83 msgid "Shaarli's cache folder has been cleared!" msgstr "Le dossier de cache de Shaarli a été vidé !" @@ -441,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:168 +#: application/front/controller/admin/ShaarePublishController.php:171 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 msgid "Edit" msgstr "Modifier" -#: application/front/controller/admin/ShaarePublishController.php:171 +#: application/front/controller/admin/ShaarePublishController.php:174 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 msgid "Shaare" msgstr "Shaare" -#: application/front/controller/admin/ShaarePublishController.php:202 +#: application/front/controller/admin/ShaarePublishController.php:205 msgid "Note: " msgstr "Note : " @@ -467,7 +485,7 @@ msgstr "Mise à jour des miniatures" msgid "Tools" msgstr "Outils" -#: application/front/controller/visitor/BookmarkListController.php:116 +#: application/front/controller/visitor/BookmarkListController.php:120 msgid "Search: " msgstr "Recherche : " @@ -572,7 +590,7 @@ msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." msgid "Picture wall" msgstr "Mur d'images" -#: application/front/controller/visitor/TagCloudController.php:88 +#: application/front/controller/visitor/TagCloudController.php:90 msgid "Tag " msgstr "Tag " @@ -1033,6 +1051,32 @@ msgstr "Vous pouvez aussi modifier les tags dans la" msgid "tag list" msgstr "liste des tags" +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "Change tags separator" +msgstr "Changer le séparateur de tags" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +msgid "Your current tag separator is" +msgstr "Votre séparateur actuel est" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +msgid "New separator" +msgstr "Nouveau séparateur" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 +msgid "Save" +msgstr "Enregistrer" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 +msgid "Note that hashtags won't fully work with a non-whitespace separator." +msgstr "" +"Notez que les hashtags ne sont pas complètement fonctionnels avec un " +"séparateur qui n'est pas un espace." + #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 msgid "title" msgstr "titre" @@ -1176,13 +1220,6 @@ msgstr "Seulement les hébergeurs de média connus" msgid "None" msgstr "Aucune" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 -#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 -#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 -msgid "Save" -msgstr "Enregistrer" - #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 msgid "1 RSS entry per :type" msgid_plural "" @@ -1435,8 +1472,8 @@ msgid "without any tag" msgstr "sans tag" #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41 msgid "Fold" msgstr "Replier" @@ -1493,8 +1530,8 @@ msgstr "Tout sélectionner" #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 msgid "Fold all" msgstr "Replier tout" @@ -1510,9 +1547,9 @@ msgid "Remember me" msgstr "Rester connecté" #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:50 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 msgid "by the Shaarli community" msgstr "par la communauté Shaarli" @@ -1521,23 +1558,23 @@ msgstr "par la communauté Shaarli" msgid "Documentation" msgstr "Documentation" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 msgid "Expand" msgstr "Déplier" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 msgid "Expand all" msgstr "Déplier tout" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:47 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 msgid "Are you sure you want to delete this link?" msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 msgid "Are you sure you want to delete this tag?" msgstr "Êtes-vous sûr de vouloir supprimer ce tag ?" From 5a09b5fffd2038a7953197bc38a01060edfd192e Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 12 Nov 2020 12:56:22 +0100 Subject: [PATCH 46/50] CHANGELOG v0.12.1 --- AUTHORS | 5 ++++- CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 0ec52acc..be815364 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,4 +1,4 @@ - 991 ArthurHoaro + 1097 ArthurHoaro 402 VirtualTam 294 nodiscc 56 Sébastien Sauvage @@ -25,6 +25,7 @@ 2 Alexandre G.-Raymond 2 Chris Kuethe 2 Felix Bartels + 2 Ganesh Kandu 2 Guillaume Virlet 2 Knah Tsaeb 2 Mathieu Chabanon @@ -39,6 +40,7 @@ 2 pips 2 trailjeep 2 yude + 2 yudete 1 Adrien Oliva 1 Adrien le Maire 1 Alexis J @@ -65,6 +67,7 @@ 1 Kevin Masson 1 Knah Tsaeb 1 Lionel Martin + 1 Loïc Carr 1 Mark Gerarts 1 Marsup 1 Paul van den Burg diff --git a/CHANGELOG.md b/CHANGELOG.md index f1686d67..18404049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,55 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [v0.12.1]() - UNRELEASED +## [v0.12.2]() - UNRELEASED + +## [v0.12.1](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-11-12 + +> nginx ([#1628](https://github.com/shaarli/Shaarli/pull/1628)) and Apache ([#1630](https://github.com/shaarli/Shaarli/pull/1630)) configurations have been reviewed. It is recommended that you +> update yours using [the documentation](https://shaarli.readthedocs.io/en/master/Server-configuration/). +> Users using official Docker image will receive updated configuration automatically. + +### Added +- Bulk creation of bookmarks +- Server administration tool page (and install page requirements) +- Support any tag separator, not just whitespaces +- Share a private bookmark using a URL with a token +- Add a setting to retrieve bookmark metadata asynchronously (enabled by default) +- Highlight fulltext search results +- Weekly and monthly view/RSS feed for daily page +- MarkdownExtra formatter +- Default formatter: add a setting to disable auto-linkification +- Add mutex on datastore I/O operations to prevent data loss +- PHP 8.0 support +- REST API: allow override of creation and update dates +- Add strict types for bookmarks management + +### Changed +- Improve regex and performances to extract HTML metadata (title, description, etc.) +- Support using Shaarli without URL rewriting (prefix URL with `/index.php/`) +- Improve the "Manage tags" tools page +- Use PSR-3 logger for login attempts +- Move utils classes to Shaarli\Helper namespace and folder +- Include php-simplexml in Docker image +- Raise 404 error instead of 500 if permalink access is denied +- Display error details even with dev.debug set to false +- Reviewed nginx configuration +- Reviewed Apache configuration +- Replace vimeo link in demo bookmarks due to IP ban on the demo instance +- Apply PSR-12 on code base, and add CI check using PHPCS + +### Fixed +- Compatiliby issue on login with PHP 7.1 +- Japanese translations update +- Redirect to referrer after bookmark deletion +- Inject ROOT_PATH in plugin instead of regenerating it everywhere +- Wallabag plugin: minor improvements +- REST API postLink: change relative path to absolute path +- Webpack: fix vintage theme images include +- Docker-compose: fix SSL certificate + add parameter for Docker tag + +### Removed +- `config.json.php` new lines in prefix/suffix to prevent issues with Windows PHP ## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-10-13 From 6f9e0609f4c118142504234ebcc7d93456b5e588 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 12 Nov 2020 13:05:19 +0100 Subject: [PATCH 47/50] Update badge versions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 46dda8d5..71198032 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._ [![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) • -[![](https://img.shields.io/badge/latest-v0.12.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) +[![](https://img.shields.io/badge/latest-v0.12.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1) [![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) • [![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli) From 831e974ea5fa93d689e65313f84d0c80999674c3 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 12 Nov 2020 13:16:20 +0100 Subject: [PATCH 48/50] Doc: fix missing merge on Release page --- doc/md/dev/Release-Shaarli.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/md/dev/Release-Shaarli.md b/doc/md/dev/Release-Shaarli.md index 2c772406..d79be9ce 100644 --- a/doc/md/dev/Release-Shaarli.md +++ b/doc/md/dev/Release-Shaarli.md @@ -64,6 +64,14 @@ git pull upstream master # If releasing a new minor version, create a release branch $ git checkout -b v0.x +# Otherwise just use the existing one +$ git checkout v0.x + +# Get the latest changes +$ git merge master + +# Check that everything went fine: +$ make test # Bump shaarli_version.php from dev to 0.x.0, **without the v** $ vim shaarli_version.php From 150f2a0f2443e1aa5d993dd2e7126a2db86fe591 Mon Sep 17 00:00:00 2001 From: prog-it Date: Sat, 14 Nov 2020 07:45:10 +0500 Subject: [PATCH 49/50] Add russian language selection --- application/Languages.php | 1 + 1 file changed, 1 insertion(+) diff --git a/application/Languages.php b/application/Languages.php index 60e91631..7177db2c 100644 --- a/application/Languages.php +++ b/application/Languages.php @@ -186,6 +186,7 @@ class Languages 'en' => t('English'), 'fr' => t('French'), 'jp' => t('Japanese'), + 'ru' => t('Russian'), ]; } } From 1595d0e2b3b399bf248148fecf0ecc76e3da5d62 Mon Sep 17 00:00:00 2001 From: prog-it Date: Sun, 15 Nov 2020 06:16:55 +0500 Subject: [PATCH 50/50] Add russian language file --- inc/languages/ru/LC_MESSAGES/shaarli.po | 1944 +++++++++++++++++++++++ 1 file changed, 1944 insertions(+) create mode 100755 inc/languages/ru/LC_MESSAGES/shaarli.po diff --git a/inc/languages/ru/LC_MESSAGES/shaarli.po b/inc/languages/ru/LC_MESSAGES/shaarli.po new file mode 100755 index 00000000..98e70425 --- /dev/null +++ b/inc/languages/ru/LC_MESSAGES/shaarli.po @@ -0,0 +1,1944 @@ +msgid "" +msgstr "" +"Project-Id-Version: Shaarli\n" +"POT-Creation-Date: 2020-11-14 07:47+0500\n" +"PO-Revision-Date: 2020-11-15 06:16+0500\n" +"Last-Translator: progit \n" +"Language-Team: Shaarli\n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.1\n" +"X-Poedit-Basepath: ../../../..\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: t:1,2;t\n" +"X-Poedit-SearchPath-0: application\n" +"X-Poedit-SearchPath-1: tmp\n" +"X-Poedit-SearchPath-2: index.php\n" +"X-Poedit-SearchPath-3: init.php\n" +"X-Poedit-SearchPath-4: plugins\n" + +#: application/History.php:181 +msgid "History file isn't readable or writable" +msgstr "Файл истории не доступен для чтения или записи" + +#: application/History.php:192 +msgid "Could not parse history file" +msgstr "Не удалось разобрать файл истории" + +#: application/Languages.php:184 +msgid "Automatic" +msgstr "Автоматический" + +#: application/Languages.php:185 +msgid "German" +msgstr "Немецкий" + +#: application/Languages.php:186 +msgid "English" +msgstr "Английский" + +#: application/Languages.php:187 +msgid "French" +msgstr "Французский" + +#: application/Languages.php:188 +msgid "Japanese" +msgstr "Японский" + +#: application/Languages.php:189 +msgid "Russian" +msgstr "Русский" + +#: application/Thumbnailer.php:62 +msgid "" +"php-gd extension must be loaded to use thumbnails. Thumbnails are now " +"disabled. Please reload the page." +msgstr "" +"для использования миниатюр необходимо загрузить расширение php-gd. Миниатюры " +"сейчас отключены. Перезагрузите страницу." + +#: application/Utils.php:405 +msgid "Setting not set" +msgstr "Настройка не задана" + +#: application/Utils.php:412 +msgid "Unlimited" +msgstr "Неограниченно" + +#: application/Utils.php:415 +msgid "B" +msgstr "Б" + +#: application/Utils.php:415 +msgid "kiB" +msgstr "КБ" + +#: application/Utils.php:415 +msgid "MiB" +msgstr "МБ" + +#: application/Utils.php:415 +msgid "GiB" +msgstr "ГБ" + +#: 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 "У вас нет прав на изменение хранилища данных" + +#: application/bookmark/BookmarkFileService.php:210 +msgid "This bookmarks already exists" +msgstr "Эта закладка уже существует" + +#: application/bookmark/BookmarkInitializer.php:42 +msgid "(private bookmark with thumbnail demo)" +msgstr "(личная закладка с показом миниатюр)" + +#: application/bookmark/BookmarkInitializer.php:45 +msgid "" +"Shaarli will automatically pick up the thumbnail for links to a variety of " +"websites.\n" +"\n" +"Explore your new Shaarli instance by trying out controls and menus.\n" +"Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the " +"documentation](https://shaarli.readthedocs.io/en/master/) to learn more " +"about Shaarli.\n" +"\n" +"Now you can edit or delete the default shaares.\n" +msgstr "" +"Shaarli автоматически подберет миниатюру для ссылок на различные сайты.\n" +"\n" +"Изучите Shaarli, попробовав элементы управления и меню.\n" +"Посетите проект [Github](https://github.com/shaarli/Shaarli) или " +"[документацию](https://shaarli.readthedocs.io/en/master/),чтобы узнать " +"больше о Shaarli.\n" +"\n" +"Теперь вы можете редактировать или удалять шаары по умолчанию.\n" + +#: application/bookmark/BookmarkInitializer.php:58 +msgid "Note: Shaare descriptions" +msgstr "Примечание: описания Шаар" + +#: application/bookmark/BookmarkInitializer.php:60 +msgid "" +"Adding a shaare without entering a URL creates a text-only \"note\" post " +"such as this one.\n" +"This note is private, so you are the only one able to see it while logged " +"in.\n" +"\n" +"You can use this to keep notes, post articles, code snippets, and much " +"more.\n" +"\n" +"The Markdown formatting setting allows you to format your notes and bookmark " +"description:\n" +"\n" +"### Title headings\n" +"\n" +"#### Multiple headings levels\n" +" * bullet lists\n" +" * _italic_ text\n" +" * **bold** text\n" +" * ~~strike through~~ text\n" +" * `code` blocks\n" +" * images\n" +" * [links](https://en.wikipedia.org/wiki/Markdown)\n" +"\n" +"Markdown also supports tables:\n" +"\n" +"| Name | Type | Color | Qty |\n" +"| ------- | --------- | ------ | ----- |\n" +"| Orange | Fruit | Orange | 126 |\n" +"| Apple | Fruit | Any | 62 |\n" +"| Lemon | Fruit | Yellow | 30 |\n" +"| Carrot | Vegetable | Red | 14 |\n" +msgstr "" +"При добавлении закладки без ввода URL адреса создается текстовая \"заметка" +"\", такая как эта.\n" +"Эта заметка является личной, поэтому вы единственный, кто может ее увидеть, " +"находясь в системе.\n" +"\n" +"Вы можете использовать это для хранения заметок, публикации статей, " +"фрагментов кода и многого другого.\n" +"\n" +"Параметр форматирования Markdown позволяет форматировать заметки и описание " +"закладок:\n" +"\n" +"### Заголовок заголовков\n" +"\n" +"#### Multiple headings levels\n" +" * маркированные списки\n" +" * _наклонный_ текст\n" +" * **жирный** текст\n" +" * ~~зачеркнутый~~ текст\n" +" * блоки `кода`\n" +" * изображения\n" +" * [ссылки](https://en.wikipedia.org/wiki/Markdown)\n" +"\n" +"Markdown также поддерживает таблицы:\n" +"\n" +"| Имя | Тип | Цвет | Количество |\n" +"| ------- | --------- | ------ | ----- |\n" +"| Апельсин | Фрукт | Оранжевый | 126 |\n" +"| Яблоко | Фрукт | Любой | 62 |\n" +"| Лимон | Фрукт | Желтый | 30 |\n" +"| Морковь | Овощ | Красный | 14 |\n" + +#: application/bookmark/BookmarkInitializer.php:94 +#: application/legacy/LegacyLinkDB.php:246 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "" +"The personal, minimalist, super-fast, database free, bookmarking service" +msgstr "Личный, минималистичный, сверхбыстрый сервис закладок без баз данных" + +#: application/bookmark/BookmarkInitializer.php:97 +msgid "" +"Welcome to Shaarli!\n" +"\n" +"Shaarli allows you to bookmark your favorite pages, and share them with " +"others or store them privately.\n" +"You can add a description to your bookmarks, such as this one, and tag " +"them.\n" +"\n" +"Create a new shaare by clicking the `+Shaare` button, or using any of the " +"recommended tools (browser extension, mobile app, bookmarklet, REST API, " +"etc.).\n" +"\n" +"You can easily retrieve your links, even with thousands of them, using the " +"internal search engine, or search through tags (e.g. this Shaare is tagged " +"with `shaarli` and `help`).\n" +"Hashtags such as #shaarli #help are also supported.\n" +"You can also filter the available [RSS feed](/feed/atom) and picture wall by " +"tag or plaintext search.\n" +"\n" +"We hope that you will enjoy using Shaarli, maintained with ❤️ by the " +"community!\n" +"Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if " +"you have a suggestion or encounter an issue.\n" +msgstr "" +"Добро пожаловать в Shaarli!\n" +"\n" +"Shaarli позволяет добавлять в закладки свои любимые страницы и делиться ими " +"с другими или хранить их в частном порядке.\n" +"Вы можете добавить описание к своим закладкам, например этой, и пометить " +"их.\n" +"\n" +"Создайте новую закладку, нажав кнопку `+Поделиться`, или используя любой из " +"рекомендуемых инструментов (расширение для браузера, мобильное приложение, " +"букмарклет, REST API и т.д.).\n" +"\n" +"Вы можете легко получить свои ссылки, даже если их тысячи, с помощью " +"внутренней поисковой системы или поиска по тегам (например, эта заметка " +"помечена тегами `shaarli` and `help`).\n" +"Также поддерживаются хэштеги, такие как #shaarli #help.\n" +"Вы можете также фильтровать доступный [RSS канал](/feed/atom) и галерею по " +"тегу или по поиску текста.\n" +"\n" +"Мы надеемся, что вам понравится использовать Shaarli, с ❤️ поддерживаемый " +"сообществом!\n" +"Не стесняйтесь открывать [запрос](https://github.com/shaarli/Shaarli/" +"issues), если у вас есть предложение или возникла проблема.\n" + +#: application/bookmark/exception/BookmarkNotFoundException.php:14 +msgid "The link you are trying to reach does not exist or has been deleted." +msgstr "" +"Ссылка, по которой вы пытаетесь перейти, не существует или была удалена." + +#: 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." +msgstr "" +"Shaarli не удалось создать файл конфигурации. Убедитесь, что у Shaarli есть " +"право на запись в папку, в которой он установлен." + +#: application/config/ConfigManager.php:137 +#: application/config/ConfigManager.php:164 +msgid "Invalid setting key parameter. String expected, got: " +msgstr "Неверная настройка ключевого параметра. Ожидалась строка, получено: " + +#: application/config/exception/MissingFieldConfigException.php:20 +#, php-format +msgid "Configuration value is required for %s" +msgstr "Значение конфигурации требуется для %s" + +#: application/config/exception/PluginConfigOrderException.php:15 +msgid "An error occurred while trying to save plugins loading order." +msgstr "Произошла ошибка при попытке сохранить порядок загрузки плагинов." + +#: application/config/exception/UnauthorizedConfigException.php:15 +msgid "You are not authorized to alter config." +msgstr "Вы не авторизованы для изменения конфигурации." + +#: application/exceptions/IOException.php:23 +msgid "Error accessing" +msgstr "Ошибка доступа" + +#: application/feed/FeedBuilder.php:180 +msgid "Direct link" +msgstr "Прямая ссылка" + +#: 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 "Постоянная ссылка" + +#: application/front/controller/admin/ConfigureController.php:56 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "Configure" +msgstr "Настройка" + +#: application/front/controller/admin/ConfigureController.php:106 +#: application/legacy/LegacyUpdater.php:539 +msgid "You have enabled or changed thumbnails mode." +msgstr "Вы включили или изменили режим миниатюр." + +#: application/front/controller/admin/ConfigureController.php:108 +#: application/front/controller/admin/ServerController.php:76 +#: application/legacy/LegacyUpdater.php:540 +msgid "Please synchronize them." +msgstr "Пожалуйста, синхронизируйте их." + +#: application/front/controller/admin/ConfigureController.php:119 +#: application/front/controller/visitor/InstallController.php:149 +msgid "Error while writing config file after configuration update." +msgstr "Ошибка при записи файла конфигурации после обновления конфигурации." + +#: application/front/controller/admin/ConfigureController.php:128 +msgid "Configuration was saved." +msgstr "Конфигурация сохранена." + +#: application/front/controller/admin/ExportController.php:26 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 +msgid "Export" +msgstr "Экспорт" + +#: application/front/controller/admin/ExportController.php:42 +msgid "Please select an export mode." +msgstr "Выберите режим экспорта." + +#: application/front/controller/admin/ImportController.php:41 +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "Import" +msgstr "Импорт" + +#: application/front/controller/admin/ImportController.php:55 +msgid "No import file provided." +msgstr "Файл импорта не предоставлен." + +#: application/front/controller/admin/ImportController.php:66 +#, php-format +msgid "" +"The file you are trying to upload is probably bigger than what this " +"webserver can accept (%s). Please upload in smaller chunks." +msgstr "" +"Файл, который вы пытаетесь загрузить, вероятно, больше, чем может принять " +"этот сервер (%s). Пожалуйста, загружайте небольшими частями." + +#: application/front/controller/admin/ManageTagController.php:30 +msgid "whitespace" +msgstr "пробел" + +#: application/front/controller/admin/ManageTagController.php:35 +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "Manage tags" +msgstr "Управление тегами" + +#: application/front/controller/admin/ManageTagController.php:54 +msgid "Invalid tags provided." +msgstr "Предоставлены недействительные теги." + +#: application/front/controller/admin/ManageTagController.php:78 +#, php-format +msgid "The tag was removed from %d bookmark." +msgid_plural "The tag was removed from %d bookmarks." +msgstr[0] "Тег был удален из %d закладки." +msgstr[1] "Тег был удален из %d закладок." +msgstr[2] "Тег был удален из %d закладок." + +#: application/front/controller/admin/ManageTagController.php:83 +#, php-format +msgid "The tag was renamed in %d bookmark." +msgid_plural "The tag was renamed in %d bookmarks." +msgstr[0] "Тег был переименован в %d закладке." +msgstr[1] "Тег был переименован в %d закладках." +msgstr[2] "Тег был переименован в %d закладках." + +#: application/front/controller/admin/ManageTagController.php:105 +msgid "Tags separator must be a single character." +msgstr "Разделитель тегов должен состоять из одного символа." + +#: application/front/controller/admin/ManageTagController.php:111 +msgid "These characters are reserved and can't be used as tags separator: " +msgstr "" +"Эти символы зарезервированы и не могут использоваться в качестве разделителя " +"тегов: " + +#: application/front/controller/admin/PasswordController.php:28 +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +msgid "Change password" +msgstr "Изменить пароль" + +#: application/front/controller/admin/PasswordController.php:55 +msgid "You must provide the current and new password to change it." +msgstr "Вы должны предоставить текущий и новый пароль, чтобы изменить его." + +#: application/front/controller/admin/PasswordController.php:71 +msgid "The old password is not correct." +msgstr "Старый пароль неверен." + +#: application/front/controller/admin/PasswordController.php:97 +msgid "Your password has been changed" +msgstr "Пароль изменен" + +#: application/front/controller/admin/PluginsController.php:45 +msgid "Plugin Administration" +msgstr "Управление плагинами" + +#: application/front/controller/admin/PluginsController.php:76 +msgid "Setting successfully saved." +msgstr "Настройка успешно сохранена." + +#: application/front/controller/admin/PluginsController.php:79 +msgid "Error while saving plugin configuration: " +msgstr "Ошибка при сохранении конфигурации плагина: " + +#: application/front/controller/admin/ServerController.php:35 +msgid "Check disabled" +msgstr "Проверка отключена" + +#: application/front/controller/admin/ServerController.php:57 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Server administration" +msgstr "Администрирование сервера" + +#: application/front/controller/admin/ServerController.php:74 +msgid "Thumbnails cache has been cleared." +msgstr "Кэш миниатюр очищен." + +#: application/front/controller/admin/ServerController.php:85 +msgid "Shaarli's cache folder has been cleared!" +msgstr "Папка с кэшем Shaarli очищена!" + +#: application/front/controller/admin/ShaareAddController.php:26 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +msgid "Shaare a new link" +msgstr "Поделиться новой ссылкой" + +#: application/front/controller/admin/ShaareManageController.php:35 +#: application/front/controller/admin/ShaareManageController.php:93 +msgid "Invalid bookmark ID provided." +msgstr "Указан неверный идентификатор закладки." + +#: application/front/controller/admin/ShaareManageController.php:47 +#: application/front/controller/admin/ShaareManageController.php:116 +#: application/front/controller/admin/ShaareManageController.php:156 +#: application/front/controller/admin/ShaarePublishController.php:82 +#, php-format +msgid "Bookmark with identifier %s could not be found." +msgstr "Закладка с идентификатором %s не найдена." + +#: application/front/controller/admin/ShaareManageController.php:101 +msgid "Invalid visibility provided." +msgstr "Предоставлена недопустимая видимость." + +#: application/front/controller/admin/ShaarePublishController.php:173 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 +msgid "Edit" +msgstr "Редактировать" + +#: application/front/controller/admin/ShaarePublishController.php:176 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 +msgid "Shaare" +msgstr "Поделиться" + +#: application/front/controller/admin/ShaarePublishController.php:208 +msgid "Note: " +msgstr "Заметка: " + +#: application/front/controller/admin/ThumbnailsController.php:37 +#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Thumbnails update" +msgstr "Обновление миниатюр" + +#: application/front/controller/admin/ToolsController.php:31 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33 +msgid "Tools" +msgstr "Инструменты" + +#: application/front/controller/visitor/BookmarkListController.php:121 +msgid "Search: " +msgstr "Поиск: " + +#: application/front/controller/visitor/DailyController.php:200 +msgid "day" +msgstr "день" + +#: application/front/controller/visitor/DailyController.php:200 +#: application/front/controller/visitor/DailyController.php:203 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "Daily" +msgstr "За день" + +#: application/front/controller/visitor/DailyController.php:201 +msgid "week" +msgstr "неделя" + +#: application/front/controller/visitor/DailyController.php:201 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Weekly" +msgstr "За неделю" + +#: application/front/controller/visitor/DailyController.php:202 +msgid "month" +msgstr "месяц" + +#: application/front/controller/visitor/DailyController.php:202 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "Monthly" +msgstr "За месяц" + +#: application/front/controller/visitor/ErrorController.php:30 +msgid "Error: " +msgstr "Ошибка: " + +#: application/front/controller/visitor/ErrorController.php:34 +msgid "Please report it on Github." +msgstr "Пожалуйста, сообщите об этом на Github." + +#: application/front/controller/visitor/ErrorController.php:39 +msgid "An unexpected error occurred." +msgstr "Произошла непредвиденная ошибка." + +#: application/front/controller/visitor/ErrorNotFoundController.php:25 +msgid "Requested page could not be found." +msgstr "Запрошенная страница не может быть найдена." + +#: application/front/controller/visitor/InstallController.php:65 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Install Shaarli" +msgstr "Установить Shaarli" + +#: application/front/controller/visitor/InstallController.php:85 +#, php-format +msgid "" +"
Sessions do not seem to work correctly on your server.
Make sure the " +"variable \"session.save_path\" is set correctly in your PHP config, and that " +"you have write access to it.
It currently points to %s.
On some " +"browsers, accessing your server via a hostname like 'localhost' or any " +"custom hostname without a dot causes cookie storage to fail. We recommend " +"accessing your server via it's IP address or Fully Qualified Domain Name.
" +msgstr "" +"
Сессии на вашем сервере работают некорректно.
Убедитесь, что " +"переменная \"session.save_path\" правильно установлена в вашей конфигурации " +"PHP и что у вас есть доступ к ней на запись.
В настоящее время она " +"указывает на %s.
В некоторых браузерах доступ к вашему серверу через имя " +"хоста, например localhost или любое другое имя хоста без точки, приводит к " +"сбою хранилища файлов cookie. Мы рекомендуем получить доступ к вашему " +"серверу через его IP адрес или полное доменное имя.
" + +#: application/front/controller/visitor/InstallController.php:157 +msgid "" +"Shaarli is now configured. Please login and start shaaring your bookmarks!" +msgstr "Shaarli настроен. Войдите и начните делиться своими закладками!" + +#: application/front/controller/visitor/InstallController.php:171 +msgid "Insufficient permissions:" +msgstr "Недостаточно разрешений:" + +#: application/front/controller/visitor/LoginController.php:46 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101 +msgid "Login" +msgstr "Вход" + +#: application/front/controller/visitor/LoginController.php:78 +msgid "Wrong login/password." +msgstr "Неверный логин или пароль." + +#: application/front/controller/visitor/PictureWallController.php:29 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43 +msgid "Picture wall" +msgstr "Галерея" + +#: application/front/controller/visitor/TagCloudController.php:90 +msgid "Tag " +msgstr "Тег " + +#: application/front/exceptions/AlreadyInstalledException.php:11 +msgid "Shaarli has already been installed. Login to edit the configuration." +msgstr "Shaarli уже установлен. Войдите, чтобы изменить конфигурацию." + +#: application/front/exceptions/LoginBannedException.php:11 +msgid "" +"You have been banned after too many failed login attempts. Try again later." +msgstr "" +"Вы были заблокированы из-за большого количества неудачных попыток входа в " +"систему. Попробуйте позже." + +#: application/front/exceptions/OpenShaarliPasswordException.php:16 +msgid "You are not supposed to change a password on an Open Shaarli." +msgstr "Вы не должны менять пароль на Open Shaarli." + +#: application/front/exceptions/ThumbnailsDisabledException.php:11 +msgid "Picture wall unavailable (thumbnails are disabled)." +msgstr "Галерея недоступна (миниатюры отключены)." + +#: application/front/exceptions/WrongTokenException.php:16 +msgid "Wrong token." +msgstr "Неправильный токен." + +#: application/helper/ApplicationUtils.php:163 +#, php-format +msgid "" +"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " +"cannot run. Your PHP version has known security vulnerabilities and should " +"be updated as soon as possible." +msgstr "" +"Ваша версия PHP устарела! Shaarli требует как минимум PHP %s, и поэтому не " +"может работать. В вашей версии PHP есть известные уязвимости в системе " +"безопасности, и ее следует обновить как можно скорее." + +#: application/helper/ApplicationUtils.php:198 +#: application/helper/ApplicationUtils.php:218 +msgid "directory is not readable" +msgstr "папка не доступна для чтения" + +#: application/helper/ApplicationUtils.php:221 +msgid "directory is not writable" +msgstr "папка не доступна для записи" + +#: application/helper/ApplicationUtils.php:245 +msgid "file is not readable" +msgstr "файл не доступен для чтения" + +#: application/helper/ApplicationUtils.php:248 +msgid "file is not writable" +msgstr "файл не доступен для записи" + +#: application/helper/ApplicationUtils.php:282 +msgid "Configuration parsing" +msgstr "Разбор конфигурации" + +#: application/helper/ApplicationUtils.php:283 +msgid "Slim Framework (routing, etc.)" +msgstr "Slim Framework (маршрутизация и т. д.)" + +#: application/helper/ApplicationUtils.php:284 +msgid "Multibyte (Unicode) string support" +msgstr "Поддержка многобайтовых (Unicode) строк" + +#: application/helper/ApplicationUtils.php:285 +msgid "Required to use thumbnails" +msgstr "Обязательно использование миниатюр" + +#: application/helper/ApplicationUtils.php:286 +msgid "Localized text sorting (e.g. e->è->f)" +msgstr "Локализованная сортировка текста (например, e->è->f)" + +#: application/helper/ApplicationUtils.php:287 +msgid "Better retrieval of bookmark metadata and thumbnail" +msgstr "Лучшее получение метаданных закладок и миниатюр" + +#: application/helper/ApplicationUtils.php:288 +msgid "Use the translation system in gettext mode" +msgstr "Используйте систему перевода в режиме gettext" + +#: application/helper/ApplicationUtils.php:289 +msgid "Login using LDAP server" +msgstr "Вход через LDAP сервер" + +#: application/helper/DailyPageHelper.php:172 +msgid "Week" +msgstr "Неделя" + +#: application/helper/DailyPageHelper.php:176 +msgid "Today" +msgstr "Сегодня" + +#: application/helper/DailyPageHelper.php:178 +msgid "Yesterday" +msgstr "Вчера" + +#: application/helper/FileUtils.php:100 +msgid "Provided path is not a directory." +msgstr "Указанный путь не является папкой." + +#: application/helper/FileUtils.php:104 +msgid "Trying to delete a folder outside of Shaarli path." +msgstr "Попытка удалить папку за пределами пути Shaarli." + +#: application/legacy/LegacyLinkDB.php:131 +msgid "You are not authorized to add a link." +msgstr "Вы не авторизованы для изменения ссылки." + +#: application/legacy/LegacyLinkDB.php:134 +msgid "Internal Error: A link should always have an id and URL." +msgstr "Внутренняя ошибка: ссылка всегда должна иметь идентификатор и URL." + +#: application/legacy/LegacyLinkDB.php:137 +msgid "You must specify an integer as a key." +msgstr "В качестве ключа необходимо указать целое число." + +#: application/legacy/LegacyLinkDB.php:140 +msgid "Array offset and link ID must be equal." +msgstr "Смещение массива и идентификатор ссылки должны быть одинаковыми." + +#: application/legacy/LegacyLinkDB.php:249 +msgid "" +"Welcome to Shaarli! This is your first public bookmark. To edit or delete " +"me, you must first login.\n" +"\n" +"To learn how to use Shaarli, consult the link \"Documentation\" at the " +"bottom of this page.\n" +"\n" +"You use the community supported version of the original Shaarli project, by " +"Sebastien Sauvage." +msgstr "" +"Добро пожаловать в Shaarli! Это ваша первая общедоступная закладка. Чтобы " +"отредактировать или удалить меня, вы должны сначала авторизоваться.\n" +"\n" +"Чтобы узнать, как использовать Shaarli, перейдите по ссылке \"Документация\" " +"внизу этой страницы.\n" +"\n" +"Вы используете поддерживаемую сообществом версию оригинального проекта " +"Shaarli от Себастьяна Соваж." + +#: application/legacy/LegacyLinkDB.php:266 +msgid "My secret stuff... - Pastebin.com" +msgstr "Мой секрет... - Pastebin.com" + +#: application/legacy/LegacyLinkDB.php:268 +msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." +msgstr "" +"Тссс! Это личная ссылка, которую видите только ВЫ. Вы тоже можете удалить " +"меня." + +#: application/legacy/LegacyUpdater.php:104 +msgid "Couldn't retrieve updater class methods." +msgstr "Не удалось получить методы класса средства обновления." + +#: application/legacy/LegacyUpdater.php:540 +msgid "" +msgstr "" + +#: application/netscape/NetscapeBookmarkUtils.php:63 +msgid "Invalid export selection:" +msgstr "Неверный выбор экспорта:" + +#: application/netscape/NetscapeBookmarkUtils.php:215 +#, php-format +msgid "File %s (%d bytes) " +msgstr "Файл %s (%d байт) " + +#: application/netscape/NetscapeBookmarkUtils.php:217 +msgid "has an unknown file format. Nothing was imported." +msgstr "имеет неизвестный формат файла. Ничего не импортировано." + +#: application/netscape/NetscapeBookmarkUtils.php:221 +#, php-format +msgid "" +"was successfully processed in %d seconds: %d bookmarks imported, %d " +"bookmarks overwritten, %d bookmarks skipped." +msgstr "" +"успешно обработано за %d секунд: %d закладок импортировано, %d закладок " +"перезаписаны, %d закладок пропущено." + +#: application/plugin/PluginManager.php:125 +msgid " [plugin incompatibility]: " +msgstr " [несовместимость плагинов]: " + +#: application/plugin/exception/PluginFileNotFoundException.php:22 +#, php-format +msgid "Plugin \"%s\" files not found." +msgstr "Файл плагина \"%s\" не найден." + +#: application/render/PageCacheManager.php:32 +#, php-format +msgid "Cannot purge %s: no directory" +msgstr "Невозможно очистить%s: нет папки" + +#: application/updater/exception/UpdaterException.php:51 +msgid "An error occurred while running the update " +msgstr "Произошла ошибка при запуске обновления " + +#: index.php:81 +msgid "Shared bookmarks on " +msgstr "Общие закладки на " + +#: plugins/addlink_toolbar/addlink_toolbar.php:31 +msgid "URI" +msgstr "URI" + +#: plugins/addlink_toolbar/addlink_toolbar.php:35 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +msgid "Add link" +msgstr "Добавить ссылку" + +#: plugins/addlink_toolbar/addlink_toolbar.php:52 +msgid "Adds the addlink input on the linklist page." +msgstr "" +"Добавляет на страницу списка ссылок поле для добавления новой закладки." + +#: plugins/archiveorg/archiveorg.php:29 +msgid "View on archive.org" +msgstr "Посмотреть на archive.org" + +#: plugins/archiveorg/archiveorg.php:42 +msgid "For each link, add an Archive.org icon." +msgstr "Для каждой ссылки добавить значок с Archive.org." + +#: plugins/default_colors/default_colors.php:38 +msgid "" +"Default colors plugin error: This plugin is active and no custom color is " +"configured." +msgstr "" +"Ошибка плагина цветов по умолчанию: этот плагин активен, и пользовательский " +"цвет не настроен." + +#: plugins/default_colors/default_colors.php:113 +msgid "Override default theme colors. Use any CSS valid color." +msgstr "" +"Переопределить цвета темы по умолчанию. Используйте любой допустимый цвет " +"CSS." + +#: plugins/default_colors/default_colors.php:114 +msgid "Main color (navbar green)" +msgstr "Основной цвет (зеленый на панели навигации)" + +#: plugins/default_colors/default_colors.php:115 +msgid "Background color (light grey)" +msgstr "Цвет фона (светло-серый)" + +#: plugins/default_colors/default_colors.php:116 +msgid "Dark main color (e.g. visited links)" +msgstr "Темный основной цвет (например, посещенные ссылки)" + +#: plugins/demo_plugin/demo_plugin.php:478 +msgid "" +"A demo plugin covering all use cases for template designers and plugin " +"developers." +msgstr "" +"Демо плагин, охватывающий все варианты использования для дизайнеров шаблонов " +"и разработчиков плагинов." + +#: plugins/demo_plugin/demo_plugin.php:479 +msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." +msgstr "" +"Это параметр предназначен для демонстрационного плагина. Это будет суффикс." + +#: plugins/demo_plugin/demo_plugin.php:480 +msgid "Other demo parameter" +msgstr "Другой демонстрационный параметр" + +#: plugins/isso/isso.php:22 +msgid "" +"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " +"administration page." +msgstr "" +"Ошибка плагина Isso: определите параметр \"ISSO_SERVER\" на странице " +"настройки плагина." + +#: plugins/isso/isso.php:92 +msgid "Let visitor comment your shaares on permalinks with Isso." +msgstr "" +"Позволить посетителю комментировать ваши закладки по постоянным ссылкам с " +"Isso." + +#: plugins/isso/isso.php:93 +msgid "Isso server URL (without 'http://')" +msgstr "URL сервера Isso (без 'http: //')" + +#: plugins/piwik/piwik.php:24 +msgid "" +"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " +"administration page." +msgstr "" +"Ошибка плагина Piwik: укажите PIWIK_URL и PIWIK_SITEID на странице настройки " +"плагина." + +#: plugins/piwik/piwik.php:73 +msgid "A plugin that adds Piwik tracking code to Shaarli pages." +msgstr "Плагин, который добавляет код отслеживания Piwik на страницы Shaarli." + +#: plugins/piwik/piwik.php:74 +msgid "Piwik URL" +msgstr "Piwik URL" + +#: plugins/piwik/piwik.php:75 +msgid "Piwik site ID" +msgstr "Piwik site ID" + +#: plugins/playvideos/playvideos.php:26 +msgid "Video player" +msgstr "Видео плеер" + +#: plugins/playvideos/playvideos.php:29 +msgid "Play Videos" +msgstr "Воспроизвести видео" + +#: plugins/playvideos/playvideos.php:60 +msgid "Add a button in the toolbar allowing to watch all videos." +msgstr "" +"Добавьте кнопку на панель инструментов, позволяющую смотреть все видео." + +#: plugins/playvideos/youtube_playlist.js:214 +msgid "plugins/playvideos/jquery-1.11.2.min.js" +msgstr "plugins/playvideos/jquery-1.11.2.min.js" + +#: plugins/pubsubhubbub/pubsubhubbub.php:72 +#, php-format +msgid "Could not publish to PubSubHubbub: %s" +msgstr "Не удалось опубликовать в PubSubHubbub: %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:99 +#, php-format +msgid "Could not post to %s" +msgstr "Не удалось отправить сообщение в %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:103 +#, php-format +msgid "Bad response from the hub %s" +msgstr "Плохой ответ от хаба %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:114 +msgid "Enable PubSubHubbub feed publishing." +msgstr "Включить публикацию канала PubSubHubbub." + +#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72 +msgid "For each link, add a QRCode icon." +msgstr "Для каждой ссылки добавить значок QR кода." + +#: plugins/wallabag/wallabag.php:22 +msgid "" +"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " +"plugin administration page." +msgstr "" +"Ошибка плагина Wallabag: определите параметр \"WALLABAG_URL\" на странице " +"настройки плагина." + +#: plugins/wallabag/wallabag.php:49 +msgid "Save to wallabag" +msgstr "Сохранить в wallabag" + +#: plugins/wallabag/wallabag.php:73 +msgid "Wallabag API URL" +msgstr "Wallabag API URL" + +#: plugins/wallabag/wallabag.php:74 +msgid "Wallabag API version (1 or 2)" +msgstr "Wallabag версия API (1 или 2)" + +#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 +msgid "Sorry, nothing to see here." +msgstr "Извините, тут ничего нет." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "URL or leave empty to post a note" +msgstr "URL или оставьте пустым, чтобы опубликовать заметку" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "BULK CREATION" +msgstr "МАССОВОЕ СОЗДАНИЕ" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "Metadata asynchronous retrieval is disabled." +msgstr "Асинхронное получение метаданных отключено." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "" +"We recommend that you enable the setting general > " +"enable_async_metadata in your configuration file to use bulk link " +"creation." +msgstr "" +"Мы рекомендуем включить параметр general > enable_async_metadata в " +"вашем файле конфигурации, чтобы использовать массовое создание ссылок." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +msgid "Shaare multiple new links" +msgstr "Поделиться несколькими новыми ссылками" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 +msgid "Add one URL per line to create multiple bookmarks." +msgstr "Добавьте по одному URL в строке, чтобы создать несколько закладок." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Tags" +msgstr "Теги" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Private" +msgstr "Личный" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +msgid "Add links" +msgstr "Добавить ссылки" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Current password" +msgstr "Текущий пароль" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "New password" +msgstr "Новый пароль" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Change" +msgstr "Изменить" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "Tag" +msgstr "Тег" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "New name" +msgstr "Новое имя" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +msgid "Case sensitive" +msgstr "С учетом регистра" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 +msgid "Rename tag" +msgstr "Переименовать тег" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +msgid "Delete tag" +msgstr "Удалить тег" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "You can also edit tags in the" +msgstr "Вы также можете редактировать теги в" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "tag list" +msgstr "список тегов" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "Change tags separator" +msgstr "Изменить разделитель тегов" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +msgid "Your current tag separator is" +msgstr "Текущий разделитель тегов" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +msgid "New separator" +msgstr "Новый разделитель" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 +msgid "Save" +msgstr "Сохранить" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 +msgid "Note that hashtags won't fully work with a non-whitespace separator." +msgstr "" +"Обратите внимание, что хэштеги не будут полностью работать с разделителем, " +"отличным от пробелов." + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "title" +msgstr "заголовок" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +msgid "Home link" +msgstr "Домашняя ссылка" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "Default value" +msgstr "Значение по умолчанию" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "Theme" +msgstr "Тема" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85 +msgid "Description formatter" +msgstr "Средство форматирования описания" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "Language" +msgstr "Язык" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 +msgid "Timezone" +msgstr "Часовой пояс" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "Continent" +msgstr "Континент" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "City" +msgstr "Город" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191 +msgid "Disable session cookie hijacking protection" +msgstr "Отключить защиту от перехвата файлов сеанса cookie" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193 +msgid "Check this if you get disconnected or if your IP address changes often" +msgstr "Проверьте это, если вы отключаетесь или ваш IP адрес часто меняется" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210 +msgid "Private links by default" +msgstr "Приватные ссылки по умолчанию" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211 +msgid "All new links are private by default" +msgstr "Все новые ссылки по умолчанию являются приватными" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226 +msgid "RSS direct links" +msgstr "RSS прямые ссылки" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227 +msgid "Check this to use direct URL instead of permalink in feeds" +msgstr "" +"Установите этот флажок, чтобы использовать прямой URL вместо постоянной " +"ссылки в фидах" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242 +msgid "Hide public links" +msgstr "Скрыть общедоступные ссылки" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243 +msgid "Do not show any links if the user is not logged in" +msgstr "Не показывать ссылки, если пользователь не авторизован" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149 +msgid "Check updates" +msgstr "Проверить обновления" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 +msgid "Notify me when a new release is ready" +msgstr "Оповестить, когда будет готов новый выпуск" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274 +msgid "Automatically retrieve description for new bookmarks" +msgstr "Автоматически получать описание для новых закладок" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275 +msgid "Shaarli will try to retrieve the description from meta HTML headers" +msgstr "Shaarli попытается получить описание из мета заголовков HTML" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 +msgid "Enable REST API" +msgstr "Включить REST API" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Allow third party software to use Shaarli such as mobile application" +msgstr "" +"Разрешить стороннему программному обеспечению использовать Shaarli, например " +"мобильное приложение" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306 +msgid "API secret" +msgstr "API ключ" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320 +msgid "Enable thumbnails" +msgstr "Включить миниатюры" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324 +msgid "You need to enable the extension php-gd to use thumbnails." +msgstr "" +"Вам необходимо включить расширение php-gd для использования " +"миниатюр." + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "Synchronize thumbnails" +msgstr "Синхронизировать миниатюры" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "All" +msgstr "Все" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 +msgid "Only common media hosts" +msgstr "Только обычные медиа хосты" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 +msgid "None" +msgstr "Ничего" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +msgid "1 RSS entry per :type" +msgid_plural "" +msgstr[0] "1 RSS запись для каждого :type" +msgstr[1] "1 RSS запись для каждого :type" +msgstr[2] "1 RSS запись для каждого :type" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +msgid "Previous :type" +msgid_plural "" +msgstr[0] "Предыдущий :type" +msgstr[1] "Предыдущих :type" +msgstr[2] "Предыдущих :type" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +msgid "All links of one :type in a single page." +msgid_plural "" +msgstr[0] "Все ссылки одного :type на одной странице." +msgstr[1] "Все ссылки одного :type на одной странице." +msgstr[2] "Все ссылки одного :type на одной странице." + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +msgid "Next :type" +msgid_plural "" +msgstr[0] "Следующий :type" +msgstr[1] "Следующие :type" +msgstr[2] "Следующие :type" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +msgid "Edit Shaare" +msgstr "Изменить закладку" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +msgid "New Shaare" +msgstr "Новая закладка" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 +msgid "Created:" +msgstr "Создано:" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "URL" +msgstr "URL" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "Title" +msgstr "Заголовок" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 +msgid "Description" +msgstr "Описание" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 +msgid "Description will be rendered with" +msgstr "Описание будет отображаться с" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "Markdown syntax documentation" +msgstr "Документация по синтаксису Markdown" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +msgid "Markdown syntax" +msgstr "Синтаксис Markdown" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115 +msgid "Cancel" +msgstr "Отменить" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +msgid "Apply Changes" +msgstr "Применить изменения" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Delete" +msgstr "Удалить" + +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +msgid "Save all" +msgstr "Сохранить все" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Export Database" +msgstr "Экспорт базы данных" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Selection" +msgstr "Выбор" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "Public" +msgstr "Общедоступно" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +msgid "Prepend note permalinks with this Shaarli instance's URL" +msgstr "" +"Добавить постоянные ссылки на заметку с URL адресом этого экземпляра Shaarli" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 +msgid "Useful to import bookmarks in a web browser" +msgstr "Useful to import bookmarks in a web browser" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Import Database" +msgstr "Импорт базы данных" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Maximum size allowed:" +msgstr "Максимально допустимый размер:" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Visibility" +msgstr "Видимость" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Use values from the imported file, default to public" +msgstr "" +"Использовать значения из импортированного файла, по умолчанию общедоступные" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "Import all bookmarks as private" +msgstr "Импортировать все закладки как личные" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +msgid "Import all bookmarks as public" +msgstr "Импортировать все закладки как общедоступные" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 +msgid "Overwrite existing bookmarks" +msgstr "Заменить существующие закладки" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "Duplicates based on URL" +msgstr "Дубликаты на основе URL" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +msgid "Add default tags" +msgstr "Добавить теги по умолчанию" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +msgid "It looks like it's the first time you run Shaarli. Please configure it." +msgstr "Похоже, вы впервые запускаете Shaarli. Пожалуйста, настройте его." + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167 +msgid "Username" +msgstr "Имя пользователя" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168 +msgid "Password" +msgstr "Пароль" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62 +msgid "Shaarli title" +msgstr "Заголовок Shaarli" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 +msgid "My links" +msgstr "Мои ссылки" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 +msgid "Install" +msgstr "Установка" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190 +msgid "Server requirements" +msgstr "Системные требования" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 +msgid "shaare" +msgid_plural "shaares" +msgstr[0] "закладка" +msgstr[1] "закладки" +msgstr[2] "закладок" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "private link" +msgid_plural "private links" +msgstr[0] "личная ссылка" +msgstr[1] "личные ссылки" +msgstr[2] "личных ссылок" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123 +msgid "Search text" +msgstr "Поиск текста" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +msgid "Filter by tag" +msgstr "Фильтровать по тегу" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:87 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:139 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 +msgid "Search" +msgstr "Поиск" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 +msgid "Nothing found." +msgstr "Ничего не найдено." + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 +#, php-format +msgid "%s result" +msgid_plural "%s results" +msgstr[0] "%s результат" +msgstr[1] "%s результатов" +msgstr[2] "%s результатов" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "for" +msgstr "для" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 +msgid "tagged" +msgstr "отмечено" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "Remove tag" +msgstr "Удалить тег" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +msgid "with status" +msgstr "со статусом" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 +msgid "without any tag" +msgstr "без тега" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41 +msgid "Fold" +msgstr "Сложить" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177 +msgid "Edited: " +msgstr "Отредактировано: " + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 +msgid "permalink" +msgstr "постоянная ссылка" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 +msgid "Add tag" +msgstr "Добавить тег" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185 +msgid "Toggle sticky" +msgstr "Закрепить / Открепить" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187 +msgid "Sticky" +msgstr "Закреплено" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 +msgid "Share a private link" +msgstr "Поделиться личной ссылкой" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 +msgid "Filters" +msgstr "Фильтры" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:10 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:10 +msgid "Only display private links" +msgstr "Отображать только личные ссылки" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:13 +msgid "Only display public links" +msgstr "Отображать только общедоступные ссылки" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 +msgid "Filter untagged links" +msgstr "Фильтровать неотмеченные ссылки" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 +msgid "Select all" +msgstr "Выбрать все" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 +msgid "Fold all" +msgstr "Сложить все" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76 +msgid "Links per page" +msgstr "Ссылок на страницу" + +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171 +msgid "Remember me" +msgstr "Запомнить меня" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "by the Shaarli community" +msgstr "сообществом Shaarli" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:16 +msgid "Documentation" +msgstr "Документация" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 +msgid "Expand" +msgstr "Развернуть" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 +msgid "Expand all" +msgstr "Развернуть все" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 +msgid "Are you sure you want to delete this link?" +msgstr "Вы уверены, что хотите удалить эту ссылку?" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 +msgid "Are you sure you want to delete this tag?" +msgstr "Вы уверены, что хотите удалить этот тег?" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11 +msgid "Menu" +msgstr "Меню" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag cloud" +msgstr "Облако тегов" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92 +msgid "RSS Feed" +msgstr "RSS канал" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108 +msgid "Logout" +msgstr "Выйти" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152 +msgid "Set public" +msgstr "Сделать общедоступным" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157 +msgid "Set private" +msgstr "Сделать личным" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189 +msgid "is available" +msgstr "доступно" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196 +msgid "Error" +msgstr "Ошибка" + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "There is no cached thumbnail." +msgstr "Нет кэшированных миниатюр." + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "Try to synchronize them." +msgstr "Попробуйте синхронизировать их." + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Picture Wall" +msgstr "Галерея" + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "pics" +msgstr "изображений" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "You need to enable Javascript to change plugin loading order." +msgstr "" +"Вам необходимо включить Javascript, чтобы изменить порядок загрузки плагинов." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Plugin administration" +msgstr "Управление плагинами" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Enabled Plugins" +msgstr "Включенные плагины" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 +msgid "No plugin enabled." +msgstr "Нет включенных плагинов." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +msgid "Disable" +msgstr "Отключить" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +msgid "Name" +msgstr "Имя" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +msgid "Order" +msgstr "Порядок" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 +msgid "Disabled Plugins" +msgstr "Отключенные плагины" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "No plugin disabled." +msgstr "Нет отключенных плагинов." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "Enable" +msgstr "Включить" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "More plugins available" +msgstr "Доступны другие плагины" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 +msgid "in the documentation" +msgstr "в документации" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 +msgid "Plugin configuration" +msgstr "Настройка плагинов" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195 +msgid "No parameter available." +msgstr "Нет доступных параметров." + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "General" +msgstr "Общее" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +msgid "Index URL" +msgstr "Индексный URL" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Base path" +msgstr "Базовый путь" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Client IP" +msgstr "IP клиента" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "Trusted reverse proxies" +msgstr "Надежные обратные прокси" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "N/A" +msgstr "Нет данных" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 +msgid "Visit releases page on Github" +msgstr "Посетить страницу релизов на Github" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +msgid "Synchronize all link thumbnails" +msgstr "Синхронизировать все миниатюры ссылок" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2 +msgid "Permissions" +msgstr "Разрешения" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8 +msgid "There are permissions that need to be fixed." +msgstr "Есть разрешения, которые нужно исправить." + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23 +msgid "All read/write permissions are properly set." +msgstr "Все разрешения на чтение и запись установлены правильно." + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32 +msgid "Running PHP" +msgstr "Запуск PHP" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36 +msgid "End of life: " +msgstr "Конец жизни: " + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "Extension" +msgstr "Расширение" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49 +msgid "Usage" +msgstr "Применение" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50 +msgid "Status" +msgstr "Статус" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66 +msgid "Loaded" +msgstr "Загружено" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 +msgid "Required" +msgstr "Обязательно" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 +msgid "Optional" +msgstr "Необязательно" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70 +msgid "Not loaded" +msgstr "Не загружено" + +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "tags" +msgstr "теги" + +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "List all links with those tags" +msgstr "Список всех ссылок с этими тегами" + +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag list" +msgstr "Список тегов" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 +msgid "Sort by:" +msgstr "Сортировать по:" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5 +msgid "Cloud" +msgstr "Облако" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6 +msgid "Most used" +msgstr "Наиболее используемое" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7 +msgid "Alphabetical" +msgstr "Алфавит" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Settings" +msgstr "Настройки" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Change Shaarli settings: title, timezone, etc." +msgstr "Измените настройки Shaarli: заголовок, часовой пояс и т.д." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "Configure your Shaarli" +msgstr "Настройка Shaarli" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +msgid "Enable, disable and configure plugins" +msgstr "Включить, отключить и настроить плагины" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 +msgid "Check instance's server configuration" +msgstr "Проверка конфигурации экземпляра сервера" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +msgid "Change your password" +msgstr "Изменить пароль" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "Rename or delete a tag in all links" +msgstr "Переименовать или удалить тег во всех ссылках" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "" +"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " +"delicious...)" +msgstr "" +"Импорт закладок Netscape HTML (экспортированные из Firefox, Chrome, Opera, " +"delicious...)" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +msgid "Import links" +msgstr "Импорт ссылок" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +msgid "" +"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " +"Opera, delicious...)" +msgstr "" +"Экспорт закладок Netscape HTML (которые могут быть импортированы в Firefox, " +"Chrome, Opera, delicious...)" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54 +msgid "Export database" +msgstr "Экспорт базы данных" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "" +"Drag one of these button to your bookmarks toolbar or right-click it and " +"\"Bookmark This Link\"" +msgstr "" +"Перетащите одну из этих кнопок на панель закладок или щелкните по ней правой " +"кнопкой мыши и выберите \"Добавить ссылку в закладки\"" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +msgid "then click on the bookmarklet in any page you want to share." +msgstr "" +"затем щелкните букмарклет на любой странице, которой хотите поделиться." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 +msgid "" +"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " +"Link" +msgstr "" +"Перетащите эту ссылку на панель закладок или щелкните по ней правой кнопкой " +"мыши и добавьте эту ссылку в закладки" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "then click ✚Shaare link button in any page you want to share" +msgstr "" +"затем нажмите кнопку ✚Поделиться ссылкой на любой странице, которой хотите " +"поделиться" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 +msgid "The selected text is too long, it will be truncated." +msgstr "Выделенный текст слишком длинный, он будет обрезан." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "Shaare link" +msgstr "Поделиться ссылкой" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 +msgid "" +"Then click ✚Add Note button anytime to start composing a private Note (text " +"post) to your Shaarli" +msgstr "" +"Затем в любое время нажмите кнопку ✚Добавить заметку, чтобы начать создавать " +"личную заметку (текстовое сообщение) в своем Shaarli" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +msgid "Add Note" +msgstr "Добавить заметку" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132 +msgid "3rd party" +msgstr "Третья сторона" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140 +msgid "plugin" +msgstr "плагин" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 +msgid "" +"Drag this link to your bookmarks toolbar, or right-click it and choose " +"Bookmark This Link" +msgstr "" +"Перетащите эту ссылку на панель закладок или щелкните по ней правой кнопкой " +"мыши и выберите \"Добавить ссылку в закладки\""