Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Mark Schmitz 2018-06-07 14:23:53 +01:00
commit 0deaedeeae
59 changed files with 4233 additions and 1631 deletions

15
.dev/.sasslintrc Normal file
View 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

View file

@ -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
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,7 +1,7 @@
<?php
/**
* GET an HTTP URL to retrieve its content
* Uses the cURL library or a fallback method
* Uses the cURL library or a fallback method
*
* @param string $url URL to get (http://...)
* @param int $timeout network timeout (in seconds)
@ -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).

View file

@ -177,6 +177,7 @@ class Languages
'auto' => t('Automatic'),
'en' => t('English'),
'fr' => t('French'),
'de' => t('German'),
];
}
}

View file

@ -436,15 +436,17 @@ You use the community supported version of the original Shaarli project, by Seba
/**
* Returns the list tags appearing in the links with the given tags
* @param $filteringTags: tags selecting the links to consider
* @param $visibility: process only all/private/public links
* @return: a tag=>linksCount array
*
* @param array $filteringTags tags selecting the links to consider
* @param string $visibility process only all/private/public links
*
* @return array tag => linksCount
*/
public function linksCountPerTag($filteringTags = [], $visibility = 'all')
{
$links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
$tags = array();
$caseMapping = array();
$links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
$tags = [];
$caseMapping = [];
foreach ($links as $link) {
foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
if (empty($tag)) {
@ -458,8 +460,19 @@ You use the community supported version of the original Shaarli project, by Seba
$tags[$caseMapping[strtolower($tag)]]++;
}
}
// Sort tags by usage (most used tag first)
arsort($tags);
/*
* Formerly used arsort(), which doesn't define the sort behaviour for equal values.
* Also, this function doesn't produce the same result between PHP 5.6 and 7.
*
* So we now use array_multisort() to sort tags by DESC occurrences,
* then ASC alphabetically for equal values.
*
* @see https://github.com/shaarli/Shaarli/issues/1142
*/
$keys = array_keys($tags);
$tmpTags = array_combine($keys, $keys);
array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
return $tags;
}

View file

@ -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)) {

View file

@ -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;
}
}

View file

@ -25,6 +25,9 @@ class PageBuilder
* @var LinkDB $linkDB instance.
*/
protected $linkDB;
/** @var bool $isLoggedIn Whether the user is logged in **/
protected $isLoggedIn = false;
/**
* PageBuilder constructor.
@ -34,12 +37,13 @@ class PageBuilder
* @param LinkDB $linkDB instance.
* @param string $token Session token
*/
public function __construct(&$conf, $linkDB = null, $token = null)
public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false)
{
$this->tpl = false;
$this->conf = $conf;
$this->linkDB = $linkDB;
$this->token = $token;
$this->isLoggedIn = $isLoggedIn;
}
/**
@ -55,7 +59,7 @@ class PageBuilder
$this->conf->get('resource.update_check'),
$this->conf->get('updates.check_updates_interval'),
$this->conf->get('updates.check_updates'),
isLoggedIn(),
$this->isLoggedIn,
$this->conf->get('updates.check_updates_branch')
);
$this->tpl->assign('newVersion', escape($version));
@ -67,6 +71,7 @@ class PageBuilder
$this->tpl->assign('versionError', escape($exc->getMessage()));
}
$this->tpl->assign('is_logged_in', $this->isLoggedIn);
$this->tpl->assign('feedurl', escape(index_url($_SERVER)));
$searchcrits = ''; // Search criteria
if (!empty($_GET['searchtags'])) {

View file

@ -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;
}
}

View 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;
}
}

View 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;
}
}

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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
View 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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

File diff suppressed because it is too large Load diff

View 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
View file

@ -78,8 +78,8 @@ require_once 'application/Updater.php';
use \Shaarli\Languages;
use \Shaarli\ThemeUtils;
use \Shaarli\Config\ConfigManager;
use \Shaarli\LoginManager;
use \Shaarli\SessionManager;
use \Shaarli\Security\LoginManager;
use \Shaarli\Security\SessionManager;
// Ensure the PHP version is supported
try {
@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
// Set default cookie expiration and path.
session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
// Set session parameters on server side.
// If the user does not access any page within this time, his/her session is considered expired.
define('INACTIVITY_TIMEOUT', 3600); // in seconds.
// Use cookies to store session.
ini_set('session.use_cookies', 1);
// Force cookies for session (phpsessionID forbidden in URL).
@ -123,8 +121,10 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli']))
}
$conf = new ConfigManager();
$loginManager = new LoginManager($GLOBALS, $conf);
$sessionManager = new SessionManager($_SESSION, $conf);
$loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
$clientIpId = client_ip_id($_SERVER);
// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
if (! defined('LC_MESSAGES')) {
@ -177,157 +177,61 @@ if (! is_file($conf->getConfigFileExt())) {
install($conf, $sessionManager);
}
// a token depending of deployment salt, user password, and the current ip
define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
$loginManager->checkLoginState($_COOKIE, $clientIpId);
/**
* Checking session state (i.e. is the user still logged in)
* Adapter function to ensure compatibility with third-party templates
*
* @param ConfigManager $conf The configuration manager.
* @see https://github.com/shaarli/Shaarli/pull/1086
*
* @return bool: true if the user is logged in, false otherwise.
* @return bool true when the user is logged in, false otherwise
*/
function setup_login_state($conf)
{
if ($conf->get('security.open_shaarli')) {
return true;
}
$userIsLoggedIn = false; // By default, we do not consider the user as logged in;
$loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
if (! $conf->exists('credentials.login')) {
$userIsLoggedIn = false; // Shaarli is not configured yet.
$loginFailure = true;
}
if (isset($_COOKIE['shaarli_staySignedIn']) &&
$_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN &&
!$loginFailure)
{
fillSessionInfo($conf);
$userIsLoggedIn = true;
}
// If session does not exist on server side, or IP address has changed, or session has expired, logout.
if (empty($_SESSION['uid'])
|| ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs())
|| time() >= $_SESSION['expires_on'])
{
logout();
$userIsLoggedIn = false;
$loginFailure = true;
}
if (!empty($_SESSION['longlastingsession'])) {
$_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
}
else {
$_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
}
if (!$loginFailure) {
$userIsLoggedIn = true;
}
return $userIsLoggedIn;
}
$userIsLoggedIn = setup_login_state($conf);
// ------------------------------------------------------------------------------------------
// Session management
// Returns the IP address of the client (Used to prevent session cookie hijacking.)
function allIPs()
{
$ip = $_SERVER['REMOTE_ADDR'];
// Then we use more HTTP headers to prevent session hijacking from users behind the same proxy.
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; }
if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; }
return $ip;
}
/**
* Load user session.
*
* @param ConfigManager $conf Configuration Manager instance.
*/
function fillSessionInfo($conf)
{
$_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid)
$_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked.
$_SESSION['username']= $conf->get('credentials.login');
$_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration.
}
/**
* Check that user/password is correct.
*
* @param string $login Username
* @param string $password User password
* @param ConfigManager $conf Configuration Manager instance.
*
* @return bool: authentication successful or not.
*/
function check_auth($login, $password, $conf)
{
$hash = sha1($password . $login . $conf->get('credentials.salt'));
if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash'))
{ // Login/password is correct.
fillSessionInfo($conf);
logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful');
return true;
}
logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login);
return false;
}
// Returns true if the user is logged in.
function isLoggedIn()
{
global $userIsLoggedIn;
return $userIsLoggedIn;
global $loginManager;
return $loginManager->isLoggedIn();
}
// Force logout.
function logout() {
if (isset($_SESSION)) {
unset($_SESSION['uid']);
unset($_SESSION['ip']);
unset($_SESSION['username']);
unset($_SESSION['visibility']);
unset($_SESSION['untaggedonly']);
}
setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
}
// ------------------------------------------------------------------------------------------
// Process login form: Check if login/password is correct.
if (isset($_POST['login']))
{
if (isset($_POST['login'])) {
if (! $loginManager->canLogin($_SERVER)) {
die(t('I said: NO. You are banned for the moment. Go away.'));
}
if (isset($_POST['password'])
&& $sessionManager->checkToken($_POST['token'])
&& (check_auth($_POST['login'], $_POST['password'], $conf))
&& $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
) {
// Login/password is OK.
$loginManager->handleSuccessfulLogin($_SERVER);
// If user wants to keep the session cookie even after the browser closes:
if (!empty($_POST['longlastingsession'])) {
$_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year)
$expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now)
setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH);
$_SESSION['expires_on'] = $expiration; // Set session expiration on server-side.
$cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side
$cookiedir = '';
if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
// Note: Never forget the trailing slash on the cookie path!
session_regenerate_id(true); // Send cookie with new expiration date to browser.
$cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
}
else // Standard session expiration (=when browser closes)
{
$cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes"
session_regenerate_id(true);
if (!empty($_POST['longlastingsession'])) {
// Keep the session cookie even after the browser closes
$sessionManager->setStaySignedIn(true);
$expirationTime = $sessionManager->extendSession();
setcookie(
$loginManager::$STAY_SIGNED_IN_COOKIE,
$loginManager->getStaySignedInToken(),
$expirationTime,
WEB_PATH
);
} else {
// Standard session expiration (=when browser closes)
$expirationTime = 0;
}
// Send cookie with the new expiration date to the browser
session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
session_regenerate_id(true);
// Optional redirect after login:
if (isset($_GET['post'])) {
$uri = '?post='. urlencode($_GET['post']);
@ -380,15 +284,16 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are atta
* Gives the last 7 days (which have links).
* This RSS feed cannot be filtered.
*
* @param ConfigManager $conf Configuration Manager instance.
* @param ConfigManager $conf Configuration Manager instance
* @param LoginManager $loginManager LoginManager instance
*/
function showDailyRSS($conf) {
function showDailyRSS($conf, $loginManager) {
// Cache system
$query = $_SERVER['QUERY_STRING'];
$cache = new CachedPage(
$conf->get('config.PAGE_CACHE'),
page_url($_SERVER),
startsWith($query,'do=dailyrss') && !isLoggedIn()
startsWith($query,'do=dailyrss') && !$loginManager->isLoggedIn()
);
$cached = $cache->cachedVersion();
if (!empty($cached)) {
@ -400,7 +305,7 @@ function showDailyRSS($conf) {
// Read links from database (and filter private links if used it not logged in).
$LINKSDB = new LinkDB(
$conf->get('resource.datastore'),
isLoggedIn(),
$loginManager->isLoggedIn(),
$conf->get('privacy.hide_public_links'),
$conf->get('redirector.url'),
$conf->get('redirector.encode_url')
@ -482,9 +387,10 @@ function showDailyRSS($conf) {
* @param PageBuilder $pageBuilder Template engine wrapper.
* @param LinkDB $LINKSDB LinkDB instance.
* @param ConfigManager $conf Configuration Manager instance.
* @param PluginManager $pluginManager Plugin Manager instane.
* @param PluginManager $pluginManager Plugin Manager instance.
* @param LoginManager $loginManager Login Manager instance
*/
function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
{
$day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
if (isset($_GET['day'])) {
@ -542,7 +448,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
/* Hook is called before column construction so that plugins don't have
to deal with columns. */
$pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn()));
$pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
/* We need to spread the articles on 3 columns.
I did not want to use a JavaScript lib like http://masonry.desandro.com/
@ -586,8 +492,8 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
* @param ConfigManager $conf Configuration Manager instance.
* @param PluginManager $pluginManager Plugin Manager instance.
*/
function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager); // Compute list of links to display
function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) {
buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager, $loginManager);
$PAGE->renderPage('linklist');
}
@ -607,7 +513,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
read_updates_file($conf->get('resource.updates')),
$LINKSDB,
$conf,
isLoggedIn()
$loginManager->isLoggedIn()
);
try {
$newUpdates = $updater->update();
@ -622,18 +528,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
die($e->getMessage());
}
$PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
$PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
$PAGE->assign('linkcount', count($LINKSDB));
$PAGE->assign('privateLinkcount', count_private($LINKSDB));
$PAGE->assign('plugin_errors', $pluginManager->getErrors());
// Determine which page will be rendered.
$query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
$targetPage = Router::findPage($query, $_GET, isLoggedIn());
$targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
if (
// if the user isn't logged in
!isLoggedIn() &&
!$loginManager->isLoggedIn() &&
// and Shaarli doesn't have public content...
$conf->get('privacy.hide_public_links') &&
// and is configured to enforce the login
@ -661,7 +567,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$pluginManager->executeHooks('render_' . $name, $plugin_data,
array(
'target' => $targetPage,
'loggedin' => isLoggedIn()
'loggedin' => $loginManager->isLoggedIn()
)
);
$PAGE->assign('plugins_' . $name, $plugin_data);
@ -686,7 +592,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout'))
{
invalidateCaches($conf->get('resource.page_cache'));
logout();
$sessionManager->logout();
setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
header('Location: ?');
exit;
}
@ -713,7 +620,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$data = array(
'linksToDisplay' => $linksToDisplay,
);
$pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn()));
$pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
foreach ($data as $key => $value) {
$PAGE->assign($key, $value);
@ -760,7 +667,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
'search_tags' => $searchTags,
'tags' => $tagList,
);
$pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
$pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
foreach ($data as $key => $value) {
$PAGE->assign($key, $value);
@ -793,7 +700,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
'search_tags' => $searchTags,
'tags' => $tags,
];
$pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
$pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
foreach ($data as $key => $value) {
$PAGE->assign($key, $value);
@ -807,7 +714,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
// Daily page.
if ($targetPage == Router::$PAGE_DAILY) {
showDaily($PAGE, $LINKSDB, $conf, $pluginManager);
showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
}
// ATOM and RSS feed.
@ -820,7 +727,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$cache = new CachedPage(
$conf->get('resource.page_cache'),
page_url($_SERVER),
startsWith($query,'do='. $targetPage) && !isLoggedIn()
startsWith($query,'do='. $targetPage) && !$loginManager->isLoggedIn()
);
$cached = $cache->cachedVersion();
if (!empty($cached)) {
@ -829,15 +736,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
}
// Generate data.
$feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn());
$feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
$feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
$feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn());
$feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
$feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
$data = $feedGenerator->buildData();
// Process plugin hook.
$pluginManager->executeHooks('render_feed', $data, array(
'loggedin' => isLoggedIn(),
'loggedin' => $loginManager->isLoggedIn(),
'target' => $targetPage,
));
@ -985,7 +892,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
}
// -------- Handle other actions allowed for non-logged in users:
if (!isLoggedIn())
if (!$loginManager->isLoggedIn())
{
// User tries to post new link but is not logged in:
// Show login screen, then redirect to ?post=...
@ -1001,7 +908,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
exit;
}
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
if (isset($_GET['edit_link'])) {
header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
exit;
@ -1052,7 +959,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
$conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt')));
try {
$conf->write(isLoggedIn());
$conf->write($loginManager->isLoggedIn());
}
catch(Exception $e) {
error_log(
@ -1103,7 +1010,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$conf->set('translation.language', escape($_POST['language']));
try {
$conf->write(isLoggedIn());
$conf->write($loginManager->isLoggedIn());
$history->updateSettings();
invalidateCaches($conf->get('resource.page_cache'));
}
@ -1376,8 +1283,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
// The callback will fill $charset and $title with data from the downloaded page.
get_http_response(
$url,
$conf->get('general.download_max_size', 4194304),
$conf->get('general.download_timeout', 30),
$conf->get('general.download_max_size', 4194304),
get_curl_download_callback($charset, $title)
);
if (! empty($title) && strtolower($charset) != 'utf-8') {
@ -1555,7 +1462,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
else {
$conf->set('general.enabled_plugins', save_plugin_config($_POST));
}
$conf->write(isLoggedIn());
$conf->write($loginManager->isLoggedIn());
$history->updateSettings();
}
catch (Exception $e) {
@ -1580,7 +1487,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
}
// -------- Otherwise, simply display search form and links:
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
exit;
}
@ -1592,8 +1499,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
* @param LinkDB $LINKSDB LinkDB instance.
* @param ConfigManager $conf Configuration Manager instance.
* @param PluginManager $pluginManager Plugin Manager instance.
* @param LoginManager $loginManager LoginManager instance
*/
function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
{
// Used in templates
if (isset($_GET['searchtags'])) {
@ -1632,8 +1540,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
$keys[] = $key;
}
// Select articles according to paging.
$pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
$pagecount = $pagecount == 0 ? 1 : $pagecount;
@ -1714,7 +1620,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
$data['pagetitle'] .= '- '. $conf->get('general.title');
}
$pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn()));
$pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
foreach ($data as $key => $value) {
$PAGE->assign($key, $value);
@ -1985,7 +1891,7 @@ function install($conf, $sessionManager) {
);
try {
// Everything is ok, let's create config file.
$conf->write(isLoggedIn());
$conf->write($loginManager->isLoggedIn());
}
catch(Exception $e) {
error_log(
@ -2249,7 +2155,7 @@ try {
$linkDb = new LinkDB(
$conf->get('resource.datastore'),
isLoggedIn(),
$loginManager->isLoggedIn(),
$conf->get('privacy.hide_public_links'),
$conf->get('redirector.url'),
$conf->get('redirector.encode_url')

View file

@ -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

View file

@ -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",

View file

@ -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('/(^| )&nbsp;/m', '$1 ', $description);
}
function reverse_feed_permalink($description)
{
return preg_replace('@&#8212; <a href="([^"]+)" title="[^"]+">(\w+)</a>$@im', '&#8212; [$2]($1)', $description);
}
/**
* Replace not whitelisted protocols with http:// in given description.
*

View 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'
])
);
}
}

View file

@ -542,4 +542,104 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
$this->assertEquals(3, count($res));
$this->assertNotContains('cartoon', $linkDB[4]['tags']);
}
/**
* Test linksCountPerTag all tags without filter.
* Equal occurrences should be sorted alphabetically.
*/
public function testCountLinkPerTagAllNoFilter()
{
$expected = [
'web' => 4,
'cartoon' => 3,
'dev' => 2,
'gnu' => 2,
'hashtag' => 2,
'sTuff' => 2,
'-exclude' => 1,
'.hidden' => 1,
'Mercurial' => 1,
'css' => 1,
'free' => 1,
'html' => 1,
'media' => 1,
'samba' => 1,
'software' => 1,
'stallman' => 1,
'tag1' => 1,
'tag2' => 1,
'tag3' => 1,
'tag4' => 1,
'ut' => 1,
'w3c' => 1,
];
$tags = self::$privateLinkDB->linksCountPerTag();
$this->assertEquals($expected, $tags, var_export($tags, true));
}
/**
* Test linksCountPerTag all tags with filter.
* Equal occurrences should be sorted alphabetically.
*/
public function testCountLinkPerTagAllWithFilter()
{
$expected = [
'gnu' => 2,
'hashtag' => 2,
'-exclude' => 1,
'.hidden' => 1,
'free' => 1,
'media' => 1,
'software' => 1,
'stallman' => 1,
'stuff' => 1,
'web' => 1,
];
$tags = self::$privateLinkDB->linksCountPerTag(['gnu']);
$this->assertEquals($expected, $tags, var_export($tags, true));
}
/**
* Test linksCountPerTag public tags with filter.
* Equal occurrences should be sorted alphabetically.
*/
public function testCountLinkPerTagPublicWithFilter()
{
$expected = [
'gnu' => 2,
'hashtag' => 2,
'-exclude' => 1,
'.hidden' => 1,
'free' => 1,
'media' => 1,
'software' => 1,
'stallman' => 1,
'stuff' => 1,
'web' => 1,
];
$tags = self::$privateLinkDB->linksCountPerTag(['gnu'], 'public');
$this->assertEquals($expected, $tags, var_export($tags, true));
}
/**
* Test linksCountPerTag public tags with filter.
* Equal occurrences should be sorted alphabetically.
*/
public function testCountLinkPerTagPrivateWithFilter()
{
$expected = [
'cartoon' => 1,
'dev' => 1,
'tag1' => 1,
'tag2' => 1,
'tag3' => 1,
'tag4' => 1,
];
$tags = self::$privateLinkDB->linksCountPerTag(['dev'], 'private');
$this->assertEquals($expected, $tags, var_export($tags, true));
}
}

View file

@ -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=')
);
}
}

View file

@ -49,6 +49,30 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
$this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
}
/**
* Test render_feed hook.
*/
public function testMarkdownFeed()
{
$markdown = '# My title' . PHP_EOL . 'Very interesting content.';
$markdown .= '&#8212; <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(
'&#8212; <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 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
$this->assertEquals($text, $reversedText);
}
public function testReverseFeedPermalink()
{
$text = 'Description... ';
$text .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
$expected = 'Description... &#8212; [Permalien](http://domain.tld/?0oc_VQ)';
$processedText = reverse_feed_permalink($text);
$this->assertEquals($expected, $processedText);
}
public function testReverseLastFeedPermalink()
{
$text = 'Description... ';
$text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
$expected = $text;
$text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
$expected .= '<br>&#8212; [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().
*/

View file

@ -1,5 +1,5 @@
<?php
namespace Shaarli;
namespace Shaarli\Security;
require_once 'tests/utils/FakeConfigManager.php';
use \PHPUnit\Framework\TestCase;
@ -9,15 +9,54 @@ use \PHPUnit\Framework\TestCase;
*/
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 @@ class LoginManagerTest extends TestCase
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 @@ class LoginManagerTest extends TestCase
'security.trusted_proxies' => [$this->trustedProxy],
]);
$this->cookie = [];
$this->globals = &$GLOBALS;
unset($this->globals['IPBANS']);
$this->loginManager = new LoginManager($this->globals, $this->configManager);
$this->session = [];
$this->sessionManager = new SessionManager($this->session, $this->configManager);
$this->loginManager = new LoginManager($this->globals, $this->configManager, $this->sessionManager);
$this->server['REMOTE_ADDR'] = $this->ipAddr;
}
@ -59,7 +108,7 @@ class LoginManagerTest extends TestCase
$this->banFile,
"<?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 @@ class LoginManagerTest extends TestCase
$this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600;
$this->assertTrue($this->loginManager->canLogin($this->server));
}
/**
* Generate a token depending on the user credentials and client IP
*/
public function testGenerateStaySignedInToken()
{
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
$this->assertEquals(
sha1($this->passwordHash . $this->clientIpAddress . $this->salt),
$this->loginManager->getStaySignedInToken()
);
}
/**
* Check user login - Shaarli has not yet been configured
*/
public function testCheckLoginStateNotConfigured()
{
$configManager = new \FakeConfigManager([
'resource.ban_file' => $this->banFile,
]);
$loginManager = new LoginManager($this->globals, $configManager, null);
$loginManager->checkLoginState([], '');
$this->assertFalse($loginManager->isLoggedIn());
}
/**
* Check user login - the client cookie does not match the server token
*/
public function testCheckLoginStateStaySignedInWithInvalidToken()
{
// simulate a previous login
$this->session = [
'ip' => $this->clientIpAddress,
'expires_on' => time() + 100,
];
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
$this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope';
$this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
$this->assertTrue($this->loginManager->isLoggedIn());
$this->assertTrue(empty($this->session['username']));
}
/**
* Check user login - the client cookie matches the server token
*/
public function testCheckLoginStateStaySignedInWithValidToken()
{
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
$this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken();
$this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
$this->assertTrue($this->loginManager->isLoggedIn());
$this->assertEquals($this->login, $this->session['username']);
$this->assertEquals($this->clientIpAddress, $this->session['ip']);
}
/**
* Check user login - the session has expired
*/
public function testCheckLoginStateSessionExpired()
{
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
$this->session['expires_on'] = time() - 100;
$this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
$this->assertFalse($this->loginManager->isLoggedIn());
}
/**
* Check user login - the remote client IP has changed
*/
public function testCheckLoginStateClientIpChanged()
{
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
$this->loginManager->checkLoginState($this->cookie, '10.7.157.98');
$this->assertFalse($this->loginManager->isLoggedIn());
}
/**
* Check user credentials - wrong login supplied
*/
public function testCheckCredentialsWrongLogin()
{
$this->assertFalse(
$this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password)
);
}
/**
* Check user credentials - wrong password supplied
*/
public function testCheckCredentialsWrongPassword()
{
$this->assertFalse(
$this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd')
);
}
/**
* Check user credentials - wrong login and password supplied
*/
public function testCheckCredentialsWrongLoginAndPassword()
{
$this->assertFalse(
$this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd')
);
}
/**
* Check user credentials - correct login and password supplied
*/
public function testCheckCredentialsGoodLoginAndPassword()
{
$this->assertTrue(
$this->loginManager->checkCredentials('', '', $this->login, $this->password)
);
}
}

View 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'));
}
}

View file

@ -42,4 +42,16 @@ class FakeConfigManager
}
return $key;
}
/**
* Check if a setting exists
*
* @param string $setting Asked setting, keys separated with dots
*
* @return bool true if the setting exists, false otherwise
*/
public function exists($setting)
{
return array_key_exists($setting, $this->values);
}
}

View file

@ -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>

View file

@ -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)"}

View file

@ -15,7 +15,7 @@
</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>

View file

@ -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 @@
<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 @@
</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 @@
{if="$link_plugin_counter - 1 != $counter"}&middot;{/if}
{/loop}
{/if}
{if="isLoggedIn()"}
{if="$is_logged_in"}
&middot;
<a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
title="{$strDelete}" class="delete-link confirm-delete">

View file

@ -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>

View file

@ -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>

View file

@ -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}
&middot;

View file

@ -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">

View file

@ -18,9 +18,9 @@
{/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}

View file

@ -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">

View file

@ -45,7 +45,7 @@
{/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>

View file

@ -21,7 +21,7 @@
</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 @@
{/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>&nbsp;&nbsp;
<a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
<i class="fa fa-pencil-square-o {$key}"></i>
@ -63,7 +63,7 @@
{$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 @@
</div>
</div>
{if="isLoggedIn()===true"}
{if="$is_logged_in===true"}
<input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
{/if}

View file

@ -146,8 +146,14 @@
</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">

View file

@ -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>

View file

@ -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}">

View file

@ -1,5 +1,5 @@
<div class="paging">
{if="isLoggedIn()"}
{if="$is_logged_in"}
<div class="paging_privatelinks">
<a href="?visibility=private">
{if="$visibility=='private'"}

View file

@ -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}

View file

@ -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
View file

@ -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"