Merge remote-tracking branch 'upstream/master'
15
.dev/.sasslintrc
Normal file
|
@ -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
|
|
@ -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]
|
||||
|
|
1
.gitattributes
vendored
|
@ -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
|
||||
|
|
42
.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
|
||||
|
|
8
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
|
||||
|
|
|
@ -415,6 +415,37 @@ function getIpAddressFromProxy($server, $trustedIps)
|
|||
return array_pop($ips);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return an identifier based on the advertised client IP address(es)
|
||||
*
|
||||
* This aims at preventing session hijacking from users behind the same proxy
|
||||
* by relying on HTTP headers.
|
||||
*
|
||||
* See:
|
||||
* - https://secure.php.net/manual/en/reserved.variables.server.php
|
||||
* - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
|
||||
* - https://stackoverflow.com/questions/12233406/preventing-session-hijacking
|
||||
* - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor
|
||||
*
|
||||
* @param array $server The $_SERVER array
|
||||
*
|
||||
* @return string An identifier based on client IP address information
|
||||
*/
|
||||
function client_ip_id($server)
|
||||
{
|
||||
$ip = $server['REMOTE_ADDR'];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if Shaarli's currently browsed in HTTPS.
|
||||
* Supports reverse proxies (if the headers are correctly set).
|
||||
|
|
|
@ -177,6 +177,7 @@ public static function getAvailableLanguages()
|
|||
'auto' => t('Automatic'),
|
||||
'en' => t('English'),
|
||||
'fr' => t('French'),
|
||||
'de' => t('German'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -436,15 +436,17 @@ public function filterSearch($filterRequest = array(), $casesensitive = false, $
|
|||
|
||||
/**
|
||||
* 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 @@ public function linksCountPerTag($filteringTags = [], $visibility = 'all')
|
|||
$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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
<?php
|
||||
namespace Shaarli;
|
||||
|
||||
/**
|
||||
* User login management
|
||||
*/
|
||||
class LoginManager
|
||||
{
|
||||
protected $globals = [];
|
||||
protected $configManager = null;
|
||||
protected $banFile = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $globals The $GLOBALS array (reference)
|
||||
* @param ConfigManager $configManager Configuration Manager instance.
|
||||
*/
|
||||
public function __construct(& $globals, $configManager)
|
||||
{
|
||||
$this->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,
|
||||
"<?php\n\$GLOBALS['IPBANS']=" . var_export($this->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;
|
||||
}
|
||||
}
|
|
@ -26,6 +26,9 @@ class PageBuilder
|
|||
*/
|
||||
protected $linkDB;
|
||||
|
||||
/** @var bool $isLoggedIn Whether the user is logged in **/
|
||||
protected $isLoggedIn = false;
|
||||
|
||||
/**
|
||||
* PageBuilder constructor.
|
||||
* $tpl is initialized at false for lazy loading.
|
||||
|
@ -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 @@ private function initialize()
|
|||
$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 @@ private function initialize()
|
|||
$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'])) {
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
<?php
|
||||
namespace Shaarli;
|
||||
|
||||
/**
|
||||
* Manages the server-side session
|
||||
*/
|
||||
class SessionManager
|
||||
{
|
||||
protected $session = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $session The $_SESSION array (reference)
|
||||
* @param ConfigManager $conf ConfigManager instance
|
||||
*/
|
||||
public function __construct(& $session, $conf)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
265
application/security/LoginManager.php
Normal file
|
@ -0,0 +1,265 @@
|
|||
<?php
|
||||
namespace Shaarli\Security;
|
||||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
|
||||
/**
|
||||
* User login management
|
||||
*/
|
||||
class LoginManager
|
||||
{
|
||||
/** @var string Name of the cookie set after logging in **/
|
||||
public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
|
||||
|
||||
/** @var array A reference to the $_GLOBALS array */
|
||||
protected $globals = [];
|
||||
|
||||
/** @var ConfigManager Configuration Manager instance **/
|
||||
protected $configManager = null;
|
||||
|
||||
/** @var SessionManager Session Manager instance **/
|
||||
protected $sessionManager = null;
|
||||
|
||||
/** @var string Path to the file containing IP bans */
|
||||
protected $banFile = '';
|
||||
|
||||
/** @var bool Whether the user is logged in **/
|
||||
protected $isLoggedIn = false;
|
||||
|
||||
/** @var bool Whether the Shaarli instance is open to public edition **/
|
||||
protected $openShaarli = false;
|
||||
|
||||
/** @var string User sign-in token depending on remote IP and credentials */
|
||||
protected $staySignedInToken = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $globals The $GLOBALS array (reference)
|
||||
* @param ConfigManager $configManager Configuration Manager instance
|
||||
* @param SessionManager $sessionManager SessionManager instance
|
||||
*/
|
||||
public function __construct(& $globals, $configManager, $sessionManager)
|
||||
{
|
||||
$this->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,
|
||||
"<?php\n\$GLOBALS['IPBANS']=" . var_export($this->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;
|
||||
}
|
||||
}
|
199
application/security/SessionManager.php
Normal file
|
@ -0,0 +1,199 @@
|
|||
<?php
|
||||
namespace Shaarli\Security;
|
||||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
|
||||
/**
|
||||
* Manages the server-side session
|
||||
*/
|
||||
class SessionManager
|
||||
{
|
||||
/** @var int Session expiration timeout, in seconds */
|
||||
public static $SHORT_TIMEOUT = 3600; // 1 hour
|
||||
|
||||
/** @var int Session expiration timeout, in seconds */
|
||||
public static $LONG_TIMEOUT = 31536000; // 1 year
|
||||
|
||||
/** @var array Local reference to the global $_SESSION array */
|
||||
protected $session = [];
|
||||
|
||||
/** @var ConfigManager Configuration Manager instance **/
|
||||
protected $conf = null;
|
||||
|
||||
/** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
|
||||
protected $staySignedIn = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $session The $_SESSION array (reference)
|
||||
* @param ConfigManager $conf ConfigManager instance
|
||||
*/
|
||||
public function __construct(& $session, $conf)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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._
|
88
doc/md/Sharing-content.md
Normal file
|
@ -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`.
|
||||
|
||||
<!-- TODO Add screenshot of add/edit link dialog -->
|
||||
|
||||
### 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.
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 757 B After Width: | Height: | Size: 715 B |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -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
|
||||
|
|
1313
inc/languages/de/LC_MESSAGES/shaarli.po
Normal file
|
@ -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 :"
|
||||
|
|
242
index.php
|
@ -78,8 +78,8 @@
|
|||
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 @@
|
|||
// 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 @@
|
|||
}
|
||||
|
||||
$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 @@
|
|||
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 @@ function logout() {
|
|||
* 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($a, $b) { return $a['order'] - $b['order']; }
|
|||
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($a, $b) { return $a['order'] - $b['order']; }
|
|||
}
|
||||
|
||||
// -------- Otherwise, simply display search form and links:
|
||||
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
|
||||
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
@ -1592,8 +1499,9 @@ function($a, $b) { return $a['order'] - $b['order']; }
|
|||
* @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 @@ function resizeImage($filepath)
|
|||
|
||||
$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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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('@— <a href="([^"]+)" title="[^"]+">(\w+)</a>$@im', '— [$2]($1)', $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace not whitelisted protocols with http:// in given description.
|
||||
*
|
||||
|
|
52
tests/HttpUtils/ClientIpIdTest.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
/**
|
||||
* HttpUtils' tests
|
||||
*/
|
||||
|
||||
require_once 'application/HttpUtils.php';
|
||||
|
||||
/**
|
||||
* Unitary tests for client_ip_id()
|
||||
*/
|
||||
class ClientIpIdTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* Get a remote client ID based on its IP
|
||||
*/
|
||||
public function testClientIpIdRemote()
|
||||
{
|
||||
$this->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'
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -542,4 +542,104 @@ public function testDeleteTag()
|
|||
$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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
<?php
|
||||
require_once 'tests/utils/FakeConfigManager.php';
|
||||
|
||||
// Initialize reference data _before_ PHPUnit starts a session
|
||||
require_once 'tests/utils/ReferenceSessionIdHashes.php';
|
||||
ReferenceSessionIdHashes::genAllHashes();
|
||||
|
||||
use \Shaarli\SessionManager;
|
||||
use \PHPUnit\Framework\TestCase;
|
||||
|
||||
|
||||
/**
|
||||
* Test coverage for SessionManager
|
||||
*/
|
||||
class SessionManagerTest extends TestCase
|
||||
{
|
||||
// Session ID hashes
|
||||
protected static $sidHashes = null;
|
||||
|
||||
// Fake ConfigManager
|
||||
protected static $conf = null;
|
||||
|
||||
/**
|
||||
* Assign reference data
|
||||
*/
|
||||
public static function setUpBeforeClass()
|
||||
{
|
||||
self::$sidHashes = ReferenceSessionIdHashes::getHashes();
|
||||
self::$conf = new FakeConfigManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a session token
|
||||
*/
|
||||
public function testGenerateToken()
|
||||
{
|
||||
$session = [];
|
||||
$sessionManager = new SessionManager($session, self::$conf);
|
||||
|
||||
$token = $sessionManager->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=')
|
||||
);
|
||||
}
|
||||
}
|
|
@ -49,6 +49,30 @@ public function testMarkdownLinklist()
|
|||
$this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test render_feed hook.
|
||||
*/
|
||||
public function testMarkdownFeed()
|
||||
{
|
||||
$markdown = '# My title' . PHP_EOL . 'Very interesting content.';
|
||||
$markdown .= '— <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
|
||||
$data = array(
|
||||
'links' => array(
|
||||
0 => array(
|
||||
'description' => $markdown,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$data = hook_markdown_render_feed($data, $this->conf);
|
||||
$this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
|
||||
$this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
|
||||
$this->assertStringEndsWith(
|
||||
'— <a href="http://domain.tld/?0oc_VQ">Permalien</a></p></div>',
|
||||
$data['links'][0]['description']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test render_daily hook.
|
||||
* Only check that there is basic markdown rendering.
|
||||
|
@ -104,6 +128,37 @@ public function testReverseSpace2nbsp()
|
|||
$this->assertEquals($text, $reversedText);
|
||||
}
|
||||
|
||||
public function testReverseFeedPermalink()
|
||||
{
|
||||
$text = 'Description... ';
|
||||
$text .= '— <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
|
||||
$expected = 'Description... — [Permalien](http://domain.tld/?0oc_VQ)';
|
||||
$processedText = reverse_feed_permalink($text);
|
||||
|
||||
$this->assertEquals($expected, $processedText);
|
||||
}
|
||||
|
||||
public function testReverseLastFeedPermalink()
|
||||
{
|
||||
$text = 'Description... ';
|
||||
$text .= '<br>— <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
|
||||
$expected = $text;
|
||||
$text .= '<br>— <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
|
||||
$expected .= '<br>— [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().
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?php
|
||||
namespace Shaarli;
|
||||
namespace Shaarli\Security;
|
||||
|
||||
require_once 'tests/utils/FakeConfigManager.php';
|
||||
use \PHPUnit\Framework\TestCase;
|
||||
|
@ -9,15 +9,54 @@
|
|||
*/
|
||||
class LoginManagerTest extends TestCase
|
||||
{
|
||||
/** @var \FakeConfigManager Configuration Manager instance */
|
||||
protected $configManager = null;
|
||||
|
||||
/** @var LoginManager Login Manager instance */
|
||||
protected $loginManager = null;
|
||||
|
||||
/** @var SessionManager Session Manager instance */
|
||||
protected $sessionManager = null;
|
||||
|
||||
/** @var string Banned IP filename */
|
||||
protected $banFile = 'sandbox/ipbans.php';
|
||||
|
||||
/** @var string Log filename */
|
||||
protected $logFile = 'sandbox/shaarli.log';
|
||||
|
||||
/** @var array Simulates the $_COOKIE array */
|
||||
protected $cookie = [];
|
||||
|
||||
/** @var array Simulates the $GLOBALS array */
|
||||
protected $globals = [];
|
||||
protected $ipAddr = '127.0.0.1';
|
||||
|
||||
/** @var array Simulates the $_SERVER array */
|
||||
protected $server = [];
|
||||
|
||||
/** @var array Simulates the $_SESSION array */
|
||||
protected $session = [];
|
||||
|
||||
/** @var string Advertised client IP address */
|
||||
protected $clientIpAddress = '10.1.47.179';
|
||||
|
||||
/** @var string Local client IP address */
|
||||
protected $ipAddr = '127.0.0.1';
|
||||
|
||||
/** @var string Trusted proxy IP address */
|
||||
protected $trustedProxy = '10.1.1.100';
|
||||
|
||||
/** @var string User login */
|
||||
protected $login = 'johndoe';
|
||||
|
||||
/** @var string User password */
|
||||
protected $password = 'IC4nHazL0g1n?';
|
||||
|
||||
/** @var string Hash of the salted user password */
|
||||
protected $passwordHash = '';
|
||||
|
||||
/** @var string Salt used by hash functions */
|
||||
protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
|
||||
|
||||
/**
|
||||
* Prepare or reset test resources
|
||||
*/
|
||||
|
@ -27,7 +66,12 @@ public function setUp()
|
|||
unlink($this->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 @@ public function setUp()
|
|||
'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 @@ public function testReadBanFile()
|
|||
$this->banFile,
|
||||
"<?php\n\$GLOBALS['IPBANS']=array('FAILURES' => 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 @@ public function testCanLoginIpBanExpired()
|
|||
$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)
|
||||
);
|
||||
}
|
||||
}
|
273
tests/security/SessionManagerTest.php
Normal file
|
@ -0,0 +1,273 @@
|
|||
<?php
|
||||
require_once 'tests/utils/FakeConfigManager.php';
|
||||
|
||||
// Initialize reference data _before_ PHPUnit starts a session
|
||||
require_once 'tests/utils/ReferenceSessionIdHashes.php';
|
||||
ReferenceSessionIdHashes::genAllHashes();
|
||||
|
||||
use \Shaarli\Security\SessionManager;
|
||||
use \PHPUnit\Framework\TestCase;
|
||||
|
||||
|
||||
/**
|
||||
* Test coverage for SessionManager
|
||||
*/
|
||||
class SessionManagerTest extends TestCase
|
||||
{
|
||||
/** @var array Session ID hashes */
|
||||
protected static $sidHashes = null;
|
||||
|
||||
/** @var \FakeConfigManager ConfigManager substitute for testing */
|
||||
protected $conf = null;
|
||||
|
||||
/** @var array $_SESSION array for testing */
|
||||
protected $session = [];
|
||||
|
||||
/** @var SessionManager Server-side session management abstraction */
|
||||
protected $sessionManager = null;
|
||||
|
||||
/**
|
||||
* Assign reference data
|
||||
*/
|
||||
public static function setUpBeforeClass()
|
||||
{
|
||||
self::$sidHashes = ReferenceSessionIdHashes::getHashes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize or reset test resources
|
||||
*/
|
||||
public function setUp()
|
||||
{
|
||||
$this->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'));
|
||||
}
|
||||
}
|
|
@ -42,4 +42,16 @@ public function get($key)
|
|||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<body>
|
||||
<div id="pageheader">
|
||||
{include="page.header"}
|
||||
<div class="center" id="page404">
|
||||
<div class="center" id="page404" class="page404-container">
|
||||
<h2>{'Sorry, nothing to see here.'|t}</h2>
|
||||
<img src="img/sad_star.png">
|
||||
<p>{$error_message}</p>
|
||||
|
|
|
@ -5,12 +5,11 @@
|
|||
</head>
|
||||
<body>
|
||||
{include="page.header"}
|
||||
<div id="editlinkform" class="pure-g">
|
||||
<div id="editlinkform" class="edit-link-container" class="pure-g">
|
||||
<div class="pure-u-lg-1-5 pure-u-1-24"></div>
|
||||
<form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light">
|
||||
<h2 class="window-title">
|
||||
{if="!$link_is_new"}{'Edit'|t}{/if}
|
||||
{'Shaare'|t}
|
||||
{if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
|
||||
</h2>
|
||||
<input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
|
||||
{if="isset($link.id)"}
|
||||
|
|
|
@ -15,7 +15,7 @@ <h2 class="window-title">{"Import Database"|t}</h2>
|
|||
</div>
|
||||
|
||||
<input type="hidden" name="token" value="{$token}">
|
||||
<div class="center" id="import-field">
|
||||
<div class="center import-field-container" id="import-field">
|
||||
<input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
|
||||
<input type="file" name="filetoupload">
|
||||
<p><br>{'Maximum size allowed:'|t} <strong>{$maxfilesizeHuman}</strong></p>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
|
||||
<input type="hidden" name="token" value="{$token}">
|
||||
<div id="search-linklist">
|
||||
<div id="search-linklist" class="searchform-block search-linklist">
|
||||
|
||||
<form method="GET" class="pure-form searchform" name="searchform">
|
||||
<input type="text" tabindex="1" name="searchterm" class="searchterm" placeholder="{'Search text'|t}"
|
||||
|
@ -136,7 +136,7 @@
|
|||
<div class="linklist-item-thumbnail">{$thumb}</div>
|
||||
{/if}
|
||||
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<div class="linklist-item-editbuttons">
|
||||
{if="$value.private"}
|
||||
<span class="label label-private">{$strPrivate}</span>
|
||||
|
@ -179,7 +179,7 @@ <h2>
|
|||
|
||||
<div class="linklist-item-infos-date-url-block pure-g">
|
||||
<div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1">
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible">
|
||||
<span class="linklist-item-infos-controls-item ctrl-checkbox">
|
||||
<input type="checkbox" class="delete-checkbox" value="{$value.id}">
|
||||
|
@ -196,7 +196,7 @@ <h2>
|
|||
</div>
|
||||
{/if}
|
||||
<a href="?{$value.shorturl}" title="{$strPermalink}">
|
||||
{if="!$hide_timestamps || isLoggedIn()"}
|
||||
{if="!$hide_timestamps || $is_logged_in"}
|
||||
{$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
|
||||
<span class="linkdate" title="{$updated}">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
|
@ -236,7 +236,7 @@ <h2>
|
|||
{if="$link_plugin_counter - 1 != $counter"}·{/if}
|
||||
{/loop}
|
||||
{/if}
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
·
|
||||
<a href="?delete_link&lf_linkdate={$value.id}&token={$token}"
|
||||
title="{$strDelete}" class="delete-link confirm-delete">
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<div class="linklist-paging">
|
||||
<div class="paging pure-g">
|
||||
<div class="linklist-filters pure-u-1-3">
|
||||
{if="isLoggedIn() or !empty($action_plugin)"}
|
||||
{if="$is_logged_in or !empty($action_plugin)"}
|
||||
<span class="linklist-filters-text pure-u-0 pure-u-lg-visible">
|
||||
{'Filters'|t}
|
||||
</span>
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<a href="?visibility=private" title="{'Only display private links'|t}"
|
||||
class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}"
|
||||
><i class="fa fa-user-secret"></i></a>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
{else}
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
|
||||
<div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
|
||||
<div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24 login-form-container">
|
||||
<form method="post" name="loginform">
|
||||
<h2 class="window-title">{'Login'|t}</h2>
|
||||
<div>
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-2-24"></div>
|
||||
<div id="footer" class="pure-u-20-24">
|
||||
<div id="footer" class="pure-u-20-24 footer-container">
|
||||
<strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong>
|
||||
{if="isLoggedIn()===true"}
|
||||
{if="$is_logged_in===true"}
|
||||
{$version}
|
||||
{/if}
|
||||
·
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
{$shaarlititle}
|
||||
</a>
|
||||
</li>
|
||||
{if="isLoggedIn() || $openshaarli"}
|
||||
{if="$is_logged_in || $openshaarli"}
|
||||
<li class="pure-menu-item">
|
||||
<a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare">
|
||||
<i class="fa fa-plus" ></i> {'Shaare'|t}
|
||||
|
@ -50,7 +50,7 @@
|
|||
<li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
|
||||
<a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
|
||||
</li>
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
|
||||
<a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a>
|
||||
</li>
|
||||
|
@ -74,7 +74,7 @@
|
|||
<i class="fa fa-rss"></i>
|
||||
</a>
|
||||
</li>
|
||||
{if="!isLoggedIn()"}
|
||||
{if="!$is_logged_in"}
|
||||
<li class="pure-menu-item" id="shaarli-menu-desktop-login">
|
||||
<a href="?do=login" class="pure-menu-link"
|
||||
data-open-id="header-login-form"
|
||||
|
@ -95,8 +95,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<div id="search" class="subheader-form">
|
||||
<div id="content" class="container">
|
||||
<div id="search" class="subheader-form searchform-block header-search">
|
||||
<form method="GET" class="pure-form searchform" name="searchform">
|
||||
<input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="{'Search text'|t}"
|
||||
{if="!empty($search_term)"}
|
||||
|
@ -120,9 +120,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{if="!isLoggedIn()"}
|
||||
{if="!$is_logged_in"}
|
||||
<form method="post" name="loginform">
|
||||
<div class="subheader-form" id="header-login-form">
|
||||
<div class="subheader-form header-login-form" id="header-login-form">
|
||||
<input type="text" name="login" placeholder="{'Username'|t}" tabindex="3">
|
||||
<input type="password" name="password" placeholder="{'Password'|t}" tabindex="5">
|
||||
<div class="remember-me">
|
||||
|
@ -155,7 +155,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{if="!empty($plugin_errors) && isLoggedIn()"}
|
||||
{if="!empty($plugin_errors) && $is_logged_in"}
|
||||
<div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
|
||||
<div class="pure-u-2-24"></div>
|
||||
<div class="pure-u-20-24">
|
||||
|
|
|
@ -18,9 +18,9 @@ <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
|
|||
{/loop}
|
||||
</div>
|
||||
|
||||
<div id="picwall_container">
|
||||
<div id="picwall_container" class="picwall-container">
|
||||
{loop="$linksToDisplay"}
|
||||
<div class="picwall_pictureframe">
|
||||
<div class="picwall-pictureframe">
|
||||
{$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
|
||||
{loop="$value.picwall_plugin"}
|
||||
{$value}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="clear"></div>
|
||||
</noscript>
|
||||
|
||||
<form method="POST" action="?do=save_pluginadmin" name="pluginform" id="pluginform">
|
||||
<form method="POST" action="?do=save_pluginadmin" name="pluginform" id="pluginform" class="pluginform-container">
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-lg-1-8 pure-u-1-24"></div>
|
||||
<div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
|
||||
|
|
|
@ -45,7 +45,7 @@ <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
|
|||
{/loop}
|
||||
</div>
|
||||
|
||||
<div id="cloudtag">
|
||||
<div id="cloudtag" class="cloudtag-container">
|
||||
{loop="tags"}
|
||||
<a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a
|
||||
><a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
|
||||
|
|
|
@ -21,7 +21,7 @@ <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
|
|||
</p>
|
||||
{/if}
|
||||
|
||||
<div id="search-tagcloud" class="pure-g">
|
||||
<div id="search-tagcloud" class="pure-g searchform-block search-tagcloud">
|
||||
<div class="pure-u-lg-1-4"></div>
|
||||
<div class="pure-u-1 pure-u-lg-1-2">
|
||||
<form method="GET">
|
||||
|
@ -45,11 +45,11 @@ <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
|
|||
{/loop}
|
||||
</div>
|
||||
|
||||
<div id="taglist">
|
||||
<div id="taglist" class="taglist-container">
|
||||
{loop="tags"}
|
||||
<div class="tag-list-item pure-g" data-tag="{$key}">
|
||||
<div class="pure-u-1">
|
||||
{if="isLoggedIn()===true"}
|
||||
{if="$is_logged_in===true"}
|
||||
<a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>
|
||||
<a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
|
||||
<i class="fa fa-pencil-square-o {$key}"></i>
|
||||
|
@ -63,7 +63,7 @@ <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
|
|||
{$value}
|
||||
{/loop}
|
||||
</div>
|
||||
{if="isLoggedIn()===true"}
|
||||
{if="$is_logged_in===true"}
|
||||
<div class="rename-tag-form pure-u-1">
|
||||
<input type="text" name="{$key}" value="{$key}" class="rename-tag-input" />
|
||||
<a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a>
|
||||
|
@ -81,7 +81,7 @@ <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{if="isLoggedIn()===true"}
|
||||
{if="$is_logged_in===true"}
|
||||
<input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -146,8 +146,14 @@ <h2 class="window-title">{'3rd party'|t}</h2>
|
|||
</div>
|
||||
<div class="tools-item">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.dimtion.shaarlier&hl=fr"
|
||||
title="Android">
|
||||
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Android</span>
|
||||
title="Android Shaarlier">
|
||||
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Android Shaarlier</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="tools-item">
|
||||
<a href="https://stakali.toneiv.eu/"
|
||||
title="Android Stakali">
|
||||
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Android Stakali</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="tools-item">
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink">
|
||||
</a>
|
||||
</div>
|
||||
{if="!$hide_timestamps || isLoggedIn()"}
|
||||
{if="!$hide_timestamps || $is_logged_in"}
|
||||
<div class="dailyEntryLinkdate">
|
||||
<a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
|
||||
</div>
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
<a id="{$value.shorturl}"></a>
|
||||
<div class="thumbnail">{$value.url|thumbnail}</div>
|
||||
<div class="linkcontainer">
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<div class="linkeditbuttons">
|
||||
<form method="GET" class="buttoneditform">
|
||||
<input type="hidden" name="edit_link" value="{$value.id}">
|
||||
|
@ -102,7 +102,7 @@
|
|||
</span>
|
||||
<br>
|
||||
{if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if}
|
||||
{if="!$hide_timestamps || isLoggedIn()"}
|
||||
{if="!$hide_timestamps || $is_logged_in"}
|
||||
{$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
|
||||
<span class="linkdate" title="Permalink">
|
||||
<a href="?{$value.shorturl}">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="paging">
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<div class="paging_privatelinks">
|
||||
<a href="?visibility=private">
|
||||
{if="$visibility=='private'"}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
<script src="js/shaarli.min.js"></script>
|
||||
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
{ignore} When called as a popup from bookmarklet, do not display menu. {/ignore}
|
||||
{else}
|
||||
<li><a href="{$titleLink}" class="nomobile">Home</a></li>
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<li><a href="?do=logout">Logout</a></li>
|
||||
<li><a href="?do=tools">Tools</a></li>
|
||||
<li><a href="?do=addlink">Add link</a></li>
|
||||
|
@ -46,7 +46,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
{if="!empty($plugin_errors) && isLoggedIn()"}
|
||||
{if="!empty($plugin_errors) && $is_logged_in"}
|
||||
<ul class="errors">
|
||||
{loop="$plugin_errors"}
|
||||
<li>{$value}</li>
|
||||
|
|
298
yarn.lock
|
@ -30,11 +30,19 @@ acorn@^5.0.0, acorn@^5.4.0:
|
|||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
|
||||
|
||||
acorn@^5.5.0:
|
||||
version "5.5.3"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
|
||||
|
||||
ajv-keywords@^1.0.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
|
||||
|
||||
ajv-keywords@^2.0.0, ajv-keywords@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
|
||||
|
||||
ajv@^4.9.1:
|
||||
ajv@^4.7.0, ajv@^4.9.1:
|
||||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
|
||||
dependencies:
|
||||
|
@ -66,6 +74,10 @@ amdefine@>=0.0.4:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
||||
|
||||
ansi-escapes@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
|
||||
|
||||
ansi-escapes@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
|
||||
|
@ -1001,6 +1013,10 @@ browserslist@^2.1.2:
|
|||
caniuse-lite "^1.0.30000792"
|
||||
electron-to-chromium "^1.3.30"
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
|
||||
|
||||
buffer-xor@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
|
||||
|
@ -1086,7 +1102,7 @@ center-align@^0.1.1:
|
|||
align-text "^0.1.3"
|
||||
lazy-cache "^1.0.3"
|
||||
|
||||
chalk@^1.1.1, chalk@^1.1.3:
|
||||
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
dependencies:
|
||||
|
@ -1140,6 +1156,12 @@ clap@^1.0.9:
|
|||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
|
||||
cli-cursor@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
|
||||
dependencies:
|
||||
restore-cursor "^1.0.1"
|
||||
|
||||
cli-cursor@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
|
||||
|
@ -1235,6 +1257,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
|
|||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@^2.8.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
|
||||
|
||||
commander@^2.9.0:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
|
||||
|
@ -1247,6 +1273,15 @@ concat-map@0.0.1:
|
|||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
||||
concat-stream@^1.4.6:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.2.2"
|
||||
typedarray "^0.0.6"
|
||||
|
||||
concat-stream@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
|
||||
|
@ -1456,7 +1491,7 @@ date-now@^0.1.4:
|
|||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
||||
|
||||
debug@^2.2.0, debug@^2.6.8, debug@^2.6.9:
|
||||
debug@^2.1.1, debug@^2.2.0, debug@^2.6.8, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
dependencies:
|
||||
|
@ -1529,7 +1564,7 @@ diffie-hellman@^5.0.0:
|
|||
miller-rabin "^4.0.0"
|
||||
randombytes "^2.0.0"
|
||||
|
||||
doctrine@1.5.0:
|
||||
doctrine@1.5.0, doctrine@^1.2.2:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
|
||||
dependencies:
|
||||
|
@ -1708,6 +1743,44 @@ eslint-visitor-keys@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
|
||||
|
||||
eslint@^2.7.0:
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11"
|
||||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
concat-stream "^1.4.6"
|
||||
debug "^2.1.1"
|
||||
doctrine "^1.2.2"
|
||||
es6-map "^0.1.3"
|
||||
escope "^3.6.0"
|
||||
espree "^3.1.6"
|
||||
estraverse "^4.2.0"
|
||||
esutils "^2.0.2"
|
||||
file-entry-cache "^1.1.1"
|
||||
glob "^7.0.3"
|
||||
globals "^9.2.0"
|
||||
ignore "^3.1.2"
|
||||
imurmurhash "^0.1.4"
|
||||
inquirer "^0.12.0"
|
||||
is-my-json-valid "^2.10.0"
|
||||
is-resolvable "^1.0.0"
|
||||
js-yaml "^3.5.1"
|
||||
json-stable-stringify "^1.0.0"
|
||||
levn "^0.3.0"
|
||||
lodash "^4.0.0"
|
||||
mkdirp "^0.5.0"
|
||||
optionator "^0.8.1"
|
||||
path-is-absolute "^1.0.0"
|
||||
path-is-inside "^1.0.1"
|
||||
pluralize "^1.2.1"
|
||||
progress "^1.1.8"
|
||||
require-uncached "^1.0.2"
|
||||
shelljs "^0.6.0"
|
||||
strip-json-comments "~1.0.1"
|
||||
table "^3.7.8"
|
||||
text-table "~0.2.0"
|
||||
user-home "^2.0.0"
|
||||
|
||||
eslint@^4.16.0:
|
||||
version "4.17.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.17.0.tgz#dc24bb51ede48df629be7031c71d9dc0ee4f3ddf"
|
||||
|
@ -1750,6 +1823,13 @@ eslint@^4.16.0:
|
|||
table "^4.0.1"
|
||||
text-table "~0.2.0"
|
||||
|
||||
espree@^3.1.6:
|
||||
version "3.5.4"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
|
||||
dependencies:
|
||||
acorn "^5.5.0"
|
||||
acorn-jsx "^3.0.0"
|
||||
|
||||
espree@^3.5.2:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.3.tgz#931e0af64e7fbbed26b050a29daad1fc64799fa6"
|
||||
|
@ -1778,7 +1858,7 @@ esrecurse@^4.1.0:
|
|||
estraverse "^4.1.0"
|
||||
object-assign "^4.0.1"
|
||||
|
||||
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
|
||||
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
|
||||
|
||||
|
@ -1816,6 +1896,10 @@ execa@^0.7.0:
|
|||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
exit-hook@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
|
||||
|
||||
expand-brackets@^0.1.4:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
|
||||
|
@ -1879,12 +1963,26 @@ fastparse@^1.1.1:
|
|||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
|
||||
|
||||
figures@^1.3.5:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
|
||||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
figures@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
|
||||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
|
||||
file-entry-cache@^1.1.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-1.3.1.tgz#44c61ea607ae4be9c1402f41f44270cbfe334ff8"
|
||||
dependencies:
|
||||
flat-cache "^1.2.1"
|
||||
object-assign "^4.0.1"
|
||||
|
||||
file-entry-cache@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
|
||||
|
@ -1991,6 +2089,20 @@ form-data@~2.3.1:
|
|||
combined-stream "^1.0.5"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
front-matter@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-2.1.2.tgz#f75983b9f2f413be658c93dfd7bd8ce4078f5cdb"
|
||||
dependencies:
|
||||
js-yaml "^3.4.6"
|
||||
|
||||
fs-extra@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
jsonfile "^3.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
|
@ -2112,7 +2224,7 @@ globals@^11.0.1:
|
|||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0"
|
||||
|
||||
globals@^9.18.0:
|
||||
globals@^9.18.0, globals@^9.2.0:
|
||||
version "9.18.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
||||
|
||||
|
@ -2135,7 +2247,13 @@ globule@^1.0.0:
|
|||
lodash "~4.17.4"
|
||||
minimatch "~3.0.2"
|
||||
|
||||
graceful-fs@^4.1.2:
|
||||
gonzales-pe-sl@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz#6a868bc380645f141feeb042c6f97fcc71b59fe6"
|
||||
dependencies:
|
||||
minimist "1.1.x"
|
||||
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.1.6:
|
||||
version "4.1.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
|
||||
|
||||
|
@ -2301,7 +2419,7 @@ ieee754@^1.1.4:
|
|||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
|
||||
|
||||
ignore@^3.3.3:
|
||||
ignore@^3.1.2, ignore@^3.3.3:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
|
||||
|
||||
|
@ -2346,6 +2464,24 @@ ini@~1.3.0:
|
|||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
|
||||
inquirer@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
|
||||
dependencies:
|
||||
ansi-escapes "^1.1.0"
|
||||
ansi-regex "^2.0.0"
|
||||
chalk "^1.0.0"
|
||||
cli-cursor "^1.0.1"
|
||||
cli-width "^2.0.0"
|
||||
figures "^1.3.5"
|
||||
lodash "^4.3.0"
|
||||
readline2 "^1.0.1"
|
||||
run-async "^0.1.0"
|
||||
rx-lite "^3.1.2"
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@^3.0.6:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
|
||||
|
@ -2443,6 +2579,20 @@ is-glob@^2.0.0, is-glob@^2.0.1:
|
|||
dependencies:
|
||||
is-extglob "^1.0.0"
|
||||
|
||||
is-my-ip-valid@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
|
||||
|
||||
is-my-json-valid@^2.10.0:
|
||||
version "2.17.2"
|
||||
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
|
||||
dependencies:
|
||||
generate-function "^2.0.0"
|
||||
generate-object-property "^1.1.0"
|
||||
is-my-ip-valid "^1.0.0"
|
||||
jsonpointer "^4.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
is-my-json-valid@^2.12.4:
|
||||
version "2.17.1"
|
||||
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz#3da98914a70a22f0a8563ef1511a246c6fc55471"
|
||||
|
@ -2558,6 +2708,13 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
|
|||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
|
||||
js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4:
|
||||
version "3.11.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js-yaml@^3.9.1:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
|
||||
|
@ -2600,7 +2757,7 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
|
||||
json-stable-stringify@^1.0.1:
|
||||
json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
|
||||
dependencies:
|
||||
|
@ -2614,6 +2771,12 @@ json5@^0.5.0, json5@^0.5.1:
|
|||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
|
||||
|
||||
jsonfile@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonify@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||
|
@ -2649,6 +2812,10 @@ kind-of@^4.0.0:
|
|||
dependencies:
|
||||
is-buffer "^1.1.5"
|
||||
|
||||
known-css-properties@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4"
|
||||
|
||||
lazy-cache@^0.2.3:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
|
||||
|
@ -2716,6 +2883,10 @@ lodash.camelcase@^4.3.0:
|
|||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
|
||||
lodash.capitalize@^4.1.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9"
|
||||
|
||||
lodash.clonedeep@^4.3.2:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
|
@ -2728,6 +2899,10 @@ lodash.isplainobject@^4.0.6:
|
|||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
|
||||
lodash.kebabcase@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
|
@ -2829,6 +3004,10 @@ meow@^3.7.0:
|
|||
redent "^1.0.0"
|
||||
trim-newlines "^1.0.0"
|
||||
|
||||
merge@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
|
||||
|
||||
micromatch@^2.1.5:
|
||||
version "2.3.11"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
|
||||
|
@ -2890,6 +3069,10 @@ minimist@0.0.8:
|
|||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
minimist@1.1.x:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8"
|
||||
|
||||
minimist@^1.1.3, minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
@ -2911,6 +3094,10 @@ ms@2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
mute-stream@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
|
||||
|
||||
mute-stream@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
|
@ -3094,13 +3281,17 @@ once@^1.3.0, once@^1.3.3:
|
|||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
onetime@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
|
||||
|
||||
onetime@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
|
||||
dependencies:
|
||||
mimic-fn "^1.0.0"
|
||||
|
||||
optionator@^0.8.2:
|
||||
optionator@^0.8.1, optionator@^0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
|
||||
dependencies:
|
||||
|
@ -3285,6 +3476,10 @@ pkg-dir@^2.0.0:
|
|||
dependencies:
|
||||
find-up "^2.1.0"
|
||||
|
||||
pluralize@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
|
||||
|
||||
pluralize@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
|
||||
|
@ -3559,6 +3754,10 @@ process@^0.11.10:
|
|||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
|
||||
progress@^1.1.8:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
|
||||
|
||||
progress@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
|
||||
|
@ -3708,6 +3907,14 @@ readdirp@^2.0.0:
|
|||
readable-stream "^2.0.2"
|
||||
set-immediate-shim "^1.0.1"
|
||||
|
||||
readline2@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
|
||||
dependencies:
|
||||
code-point-at "^1.0.0"
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
mute-stream "0.0.5"
|
||||
|
||||
redent@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
|
||||
|
@ -3882,7 +4089,7 @@ require-main-filename@^1.0.1:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
||||
|
||||
require-uncached@^1.0.3:
|
||||
require-uncached@^1.0.2, require-uncached@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
|
||||
dependencies:
|
||||
|
@ -3899,6 +4106,13 @@ resolve@^1.5.0:
|
|||
dependencies:
|
||||
path-parse "^1.0.5"
|
||||
|
||||
restore-cursor@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
|
||||
dependencies:
|
||||
exit-hook "^1.0.0"
|
||||
onetime "^1.0.0"
|
||||
|
||||
restore-cursor@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
|
||||
|
@ -3925,6 +4139,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
|||
hash-base "^2.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
run-async@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
|
||||
run-async@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
||||
|
@ -3941,6 +4161,10 @@ rx-lite@*, rx-lite@^4.0.8:
|
|||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
|
||||
|
||||
rx-lite@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
@ -3954,6 +4178,25 @@ sass-graph@^2.2.4:
|
|||
scss-tokenizer "^0.2.3"
|
||||
yargs "^7.0.0"
|
||||
|
||||
sass-lint@^1.12.1:
|
||||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83"
|
||||
dependencies:
|
||||
commander "^2.8.1"
|
||||
eslint "^2.7.0"
|
||||
front-matter "2.1.2"
|
||||
fs-extra "^3.0.1"
|
||||
glob "^7.0.0"
|
||||
globule "^1.0.0"
|
||||
gonzales-pe-sl "^4.2.3"
|
||||
js-yaml "^3.5.4"
|
||||
known-css-properties "^0.3.0"
|
||||
lodash.capitalize "^4.1.0"
|
||||
lodash.kebabcase "^4.0.0"
|
||||
merge "^1.2.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
util "^0.10.3"
|
||||
|
||||
sass-loader@^6.0.6:
|
||||
version "6.0.6"
|
||||
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9"
|
||||
|
@ -4027,6 +4270,10 @@ shebang-regex@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
||||
|
||||
shelljs@^0.6.0:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8"
|
||||
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
|
@ -4035,6 +4282,10 @@ slash@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
|
||||
|
||||
slice-ansi@0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
|
||||
|
||||
slice-ansi@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
|
||||
|
@ -4199,6 +4450,10 @@ strip-indent@^1.0.1:
|
|||
dependencies:
|
||||
get-stdin "^4.0.1"
|
||||
|
||||
strip-json-comments@~1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
|
@ -4244,6 +4499,17 @@ svgo@^0.7.0:
|
|||
sax "~1.2.1"
|
||||
whet.extend "~0.9.9"
|
||||
|
||||
table@^3.7.8:
|
||||
version "3.8.3"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
|
||||
dependencies:
|
||||
ajv "^4.7.0"
|
||||
ajv-keywords "^1.0.0"
|
||||
chalk "^1.1.1"
|
||||
lodash "^4.0.0"
|
||||
slice-ansi "0.0.4"
|
||||
string-width "^2.0.0"
|
||||
|
||||
table@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
|
||||
|
@ -4395,6 +4661,10 @@ uniqs@^2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
|
||||
|
||||
url-loader@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
|
||||
|
@ -4410,6 +4680,12 @@ url@^0.11.0:
|
|||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
user-home@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
|
||||
dependencies:
|
||||
os-homedir "^1.0.0"
|
||||
|
||||
util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
|