diff --git a/.eslintrc.js b/.dev/.eslintrc.js similarity index 100% rename from .eslintrc.js rename to .dev/.eslintrc.js diff --git a/.dev/.sasslintrc b/.dev/.sasslintrc new file mode 100644 index 0000000..ac406d7 --- /dev/null +++ b/.dev/.sasslintrc @@ -0,0 +1,15 @@ +options: + max-warnings: 0 +rules: + property-sort-order: + - 1 + - + order: 'concentric' + no-important: + - 0 + no-vendor-prefixes: + - 0 # this will be fixed with v2: see https://github.com/sasstools/sass-lint/pull/1137 + nesting-depth: + - 1 + - + max-depth: 4 diff --git a/.editorconfig b/.editorconfig index 8783e4c..34bd799 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ trim_trailing_whitespace = true indent_style = space indent_size = 4 -[*.{htaccess,html,js,json,xml}] +[*.{htaccess,html,scss,js,json,xml,yml}] indent_size = 2 [*.php] diff --git a/.gitattributes b/.gitattributes index 549777e..6b6ffbd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -26,6 +26,7 @@ Dockerfile text # Exclude from Git archives .editorconfig export-ignore +.dev export-ignore .gitattributes export-ignore .github export-ignore .gitignore export-ignore diff --git a/.travis.yml b/.travis.yml index 1b2bf97..eee1ca7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,45 @@ sudo: false dist: trusty -language: php + +matrix: + include: + - language: php + php: 7.2 + - language: php + php: 7.1 + - language: php + php: 7.0 + - language: php + php: 5.6 + - language: node_js + node_js: 8 + cache: + yarn: true + directories: + - $HOME/.cache/yarn + + install: + - yarn install + + before_script: + - PATH=${PATH//:\.\/node_modules\/\.bin/} + + script: + - yarn run build # Just to be sure that the build isn't broken + - make eslint + - make sasslint + cache: - yarn: true directories: - $HOME/.composer/cache - - $HOME/.cache/yarn -php: - - 7.2 - - 7.1 - - 7.0 - - 5.6 + install: - - yarn install - composer install --prefer-dist + before_script: - PATH=${PATH//:\.\/node_modules\/\.bin/} + script: - make clean - make check_permissions - - make eslint - make all_tests diff --git a/Makefile b/Makefile index d121656..4adbdd6 100644 --- a/Makefile +++ b/Makefile @@ -218,5 +218,9 @@ translate: ### Run ESLint check against Shaarli's JS files eslint: - @yarn run eslint assets/vintage/js/ - @yarn run eslint assets/default/js/ + @yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/ + @yarn run eslint -c .dev/.eslintrc.js assets/default/js/ + +### Run CSSLint check against Shaarli's SCSS files +sasslint: + @yarn run sass-lint -c .dev/.sasslintrc 'assets/default/scss/*.scss' -v -q diff --git a/application/HttpUtils.php b/application/HttpUtils.php index 83a4c5e..e928250 100644 --- a/application/HttpUtils.php +++ b/application/HttpUtils.php @@ -1,7 +1,7 @@ t('Automatic'), 'en' => t('English'), 'fr' => t('French'), + 'de' => t('German'), ]; } } diff --git a/application/LinkDB.php b/application/LinkDB.php index c1661d5..cd0f296 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@ -436,15 +436,17 @@ You use the community supported version of the original Shaarli project, by Seba /** * Returns the list tags appearing in the links with the given tags - * @param $filteringTags: tags selecting the links to consider - * @param $visibility: process only all/private/public links - * @return: a tag=>linksCount array + * + * @param array $filteringTags tags selecting the links to consider + * @param string $visibility process only all/private/public links + * + * @return array tag => linksCount */ public function linksCountPerTag($filteringTags = [], $visibility = 'all') { - $links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility); - $tags = array(); - $caseMapping = array(); + $links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility); + $tags = []; + $caseMapping = []; foreach ($links as $link) { foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { if (empty($tag)) { @@ -458,8 +460,19 @@ You use the community supported version of the original Shaarli project, by Seba $tags[$caseMapping[strtolower($tag)]]++; } } - // Sort tags by usage (most used tag first) - arsort($tags); + + /* + * Formerly used arsort(), which doesn't define the sort behaviour for equal values. + * Also, this function doesn't produce the same result between PHP 5.6 and 7. + * + * So we now use array_multisort() to sort tags by DESC occurrences, + * then ASC alphabetically for equal values. + * + * @see https://github.com/shaarli/Shaarli/issues/1142 + */ + $keys = array_keys($tags); + $tmpTags = array_combine($keys, $keys); + array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); return $tags; } diff --git a/application/LinkUtils.php b/application/LinkUtils.php index 3705f7e..4df5c0c 100644 --- a/application/LinkUtils.php +++ b/application/LinkUtils.php @@ -11,6 +11,7 @@ */ function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo') { + $isRedirected = false; /** * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). * @@ -22,16 +23,24 @@ function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_get * * @return int|bool length of $data or false if we need to stop the download */ - return function(&$ch, $data) use ($curlGetInfo, &$charset, &$title) { + return function(&$ch, $data) use ($curlGetInfo, &$charset, &$title, &$isRedirected) { $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); - if (!empty($responseCode) && $responseCode != 200) { + if (!empty($responseCode) && in_array($responseCode, [301, 302])) { + $isRedirected = true; + return strlen($data); + } + if (!empty($responseCode) && $responseCode !== 200) { return false; } - $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); + // After a redirection, the content type will keep the previous request value + // until it finds the next content-type header. + if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { + $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); + } if (!empty($contentType) && strpos($contentType, 'text/html') === false) { return false; } - if (empty($charset)) { + if (!empty($contentType) && empty($charset)) { $charset = header_extract_charset($contentType); } if (empty($charset)) { diff --git a/application/LoginManager.php b/application/LoginManager.php deleted file mode 100644 index 397bc6e..0000000 --- a/application/LoginManager.php +++ /dev/null @@ -1,134 +0,0 @@ -globals = &$globals; - $this->configManager = $configManager; - $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php'); - $this->readBanFile(); - } - - /** - * Read a file containing banned IPs - */ - protected function readBanFile() - { - if (! file_exists($this->banFile)) { - return; - } - include $this->banFile; - } - - /** - * Write the banned IPs to a file - */ - protected function writeBanFile() - { - if (! array_key_exists('IPBANS', $this->globals)) { - return; - } - file_put_contents( - $this->banFile, - "globals['IPBANS'], true) . ";\n?>" - ); - } - - /** - * Handle a failed login and ban the IP after too many failed attempts - * - * @param array $server The $_SERVER array - */ - public function handleFailedLogin($server) - { - $ip = $server['REMOTE_ADDR']; - $trusted = $this->configManager->get('security.trusted_proxies', []); - - if (in_array($ip, $trusted)) { - $ip = getIpAddressFromProxy($server, $trusted); - if (! $ip) { - // the IP is behind a trusted forward proxy, but is not forwarded - // in the HTTP headers, so we do nothing - return; - } - } - - // increment the fail count for this IP - if (isset($this->globals['IPBANS']['FAILURES'][$ip])) { - $this->globals['IPBANS']['FAILURES'][$ip]++; - } else { - $this->globals['IPBANS']['FAILURES'][$ip] = 1; - } - - if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) { - $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800); - logm( - $this->configManager->get('resource.log'), - $server['REMOTE_ADDR'], - 'IP address banned from login' - ); - } - $this->writeBanFile(); - } - - /** - * Handle a successful login - * - * @param array $server The $_SERVER array - */ - public function handleSuccessfulLogin($server) - { - $ip = $server['REMOTE_ADDR']; - // FIXME unban when behind a trusted proxy? - - unset($this->globals['IPBANS']['FAILURES'][$ip]); - unset($this->globals['IPBANS']['BANS'][$ip]); - - $this->writeBanFile(); - } - - /** - * Check if the user can login from this IP - * - * @param array $server The $_SERVER array - * - * @return bool true if the user is allowed to login - */ - public function canLogin($server) - { - $ip = $server['REMOTE_ADDR']; - - if (! isset($this->globals['IPBANS']['BANS'][$ip])) { - // the user is not banned - return true; - } - - if ($this->globals['IPBANS']['BANS'][$ip] > time()) { - // the user is still banned - return false; - } - - // the ban has expired, the user can attempt to log in again - logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.'); - unset($this->globals['IPBANS']['FAILURES'][$ip]); - unset($this->globals['IPBANS']['BANS'][$ip]); - - $this->writeBanFile(); - return true; - } -} diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 3233d6b..a448387 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php @@ -25,6 +25,9 @@ class PageBuilder * @var LinkDB $linkDB instance. */ protected $linkDB; + + /** @var bool $isLoggedIn Whether the user is logged in **/ + protected $isLoggedIn = false; /** * PageBuilder constructor. @@ -34,12 +37,13 @@ class PageBuilder * @param LinkDB $linkDB instance. * @param string $token Session token */ - public function __construct(&$conf, $linkDB = null, $token = null) + public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false) { $this->tpl = false; $this->conf = $conf; $this->linkDB = $linkDB; $this->token = $token; + $this->isLoggedIn = $isLoggedIn; } /** @@ -55,7 +59,7 @@ class PageBuilder $this->conf->get('resource.update_check'), $this->conf->get('updates.check_updates_interval'), $this->conf->get('updates.check_updates'), - isLoggedIn(), + $this->isLoggedIn, $this->conf->get('updates.check_updates_branch') ); $this->tpl->assign('newVersion', escape($version)); @@ -67,6 +71,7 @@ class PageBuilder $this->tpl->assign('versionError', escape($exc->getMessage())); } + $this->tpl->assign('is_logged_in', $this->isLoggedIn); $this->tpl->assign('feedurl', escape(index_url($_SERVER))); $searchcrits = ''; // Search criteria if (!empty($_GET['searchtags'])) { diff --git a/application/SessionManager.php b/application/SessionManager.php deleted file mode 100644 index 71f0b38..0000000 --- a/application/SessionManager.php +++ /dev/null @@ -1,83 +0,0 @@ -session = &$session; - $this->conf = $conf; - } - - /** - * Generates a session token - * - * @return string token - */ - public function generateToken() - { - $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt')); - $this->session['tokens'][$token] = 1; - return $token; - } - - /** - * Checks the validity of a session token, and destroys it afterwards - * - * @param string $token The token to check - * - * @return bool true if the token is valid, else false - */ - public function checkToken($token) - { - if (! isset($this->session['tokens'][$token])) { - // the token is wrong, or has already been used - return false; - } - - // destroy the token to prevent future use - unset($this->session['tokens'][$token]); - return true; - } - - /** - * Validate session ID to prevent Full Path Disclosure. - * - * See #298. - * The session ID's format depends on the hash algorithm set in PHP settings - * - * @param string $sessionId Session ID - * - * @return true if valid, false otherwise. - * - * @see http://php.net/manual/en/function.hash-algos.php - * @see http://php.net/manual/en/session.configuration.php - */ - public static function checkId($sessionId) - { - if (empty($sessionId)) { - return false; - } - - if (!$sessionId) { - return false; - } - - if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { - return false; - } - - return true; - } -} diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php new file mode 100644 index 0000000..d6784d6 --- /dev/null +++ b/application/security/LoginManager.php @@ -0,0 +1,265 @@ +globals = &$globals; + $this->configManager = $configManager; + $this->sessionManager = $sessionManager; + $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php'); + $this->readBanFile(); + if ($this->configManager->get('security.open_shaarli') === true) { + $this->openShaarli = true; + } + } + + /** + * Generate a token depending on deployment salt, user password and client IP + * + * @param string $clientIpAddress The remote client IP address + */ + public function generateStaySignedInToken($clientIpAddress) + { + $this->staySignedInToken = sha1( + $this->configManager->get('credentials.hash') + . $clientIpAddress + . $this->configManager->get('credentials.salt') + ); + } + + /** + * Return the user's client stay-signed-in token + * + * @return string User's client stay-signed-in token + */ + public function getStaySignedInToken() + { + return $this->staySignedInToken; + } + + /** + * Check user session state and validity (expiration) + * + * @param array $cookie The $_COOKIE array + * @param string $clientIpId Client IP address identifier + */ + public function checkLoginState($cookie, $clientIpId) + { + if (! $this->configManager->exists('credentials.login')) { + // Shaarli is not configured yet + $this->isLoggedIn = false; + return; + } + + if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) + && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken + ) { + // The user client has a valid stay-signed-in cookie + // Session information is updated with the current client information + $this->sessionManager->storeLoginInfo($clientIpId); + + } elseif ($this->sessionManager->hasSessionExpired() + || $this->sessionManager->hasClientIpChanged($clientIpId) + ) { + $this->sessionManager->logout(); + $this->isLoggedIn = false; + return; + } + + $this->isLoggedIn = true; + $this->sessionManager->extendSession(); + } + + /** + * Return whether the user is currently logged in + * + * @return true when the user is logged in, false otherwise + */ + public function isLoggedIn() + { + if ($this->openShaarli) { + return true; + } + return $this->isLoggedIn; + } + + /** + * 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) + { + $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); + + if ($login != $this->configManager->get('credentials.login') + || $hash != $this->configManager->get('credentials.hash') + ) { + logm( + $this->configManager->get('resource.log'), + $remoteIp, + 'Login failed for user ' . $login + ); + return false; + } + + $this->sessionManager->storeLoginInfo($clientIpId); + logm( + $this->configManager->get('resource.log'), + $remoteIp, + 'Login successful' + ); + return true; + } + + /** + * Read a file containing banned IPs + */ + protected function readBanFile() + { + if (! file_exists($this->banFile)) { + return; + } + include $this->banFile; + } + + /** + * Write the banned IPs to a file + */ + protected function writeBanFile() + { + if (! array_key_exists('IPBANS', $this->globals)) { + return; + } + file_put_contents( + $this->banFile, + "globals['IPBANS'], true) . ";\n?>" + ); + } + + /** + * Handle a failed login and ban the IP after too many failed attempts + * + * @param array $server The $_SERVER array + */ + public function handleFailedLogin($server) + { + $ip = $server['REMOTE_ADDR']; + $trusted = $this->configManager->get('security.trusted_proxies', []); + + if (in_array($ip, $trusted)) { + $ip = getIpAddressFromProxy($server, $trusted); + if (! $ip) { + // the IP is behind a trusted forward proxy, but is not forwarded + // in the HTTP headers, so we do nothing + return; + } + } + + // increment the fail count for this IP + if (isset($this->globals['IPBANS']['FAILURES'][$ip])) { + $this->globals['IPBANS']['FAILURES'][$ip]++; + } else { + $this->globals['IPBANS']['FAILURES'][$ip] = 1; + } + + if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) { + $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800); + logm( + $this->configManager->get('resource.log'), + $server['REMOTE_ADDR'], + 'IP address banned from login' + ); + } + $this->writeBanFile(); + } + + /** + * Handle a successful login + * + * @param array $server The $_SERVER array + */ + public function handleSuccessfulLogin($server) + { + $ip = $server['REMOTE_ADDR']; + // FIXME unban when behind a trusted proxy? + + unset($this->globals['IPBANS']['FAILURES'][$ip]); + unset($this->globals['IPBANS']['BANS'][$ip]); + + $this->writeBanFile(); + } + + /** + * Check if the user can login from this IP + * + * @param array $server The $_SERVER array + * + * @return bool true if the user is allowed to login + */ + public function canLogin($server) + { + $ip = $server['REMOTE_ADDR']; + + if (! isset($this->globals['IPBANS']['BANS'][$ip])) { + // the user is not banned + return true; + } + + if ($this->globals['IPBANS']['BANS'][$ip] > time()) { + // the user is still banned + return false; + } + + // the ban has expired, the user can attempt to log in again + logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.'); + unset($this->globals['IPBANS']['FAILURES'][$ip]); + unset($this->globals['IPBANS']['BANS'][$ip]); + + $this->writeBanFile(); + return true; + } +} diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php new file mode 100644 index 0000000..b8b8ab8 --- /dev/null +++ b/application/security/SessionManager.php @@ -0,0 +1,199 @@ +session = &$session; + $this->conf = $conf; + } + + /** + * Define whether the user should stay signed in across browser sessions + * + * @param bool $staySignedIn Keep the user signed in + */ + public function setStaySignedIn($staySignedIn) + { + $this->staySignedIn = $staySignedIn; + } + + /** + * Generates a session token + * + * @return string token + */ + public function generateToken() + { + $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt')); + $this->session['tokens'][$token] = 1; + return $token; + } + + /** + * Checks the validity of a session token, and destroys it afterwards + * + * @param string $token The token to check + * + * @return bool true if the token is valid, else false + */ + public function checkToken($token) + { + if (! isset($this->session['tokens'][$token])) { + // the token is wrong, or has already been used + return false; + } + + // destroy the token to prevent future use + unset($this->session['tokens'][$token]); + return true; + } + + /** + * Validate session ID to prevent Full Path Disclosure. + * + * See #298. + * The session ID's format depends on the hash algorithm set in PHP settings + * + * @param string $sessionId Session ID + * + * @return true if valid, false otherwise. + * + * @see http://php.net/manual/en/function.hash-algos.php + * @see http://php.net/manual/en/session.configuration.php + */ + public static function checkId($sessionId) + { + if (empty($sessionId)) { + return false; + } + + if (!$sessionId) { + return false; + } + + if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { + return false; + } + + return true; + } + + /** + * Store user login information after a successful login + * + * @param string $clientIpId Client IP address identifier + */ + public function storeLoginInfo($clientIpId) + { + $this->session['ip'] = $clientIpId; + $this->session['username'] = $this->conf->get('credentials.login'); + $this->extendTimeValidityBy(self::$SHORT_TIMEOUT); + } + + /** + * Extend session validity + */ + public function extendSession() + { + if ($this->staySignedIn) { + return $this->extendTimeValidityBy(self::$LONG_TIMEOUT); + } + return $this->extendTimeValidityBy(self::$SHORT_TIMEOUT); + } + + /** + * Extend expiration time + * + * @param int $duration Expiration time extension (seconds) + * + * @return int New session expiration time + */ + protected function extendTimeValidityBy($duration) + { + $expirationTime = time() + $duration; + $this->session['expires_on'] = $expirationTime; + return $expirationTime; + } + + /** + * Logout a user by unsetting all login information + * + * See: + * - https://secure.php.net/manual/en/function.setcookie.php + */ + public function logout() + { + if (isset($this->session)) { + unset($this->session['ip']); + unset($this->session['expires_on']); + unset($this->session['username']); + unset($this->session['visibility']); + unset($this->session['untaggedonly']); + } + } + + /** + * Check whether the session has expired + * + * @param string $clientIpId Client IP address identifier + * + * @return bool true if the session has expired, false otherwise + */ + public function hasSessionExpired() + { + if (empty($this->session['expires_on'])) { + return true; + } + if (time() >= $this->session['expires_on']) { + return true; + } + return false; + } + + /** + * Check whether the client IP address has changed + * + * @param string $clientIpId Client IP address identifier + * + * @return bool true if the IP has changed, false if it has not, or + * if session protection has been disabled + */ + public function hasClientIpChanged($clientIpId) + { + if ($this->conf->get('security.session_protection_disabled') === true) { + return false; + } + if (isset($this->session['ip']) && $this->session['ip'] === $clientIpId) { + return false; + } + return true; + } +} diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index 25440de..09d5efb 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss @@ -1,1357 +1,1545 @@ -$fa-font-path: "~font-awesome/fonts"; +$fa-font-path: '~font-awesome/fonts'; -@import "~font-awesome/scss/font-awesome.scss"; +@import '~font-awesome/scss/font-awesome'; @import '~purecss/build/pure.css'; @import '~purecss/build/grids-responsive.css'; @import '~pure-extras/css/pure-extras.css'; @import '~awesomplete/awesomplete.css'; -/** - * General - */ +$white: #fff; +$black: #000; +$almost-white: #f5f5f5; +$dark-grey: #252525; +$light-grey: #797979; +$main-green: #1b926c; +$light-green: #b0ddce; +$dark-green: #2a4c41; +$red: #ac2925; +$orange: #f89406; +$blue: #0b5ea6; +$background-color: #d0d0d0; +$background-linklist-info: #ddd; +$light-shadow: rgba(255, 255, 255, .078); +$dark-shadow: rgba(0, 0, 0, .298); +$warning-text: #97600d; +$form-input-border: #d8d8d8; +$form-input-background: #eee; + +// General body { - background: #d0d0d0; + background: $background-color; } .strong { - font-weight: bold; + font-weight: bold; } .clear { - clear: both; + clear: both; } .center { - text-align: center; - margin: auto; + margin: auto; + text-align: center; } .label { - display: inline-block; - padding: .25em .4em; - font-size: 75%; - font-weight: 700; - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25rem; + display: inline-block; + border-radius: .25rem; + padding: .25em .4em; + vertical-align: baseline; + text-align: center; + line-height: 1; + white-space: nowrap; + font-size: 75%; + font-weight: 700; } pre { - max-width: 100%; + max-width: 100%; } @font-face { - font-family: 'Roboto'; - font-weight: 400; - font-style: normal; - src: - local('Roboto'), - local('Roboto-Regular'), - url('../fonts/Roboto-Regular.woff2') format('woff2'), - url('../fonts/Roboto-Regular.woff') format('woff'); + font-family: 'Roboto'; + font-weight: 400; + font-style: normal; + src: local('Roboto'), + local('Roboto-Regular'), + url('../fonts/Roboto-Regular.woff2') format('woff2'), + url('../fonts/Roboto-Regular.woff') format('woff'); } @font-face { - font-family: 'Roboto'; - font-weight: 700; - font-style: normal; - src: - local('Roboto'), - local('Roboto-Bold'), - url('../fonts/Roboto-Bold.woff2') format('woff2'), - url('../fonts/Roboto-Bold.woff') format('woff'); + font-family: 'Roboto'; + font-weight: 700; + font-style: normal; + src: local('Roboto'), + local('Roboto-Bold'), + url('../fonts/Roboto-Bold.woff2') format('woff2'), + url('../fonts/Roboto-Bold.woff') format('woff'); } -body, .pure-g [class*="pure-u"] { - font-family: Roboto, Arial, sans-serif; +body, +.pure-g [class*='pure-u'] { + font-family: Roboto, Arial, sans-serif; +} + +// Extends Pure grids responsive to hide items. +// Use xx-0 to hide an item on xx screen. +// Display it at any level with xx-visible. +.pure-u-0 { + display: none !important; } -/** - * Extends Pure grids responsive to hide items. - * Use xx-0 to hide an item on xx screen. - * Display it at any level with xx-visible. - */ -.pure-u-0 { display: none !important; } @media screen and (min-width: 35.5em) { - .pure-u-sm-0 { display: none !important; } - .pure-u-sm-visible { display: inline-block !important; } -} -@media screen and (min-width: 48em) { - .pure-u-md-0 { display: none !important; } - .pure-u-md-visible { display: inline-block !important; } -} -@media screen and (min-width: 64em) { - .pure-u-lg-0 { display: none !important; } - .pure-u-lg-visible { display: inline-block !important; } -} -@media screen and (min-width: 80em) { - .pure-u-xl-0 { display: none !important; } - .pure-u-xl-visible { display: inline-block !important; } + .pure-u-sm-0 { + display: none !important; + } + + .pure-u-sm-visible { + display: inline-block !important; + } } -/** - * Make pure-extras alert closable. - */ -.pure-alert-closable .fa-times { - float: right; +@media screen and (min-width: 48em) { + .pure-u-md-0 { + display: none !important; + } + + .pure-u-md-visible { + display: inline-block !important; + } } + +@media screen and (min-width: 64em) { + .pure-u-lg-0 { + display: none !important; + } + + .pure-u-lg-visible { + display: inline-block !important; + } +} + +@media screen and (min-width: 80em) { + .pure-u-xl-0 { + display: none !important; + } + + .pure-u-xl-visible { + display: inline-block !important; + } +} + +// Make pure-extras alert closable. +.pure-alert-closable { + .fa-times { + float: right; + } +} + .pure-alert-close { - cursor: pointer; + cursor: pointer; } .pure-alert-success { - background-color: #1b926c; + background-color: $main-green; } -.anchor:target { +.anchor { + &:target { padding-top: 40px; + } } -/** - * MENU - **/ + +// MENU .shaarli-menu { - position: fixed; - top: 0; - width: 100%; - --height: 50px; - background: #1b926c; - -webkit-font-smoothing: antialiased; - /* Hack to transition with auto height: http://stackoverflow.com/a/8331169/1484919 */ - max-height: 45px; - transition: max-height 0.5s; - overflow: hidden; - z-index: 999; -} + position: fixed; + top: 0; + transition: max-height .5s; + z-index: 999; + background: $main-green; + width: 100%; + // Hack to transition with auto height: http://stackoverflow.com/a/8331169/1484919 + max-height: 45px; + overflow: hidden; + -webkit-font-smoothing: antialiased; -/* Chrome bugfix: with 100% height, it only displays the first element. */ -.pure-menu-item { - height: 45px; -} - -.shaarli-menu.open { + &.open { + transition: max-height .75s; max-height: 500px; - transition: max-height 0.75s; + } +} + +.pure-menu-item { + // Chrome bugfix: with 100% height, it only displays the first element. + height: 45px; + + &:hover { + &::after { + display: block; + margin: -4px auto 0; + background: $white; + width: 100%; + height: 4px; + content: ''; + } + } } .head-logo { - float: left; - margin: 0 5px 0 0; + float: left; + margin: 0 5px 0 0; } -.pure-menu-link, -.pure-menu-link:visited, -.pure-menu-selected .pure-menu-link, -.pure-menu-selected .pure-menu-link:visited { - padding: 0.8em 1em; - color: #f5f5f5; +%menu-link { + padding: .8em 1em; + color: $almost-white; } -.pure-menu-link:hover, .pure-menu-link:focus, -.pure-menu-selected .pure-menu-link:hover, -.pure-menu-selected .pure-menu-link:focus { - color: #fff; - background: transparent; +%menu-link-hover { + background: transparent; + color: $white; } -.pure-menu-item:hover::after { - margin: -4px auto 0 auto; - display: block; - content:""; - background: #fff; - height: 4px; - width: 100%; +.pure-menu-link { + @extend %menu-link; + + &:visited { + @extend %menu-link; + } + + &:hover, + &:focus { + @extend %menu-link-hover; + } +} + +.pure-menu-selected { + .pure-menu-link { + @extend %menu-link; + + &:visited { + @extend %menu-link; + } + + &:hover, + &:focus { + @extend %menu-link-hover; + } + } } .menu-toggle { - width: 34px; - height: 45px; - position: absolute; - top: 5px; - right: 0; - display: none; -} + display: none; + position: absolute; + top: 5px; + right: 0; + width: 34px; + height: 45px; -.menu-toggle .bar { - background-color: #b0ddce; + .bar { display: block; - width: 20px; - height: 2px; - border-radius: 100px; position: absolute; top: 18px; right: 7px; - transition: all 0.5s; -} + border-radius: 100px; + background-color: $light-green; + width: 20px; + height: 2px; + transition-duration: .5s; -.menu-toggle .bar:first-child { - transform: translateY(-6px); -} + &:first-child { + transform: translateY(-6px); + } + } -.menu-toggle.x .bar { - transform: rotate(45deg); -} + &.x { + .bar { + transform: rotate(45deg); -.menu-toggle.x .bar:first-child { - transform: rotate(-45deg); + &:first-child { + transform: rotate(-45deg); + } + } + } } @media screen and (max-width: 64em) { - .menu-toggle { - display: block; - } + .menu-toggle { + display: block; + } } .header-buttons { - text-align: right; + text-align: right; } .linkcount { - color: #252525; - font-size: 0.8em; + color: $dark-grey; + font-size: .8em; } @media screen and (min-width: 64em) { - .linkcount { - position: absolute; - right: 5px; + .linkcount { + position: absolute; + right: 5px; + } +} + +.searchform-block { + width: 100%; + text-align: center; + + input { + &[type='text'] { + border: medium none currentColor; + border-radius: 2px; + box-shadow: 0 1px 0 $light-shadow, 0 1px 1px $dark-shadow inset; + background: $almost-white; + padding: 0 5px; + width: 260px; + height: 30px; + color: $dark-grey; + + &::-webkit-input-placeholder { + color: $light-grey; + } } -} + } -#search, #search-linklist, #search-tagcloud { - text-align: center; - width: 100%; -} - -#search input[type="text"], #search-linklist input[type="text"] { - padding: 0 5px; - height: 30px; - width: 260px; - background: #f5f5f5; - border: medium none currentColor; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.078), 0 1px 1px rgba(0, 0, 0, 0.298) inset; + button { + border: 0; border-radius: 2px; - color: #252525; + background-color: $main-green; + padding: 4px 8px 6px; + color: $almost-white; + } } + @media screen and (max-width: 64em) { - .searchform { - max-width: 260px; - margin: 0 auto; - } + .searchform { + margin: 0 auto; + max-width: 260px; + } } -/* because chrome */ -#search input[type="text"]::-webkit-input-placeholder, -#search-linklist input[type="text"]::-webkit-input-placeholder { - color: #777777; -} - -#search button, -#search-tagcloud button, -#search-linklist button { - padding: 4px 8px 6px 8px; - background-color: #1B926C; - color: #f5f5f5; - border: none; - border-radius: 2px; -} - -#search-tagcloud button { +.search-tagcloud { + button { width: 90%; + } } @media screen and (max-width: 64em) { - #search-linklist button { - width: 100%; + .search-linklist { + button { + width: 100%; } - #search-linklist .awesomplete { - margin: 5px 0; + + .awesomplete { + margin: 5px 0; } + } } -#search button:hover, -#search-linklist button:hover, -#search-tagcloud button:hover { - color: #d0d0d0; +.header-search, +.search-linklist, +.search-tagcloud { + button { + &:hover { + color: $background-color; + } + } } -#search, -#search-linklist { - padding: 6px 0; +.header-search, +.search-linklist { + padding: 6px 0; } @media screen and (max-width: 64em) { - #search, #search * { - visibility: hidden; - } + .header-search , + .header-search * { + visibility: hidden; + } } -.subheader-form a.button { - color: #f5f5f5; - font-weight: bold; - text-decoration: none; - border: 2px solid #f5f5f5; - border-radius: 5px; - padding: 3px 10px; -} - -.linklist-item-editbuttons .delete-checkbox { - display: none; -} - -#header-login-form input[type="text"], #header-login-form input[type="password"] { - width: 200px; -} - -/* because chrome */ -#header-login-form input[type="text"]::-webkit-input-placeholder, -#header-login-form input[type="password"]::-webkit-input-placeholder { - color: #777777; +%subheader-form-input { + border: medium none currentColor; + border-radius: 2px; + box-shadow: 0 1px 0 $light-shadow, 0 1px 4px $dark-shadow inset; + background: $almost-white; + padding: 5px 5px 3px 15px; + width: 20%; + height: 20px; + color: $dark-grey; } .subheader-form { - visibility: hidden; - position: fixed; - width: 100%; - text-align: center; - background: #1b926c; - display: block; - z-index: 999; - height: 30px; - padding: 5px 0; + display: block; + position: fixed; + visibility: hidden; + z-index: 999; + background: $main-green; + padding: 5px 0; + width: 100%; + height: 30px; + text-align: center; + + input { + &[type='text'], + &[type='password'] { + @extend %subheader-form-input; + + &::-webkit-input-placeholder { + color: $dark-grey; + } + } + } + + &[type='submit'] { + display: inline-block; + margin: 0 0 5px; + border: 1px solid $almost-white; + border-radius: 2px; + background: $main-green; + padding: 4px 0; + width: 100px; + height: 28px; + color: $almost-white; + + &:hover { + background: $almost-white; + color: $main-green; + } + } + + .remember-me { + @extend %subheader-form-input; + + display: inline-block; + cursor: pointer; + padding: 5px 20px 3px; + width: auto; + + label, + input { + cursor: pointer; + } + } + + a { + &.button { + border: 2px solid $almost-white; + border-radius: 5px; + padding: 3px 10px; + text-decoration: none; + color: $almost-white; + font-weight: bold; + } + } +} + +.header-login-form { + input { + &[type='text'], + &[type='password'] { + width: 200px; + + // because chrome + &::-webkit-input-placeholder { + color: $light-grey; + } + } + } } @media screen and (min-width: 64em) { - .subheader-form.open, .subheader-form.open * { + .subheader-form { + &.open { + visibility: visible; + + * { visibility: visible; + } } -} - -.subheader-form input[type="text"], .subheader-form input[type="password"], .subheader-form .remember-me { - padding: 5px 5px 3px 15px; - height: 20px; - width: 20%; - background: #f5f5f5; - border: medium none currentColor; - border-radius: 2px; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.078), 0 1px 4px rgba(0, 0, 0, 0.298) inset; - color: #252525; -} - -/* because chrome */ -.subheader-form input[type="text"]::-webkit-input-placeholder, -.subheader-form input[type="password"]::-webkit-input-placeholder -{ - color: #252525; -} - -.subheader-form .remember-me { - display: inline-block; - width: auto; - padding: 5px 20px 3px 20px; - cursor: pointer; -} - -.subheader-form .remember-me label, .subheader-form .remember-me input { - cursor: pointer; -} - -.subheader-form input[type="submit"] { - display: inline-block; - margin: 0 0 5px 0; - padding: 4px 0 4px 0; - height: 28px; - width: 100px; - background: #1b926c; - border: 1px solid #f5f5f5; - color: #f5f5f5; - border-radius: 2px; -} - -.subheader-form input[type="submit"]:hover { - background: #f5f5f5; - color: #1b926c; + } } .new-version-message { - text-align: center; -} + text-align: center; -.new-version-message a { - color: rgb(151, 96, 13); + a { + color: $warning-text; font-weight: bold; + } } -/** - * CONTENT - GENERAL - */ -#content { - position: relative; - z-index: 2; - margin-top: 45px; +// CONTENT - GENERAL +.container { + position: relative; + z-index: 2; + margin-top: 45px; } -/** - * Plugins additional forms - */ +// Plugins additional forms .toolbar-plugin { - margin: 5px 0; - text-align: center; -} + margin: 5px 0; + text-align: center; -.toolbar-plugin input[type="text"] { - padding: 0 5px; - height: 30px; - width: 300px; - background: #f5f5f5; - border: medium none currentColor; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.078), 0 1px 1px rgba(0, 0, 0, 0.298) inset; - border-radius: 2px; - color: #252525; -} + input { + &[type='text'] { + border: medium none currentColor; + border-radius: 2px; + box-shadow: 0 1px 0 $light-shadow, 0 1px 1px $dark-shadow inset; + background: $almost-white; + padding: 0 5px; + width: 300px; + height: 30px; + color: $dark-grey; -/* because chrome */ -.toolbar-plugin input[type="text"]::-webkit-input-placeholder { - color: #777777; -} + &::-webkit-input-placeholder { + color: $light-grey; + } + } -.toolbar-plugin input[type="submit"] { - padding: 0 10px; - height: 30px; - background: #f5f5f5; - border: medium none currentColor; - border-radius: 2px; - color: #252525; -} + &[type='submit'] { + border: medium none currentColor; + border-radius: 2px; + background: $almost-white; + padding: 0 10px; + height: 30px; + color: $dark-grey; -.toolbar-plugin input[type="submit"]:hover { - background: #fff; + &:hover { + background: $white; + } + } + } } @media screen and (max-width: 64em) { - .toolbar-plugin input[type="text"] { + .toolbar-plugin { + input { + &[type='text'] { width: 70%; - + } } + } } -/** - * CONTENT - LINKLIST PAGING - * 64em -> lg - */ +// CONTENT - LINKLIST PAGING +// 64em -> lg .linklist-filters { - margin: 5px 0; - color: #252525; - font-size: 0.9em; -} + margin: 5px 0; + color: $dark-grey; + font-size: .9em; -.linklist-filters a { + a { padding: 5px 8px; text-decoration: none; -} + } -.linklist-filters .filter-off { - color: #252525; - background: #f5f5f5; -} + .filter-off { + background: $almost-white; + color: $dark-grey; + } -.linklist-filters .filter-on { - color: #b0ddce; - background: #1b926c; -} + .filter-on { + background: $main-green; + color: $light-green; + } -.linklist-filters .filter-block { - color: #f5f5f5; - background: #ac2925; + .filter-block { + background: $red; + color: $almost-white; + } } .linklist-pages { - margin: 5px 0; - color: #252525; - text-align: center; -} + margin: 5px 0; + text-align: center; + color: $dark-grey; -.linklist-pages a { - color: #252525; + a { text-decoration: none; + color: $dark-grey; + + &:hover { + color: $white; + } + } } -.linklist-pages a:hover { - color: #fff; +%linksperpage-button { + display: inline-block; + width: 20px; + text-align: center; } .linksperpage { - margin: 5px 0; - text-align: right; - color: #252525; - font-size: 0.9em; -} + margin: 5px 0; + text-align: right; + color: $dark-grey; + font-size: .9em; -.linksperpage a { - padding: 5px 5px; - text-decoration: none; - color: #252525; - background: #f5f5f5; -} - -.linksperpage a, .linksperpage input[type="text"] { - display: inline-block; - width: 20px; - text-align: center; -} - -.linksperpage form { + form { display: inline; + } + + a { + @extend %linksperpage-button; + + background: $almost-white; + padding: 5px; + text-decoration: none; + color: $dark-grey; + } + + input { + &[type='text'] { + @extend %linksperpage-button; + + margin: 0; + border: medium none currentColor; + background: $almost-white; + padding: 4px 5px 3px 8px; + height: 20px; + color: $dark-grey; + font-size: .8em; + } + } } -.linksperpage input[type="text"] { - height: 20px; - margin: 0; - padding: 4px 5px 3px 8px; - background: #f5f5f5; - border: medium none currentColor; - color: #252525; - font-size: 0.8em; +// CONTENT - LINKLIST ITEMS +%private-border { + display: block; + position: absolute; + top: 0; + left: 3px; + z-index: 1; + background: $orange; + width: 2px; + height: 96%; + content: ''; } -/** - * CONTENT - LINKLIST ITEMS - */ .linklist-item { - margin: 0 0 10px 0; - background: #f5f5f5; - box-shadow: 1px 1px 3px #797979; + margin: 0 0 10px; + box-shadow: 1px 1px 3px $light-grey; + background: $almost-white; + + &.private { + .linklist-item-title { + &::before { + @extend %private-border; + margin-top: 3px; + } + } + + .linklist-item-description { + &::before { + @extend %private-border; + height: 100%; + } + } + } } .linklist-item-buttons { - background: transparent; - position: relative; - width: 23px; - z-index: 99; + position: relative; + z-index: 99; + background: transparent; + width: 23px; } .linklist-item-buttons-right { - float: right; - margin-right: -25px; + float: right; + margin-right: -25px; } .linklist-item-buttons * { - display: block; - float: left; - width:100%; - margin: auto; - text-align: center; -} - -.linklist-item-title, .linklist-item-title h2 { - margin: 0; - word-wrap: break-word; + display: block; + float: left; + margin: auto; + width: 100%; + text-align: center; } .linklist-item-title { - position: relative; - background: #f5f5f5; -} + position: relative; + margin: 0; + background: $almost-white; + word-wrap: break-word; -.linklist-item-title h2 { - padding: 3px 10px 0 10px; + h2 { + margin: 0; + padding: 3px 10px 0; line-height: 30px; -} + word-wrap: break-word; -.linklist-item-title h2 a { - font-size: 0.7em; - color: #252525; - text-decoration: none; - vertical-align: middle; -} + a { + vertical-align: middle; + text-decoration: none; + color: $dark-grey; + font-size: .7em; -.linklist-item-title .linklist-link { + &:visited { + .linklist-link { + color: $dark-green; + } + } + + &:hover { + color: $dark-grey; + } + } + } + + .linklist-link { + color: $main-green; font-size: 1.1em; - color: #1b926c; -} -.linklist-item-title h2 a:visited .linklist-link { - color: #2a4c41; -} + &:hover { + color: $dark-grey; + } + } -.linklist-item-title h2 a:hover, .linklist-item-title .linklist-link:hover{ - color: #252525; -} - - -.linklist-item-title .label-private { - border: solid 1px #F89406; + .label-private { + border: solid 1px $orange; + color: $orange; font-family: Arial, sans-serif; - font-size: 0.65em; - color: #F89406; + font-size: .65em; + } } .fold-button { - display: none; - color: #252525; + display: none; + color: $dark-grey; } .linklist-item-editbuttons { - float: right; - padding: 8px 5px; -} + float: right; + padding: 8px 5px; -.linklist-item-editbuttons * { + * { display: block; float: left; margin: 0 1px; -} + } -.linklist-item-editbuttons a { + a { font-size: 1em; + } + + .delete-checkbox { + display: none; + } } .edit-link { - font-size: 1.2em; - color: #0b5ea6; + color: $blue; + font-size: 1.2em; } .delete-link { - font-size: 1.3em; - color: #ac2925 !important; + color: $red !important; + font-size: 1.3em; } .linklist-item-description { - position: relative; - padding: 0 10px; - word-wrap: break-word; - color: #252525; - line-height: 1.3em; -} + position: relative; + padding: 0 10px; + line-height: 1.3em; + color: $dark-grey; + word-wrap: break-word; -.linklist-item-description a { + a { text-decoration: none; - color: #1b926c; -} + color: $main-green; -.linklist-item-description a:hover { - color: #252525; -} + &:hover { + color: $dark-grey; + } -.linklist-item-description a:visited { - color: #14553f; + &:visited { + color: $dark-green; + } + } } .linklist-item-thumbnail { - position: relative; - padding: 0 0 0 5px; - margin: 0; - float: right; - z-index: 50; - height: 90px; -} - -.linklist-item.private .linklist-item-title::before, -.linklist-item.private .linklist-item-description::before { - position: absolute; - left: 3px; - top: 0; - display: block; - content:""; - background: #F89406; - height: 96%; - width: 2px; - z-index: 1; -} - -.linklist-item.private .linklist-item-description::before { - height: 100%; -} - -.linklist-item.private .linklist-item-title::before { - margin-top: 3px; + position: relative; + float: right; + z-index: 50; + margin: 0; + padding: 0 0 0 5px; + height: 90px; } .linklist-item-infos { - padding: 4px 8px 4px 8px; - background: #ddd; - color: #252525; -} + background: $background-linklist-info; + padding: 4px 8px; + color: $dark-grey; -.linklist-item-infos a { - color: #252525; + a { text-decoration: none; -} + color: $dark-grey; -.linklist-item-infos a:hover { - color: #000; -} + &:hover { + color: $black; + } + } -.linklist-item-infos .linklist-item-tags { - font-size: 0.8em; -} + .linklist-item-tags { + font-size: .8em; + } -.linklist-item-infos .label-tag { + .label-tag { font-size: 1em; -} + } -.linklist-item-infos-dateblock { - font-size: 0.9em; -} - -.linklist-plugin-icon { - width: 13px; - height: 13px; -} - -.linklist-item-infos-url { + .mobile-buttons { text-align: right; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 0.8em; - height:23px; - line-height:23px; -} + } -.linklist-item-infos .mobile-buttons { - text-align: right; -} - -.linklist-item-infos .linklist-plugin-icon { + .linklist-plugin-icon { display: inline-block; margin: 0 2px; width: 16px; height: 16px; + } +} + +.linklist-item-infos-dateblock { + font-size: .9em; +} + +.linklist-plugin-icon { + width: 13px; + height: 13px; +} + +.linklist-item-infos-url { + height: 23px; + overflow: hidden; + text-align: right; + text-overflow: ellipsis; + line-height: 23px; + white-space: nowrap; + font-size: .8em; } .linklist-item-infos-controls-group { - display: inline-block; - border-right: 1px solid #5d5d5d; - padding-right: 6px; + display: inline-block; + border-right: 1px solid $light-grey; + padding-right: 6px; } .ctrl-edit { - margin: 0 7px; + margin: 0 7px; } -/** 64em -> lg **/ +// 64em -> lg @media screen and (max-width: 64em) { - .linklist-item-infos-url { - text-align: left; - } + .linklist-item-infos-url { + text-align: left; + } } -/** - * Footer - */ -#footer { - margin: 20px 0; - padding: 5px; - text-align: center; - color: #252525; -} +// Footer +.footer-container { + margin: 20px 0; + padding: 5px; + text-align: center; + color: $dark-grey; -#footer:before { + &::before { display: block; - content:""; - background: linear-gradient(to right, #949393, #252525, #949393); - height: 1px; - width: 80%; margin: 10px auto; + background: linear-gradient(to right, $background-color, $dark-grey, $background-color); + width: 80%; + height: 1px; + content: ''; + } + + a { + color: $dark-grey; + } } -#footer a { - color: #252525; +// PAGE FORM +%page-form-input { + margin: 10px 0; + border: solid 1px $form-input-border; + border-radius: 2px; + background: $form-input-background; + padding: 5px 5px 3px 15px; + width: 90%; + height: 35px; + color: $dark-grey; + box-sizing: border-box; +} + +%page-form-button { + display: inline-block; + margin: 15px 5px; + border: 0; + box-shadow: 1px 1px 1px $form-input-border, -1px -1px 6px $form-input-border, -1px 1px 2px $form-input-border, 1px -1px 2px $form-input-border; + background: $main-green; + min-width: 150px; + height: 35px; + vertical-align: center; + text-decoration: none; + line-height: 35px; + color: $almost-white; + font-size: 1.2em; + font-weight: normal; } -/** - * PAGE FORM - */ .page-form { - margin: 20px 0 0 0; - background: #f5f5f5; - box-shadow: 1px 1px 2px #797979; - color: #252525; - overflow: hidden; -} + margin: 20px 0 0; + box-shadow: 1px 1px 2px $light-grey; + background: $almost-white; + overflow: hidden; + color: $dark-grey; -.page-form .window-title { - margin: 0 0 10px 0; + .window-title { + margin: 0 0 10px; + background: $almost-white; padding: 10px 0; width: 100%; - color: #1b926c; - background: #f5f5f5; text-align: center; -} + color: $main-green; + } -.page-form .window-subtitle { + .window-subtitle { text-align: center; -} + } -.page-form a { - color: #1b926c; - font-weight: bold; + a { text-decoration: none; -} + color: $main-green; + font-weight: bold; -.page-form p { - padding: 5px 10px; + &.button { + @extend %page-form-button; + } + } + + p { margin: 0; -} + padding: 5px 10px; + } -.page-form input[type="text"], -.page-form input[type="password"], -.page-form textarea { - box-sizing: border-box; - margin: 10px 0; - padding: 5px 5px 3px 15px; - height: 35px; - width: 90%; - background: #eeeeee; - border: solid 1px #d8d8d8; - border-radius: 2px; - color: #252525; -} + input { + &[type='text'] { + @extend %page-form-input; + + &::-webkit-input-placeholder { + color: $light-grey; + } + } + + &[type='password'] { + @extend %page-form-input; + + &::-webkit-input-placeholder { + color: $light-grey; + } + } + + &[type='submit'] { + @extend %page-form-button; + } + } + + textarea { + @extend %page-form-input; -.page-form textarea { - min-height: 240px; padding: 15px 5px 3px 15px; + min-height: 240px; resize: vertical; overflow-y: auto; - word-wrap:break-word -} + word-wrap: break-word; + } -/* because chrome */ -.page-form input[type="text"]::-webkit-input-placeholder, -.page-form input[type="password"]::-webkit-input-placeholder { - color: #777777; -} + select { + color: $dark-grey; + } -.page-form input[type="submit"], .page-form a.button { - margin: 15px 5px; - height: 35px; - line-height: 35px; - width: 150px; - background: #1b926c; - color: #f5f5f5; - border: none; - box-shadow: 1px 1px 1px #ddd, -1px -1px 6px #ddd, -1px 1px 2px #ddd, 1px -1px 2px #ddd; - font-size: 1.2em; - text-decoration: none; - vertical-align: center; - font-weight: normal; - display: inline-block; -} + .button { + &.button-red { + background: $red; + } + } - -.page-form .button.button-red { - background: #ac2925; -} - -.page-form .submit-buttons { + .submit-buttons { margin-bottom: 10px; + } + + section { + margin: 10px 0 25px; + } + + table, + th, + td { + border-width: 1px 0; + border-style: solid; + border-color: $light-grey; + } + + th, + td { + padding: 5px; + } + + table { + margin: auto; + width: 90%; + + .order { + text-decoration: none; + color: $dark-grey; + } + } + + .awesomplete { + width: 90%; + + input { + width: 100%; + } + } + + div { + .awesomplete { + > ul { + color: $black; + } + } + } } @media screen and (min-width: 64em) { - .page-form .submit-buttons { - position: relative; - } + .page-form { + .submit-buttons { + position: relative; - .page-form .submit-buttons .button.button-red { - position: absolute; - right: 5%; + .button { + &.button-red { + position: absolute; + right: 5%; + } + } } + } } @media screen and (max-width: 64em) { - .page-form .submit-buttons .button { + .page-form { + .submit-buttons { + .button { display: block; margin: auto; + } } + } } -.page-form select { - color: #252525; -} - -/** - * PAGE FORM - LIGHT - */ -.page-form-light div, .page-form-light p { +// PAGE FORM - LIGHT +.page-form-light { + div, + p { text-align: center; + } } -/** - * PAGE FORM - COMPLETE - */ -.page-form-complete div, .page-form-complete p { - color: #252525; +// PAGE FORM - COMPLETE +%page-form-valign { + position: absolute; + top: 50%; + transform: translateY(-50%); } -.page-form-complete .form-label, .page-form-complete .form-input { +.page-form-complete { + div, + p { + color: $dark-grey; + } + + .form-label, + .form-input { position: relative; height: 60px; -} + } -.page-form-complete .form-label label, -.page-form-complete .form-input input, -.page-form-complete .form-input select.align, -.page-form-complete .timezone { - position: absolute; - top: 50%; - transform: translateY(-50%); -} + .form-label { + label { + @extend %page-form-valign; -.page-form-complete .form-label label { - text-align: right; - right: 0; - padding: 0 20px; -} + right: 0; + padding: 0 20px; + text-align: right; + } + } -.page-form-complete .label-name { + .label-name { font-weight: bold; -} + } -.page-form-complete .label-desc { - font-size: 0.8em; -} + .label-desc { + font-size: .8em; + } -.page-form-complete input[type="text"], -.page-form-complete input[type="password"], -.page-form-complete textarea { + .form-input { + input { + @extend %page-form-valign; + + &[type='text'], + &[type='password'] { + margin: 0; + } + } + + select { + &.align { + @extend %page-form-valign; + } + } + } + + textarea { margin: 0; + } + + .timezone { + @extend %page-form-valign; + } } -.page-form section { - margin: 10px 0 25px 0; -} - -.page-form table { - margin: auto; - width: 90%; -} - -.page-form table .order { - text-decoration: none; - color: #252525; -} - -.page-form table, .page-form th, .page-form td { - border-width: 1px 0; - border-style: solid; - border-color: #aaaaaa; -} - -.page-form th, .page-form td { - padding: 5px; - -} - -/* Awesomeplete fix */ -div.awesomplete { +// Awesomeplete fix +div { + &.awesomplete { width: inherit; + + > input { + display: inherit; + } + + > ul { + z-index: 9999; + } + } } -div.awesomplete > input { - display: inherit; -} - -div.awesomplete > ul { - z-index: 9999; -} - -.page-form .awesomplete { - width: 90%; -} - -.page-form .awesomplete input { - width: 100%; -} - -.page-form div.awesomplete > ul { - color: black; -} - -form[name="linkform"].page-form { - overflow: visible; +form { + &[name='linkform'] { + &.page-form { + overflow: visible; + } + } } @media screen and (max-width: 64em) { - .page-form-complete .form-label { - height: inherit; - } + %page-form-valign-mobile { + position: inherit; + top: inherit; + transform: translateY(0); + } - .page-form-complete .form-label label, - .page-form-complete .form-input input, - .page-form-complete .timezone { - position: inherit; - top: inherit; - transform: translateY(0); - } + .page-form-complete { + .form-label { + height: inherit; - .page-form-complete .form-input input[type="checkbox"] { - position: absolute; - top: 50%; - right: 50%; - transform: translateY(-50%); - } + label { + @extend %page-form-valign-mobile; - .page-form-complete .form-input { - text-align: center; - } - - .page-form-complete .form-label label { display: block; + margin: 10px 0 0; text-align: left; - margin: 10px 0 0 0; + } } - .timezone-continent:after { - content:"\a\a"; - white-space: pre; + .form-input { + text-align: center; + + input { + @extend %page-form-valign-mobile; + + &[type='checkbox'] { + position: absolute; + top: 50%; + right: 50%; + transform: translateY(-50%); + } + } } - .page-form-complete .radio-buttons { - text-align: left; - padding: 5px 15px; + .timezone { + @extend %page-form-valign-mobile; } + + .radio-buttons { + padding: 5px 15px; + text-align: left; + } + } + + .timezone-continent { + &::after { + white-space: pre; + content: '\a\a'; + } + } } -/** - * Page visitor (page form extended) - */ +// Page visitor (page form extended) .page-visitor { - color: #252525; + color: $dark-grey; } -#page404 { - color: #3f3f3f; +.page404-container { + color: $dark-grey; } -/** - * EDIT LINK - */ -#editlinkform .created-date { - color: #767676; +// EDIT LINK +.edit-link-container { + .created-date { margin-bottom: 10px; + color: $light-grey; + } } -/** - * LOGIN - */ -#login-form .remember-me { +// LOGIN +.login-form-container { + .remember-me { margin: 5px 0; + } } -/** - * Search results - */ -.search-result a { - color: white; +// Search results +.search-result { + a { text-decoration: none; + color: $white; + } + + .label-tag { + border-color: $white; + + .remove { + margin: 0 0 0 5px; + border-left: $white 1px solid; + padding: 0 0 0 5px; + } + } + + .label-private { + border: 1px solid $white; + } } -.search-result .label-tag { - border-color: white; -} - -.search-result .label-tag .remove { - border-left: white 1px solid; - padding: 0 0 0 5px; - margin: 0 0 0 5px; -} - -.search-result .label-private { - border: 1px solid white; -} - -/** - * TOOLS - */ +// TOOLS .tools-item { - margin: 10px 0; + margin: 10px 0; + + .pure-button { + &:hover { + background-color: $main-green; + background-image: none; + color: $almost-white; + } + } } -.tools-item .pure-button:hover { - background-image: none; - background-color: #1b926c; - color: #f5f5f5; -} +// PLUGIN ADMIN +.pluginform-container { + .mobile-row { + font-size: .9em; + } -/** - * PLUGIN ADMIN - */ -#pluginform .mobile-row { - font-size: 0.9em; -} - -#pluginform .more { + .more { margin-top: 10px; + } } @media screen and (max-width: 64em) { - #pluginform .main-row, #pluginform .main-row td { - border-bottom-style: none; - } + .pluginform-container { + .main-row { + border-top-style: none; + border-bottom-style: none; - #pluginform .mobile-row, #pluginform .mobile-row td { + td { border-top-style: none; + border-bottom-style: none; + } } + } } -/** - * IMPORT - */ -#import-field { - margin: 15px 0; +// IMPORT +.import-field-container { + margin: 15px 0; } -/** - * TAG CLOUD - */ -#cloudtag { - padding: 10px; - text-align: center; -} +// TAG CLOUD +.cloudtag-container { + padding: 10px; + text-align: center; + text-decoration: none; + color: $dark-grey; -#cloudtag, #cloudtag a { - color: #252525; + a { text-decoration: none; + color: $dark-grey; + } + + .count { + color: $light-grey; + } } -#cloudtag .count { - color: #7f7f7f; -} +// TAG LIST +.taglist-container { + padding: 0 10px; -/** - * TAG LIST - */ -#taglist { - padding: 0 10px; -} - -#taglist a { - color: #252525; + a { text-decoration: none; -} + color: $dark-grey; + } -#taglist .count { + .count { display: inline-block; width: 35px; text-align: right; - color: #7f7f7f; -} + color: $light-grey; + } -#taglist .rename-tag-form { + .rename-tag-form { display: none; -} + } -#taglist .delete-tag { - color: #ac2925; + .delete-tag { display: none; + color: $red; + } + + .rename-tag { + color: $blue; + } + + .validate-rename-tag { + color: $main-green; + } } -#taglist .rename-tag { - color: #0b5ea6; +// Picture wall CSS +.picwall-container { + clear: both; + margin: 0 10px 10px; + background-color: $almost-white; + color: $dark-grey; } -#taglist .validate-rename-tag { - color: #1b926c; -} +.picwall-pictureframe { + display: table-cell; + position: relative; + float: left; + z-index: 5; + margin: 2px; + background-color: $almost-white; + width: 90px; + height: 90px; + overflow: hidden; + vertical-align: middle; + text-align: center; -/** - * Picture wall CSS - */ -#picwall_container { - margin: 0 10px 10px 10px; - color: #252525; - background-color: #f5f5f5; - clear: both; -} - -.picwall_pictureframe { - margin: 2px; - background-color: #f5f5f5; - z-index: 5; - position: relative; - display: table-cell; - vertical-align: middle; - width: 90px; - height: 90px; - overflow: hidden; - text-align: center; - float: left; -} - -.b-lazy { - -webkit-transition: opacity 500ms ease-in-out; - -moz-transition: opacity 500ms ease-in-out; - -o-transition: opacity 500ms ease-in-out; - transition: opacity 500ms ease-in-out; - opacity: 0; -} -.b-lazy.b-loaded { - opacity: 1; -} - -.picwall_pictureframe img { + // Adapt the width of the image + img { max-width: 100%; height: auto; color: transparent; -} /* Adapt the width of the image */ + } -.picwall_pictureframe a { + a { text-decoration: none; + } + + span { + &.info { + display: none; + font-family: Arial, sans-serif; + } + } + + // CSS to show title when hovering an image - no javascript required. + &:hover { + span { + &.info { + display: block; + position: absolute; + top: 0; + left: 0; + background-color: $dark-shadow; + width: 90px; + height: 90px; + text-align: left; + color: $almost-white; + font-size: 9pt; + font-weight: bold; + } + } + } } -/* CSS to show title when hovering an image - no javascript required. */ -.picwall_pictureframe span.info { - display: none; - font-family: Arial, sans-serif; +.b-lazy { + transition: opacity 500ms ease-in-out; + opacity: 0; + -webkit-transition: opacity 500ms ease-in-out; + -moz-transition: opacity 500ms ease-in-out; + -o-transition: opacity 500ms ease-in-out; + + &.b-loaded { + opacity: 1; + } } -.picwall_pictureframe:hover span.info { - display: block; - position: absolute; - top: 0; - left: 0; - width: 90px; - height: 90px; - font-weight: bold; - font-size: 9pt; - color: #f5f5f5; - text-align: left; - background-color: rgba(0, 0, 0, 0.8); -} - -/** - * DAILY - */ +// DAILY .daily-desc { - color: #7f7f7f; - font-size: 0.8em; -} + color: $light-grey; + font-size: .8em; -.daily-about a { - color: #343434; + a { text-decoration: none; + color: $dark-grey; + + &:hover { + color: $light-grey; + } + } } -.daily-about a:hover { - color: #7f7f7f; -} - -.daily-about h3:before, .daily-about h3:after { - display: block; - content:""; - background: linear-gradient(to right, #d5d4d4, #252525, #d5d4d4); - height: 1px; - width: 90%; - margin: 10px auto; +.daily-about { + h3 { + &::before, + &::after { + display: block; + margin: 10px auto; + background: linear-gradient(to right, $background-color, $dark-grey, $background-color); + width: 90%; + height: 1px; + content: ''; + } + } } .daily-entry { - padding: 0 10px; -} + padding: 0 10px; -.daily-entry .daily-entry-title:after { - display: block; - content:""; - background: linear-gradient(to right, #fff, #515151, #fff); - height: 1px; - width: 70%; - margin: 5px auto; -} + .daily-entry-title { + margin: 10px 0 0; -.daily-entry .daily-entry-title { - margin: 10px 0 0 0; -} + a { + text-decoration: none; + color: $black; + } -.daily-entry .daily-entry-title a { - color: #000; - text-decoration: none; -} + &::after { + display: block; + margin: 5px auto; + background: linear-gradient(to right, $white, $light-grey, $white); + width: 70%; + height: 1px; + content: ''; + } + } -.daily-entry .daily-entry-description { - padding: 5px 5px 0 5px; - font-size: 0.9em; + .daily-entry-description { + padding: 5px 5px 0; text-align: justify; + font-size: .9em; word-wrap: break-word; -} + } -.daily-entry .daily-entry-tags { - padding: 0 5px 5px 5px; - font-size: 0.8em; + .daily-entry-tags { + padding: 0 5px 5px; + font-size: .8em; + } } .daily-entry-thumbnail { - float: left; - margin: 15px 5px 5px 15px; + float: left; + margin: 15px 5px 5px 15px; } -.daily-entry-description a { +.daily-entry-description { + a { text-decoration: none; - color: #1b926c; + color: $main-green; + + &:hover { + text-shadow: 1px 1px $background-linklist-info; + } + + &:visited { + color: $dark-green; + } + } } -.daily-entry-description a:hover { - text-shadow: 1px 1px #ddd; -} - -.daily-entry-description a:visited { - color: #20b988; -} - -/* - * Fix empty bookmarklet name in Firefox - */ +// Fix empty bookmarklet name in Firefox .pure-button { - -moz-user-select: auto; + -moz-user-select: auto; } .tag-sort { - margin-top: 30px; - text-align: center; -} + margin-top: 30px; + text-align: center; -.tag-sort a { + a { display: inline-block; margin: 0 15px; - color: white; text-decoration: none; + color: $white; font-weight: bold; + } } -/** - * Markdown - */ -.markdown p { +// Markdown +.markdown { + p { margin: 0 !important; + } + + p + p { + margin: .5em 0 0 !important; + } + + * { + &:first-child { + margin-top: 0 !important; + } + + &:last-child { + margin-bottom: 5px !important; + } + } } -.markdown p + p { - margin: 0.5em 0 0 0 !important; -} - -.markdown *:first-child { - margin-top: 0 !important; -} - -.markdown *:last-child { - margin-bottom: 5px !important; -} - -/** - * Pure Button - */ +// Pure Button .pure-button-success, .pure-button-error, .pure-button-warning, .pure-button-primary, .pure-button-shaarli, .pure-button-secondary { - color: white !important; - border-radius: 4px; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); + border-radius: 4px; + text-shadow: 0 1px 1px $dark-shadow; + color: $white !important; } .pure-button-shaarli { - background-color: #1B926C; + background-color: $main-green; } diff --git a/composer.json b/composer.json index 15e082f..0d4c623 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "Shaarli\\Api\\Controllers\\": "application/api/controllers", "Shaarli\\Api\\Exceptions\\": "application/api/exceptions", "Shaarli\\Config\\": "application/config/", - "Shaarli\\Config\\Exception\\": "application/config/exception" + "Shaarli\\Config\\Exception\\": "application/config/exception", + "Shaarli\\Security\\": "application/security" } } } diff --git a/doc/md/Bookmarklet.md b/doc/md/Bookmarklet.md deleted file mode 100644 index 6c7f1c6..0000000 --- a/doc/md/Bookmarklet.md +++ /dev/null @@ -1,29 +0,0 @@ -## Add the sharing button (_bookmarklet_) to your browser - -- Open your Shaarli and `Login` -- Click the `Tools` button in the top bar -- Drag the **`✚Shaare link` button**, and drop it to your browser's bookmarks bar. - -_This bookmarklet button is compatible with Firefox, Opera, Chrome and Safari. Under Opera, you can't drag'n drop the button: You have to right-click on it and add a bookmark to your personal toolbar._ - -![](images/bookmarklet.png) - -## Share links using the _bookmarklet_ - -- When you are visiting a webpage you would like to share with Shaarli, click the _bookmarklet_ you just added. -- A window opens. - - You can freely edit title, description, tags... to find it later using the text search or tag filtering. - - You will be able to edit this link later using the ![](images/edit_icon.png) edit button. - - You can also check the “Private” box so that the link is saved but only visible to you. -- Click `Save`.**Voilà! Your link is now shared.** - -## Troubleshooting: The bookmarklet doesn't work with a few websites (e.g. Github.com) - -Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunatly, there is nothing Shaarli can do about it. - -See [#196](https://github.com/shaarli/Shaarli/issues/196). - -There is an open bug for both Firefox and Chromium: - -- https://bugzilla.mozilla.org/show_bug.cgi?id=866522 -- https://code.google.com/p/chromium/issues/detail?id=233903 diff --git a/doc/md/Community-&-Related-software.md b/doc/md/Community-&-Related-software.md index 207153b..7c570ac 100644 --- a/doc/md/Community-&-Related-software.md +++ b/doc/md/Community-&-Related-software.md @@ -38,9 +38,11 @@ See [Theming](Theming) for a list of community-contributed themes, and an instal - [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension. - [Shaarli for Android](http://sebsauvage.net/links/?ZAyDzg) - Android application that adds Shaarli as a sharing provider - [Shaarlier for Android](https://github.com/dimtion/Shaarlier) - Android application to simply add links directly into your Shaarli +- [Stakali for Android](https://stakali.toneiv.eu) - Stakali is a personal bookmark manager which synchronizes with Shaarli ### Browser addons - * [Shaarli Web Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli. +- [Shaarli Firefox Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli. +- [Shaarli Chrome Extension](https://github.com/octplane/Shiny-Shaarli) - toolbar button to share your current tab with Shaarli. ### Server apps - [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content diff --git a/doc/md/Firefox-share.md b/doc/md/Firefox-share.md deleted file mode 100644 index 9a46b18..0000000 --- a/doc/md/Firefox-share.md +++ /dev/null @@ -1,20 +0,0 @@ -| Note | Firefox Share is no longer available for Firefox 57 and later versions. | -|---------|---------| - -### Add Shaarli as a sharing service to Firefox - -- Open your Shaarli and `Login` -- Click the `Tools` button in the top bar -- Click the `✚Add to Firefox social` button and accept the activation. - - -### Sharing links using Firefox share - -- Add the sharing service as described above -- When you are visiting a webpage you would like to share with Shaarli, - click the Firefox _Share_ button [images/firefoxshare.png](images/firefoxshare.png) -- You can edit your link before and after saving, just like the bookmarklet above. - -_Your Shaarli instance must be hosted on an HTTPS (SSL/TLS secure connection) -enabled server for Firefox Share to work. Firefox Share will not work over -plain HTTP connections._ diff --git a/doc/md/Sharing-content.md b/doc/md/Sharing-content.md new file mode 100644 index 0000000..faacc1f --- /dev/null +++ b/doc/md/Sharing-content.md @@ -0,0 +1,88 @@ +Content posted to Shaarli is separated in items called _Shaares_. For each Shaare, +you can customize the following aspects: + + * URL to link to + * Title + * Free-text description + * Tags + * Public/private status + +-------------------------------------------------------------------------------- + +## Adding new Shaares + +While logged in to your Shaarli, you can add new Shaares in several ways: + + * [+Shaare button] + * [Bookmarklet] + * [Firefox Share](#firefox-share) + * Third-party [apps and browser addons](Community-\&-Related-software.md#mobile-apps) + * [REST API](https://shaarli.github.io/api-documentation/) + +### +Shaare button + + * While logged in to your Shaarli, click the **`+Shaare`** button located in the toolbar. + * Enter the URL of a link you want to share. + * Click `Add link` + * The `New Shaare` dialog appears, allowing you to fill in the details of your Shaare. + * The Description, Title, and Tags will help you find your Shaare later using tags or full-text search. + * You can also check the “Private” box so that the link is saved but only visible to you (the logged-in user). + * Click `Save`. + + + +### Bookmarklet + +The _Bookmarklet_ \[[1](https://en.wikipedia.org/wiki/Bookmarklet)\] is a special +browser bookmark you can use to add new content to your Shaarli. This bookmarklet is +compatible with Firefox, Opera, Chrome and Safari. To set it up: + + * Access the `Tools` page from the button in the toolbar. + * Drag the **`✚Shaare link` button** to your browser's bookmarks bar. + +Once this is done, you can shaare any URL you are visiting simply by clicking the +bookmarklet in your browser! The same `New Shaare` dialog as above is displayed. + +| Note | Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunately, there is nothing Shaarli can do about it. \[[1](https://github.com/shaarli/Shaarli/issues/196)]\ \[[2](https://bugzilla.mozilla.org/show_bug.cgi?id=866522)]\ \[[3](https://code.google.com/p/chromium/issues/detail?id=233903)]\ | +|---------|---------| + +| Note | Under Opera, you can't drag'n drop the button: You have to right-click on it and add a bookmark to your personal toolbar. | +|---------|---------| + +![](images/bookmarklet.png) + + +### Firefox Share + +Before using Firefox Share, you must first add Shaarli as a sharing provider: + +- Click the `Tools` button in the top bar +- Click the `✚Add to Firefox social` button and accept the activation. + +Once this is done, you can share any URL you are visiting by clicking the Firefox +_Share_ button [images/firefoxshare.png](images/firefoxshare.png) + +| Note | Firefox Share is no longer available for Firefox 57 and later versions. | +|---------|---------| + +| Note | Your Shaarli instance must be hosted on an HTTPS (SSL/TLS secure connection) enabled server for Firefox Share to work. Firefox Share will not work over plaintext HTTP connections. | +|---------|---------| + +-------------------------------------------------------------------------------- + +## Editing Shaares + +Any Shaare can edited by clicking its ![](images/edit_icon.png) `Edit` button. + +Editing a Shaare will not change it's permalink, each permalink always points to the +latest revision of a Shaare. + +-------------------------------------------------------------------------------- + +## Using shaarli as a blog, notepad, pastebin... + +While adding or editing a link, leave the URL field blank to create a text-only +("note") post. This allows you to post any kind of text content, such as blog +articles, private or public notes, snippets... There is no character limit! You can +access your Shaare from its permalink. + diff --git a/doc/md/images/doc-logo.png b/doc/md/images/doc-logo.png index 3d8d178..3da7ba5 100644 Binary files a/doc/md/images/doc-logo.png and b/doc/md/images/doc-logo.png differ diff --git a/doc/md/images/firefoxshare.png b/doc/md/images/firefoxshare.png index 98c2fdd..8f8fdba 100644 Binary files a/doc/md/images/firefoxshare.png and b/doc/md/images/firefoxshare.png differ diff --git a/doc/md/images/install-shaarli.png b/doc/md/images/install-shaarli.png index 7ae3381..d5d5baa 100644 Binary files a/doc/md/images/install-shaarli.png and b/doc/md/images/install-shaarli.png differ diff --git a/doc/md/images/rss-filter-1.png b/doc/md/images/rss-filter-1.png index d2a03f6..0cf1591 100644 Binary files a/doc/md/images/rss-filter-1.png and b/doc/md/images/rss-filter-1.png differ diff --git a/doc/md/images/rss-filter-2.png b/doc/md/images/rss-filter-2.png index 538b126..5a40755 100644 Binary files a/doc/md/images/rss-filter-2.png and b/doc/md/images/rss-filter-2.png differ diff --git a/doc/md/index.md b/doc/md/index.md index e77b4d3..224070c 100644 --- a/doc/md/index.md +++ b/doc/md/index.md @@ -94,13 +94,6 @@ Easily extensible by any client using the REST API exposed by Shaarli. See the [API documentation](http://shaarli.github.io/api-documentation/). -### Using Shaarli as a blog, notepad, pastebin... -- Go to your Shaarli setup and log in -- Click the `Add Link` button -- To share text only, do not enter any URL in the corresponding input field and click `Add Link` -- Pick a title and enter your article, or note, in the description field; add a few tags; optionally check `Private` then click `Save` -- Voilà! Your article is now published (privately if you selected that option) and accessible using its permalink. - ## About ### Shaarli community fork This friendly fork is maintained by the Shaarli community at https://github.com/shaarli/Shaarli diff --git a/inc/languages/de/LC_MESSAGES/shaarli.po b/inc/languages/de/LC_MESSAGES/shaarli.po new file mode 100644 index 0000000..34d29ce --- /dev/null +++ b/inc/languages/de/LC_MESSAGES/shaarli.po @@ -0,0 +1,1313 @@ +msgid "" +msgstr "" +"Project-Id-Version: Shaarli\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-31 09:09+0200\n" +"PO-Revision-Date: 2018-03-31 09:12+0200\n" +"Last-Translator: \n" +"Language-Team: Shaarli\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.6\n" +"X-Poedit-Basepath: ../../../..\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: t:1,2;t\n" +"X-Poedit-SearchPath-0: .\n" +"X-Poedit-SearchPathExcluded-0: node_modules\n" +"X-Poedit-SearchPathExcluded-1: vendor\n" + +#: application/ApplicationUtils.php:153 +#, 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 "" +"Deine PHP-Version ist veraltet! Shaarli benötigt mindestens PHP %s, und kann " +"daher nicht laufen. Deine PHP-Version hat bekannte Sicherheitslücken und " +"sollte so bald wie möglich aktualisiert werden." + +#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195 +msgid "directory is not readable" +msgstr "Verzeichnis ist nicht lesbar" + +#: application/ApplicationUtils.php:198 +msgid "directory is not writable" +msgstr "Verzeichnis ist nicht beschreibbar" + +#: application/ApplicationUtils.php:216 +msgid "file is not readable" +msgstr "Datei ist nicht lesbar" + +#: application/ApplicationUtils.php:219 +msgid "file is not writable" +msgstr "Datei ist nicht beschreibbar" + +#: application/Cache.php:16 +#, php-format +msgid "Cannot purge %s: no directory" +msgstr "Kann nicht löschen, %s ist kein Verzeichnis" + +#: application/FeedBuilder.php:151 +msgid "Direct link" +msgstr "Direct Link" + +#: application/FeedBuilder.php:153 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178 +msgid "Permalink" +msgstr "Permalink" + +#: application/History.php:174 +msgid "History file isn't readable or writable" +msgstr "Protokolldatei nicht lesbar oder beschreibbar" + +#: application/History.php:185 +msgid "Could not parse history file" +msgstr "Protokolldatei konnte nicht analysiert werden" + +#: application/Languages.php:177 +msgid "Automatic" +msgstr "Automatisch" + +#: application/Languages.php:178 +msgid "English" +msgstr "Englisch" + +#: application/Languages.php:179 +msgid "French" +msgstr "Französisch" + +#: application/Languages.php:180 +msgid "German" +msgstr "Deutsch" + +#: application/LinkDB.php:136 +msgid "You are not authorized to add a link." +msgstr "Du bist nicht berechtigt einen Link hinzuzufügen." + +#: application/LinkDB.php:139 +msgid "Internal Error: A link should always have an id and URL." +msgstr "Interner Fehler: Ein Link sollte immer eine ID und URL haben." + +#: application/LinkDB.php:142 +msgid "You must specify an integer as a key." +msgstr "Du musst eine Ganzzahl als Schlüssel angeben." + +#: application/LinkDB.php:145 +msgid "Array offset and link ID must be equal." +msgstr "Array-Offset und Link-ID müssen gleich sein." + +#: application/LinkDB.php:251 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "" +"The personal, minimalist, super-fast, database free, bookmarking service" +msgstr "" +"Der persönliche, minimalistische, superschnelle, datenbankfreie " +"Lesezeichenservice" + +#: application/LinkDB.php:253 +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 "" +"Willkommen bei Shaarli! Dies ist dein erstes öffentliches Lesezeichen. Um " +"mich zu bearbeiten oder zu löschen, musst du dich zuerst einloggen.\n" +"\n" +"Um zu erfahren, wie man Shaarli benutzt, öffne den Link \"Dokumentation\" am " +"Ende dieser Seite.\n" +"\n" +"Du verwendest die von der Community unterstützte Version des ursprünglichen " +"Shaarli-Projekts von Sebastien Sauvage." + +#: application/LinkDB.php:267 +msgid "My secret stuff... - Pastebin.com" +msgstr "Meine geheimen Sachen... - Pastebin.com" + +#: application/LinkDB.php:269 +msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." +msgstr "" +"Pssst Ich bin ein privater Link, den nur du sehen kannst. Du kannst mich " +"auch löschen." + +#: application/LinkFilter.php:452 +msgid "The link you are trying to reach does not exist or has been deleted." +msgstr "" +"Den Link, den du versucht zu erreichen, existiert nicht oder wurde gelöscht." + +#: application/NetscapeBookmarkUtils.php:35 +msgid "Invalid export selection:" +msgstr "Ungültige Exportauswahl:" + +#: application/NetscapeBookmarkUtils.php:81 +#, php-format +msgid "File %s (%d bytes) " +msgstr "Datei %s (%d bytes) " + +#: application/NetscapeBookmarkUtils.php:83 +msgid "has an unknown file format. Nothing was imported." +msgstr "hat ein unbekanntes Dateiformat. Es wurde nichts importiert." + +#: application/NetscapeBookmarkUtils.php:86 +#, php-format +msgid "" +"was successfully processed in %d seconds: %d links imported, %d links " +"overwritten, %d links skipped." +msgstr "" +"wurde erfolgreich in %d Sekunden verarbeitet: %d Links importiert, %d Links " +"überschrieben, %d Links übersprungen." + +#: application/PageBuilder.php:168 +msgid "The page you are trying to reach does not exist or has been deleted." +msgstr "" +"Die Seite, die du erreichen möchtest, existiert nicht oder wurde gelöscht." + +#: application/PageBuilder.php:170 +msgid "404 Not Found" +msgstr "404 Nicht gefunden" + +#: application/PluginManager.php:243 +#, php-format +msgid "Plugin \"%s\" files not found." +msgstr "Plugin \"%s\" Dateien nicht gefunden." + +#: application/Updater.php:76 +msgid "Couldn't retrieve Updater class methods." +msgstr "Die Updater-Klassenmethoden konnten nicht abgerufen werden." + +#: application/Updater.php:532 +msgid "An error occurred while running the update " +msgstr "Beim Ausführen des Updates ist ein Fehler aufgetreten " + +#: application/Updater.php:572 +msgid "Updates file path is not set, can't write updates." +msgstr "" +"Der Update-Dateipfad ist nicht festgelegt, es können keine Updates " +"geschrieben werden." + +#: application/Updater.php:577 +msgid "Unable to write updates in " +msgstr "Es ist nicht möglich Updates zu schreiben in " + +#: application/Utils.php:376 tests/UtilsTest.php:340 +msgid "Setting not set" +msgstr "Einstellung nicht gesetzt" + +#: application/Utils.php:383 tests/UtilsTest.php:338 tests/UtilsTest.php:339 +msgid "Unlimited" +msgstr "Unbegrenzt" + +#: application/Utils.php:386 tests/UtilsTest.php:335 tests/UtilsTest.php:336 +#: tests/UtilsTest.php:350 +msgid "B" +msgstr "B" + +#: application/Utils.php:386 tests/UtilsTest.php:329 tests/UtilsTest.php:330 +#: tests/UtilsTest.php:337 +msgid "kiB" +msgstr "kiB" + +#: application/Utils.php:386 tests/UtilsTest.php:331 tests/UtilsTest.php:332 +#: tests/UtilsTest.php:348 tests/UtilsTest.php:349 +msgid "MiB" +msgstr "MiB" + +#: application/Utils.php:386 tests/UtilsTest.php:333 tests/UtilsTest.php:334 +msgid "GiB" +msgstr "GiB" + +#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121 +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 konnte die Konfigurationsdatei nicht erstellen. Bitte stelle sicher, " +"dass Shaarli das Recht hat, in den Ordner zu schreiben, in dem es " +"installiert ist." + +#: application/config/ConfigManager.php:135 +msgid "Invalid setting key parameter. String expected, got: " +msgstr "" +"Ungültiger Parameter für den Einstellungsschlüssel. Zeichenfolge erwartet, " +"erhalten: " + +#: application/config/exception/MissingFieldConfigException.php:21 +#, php-format +msgid "Configuration value is required for %s" +msgstr "Konfigurationswert erforderlich für %s" + +#: application/config/exception/PluginConfigOrderException.php:15 +msgid "An error occurred while trying to save plugins loading order." +msgstr "" +"Beim Versuch, die Ladereihenfolge der Plugins zu speichern, ist ein Fehler " +"aufgetreten." + +#: application/config/exception/UnauthorizedConfigException.php:16 +msgid "You are not authorized to alter config." +msgstr "Du bist nicht berechtigt, die Konfiguration zu ändern." + +#: application/exceptions/IOException.php:19 +msgid "Error accessing" +msgstr "Fehler beim Zugriff" + +#: index.php:142 +msgid "Shared links on " +msgstr "Geteilte Links auf " + +#: index.php:164 +msgid "Insufficient permissions:" +msgstr "Unzureichende Berechtigungen:" + +#: index.php:303 +msgid "I said: NO. You are banned for the moment. Go away." +msgstr "Ich sagte NEIN. Du bist für den Moment gesperrt. Verschwinde." + +#: index.php:368 +msgid "Wrong login/password." +msgstr "Falscher Loging/Passwort." + +#: index.php:576 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42 +msgid "Daily" +msgstr "Täglich" + +#: index.php:681 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95 +msgid "Login" +msgstr "Einloggen" + +#: index.php:722 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39 +msgid "Picture wall" +msgstr "Bildwand" + +#: index.php:770 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag cloud" +msgstr "Tag Cloud" + +#: index.php:803 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag list" +msgstr "Tag Liste" + +#: index.php:1028 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 +msgid "Tools" +msgstr "Tools" + +#: index.php:1037 +msgid "You are not supposed to change a password on an Open Shaarli." +msgstr "Du darfst kein Passwort für ein offenes Shaarli ändern." + +#: index.php:1042 index.php:1084 index.php:1160 index.php:1191 index.php:1291 +msgid "Wrong token." +msgstr "Falsches Zeichen." + +#: index.php:1047 +msgid "The old password is not correct." +msgstr "Das alte Passwort ist nicht korrekt." + +#: index.php:1067 +msgid "Your password has been changed" +msgstr "Dein Passwort wurde geändert" + +#: index.php:1072 +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Change password" +msgstr "Passwort ändern" + +#: index.php:1120 +msgid "Configuration was saved." +msgstr "Konfiguration wurde gespeichert." + +#: index.php:1143 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "Configure" +msgstr "Konfigurieren" + +#: index.php:1154 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Manage tags" +msgstr "Tags verwalten" + +#: index.php:1172 +#, php-format +msgid "The tag was removed from %d link." +msgid_plural "The tag was removed from %d links." +msgstr[0] "Der Tag wurde aus dem Link %d entfernt." +msgstr[1] "Der Tag wurde aus den Links %d entfernt." + +#: index.php:1173 +#, php-format +msgid "The tag was renamed in %d link." +msgid_plural "The tag was renamed in %d links." +msgstr[0] "Der Tag wurde im Link %d umbenannt." +msgstr[1] "Der Tag wurde in den Links %d umbenannt." + +#: index.php:1181 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +msgid "Shaare a new link" +msgstr "Teile einen neuen Link" + +#: index.php:1351 tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 +msgid "Edit" +msgstr "Bearbeiten" + +#: index.php:1351 index.php:1421 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26 +msgid "Shaare" +msgstr "Teilen" + +#: index.php:1390 +msgid "Note: " +msgstr "Notiz: " + +#: index.php:1430 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 +msgid "Export" +msgstr "Exportieren" + +#: index.php:1492 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "Import" +msgstr "Importieren" + +#: index.php:1502 +#, 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 "" +"Die Datei, die du hochladen möchtest, ist wahrscheinlich größer als das, was " +"dieser Webserver akzeptieren kann (%s). Bitte lade in kleineren Blöcken hoch." + +#: index.php:1541 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Plugin administration" +msgstr "Plugin Adminstration" + +#: index.php:1706 +msgid "Search: " +msgstr "Suche: " + +#: index.php:1933 +#, 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 "" +"
Sessions scheinen auf deinem Server nicht korrekt zu funktionieren. "
+"
Stelle sicher, dass die Variable \"session.save_path\" in deiner PHP-" +"Konfiguration richtig eingestellt ist und dass du Schreibzugriff darauf hast." +"
Es verweist aktuell auf %s.
Bei einigen Browsern führt der Zugriff " +"auf deinen Server über einen Hostnamen wie \"localhost\" oder einen " +"beliebigen benutzerdefinierten Hostnamen ohne Punkt dazu, dass der Cookie-" +"Speicher fehlschlägt. Wir empfehlen den Zugriff auf deinen Server über die " +"IP-Adresse oder den Fully Qualified Domain Namen.
" + +#: index.php:1943 +msgid "Click to try again." +msgstr "Klicke um es erneut zu versuchen." + +#: plugins/addlink_toolbar/addlink_toolbar.php:29 +msgid "URI" +msgstr "URI" + +#: plugins/addlink_toolbar/addlink_toolbar.php:33 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Add link" +msgstr "Link hinzufügen" + +#: plugins/addlink_toolbar/addlink_toolbar.php:50 +msgid "Adds the addlink input on the linklist page." +msgstr "Fügt die Link-hinzufügen-Eingabe auf der Linkliste hinzu." + +#: plugins/archiveorg/archiveorg.php:23 +msgid "View on archive.org" +msgstr "Auf archive.org ansehen" + +#: plugins/archiveorg/archiveorg.php:36 +msgid "For each link, add an Archive.org icon." +msgstr "Füge für jeden Link ein Archive.org Symbol hinzu." + +#: plugins/demo_plugin/demo_plugin.php:465 +msgid "" +"A demo plugin covering all use cases for template designers and plugin " +"developers." +msgstr "" +"Ein Demo-Plugin, das alle Anwendungsfälle für Template-Designer und Plugin-" +"Entwickler abdeckt." + +#: plugins/isso/isso.php:20 +msgid "" +"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " +"administration page." +msgstr "" +"Isso Plugin Fehler: Bitte definiere die Einstellung \"ISSO_SERVER\" auf der " +"Plugin-Administrationsseite." + +#: plugins/isso/isso.php:63 +msgid "Let visitor comment your shaares on permalinks with Isso." +msgstr "" +"Lassen Sie Besucher ihre geteilten Links auf Permalinks mit Isso " +"kommentieren." + +#: plugins/isso/isso.php:64 +msgid "Isso server URL (without 'http://')" +msgstr "Isso Server URL (ohne 'http://')" + +#: plugins/markdown/markdown.php:158 +msgid "Description will be rendered with" +msgstr "Die Beschreibung wird dargestellt mit" + +#: plugins/markdown/markdown.php:159 +msgid "Markdown syntax documentation" +msgstr "Markdown Syntax Dokumentation" + +#: plugins/markdown/markdown.php:160 +msgid "Markdown syntax" +msgstr "Markdown Syntax" + +#: plugins/markdown/markdown.php:339 +msgid "" +"Render shaare description with Markdown syntax.
Warning:\n" +"If your shaared descriptions contained HTML tags before enabling the " +"markdown plugin,\n" +"enabling it might break your page.\n" +"See the README." +msgstr "" +"Übertrage Teilen Beschreibung mit Markdown-Syntax.
Warnung:\n" +"Wenn deine Teilen Beschreibungen HTML-Tags enthielten, bevor das Markdown-" +"Plugin aktiviert wurde,\n" +"kann es deine Seite beschädigen, solltest du es aktivieren.\n" +"Weitere Informationen findest du in der README." + +#: plugins/piwik/piwik.php:21 +msgid "" +"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " +"administration page." +msgstr "" +"Piwik-Plugin-Fehler: Bitte definiere die PIWIK_URL und PIWIK_SITEID auf der " +"Plugin-Administrationsseite." + +#: plugins/piwik/piwik.php:70 +msgid "A plugin that adds Piwik tracking code to Shaarli pages." +msgstr "" +"Ein Plugin, das einen Piwik-Tracking-Code auf Shaarli-Seiten hinzufügt." + +#: plugins/piwik/piwik.php:71 +msgid "Piwik URL" +msgstr "Piwik URL" + +#: plugins/piwik/piwik.php:72 +msgid "Piwik site ID" +msgstr "Piwik site ID" + +#: plugins/playvideos/playvideos.php:22 +msgid "Video player" +msgstr "Videoplayer" + +#: plugins/playvideos/playvideos.php:25 +msgid "Play Videos" +msgstr "Videos abspielen" + +#: plugins/playvideos/playvideos.php:56 +msgid "Add a button in the toolbar allowing to watch all videos." +msgstr "" +"Fügt eine Schaltfläche in der Symbolleiste hinzu, mit der man alle Videos " +"ansehen kann." + +#: 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:69 +#, php-format +msgid "Could not publish to PubSubHubbub: %s" +msgstr "Veröffentlichung auf PubSubHubbub nicht möglich: %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:95 +#, php-format +msgid "Could not post to %s" +msgstr "Kann nicht posten auf %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:99 +#, php-format +msgid "Bad response from the hub %s" +msgstr "Ungültige Antwort vom Hub %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:110 +msgid "Enable PubSubHubbub feed publishing." +msgstr "Aktiviere PubSubHubbub Feed Veröffentlichung." + +#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68 +msgid "For each link, add a QRCode icon." +msgstr "Für jeden Link, füge eine QRCode Icon hinzu." + +#: plugins/wallabag/wallabag.php:21 +msgid "" +"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " +"plugin administration page." +msgstr "" +"Wallabag Plugin Fehler: Bitte definiere die Einstellung \"WALLABAG_URL\" auf " +"der Plugin Administrationsseite." + +#: plugins/wallabag/wallabag.php:47 +msgid "Save to wallabag" +msgstr "Auf Wallabag speichern" + +#: plugins/wallabag/wallabag.php:69 +msgid "Wallabag API URL" +msgstr "Wallabag API URL" + +#: plugins/wallabag/wallabag.php:70 +msgid "Wallabag API version (1 or 2)" +msgstr "Wallabag API version (1 oder 2)" + +#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227 +#: tests/languages/fr/LanguagesFrTest.php:160 +#: tests/languages/fr/LanguagesFrTest.php:173 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81 +msgid "Search" +msgid_plural "Search" +msgstr[0] "Suche" +msgstr[1] "Suchen" + +#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 +msgid "Sorry, nothing to see here." +msgstr "Entschuldige, hier gibt es nichts zu sehen." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "URL or leave empty to post a note" +msgstr "URL oder leer lassen um eine Notiz hinzuzufügen" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Current password" +msgstr "Aktuelles Passwort" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "New password" +msgstr "Neues Passwort" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Change" +msgstr "Wechseln" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "Tag" +msgstr "Tag" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "New name" +msgstr "Neuer Name" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +msgid "Case sensitive" +msgstr "Groß- / Kleinschreibung-unterscheidend" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +msgid "Rename" +msgstr "Umbenennen" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172 +msgid "Delete" +msgstr "Löschen" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +msgid "You can also edit tags in the" +msgstr "Du kannst auch Tags bearbeiten in der" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +msgid "tag list" +msgstr "Tag Liste" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "title" +msgstr "Titel" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +msgid "Home link" +msgstr "Home Link" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "Default value" +msgstr "Standardwert" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "Theme" +msgstr "Thema" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +msgid "Language" +msgstr "Sprache" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "Timezone" +msgstr "Zeitzone" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 +msgid "Continent" +msgstr "Kontinent" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 +msgid "City" +msgstr "Stadt" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164 +msgid "Disable session cookie hijacking protection" +msgstr "Deaktiviere Session Cookie Hijacking Schutz" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 +msgid "Check this if you get disconnected or if your IP address changes often" +msgstr "" +"Überprüfe dies, wenn die Verbindung getrennt wird oder wenn sich deine IP-" +"Adresse häufig ändert" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 +msgid "Private links by default" +msgstr "Standardmäßig Private Links" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184 +msgid "All new links are private by default" +msgstr "Alle neuen Links sind standardmäßig privat" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 +msgid "RSS direct links" +msgstr "RSS Direkt Links" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200 +msgid "Check this to use direct URL instead of permalink in feeds" +msgstr "" +"Aktivieren diese Option, um direkte URLs anstelle von Permalinks in Feeds zu " +"verwenden" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215 +msgid "Hide public links" +msgstr "Verstecke öffentliche Links" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216 +msgid "Do not show any links if the user is not logged in" +msgstr "Zeige keine Links, wenn der Benutzer nicht angemeldet ist" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 +msgid "Check updates" +msgstr "Auf Updates prüfen" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 +msgid "Notify me when a new release is ready" +msgstr "Benachrichtige mich, wenn eine neue Version zur Verfügung steht" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Enable REST API" +msgstr "Aktiviere REST API" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 +msgid "Allow third party software to use Shaarli such as mobile application" +msgstr "" +"Erlaube Software von Drittanbietern für Shaarli, wie z.B. die mobile " +"Anwendung" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263 +msgid "API secret" +msgstr "API secret" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 +msgid "Save" +msgstr "Speichern" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "The Daily Shaarli" +msgstr "Der tägliche Shaarli" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "1 RSS entry per day" +msgstr "1 RSS Eintrag pro Tag" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 +msgid "Previous day" +msgstr "Vorheriger Tag" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "All links of one day in a single page." +msgstr "Alle Links eines Tages auf einer Seite." + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +msgid "Next day" +msgstr "Nächster Tag" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +msgid "Created:" +msgstr "Erstellt:" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "URL" +msgstr "URL" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +msgid "Title" +msgstr "Titel" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +#: 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 "Beschreibung" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +msgid "Tags" +msgstr "Tags" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 +msgid "Private" +msgstr "Privat" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +msgid "Apply Changes" +msgstr "Änderungen übernehmen" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Export Database" +msgstr "Exportiere Datenbank" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "Selection" +msgstr "Beschreibung" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +msgid "All" +msgstr "Alle" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "Public" +msgstr "Öffentlich" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 +msgid "Prepend note permalinks with this Shaarli instance's URL" +msgstr "Voranstellen von Notizen-Permalinks mit der URL dieser Shaarli-Instanz" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +msgid "Useful to import bookmarks in a web browser" +msgstr "Sinnvoll Lesezeichen im Browser zu importieren" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Import Database" +msgstr "Importiere Datenbank" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Maximum size allowed:" +msgstr "Maximale Größe erlaubt:" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Visibility" +msgstr "Sichtbarkeit" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Use values from the imported file, default to public" +msgstr "Verwende Werte aus der importierten Datei, standardmäßig öffentlich" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "Import all bookmarks as private" +msgstr "Importiere alle Lesezeichen als Privat" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +msgid "Import all bookmarks as public" +msgstr "Importiere alles Lesezeichen als öffentlich" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 +msgid "Overwrite existing bookmarks" +msgstr "Überschreibe alle bestehenden Lesezeichen" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "Duplicates based on URL" +msgstr "Duplikate basierend auf URL" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +msgid "Add default tags" +msgstr "Standard-Tag hinzufügen" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Install Shaarli" +msgstr "Installiere Shaarli" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +msgid "It looks like it's the first time you run Shaarli. Please configure it." +msgstr "" +"Es sieht so aus, als ob du Shaarli das erste mal verwendest. Bitte " +"konfiguriere es." + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 +msgid "Username" +msgstr "Benutzername" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148 +msgid "Password" +msgstr "Passwort" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +msgid "Shaarli title" +msgstr "Shaarli Titel" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 +msgid "My links" +msgstr "Meine Links" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 +msgid "Install" +msgstr "Installiere" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 +msgid "shaare" +msgid_plural "shaares" +msgstr[0] "Teile" +msgstr[1] "Teilen" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 +msgid "private link" +msgid_plural "private links" +msgstr[0] "Privater Link" +msgstr[1] "Private Links" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117 +msgid "Search text" +msgstr "Text durchsuchen" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +msgid "Filter by tag" +msgstr "Nach Tag filtern" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 +msgid "Nothing found." +msgstr "Nichts gefunden." + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119 +#, php-format +msgid "%s result" +msgid_plural "%s results" +msgstr[0] "%s Ergebnis" +msgstr[1] "%s Ergebnisse" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +msgid "for" +msgstr "für" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 +msgid "tagged" +msgstr "markiert" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "Remove tag" +msgstr "Tag entfernen" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 +msgid "with status" +msgstr "mit Status" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 +msgid "without any tag" +msgstr "ohne irgendeinen Tag" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 +msgid "Fold" +msgstr "Ablegen" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 +msgid "Edited: " +msgstr "Bearbeitet: " + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180 +msgid "permalink" +msgstr "Permalink" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 +msgid "Add tag" +msgstr "Tag hinzufügen" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7 +msgid "Filters" +msgstr "Filter" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12 +msgid "Only display private links" +msgstr "Zeige nur private Links" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:15 +msgid "Only display public links" +msgstr "Zeige nur öffentliche Links" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:20 +msgid "Filter untagged links" +msgstr "Unmarkierte Tags filtern" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 +msgid "Fold all" +msgstr "Alles ablegen" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:69 +msgid "Links per page" +msgstr "Links pro Seite" + +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "" +"You have been banned after too many failed login attempts. Try again later." +msgstr "" +"Du wurdest nach zu vielen fehlgeschlagenen Anmeldeversuchen gesperrt. " +"Versuche es später noch einmal." + +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151 +msgid "Remember me" +msgstr "Erinnere dich an mich" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "by the Shaarli community" +msgstr "von der Shaarli Community" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 +msgid "Documentation" +msgstr "Dokumentation" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 +msgid "Expand" +msgstr "Erweitern" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 +msgid "Expand all" +msgstr "Alles erweitern" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 +msgid "Are you sure you want to delete this link?" +msgstr "Bist du sicher das du diesen Link löschen möchtest?" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86 +msgid "RSS Feed" +msgstr "RSS Feed" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102 +msgid "Logout" +msgstr "Ausloggen" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169 +msgid "is available" +msgstr "ist verfügbar" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176 +msgid "Error" +msgstr "Fehler" + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Picture Wall" +msgstr "Bildwand" + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "pics" +msgstr "Bilder" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "You need to enable Javascript to change plugin loading order." +msgstr "" +"Du musst Javascript aktivieren um die Ladereihenfolge der Plugins zu ändern." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Enabled Plugins" +msgstr "Aktivierte Plugins" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 +msgid "No plugin enabled." +msgstr "Kein Plugin aktiviert." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +msgid "Disable" +msgstr "Deaktivieren" + +#: 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 "Name" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +msgid "Order" +msgstr "Reihenfolge" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 +msgid "Disabled Plugins" +msgstr "Deaktivierte Plugins" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "No plugin disabled." +msgstr "Kein Plugin deaktiviert." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "Enable" +msgstr "Aktiviere" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "More plugins available" +msgstr "Weitere Plugins verfügbar" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 +msgid "in the documentation" +msgstr "In der Dokumentation" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 +msgid "Plugin configuration" +msgstr "Plugin Konfiguration" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195 +msgid "No parameter available." +msgstr "Kein Parameter verfügbar." + +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "tags" +msgstr "Tags" + +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "List all links with those tags" +msgstr "Zeige alle Links mit diesen Tags" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 +msgid "Sort by:" +msgstr "Sortiere nach:" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5 +msgid "Cloud" +msgstr "Cloud" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6 +msgid "Most used" +msgstr "Am meisten verwendet" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7 +msgid "Alphabetical" +msgstr "Alphabetisch" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Settings" +msgstr "Einstellungen" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Change Shaarli settings: title, timezone, etc." +msgstr "Shaarli Einstellungen ändern: Titel, Zeitzone, usw." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "Configure your Shaarli" +msgstr "Shaarli konfigurieren" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +msgid "Enable, disable and configure plugins" +msgstr "Plugins aktivieren, deaktivieren und konfigurieren" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Change your password" +msgstr "Ändere dein Passwort" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +msgid "Rename or delete a tag in all links" +msgstr "Umbenennen oder löschen eines Tags in allen Links" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "" +"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " +"delicious...)" +msgstr "" +"Importiere Netscape Lesezeichen (wie aus Firefox exportiert, Chrome, Opera, " +"delicious...)" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "Import links" +msgstr "Importiere Links" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "" +"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " +"Opera, delicious...)" +msgstr "" +"Exportiere Netscape HTML Lesezeichen (welche in Firefox importiert werden " +"können, Chrome, Opera, delicious...)" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +msgid "Export database" +msgstr "Exportiere Datenbank" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 +msgid "" +"Drag one of these button to your bookmarks toolbar or right-click it and " +"\"Bookmark This Link\"" +msgstr "" +"Ziehe eine dieser Schaltflächen in deine Lesezeichen-Symbolleiste oder " +"klicke mit der rechten Maustaste darauf und \"Speichere diesen Link als " +"Lesezeichen\"" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +msgid "then click on the bookmarklet in any page you want to share." +msgstr "" +"Klicke dann auf das Bookmarklet auf jeder Seite, welches du teilen möchtest." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100 +msgid "" +"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " +"Link" +msgstr "" +"Ziehe diese Link in deine Lesezeichen-Symbolleiste oder klicke mit der " +"rechten Maustaste darauf und \"Speichere diesen Link als Lesezeichen\"" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "then click ✚Shaare link button in any page you want to share" +msgstr "" +"klicke dann auf die Schaltfläche ✚Teilen auf jeder Seite, die du teilen " +"möchtest" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 +msgid "The selected text is too long, it will be truncated." +msgstr "Der ausgewählte Text ist zu lang, er wird gekürzt." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 +msgid "Shaare link" +msgstr "Teile Link" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 +msgid "" +"Then click ✚Add Note button anytime to start composing a private Note (text " +"post) to your Shaarli" +msgstr "" +"Klicke auf ✚Notiz hinzufügen um eine private Notiz (Textnachricht) zu " +"Shaarli hinzuzufügen" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 +msgid "Add Note" +msgstr "Notiz hinzufügen" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 +msgid "" +"You need to browse your Shaarli over HTTPS to use this " +"functionality." +msgstr "" +"Um diese Funktion nutzen zu können, musst du Shaarli über HTTPS aufrufen." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "Add to" +msgstr "Hinzufügen zu" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145 +msgid "3rd party" +msgstr "Von Dritten" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 +msgid "Plugin" +msgstr "Plugin" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 +msgid "plugin" +msgstr "Plugin" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 +msgid "" +"Drag this link to your bookmarks toolbar, or right-click it and choose " +"Bookmark This Link" +msgstr "" +"Ziehe diesen Link in deine Lesezeichen-Symbolleiste oder klicke mit der " +"rechten Maustaste darauf und wähle \"Speichere diesen Link als Lesezeichen\"" diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index fd47217..2ebeccb 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,9 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-02-24 12:39+0100\n" -"PO-Revision-Date: 2018-02-24 12:43+0100\n" +"POT-Creation-Date: 2018-01-24 18:43+0100\n" +"PO-Revision-Date: 2018-03-06 18:44+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -764,6 +763,12 @@ msgstr "Tous les liens d'un jour sur une page." msgid "Next day" msgstr "Jour suivant" +#: tpl/editlink.html +msgid "Edit Shaare" +msgstr "Modifier le Shaare" +msgid "New Shaare" +msgstr "Nouveau Shaare" + #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 msgid "Created:" msgstr "Création :" diff --git a/index.php b/index.php index dbc2bb3..c34434d 100644 --- a/index.php +++ b/index.php @@ -78,8 +78,8 @@ require_once 'application/Updater.php'; use \Shaarli\Languages; use \Shaarli\ThemeUtils; use \Shaarli\Config\ConfigManager; -use \Shaarli\LoginManager; -use \Shaarli\SessionManager; +use \Shaarli\Security\LoginManager; +use \Shaarli\Security\SessionManager; // Ensure the PHP version is supported try { @@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') { // Set default cookie expiration and path. session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); // Set session parameters on server side. -// If the user does not access any page within this time, his/her session is considered expired. -define('INACTIVITY_TIMEOUT', 3600); // in seconds. // Use cookies to store session. ini_set('session.use_cookies', 1); // Force cookies for session (phpsessionID forbidden in URL). @@ -123,8 +121,10 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) } $conf = new ConfigManager(); -$loginManager = new LoginManager($GLOBALS, $conf); $sessionManager = new SessionManager($_SESSION, $conf); +$loginManager = new LoginManager($GLOBALS, $conf, $sessionManager); +$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); +$clientIpId = client_ip_id($_SERVER); // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. if (! defined('LC_MESSAGES')) { @@ -177,157 +177,61 @@ if (! is_file($conf->getConfigFileExt())) { install($conf, $sessionManager); } -// a token depending of deployment salt, user password, and the current ip -define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); +$loginManager->checkLoginState($_COOKIE, $clientIpId); /** - * Checking session state (i.e. is the user still logged in) + * Adapter function to ensure compatibility with third-party templates * - * @param ConfigManager $conf The configuration manager. + * @see https://github.com/shaarli/Shaarli/pull/1086 * - * @return bool: true if the user is logged in, false otherwise. + * @return bool true when the user is logged in, false otherwise */ -function setup_login_state($conf) -{ - if ($conf->get('security.open_shaarli')) { - return true; - } - $userIsLoggedIn = false; // By default, we do not consider the user as logged in; - $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met. - if (! $conf->exists('credentials.login')) { - $userIsLoggedIn = false; // Shaarli is not configured yet. - $loginFailure = true; - } - if (isset($_COOKIE['shaarli_staySignedIn']) && - $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN && - !$loginFailure) - { - fillSessionInfo($conf); - $userIsLoggedIn = true; - } - // If session does not exist on server side, or IP address has changed, or session has expired, logout. - if (empty($_SESSION['uid']) - || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs()) - || time() >= $_SESSION['expires_on']) - { - logout(); - $userIsLoggedIn = false; - $loginFailure = true; - } - if (!empty($_SESSION['longlastingsession'])) { - $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked. - } - else { - $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date. - } - if (!$loginFailure) { - $userIsLoggedIn = true; - } - - return $userIsLoggedIn; -} -$userIsLoggedIn = setup_login_state($conf); - -// ------------------------------------------------------------------------------------------ -// Session management - -// Returns the IP address of the client (Used to prevent session cookie hijacking.) -function allIPs() -{ - $ip = $_SERVER['REMOTE_ADDR']; - // Then we use more HTTP headers to prevent session hijacking from users behind the same proxy. - if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; } - if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; } - return $ip; -} - -/** - * Load user session. - * - * @param ConfigManager $conf Configuration Manager instance. - */ -function fillSessionInfo($conf) -{ - $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid) - $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked. - $_SESSION['username']= $conf->get('credentials.login'); - $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. -} - -/** - * Check that user/password is correct. - * - * @param string $login Username - * @param string $password User password - * @param ConfigManager $conf Configuration Manager instance. - * - * @return bool: authentication successful or not. - */ -function check_auth($login, $password, $conf) -{ - $hash = sha1($password . $login . $conf->get('credentials.salt')); - if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash')) - { // Login/password is correct. - fillSessionInfo($conf); - logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful'); - return true; - } - logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login); - return false; -} - -// Returns true if the user is logged in. function isLoggedIn() { - global $userIsLoggedIn; - return $userIsLoggedIn; + global $loginManager; + return $loginManager->isLoggedIn(); } -// Force logout. -function logout() { - if (isset($_SESSION)) { - unset($_SESSION['uid']); - unset($_SESSION['ip']); - unset($_SESSION['username']); - unset($_SESSION['visibility']); - unset($_SESSION['untaggedonly']); - } - setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH); -} // ------------------------------------------------------------------------------------------ // Process login form: Check if login/password is correct. -if (isset($_POST['login'])) -{ +if (isset($_POST['login'])) { if (! $loginManager->canLogin($_SERVER)) { die(t('I said: NO. You are banned for the moment. Go away.')); } if (isset($_POST['password']) && $sessionManager->checkToken($_POST['token']) - && (check_auth($_POST['login'], $_POST['password'], $conf)) + && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password']) ) { - // Login/password is OK. $loginManager->handleSuccessfulLogin($_SERVER); - // If user wants to keep the session cookie even after the browser closes: - if (!empty($_POST['longlastingsession'])) { - $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year) - $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now) - setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH); - $_SESSION['expires_on'] = $expiration; // Set session expiration on server-side. - - $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; - session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side + $cookiedir = ''; + if (dirname($_SERVER['SCRIPT_NAME']) != '/') { // Note: Never forget the trailing slash on the cookie path! - session_regenerate_id(true); // Send cookie with new expiration date to browser. + $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/'; } - else // Standard session expiration (=when browser closes) - { - $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; - session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes" - session_regenerate_id(true); + + if (!empty($_POST['longlastingsession'])) { + // Keep the session cookie even after the browser closes + $sessionManager->setStaySignedIn(true); + $expirationTime = $sessionManager->extendSession(); + + setcookie( + $loginManager::$STAY_SIGNED_IN_COOKIE, + $loginManager->getStaySignedInToken(), + $expirationTime, + WEB_PATH + ); + + } else { + // Standard session expiration (=when browser closes) + $expirationTime = 0; } + // Send cookie with the new expiration date to the browser + session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']); + session_regenerate_id(true); + // Optional redirect after login: if (isset($_GET['post'])) { $uri = '?post='. urlencode($_GET['post']); @@ -380,15 +284,16 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are atta * Gives the last 7 days (which have links). * This RSS feed cannot be filtered. * - * @param ConfigManager $conf Configuration Manager instance. + * @param ConfigManager $conf Configuration Manager instance + * @param LoginManager $loginManager LoginManager instance */ -function showDailyRSS($conf) { +function showDailyRSS($conf, $loginManager) { // Cache system $query = $_SERVER['QUERY_STRING']; $cache = new CachedPage( $conf->get('config.PAGE_CACHE'), page_url($_SERVER), - startsWith($query,'do=dailyrss') && !isLoggedIn() + startsWith($query,'do=dailyrss') && !$loginManager->isLoggedIn() ); $cached = $cache->cachedVersion(); if (!empty($cached)) { @@ -400,7 +305,7 @@ function showDailyRSS($conf) { // Read links from database (and filter private links if used it not logged in). $LINKSDB = new LinkDB( $conf->get('resource.datastore'), - isLoggedIn(), + $loginManager->isLoggedIn(), $conf->get('privacy.hide_public_links'), $conf->get('redirector.url'), $conf->get('redirector.encode_url') @@ -482,9 +387,10 @@ function showDailyRSS($conf) { * @param PageBuilder $pageBuilder Template engine wrapper. * @param LinkDB $LINKSDB LinkDB instance. * @param ConfigManager $conf Configuration Manager instance. - * @param PluginManager $pluginManager Plugin Manager instane. + * @param PluginManager $pluginManager Plugin Manager instance. + * @param LoginManager $loginManager Login Manager instance */ -function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) +function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager) { $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD. if (isset($_GET['day'])) { @@ -542,7 +448,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) /* Hook is called before column construction so that plugins don't have to deal with columns. */ - $pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn())); + $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn())); /* We need to spread the articles on 3 columns. I did not want to use a JavaScript lib like http://masonry.desandro.com/ @@ -586,8 +492,8 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) * @param ConfigManager $conf Configuration Manager instance. * @param PluginManager $pluginManager Plugin Manager instance. */ -function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { - buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager); // Compute list of links to display +function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) { + buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager, $loginManager); $PAGE->renderPage('linklist'); } @@ -607,7 +513,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, read_updates_file($conf->get('resource.updates')), $LINKSDB, $conf, - isLoggedIn() + $loginManager->isLoggedIn() ); try { $newUpdates = $updater->update(); @@ -622,18 +528,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, die($e->getMessage()); } - $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken()); + $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn()); $PAGE->assign('linkcount', count($LINKSDB)); $PAGE->assign('privateLinkcount', count_private($LINKSDB)); $PAGE->assign('plugin_errors', $pluginManager->getErrors()); // Determine which page will be rendered. $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; - $targetPage = Router::findPage($query, $_GET, isLoggedIn()); + $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn()); if ( // if the user isn't logged in - !isLoggedIn() && + !$loginManager->isLoggedIn() && // and Shaarli doesn't have public content... $conf->get('privacy.hide_public_links') && // and is configured to enforce the login @@ -661,7 +567,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $pluginManager->executeHooks('render_' . $name, $plugin_data, array( 'target' => $targetPage, - 'loggedin' => isLoggedIn() + 'loggedin' => $loginManager->isLoggedIn() ) ); $PAGE->assign('plugins_' . $name, $plugin_data); @@ -686,7 +592,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) { invalidateCaches($conf->get('resource.page_cache')); - logout(); + $sessionManager->logout(); + setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH); header('Location: ?'); exit; } @@ -713,7 +620,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $data = array( 'linksToDisplay' => $linksToDisplay, ); - $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn())); + $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn())); foreach ($data as $key => $value) { $PAGE->assign($key, $value); @@ -760,7 +667,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, 'search_tags' => $searchTags, 'tags' => $tagList, ); - $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); + $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn())); foreach ($data as $key => $value) { $PAGE->assign($key, $value); @@ -793,7 +700,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, 'search_tags' => $searchTags, 'tags' => $tags, ]; - $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); + $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]); foreach ($data as $key => $value) { $PAGE->assign($key, $value); @@ -807,7 +714,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, // Daily page. if ($targetPage == Router::$PAGE_DAILY) { - showDaily($PAGE, $LINKSDB, $conf, $pluginManager); + showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); } // ATOM and RSS feed. @@ -820,7 +727,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $cache = new CachedPage( $conf->get('resource.page_cache'), page_url($_SERVER), - startsWith($query,'do='. $targetPage) && !isLoggedIn() + startsWith($query,'do='. $targetPage) && !$loginManager->isLoggedIn() ); $cached = $cache->cachedVersion(); if (!empty($cached)) { @@ -829,15 +736,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, } // Generate data. - $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn()); + $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn()); $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); - $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn()); + $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn()); $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); $data = $feedGenerator->buildData(); // Process plugin hook. $pluginManager->executeHooks('render_feed', $data, array( - 'loggedin' => isLoggedIn(), + 'loggedin' => $loginManager->isLoggedIn(), 'target' => $targetPage, )); @@ -985,7 +892,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, } // -------- Handle other actions allowed for non-logged in users: - if (!isLoggedIn()) + if (!$loginManager->isLoggedIn()) { // User tries to post new link but is not logged in: // Show login screen, then redirect to ?post=... @@ -1001,7 +908,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, exit; } - showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); + showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); if (isset($_GET['edit_link'])) { header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); exit; @@ -1052,7 +959,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt'))); try { - $conf->write(isLoggedIn()); + $conf->write($loginManager->isLoggedIn()); } catch(Exception $e) { error_log( @@ -1103,7 +1010,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $conf->set('translation.language', escape($_POST['language'])); try { - $conf->write(isLoggedIn()); + $conf->write($loginManager->isLoggedIn()); $history->updateSettings(); invalidateCaches($conf->get('resource.page_cache')); } @@ -1376,8 +1283,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, // The callback will fill $charset and $title with data from the downloaded page. get_http_response( $url, - $conf->get('general.download_max_size', 4194304), $conf->get('general.download_timeout', 30), + $conf->get('general.download_max_size', 4194304), get_curl_download_callback($charset, $title) ); if (! empty($title) && strtolower($charset) != 'utf-8') { @@ -1555,7 +1462,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, else { $conf->set('general.enabled_plugins', save_plugin_config($_POST)); } - $conf->write(isLoggedIn()); + $conf->write($loginManager->isLoggedIn()); $history->updateSettings(); } catch (Exception $e) { @@ -1580,7 +1487,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, } // -------- Otherwise, simply display search form and links: - showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); + showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); exit; } @@ -1592,8 +1499,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, * @param LinkDB $LINKSDB LinkDB instance. * @param ConfigManager $conf Configuration Manager instance. * @param PluginManager $pluginManager Plugin Manager instance. + * @param LoginManager $loginManager LoginManager instance */ -function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) +function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) { // Used in templates if (isset($_GET['searchtags'])) { @@ -1632,8 +1540,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) $keys[] = $key; } - - // Select articles according to paging. $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']); $pagecount = $pagecount == 0 ? 1 : $pagecount; @@ -1714,7 +1620,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) $data['pagetitle'] .= '- '. $conf->get('general.title'); } - $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn())); + $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn())); foreach ($data as $key => $value) { $PAGE->assign($key, $value); @@ -1985,7 +1891,7 @@ function install($conf, $sessionManager) { ); try { // Everything is ok, let's create config file. - $conf->write(isLoggedIn()); + $conf->write($loginManager->isLoggedIn()); } catch(Exception $e) { error_log( @@ -2249,7 +2155,7 @@ try { $linkDb = new LinkDB( $conf->get('resource.datastore'), - isLoggedIn(), + $loginManager->isLoggedIn(), $conf->get('privacy.hide_public_links'), $conf->get('redirector.url'), $conf->get('redirector.encode_url') diff --git a/mkdocs.yml b/mkdocs.yml index 443c3a0..8ba2554 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,9 +22,8 @@ pages: - Reverse proxy configuration: docker/reverse-proxy-configuration.md - Docker resources: docker/resources.md - Usage: - - Bookmarklet: Bookmarklet.md - Browsing and searching: Browsing-and-searching.md - - Firefox share: Firefox-share.md + - Sharing content: Sharing-content.md - RSS feeds: RSS-feeds.md - REST API: REST-API.md - Community & Related software: Community-&-Related-software.md diff --git a/package.json b/package.json index ba997c9..3dd1e0f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^1.1.6", "node-sass": "^4.7.2", + "sass-lint": "^1.12.1", "sass-loader": "^6.0.6", "style-loader": "^0.19.1", "url-loader": "^0.6.2", diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php index 2f24e41..821bb12 100644 --- a/plugins/markdown/markdown.php +++ b/plugins/markdown/markdown.php @@ -6,6 +6,8 @@ * Shaare's descriptions are parsed with Markdown. */ +use Shaarli\Config\ConfigManager; + /* * If this tag is used on a shaare, the description won't be processed by Parsedown. */ @@ -50,6 +52,7 @@ function hook_markdown_render_feed($data, $conf) $value = stripNoMarkdownTag($value); continue; } + $value['description'] = reverse_feed_permalink($value['description']); $value['description'] = process_markdown( $value['description'], $conf->get('security.markdown_escape', true), @@ -244,6 +247,11 @@ function reverse_space2nbsp($description) return preg_replace('/(^| ) /m', '$1 ', $description); } +function reverse_feed_permalink($description) +{ + return preg_replace('@— (\w+)$@im', '— [$2]($1)', $description); +} + /** * Replace not whitelisted protocols with http:// in given description. * diff --git a/tests/HttpUtils/ClientIpIdTest.php b/tests/HttpUtils/ClientIpIdTest.php new file mode 100644 index 0000000..c15ac5c --- /dev/null +++ b/tests/HttpUtils/ClientIpIdTest.php @@ -0,0 +1,52 @@ +assertEquals( + '10.1.167.42', + client_ip_id(['REMOTE_ADDR' => '10.1.167.42']) + ); + } + + /** + * Get a remote client ID based on its IP and proxy information (1) + */ + public function testClientIpIdRemoteForwarded() + { + $this->assertEquals( + '10.1.167.42_127.0.1.47', + client_ip_id([ + 'REMOTE_ADDR' => '10.1.167.42', + 'HTTP_X_FORWARDED_FOR' => '127.0.1.47' + ]) + ); + } + + /** + * Get a remote client ID based on its IP and proxy information (2) + */ + public function testClientIpIdRemoteForwardedClient() + { + $this->assertEquals( + '10.1.167.42_10.1.167.56_127.0.1.47', + client_ip_id([ + 'REMOTE_ADDR' => '10.1.167.42', + 'HTTP_X_FORWARDED_FOR' => '10.1.167.56', + 'HTTP_CLIENT_IP' => '127.0.1.47' + ]) + ); + } +} diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 5b2f366..3b98087 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php @@ -542,4 +542,104 @@ class LinkDBTest extends PHPUnit_Framework_TestCase $this->assertEquals(3, count($res)); $this->assertNotContains('cartoon', $linkDB[4]['tags']); } + + /** + * Test linksCountPerTag all tags without filter. + * Equal occurrences should be sorted alphabetically. + */ + public function testCountLinkPerTagAllNoFilter() + { + $expected = [ + 'web' => 4, + 'cartoon' => 3, + 'dev' => 2, + 'gnu' => 2, + 'hashtag' => 2, + 'sTuff' => 2, + '-exclude' => 1, + '.hidden' => 1, + 'Mercurial' => 1, + 'css' => 1, + 'free' => 1, + 'html' => 1, + 'media' => 1, + 'samba' => 1, + 'software' => 1, + 'stallman' => 1, + 'tag1' => 1, + 'tag2' => 1, + 'tag3' => 1, + 'tag4' => 1, + 'ut' => 1, + 'w3c' => 1, + ]; + $tags = self::$privateLinkDB->linksCountPerTag(); + + $this->assertEquals($expected, $tags, var_export($tags, true)); + } + + /** + * Test linksCountPerTag all tags with filter. + * Equal occurrences should be sorted alphabetically. + */ + public function testCountLinkPerTagAllWithFilter() + { + $expected = [ + 'gnu' => 2, + 'hashtag' => 2, + '-exclude' => 1, + '.hidden' => 1, + 'free' => 1, + 'media' => 1, + 'software' => 1, + 'stallman' => 1, + 'stuff' => 1, + 'web' => 1, + ]; + $tags = self::$privateLinkDB->linksCountPerTag(['gnu']); + + $this->assertEquals($expected, $tags, var_export($tags, true)); + } + + /** + * Test linksCountPerTag public tags with filter. + * Equal occurrences should be sorted alphabetically. + */ + public function testCountLinkPerTagPublicWithFilter() + { + $expected = [ + 'gnu' => 2, + 'hashtag' => 2, + '-exclude' => 1, + '.hidden' => 1, + 'free' => 1, + 'media' => 1, + 'software' => 1, + 'stallman' => 1, + 'stuff' => 1, + 'web' => 1, + ]; + $tags = self::$privateLinkDB->linksCountPerTag(['gnu'], 'public'); + + $this->assertEquals($expected, $tags, var_export($tags, true)); + } + + /** + * Test linksCountPerTag public tags with filter. + * Equal occurrences should be sorted alphabetically. + */ + public function testCountLinkPerTagPrivateWithFilter() + { + $expected = [ + 'cartoon' => 1, + 'dev' => 1, + 'tag1' => 1, + 'tag2' => 1, + 'tag3' => 1, + 'tag4' => 1, + ]; + $tags = self::$privateLinkDB->linksCountPerTag(['dev'], 'private'); + + $this->assertEquals($expected, $tags, var_export($tags, true)); + } } diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php deleted file mode 100644 index aa75962..0000000 --- a/tests/SessionManagerTest.php +++ /dev/null @@ -1,149 +0,0 @@ -generateToken(); - - $this->assertEquals(1, $session['tokens'][$token]); - $this->assertEquals(40, strlen($token)); - } - - /** - * Check a session token - */ - public function testCheckToken() - { - $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'; - $session = [ - 'tokens' => [ - $token => 1, - ], - ]; - $sessionManager = new SessionManager($session, self::$conf); - - // check and destroy the token - $this->assertTrue($sessionManager->checkToken($token)); - $this->assertFalse(isset($session['tokens'][$token])); - - // ensure the token has been destroyed - $this->assertFalse($sessionManager->checkToken($token)); - } - - /** - * Generate and check a session token - */ - public function testGenerateAndCheckToken() - { - $session = []; - $sessionManager = new SessionManager($session, self::$conf); - - $token = $sessionManager->generateToken(); - - // ensure a token has been generated - $this->assertEquals(1, $session['tokens'][$token]); - $this->assertEquals(40, strlen($token)); - - // check and destroy the token - $this->assertTrue($sessionManager->checkToken($token)); - $this->assertFalse(isset($session['tokens'][$token])); - - // ensure the token has been destroyed - $this->assertFalse($sessionManager->checkToken($token)); - } - - /** - * Check an invalid session token - */ - public function testCheckInvalidToken() - { - $session = []; - $sessionManager = new SessionManager($session, self::$conf); - - $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b')); - } - - /** - * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES! - * - * This tests extensively covers all hash algorithms / bit representations - */ - public function testIsAnyHashSessionIdValid() - { - foreach (self::$sidHashes as $algo => $bpcs) { - foreach ($bpcs as $bpc => $hash) { - $this->assertTrue(SessionManager::checkId($hash)); - } - } - } - - /** - * Test checkId with a valid ID - SHA-1 hashes - */ - public function testIsSha1SessionIdValid() - { - $this->assertTrue(SessionManager::checkId(sha1('shaarli'))); - } - - /** - * Test checkId with a valid ID - SHA-256 hashes - */ - public function testIsSha256SessionIdValid() - { - $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli'))); - } - - /** - * Test checkId with a valid ID - SHA-512 hashes - */ - public function testIsSha512SessionIdValid() - { - $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli'))); - } - - /** - * Test checkId with invalid IDs. - */ - public function testIsSessionIdInvalid() - { - $this->assertFalse(SessionManager::checkId('')); - $this->assertFalse(SessionManager::checkId([])); - $this->assertFalse( - SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') - ); - } -} diff --git a/tests/plugins/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php index ddc2728..b31e817 100644 --- a/tests/plugins/PluginMarkdownTest.php +++ b/tests/plugins/PluginMarkdownTest.php @@ -49,6 +49,30 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase $this->assertNotFalse(strpos($data['links'][0]['description'], '

')); } + /** + * Test render_feed hook. + */ + public function testMarkdownFeed() + { + $markdown = '# My title' . PHP_EOL . 'Very interesting content.'; + $markdown .= '— Permalien'; + $data = array( + 'links' => array( + 0 => array( + 'description' => $markdown, + ), + ), + ); + + $data = hook_markdown_render_feed($data, $this->conf); + $this->assertNotFalse(strpos($data['links'][0]['description'], '

')); + $this->assertNotFalse(strpos($data['links'][0]['description'], '

')); + $this->assertStringEndsWith( + '— Permalien

', + $data['links'][0]['description'] + ); + } + /** * Test render_daily hook. * Only check that there is basic markdown rendering. @@ -104,6 +128,37 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase $this->assertEquals($text, $reversedText); } + public function testReverseFeedPermalink() + { + $text = 'Description... '; + $text .= '— Permalien'; + $expected = 'Description... — [Permalien](http://domain.tld/?0oc_VQ)'; + $processedText = reverse_feed_permalink($text); + + $this->assertEquals($expected, $processedText); + } + + public function testReverseLastFeedPermalink() + { + $text = 'Description... '; + $text .= '
Permalien'; + $expected = $text; + $text .= '
Permalien'; + $expected .= '
— [Permalien](http://domain.tld/?0oc_VQ)'; + $processedText = reverse_feed_permalink($text); + + $this->assertEquals($expected, $processedText); + } + + public function testReverseNoFeedPermalink() + { + $text = 'Hello! Where are you from?'; + $expected = $text; + $processedText = reverse_feed_permalink($text); + + $this->assertEquals($expected, $processedText); + } + /** * Test sanitize_html(). */ diff --git a/tests/LoginManagerTest.php b/tests/security/LoginManagerTest.php similarity index 54% rename from tests/LoginManagerTest.php rename to tests/security/LoginManagerTest.php index 4159038..f26cd1e 100644 --- a/tests/LoginManagerTest.php +++ b/tests/security/LoginManagerTest.php @@ -1,5 +1,5 @@ banFile); } + $this->passwordHash = sha1($this->password . $this->login . $this->salt); + $this->configManager = new \FakeConfigManager([ + 'credentials.login' => $this->login, + 'credentials.hash' => $this->passwordHash, + 'credentials.salt' => $this->salt, 'resource.ban_file' => $this->banFile, 'resource.log' => $this->logFile, 'security.ban_after' => 4, @@ -35,10 +79,15 @@ class LoginManagerTest extends TestCase 'security.trusted_proxies' => [$this->trustedProxy], ]); + $this->cookie = []; + $this->globals = &$GLOBALS; unset($this->globals['IPBANS']); - $this->loginManager = new LoginManager($this->globals, $this->configManager); + $this->session = []; + + $this->sessionManager = new SessionManager($this->session, $this->configManager); + $this->loginManager = new LoginManager($this->globals, $this->configManager, $this->sessionManager); $this->server['REMOTE_ADDR'] = $this->ipAddr; } @@ -59,7 +108,7 @@ class LoginManagerTest extends TestCase $this->banFile, " array('127.0.0.1' => 99));\n?>" ); - new LoginManager($this->globals, $this->configManager); + new LoginManager($this->globals, $this->configManager, null); $this->assertEquals(99, $this->globals['IPBANS']['FAILURES']['127.0.0.1']); } @@ -196,4 +245,130 @@ class LoginManagerTest extends TestCase $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600; $this->assertTrue($this->loginManager->canLogin($this->server)); } + + /** + * Generate a token depending on the user credentials and client IP + */ + public function testGenerateStaySignedInToken() + { + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + + $this->assertEquals( + sha1($this->passwordHash . $this->clientIpAddress . $this->salt), + $this->loginManager->getStaySignedInToken() + ); + } + + /** + * Check user login - Shaarli has not yet been configured + */ + public function testCheckLoginStateNotConfigured() + { + $configManager = new \FakeConfigManager([ + 'resource.ban_file' => $this->banFile, + ]); + $loginManager = new LoginManager($this->globals, $configManager, null); + $loginManager->checkLoginState([], ''); + + $this->assertFalse($loginManager->isLoggedIn()); + } + + /** + * Check user login - the client cookie does not match the server token + */ + public function testCheckLoginStateStaySignedInWithInvalidToken() + { + // simulate a previous login + $this->session = [ + 'ip' => $this->clientIpAddress, + 'expires_on' => time() + 100, + ]; + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope'; + + $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); + + $this->assertTrue($this->loginManager->isLoggedIn()); + $this->assertTrue(empty($this->session['username'])); + } + + /** + * Check user login - the client cookie matches the server token + */ + public function testCheckLoginStateStaySignedInWithValidToken() + { + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken(); + + $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); + + $this->assertTrue($this->loginManager->isLoggedIn()); + $this->assertEquals($this->login, $this->session['username']); + $this->assertEquals($this->clientIpAddress, $this->session['ip']); + } + + /** + * Check user login - the session has expired + */ + public function testCheckLoginStateSessionExpired() + { + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + $this->session['expires_on'] = time() - 100; + + $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); + + $this->assertFalse($this->loginManager->isLoggedIn()); + } + + /** + * Check user login - the remote client IP has changed + */ + public function testCheckLoginStateClientIpChanged() + { + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + + $this->loginManager->checkLoginState($this->cookie, '10.7.157.98'); + + $this->assertFalse($this->loginManager->isLoggedIn()); + } + + /** + * Check user credentials - wrong login supplied + */ + public function testCheckCredentialsWrongLogin() + { + $this->assertFalse( + $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password) + ); + } + + /** + * Check user credentials - wrong password supplied + */ + public function testCheckCredentialsWrongPassword() + { + $this->assertFalse( + $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd') + ); + } + + /** + * Check user credentials - wrong login and password supplied + */ + public function testCheckCredentialsWrongLoginAndPassword() + { + $this->assertFalse( + $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd') + ); + } + + /** + * Check user credentials - correct login and password supplied + */ + public function testCheckCredentialsGoodLoginAndPassword() + { + $this->assertTrue( + $this->loginManager->checkCredentials('', '', $this->login, $this->password) + ); + } } diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php new file mode 100644 index 0000000..9bd868f --- /dev/null +++ b/tests/security/SessionManagerTest.php @@ -0,0 +1,273 @@ +conf = new FakeConfigManager([ + 'credentials.login' => 'johndoe', + 'credentials.salt' => 'salt', + 'security.session_protection_disabled' => false, + ]); + $this->session = []; + $this->sessionManager = new SessionManager($this->session, $this->conf); + } + + /** + * Generate a session token + */ + public function testGenerateToken() + { + $token = $this->sessionManager->generateToken(); + + $this->assertEquals(1, $this->session['tokens'][$token]); + $this->assertEquals(40, strlen($token)); + } + + /** + * Check a session token + */ + public function testCheckToken() + { + $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'; + $session = [ + 'tokens' => [ + $token => 1, + ], + ]; + $sessionManager = new SessionManager($session, $this->conf); + + // check and destroy the token + $this->assertTrue($sessionManager->checkToken($token)); + $this->assertFalse(isset($session['tokens'][$token])); + + // ensure the token has been destroyed + $this->assertFalse($sessionManager->checkToken($token)); + } + + /** + * Generate and check a session token + */ + public function testGenerateAndCheckToken() + { + $token = $this->sessionManager->generateToken(); + + // ensure a token has been generated + $this->assertEquals(1, $this->session['tokens'][$token]); + $this->assertEquals(40, strlen($token)); + + // check and destroy the token + $this->assertTrue($this->sessionManager->checkToken($token)); + $this->assertFalse(isset($this->session['tokens'][$token])); + + // ensure the token has been destroyed + $this->assertFalse($this->sessionManager->checkToken($token)); + } + + /** + * Check an invalid session token + */ + public function testCheckInvalidToken() + { + $this->assertFalse($this->sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b')); + } + + /** + * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES! + * + * This tests extensively covers all hash algorithms / bit representations + */ + public function testIsAnyHashSessionIdValid() + { + foreach (self::$sidHashes as $algo => $bpcs) { + foreach ($bpcs as $bpc => $hash) { + $this->assertTrue(SessionManager::checkId($hash)); + } + } + } + + /** + * Test checkId with a valid ID - SHA-1 hashes + */ + public function testIsSha1SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(sha1('shaarli'))); + } + + /** + * Test checkId with a valid ID - SHA-256 hashes + */ + public function testIsSha256SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli'))); + } + + /** + * Test checkId with a valid ID - SHA-512 hashes + */ + public function testIsSha512SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli'))); + } + + /** + * Test checkId with invalid IDs. + */ + public function testIsSessionIdInvalid() + { + $this->assertFalse(SessionManager::checkId('')); + $this->assertFalse(SessionManager::checkId([])); + $this->assertFalse( + SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') + ); + } + + /** + * Store login information after a successful login + */ + public function testStoreLoginInfo() + { + $this->sessionManager->storeLoginInfo('ip_id'); + + $this->assertGreaterThan(time(), $this->session['expires_on']); + $this->assertEquals('ip_id', $this->session['ip']); + $this->assertEquals('johndoe', $this->session['username']); + } + + /** + * Extend a server-side session by SessionManager::$SHORT_TIMEOUT + */ + public function testExtendSession() + { + $this->sessionManager->extendSession(); + + $this->assertGreaterThan(time(), $this->session['expires_on']); + $this->assertLessThanOrEqual( + time() + SessionManager::$SHORT_TIMEOUT, + $this->session['expires_on'] + ); + } + + /** + * Extend a server-side session by SessionManager::$LONG_TIMEOUT + */ + public function testExtendSessionStaySignedIn() + { + $this->sessionManager->setStaySignedIn(true); + $this->sessionManager->extendSession(); + + $this->assertGreaterThan(time(), $this->session['expires_on']); + $this->assertGreaterThan( + time() + SessionManager::$LONG_TIMEOUT - 10, + $this->session['expires_on'] + ); + $this->assertLessThanOrEqual( + time() + SessionManager::$LONG_TIMEOUT, + $this->session['expires_on'] + ); + } + + /** + * Unset session variables after logging out + */ + public function testLogout() + { + $this->session = [ + 'ip' => 'ip_id', + 'expires_on' => time() + 1000, + 'username' => 'johndoe', + 'visibility' => 'public', + 'untaggedonly' => false, + ]; + $this->sessionManager->logout(); + + $this->assertFalse(isset($this->session['ip'])); + $this->assertFalse(isset($this->session['expires_on'])); + $this->assertFalse(isset($this->session['username'])); + $this->assertFalse(isset($this->session['visibility'])); + $this->assertFalse(isset($this->session['untaggedonly'])); + } + + /** + * The session is active and expiration time has been reached + */ + public function testHasExpiredTimeElapsed() + { + $this->session['expires_on'] = time() - 10; + + $this->assertTrue($this->sessionManager->hasSessionExpired()); + } + + /** + * The session is active and expiration time has not been reached + */ + public function testHasNotExpired() + { + $this->session['expires_on'] = time() + 1000; + + $this->assertFalse($this->sessionManager->hasSessionExpired()); + } + + /** + * Session hijacking protection is disabled, we assume the IP has not changed + */ + public function testHasClientIpChangedNoSessionProtection() + { + $this->conf->set('security.session_protection_disabled', true); + + $this->assertFalse($this->sessionManager->hasClientIpChanged('')); + } + + /** + * The client IP identifier has not changed + */ + public function testHasClientIpChangedNope() + { + $this->session['ip'] = 'ip_id'; + $this->assertFalse($this->sessionManager->hasClientIpChanged('ip_id')); + } + + /** + * The client IP identifier has changed + */ + public function testHasClientIpChanged() + { + $this->session['ip'] = 'ip_id_one'; + $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two')); + } +} diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php index 85434de..360b34a 100644 --- a/tests/utils/FakeConfigManager.php +++ b/tests/utils/FakeConfigManager.php @@ -42,4 +42,16 @@ class FakeConfigManager } return $key; } + + /** + * Check if a setting exists + * + * @param string $setting Asked setting, keys separated with dots + * + * @return bool true if the setting exists, false otherwise + */ + public function exists($setting) + { + return array_key_exists($setting, $this->values); + } } diff --git a/tpl/default/404.html b/tpl/default/404.html index 2de6b6d..fd337ca 100644 --- a/tpl/default/404.html +++ b/tpl/default/404.html @@ -6,7 +6,7 @@