aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.dev/.eslintrc.js (renamed from .eslintrc.js)0
-rw-r--r--.dev/.sasslintrc15
-rw-r--r--.editorconfig2
-rw-r--r--.gitattributes1
-rw-r--r--.travis.yml1
-rw-r--r--Makefile8
-rw-r--r--application/HttpUtils.php33
-rw-r--r--application/LoginManager.php134
-rw-r--r--application/PageBuilder.php9
-rw-r--r--application/SessionManager.php83
-rw-r--r--application/security/LoginManager.php265
-rw-r--r--application/security/SessionManager.php199
-rw-r--r--assets/default/scss/shaarli.scss2046
-rw-r--r--composer.json3
-rw-r--r--index.php240
-rw-r--r--package.json1
-rw-r--r--plugins/markdown/markdown.php8
-rw-r--r--tests/HttpUtils/ClientIpIdTest.php52
-rw-r--r--tests/SessionManagerTest.php149
-rw-r--r--tests/plugins/PluginMarkdownTest.php55
-rw-r--r--tests/security/LoginManagerTest.php (renamed from tests/LoginManagerTest.php)183
-rw-r--r--tests/security/SessionManagerTest.php273
-rw-r--r--tests/utils/FakeConfigManager.php12
-rw-r--r--tpl/default/404.html2
-rw-r--r--tpl/default/editlink.html2
-rw-r--r--tpl/default/import.html2
-rw-r--r--tpl/default/linklist.html10
-rw-r--r--tpl/default/linklist.paging.html4
-rw-r--r--tpl/default/loginform.html2
-rw-r--r--tpl/default/page.footer.html4
-rw-r--r--tpl/default/page.header.html16
-rw-r--r--tpl/default/picwall.html4
-rw-r--r--tpl/default/pluginsadmin.html2
-rw-r--r--tpl/default/tag.cloud.html2
-rw-r--r--tpl/default/tag.list.html10
-rw-r--r--tpl/vintage/daily.html2
-rw-r--r--tpl/vintage/linklist.html4
-rw-r--r--tpl/vintage/linklist.paging.html2
-rw-r--r--tpl/vintage/page.footer.html2
-rw-r--r--tpl/vintage/page.header.html4
-rw-r--r--yarn.lock298
41 files changed, 2623 insertions, 1521 deletions
diff --git a/.eslintrc.js b/.dev/.eslintrc.js
index 151b785b..151b785b 100644
--- a/.eslintrc.js
+++ b/.dev/.eslintrc.js
diff --git a/.dev/.sasslintrc b/.dev/.sasslintrc
new file mode 100644
index 00000000..ac406d7b
--- /dev/null
+++ b/.dev/.sasslintrc
@@ -0,0 +1,15 @@
1options:
2 max-warnings: 0
3rules:
4 property-sort-order:
5 - 1
6 -
7 order: 'concentric'
8 no-important:
9 - 0
10 no-vendor-prefixes:
11 - 0 # this will be fixed with v2: see https://github.com/sasstools/sass-lint/pull/1137
12 nesting-depth:
13 - 1
14 -
15 max-depth: 4
diff --git a/.editorconfig b/.editorconfig
index f0d83ee3..34bd7994 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,7 +10,7 @@ trim_trailing_whitespace = true
10indent_style = space 10indent_style = space
11indent_size = 4 11indent_size = 4
12 12
13[*.{htaccess,html,js,json,xml,yml}] 13[*.{htaccess,html,scss,js,json,xml,yml}]
14indent_size = 2 14indent_size = 2
15 15
16[*.php] 16[*.php]
diff --git a/.gitattributes b/.gitattributes
index 549777ef..6b6ffbd5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -26,6 +26,7 @@ Dockerfile text
26 26
27# Exclude from Git archives 27# Exclude from Git archives
28.editorconfig export-ignore 28.editorconfig export-ignore
29.dev export-ignore
29.gitattributes export-ignore 30.gitattributes export-ignore
30.github export-ignore 31.github export-ignore
31.gitignore export-ignore 32.gitignore export-ignore
diff --git a/.travis.yml b/.travis.yml
index 14b91cf2..eee1ca74 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,6 +27,7 @@ matrix:
27 script: 27 script:
28 - yarn run build # Just to be sure that the build isn't broken 28 - yarn run build # Just to be sure that the build isn't broken
29 - make eslint 29 - make eslint
30 - make sasslint
30 31
31cache: 32cache:
32 directories: 33 directories:
diff --git a/Makefile b/Makefile
index d1216569..4adbdd68 100644
--- a/Makefile
+++ b/Makefile
@@ -218,5 +218,9 @@ translate:
218 218
219### Run ESLint check against Shaarli's JS files 219### Run ESLint check against Shaarli's JS files
220eslint: 220eslint:
221 @yarn run eslint assets/vintage/js/ 221 @yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/
222 @yarn run eslint assets/default/js/ 222 @yarn run eslint -c .dev/.eslintrc.js assets/default/js/
223
224### Run CSSLint check against Shaarli's SCSS files
225sasslint:
226 @yarn run sass-lint -c .dev/.sasslintrc 'assets/default/scss/*.scss' -v -q
diff --git a/application/HttpUtils.php b/application/HttpUtils.php
index 83a4c5e2..e9282506 100644
--- a/application/HttpUtils.php
+++ b/application/HttpUtils.php
@@ -1,7 +1,7 @@
1<?php 1<?php
2/** 2/**
3 * GET an HTTP URL to retrieve its content 3 * GET an HTTP URL to retrieve its content
4 * Uses the cURL library or a fallback method 4 * Uses the cURL library or a fallback method
5 * 5 *
6 * @param string $url URL to get (http://...) 6 * @param string $url URL to get (http://...)
7 * @param int $timeout network timeout (in seconds) 7 * @param int $timeout network timeout (in seconds)
@@ -415,6 +415,37 @@ function getIpAddressFromProxy($server, $trustedIps)
415 return array_pop($ips); 415 return array_pop($ips);
416} 416}
417 417
418
419/**
420 * Return an identifier based on the advertised client IP address(es)
421 *
422 * This aims at preventing session hijacking from users behind the same proxy
423 * by relying on HTTP headers.
424 *
425 * See:
426 * - https://secure.php.net/manual/en/reserved.variables.server.php
427 * - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
428 * - https://stackoverflow.com/questions/12233406/preventing-session-hijacking
429 * - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor
430 *
431 * @param array $server The $_SERVER array
432 *
433 * @return string An identifier based on client IP address information
434 */
435function client_ip_id($server)
436{
437 $ip = $server['REMOTE_ADDR'];
438
439 if (isset($server['HTTP_X_FORWARDED_FOR'])) {
440 $ip = $ip . '_' . $server['HTTP_X_FORWARDED_FOR'];
441 }
442 if (isset($server['HTTP_CLIENT_IP'])) {
443 $ip = $ip . '_' . $server['HTTP_CLIENT_IP'];
444 }
445 return $ip;
446}
447
448
418/** 449/**
419 * Returns true if Shaarli's currently browsed in HTTPS. 450 * Returns true if Shaarli's currently browsed in HTTPS.
420 * Supports reverse proxies (if the headers are correctly set). 451 * Supports reverse proxies (if the headers are correctly set).
diff --git a/application/LoginManager.php b/application/LoginManager.php
deleted file mode 100644
index 397bc6e3..00000000
--- a/application/LoginManager.php
+++ /dev/null
@@ -1,134 +0,0 @@
1<?php
2namespace Shaarli;
3
4/**
5 * User login management
6 */
7class LoginManager
8{
9 protected $globals = [];
10 protected $configManager = null;
11 protected $banFile = '';
12
13 /**
14 * Constructor
15 *
16 * @param array $globals The $GLOBALS array (reference)
17 * @param ConfigManager $configManager Configuration Manager instance.
18 */
19 public function __construct(& $globals, $configManager)
20 {
21 $this->globals = &$globals;
22 $this->configManager = $configManager;
23 $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php');
24 $this->readBanFile();
25 }
26
27 /**
28 * Read a file containing banned IPs
29 */
30 protected function readBanFile()
31 {
32 if (! file_exists($this->banFile)) {
33 return;
34 }
35 include $this->banFile;
36 }
37
38 /**
39 * Write the banned IPs to a file
40 */
41 protected function writeBanFile()
42 {
43 if (! array_key_exists('IPBANS', $this->globals)) {
44 return;
45 }
46 file_put_contents(
47 $this->banFile,
48 "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>"
49 );
50 }
51
52 /**
53 * Handle a failed login and ban the IP after too many failed attempts
54 *
55 * @param array $server The $_SERVER array
56 */
57 public function handleFailedLogin($server)
58 {
59 $ip = $server['REMOTE_ADDR'];
60 $trusted = $this->configManager->get('security.trusted_proxies', []);
61
62 if (in_array($ip, $trusted)) {
63 $ip = getIpAddressFromProxy($server, $trusted);
64 if (! $ip) {
65 // the IP is behind a trusted forward proxy, but is not forwarded
66 // in the HTTP headers, so we do nothing
67 return;
68 }
69 }
70
71 // increment the fail count for this IP
72 if (isset($this->globals['IPBANS']['FAILURES'][$ip])) {
73 $this->globals['IPBANS']['FAILURES'][$ip]++;
74 } else {
75 $this->globals['IPBANS']['FAILURES'][$ip] = 1;
76 }
77
78 if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) {
79 $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800);
80 logm(
81 $this->configManager->get('resource.log'),
82 $server['REMOTE_ADDR'],
83 'IP address banned from login'
84 );
85 }
86 $this->writeBanFile();
87 }
88
89 /**
90 * Handle a successful login
91 *
92 * @param array $server The $_SERVER array
93 */
94 public function handleSuccessfulLogin($server)
95 {
96 $ip = $server['REMOTE_ADDR'];
97 // FIXME unban when behind a trusted proxy?
98
99 unset($this->globals['IPBANS']['FAILURES'][$ip]);
100 unset($this->globals['IPBANS']['BANS'][$ip]);
101
102 $this->writeBanFile();
103 }
104
105 /**
106 * Check if the user can login from this IP
107 *
108 * @param array $server The $_SERVER array
109 *
110 * @return bool true if the user is allowed to login
111 */
112 public function canLogin($server)
113 {
114 $ip = $server['REMOTE_ADDR'];
115
116 if (! isset($this->globals['IPBANS']['BANS'][$ip])) {
117 // the user is not banned
118 return true;
119 }
120
121 if ($this->globals['IPBANS']['BANS'][$ip] > time()) {
122 // the user is still banned
123 return false;
124 }
125
126 // the ban has expired, the user can attempt to log in again
127 logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.');
128 unset($this->globals['IPBANS']['FAILURES'][$ip]);
129 unset($this->globals['IPBANS']['BANS'][$ip]);
130
131 $this->writeBanFile();
132 return true;
133 }
134}
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index 3233d6b6..a4483870 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -25,6 +25,9 @@ class PageBuilder
25 * @var LinkDB $linkDB instance. 25 * @var LinkDB $linkDB instance.
26 */ 26 */
27 protected $linkDB; 27 protected $linkDB;
28
29 /** @var bool $isLoggedIn Whether the user is logged in **/
30 protected $isLoggedIn = false;
28 31
29 /** 32 /**
30 * PageBuilder constructor. 33 * PageBuilder constructor.
@@ -34,12 +37,13 @@ class PageBuilder
34 * @param LinkDB $linkDB instance. 37 * @param LinkDB $linkDB instance.
35 * @param string $token Session token 38 * @param string $token Session token
36 */ 39 */
37 public function __construct(&$conf, $linkDB = null, $token = null) 40 public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false)
38 { 41 {
39 $this->tpl = false; 42 $this->tpl = false;
40 $this->conf = $conf; 43 $this->conf = $conf;
41 $this->linkDB = $linkDB; 44 $this->linkDB = $linkDB;
42 $this->token = $token; 45 $this->token = $token;
46 $this->isLoggedIn = $isLoggedIn;
43 } 47 }
44 48
45 /** 49 /**
@@ -55,7 +59,7 @@ class PageBuilder
55 $this->conf->get('resource.update_check'), 59 $this->conf->get('resource.update_check'),
56 $this->conf->get('updates.check_updates_interval'), 60 $this->conf->get('updates.check_updates_interval'),
57 $this->conf->get('updates.check_updates'), 61 $this->conf->get('updates.check_updates'),
58 isLoggedIn(), 62 $this->isLoggedIn,
59 $this->conf->get('updates.check_updates_branch') 63 $this->conf->get('updates.check_updates_branch')
60 ); 64 );
61 $this->tpl->assign('newVersion', escape($version)); 65 $this->tpl->assign('newVersion', escape($version));
@@ -67,6 +71,7 @@ class PageBuilder
67 $this->tpl->assign('versionError', escape($exc->getMessage())); 71 $this->tpl->assign('versionError', escape($exc->getMessage()));
68 } 72 }
69 73
74 $this->tpl->assign('is_logged_in', $this->isLoggedIn);
70 $this->tpl->assign('feedurl', escape(index_url($_SERVER))); 75 $this->tpl->assign('feedurl', escape(index_url($_SERVER)));
71 $searchcrits = ''; // Search criteria 76 $searchcrits = ''; // Search criteria
72 if (!empty($_GET['searchtags'])) { 77 if (!empty($_GET['searchtags'])) {
diff --git a/application/SessionManager.php b/application/SessionManager.php
deleted file mode 100644
index 71f0b38d..00000000
--- a/application/SessionManager.php
+++ /dev/null
@@ -1,83 +0,0 @@
1<?php
2namespace Shaarli;
3
4/**
5 * Manages the server-side session
6 */
7class SessionManager
8{
9 protected $session = [];
10
11 /**
12 * Constructor
13 *
14 * @param array $session The $_SESSION array (reference)
15 * @param ConfigManager $conf ConfigManager instance
16 */
17 public function __construct(& $session, $conf)
18 {
19 $this->session = &$session;
20 $this->conf = $conf;
21 }
22
23 /**
24 * Generates a session token
25 *
26 * @return string token
27 */
28 public function generateToken()
29 {
30 $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
31 $this->session['tokens'][$token] = 1;
32 return $token;
33 }
34
35 /**
36 * Checks the validity of a session token, and destroys it afterwards
37 *
38 * @param string $token The token to check
39 *
40 * @return bool true if the token is valid, else false
41 */
42 public function checkToken($token)
43 {
44 if (! isset($this->session['tokens'][$token])) {
45 // the token is wrong, or has already been used
46 return false;
47 }
48
49 // destroy the token to prevent future use
50 unset($this->session['tokens'][$token]);
51 return true;
52 }
53
54 /**
55 * Validate session ID to prevent Full Path Disclosure.
56 *
57 * See #298.
58 * The session ID's format depends on the hash algorithm set in PHP settings
59 *
60 * @param string $sessionId Session ID
61 *
62 * @return true if valid, false otherwise.
63 *
64 * @see http://php.net/manual/en/function.hash-algos.php
65 * @see http://php.net/manual/en/session.configuration.php
66 */
67 public static function checkId($sessionId)
68 {
69 if (empty($sessionId)) {
70 return false;
71 }
72
73 if (!$sessionId) {
74 return false;
75 }
76
77 if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
78 return false;
79 }
80
81 return true;
82 }
83}
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
new file mode 100644
index 00000000..d6784d6d
--- /dev/null
+++ b/application/security/LoginManager.php
@@ -0,0 +1,265 @@
1<?php
2namespace Shaarli\Security;
3
4use Shaarli\Config\ConfigManager;
5
6/**
7 * User login management
8 */
9class LoginManager
10{
11 /** @var string Name of the cookie set after logging in **/
12 public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
13
14 /** @var array A reference to the $_GLOBALS array */
15 protected $globals = [];
16
17 /** @var ConfigManager Configuration Manager instance **/
18 protected $configManager = null;
19
20 /** @var SessionManager Session Manager instance **/
21 protected $sessionManager = null;
22
23 /** @var string Path to the file containing IP bans */
24 protected $banFile = '';
25
26 /** @var bool Whether the user is logged in **/
27 protected $isLoggedIn = false;
28
29 /** @var bool Whether the Shaarli instance is open to public edition **/
30 protected $openShaarli = false;
31
32 /** @var string User sign-in token depending on remote IP and credentials */
33 protected $staySignedInToken = '';
34
35 /**
36 * Constructor
37 *
38 * @param array $globals The $GLOBALS array (reference)
39 * @param ConfigManager $configManager Configuration Manager instance
40 * @param SessionManager $sessionManager SessionManager instance
41 */
42 public function __construct(& $globals, $configManager, $sessionManager)
43 {
44 $this->globals = &$globals;
45 $this->configManager = $configManager;
46 $this->sessionManager = $sessionManager;
47 $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php');
48 $this->readBanFile();
49 if ($this->configManager->get('security.open_shaarli') === true) {
50 $this->openShaarli = true;
51 }
52 }
53
54 /**
55 * Generate a token depending on deployment salt, user password and client IP
56 *
57 * @param string $clientIpAddress The remote client IP address
58 */
59 public function generateStaySignedInToken($clientIpAddress)
60 {
61 $this->staySignedInToken = sha1(
62 $this->configManager->get('credentials.hash')
63 . $clientIpAddress
64 . $this->configManager->get('credentials.salt')
65 );
66 }
67
68 /**
69 * Return the user's client stay-signed-in token
70 *
71 * @return string User's client stay-signed-in token
72 */
73 public function getStaySignedInToken()
74 {
75 return $this->staySignedInToken;
76 }
77
78 /**
79 * Check user session state and validity (expiration)
80 *
81 * @param array $cookie The $_COOKIE array
82 * @param string $clientIpId Client IP address identifier
83 */
84 public function checkLoginState($cookie, $clientIpId)
85 {
86 if (! $this->configManager->exists('credentials.login')) {
87 // Shaarli is not configured yet
88 $this->isLoggedIn = false;
89 return;
90 }
91
92 if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE])
93 && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
94 ) {
95 // The user client has a valid stay-signed-in cookie
96 // Session information is updated with the current client information
97 $this->sessionManager->storeLoginInfo($clientIpId);
98
99 } elseif ($this->sessionManager->hasSessionExpired()
100 || $this->sessionManager->hasClientIpChanged($clientIpId)
101 ) {
102 $this->sessionManager->logout();
103 $this->isLoggedIn = false;
104 return;
105 }
106
107 $this->isLoggedIn = true;
108 $this->sessionManager->extendSession();
109 }
110
111 /**
112 * Return whether the user is currently logged in
113 *
114 * @return true when the user is logged in, false otherwise
115 */
116 public function isLoggedIn()
117 {
118 if ($this->openShaarli) {
119 return true;
120 }
121 return $this->isLoggedIn;
122 }
123
124 /**
125 * Check user credentials are valid
126 *
127 * @param string $remoteIp Remote client IP address
128 * @param string $clientIpId Client IP address identifier
129 * @param string $login Username
130 * @param string $password Password
131 *
132 * @return bool true if the provided credentials are valid, false otherwise
133 */
134 public function checkCredentials($remoteIp, $clientIpId, $login, $password)
135 {
136 $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
137
138 if ($login != $this->configManager->get('credentials.login')
139 || $hash != $this->configManager->get('credentials.hash')
140 ) {
141 logm(
142 $this->configManager->get('resource.log'),
143 $remoteIp,
144 'Login failed for user ' . $login
145 );
146 return false;
147 }
148
149 $this->sessionManager->storeLoginInfo($clientIpId);
150 logm(
151 $this->configManager->get('resource.log'),
152 $remoteIp,
153 'Login successful'
154 );
155 return true;
156 }
157
158 /**
159 * Read a file containing banned IPs
160 */
161 protected function readBanFile()
162 {
163 if (! file_exists($this->banFile)) {
164 return;
165 }
166 include $this->banFile;
167 }
168
169 /**
170 * Write the banned IPs to a file
171 */
172 protected function writeBanFile()
173 {
174 if (! array_key_exists('IPBANS', $this->globals)) {
175 return;
176 }
177 file_put_contents(
178 $this->banFile,
179 "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>"
180 );
181 }
182
183 /**
184 * Handle a failed login and ban the IP after too many failed attempts
185 *
186 * @param array $server The $_SERVER array
187 */
188 public function handleFailedLogin($server)
189 {
190 $ip = $server['REMOTE_ADDR'];
191 $trusted = $this->configManager->get('security.trusted_proxies', []);
192
193 if (in_array($ip, $trusted)) {
194 $ip = getIpAddressFromProxy($server, $trusted);
195 if (! $ip) {
196 // the IP is behind a trusted forward proxy, but is not forwarded
197 // in the HTTP headers, so we do nothing
198 return;
199 }
200 }
201
202 // increment the fail count for this IP
203 if (isset($this->globals['IPBANS']['FAILURES'][$ip])) {
204 $this->globals['IPBANS']['FAILURES'][$ip]++;
205 } else {
206 $this->globals['IPBANS']['FAILURES'][$ip] = 1;
207 }
208
209 if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) {
210 $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800);
211 logm(
212 $this->configManager->get('resource.log'),
213 $server['REMOTE_ADDR'],
214 'IP address banned from login'
215 );
216 }
217 $this->writeBanFile();
218 }
219
220 /**
221 * Handle a successful login
222 *
223 * @param array $server The $_SERVER array
224 */
225 public function handleSuccessfulLogin($server)
226 {
227 $ip = $server['REMOTE_ADDR'];
228 // FIXME unban when behind a trusted proxy?
229
230 unset($this->globals['IPBANS']['FAILURES'][$ip]);
231 unset($this->globals['IPBANS']['BANS'][$ip]);
232
233 $this->writeBanFile();
234 }
235
236 /**
237 * Check if the user can login from this IP
238 *
239 * @param array $server The $_SERVER array
240 *
241 * @return bool true if the user is allowed to login
242 */
243 public function canLogin($server)
244 {
245 $ip = $server['REMOTE_ADDR'];
246
247 if (! isset($this->globals['IPBANS']['BANS'][$ip])) {
248 // the user is not banned
249 return true;
250 }
251
252 if ($this->globals['IPBANS']['BANS'][$ip] > time()) {
253 // the user is still banned
254 return false;
255 }
256
257 // the ban has expired, the user can attempt to log in again
258 logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.');
259 unset($this->globals['IPBANS']['FAILURES'][$ip]);
260 unset($this->globals['IPBANS']['BANS'][$ip]);
261
262 $this->writeBanFile();
263 return true;
264 }
265}
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
new file mode 100644
index 00000000..b8b8ab8d
--- /dev/null
+++ b/application/security/SessionManager.php
@@ -0,0 +1,199 @@
1<?php
2namespace Shaarli\Security;
3
4use Shaarli\Config\ConfigManager;
5
6/**
7 * Manages the server-side session
8 */
9class SessionManager
10{
11 /** @var int Session expiration timeout, in seconds */
12 public static $SHORT_TIMEOUT = 3600; // 1 hour
13
14 /** @var int Session expiration timeout, in seconds */
15 public static $LONG_TIMEOUT = 31536000; // 1 year
16
17 /** @var array Local reference to the global $_SESSION array */
18 protected $session = [];
19
20 /** @var ConfigManager Configuration Manager instance **/
21 protected $conf = null;
22
23 /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
24 protected $staySignedIn = false;
25
26 /**
27 * Constructor
28 *
29 * @param array $session The $_SESSION array (reference)
30 * @param ConfigManager $conf ConfigManager instance
31 */
32 public function __construct(& $session, $conf)
33 {
34 $this->session = &$session;
35 $this->conf = $conf;
36 }
37
38 /**
39 * Define whether the user should stay signed in across browser sessions
40 *
41 * @param bool $staySignedIn Keep the user signed in
42 */
43 public function setStaySignedIn($staySignedIn)
44 {
45 $this->staySignedIn = $staySignedIn;
46 }
47
48 /**
49 * Generates a session token
50 *
51 * @return string token
52 */
53 public function generateToken()
54 {
55 $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
56 $this->session['tokens'][$token] = 1;
57 return $token;
58 }
59
60 /**
61 * Checks the validity of a session token, and destroys it afterwards
62 *
63 * @param string $token The token to check
64 *
65 * @return bool true if the token is valid, else false
66 */
67 public function checkToken($token)
68 {
69 if (! isset($this->session['tokens'][$token])) {
70 // the token is wrong, or has already been used
71 return false;
72 }
73
74 // destroy the token to prevent future use
75 unset($this->session['tokens'][$token]);
76 return true;
77 }
78
79 /**
80 * Validate session ID to prevent Full Path Disclosure.
81 *
82 * See #298.
83 * The session ID's format depends on the hash algorithm set in PHP settings
84 *
85 * @param string $sessionId Session ID
86 *
87 * @return true if valid, false otherwise.
88 *
89 * @see http://php.net/manual/en/function.hash-algos.php
90 * @see http://php.net/manual/en/session.configuration.php
91 */
92 public static function checkId($sessionId)
93 {
94 if (empty($sessionId)) {
95 return false;
96 }
97
98 if (!$sessionId) {
99 return false;
100 }
101
102 if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
103 return false;
104 }
105
106 return true;
107 }
108
109 /**
110 * Store user login information after a successful login
111 *
112 * @param string $clientIpId Client IP address identifier
113 */
114 public function storeLoginInfo($clientIpId)
115 {
116 $this->session['ip'] = $clientIpId;
117 $this->session['username'] = $this->conf->get('credentials.login');
118 $this->extendTimeValidityBy(self::$SHORT_TIMEOUT);
119 }
120
121 /**
122 * Extend session validity
123 */
124 public function extendSession()
125 {
126 if ($this->staySignedIn) {
127 return $this->extendTimeValidityBy(self::$LONG_TIMEOUT);
128 }
129 return $this->extendTimeValidityBy(self::$SHORT_TIMEOUT);
130 }
131
132 /**
133 * Extend expiration time
134 *
135 * @param int $duration Expiration time extension (seconds)
136 *
137 * @return int New session expiration time
138 */
139 protected function extendTimeValidityBy($duration)
140 {
141 $expirationTime = time() + $duration;
142 $this->session['expires_on'] = $expirationTime;
143 return $expirationTime;
144 }
145
146 /**
147 * Logout a user by unsetting all login information
148 *
149 * See:
150 * - https://secure.php.net/manual/en/function.setcookie.php
151 */
152 public function logout()
153 {
154 if (isset($this->session)) {
155 unset($this->session['ip']);
156 unset($this->session['expires_on']);
157 unset($this->session['username']);
158 unset($this->session['visibility']);
159 unset($this->session['untaggedonly']);
160 }
161 }
162
163 /**
164 * Check whether the session has expired
165 *
166 * @param string $clientIpId Client IP address identifier
167 *
168 * @return bool true if the session has expired, false otherwise
169 */
170 public function hasSessionExpired()
171 {
172 if (empty($this->session['expires_on'])) {
173 return true;
174 }
175 if (time() >= $this->session['expires_on']) {
176 return true;
177 }
178 return false;
179 }
180
181 /**
182 * Check whether the client IP address has changed
183 *
184 * @param string $clientIpId Client IP address identifier
185 *
186 * @return bool true if the IP has changed, false if it has not, or
187 * if session protection has been disabled
188 */
189 public function hasClientIpChanged($clientIpId)
190 {
191 if ($this->conf->get('security.session_protection_disabled') === true) {
192 return false;
193 }
194 if (isset($this->session['ip']) && $this->session['ip'] === $clientIpId) {
195 return false;
196 }
197 return true;
198 }
199}
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index 25440de1..09d5efbe 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -1,1357 +1,1545 @@
1$fa-font-path: "~font-awesome/fonts"; 1$fa-font-path: '~font-awesome/fonts';
2 2
3@import "~font-awesome/scss/font-awesome.scss"; 3@import '~font-awesome/scss/font-awesome';
4@import '~purecss/build/pure.css'; 4@import '~purecss/build/pure.css';
5@import '~purecss/build/grids-responsive.css'; 5@import '~purecss/build/grids-responsive.css';
6@import '~pure-extras/css/pure-extras.css'; 6@import '~pure-extras/css/pure-extras.css';
7@import '~awesomplete/awesomplete.css'; 7@import '~awesomplete/awesomplete.css';
8 8
9/** 9$white: #fff;
10 * General 10$black: #000;
11 */ 11$almost-white: #f5f5f5;
12$dark-grey: #252525;
13$light-grey: #797979;
14$main-green: #1b926c;
15$light-green: #b0ddce;
16$dark-green: #2a4c41;
17$red: #ac2925;
18$orange: #f89406;
19$blue: #0b5ea6;
20$background-color: #d0d0d0;
21$background-linklist-info: #ddd;
22$light-shadow: rgba(255, 255, 255, .078);
23$dark-shadow: rgba(0, 0, 0, .298);
24$warning-text: #97600d;
25$form-input-border: #d8d8d8;
26$form-input-background: #eee;
27
28// General
12body { 29body {
13 background: #d0d0d0; 30 background: $background-color;
14} 31}
15 32
16.strong { 33.strong {
17 font-weight: bold; 34 font-weight: bold;
18} 35}
19 36
20.clear { 37.clear {
21 clear: both; 38 clear: both;
22} 39}
23 40
24.center { 41.center {
25 text-align: center; 42 margin: auto;
26 margin: auto; 43 text-align: center;
27} 44}
28 45
29.label { 46.label {
30 display: inline-block; 47 display: inline-block;
31 padding: .25em .4em; 48 border-radius: .25rem;
32 font-size: 75%; 49 padding: .25em .4em;
33 font-weight: 700; 50 vertical-align: baseline;
34 line-height: 1; 51 text-align: center;
35 text-align: center; 52 line-height: 1;
36 white-space: nowrap; 53 white-space: nowrap;
37 vertical-align: baseline; 54 font-size: 75%;
38 border-radius: .25rem; 55 font-weight: 700;
39} 56}
40 57
41pre { 58pre {
42 max-width: 100%; 59 max-width: 100%;
43} 60}
44 61
45@font-face { 62@font-face {
46 font-family: 'Roboto'; 63 font-family: 'Roboto';
47 font-weight: 400; 64 font-weight: 400;
48 font-style: normal; 65 font-style: normal;
49 src: 66 src: local('Roboto'),
50 local('Roboto'), 67 local('Roboto-Regular'),
51 local('Roboto-Regular'), 68 url('../fonts/Roboto-Regular.woff2') format('woff2'),
52 url('../fonts/Roboto-Regular.woff2') format('woff2'), 69 url('../fonts/Roboto-Regular.woff') format('woff');
53 url('../fonts/Roboto-Regular.woff') format('woff');
54} 70}
55 71
56@font-face { 72@font-face {
57 font-family: 'Roboto'; 73 font-family: 'Roboto';
58 font-weight: 700; 74 font-weight: 700;
59 font-style: normal; 75 font-style: normal;
60 src: 76 src: local('Roboto'),
61 local('Roboto'), 77 local('Roboto-Bold'),
62 local('Roboto-Bold'), 78 url('../fonts/Roboto-Bold.woff2') format('woff2'),
63 url('../fonts/Roboto-Bold.woff2') format('woff2'), 79 url('../fonts/Roboto-Bold.woff') format('woff');
64 url('../fonts/Roboto-Bold.woff') format('woff'); 80}
65} 81
66 82body,
67body, .pure-g [class*="pure-u"] { 83.pure-g [class*='pure-u'] {
68 font-family: Roboto, Arial, sans-serif; 84 font-family: Roboto, Arial, sans-serif;
69} 85}
70 86
71/** 87// Extends Pure grids responsive to hide items.
72 * Extends Pure grids responsive to hide items. 88// Use xx-0 to hide an item on xx screen.
73 * Use xx-0 to hide an item on xx screen. 89// Display it at any level with xx-visible.
74 * Display it at any level with xx-visible. 90.pure-u-0 {
75 */ 91 display: none !important;
76.pure-u-0 { display: none !important; } 92}
93
77@media screen and (min-width: 35.5em) { 94@media screen and (min-width: 35.5em) {
78 .pure-u-sm-0 { display: none !important; } 95 .pure-u-sm-0 {
79 .pure-u-sm-visible { display: inline-block !important; } 96 display: none !important;
97 }
98
99 .pure-u-sm-visible {
100 display: inline-block !important;
101 }
80} 102}
103
81@media screen and (min-width: 48em) { 104@media screen and (min-width: 48em) {
82 .pure-u-md-0 { display: none !important; } 105 .pure-u-md-0 {
83 .pure-u-md-visible { display: inline-block !important; } 106 display: none !important;
107 }
108
109 .pure-u-md-visible {
110 display: inline-block !important;
111 }
84} 112}
113
85@media screen and (min-width: 64em) { 114@media screen and (min-width: 64em) {
86 .pure-u-lg-0 { display: none !important; } 115 .pure-u-lg-0 {
87 .pure-u-lg-visible { display: inline-block !important; } 116 display: none !important;
117 }
118
119 .pure-u-lg-visible {
120 display: inline-block !important;
121 }
88} 122}
123
89@media screen and (min-width: 80em) { 124@media screen and (min-width: 80em) {
90 .pure-u-xl-0 { display: none !important; } 125 .pure-u-xl-0 {
91 .pure-u-xl-visible { display: inline-block !important; } 126 display: none !important;
127 }
128
129 .pure-u-xl-visible {
130 display: inline-block !important;
131 }
92} 132}
93 133
94/** 134// Make pure-extras alert closable.
95 * Make pure-extras alert closable. 135.pure-alert-closable {
96 */ 136 .fa-times {
97.pure-alert-closable .fa-times {
98 float: right; 137 float: right;
138 }
99} 139}
140
100.pure-alert-close { 141.pure-alert-close {
101 cursor: pointer; 142 cursor: pointer;
102} 143}
103 144
104.pure-alert-success { 145.pure-alert-success {
105 background-color: #1b926c; 146 background-color: $main-green;
106} 147}
107 148
108.anchor:target { 149.anchor {
150 &:target {
109 padding-top: 40px; 151 padding-top: 40px;
152 }
110} 153}
111/** 154
112 * MENU 155// MENU
113 **/
114.shaarli-menu { 156.shaarli-menu {
115 position: fixed; 157 position: fixed;
116 top: 0; 158 top: 0;
117 width: 100%; 159 transition: max-height .5s;
118 --height: 50px; 160 z-index: 999;
119 background: #1b926c; 161 background: $main-green;
120 -webkit-font-smoothing: antialiased; 162 width: 100%;
121 /* Hack to transition with auto height: http://stackoverflow.com/a/8331169/1484919 */ 163 // Hack to transition with auto height: http://stackoverflow.com/a/8331169/1484919
122 max-height: 45px; 164 max-height: 45px;
123 transition: max-height 0.5s; 165 overflow: hidden;
124 overflow: hidden; 166 -webkit-font-smoothing: antialiased;
125 z-index: 999; 167
168 &.open {
169 transition: max-height .75s;
170 max-height: 500px;
171 }
126} 172}
127 173
128/* Chrome bugfix: with 100% height, it only displays the first element. */
129.pure-menu-item { 174.pure-menu-item {
130 height: 45px; 175 // Chrome bugfix: with 100% height, it only displays the first element.
131} 176 height: 45px;
132 177
133.shaarli-menu.open { 178 &:hover {
134 max-height: 500px; 179 &::after {
135 transition: max-height 0.75s; 180 display: block;
181 margin: -4px auto 0;
182 background: $white;
183 width: 100%;
184 height: 4px;
185 content: '';
186 }
187 }
136} 188}
137 189
138.head-logo { 190.head-logo {
139 float: left; 191 float: left;
140 margin: 0 5px 0 0; 192 margin: 0 5px 0 0;
141} 193}
142 194
143.pure-menu-link, 195%menu-link {
144.pure-menu-link:visited, 196 padding: .8em 1em;
145.pure-menu-selected .pure-menu-link, 197 color: $almost-white;
146.pure-menu-selected .pure-menu-link:visited {
147 padding: 0.8em 1em;
148 color: #f5f5f5;
149} 198}
150 199
151.pure-menu-link:hover, .pure-menu-link:focus, 200%menu-link-hover {
152.pure-menu-selected .pure-menu-link:hover, 201 background: transparent;
153.pure-menu-selected .pure-menu-link:focus { 202 color: $white;
154 color: #fff;
155 background: transparent;
156} 203}
157 204
158.pure-menu-item:hover::after { 205.pure-menu-link {
159 margin: -4px auto 0 auto; 206 @extend %menu-link;
160 display: block; 207
161 content:""; 208 &:visited {
162 background: #fff; 209 @extend %menu-link;
163 height: 4px; 210 }
164 width: 100%; 211
212 &:hover,
213 &:focus {
214 @extend %menu-link-hover;
215 }
165} 216}
166 217
167.menu-toggle { 218.pure-menu-selected {
168 width: 34px; 219 .pure-menu-link {
169 height: 45px; 220 @extend %menu-link;
170 position: absolute; 221
171 top: 5px; 222 &:visited {
172 right: 0; 223 @extend %menu-link;
173 display: none; 224 }
225
226 &:hover,
227 &:focus {
228 @extend %menu-link-hover;
229 }
230 }
174} 231}
175 232
176.menu-toggle .bar { 233.menu-toggle {
177 background-color: #b0ddce; 234 display: none;
235 position: absolute;
236 top: 5px;
237 right: 0;
238 width: 34px;
239 height: 45px;
240
241 .bar {
178 display: block; 242 display: block;
179 width: 20px;
180 height: 2px;
181 border-radius: 100px;
182 position: absolute; 243 position: absolute;
183 top: 18px; 244 top: 18px;
184 right: 7px; 245 right: 7px;
185 transition: all 0.5s; 246 border-radius: 100px;
186} 247 background-color: $light-green;
248 width: 20px;
249 height: 2px;
250 transition-duration: .5s;
187 251
188.menu-toggle .bar:first-child { 252 &:first-child {
189 transform: translateY(-6px); 253 transform: translateY(-6px);
190} 254 }
255 }
191 256
192.menu-toggle.x .bar { 257 &.x {
193 transform: rotate(45deg); 258 .bar {
194} 259 transform: rotate(45deg);
195 260
196.menu-toggle.x .bar:first-child { 261 &:first-child {
197 transform: rotate(-45deg); 262 transform: rotate(-45deg);
263 }
264 }
265 }
198} 266}
199 267
200@media screen and (max-width: 64em) { 268@media screen and (max-width: 64em) {
201 .menu-toggle { 269 .menu-toggle {
202 display: block; 270 display: block;
203 } 271 }
204} 272}
205 273
206.header-buttons { 274.header-buttons {
207 text-align: right; 275 text-align: right;
208} 276}
209 277
210.linkcount { 278.linkcount {
211 color: #252525; 279 color: $dark-grey;
212 font-size: 0.8em; 280 font-size: .8em;
213} 281}
214 282
215@media screen and (min-width: 64em) { 283@media screen and (min-width: 64em) {
216 .linkcount { 284 .linkcount {
217 position: absolute; 285 position: absolute;
218 right: 5px; 286 right: 5px;
287 }
288}
289
290.searchform-block {
291 width: 100%;
292 text-align: center;
293
294 input {
295 &[type='text'] {
296 border: medium none currentColor;
297 border-radius: 2px;
298 box-shadow: 0 1px 0 $light-shadow, 0 1px 1px $dark-shadow inset;
299 background: $almost-white;
300 padding: 0 5px;
301 width: 260px;
302 height: 30px;
303 color: $dark-grey;
304
305 &::-webkit-input-placeholder {
306 color: $light-grey;
307 }
219 } 308 }
220} 309 }
221
222#search, #search-linklist, #search-tagcloud {
223 text-align: center;
224 width: 100%;
225}
226 310
227#search input[type="text"], #search-linklist input[type="text"] { 311 button {
228 padding: 0 5px; 312 border: 0;
229 height: 30px;
230 width: 260px;
231 background: #f5f5f5;
232 border: medium none currentColor;
233 box-shadow: 0 1px 0 rgba(255, 255, 255, 0.078), 0 1px 1px rgba(0, 0, 0, 0.298) inset;
234 border-radius: 2px; 313 border-radius: 2px;
235 color: #252525; 314 background-color: $main-green;
236} 315 padding: 4px 8px 6px;
237@media screen and (max-width: 64em) { 316 color: $almost-white;
238 .searchform { 317 }
239 max-width: 260px;
240 margin: 0 auto;
241 }
242}
243
244/* because chrome */
245#search input[type="text"]::-webkit-input-placeholder,
246#search-linklist input[type="text"]::-webkit-input-placeholder {
247 color: #777777;
248} 318}
249 319
250#search button, 320@media screen and (max-width: 64em) {
251#search-tagcloud button, 321 .searchform {
252#search-linklist button { 322 margin: 0 auto;
253 padding: 4px 8px 6px 8px; 323 max-width: 260px;
254 background-color: #1B926C; 324 }
255 color: #f5f5f5;
256 border: none;
257 border-radius: 2px;
258} 325}
259 326
260#search-tagcloud button { 327.search-tagcloud {
328 button {
261 width: 90%; 329 width: 90%;
330 }
262} 331}
263 332
264@media screen and (max-width: 64em) { 333@media screen and (max-width: 64em) {
265 #search-linklist button { 334 .search-linklist {
266 width: 100%; 335 button {
336 width: 100%;
267 } 337 }
268 #search-linklist .awesomplete {
269 margin: 5px 0;
270 }
271}
272 338
273#search button:hover, 339 .awesomplete {
274#search-linklist button:hover, 340 margin: 5px 0;
275#search-tagcloud button:hover {
276 color: #d0d0d0;
277}
278
279#search,
280#search-linklist {
281 padding: 6px 0;
282}
283
284@media screen and (max-width: 64em) {
285 #search, #search * {
286 visibility: hidden;
287 } 341 }
342 }
288} 343}
289 344
290.subheader-form a.button { 345.header-search,
291 color: #f5f5f5; 346.search-linklist,
292 font-weight: bold; 347.search-tagcloud {
293 text-decoration: none; 348 button {
294 border: 2px solid #f5f5f5; 349 &:hover {
295 border-radius: 5px; 350 color: $background-color;
296 padding: 3px 10px; 351 }
352 }
297} 353}
298 354
299.linklist-item-editbuttons .delete-checkbox { 355.header-search,
300 display: none; 356.search-linklist {
357 padding: 6px 0;
301} 358}
302 359
303#header-login-form input[type="text"], #header-login-form input[type="password"] { 360@media screen and (max-width: 64em) {
304 width: 200px; 361 .header-search ,
362 .header-search * {
363 visibility: hidden;
364 }
305} 365}
306 366
307/* because chrome */ 367%subheader-form-input {
308#header-login-form input[type="text"]::-webkit-input-placeholder, 368 border: medium none currentColor;
309#header-login-form input[type="password"]::-webkit-input-placeholder { 369 border-radius: 2px;
310 color: #777777; 370 box-shadow: 0 1px 0 $light-shadow, 0 1px 4px $dark-shadow inset;
371 background: $almost-white;
372 padding: 5px 5px 3px 15px;
373 width: 20%;
374 height: 20px;
375 color: $dark-grey;
311} 376}
312 377
313.subheader-form { 378.subheader-form {
314 visibility: hidden; 379 display: block;
315 position: fixed; 380 position: fixed;
316 width: 100%; 381 visibility: hidden;
317 text-align: center; 382 z-index: 999;
318 background: #1b926c; 383 background: $main-green;
319 display: block; 384 padding: 5px 0;
320 z-index: 999; 385 width: 100%;
321 height: 30px; 386 height: 30px;
322 padding: 5px 0; 387 text-align: center;
323} 388
324 389 input {
325@media screen and (min-width: 64em) { 390 &[type='text'],
326 .subheader-form.open, .subheader-form.open * { 391 &[type='password'] {
327 visibility: visible; 392 @extend %subheader-form-input;
393
394 &::-webkit-input-placeholder {
395 color: $dark-grey;
396 }
328 } 397 }
329} 398 }
330 399
331.subheader-form input[type="text"], .subheader-form input[type="password"], .subheader-form .remember-me { 400 &[type='submit'] {
332 padding: 5px 5px 3px 15px; 401 display: inline-block;
333 height: 20px; 402 margin: 0 0 5px;
334 width: 20%; 403 border: 1px solid $almost-white;
335 background: #f5f5f5;
336 border: medium none currentColor;
337 border-radius: 2px; 404 border-radius: 2px;
338 box-shadow: 0 1px 0 rgba(255, 255, 255, 0.078), 0 1px 4px rgba(0, 0, 0, 0.298) inset; 405 background: $main-green;
339 color: #252525; 406 padding: 4px 0;
340} 407 width: 100px;
408 height: 28px;
409 color: $almost-white;
341 410
342/* because chrome */ 411 &:hover {
343.subheader-form input[type="text"]::-webkit-input-placeholder, 412 background: $almost-white;
344.subheader-form input[type="password"]::-webkit-input-placeholder 413 color: $main-green;
345{ 414 }
346 color: #252525; 415 }
347} 416
417 .remember-me {
418 @extend %subheader-form-input;
348 419
349.subheader-form .remember-me {
350 display: inline-block; 420 display: inline-block;
351 width: auto;
352 padding: 5px 20px 3px 20px;
353 cursor: pointer; 421 cursor: pointer;
354} 422 padding: 5px 20px 3px;
423 width: auto;
355 424
356.subheader-form .remember-me label, .subheader-form .remember-me input { 425 label,
357 cursor: pointer; 426 input {
427 cursor: pointer;
428 }
429 }
430
431 a {
432 &.button {
433 border: 2px solid $almost-white;
434 border-radius: 5px;
435 padding: 3px 10px;
436 text-decoration: none;
437 color: $almost-white;
438 font-weight: bold;
439 }
440 }
358} 441}
359 442
360.subheader-form input[type="submit"] { 443.header-login-form {
361 display: inline-block; 444 input {
362 margin: 0 0 5px 0; 445 &[type='text'],
363 padding: 4px 0 4px 0; 446 &[type='password'] {
364 height: 28px; 447 width: 200px;
365 width: 100px; 448
366 background: #1b926c; 449 // because chrome
367 border: 1px solid #f5f5f5; 450 &::-webkit-input-placeholder {
368 color: #f5f5f5; 451 color: $light-grey;
369 border-radius: 2px; 452 }
453 }
454 }
370} 455}
371 456
372.subheader-form input[type="submit"]:hover { 457@media screen and (min-width: 64em) {
373 background: #f5f5f5; 458 .subheader-form {
374 color: #1b926c; 459 &.open {
460 visibility: visible;
461
462 * {
463 visibility: visible;
464 }
465 }
466 }
375} 467}
376 468
377.new-version-message { 469.new-version-message {
378 text-align: center; 470 text-align: center;
379}
380 471
381.new-version-message a { 472 a {
382 color: rgb(151, 96, 13); 473 color: $warning-text;
383 font-weight: bold; 474 font-weight: bold;
475 }
384} 476}
385 477
386/** 478// CONTENT - GENERAL
387 * CONTENT - GENERAL 479.container {
388 */ 480 position: relative;
389#content { 481 z-index: 2;
390 position: relative; 482 margin-top: 45px;
391 z-index: 2;
392 margin-top: 45px;
393} 483}
394 484
395/** 485// Plugins additional forms
396 * Plugins additional forms
397 */
398.toolbar-plugin { 486.toolbar-plugin {
399 margin: 5px 0; 487 margin: 5px 0;
400 text-align: center; 488 text-align: center;
401} 489
402 490 input {
403.toolbar-plugin input[type="text"] { 491 &[type='text'] {
404 padding: 0 5px; 492 border: medium none currentColor;
405 height: 30px; 493 border-radius: 2px;
406 width: 300px; 494 box-shadow: 0 1px 0 $light-shadow, 0 1px 1px $dark-shadow inset;
407 background: #f5f5f5; 495 background: $almost-white;
408 border: medium none currentColor; 496 padding: 0 5px;
409 box-shadow: 0 1px 0 rgba(255, 255, 255, 0.078), 0 1px 1px rgba(0, 0, 0, 0.298) inset; 497 width: 300px;
410 border-radius: 2px; 498 height: 30px;
411 color: #252525; 499 color: $dark-grey;
412} 500
413 501 &::-webkit-input-placeholder {
414/* because chrome */ 502 color: $light-grey;
415.toolbar-plugin input[type="text"]::-webkit-input-placeholder { 503 }
416 color: #777777; 504 }
417}
418
419.toolbar-plugin input[type="submit"] {
420 padding: 0 10px;
421 height: 30px;
422 background: #f5f5f5;
423 border: medium none currentColor;
424 border-radius: 2px;
425 color: #252525;
426}
427 505
428.toolbar-plugin input[type="submit"]:hover { 506 &[type='submit'] {
429 background: #fff; 507 border: medium none currentColor;
508 border-radius: 2px;
509 background: $almost-white;
510 padding: 0 10px;
511 height: 30px;
512 color: $dark-grey;
513
514 &:hover {
515 background: $white;
516 }
517 }
518 }
430} 519}
431 520
432@media screen and (max-width: 64em) { 521@media screen and (max-width: 64em) {
433 .toolbar-plugin input[type="text"] { 522 .toolbar-plugin {
523 input {
524 &[type='text'] {
434 width: 70%; 525 width: 70%;
435 526 }
436 } 527 }
528 }
437} 529}
438 530
439/** 531// CONTENT - LINKLIST PAGING
440 * CONTENT - LINKLIST PAGING 532// 64em -> lg
441 * 64em -> lg
442 */
443.linklist-filters { 533.linklist-filters {
444 margin: 5px 0; 534 margin: 5px 0;
445 color: #252525; 535 color: $dark-grey;
446 font-size: 0.9em; 536 font-size: .9em;
447}
448 537
449.linklist-filters a { 538 a {
450 padding: 5px 8px; 539 padding: 5px 8px;
451 text-decoration: none; 540 text-decoration: none;
452} 541 }
453 542
454.linklist-filters .filter-off { 543 .filter-off {
455 color: #252525; 544 background: $almost-white;
456 background: #f5f5f5; 545 color: $dark-grey;
457} 546 }
458 547
459.linklist-filters .filter-on { 548 .filter-on {
460 color: #b0ddce; 549 background: $main-green;
461 background: #1b926c; 550 color: $light-green;
462} 551 }
463 552
464.linklist-filters .filter-block { 553 .filter-block {
465 color: #f5f5f5; 554 background: $red;
466 background: #ac2925; 555 color: $almost-white;
556 }
467} 557}
468 558
469.linklist-pages { 559.linklist-pages {
470 margin: 5px 0; 560 margin: 5px 0;
471 color: #252525; 561 text-align: center;
472 text-align: center; 562 color: $dark-grey;
473}
474 563
475.linklist-pages a { 564 a {
476 color: #252525;
477 text-decoration: none; 565 text-decoration: none;
566 color: $dark-grey;
567
568 &:hover {
569 color: $white;
570 }
571 }
478} 572}
479 573
480.linklist-pages a:hover { 574%linksperpage-button {
481 color: #fff; 575 display: inline-block;
576 width: 20px;
577 text-align: center;
482} 578}
483 579
484.linksperpage { 580.linksperpage {
485 margin: 5px 0; 581 margin: 5px 0;
486 text-align: right; 582 text-align: right;
487 color: #252525; 583 color: $dark-grey;
488 font-size: 0.9em; 584 font-size: .9em;
489}
490 585
491.linksperpage a { 586 form {
492 padding: 5px 5px; 587 display: inline;
493 text-decoration: none; 588 }
494 color: #252525;
495 background: #f5f5f5;
496}
497 589
498.linksperpage a, .linksperpage input[type="text"] { 590 a {
499 display: inline-block; 591 @extend %linksperpage-button;
500 width: 20px;
501 text-align: center;
502}
503 592
504.linksperpage form { 593 background: $almost-white;
505 display: inline; 594 padding: 5px;
595 text-decoration: none;
596 color: $dark-grey;
597 }
598
599 input {
600 &[type='text'] {
601 @extend %linksperpage-button;
602
603 margin: 0;
604 border: medium none currentColor;
605 background: $almost-white;
606 padding: 4px 5px 3px 8px;
607 height: 20px;
608 color: $dark-grey;
609 font-size: .8em;
610 }
611 }
506} 612}
507 613
508.linksperpage input[type="text"] { 614// CONTENT - LINKLIST ITEMS
509 height: 20px; 615%private-border {
510 margin: 0; 616 display: block;
511 padding: 4px 5px 3px 8px; 617 position: absolute;
512 background: #f5f5f5; 618 top: 0;
513 border: medium none currentColor; 619 left: 3px;
514 color: #252525; 620 z-index: 1;
515 font-size: 0.8em; 621 background: $orange;
622 width: 2px;
623 height: 96%;
624 content: '';
516} 625}
517 626
518/**
519 * CONTENT - LINKLIST ITEMS
520 */
521.linklist-item { 627.linklist-item {
522 margin: 0 0 10px 0; 628 margin: 0 0 10px;
523 background: #f5f5f5; 629 box-shadow: 1px 1px 3px $light-grey;
524 box-shadow: 1px 1px 3px #797979; 630 background: $almost-white;
631
632 &.private {
633 .linklist-item-title {
634 &::before {
635 @extend %private-border;
636 margin-top: 3px;
637 }
638 }
639
640 .linklist-item-description {
641 &::before {
642 @extend %private-border;
643 height: 100%;
644 }
645 }
646 }
525} 647}
526 648
527.linklist-item-buttons { 649.linklist-item-buttons {
528 background: transparent; 650 position: relative;
529 position: relative; 651 z-index: 99;
530 width: 23px; 652 background: transparent;
531 z-index: 99; 653 width: 23px;
532} 654}
533 655
534.linklist-item-buttons-right { 656.linklist-item-buttons-right {
535 float: right; 657 float: right;
536 margin-right: -25px; 658 margin-right: -25px;
537} 659}
538 660
539.linklist-item-buttons * { 661.linklist-item-buttons * {
540 display: block; 662 display: block;
541 float: left; 663 float: left;
542 width:100%; 664 margin: auto;
543 margin: auto; 665 width: 100%;
544 text-align: center; 666 text-align: center;
545}
546
547.linklist-item-title, .linklist-item-title h2 {
548 margin: 0;
549 word-wrap: break-word;
550} 667}
551 668
552.linklist-item-title { 669.linklist-item-title {
553 position: relative; 670 position: relative;
554 background: #f5f5f5; 671 margin: 0;
555} 672 background: $almost-white;
673 word-wrap: break-word;
556 674
557.linklist-item-title h2 { 675 h2 {
558 padding: 3px 10px 0 10px; 676 margin: 0;
677 padding: 3px 10px 0;
559 line-height: 30px; 678 line-height: 30px;
560} 679 word-wrap: break-word;
561 680
562.linklist-item-title h2 a { 681 a {
563 font-size: 0.7em; 682 vertical-align: middle;
564 color: #252525; 683 text-decoration: none;
565 text-decoration: none; 684 color: $dark-grey;
566 vertical-align: middle; 685 font-size: .7em;
567} 686
687 &:visited {
688 .linklist-link {
689 color: $dark-green;
690 }
691 }
692
693 &:hover {
694 color: $dark-grey;
695 }
696 }
697 }
568 698
569.linklist-item-title .linklist-link { 699 .linklist-link {
700 color: $main-green;
570 font-size: 1.1em; 701 font-size: 1.1em;
571 color: #1b926c;
572}
573
574.linklist-item-title h2 a:visited .linklist-link {
575 color: #2a4c41;
576}
577
578.linklist-item-title h2 a:hover, .linklist-item-title .linklist-link:hover{
579 color: #252525;
580}
581 702
703 &:hover {
704 color: $dark-grey;
705 }
706 }
582 707
583.linklist-item-title .label-private { 708 .label-private {
584 border: solid 1px #F89406; 709 border: solid 1px $orange;
710 color: $orange;
585 font-family: Arial, sans-serif; 711 font-family: Arial, sans-serif;
586 font-size: 0.65em; 712 font-size: .65em;
587 color: #F89406; 713 }
588} 714}
589 715
590.fold-button { 716.fold-button {
591 display: none; 717 display: none;
592 color: #252525; 718 color: $dark-grey;
593} 719}
594 720
595.linklist-item-editbuttons { 721.linklist-item-editbuttons {
596 float: right; 722 float: right;
597 padding: 8px 5px; 723 padding: 8px 5px;
598}
599 724
600.linklist-item-editbuttons * { 725 * {
601 display: block; 726 display: block;
602 float: left; 727 float: left;
603 margin: 0 1px; 728 margin: 0 1px;
604} 729 }
605 730
606.linklist-item-editbuttons a { 731 a {
607 font-size: 1em; 732 font-size: 1em;
733 }
734
735 .delete-checkbox {
736 display: none;
737 }
608} 738}
609 739
610.edit-link { 740.edit-link {
611 font-size: 1.2em; 741 color: $blue;
612 color: #0b5ea6; 742 font-size: 1.2em;
613} 743}
614 744
615.delete-link { 745.delete-link {
616 font-size: 1.3em; 746 color: $red !important;
617 color: #ac2925 !important; 747 font-size: 1.3em;
618} 748}
619 749
620.linklist-item-description { 750.linklist-item-description {
621 position: relative; 751 position: relative;
622 padding: 0 10px; 752 padding: 0 10px;
623 word-wrap: break-word; 753 line-height: 1.3em;
624 color: #252525; 754 color: $dark-grey;
625 line-height: 1.3em; 755 word-wrap: break-word;
626}
627 756
628.linklist-item-description a { 757 a {
629 text-decoration: none; 758 text-decoration: none;
630 color: #1b926c; 759 color: $main-green;
631}
632 760
633.linklist-item-description a:hover { 761 &:hover {
634 color: #252525; 762 color: $dark-grey;
635} 763 }
636 764
637.linklist-item-description a:visited { 765 &:visited {
638 color: #14553f; 766 color: $dark-green;
767 }
768 }
639} 769}
640 770
641.linklist-item-thumbnail { 771.linklist-item-thumbnail {
642 position: relative; 772 position: relative;
643 padding: 0 0 0 5px; 773 float: right;
644 margin: 0; 774 z-index: 50;
645 float: right; 775 margin: 0;
646 z-index: 50; 776 padding: 0 0 0 5px;
647 height: 90px; 777 height: 90px;
648}
649
650.linklist-item.private .linklist-item-title::before,
651.linklist-item.private .linklist-item-description::before {
652 position: absolute;
653 left: 3px;
654 top: 0;
655 display: block;
656 content:"";
657 background: #F89406;
658 height: 96%;
659 width: 2px;
660 z-index: 1;
661}
662
663.linklist-item.private .linklist-item-description::before {
664 height: 100%;
665}
666
667.linklist-item.private .linklist-item-title::before {
668 margin-top: 3px;
669} 778}
670 779
671.linklist-item-infos { 780.linklist-item-infos {
672 padding: 4px 8px 4px 8px; 781 background: $background-linklist-info;
673 background: #ddd; 782 padding: 4px 8px;
674 color: #252525; 783 color: $dark-grey;
675}
676 784
677.linklist-item-infos a { 785 a {
678 color: #252525;
679 text-decoration: none; 786 text-decoration: none;
680} 787 color: $dark-grey;
681 788
682.linklist-item-infos a:hover { 789 &:hover {
683 color: #000; 790 color: $black;
684} 791 }
792 }
685 793
686.linklist-item-infos .linklist-item-tags { 794 .linklist-item-tags {
687 font-size: 0.8em; 795 font-size: .8em;
688} 796 }
689 797
690.linklist-item-infos .label-tag { 798 .label-tag {
691 font-size: 1em; 799 font-size: 1em;
800 }
801
802 .mobile-buttons {
803 text-align: right;
804 }
805
806 .linklist-plugin-icon {
807 display: inline-block;
808 margin: 0 2px;
809 width: 16px;
810 height: 16px;
811 }
692} 812}
693 813
694.linklist-item-infos-dateblock { 814.linklist-item-infos-dateblock {
695 font-size: 0.9em; 815 font-size: .9em;
696} 816}
697 817
698.linklist-plugin-icon { 818.linklist-plugin-icon {
699 width: 13px; 819 width: 13px;
700 height: 13px; 820 height: 13px;
701} 821}
702 822
703.linklist-item-infos-url { 823.linklist-item-infos-url {
704 text-align: right; 824 height: 23px;
705 white-space: nowrap; 825 overflow: hidden;
706 overflow: hidden; 826 text-align: right;
707 text-overflow: ellipsis; 827 text-overflow: ellipsis;
708 font-size: 0.8em; 828 line-height: 23px;
709 height:23px; 829 white-space: nowrap;
710 line-height:23px; 830 font-size: .8em;
711}
712
713.linklist-item-infos .mobile-buttons {
714 text-align: right;
715}
716
717.linklist-item-infos .linklist-plugin-icon {
718 display: inline-block;
719 margin: 0 2px;
720 width: 16px;
721 height: 16px;
722} 831}
723 832
724.linklist-item-infos-controls-group { 833.linklist-item-infos-controls-group {
725 display: inline-block; 834 display: inline-block;
726 border-right: 1px solid #5d5d5d; 835 border-right: 1px solid $light-grey;
727 padding-right: 6px; 836 padding-right: 6px;
728} 837}
729 838
730.ctrl-edit { 839.ctrl-edit {
731 margin: 0 7px; 840 margin: 0 7px;
732} 841}
733 842
734/** 64em -> lg **/ 843// 64em -> lg
735@media screen and (max-width: 64em) { 844@media screen and (max-width: 64em) {
736 .linklist-item-infos-url { 845 .linklist-item-infos-url {
737 text-align: left; 846 text-align: left;
738 } 847 }
739} 848}
740 849
741/** 850// Footer
742 * Footer 851.footer-container {
743 */ 852 margin: 20px 0;
744#footer { 853 padding: 5px;
745 margin: 20px 0; 854 text-align: center;
746 padding: 5px; 855 color: $dark-grey;
747 text-align: center;
748 color: #252525;
749}
750 856
751#footer:before { 857 &::before {
752 display: block; 858 display: block;
753 content:"";
754 background: linear-gradient(to right, #949393, #252525, #949393);
755 height: 1px;
756 width: 80%;
757 margin: 10px auto; 859 margin: 10px auto;
860 background: linear-gradient(to right, $background-color, $dark-grey, $background-color);
861 width: 80%;
862 height: 1px;
863 content: '';
864 }
865
866 a {
867 color: $dark-grey;
868 }
869}
870
871// PAGE FORM
872%page-form-input {
873 margin: 10px 0;
874 border: solid 1px $form-input-border;
875 border-radius: 2px;
876 background: $form-input-background;
877 padding: 5px 5px 3px 15px;
878 width: 90%;
879 height: 35px;
880 color: $dark-grey;
881 box-sizing: border-box;
882}
883
884%page-form-button {
885 display: inline-block;
886 margin: 15px 5px;
887 border: 0;
888 box-shadow: 1px 1px 1px $form-input-border, -1px -1px 6px $form-input-border, -1px 1px 2px $form-input-border, 1px -1px 2px $form-input-border;
889 background: $main-green;
890 min-width: 150px;
891 height: 35px;
892 vertical-align: center;
893 text-decoration: none;
894 line-height: 35px;
895 color: $almost-white;
896 font-size: 1.2em;
897 font-weight: normal;
758} 898}
759 899
760#footer a {
761 color: #252525;
762}
763
764/**
765 * PAGE FORM
766 */
767.page-form { 900.page-form {
768 margin: 20px 0 0 0; 901 margin: 20px 0 0;
769 background: #f5f5f5; 902 box-shadow: 1px 1px 2px $light-grey;
770 box-shadow: 1px 1px 2px #797979; 903 background: $almost-white;
771 color: #252525; 904 overflow: hidden;
772 overflow: hidden; 905 color: $dark-grey;
773} 906
774 907 .window-title {
775.page-form .window-title { 908 margin: 0 0 10px;
776 margin: 0 0 10px 0; 909 background: $almost-white;
777 padding: 10px 0; 910 padding: 10px 0;
778 width: 100%; 911 width: 100%;
779 color: #1b926c;
780 background: #f5f5f5;
781 text-align: center; 912 text-align: center;
782} 913 color: $main-green;
914 }
783 915
784.page-form .window-subtitle { 916 .window-subtitle {
785 text-align: center; 917 text-align: center;
786} 918 }
787 919
788.page-form a { 920 a {
789 color: #1b926c;
790 font-weight: bold;
791 text-decoration: none; 921 text-decoration: none;
792} 922 color: $main-green;
923 font-weight: bold;
793 924
794.page-form p { 925 &.button {
795 padding: 5px 10px; 926 @extend %page-form-button;
927 }
928 }
929
930 p {
796 margin: 0; 931 margin: 0;
797} 932 padding: 5px 10px;
933 }
798 934
799.page-form input[type="text"], 935 input {
800.page-form input[type="password"], 936 &[type='text'] {
801.page-form textarea { 937 @extend %page-form-input;
802 box-sizing: border-box; 938
803 margin: 10px 0; 939 &::-webkit-input-placeholder {
804 padding: 5px 5px 3px 15px; 940 color: $light-grey;
805 height: 35px; 941 }
806 width: 90%; 942 }
807 background: #eeeeee; 943
808 border: solid 1px #d8d8d8; 944 &[type='password'] {
809 border-radius: 2px; 945 @extend %page-form-input;
810 color: #252525; 946
811} 947 &::-webkit-input-placeholder {
948 color: $light-grey;
949 }
950 }
951
952 &[type='submit'] {
953 @extend %page-form-button;
954 }
955 }
956
957 textarea {
958 @extend %page-form-input;
812 959
813.page-form textarea {
814 min-height: 240px;
815 padding: 15px 5px 3px 15px; 960 padding: 15px 5px 3px 15px;
961 min-height: 240px;
816 resize: vertical; 962 resize: vertical;
817 overflow-y: auto; 963 overflow-y: auto;
818 word-wrap:break-word 964 word-wrap: break-word;
819} 965 }
820 966
821/* because chrome */ 967 select {
822.page-form input[type="text"]::-webkit-input-placeholder, 968 color: $dark-grey;
823.page-form input[type="password"]::-webkit-input-placeholder { 969 }
824 color: #777777;
825}
826 970
827.page-form input[type="submit"], .page-form a.button { 971 .button {
828 margin: 15px 5px; 972 &.button-red {
829 height: 35px; 973 background: $red;
830 line-height: 35px; 974 }
831 width: 150px; 975 }
832 background: #1b926c;
833 color: #f5f5f5;
834 border: none;
835 box-shadow: 1px 1px 1px #ddd, -1px -1px 6px #ddd, -1px 1px 2px #ddd, 1px -1px 2px #ddd;
836 font-size: 1.2em;
837 text-decoration: none;
838 vertical-align: center;
839 font-weight: normal;
840 display: inline-block;
841}
842 976
977 .submit-buttons {
978 margin-bottom: 10px;
979 }
843 980
844.page-form .button.button-red { 981 section {
845 background: #ac2925; 982 margin: 10px 0 25px;
846} 983 }
847 984
848.page-form .submit-buttons { 985 table,
849 margin-bottom: 10px; 986 th,
850} 987 td {
988 border-width: 1px 0;
989 border-style: solid;
990 border-color: $light-grey;
991 }
851 992
852@media screen and (min-width: 64em) { 993 th,
853 .page-form .submit-buttons { 994 td {
854 position: relative; 995 padding: 5px;
996 }
997
998 table {
999 margin: auto;
1000 width: 90%;
1001
1002 .order {
1003 text-decoration: none;
1004 color: $dark-grey;
855 } 1005 }
1006 }
856 1007
857 .page-form .submit-buttons .button.button-red { 1008 .awesomplete {
858 position: absolute; 1009 width: 90%;
859 right: 5%; 1010
1011 input {
1012 width: 100%;
1013 }
1014 }
1015
1016 div {
1017 .awesomplete {
1018 > ul {
1019 color: $black;
1020 }
860 } 1021 }
1022 }
1023}
1024
1025@media screen and (min-width: 64em) {
1026 .page-form {
1027 .submit-buttons {
1028 position: relative;
1029
1030 .button {
1031 &.button-red {
1032 position: absolute;
1033 right: 5%;
1034 }
1035 }
1036 }
1037 }
861} 1038}
862 1039
863@media screen and (max-width: 64em) { 1040@media screen and (max-width: 64em) {
864 .page-form .submit-buttons .button { 1041 .page-form {
1042 .submit-buttons {
1043 .button {
865 display: block; 1044 display: block;
866 margin: auto; 1045 margin: auto;
1046 }
867 } 1047 }
1048 }
868} 1049}
869 1050
870.page-form select { 1051// PAGE FORM - LIGHT
871 color: #252525; 1052.page-form-light {
872} 1053 div,
873 1054 p {
874/**
875 * PAGE FORM - LIGHT
876 */
877.page-form-light div, .page-form-light p {
878 text-align: center; 1055 text-align: center;
1056 }
879} 1057}
880 1058
881/** 1059// PAGE FORM - COMPLETE
882 * PAGE FORM - COMPLETE 1060%page-form-valign {
883 */ 1061 position: absolute;
884.page-form-complete div, .page-form-complete p { 1062 top: 50%;
885 color: #252525; 1063 transform: translateY(-50%);
886} 1064}
887 1065
888.page-form-complete .form-label, .page-form-complete .form-input { 1066.page-form-complete {
1067 div,
1068 p {
1069 color: $dark-grey;
1070 }
1071
1072 .form-label,
1073 .form-input {
889 position: relative; 1074 position: relative;
890 height: 60px; 1075 height: 60px;
891} 1076 }
892 1077
893.page-form-complete .form-label label, 1078 .form-label {
894.page-form-complete .form-input input, 1079 label {
895.page-form-complete .form-input select.align, 1080 @extend %page-form-valign;
896.page-form-complete .timezone {
897 position: absolute;
898 top: 50%;
899 transform: translateY(-50%);
900}
901 1081
902.page-form-complete .form-label label { 1082 right: 0;
903 text-align: right; 1083 padding: 0 20px;
904 right: 0; 1084 text-align: right;
905 padding: 0 20px; 1085 }
906} 1086 }
907 1087
908.page-form-complete .label-name { 1088 .label-name {
909 font-weight: bold; 1089 font-weight: bold;
910} 1090 }
911
912.page-form-complete .label-desc {
913 font-size: 0.8em;
914}
915 1091
916.page-form-complete input[type="text"], 1092 .label-desc {
917.page-form-complete input[type="password"], 1093 font-size: .8em;
918.page-form-complete textarea { 1094 }
919 margin: 0;
920}
921
922.page-form section {
923 margin: 10px 0 25px 0;
924}
925 1095
926.page-form table { 1096 .form-input {
927 margin: auto; 1097 input {
928 width: 90%; 1098 @extend %page-form-valign;
929}
930 1099
931.page-form table .order { 1100 &[type='text'],
932 text-decoration: none; 1101 &[type='password'] {
933 color: #252525; 1102 margin: 0;
934} 1103 }
1104 }
935 1105
936.page-form table, .page-form th, .page-form td { 1106 select {
937 border-width: 1px 0; 1107 &.align {
938 border-style: solid; 1108 @extend %page-form-valign;
939 border-color: #aaaaaa; 1109 }
940} 1110 }
1111 }
941 1112
942.page-form th, .page-form td { 1113 textarea {
943 padding: 5px; 1114 margin: 0;
1115 }
944 1116
1117 .timezone {
1118 @extend %page-form-valign;
1119 }
945} 1120}
946 1121
947/* Awesomeplete fix */ 1122// Awesomeplete fix
948div.awesomplete { 1123div {
1124 &.awesomplete {
949 width: inherit; 1125 width: inherit;
950}
951 1126
952div.awesomplete > input { 1127 > input {
953 display: inherit; 1128 display: inherit;
954} 1129 }
955 1130
956div.awesomplete > ul { 1131 > ul {
957 z-index: 9999; 1132 z-index: 9999;
1133 }
1134 }
958} 1135}
959 1136
960.page-form .awesomplete { 1137form {
961 width: 90%; 1138 &[name='linkform'] {
1139 &.page-form {
1140 overflow: visible;
1141 }
1142 }
962} 1143}
963 1144
964.page-form .awesomplete input { 1145@media screen and (max-width: 64em) {
965 width: 100%; 1146 %page-form-valign-mobile {
966} 1147 position: inherit;
1148 top: inherit;
1149 transform: translateY(0);
1150 }
967 1151
968.page-form div.awesomplete > ul { 1152 .page-form-complete {
969 color: black; 1153 .form-label {
970} 1154 height: inherit;
971 1155
972form[name="linkform"].page-form { 1156 label {
973 overflow: visible; 1157 @extend %page-form-valign-mobile;
974}
975 1158
976@media screen and (max-width: 64em) { 1159 display: block;
977 .page-form-complete .form-label { 1160 margin: 10px 0 0;
978 height: inherit; 1161 text-align: left;
1162 }
979 } 1163 }
980 1164
981 .page-form-complete .form-label label, 1165 .form-input {
982 .page-form-complete .form-input input, 1166 text-align: center;
983 .page-form-complete .timezone {
984 position: inherit;
985 top: inherit;
986 transform: translateY(0);
987 }
988 1167
989 .page-form-complete .form-input input[type="checkbox"] { 1168 input {
990 position: absolute; 1169 @extend %page-form-valign-mobile;
991 top: 50%;
992 right: 50%;
993 transform: translateY(-50%);
994 }
995 1170
996 .page-form-complete .form-input { 1171 &[type='checkbox'] {
997 text-align: center; 1172 position: absolute;
1173 top: 50%;
1174 right: 50%;
1175 transform: translateY(-50%);
1176 }
1177 }
998 } 1178 }
999 1179
1000 .page-form-complete .form-label label { 1180 .timezone {
1001 display: block; 1181 @extend %page-form-valign-mobile;
1002 text-align: left;
1003 margin: 10px 0 0 0;
1004 } 1182 }
1005 1183
1006 .timezone-continent:after { 1184 .radio-buttons {
1007 content:"\a\a"; 1185 padding: 5px 15px;
1008 white-space: pre; 1186 text-align: left;
1009 } 1187 }
1188 }
1010 1189
1011 .page-form-complete .radio-buttons { 1190 .timezone-continent {
1012 text-align: left; 1191 &::after {
1013 padding: 5px 15px; 1192 white-space: pre;
1193 content: '\a\a';
1014 } 1194 }
1195 }
1015} 1196}
1016 1197
1017/** 1198// Page visitor (page form extended)
1018 * Page visitor (page form extended)
1019 */
1020.page-visitor { 1199.page-visitor {
1021 color: #252525; 1200 color: $dark-grey;
1022} 1201}
1023 1202
1024#page404 { 1203.page404-container {
1025 color: #3f3f3f; 1204 color: $dark-grey;
1026} 1205}
1027 1206
1028/** 1207// EDIT LINK
1029 * EDIT LINK 1208.edit-link-container {
1030 */ 1209 .created-date {
1031#editlinkform .created-date {
1032 color: #767676;
1033 margin-bottom: 10px; 1210 margin-bottom: 10px;
1211 color: $light-grey;
1212 }
1034} 1213}
1035 1214
1036/** 1215// LOGIN
1037 * LOGIN 1216.login-form-container {
1038 */ 1217 .remember-me {
1039#login-form .remember-me {
1040 margin: 5px 0; 1218 margin: 5px 0;
1219 }
1041} 1220}
1042 1221
1043/** 1222// Search results
1044 * Search results 1223.search-result {
1045 */ 1224 a {
1046.search-result a {
1047 color: white;
1048 text-decoration: none; 1225 text-decoration: none;
1049} 1226 color: $white;
1227 }
1050 1228
1051.search-result .label-tag { 1229 .label-tag {
1052 border-color: white; 1230 border-color: $white;
1053}
1054 1231
1055.search-result .label-tag .remove { 1232 .remove {
1056 border-left: white 1px solid; 1233 margin: 0 0 0 5px;
1057 padding: 0 0 0 5px; 1234 border-left: $white 1px solid;
1058 margin: 0 0 0 5px; 1235 padding: 0 0 0 5px;
1059} 1236 }
1237 }
1060 1238
1061.search-result .label-private { 1239 .label-private {
1062 border: 1px solid white; 1240 border: 1px solid $white;
1241 }
1063} 1242}
1064 1243
1065/** 1244// TOOLS
1066 * TOOLS
1067 */
1068.tools-item { 1245.tools-item {
1069 margin: 10px 0; 1246 margin: 10px 0;
1070}
1071 1247
1072.tools-item .pure-button:hover { 1248 .pure-button {
1073 background-image: none; 1249 &:hover {
1074 background-color: #1b926c; 1250 background-color: $main-green;
1075 color: #f5f5f5; 1251 background-image: none;
1252 color: $almost-white;
1253 }
1254 }
1076} 1255}
1077 1256
1078/** 1257// PLUGIN ADMIN
1079 * PLUGIN ADMIN 1258.pluginform-container {
1080 */ 1259 .mobile-row {
1081#pluginform .mobile-row { 1260 font-size: .9em;
1082 font-size: 0.9em; 1261 }
1083}
1084 1262
1085#pluginform .more { 1263 .more {
1086 margin-top: 10px; 1264 margin-top: 10px;
1265 }
1087} 1266}
1088 1267
1089@media screen and (max-width: 64em) { 1268@media screen and (max-width: 64em) {
1090 #pluginform .main-row, #pluginform .main-row td { 1269 .pluginform-container {
1091 border-bottom-style: none; 1270 .main-row {
1092 } 1271 border-top-style: none;
1272 border-bottom-style: none;
1093 1273
1094 #pluginform .mobile-row, #pluginform .mobile-row td { 1274 td {
1095 border-top-style: none; 1275 border-top-style: none;
1276 border-bottom-style: none;
1277 }
1096 } 1278 }
1279 }
1097} 1280}
1098 1281
1099/** 1282// IMPORT
1100 * IMPORT 1283.import-field-container {
1101 */ 1284 margin: 15px 0;
1102#import-field {
1103 margin: 15px 0;
1104} 1285}
1105 1286
1106/** 1287// TAG CLOUD
1107 * TAG CLOUD 1288.cloudtag-container {
1108 */ 1289 padding: 10px;
1109#cloudtag { 1290 text-align: center;
1110 padding: 10px; 1291 text-decoration: none;
1111 text-align: center; 1292 color: $dark-grey;
1112}
1113 1293
1114#cloudtag, #cloudtag a { 1294 a {
1115 color: #252525;
1116 text-decoration: none; 1295 text-decoration: none;
1117} 1296 color: $dark-grey;
1297 }
1118 1298
1119#cloudtag .count { 1299 .count {
1120 color: #7f7f7f; 1300 color: $light-grey;
1301 }
1121} 1302}
1122 1303
1123/** 1304// TAG LIST
1124 * TAG LIST 1305.taglist-container {
1125 */ 1306 padding: 0 10px;
1126#taglist {
1127 padding: 0 10px;
1128}
1129 1307
1130#taglist a { 1308 a {
1131 color: #252525;
1132 text-decoration: none; 1309 text-decoration: none;
1133} 1310 color: $dark-grey;
1311 }
1134 1312
1135#taglist .count { 1313 .count {
1136 display: inline-block; 1314 display: inline-block;
1137 width: 35px; 1315 width: 35px;
1138 text-align: right; 1316 text-align: right;
1139 color: #7f7f7f; 1317 color: $light-grey;
1140} 1318 }
1141 1319
1142#taglist .rename-tag-form { 1320 .rename-tag-form {
1143 display: none; 1321 display: none;
1144} 1322 }
1145 1323
1146#taglist .delete-tag { 1324 .delete-tag {
1147 color: #ac2925;
1148 display: none; 1325 display: none;
1149} 1326 color: $red;
1150 1327 }
1151#taglist .rename-tag { 1328
1152 color: #0b5ea6; 1329 .rename-tag {
1153} 1330 color: $blue;
1154 1331 }
1155#taglist .validate-rename-tag { 1332
1156 color: #1b926c; 1333 .validate-rename-tag {
1157} 1334 color: $main-green;
1158 1335 }
1159/** 1336}
1160 * Picture wall CSS 1337
1161 */ 1338// Picture wall CSS
1162#picwall_container { 1339.picwall-container {
1163 margin: 0 10px 10px 10px; 1340 clear: both;
1164 color: #252525; 1341 margin: 0 10px 10px;
1165 background-color: #f5f5f5; 1342 background-color: $almost-white;
1166 clear: both; 1343 color: $dark-grey;
1167} 1344}
1168 1345
1169.picwall_pictureframe { 1346.picwall-pictureframe {
1170 margin: 2px; 1347 display: table-cell;
1171 background-color: #f5f5f5; 1348 position: relative;
1172 z-index: 5; 1349 float: left;
1173 position: relative; 1350 z-index: 5;
1174 display: table-cell; 1351 margin: 2px;
1175 vertical-align: middle; 1352 background-color: $almost-white;
1176 width: 90px; 1353 width: 90px;
1177 height: 90px; 1354 height: 90px;
1178 overflow: hidden; 1355 overflow: hidden;
1179 text-align: center; 1356 vertical-align: middle;
1180 float: left; 1357 text-align: center;
1181} 1358
1182 1359 // Adapt the width of the image
1183.b-lazy { 1360 img {
1184 -webkit-transition: opacity 500ms ease-in-out;
1185 -moz-transition: opacity 500ms ease-in-out;
1186 -o-transition: opacity 500ms ease-in-out;
1187 transition: opacity 500ms ease-in-out;
1188 opacity: 0;
1189}
1190.b-lazy.b-loaded {
1191 opacity: 1;
1192}
1193
1194.picwall_pictureframe img {
1195 max-width: 100%; 1361 max-width: 100%;
1196 height: auto; 1362 height: auto;
1197 color: transparent; 1363 color: transparent;
1198} /* Adapt the width of the image */ 1364 }
1199 1365
1200.picwall_pictureframe a { 1366 a {
1201 text-decoration: none; 1367 text-decoration: none;
1202} 1368 }
1203 1369
1204/* CSS to show title when hovering an image - no javascript required. */ 1370 span {
1205.picwall_pictureframe span.info { 1371 &.info {
1206 display: none; 1372 display: none;
1207 font-family: Arial, sans-serif; 1373 font-family: Arial, sans-serif;
1374 }
1375 }
1376
1377 // CSS to show title when hovering an image - no javascript required.
1378 &:hover {
1379 span {
1380 &.info {
1381 display: block;
1382 position: absolute;
1383 top: 0;
1384 left: 0;
1385 background-color: $dark-shadow;
1386 width: 90px;
1387 height: 90px;
1388 text-align: left;
1389 color: $almost-white;
1390 font-size: 9pt;
1391 font-weight: bold;
1392 }
1393 }
1394 }
1208} 1395}
1209 1396
1210.picwall_pictureframe:hover span.info { 1397.b-lazy {
1211 display: block; 1398 transition: opacity 500ms ease-in-out;
1212 position: absolute; 1399 opacity: 0;
1213 top: 0; 1400 -webkit-transition: opacity 500ms ease-in-out;
1214 left: 0; 1401 -moz-transition: opacity 500ms ease-in-out;
1215 width: 90px; 1402 -o-transition: opacity 500ms ease-in-out;
1216 height: 90px; 1403
1217 font-weight: bold; 1404 &.b-loaded {
1218 font-size: 9pt; 1405 opacity: 1;
1219 color: #f5f5f5; 1406 }
1220 text-align: left;
1221 background-color: rgba(0, 0, 0, 0.8);
1222} 1407}
1223 1408
1224/** 1409// DAILY
1225 * DAILY
1226 */
1227.daily-desc { 1410.daily-desc {
1228 color: #7f7f7f; 1411 color: $light-grey;
1229 font-size: 0.8em; 1412 font-size: .8em;
1230}
1231 1413
1232.daily-about a { 1414 a {
1233 color: #343434;
1234 text-decoration: none; 1415 text-decoration: none;
1235} 1416 color: $dark-grey;
1236
1237.daily-about a:hover {
1238 color: #7f7f7f;
1239}
1240 1417
1241.daily-about h3:before, .daily-about h3:after { 1418 &:hover {
1242 display: block; 1419 color: $light-grey;
1243 content:""; 1420 }
1244 background: linear-gradient(to right, #d5d4d4, #252525, #d5d4d4); 1421 }
1245 height: 1px; 1422}
1246 width: 90%; 1423
1247 margin: 10px auto; 1424.daily-about {
1425 h3 {
1426 &::before,
1427 &::after {
1428 display: block;
1429 margin: 10px auto;
1430 background: linear-gradient(to right, $background-color, $dark-grey, $background-color);
1431 width: 90%;
1432 height: 1px;
1433 content: '';
1434 }
1435 }
1248} 1436}
1249 1437
1250.daily-entry { 1438.daily-entry {
1251 padding: 0 10px; 1439 padding: 0 10px;
1252}
1253 1440
1254.daily-entry .daily-entry-title:after { 1441 .daily-entry-title {
1255 display: block; 1442 margin: 10px 0 0;
1256 content:"";
1257 background: linear-gradient(to right, #fff, #515151, #fff);
1258 height: 1px;
1259 width: 70%;
1260 margin: 5px auto;
1261}
1262 1443
1263.daily-entry .daily-entry-title { 1444 a {
1264 margin: 10px 0 0 0; 1445 text-decoration: none;
1265} 1446 color: $black;
1447 }
1266 1448
1267.daily-entry .daily-entry-title a { 1449 &::after {
1268 color: #000; 1450 display: block;
1269 text-decoration: none; 1451 margin: 5px auto;
1270} 1452 background: linear-gradient(to right, $white, $light-grey, $white);
1453 width: 70%;
1454 height: 1px;
1455 content: '';
1456 }
1457 }
1271 1458
1272.daily-entry .daily-entry-description { 1459 .daily-entry-description {
1273 padding: 5px 5px 0 5px; 1460 padding: 5px 5px 0;
1274 font-size: 0.9em;
1275 text-align: justify; 1461 text-align: justify;
1462 font-size: .9em;
1276 word-wrap: break-word; 1463 word-wrap: break-word;
1277} 1464 }
1278 1465
1279.daily-entry .daily-entry-tags { 1466 .daily-entry-tags {
1280 padding: 0 5px 5px 5px; 1467 padding: 0 5px 5px;
1281 font-size: 0.8em; 1468 font-size: .8em;
1469 }
1282} 1470}
1283 1471
1284.daily-entry-thumbnail { 1472.daily-entry-thumbnail {
1285 float: left; 1473 float: left;
1286 margin: 15px 5px 5px 15px; 1474 margin: 15px 5px 5px 15px;
1287} 1475}
1288 1476
1289.daily-entry-description a { 1477.daily-entry-description {
1478 a {
1290 text-decoration: none; 1479 text-decoration: none;
1291 color: #1b926c; 1480 color: $main-green;
1292}
1293 1481
1294.daily-entry-description a:hover { 1482 &:hover {
1295 text-shadow: 1px 1px #ddd; 1483 text-shadow: 1px 1px $background-linklist-info;
1296} 1484 }
1297 1485
1298.daily-entry-description a:visited { 1486 &:visited {
1299 color: #20b988; 1487 color: $dark-green;
1488 }
1489 }
1300} 1490}
1301 1491
1302/* 1492// Fix empty bookmarklet name in Firefox
1303 * Fix empty bookmarklet name in Firefox
1304 */
1305.pure-button { 1493.pure-button {
1306 -moz-user-select: auto; 1494 -moz-user-select: auto;
1307} 1495}
1308 1496
1309.tag-sort { 1497.tag-sort {
1310 margin-top: 30px; 1498 margin-top: 30px;
1311 text-align: center; 1499 text-align: center;
1312}
1313 1500
1314.tag-sort a { 1501 a {
1315 display: inline-block; 1502 display: inline-block;
1316 margin: 0 15px; 1503 margin: 0 15px;
1317 color: white;
1318 text-decoration: none; 1504 text-decoration: none;
1505 color: $white;
1319 font-weight: bold; 1506 font-weight: bold;
1507 }
1320} 1508}
1321 1509
1322/** 1510// Markdown
1323 * Markdown 1511.markdown {
1324 */ 1512 p {
1325.markdown p {
1326 margin: 0 !important; 1513 margin: 0 !important;
1327} 1514 }
1328 1515
1329.markdown p + p { 1516 p + p {
1330 margin: 0.5em 0 0 0 !important; 1517 margin: .5em 0 0 !important;
1331} 1518 }
1332 1519
1333.markdown *:first-child { 1520 * {
1334 margin-top: 0 !important; 1521 &:first-child {
1335} 1522 margin-top: 0 !important;
1523 }
1336 1524
1337.markdown *:last-child { 1525 &:last-child {
1338 margin-bottom: 5px !important; 1526 margin-bottom: 5px !important;
1527 }
1528 }
1339} 1529}
1340 1530
1341/** 1531// Pure Button
1342 * Pure Button
1343 */
1344.pure-button-success, 1532.pure-button-success,
1345.pure-button-error, 1533.pure-button-error,
1346.pure-button-warning, 1534.pure-button-warning,
1347.pure-button-primary, 1535.pure-button-primary,
1348.pure-button-shaarli, 1536.pure-button-shaarli,
1349.pure-button-secondary { 1537.pure-button-secondary {
1350 color: white !important; 1538 border-radius: 4px;
1351 border-radius: 4px; 1539 text-shadow: 0 1px 1px $dark-shadow;
1352 text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 1540 color: $white !important;
1353} 1541}
1354 1542
1355.pure-button-shaarli { 1543.pure-button-shaarli {
1356 background-color: #1B926C; 1544 background-color: $main-green;
1357} 1545}
diff --git a/composer.json b/composer.json
index 15e082f8..0d4c623c 100644
--- a/composer.json
+++ b/composer.json
@@ -36,7 +36,8 @@
36 "Shaarli\\Api\\Controllers\\": "application/api/controllers", 36 "Shaarli\\Api\\Controllers\\": "application/api/controllers",
37 "Shaarli\\Api\\Exceptions\\": "application/api/exceptions", 37 "Shaarli\\Api\\Exceptions\\": "application/api/exceptions",
38 "Shaarli\\Config\\": "application/config/", 38 "Shaarli\\Config\\": "application/config/",
39 "Shaarli\\Config\\Exception\\": "application/config/exception" 39 "Shaarli\\Config\\Exception\\": "application/config/exception",
40 "Shaarli\\Security\\": "application/security"
40 } 41 }
41 } 42 }
42} 43}
diff --git a/index.php b/index.php
index 2fe3f821..c34434dd 100644
--- a/index.php
+++ b/index.php
@@ -78,8 +78,8 @@ require_once 'application/Updater.php';
78use \Shaarli\Languages; 78use \Shaarli\Languages;
79use \Shaarli\ThemeUtils; 79use \Shaarli\ThemeUtils;
80use \Shaarli\Config\ConfigManager; 80use \Shaarli\Config\ConfigManager;
81use \Shaarli\LoginManager; 81use \Shaarli\Security\LoginManager;
82use \Shaarli\SessionManager; 82use \Shaarli\Security\SessionManager;
83 83
84// Ensure the PHP version is supported 84// Ensure the PHP version is supported
85try { 85try {
@@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
101// Set default cookie expiration and path. 101// Set default cookie expiration and path.
102session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); 102session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
103// Set session parameters on server side. 103// Set session parameters on server side.
104// If the user does not access any page within this time, his/her session is considered expired.
105define('INACTIVITY_TIMEOUT', 3600); // in seconds.
106// Use cookies to store session. 104// Use cookies to store session.
107ini_set('session.use_cookies', 1); 105ini_set('session.use_cookies', 1);
108// Force cookies for session (phpsessionID forbidden in URL). 106// Force cookies for session (phpsessionID forbidden in URL).
@@ -123,8 +121,10 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli']))
123} 121}
124 122
125$conf = new ConfigManager(); 123$conf = new ConfigManager();
126$loginManager = new LoginManager($GLOBALS, $conf);
127$sessionManager = new SessionManager($_SESSION, $conf); 124$sessionManager = new SessionManager($_SESSION, $conf);
125$loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
126$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
127$clientIpId = client_ip_id($_SERVER);
128 128
129// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. 129// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
130if (! defined('LC_MESSAGES')) { 130if (! defined('LC_MESSAGES')) {
@@ -177,157 +177,61 @@ if (! is_file($conf->getConfigFileExt())) {
177 install($conf, $sessionManager); 177 install($conf, $sessionManager);
178} 178}
179 179
180// a token depending of deployment salt, user password, and the current ip 180$loginManager->checkLoginState($_COOKIE, $clientIpId);
181define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
182 181
183/** 182/**
184 * Checking session state (i.e. is the user still logged in) 183 * Adapter function to ensure compatibility with third-party templates
185 * 184 *
186 * @param ConfigManager $conf The configuration manager. 185 * @see https://github.com/shaarli/Shaarli/pull/1086
187 * 186 *
188 * @return bool: true if the user is logged in, false otherwise. 187 * @return bool true when the user is logged in, false otherwise
189 */ 188 */
190function setup_login_state($conf)
191{
192 if ($conf->get('security.open_shaarli')) {
193 return true;
194 }
195 $userIsLoggedIn = false; // By default, we do not consider the user as logged in;
196 $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
197 if (! $conf->exists('credentials.login')) {
198 $userIsLoggedIn = false; // Shaarli is not configured yet.
199 $loginFailure = true;
200 }
201 if (isset($_COOKIE['shaarli_staySignedIn']) &&
202 $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN &&
203 !$loginFailure)
204 {
205 fillSessionInfo($conf);
206 $userIsLoggedIn = true;
207 }
208 // If session does not exist on server side, or IP address has changed, or session has expired, logout.
209 if (empty($_SESSION['uid'])
210 || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs())
211 || time() >= $_SESSION['expires_on'])
212 {
213 logout();
214 $userIsLoggedIn = false;
215 $loginFailure = true;
216 }
217 if (!empty($_SESSION['longlastingsession'])) {
218 $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
219 }
220 else {
221 $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
222 }
223 if (!$loginFailure) {
224 $userIsLoggedIn = true;
225 }
226
227 return $userIsLoggedIn;
228}
229$userIsLoggedIn = setup_login_state($conf);
230
231// ------------------------------------------------------------------------------------------
232// Session management
233
234// Returns the IP address of the client (Used to prevent session cookie hijacking.)
235function allIPs()
236{
237 $ip = $_SERVER['REMOTE_ADDR'];
238 // Then we use more HTTP headers to prevent session hijacking from users behind the same proxy.
239 if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; }
240 if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; }
241 return $ip;
242}
243
244/**
245 * Load user session.
246 *
247 * @param ConfigManager $conf Configuration Manager instance.
248 */
249function fillSessionInfo($conf)
250{
251 $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid)
252 $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked.
253 $_SESSION['username']= $conf->get('credentials.login');
254 $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration.
255}
256
257/**
258 * Check that user/password is correct.
259 *
260 * @param string $login Username
261 * @param string $password User password
262 * @param ConfigManager $conf Configuration Manager instance.
263 *
264 * @return bool: authentication successful or not.
265 */
266function check_auth($login, $password, $conf)
267{
268 $hash = sha1($password . $login . $conf->get('credentials.salt'));
269 if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash'))
270 { // Login/password is correct.
271 fillSessionInfo($conf);
272 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful');
273 return true;
274 }
275 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login);
276 return false;
277}
278
279// Returns true if the user is logged in.
280function isLoggedIn() 189function isLoggedIn()
281{ 190{
282 global $userIsLoggedIn; 191 global $loginManager;
283 return $userIsLoggedIn; 192 return $loginManager->isLoggedIn();
284} 193}
285 194
286// Force logout.
287function logout() {
288 if (isset($_SESSION)) {
289 unset($_SESSION['uid']);
290 unset($_SESSION['ip']);
291 unset($_SESSION['username']);
292 unset($_SESSION['visibility']);
293 unset($_SESSION['untaggedonly']);
294 }
295 setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
296}
297 195
298// ------------------------------------------------------------------------------------------ 196// ------------------------------------------------------------------------------------------
299// Process login form: Check if login/password is correct. 197// Process login form: Check if login/password is correct.
300if (isset($_POST['login'])) 198if (isset($_POST['login'])) {
301{
302 if (! $loginManager->canLogin($_SERVER)) { 199 if (! $loginManager->canLogin($_SERVER)) {
303 die(t('I said: NO. You are banned for the moment. Go away.')); 200 die(t('I said: NO. You are banned for the moment. Go away.'));
304 } 201 }
305 if (isset($_POST['password']) 202 if (isset($_POST['password'])
306 && $sessionManager->checkToken($_POST['token']) 203 && $sessionManager->checkToken($_POST['token'])
307 && (check_auth($_POST['login'], $_POST['password'], $conf)) 204 && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
308 ) { 205 ) {
309 // Login/password is OK.
310 $loginManager->handleSuccessfulLogin($_SERVER); 206 $loginManager->handleSuccessfulLogin($_SERVER);
311 207
312 // If user wants to keep the session cookie even after the browser closes: 208 $cookiedir = '';
313 if (!empty($_POST['longlastingsession'])) { 209 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
314 $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year)
315 $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now)
316 setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH);
317 $_SESSION['expires_on'] = $expiration; // Set session expiration on server-side.
318
319 $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
320 session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side
321 // Note: Never forget the trailing slash on the cookie path! 210 // Note: Never forget the trailing slash on the cookie path!
322 session_regenerate_id(true); // Send cookie with new expiration date to browser. 211 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
323 } 212 }
324 else // Standard session expiration (=when browser closes) 213
325 { 214 if (!empty($_POST['longlastingsession'])) {
326 $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; 215 // Keep the session cookie even after the browser closes
327 session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes" 216 $sessionManager->setStaySignedIn(true);
328 session_regenerate_id(true); 217 $expirationTime = $sessionManager->extendSession();
218
219 setcookie(
220 $loginManager::$STAY_SIGNED_IN_COOKIE,
221 $loginManager->getStaySignedInToken(),
222 $expirationTime,
223 WEB_PATH
224 );
225
226 } else {
227 // Standard session expiration (=when browser closes)
228 $expirationTime = 0;
329 } 229 }
330 230
231 // Send cookie with the new expiration date to the browser
232 session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
233 session_regenerate_id(true);
234
331 // Optional redirect after login: 235 // Optional redirect after login:
332 if (isset($_GET['post'])) { 236 if (isset($_GET['post'])) {
333 $uri = '?post='. urlencode($_GET['post']); 237 $uri = '?post='. urlencode($_GET['post']);
@@ -380,15 +284,16 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are atta
380 * Gives the last 7 days (which have links). 284 * Gives the last 7 days (which have links).
381 * This RSS feed cannot be filtered. 285 * This RSS feed cannot be filtered.
382 * 286 *
383 * @param ConfigManager $conf Configuration Manager instance. 287 * @param ConfigManager $conf Configuration Manager instance
288 * @param LoginManager $loginManager LoginManager instance
384 */ 289 */
385function showDailyRSS($conf) { 290function showDailyRSS($conf, $loginManager) {
386 // Cache system 291 // Cache system
387 $query = $_SERVER['QUERY_STRING']; 292 $query = $_SERVER['QUERY_STRING'];
388 $cache = new CachedPage( 293 $cache = new CachedPage(
389 $conf->get('config.PAGE_CACHE'), 294 $conf->get('config.PAGE_CACHE'),
390 page_url($_SERVER), 295 page_url($_SERVER),
391 startsWith($query,'do=dailyrss') && !isLoggedIn() 296 startsWith($query,'do=dailyrss') && !$loginManager->isLoggedIn()
392 ); 297 );
393 $cached = $cache->cachedVersion(); 298 $cached = $cache->cachedVersion();
394 if (!empty($cached)) { 299 if (!empty($cached)) {
@@ -400,7 +305,7 @@ function showDailyRSS($conf) {
400 // Read links from database (and filter private links if used it not logged in). 305 // Read links from database (and filter private links if used it not logged in).
401 $LINKSDB = new LinkDB( 306 $LINKSDB = new LinkDB(
402 $conf->get('resource.datastore'), 307 $conf->get('resource.datastore'),
403 isLoggedIn(), 308 $loginManager->isLoggedIn(),
404 $conf->get('privacy.hide_public_links'), 309 $conf->get('privacy.hide_public_links'),
405 $conf->get('redirector.url'), 310 $conf->get('redirector.url'),
406 $conf->get('redirector.encode_url') 311 $conf->get('redirector.encode_url')
@@ -482,9 +387,10 @@ function showDailyRSS($conf) {
482 * @param PageBuilder $pageBuilder Template engine wrapper. 387 * @param PageBuilder $pageBuilder Template engine wrapper.
483 * @param LinkDB $LINKSDB LinkDB instance. 388 * @param LinkDB $LINKSDB LinkDB instance.
484 * @param ConfigManager $conf Configuration Manager instance. 389 * @param ConfigManager $conf Configuration Manager instance.
485 * @param PluginManager $pluginManager Plugin Manager instane. 390 * @param PluginManager $pluginManager Plugin Manager instance.
391 * @param LoginManager $loginManager Login Manager instance
486 */ 392 */
487function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) 393function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
488{ 394{
489 $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD. 395 $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
490 if (isset($_GET['day'])) { 396 if (isset($_GET['day'])) {
@@ -542,7 +448,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
542 448
543 /* Hook is called before column construction so that plugins don't have 449 /* Hook is called before column construction so that plugins don't have
544 to deal with columns. */ 450 to deal with columns. */
545 $pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn())); 451 $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
546 452
547 /* We need to spread the articles on 3 columns. 453 /* We need to spread the articles on 3 columns.
548 I did not want to use a JavaScript lib like http://masonry.desandro.com/ 454 I did not want to use a JavaScript lib like http://masonry.desandro.com/
@@ -586,8 +492,8 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
586 * @param ConfigManager $conf Configuration Manager instance. 492 * @param ConfigManager $conf Configuration Manager instance.
587 * @param PluginManager $pluginManager Plugin Manager instance. 493 * @param PluginManager $pluginManager Plugin Manager instance.
588 */ 494 */
589function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { 495function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) {
590 buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager); // Compute list of links to display 496 buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager, $loginManager);
591 $PAGE->renderPage('linklist'); 497 $PAGE->renderPage('linklist');
592} 498}
593 499
@@ -607,7 +513,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
607 read_updates_file($conf->get('resource.updates')), 513 read_updates_file($conf->get('resource.updates')),
608 $LINKSDB, 514 $LINKSDB,
609 $conf, 515 $conf,
610 isLoggedIn() 516 $loginManager->isLoggedIn()
611 ); 517 );
612 try { 518 try {
613 $newUpdates = $updater->update(); 519 $newUpdates = $updater->update();
@@ -622,18 +528,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
622 die($e->getMessage()); 528 die($e->getMessage());
623 } 529 }
624 530
625 $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken()); 531 $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
626 $PAGE->assign('linkcount', count($LINKSDB)); 532 $PAGE->assign('linkcount', count($LINKSDB));
627 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 533 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
628 $PAGE->assign('plugin_errors', $pluginManager->getErrors()); 534 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
629 535
630 // Determine which page will be rendered. 536 // Determine which page will be rendered.
631 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; 537 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
632 $targetPage = Router::findPage($query, $_GET, isLoggedIn()); 538 $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
633 539
634 if ( 540 if (
635 // if the user isn't logged in 541 // if the user isn't logged in
636 !isLoggedIn() && 542 !$loginManager->isLoggedIn() &&
637 // and Shaarli doesn't have public content... 543 // and Shaarli doesn't have public content...
638 $conf->get('privacy.hide_public_links') && 544 $conf->get('privacy.hide_public_links') &&
639 // and is configured to enforce the login 545 // and is configured to enforce the login
@@ -661,7 +567,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
661 $pluginManager->executeHooks('render_' . $name, $plugin_data, 567 $pluginManager->executeHooks('render_' . $name, $plugin_data,
662 array( 568 array(
663 'target' => $targetPage, 569 'target' => $targetPage,
664 'loggedin' => isLoggedIn() 570 'loggedin' => $loginManager->isLoggedIn()
665 ) 571 )
666 ); 572 );
667 $PAGE->assign('plugins_' . $name, $plugin_data); 573 $PAGE->assign('plugins_' . $name, $plugin_data);
@@ -686,7 +592,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
686 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) 592 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout'))
687 { 593 {
688 invalidateCaches($conf->get('resource.page_cache')); 594 invalidateCaches($conf->get('resource.page_cache'));
689 logout(); 595 $sessionManager->logout();
596 setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
690 header('Location: ?'); 597 header('Location: ?');
691 exit; 598 exit;
692 } 599 }
@@ -713,7 +620,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
713 $data = array( 620 $data = array(
714 'linksToDisplay' => $linksToDisplay, 621 'linksToDisplay' => $linksToDisplay,
715 ); 622 );
716 $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn())); 623 $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
717 624
718 foreach ($data as $key => $value) { 625 foreach ($data as $key => $value) {
719 $PAGE->assign($key, $value); 626 $PAGE->assign($key, $value);
@@ -760,7 +667,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
760 'search_tags' => $searchTags, 667 'search_tags' => $searchTags,
761 'tags' => $tagList, 668 'tags' => $tagList,
762 ); 669 );
763 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); 670 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
764 671
765 foreach ($data as $key => $value) { 672 foreach ($data as $key => $value) {
766 $PAGE->assign($key, $value); 673 $PAGE->assign($key, $value);
@@ -793,7 +700,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
793 'search_tags' => $searchTags, 700 'search_tags' => $searchTags,
794 'tags' => $tags, 701 'tags' => $tags,
795 ]; 702 ];
796 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); 703 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
797 704
798 foreach ($data as $key => $value) { 705 foreach ($data as $key => $value) {
799 $PAGE->assign($key, $value); 706 $PAGE->assign($key, $value);
@@ -807,7 +714,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
807 714
808 // Daily page. 715 // Daily page.
809 if ($targetPage == Router::$PAGE_DAILY) { 716 if ($targetPage == Router::$PAGE_DAILY) {
810 showDaily($PAGE, $LINKSDB, $conf, $pluginManager); 717 showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
811 } 718 }
812 719
813 // ATOM and RSS feed. 720 // ATOM and RSS feed.
@@ -820,7 +727,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
820 $cache = new CachedPage( 727 $cache = new CachedPage(
821 $conf->get('resource.page_cache'), 728 $conf->get('resource.page_cache'),
822 page_url($_SERVER), 729 page_url($_SERVER),
823 startsWith($query,'do='. $targetPage) && !isLoggedIn() 730 startsWith($query,'do='. $targetPage) && !$loginManager->isLoggedIn()
824 ); 731 );
825 $cached = $cache->cachedVersion(); 732 $cached = $cache->cachedVersion();
826 if (!empty($cached)) { 733 if (!empty($cached)) {
@@ -829,15 +736,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
829 } 736 }
830 737
831 // Generate data. 738 // Generate data.
832 $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn()); 739 $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
833 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); 740 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
834 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn()); 741 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
835 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); 742 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
836 $data = $feedGenerator->buildData(); 743 $data = $feedGenerator->buildData();
837 744
838 // Process plugin hook. 745 // Process plugin hook.
839 $pluginManager->executeHooks('render_feed', $data, array( 746 $pluginManager->executeHooks('render_feed', $data, array(
840 'loggedin' => isLoggedIn(), 747 'loggedin' => $loginManager->isLoggedIn(),
841 'target' => $targetPage, 748 'target' => $targetPage,
842 )); 749 ));
843 750
@@ -985,7 +892,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
985 } 892 }
986 893
987 // -------- Handle other actions allowed for non-logged in users: 894 // -------- Handle other actions allowed for non-logged in users:
988 if (!isLoggedIn()) 895 if (!$loginManager->isLoggedIn())
989 { 896 {
990 // User tries to post new link but is not logged in: 897 // User tries to post new link but is not logged in:
991 // Show login screen, then redirect to ?post=... 898 // Show login screen, then redirect to ?post=...
@@ -1001,7 +908,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1001 exit; 908 exit;
1002 } 909 }
1003 910
1004 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); 911 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
1005 if (isset($_GET['edit_link'])) { 912 if (isset($_GET['edit_link'])) {
1006 header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); 913 header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
1007 exit; 914 exit;
@@ -1052,7 +959,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1052 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); 959 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
1053 $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt'))); 960 $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt')));
1054 try { 961 try {
1055 $conf->write(isLoggedIn()); 962 $conf->write($loginManager->isLoggedIn());
1056 } 963 }
1057 catch(Exception $e) { 964 catch(Exception $e) {
1058 error_log( 965 error_log(
@@ -1103,7 +1010,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1103 $conf->set('translation.language', escape($_POST['language'])); 1010 $conf->set('translation.language', escape($_POST['language']));
1104 1011
1105 try { 1012 try {
1106 $conf->write(isLoggedIn()); 1013 $conf->write($loginManager->isLoggedIn());
1107 $history->updateSettings(); 1014 $history->updateSettings();
1108 invalidateCaches($conf->get('resource.page_cache')); 1015 invalidateCaches($conf->get('resource.page_cache'));
1109 } 1016 }
@@ -1555,7 +1462,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1555 else { 1462 else {
1556 $conf->set('general.enabled_plugins', save_plugin_config($_POST)); 1463 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
1557 } 1464 }
1558 $conf->write(isLoggedIn()); 1465 $conf->write($loginManager->isLoggedIn());
1559 $history->updateSettings(); 1466 $history->updateSettings();
1560 } 1467 }
1561 catch (Exception $e) { 1468 catch (Exception $e) {
@@ -1580,7 +1487,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1580 } 1487 }
1581 1488
1582 // -------- Otherwise, simply display search form and links: 1489 // -------- Otherwise, simply display search form and links:
1583 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); 1490 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
1584 exit; 1491 exit;
1585} 1492}
1586 1493
@@ -1592,8 +1499,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1592 * @param LinkDB $LINKSDB LinkDB instance. 1499 * @param LinkDB $LINKSDB LinkDB instance.
1593 * @param ConfigManager $conf Configuration Manager instance. 1500 * @param ConfigManager $conf Configuration Manager instance.
1594 * @param PluginManager $pluginManager Plugin Manager instance. 1501 * @param PluginManager $pluginManager Plugin Manager instance.
1502 * @param LoginManager $loginManager LoginManager instance
1595 */ 1503 */
1596function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) 1504function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1597{ 1505{
1598 // Used in templates 1506 // Used in templates
1599 if (isset($_GET['searchtags'])) { 1507 if (isset($_GET['searchtags'])) {
@@ -1632,8 +1540,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1632 $keys[] = $key; 1540 $keys[] = $key;
1633 } 1541 }
1634 1542
1635
1636
1637 // Select articles according to paging. 1543 // Select articles according to paging.
1638 $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']); 1544 $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
1639 $pagecount = $pagecount == 0 ? 1 : $pagecount; 1545 $pagecount = $pagecount == 0 ? 1 : $pagecount;
@@ -1714,7 +1620,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1714 $data['pagetitle'] .= '- '. $conf->get('general.title'); 1620 $data['pagetitle'] .= '- '. $conf->get('general.title');
1715 } 1621 }
1716 1622
1717 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn())); 1623 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
1718 1624
1719 foreach ($data as $key => $value) { 1625 foreach ($data as $key => $value) {
1720 $PAGE->assign($key, $value); 1626 $PAGE->assign($key, $value);
@@ -1985,7 +1891,7 @@ function install($conf, $sessionManager) {
1985 ); 1891 );
1986 try { 1892 try {
1987 // Everything is ok, let's create config file. 1893 // Everything is ok, let's create config file.
1988 $conf->write(isLoggedIn()); 1894 $conf->write($loginManager->isLoggedIn());
1989 } 1895 }
1990 catch(Exception $e) { 1896 catch(Exception $e) {
1991 error_log( 1897 error_log(
@@ -2249,7 +2155,7 @@ try {
2249 2155
2250$linkDb = new LinkDB( 2156$linkDb = new LinkDB(
2251 $conf->get('resource.datastore'), 2157 $conf->get('resource.datastore'),
2252 isLoggedIn(), 2158 $loginManager->isLoggedIn(),
2253 $conf->get('privacy.hide_public_links'), 2159 $conf->get('privacy.hide_public_links'),
2254 $conf->get('redirector.url'), 2160 $conf->get('redirector.url'),
2255 $conf->get('redirector.encode_url') 2161 $conf->get('redirector.encode_url')
diff --git a/package.json b/package.json
index ba997c9a..3dd1e0fc 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
22 "extract-text-webpack-plugin": "^3.0.2", 22 "extract-text-webpack-plugin": "^3.0.2",
23 "file-loader": "^1.1.6", 23 "file-loader": "^1.1.6",
24 "node-sass": "^4.7.2", 24 "node-sass": "^4.7.2",
25 "sass-lint": "^1.12.1",
25 "sass-loader": "^6.0.6", 26 "sass-loader": "^6.0.6",
26 "style-loader": "^0.19.1", 27 "style-loader": "^0.19.1",
27 "url-loader": "^0.6.2", 28 "url-loader": "^0.6.2",
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
index 2f24e417..821bb125 100644
--- a/plugins/markdown/markdown.php
+++ b/plugins/markdown/markdown.php
@@ -6,6 +6,8 @@
6 * Shaare's descriptions are parsed with Markdown. 6 * Shaare's descriptions are parsed with Markdown.
7 */ 7 */
8 8
9use Shaarli\Config\ConfigManager;
10
9/* 11/*
10 * If this tag is used on a shaare, the description won't be processed by Parsedown. 12 * If this tag is used on a shaare, the description won't be processed by Parsedown.
11 */ 13 */
@@ -50,6 +52,7 @@ function hook_markdown_render_feed($data, $conf)
50 $value = stripNoMarkdownTag($value); 52 $value = stripNoMarkdownTag($value);
51 continue; 53 continue;
52 } 54 }
55 $value['description'] = reverse_feed_permalink($value['description']);
53 $value['description'] = process_markdown( 56 $value['description'] = process_markdown(
54 $value['description'], 57 $value['description'],
55 $conf->get('security.markdown_escape', true), 58 $conf->get('security.markdown_escape', true),
@@ -244,6 +247,11 @@ function reverse_space2nbsp($description)
244 return preg_replace('/(^| )&nbsp;/m', '$1 ', $description); 247 return preg_replace('/(^| )&nbsp;/m', '$1 ', $description);
245} 248}
246 249
250function reverse_feed_permalink($description)
251{
252 return preg_replace('@&#8212; <a href="([^"]+)" title="[^"]+">(\w+)</a>$@im', '&#8212; [$2]($1)', $description);
253}
254
247/** 255/**
248 * Replace not whitelisted protocols with http:// in given description. 256 * Replace not whitelisted protocols with http:// in given description.
249 * 257 *
diff --git a/tests/HttpUtils/ClientIpIdTest.php b/tests/HttpUtils/ClientIpIdTest.php
new file mode 100644
index 00000000..c15ac5cc
--- /dev/null
+++ b/tests/HttpUtils/ClientIpIdTest.php
@@ -0,0 +1,52 @@
1<?php
2/**
3 * HttpUtils' tests
4 */
5
6require_once 'application/HttpUtils.php';
7
8/**
9 * Unitary tests for client_ip_id()
10 */
11class ClientIpIdTest extends PHPUnit_Framework_TestCase
12{
13 /**
14 * Get a remote client ID based on its IP
15 */
16 public function testClientIpIdRemote()
17 {
18 $this->assertEquals(
19 '10.1.167.42',
20 client_ip_id(['REMOTE_ADDR' => '10.1.167.42'])
21 );
22 }
23
24 /**
25 * Get a remote client ID based on its IP and proxy information (1)
26 */
27 public function testClientIpIdRemoteForwarded()
28 {
29 $this->assertEquals(
30 '10.1.167.42_127.0.1.47',
31 client_ip_id([
32 'REMOTE_ADDR' => '10.1.167.42',
33 'HTTP_X_FORWARDED_FOR' => '127.0.1.47'
34 ])
35 );
36 }
37
38 /**
39 * Get a remote client ID based on its IP and proxy information (2)
40 */
41 public function testClientIpIdRemoteForwardedClient()
42 {
43 $this->assertEquals(
44 '10.1.167.42_10.1.167.56_127.0.1.47',
45 client_ip_id([
46 'REMOTE_ADDR' => '10.1.167.42',
47 'HTTP_X_FORWARDED_FOR' => '10.1.167.56',
48 'HTTP_CLIENT_IP' => '127.0.1.47'
49 ])
50 );
51 }
52}
diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php
deleted file mode 100644
index aa75962a..00000000
--- a/tests/SessionManagerTest.php
+++ /dev/null
@@ -1,149 +0,0 @@
1<?php
2require_once 'tests/utils/FakeConfigManager.php';
3
4// Initialize reference data _before_ PHPUnit starts a session
5require_once 'tests/utils/ReferenceSessionIdHashes.php';
6ReferenceSessionIdHashes::genAllHashes();
7
8use \Shaarli\SessionManager;
9use \PHPUnit\Framework\TestCase;
10
11
12/**
13 * Test coverage for SessionManager
14 */
15class SessionManagerTest extends TestCase
16{
17 // Session ID hashes
18 protected static $sidHashes = null;
19
20 // Fake ConfigManager
21 protected static $conf = null;
22
23 /**
24 * Assign reference data
25 */
26 public static function setUpBeforeClass()
27 {
28 self::$sidHashes = ReferenceSessionIdHashes::getHashes();
29 self::$conf = new FakeConfigManager();
30 }
31
32 /**
33 * Generate a session token
34 */
35 public function testGenerateToken()
36 {
37 $session = [];
38 $sessionManager = new SessionManager($session, self::$conf);
39
40 $token = $sessionManager->generateToken();
41
42 $this->assertEquals(1, $session['tokens'][$token]);
43 $this->assertEquals(40, strlen($token));
44 }
45
46 /**
47 * Check a session token
48 */
49 public function testCheckToken()
50 {
51 $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
52 $session = [
53 'tokens' => [
54 $token => 1,
55 ],
56 ];
57 $sessionManager = new SessionManager($session, self::$conf);
58
59 // check and destroy the token
60 $this->assertTrue($sessionManager->checkToken($token));
61 $this->assertFalse(isset($session['tokens'][$token]));
62
63 // ensure the token has been destroyed
64 $this->assertFalse($sessionManager->checkToken($token));
65 }
66
67 /**
68 * Generate and check a session token
69 */
70 public function testGenerateAndCheckToken()
71 {
72 $session = [];
73 $sessionManager = new SessionManager($session, self::$conf);
74
75 $token = $sessionManager->generateToken();
76
77 // ensure a token has been generated
78 $this->assertEquals(1, $session['tokens'][$token]);
79 $this->assertEquals(40, strlen($token));
80
81 // check and destroy the token
82 $this->assertTrue($sessionManager->checkToken($token));
83 $this->assertFalse(isset($session['tokens'][$token]));
84
85 // ensure the token has been destroyed
86 $this->assertFalse($sessionManager->checkToken($token));
87 }
88
89 /**
90 * Check an invalid session token
91 */
92 public function testCheckInvalidToken()
93 {
94 $session = [];
95 $sessionManager = new SessionManager($session, self::$conf);
96
97 $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
98 }
99
100 /**
101 * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
102 *
103 * This tests extensively covers all hash algorithms / bit representations
104 */
105 public function testIsAnyHashSessionIdValid()
106 {
107 foreach (self::$sidHashes as $algo => $bpcs) {
108 foreach ($bpcs as $bpc => $hash) {
109 $this->assertTrue(SessionManager::checkId($hash));
110 }
111 }
112 }
113
114 /**
115 * Test checkId with a valid ID - SHA-1 hashes
116 */
117 public function testIsSha1SessionIdValid()
118 {
119 $this->assertTrue(SessionManager::checkId(sha1('shaarli')));
120 }
121
122 /**
123 * Test checkId with a valid ID - SHA-256 hashes
124 */
125 public function testIsSha256SessionIdValid()
126 {
127 $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
128 }
129
130 /**
131 * Test checkId with a valid ID - SHA-512 hashes
132 */
133 public function testIsSha512SessionIdValid()
134 {
135 $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
136 }
137
138 /**
139 * Test checkId with invalid IDs.
140 */
141 public function testIsSessionIdInvalid()
142 {
143 $this->assertFalse(SessionManager::checkId(''));
144 $this->assertFalse(SessionManager::checkId([]));
145 $this->assertFalse(
146 SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
147 );
148 }
149}
diff --git a/tests/plugins/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php
index ddc2728d..b31e817f 100644
--- a/tests/plugins/PluginMarkdownTest.php
+++ b/tests/plugins/PluginMarkdownTest.php
@@ -50,6 +50,30 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
50 } 50 }
51 51
52 /** 52 /**
53 * Test render_feed hook.
54 */
55 public function testMarkdownFeed()
56 {
57 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
58 $markdown .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
59 $data = array(
60 'links' => array(
61 0 => array(
62 'description' => $markdown,
63 ),
64 ),
65 );
66
67 $data = hook_markdown_render_feed($data, $this->conf);
68 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
69 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
70 $this->assertStringEndsWith(
71 '&#8212; <a href="http://domain.tld/?0oc_VQ">Permalien</a></p></div>',
72 $data['links'][0]['description']
73 );
74 }
75
76 /**
53 * Test render_daily hook. 77 * Test render_daily hook.
54 * Only check that there is basic markdown rendering. 78 * Only check that there is basic markdown rendering.
55 */ 79 */
@@ -104,6 +128,37 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
104 $this->assertEquals($text, $reversedText); 128 $this->assertEquals($text, $reversedText);
105 } 129 }
106 130
131 public function testReverseFeedPermalink()
132 {
133 $text = 'Description... ';
134 $text .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
135 $expected = 'Description... &#8212; [Permalien](http://domain.tld/?0oc_VQ)';
136 $processedText = reverse_feed_permalink($text);
137
138 $this->assertEquals($expected, $processedText);
139 }
140
141 public function testReverseLastFeedPermalink()
142 {
143 $text = 'Description... ';
144 $text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
145 $expected = $text;
146 $text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
147 $expected .= '<br>&#8212; [Permalien](http://domain.tld/?0oc_VQ)';
148 $processedText = reverse_feed_permalink($text);
149
150 $this->assertEquals($expected, $processedText);
151 }
152
153 public function testReverseNoFeedPermalink()
154 {
155 $text = 'Hello! Where are you from?';
156 $expected = $text;
157 $processedText = reverse_feed_permalink($text);
158
159 $this->assertEquals($expected, $processedText);
160 }
161
107 /** 162 /**
108 * Test sanitize_html(). 163 * Test sanitize_html().
109 */ 164 */
diff --git a/tests/LoginManagerTest.php b/tests/security/LoginManagerTest.php
index 4159038e..f26cd1eb 100644
--- a/tests/LoginManagerTest.php
+++ b/tests/security/LoginManagerTest.php
@@ -1,5 +1,5 @@
1<?php 1<?php
2namespace Shaarli; 2namespace Shaarli\Security;
3 3
4require_once 'tests/utils/FakeConfigManager.php'; 4require_once 'tests/utils/FakeConfigManager.php';
5use \PHPUnit\Framework\TestCase; 5use \PHPUnit\Framework\TestCase;
@@ -9,15 +9,54 @@ use \PHPUnit\Framework\TestCase;
9 */ 9 */
10class LoginManagerTest extends TestCase 10class LoginManagerTest extends TestCase
11{ 11{
12 /** @var \FakeConfigManager Configuration Manager instance */
12 protected $configManager = null; 13 protected $configManager = null;
14
15 /** @var LoginManager Login Manager instance */
13 protected $loginManager = null; 16 protected $loginManager = null;
17
18 /** @var SessionManager Session Manager instance */
19 protected $sessionManager = null;
20
21 /** @var string Banned IP filename */
14 protected $banFile = 'sandbox/ipbans.php'; 22 protected $banFile = 'sandbox/ipbans.php';
23
24 /** @var string Log filename */
15 protected $logFile = 'sandbox/shaarli.log'; 25 protected $logFile = 'sandbox/shaarli.log';
26
27 /** @var array Simulates the $_COOKIE array */
28 protected $cookie = [];
29
30 /** @var array Simulates the $GLOBALS array */
16 protected $globals = []; 31 protected $globals = [];
17 protected $ipAddr = '127.0.0.1'; 32
33 /** @var array Simulates the $_SERVER array */
18 protected $server = []; 34 protected $server = [];
35
36 /** @var array Simulates the $_SESSION array */
37 protected $session = [];
38
39 /** @var string Advertised client IP address */
40 protected $clientIpAddress = '10.1.47.179';
41
42 /** @var string Local client IP address */
43 protected $ipAddr = '127.0.0.1';
44
45 /** @var string Trusted proxy IP address */
19 protected $trustedProxy = '10.1.1.100'; 46 protected $trustedProxy = '10.1.1.100';
20 47
48 /** @var string User login */
49 protected $login = 'johndoe';
50
51 /** @var string User password */
52 protected $password = 'IC4nHazL0g1n?';
53
54 /** @var string Hash of the salted user password */
55 protected $passwordHash = '';
56
57 /** @var string Salt used by hash functions */
58 protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
59
21 /** 60 /**
22 * Prepare or reset test resources 61 * Prepare or reset test resources
23 */ 62 */
@@ -27,7 +66,12 @@ class LoginManagerTest extends TestCase
27 unlink($this->banFile); 66 unlink($this->banFile);
28 } 67 }
29 68
69 $this->passwordHash = sha1($this->password . $this->login . $this->salt);
70
30 $this->configManager = new \FakeConfigManager([ 71 $this->configManager = new \FakeConfigManager([
72 'credentials.login' => $this->login,
73 'credentials.hash' => $this->passwordHash,
74 'credentials.salt' => $this->salt,
31 'resource.ban_file' => $this->banFile, 75 'resource.ban_file' => $this->banFile,
32 'resource.log' => $this->logFile, 76 'resource.log' => $this->logFile,
33 'security.ban_after' => 4, 77 'security.ban_after' => 4,
@@ -35,10 +79,15 @@ class LoginManagerTest extends TestCase
35 'security.trusted_proxies' => [$this->trustedProxy], 79 'security.trusted_proxies' => [$this->trustedProxy],
36 ]); 80 ]);
37 81
82 $this->cookie = [];
83
38 $this->globals = &$GLOBALS; 84 $this->globals = &$GLOBALS;
39 unset($this->globals['IPBANS']); 85 unset($this->globals['IPBANS']);
40 86
41 $this->loginManager = new LoginManager($this->globals, $this->configManager); 87 $this->session = [];
88
89 $this->sessionManager = new SessionManager($this->session, $this->configManager);
90 $this->loginManager = new LoginManager($this->globals, $this->configManager, $this->sessionManager);
42 $this->server['REMOTE_ADDR'] = $this->ipAddr; 91 $this->server['REMOTE_ADDR'] = $this->ipAddr;
43 } 92 }
44 93
@@ -59,7 +108,7 @@ class LoginManagerTest extends TestCase
59 $this->banFile, 108 $this->banFile,
60 "<?php\n\$GLOBALS['IPBANS']=array('FAILURES' => array('127.0.0.1' => 99));\n?>" 109 "<?php\n\$GLOBALS['IPBANS']=array('FAILURES' => array('127.0.0.1' => 99));\n?>"
61 ); 110 );
62 new LoginManager($this->globals, $this->configManager); 111 new LoginManager($this->globals, $this->configManager, null);
63 $this->assertEquals(99, $this->globals['IPBANS']['FAILURES']['127.0.0.1']); 112 $this->assertEquals(99, $this->globals['IPBANS']['FAILURES']['127.0.0.1']);
64 } 113 }
65 114
@@ -196,4 +245,130 @@ class LoginManagerTest extends TestCase
196 $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600; 245 $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600;
197 $this->assertTrue($this->loginManager->canLogin($this->server)); 246 $this->assertTrue($this->loginManager->canLogin($this->server));
198 } 247 }
248
249 /**
250 * Generate a token depending on the user credentials and client IP
251 */
252 public function testGenerateStaySignedInToken()
253 {
254 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
255
256 $this->assertEquals(
257 sha1($this->passwordHash . $this->clientIpAddress . $this->salt),
258 $this->loginManager->getStaySignedInToken()
259 );
260 }
261
262 /**
263 * Check user login - Shaarli has not yet been configured
264 */
265 public function testCheckLoginStateNotConfigured()
266 {
267 $configManager = new \FakeConfigManager([
268 'resource.ban_file' => $this->banFile,
269 ]);
270 $loginManager = new LoginManager($this->globals, $configManager, null);
271 $loginManager->checkLoginState([], '');
272
273 $this->assertFalse($loginManager->isLoggedIn());
274 }
275
276 /**
277 * Check user login - the client cookie does not match the server token
278 */
279 public function testCheckLoginStateStaySignedInWithInvalidToken()
280 {
281 // simulate a previous login
282 $this->session = [
283 'ip' => $this->clientIpAddress,
284 'expires_on' => time() + 100,
285 ];
286 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
287 $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope';
288
289 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
290
291 $this->assertTrue($this->loginManager->isLoggedIn());
292 $this->assertTrue(empty($this->session['username']));
293 }
294
295 /**
296 * Check user login - the client cookie matches the server token
297 */
298 public function testCheckLoginStateStaySignedInWithValidToken()
299 {
300 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
301 $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken();
302
303 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
304
305 $this->assertTrue($this->loginManager->isLoggedIn());
306 $this->assertEquals($this->login, $this->session['username']);
307 $this->assertEquals($this->clientIpAddress, $this->session['ip']);
308 }
309
310 /**
311 * Check user login - the session has expired
312 */
313 public function testCheckLoginStateSessionExpired()
314 {
315 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
316 $this->session['expires_on'] = time() - 100;
317
318 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
319
320 $this->assertFalse($this->loginManager->isLoggedIn());
321 }
322
323 /**
324 * Check user login - the remote client IP has changed
325 */
326 public function testCheckLoginStateClientIpChanged()
327 {
328 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
329
330 $this->loginManager->checkLoginState($this->cookie, '10.7.157.98');
331
332 $this->assertFalse($this->loginManager->isLoggedIn());
333 }
334
335 /**
336 * Check user credentials - wrong login supplied
337 */
338 public function testCheckCredentialsWrongLogin()
339 {
340 $this->assertFalse(
341 $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password)
342 );
343 }
344
345 /**
346 * Check user credentials - wrong password supplied
347 */
348 public function testCheckCredentialsWrongPassword()
349 {
350 $this->assertFalse(
351 $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd')
352 );
353 }
354
355 /**
356 * Check user credentials - wrong login and password supplied
357 */
358 public function testCheckCredentialsWrongLoginAndPassword()
359 {
360 $this->assertFalse(
361 $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd')
362 );
363 }
364
365 /**
366 * Check user credentials - correct login and password supplied
367 */
368 public function testCheckCredentialsGoodLoginAndPassword()
369 {
370 $this->assertTrue(
371 $this->loginManager->checkCredentials('', '', $this->login, $this->password)
372 );
373 }
199} 374}
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php
new file mode 100644
index 00000000..9bd868f8
--- /dev/null
+++ b/tests/security/SessionManagerTest.php
@@ -0,0 +1,273 @@
1<?php
2require_once 'tests/utils/FakeConfigManager.php';
3
4// Initialize reference data _before_ PHPUnit starts a session
5require_once 'tests/utils/ReferenceSessionIdHashes.php';
6ReferenceSessionIdHashes::genAllHashes();
7
8use \Shaarli\Security\SessionManager;
9use \PHPUnit\Framework\TestCase;
10
11
12/**
13 * Test coverage for SessionManager
14 */
15class SessionManagerTest extends TestCase
16{
17 /** @var array Session ID hashes */
18 protected static $sidHashes = null;
19
20 /** @var \FakeConfigManager ConfigManager substitute for testing */
21 protected $conf = null;
22
23 /** @var array $_SESSION array for testing */
24 protected $session = [];
25
26 /** @var SessionManager Server-side session management abstraction */
27 protected $sessionManager = null;
28
29 /**
30 * Assign reference data
31 */
32 public static function setUpBeforeClass()
33 {
34 self::$sidHashes = ReferenceSessionIdHashes::getHashes();
35 }
36
37 /**
38 * Initialize or reset test resources
39 */
40 public function setUp()
41 {
42 $this->conf = new FakeConfigManager([
43 'credentials.login' => 'johndoe',
44 'credentials.salt' => 'salt',
45 'security.session_protection_disabled' => false,
46 ]);
47 $this->session = [];
48 $this->sessionManager = new SessionManager($this->session, $this->conf);
49 }
50
51 /**
52 * Generate a session token
53 */
54 public function testGenerateToken()
55 {
56 $token = $this->sessionManager->generateToken();
57
58 $this->assertEquals(1, $this->session['tokens'][$token]);
59 $this->assertEquals(40, strlen($token));
60 }
61
62 /**
63 * Check a session token
64 */
65 public function testCheckToken()
66 {
67 $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
68 $session = [
69 'tokens' => [
70 $token => 1,
71 ],
72 ];
73 $sessionManager = new SessionManager($session, $this->conf);
74
75 // check and destroy the token
76 $this->assertTrue($sessionManager->checkToken($token));
77 $this->assertFalse(isset($session['tokens'][$token]));
78
79 // ensure the token has been destroyed
80 $this->assertFalse($sessionManager->checkToken($token));
81 }
82
83 /**
84 * Generate and check a session token
85 */
86 public function testGenerateAndCheckToken()
87 {
88 $token = $this->sessionManager->generateToken();
89
90 // ensure a token has been generated
91 $this->assertEquals(1, $this->session['tokens'][$token]);
92 $this->assertEquals(40, strlen($token));
93
94 // check and destroy the token
95 $this->assertTrue($this->sessionManager->checkToken($token));
96 $this->assertFalse(isset($this->session['tokens'][$token]));
97
98 // ensure the token has been destroyed
99 $this->assertFalse($this->sessionManager->checkToken($token));
100 }
101
102 /**
103 * Check an invalid session token
104 */
105 public function testCheckInvalidToken()
106 {
107 $this->assertFalse($this->sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
108 }
109
110 /**
111 * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
112 *
113 * This tests extensively covers all hash algorithms / bit representations
114 */
115 public function testIsAnyHashSessionIdValid()
116 {
117 foreach (self::$sidHashes as $algo => $bpcs) {
118 foreach ($bpcs as $bpc => $hash) {
119 $this->assertTrue(SessionManager::checkId($hash));
120 }
121 }
122 }
123
124 /**
125 * Test checkId with a valid ID - SHA-1 hashes
126 */
127 public function testIsSha1SessionIdValid()
128 {
129 $this->assertTrue(SessionManager::checkId(sha1('shaarli')));
130 }
131
132 /**
133 * Test checkId with a valid ID - SHA-256 hashes
134 */
135 public function testIsSha256SessionIdValid()
136 {
137 $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
138 }
139
140 /**
141 * Test checkId with a valid ID - SHA-512 hashes
142 */
143 public function testIsSha512SessionIdValid()
144 {
145 $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
146 }
147
148 /**
149 * Test checkId with invalid IDs.
150 */
151 public function testIsSessionIdInvalid()
152 {
153 $this->assertFalse(SessionManager::checkId(''));
154 $this->assertFalse(SessionManager::checkId([]));
155 $this->assertFalse(
156 SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
157 );
158 }
159
160 /**
161 * Store login information after a successful login
162 */
163 public function testStoreLoginInfo()
164 {
165 $this->sessionManager->storeLoginInfo('ip_id');
166
167 $this->assertGreaterThan(time(), $this->session['expires_on']);
168 $this->assertEquals('ip_id', $this->session['ip']);
169 $this->assertEquals('johndoe', $this->session['username']);
170 }
171
172 /**
173 * Extend a server-side session by SessionManager::$SHORT_TIMEOUT
174 */
175 public function testExtendSession()
176 {
177 $this->sessionManager->extendSession();
178
179 $this->assertGreaterThan(time(), $this->session['expires_on']);
180 $this->assertLessThanOrEqual(
181 time() + SessionManager::$SHORT_TIMEOUT,
182 $this->session['expires_on']
183 );
184 }
185
186 /**
187 * Extend a server-side session by SessionManager::$LONG_TIMEOUT
188 */
189 public function testExtendSessionStaySignedIn()
190 {
191 $this->sessionManager->setStaySignedIn(true);
192 $this->sessionManager->extendSession();
193
194 $this->assertGreaterThan(time(), $this->session['expires_on']);
195 $this->assertGreaterThan(
196 time() + SessionManager::$LONG_TIMEOUT - 10,
197 $this->session['expires_on']
198 );
199 $this->assertLessThanOrEqual(
200 time() + SessionManager::$LONG_TIMEOUT,
201 $this->session['expires_on']
202 );
203 }
204
205 /**
206 * Unset session variables after logging out
207 */
208 public function testLogout()
209 {
210 $this->session = [
211 'ip' => 'ip_id',
212 'expires_on' => time() + 1000,
213 'username' => 'johndoe',
214 'visibility' => 'public',
215 'untaggedonly' => false,
216 ];
217 $this->sessionManager->logout();
218
219 $this->assertFalse(isset($this->session['ip']));
220 $this->assertFalse(isset($this->session['expires_on']));
221 $this->assertFalse(isset($this->session['username']));
222 $this->assertFalse(isset($this->session['visibility']));
223 $this->assertFalse(isset($this->session['untaggedonly']));
224 }
225
226 /**
227 * The session is active and expiration time has been reached
228 */
229 public function testHasExpiredTimeElapsed()
230 {
231 $this->session['expires_on'] = time() - 10;
232
233 $this->assertTrue($this->sessionManager->hasSessionExpired());
234 }
235
236 /**
237 * The session is active and expiration time has not been reached
238 */
239 public function testHasNotExpired()
240 {
241 $this->session['expires_on'] = time() + 1000;
242
243 $this->assertFalse($this->sessionManager->hasSessionExpired());
244 }
245
246 /**
247 * Session hijacking protection is disabled, we assume the IP has not changed
248 */
249 public function testHasClientIpChangedNoSessionProtection()
250 {
251 $this->conf->set('security.session_protection_disabled', true);
252
253 $this->assertFalse($this->sessionManager->hasClientIpChanged(''));
254 }
255
256 /**
257 * The client IP identifier has not changed
258 */
259 public function testHasClientIpChangedNope()
260 {
261 $this->session['ip'] = 'ip_id';
262 $this->assertFalse($this->sessionManager->hasClientIpChanged('ip_id'));
263 }
264
265 /**
266 * The client IP identifier has changed
267 */
268 public function testHasClientIpChanged()
269 {
270 $this->session['ip'] = 'ip_id_one';
271 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two'));
272 }
273}
diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php
index 85434de7..360b34a9 100644
--- a/tests/utils/FakeConfigManager.php
+++ b/tests/utils/FakeConfigManager.php
@@ -42,4 +42,16 @@ class FakeConfigManager
42 } 42 }
43 return $key; 43 return $key;
44 } 44 }
45
46 /**
47 * Check if a setting exists
48 *
49 * @param string $setting Asked setting, keys separated with dots
50 *
51 * @return bool true if the setting exists, false otherwise
52 */
53 public function exists($setting)
54 {
55 return array_key_exists($setting, $this->values);
56 }
45} 57}
diff --git a/tpl/default/404.html b/tpl/default/404.html
index 2de6b6da..fd337cad 100644
--- a/tpl/default/404.html
+++ b/tpl/default/404.html
@@ -6,7 +6,7 @@
6<body> 6<body>
7<div id="pageheader"> 7<div id="pageheader">
8 {include="page.header"} 8 {include="page.header"}
9<div class="center" id="page404"> 9<div class="center" id="page404" class="page404-container">
10 <h2>{'Sorry, nothing to see here.'|t}</h2> 10 <h2>{'Sorry, nothing to see here.'|t}</h2>
11 <img src="img/sad_star.png"> 11 <img src="img/sad_star.png">
12 <p>{$error_message}</p> 12 <p>{$error_message}</p>
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html
index f8e968f1..d8c57155 100644
--- a/tpl/default/editlink.html
+++ b/tpl/default/editlink.html
@@ -5,7 +5,7 @@
5</head> 5</head>
6<body> 6<body>
7 {include="page.header"} 7 {include="page.header"}
8 <div id="editlinkform" class="pure-g"> 8 <div id="editlinkform" class="edit-link-container" class="pure-g">
9 <div class="pure-u-lg-1-5 pure-u-1-24"></div> 9 <div class="pure-u-lg-1-5 pure-u-1-24"></div>
10 <form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"> 10 <form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light">
11 <h2 class="window-title"> 11 <h2 class="window-title">
diff --git a/tpl/default/import.html b/tpl/default/import.html
index 000a50ac..bdc9086e 100644
--- a/tpl/default/import.html
+++ b/tpl/default/import.html
@@ -15,7 +15,7 @@
15 </div> 15 </div>
16 16
17 <input type="hidden" name="token" value="{$token}"> 17 <input type="hidden" name="token" value="{$token}">
18 <div class="center" id="import-field"> 18 <div class="center import-field-container" id="import-field">
19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
20 <input type="file" name="filetoupload"> 20 <input type="file" name="filetoupload">
21 <p><br>{'Maximum size allowed:'|t} <strong>{$maxfilesizeHuman}</strong></p> 21 <p><br>{'Maximum size allowed:'|t} <strong>{$maxfilesizeHuman}</strong></p>
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index 933c1ef2..322cddd5 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -16,7 +16,7 @@
16</div> 16</div>
17 17
18<input type="hidden" name="token" value="{$token}"> 18<input type="hidden" name="token" value="{$token}">
19<div id="search-linklist"> 19<div id="search-linklist" class="searchform-block search-linklist">
20 20
21 <form method="GET" class="pure-form searchform" name="searchform"> 21 <form method="GET" class="pure-form searchform" name="searchform">
22 <input type="text" tabindex="1" name="searchterm" class="searchterm" placeholder="{'Search text'|t}" 22 <input type="text" tabindex="1" name="searchterm" class="searchterm" placeholder="{'Search text'|t}"
@@ -136,7 +136,7 @@
136 <div class="linklist-item-thumbnail">{$thumb}</div> 136 <div class="linklist-item-thumbnail">{$thumb}</div>
137 {/if} 137 {/if}
138 138
139 {if="isLoggedIn()"} 139 {if="$is_logged_in"}
140 <div class="linklist-item-editbuttons"> 140 <div class="linklist-item-editbuttons">
141 {if="$value.private"} 141 {if="$value.private"}
142 <span class="label label-private">{$strPrivate}</span> 142 <span class="label label-private">{$strPrivate}</span>
@@ -179,7 +179,7 @@
179 179
180 <div class="linklist-item-infos-date-url-block pure-g"> 180 <div class="linklist-item-infos-date-url-block pure-g">
181 <div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1"> 181 <div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1">
182 {if="isLoggedIn()"} 182 {if="$is_logged_in"}
183 <div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible"> 183 <div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible">
184 <span class="linklist-item-infos-controls-item ctrl-checkbox"> 184 <span class="linklist-item-infos-controls-item ctrl-checkbox">
185 <input type="checkbox" class="delete-checkbox" value="{$value.id}"> 185 <input type="checkbox" class="delete-checkbox" value="{$value.id}">
@@ -196,7 +196,7 @@
196 </div> 196 </div>
197 {/if} 197 {/if}
198 <a href="?{$value.shorturl}" title="{$strPermalink}"> 198 <a href="?{$value.shorturl}" title="{$strPermalink}">
199 {if="!$hide_timestamps || isLoggedIn()"} 199 {if="!$hide_timestamps || $is_logged_in"}
200 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink} 200 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
201 <span class="linkdate" title="{$updated}"> 201 <span class="linkdate" title="{$updated}">
202 <i class="fa fa-clock-o"></i> 202 <i class="fa fa-clock-o"></i>
@@ -236,7 +236,7 @@
236 {if="$link_plugin_counter - 1 != $counter"}&middot;{/if} 236 {if="$link_plugin_counter - 1 != $counter"}&middot;{/if}
237 {/loop} 237 {/loop}
238 {/if} 238 {/if}
239 {if="isLoggedIn()"} 239 {if="$is_logged_in"}
240 &middot; 240 &middot;
241 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" 241 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
242 title="{$strDelete}" class="delete-link confirm-delete"> 242 title="{$strDelete}" class="delete-link confirm-delete">
diff --git a/tpl/default/linklist.paging.html b/tpl/default/linklist.paging.html
index 72bdd931..5309e348 100644
--- a/tpl/default/linklist.paging.html
+++ b/tpl/default/linklist.paging.html
@@ -1,11 +1,11 @@
1<div class="linklist-paging"> 1<div class="linklist-paging">
2 <div class="paging pure-g"> 2 <div class="paging pure-g">
3 <div class="linklist-filters pure-u-1-3"> 3 <div class="linklist-filters pure-u-1-3">
4 {if="isLoggedIn() or !empty($action_plugin)"} 4 {if="$is_logged_in or !empty($action_plugin)"}
5 <span class="linklist-filters-text pure-u-0 pure-u-lg-visible"> 5 <span class="linklist-filters-text pure-u-0 pure-u-lg-visible">
6 {'Filters'|t} 6 {'Filters'|t}
7 </span> 7 </span>
8 {if="isLoggedIn()"} 8 {if="$is_logged_in"}
9 <a href="?visibility=private" title="{'Only display private links'|t}" 9 <a href="?visibility=private" title="{'Only display private links'|t}"
10 class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}" 10 class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}"
11 ><i class="fa fa-user-secret"></i></a> 11 ><i class="fa fa-user-secret"></i></a>
diff --git a/tpl/default/loginform.html b/tpl/default/loginform.html
index d481f452..3cdab65a 100644
--- a/tpl/default/loginform.html
+++ b/tpl/default/loginform.html
@@ -18,7 +18,7 @@
18{else} 18{else}
19 <div class="pure-g"> 19 <div class="pure-g">
20 <div class="pure-u-lg-1-3 pure-u-1-24"></div> 20 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
21 <div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> 21 <div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24 login-form-container">
22 <form method="post" name="loginform"> 22 <form method="post" name="loginform">
23 <h2 class="window-title">{'Login'|t}</h2> 23 <h2 class="window-title">{'Login'|t}</h2>
24 <div> 24 <div>
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index f4f09e92..5af39be7 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -2,9 +2,9 @@
2 2
3<div class="pure-g"> 3<div class="pure-g">
4 <div class="pure-u-2-24"></div> 4 <div class="pure-u-2-24"></div>
5 <div id="footer" class="pure-u-20-24"> 5 <div id="footer" class="pure-u-20-24 footer-container">
6 <strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong> 6 <strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong>
7 {if="isLoggedIn()===true"} 7 {if="$is_logged_in===true"}
8 {$version} 8 {$version}
9 {/if} 9 {/if}
10 &middot; 10 &middot;
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html
index 6f15c1c5..82568d63 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -17,7 +17,7 @@
17 {$shaarlititle} 17 {$shaarlititle}
18 </a> 18 </a>
19 </li> 19 </li>
20 {if="isLoggedIn() || $openshaarli"} 20 {if="$is_logged_in || $openshaarli"}
21 <li class="pure-menu-item"> 21 <li class="pure-menu-item">
22 <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare"> 22 <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare">
23 <i class="fa fa-plus" ></i> {'Shaare'|t} 23 <i class="fa fa-plus" ></i> {'Shaare'|t}
@@ -50,7 +50,7 @@
50 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss"> 50 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
51 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a> 51 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
52 </li> 52 </li>
53 {if="isLoggedIn()"} 53 {if="$is_logged_in"}
54 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout"> 54 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
55 <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a> 55 <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a>
56 </li> 56 </li>
@@ -74,7 +74,7 @@
74 <i class="fa fa-rss"></i> 74 <i class="fa fa-rss"></i>
75 </a> 75 </a>
76 </li> 76 </li>
77 {if="!isLoggedIn()"} 77 {if="!$is_logged_in"}
78 <li class="pure-menu-item" id="shaarli-menu-desktop-login"> 78 <li class="pure-menu-item" id="shaarli-menu-desktop-login">
79 <a href="?do=login" class="pure-menu-link" 79 <a href="?do=login" class="pure-menu-link"
80 data-open-id="header-login-form" 80 data-open-id="header-login-form"
@@ -95,8 +95,8 @@
95 </div> 95 </div>
96</div> 96</div>
97 97
98<div id="content"> 98<div id="content" class="container">
99 <div id="search" class="subheader-form"> 99 <div id="search" class="subheader-form searchform-block header-search">
100 <form method="GET" class="pure-form searchform" name="searchform"> 100 <form method="GET" class="pure-form searchform" name="searchform">
101 <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="{'Search text'|t}" 101 <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="{'Search text'|t}"
102 {if="!empty($search_term)"} 102 {if="!empty($search_term)"}
@@ -120,9 +120,9 @@
120 </div> 120 </div>
121 </div> 121 </div>
122 </div> 122 </div>
123 {if="!isLoggedIn()"} 123 {if="!$is_logged_in"}
124 <form method="post" name="loginform"> 124 <form method="post" name="loginform">
125 <div class="subheader-form" id="header-login-form"> 125 <div class="subheader-form header-login-form" id="header-login-form">
126 <input type="text" name="login" placeholder="{'Username'|t}" tabindex="3"> 126 <input type="text" name="login" placeholder="{'Username'|t}" tabindex="3">
127 <input type="password" name="password" placeholder="{'Password'|t}" tabindex="5"> 127 <input type="password" name="password" placeholder="{'Password'|t}" tabindex="5">
128 <div class="remember-me"> 128 <div class="remember-me">
@@ -155,7 +155,7 @@
155 </div> 155 </div>
156{/if} 156{/if}
157 157
158{if="!empty($plugin_errors) && isLoggedIn()"} 158{if="!empty($plugin_errors) && $is_logged_in"}
159 <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert"> 159 <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
160 <div class="pure-u-2-24"></div> 160 <div class="pure-u-2-24"></div>
161 <div class="pure-u-20-24"> 161 <div class="pure-u-20-24">
diff --git a/tpl/default/picwall.html b/tpl/default/picwall.html
index 23796ac9..2f7e03dc 100644
--- a/tpl/default/picwall.html
+++ b/tpl/default/picwall.html
@@ -18,9 +18,9 @@
18 {/loop} 18 {/loop}
19 </div> 19 </div>
20 20
21 <div id="picwall_container"> 21 <div id="picwall_container" class="picwall-container">
22 {loop="$linksToDisplay"} 22 {loop="$linksToDisplay"}
23 <div class="picwall_pictureframe"> 23 <div class="picwall-pictureframe">
24 {$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a> 24 {$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
25 {loop="$value.picwall_plugin"} 25 {loop="$value.picwall_plugin"}
26 {$value} 26 {$value}
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index fa26c859..8f2597df 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -16,7 +16,7 @@
16 <div class="clear"></div> 16 <div class="clear"></div>
17</noscript> 17</noscript>
18 18
19<form method="POST" action="?do=save_pluginadmin" name="pluginform" id="pluginform"> 19<form method="POST" action="?do=save_pluginadmin" name="pluginform" id="pluginform" class="pluginform-container">
20 <div class="pure-g"> 20 <div class="pure-g">
21 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 21 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
22 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete"> 22 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html
index 12701465..9e52158d 100644
--- a/tpl/default/tag.cloud.html
+++ b/tpl/default/tag.cloud.html
@@ -45,7 +45,7 @@
45 {/loop} 45 {/loop}
46 </div> 46 </div>
47 47
48 <div id="cloudtag"> 48 <div id="cloudtag" class="cloudtag-container">
49 {loop="tags"} 49 {loop="tags"}
50 <a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a 50 <a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a
51 ><a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a> 51 ><a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html
index 7140c67a..bcddcd56 100644
--- a/tpl/default/tag.list.html
+++ b/tpl/default/tag.list.html
@@ -21,7 +21,7 @@
21 </p> 21 </p>
22 {/if} 22 {/if}
23 23
24 <div id="search-tagcloud" class="pure-g"> 24 <div id="search-tagcloud" class="pure-g searchform-block search-tagcloud">
25 <div class="pure-u-lg-1-4"></div> 25 <div class="pure-u-lg-1-4"></div>
26 <div class="pure-u-1 pure-u-lg-1-2"> 26 <div class="pure-u-1 pure-u-lg-1-2">
27 <form method="GET"> 27 <form method="GET">
@@ -45,11 +45,11 @@
45 {/loop} 45 {/loop}
46 </div> 46 </div>
47 47
48 <div id="taglist"> 48 <div id="taglist" class="taglist-container">
49 {loop="tags"} 49 {loop="tags"}
50 <div class="tag-list-item pure-g" data-tag="{$key}"> 50 <div class="tag-list-item pure-g" data-tag="{$key}">
51 <div class="pure-u-1"> 51 <div class="pure-u-1">
52 {if="isLoggedIn()===true"} 52 {if="$is_logged_in===true"}
53 <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>&nbsp;&nbsp; 53 <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>&nbsp;&nbsp;
54 <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag"> 54 <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
55 <i class="fa fa-pencil-square-o {$key}"></i> 55 <i class="fa fa-pencil-square-o {$key}"></i>
@@ -63,7 +63,7 @@
63 {$value} 63 {$value}
64 {/loop} 64 {/loop}
65 </div> 65 </div>
66 {if="isLoggedIn()===true"} 66 {if="$is_logged_in===true"}
67 <div class="rename-tag-form pure-u-1"> 67 <div class="rename-tag-form pure-u-1">
68 <input type="text" name="{$key}" value="{$key}" class="rename-tag-input" /> 68 <input type="text" name="{$key}" value="{$key}" class="rename-tag-input" />
69 <a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a> 69 <a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a>
@@ -81,7 +81,7 @@
81 </div> 81 </div>
82</div> 82</div>
83 83
84{if="isLoggedIn()===true"} 84{if="$is_logged_in===true"}
85 <input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}" 85 <input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
86{/if} 86{/if}
87 87
diff --git a/tpl/vintage/daily.html b/tpl/vintage/daily.html
index 42db16a7..ede35910 100644
--- a/tpl/vintage/daily.html
+++ b/tpl/vintage/daily.html
@@ -53,7 +53,7 @@
53 <img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink"> 53 <img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink">
54 </a> 54 </a>
55 </div> 55 </div>
56 {if="!$hide_timestamps || isLoggedIn()"} 56 {if="!$hide_timestamps || $is_logged_in"}
57 <div class="dailyEntryLinkdate"> 57 <div class="dailyEntryLinkdate">
58 <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a> 58 <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
59 </div> 59 </div>
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html
index e7137246..1ca51be3 100644
--- a/tpl/vintage/linklist.html
+++ b/tpl/vintage/linklist.html
@@ -82,7 +82,7 @@
82 <a id="{$value.shorturl}"></a> 82 <a id="{$value.shorturl}"></a>
83 <div class="thumbnail">{$value.url|thumbnail}</div> 83 <div class="thumbnail">{$value.url|thumbnail}</div>
84 <div class="linkcontainer"> 84 <div class="linkcontainer">
85 {if="isLoggedIn()"} 85 {if="$is_logged_in"}
86 <div class="linkeditbuttons"> 86 <div class="linkeditbuttons">
87 <form method="GET" class="buttoneditform"> 87 <form method="GET" class="buttoneditform">
88 <input type="hidden" name="edit_link" value="{$value.id}"> 88 <input type="hidden" name="edit_link" value="{$value.id}">
@@ -102,7 +102,7 @@
102 </span> 102 </span>
103 <br> 103 <br>
104 {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if} 104 {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if}
105 {if="!$hide_timestamps || isLoggedIn()"} 105 {if="!$hide_timestamps || $is_logged_in"}
106 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} 106 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
107 <span class="linkdate" title="Permalink"> 107 <span class="linkdate" title="Permalink">
108 <a href="?{$value.shorturl}"> 108 <a href="?{$value.shorturl}">
diff --git a/tpl/vintage/linklist.paging.html b/tpl/vintage/linklist.paging.html
index e3b88ee6..35149a6b 100644
--- a/tpl/vintage/linklist.paging.html
+++ b/tpl/vintage/linklist.paging.html
@@ -1,5 +1,5 @@
1<div class="paging"> 1<div class="paging">
2{if="isLoggedIn()"} 2{if="$is_logged_in"}
3 <div class="paging_privatelinks"> 3 <div class="paging_privatelinks">
4 <a href="?visibility=private"> 4 <a href="?visibility=private">
5 {if="$visibility=='private'"} 5 {if="$visibility=='private'"}
diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html
index 1485e1ce..f409721e 100644
--- a/tpl/vintage/page.footer.html
+++ b/tpl/vintage/page.footer.html
@@ -25,7 +25,7 @@
25 25
26<script src="js/shaarli.min.js"></script> 26<script src="js/shaarli.min.js"></script>
27 27
28{if="isLoggedIn()"} 28{if="$is_logged_in"}
29<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> 29<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
30{/if} 30{/if}
31 31
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html
index 8a58844e..40c53e5b 100644
--- a/tpl/vintage/page.header.html
+++ b/tpl/vintage/page.header.html
@@ -17,7 +17,7 @@
17 {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore} 17 {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore}
18{else} 18{else}
19<li><a href="{$titleLink}" class="nomobile">Home</a></li> 19<li><a href="{$titleLink}" class="nomobile">Home</a></li>
20 {if="isLoggedIn()"} 20 {if="$is_logged_in"}
21 <li><a href="?do=logout">Logout</a></li> 21 <li><a href="?do=logout">Logout</a></li>
22 <li><a href="?do=tools">Tools</a></li> 22 <li><a href="?do=tools">Tools</a></li>
23 <li><a href="?do=addlink">Add link</a></li> 23 <li><a href="?do=addlink">Add link</a></li>
@@ -46,7 +46,7 @@
46 </ul> 46 </ul>
47</div> 47</div>
48 48
49{if="!empty($plugin_errors) && isLoggedIn()"} 49{if="!empty($plugin_errors) && $is_logged_in"}
50 <ul class="errors"> 50 <ul class="errors">
51 {loop="$plugin_errors"} 51 {loop="$plugin_errors"}
52 <li>{$value}</li> 52 <li>{$value}</li>
diff --git a/yarn.lock b/yarn.lock
index aeeef8a7..f1c8e9cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -30,11 +30,19 @@ acorn@^5.0.0, acorn@^5.4.0:
30 version "5.4.1" 30 version "5.4.1"
31 resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102" 31 resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
32 32
33acorn@^5.5.0:
34 version "5.5.3"
35 resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
36
37ajv-keywords@^1.0.0:
38 version "1.5.1"
39 resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
40
33ajv-keywords@^2.0.0, ajv-keywords@^2.1.0: 41ajv-keywords@^2.0.0, ajv-keywords@^2.1.0:
34 version "2.1.1" 42 version "2.1.1"
35 resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" 43 resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
36 44
37ajv@^4.9.1: 45ajv@^4.7.0, ajv@^4.9.1:
38 version "4.11.8" 46 version "4.11.8"
39 resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" 47 resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
40 dependencies: 48 dependencies:
@@ -66,6 +74,10 @@ amdefine@>=0.0.4:
66 version "1.0.1" 74 version "1.0.1"
67 resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" 75 resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
68 76
77ansi-escapes@^1.1.0:
78 version "1.4.0"
79 resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
80
69ansi-escapes@^3.0.0: 81ansi-escapes@^3.0.0:
70 version "3.0.0" 82 version "3.0.0"
71 resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" 83 resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
@@ -1001,6 +1013,10 @@ browserslist@^2.1.2:
1001 caniuse-lite "^1.0.30000792" 1013 caniuse-lite "^1.0.30000792"
1002 electron-to-chromium "^1.3.30" 1014 electron-to-chromium "^1.3.30"
1003 1015
1016buffer-from@^1.0.0:
1017 version "1.0.0"
1018 resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
1019
1004buffer-xor@^1.0.3: 1020buffer-xor@^1.0.3:
1005 version "1.0.3" 1021 version "1.0.3"
1006 resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" 1022 resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
@@ -1086,7 +1102,7 @@ center-align@^0.1.1:
1086 align-text "^0.1.3" 1102 align-text "^0.1.3"
1087 lazy-cache "^1.0.3" 1103 lazy-cache "^1.0.3"
1088 1104
1089chalk@^1.1.1, chalk@^1.1.3: 1105chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
1090 version "1.1.3" 1106 version "1.1.3"
1091 resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 1107 resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
1092 dependencies: 1108 dependencies:
@@ -1140,6 +1156,12 @@ clap@^1.0.9:
1140 dependencies: 1156 dependencies:
1141 chalk "^1.1.3" 1157 chalk "^1.1.3"
1142 1158
1159cli-cursor@^1.0.1:
1160 version "1.0.2"
1161 resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
1162 dependencies:
1163 restore-cursor "^1.0.1"
1164
1143cli-cursor@^2.1.0: 1165cli-cursor@^2.1.0:
1144 version "2.1.0" 1166 version "2.1.0"
1145 resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" 1167 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:
1235 dependencies: 1257 dependencies:
1236 delayed-stream "~1.0.0" 1258 delayed-stream "~1.0.0"
1237 1259
1260commander@^2.8.1:
1261 version "2.15.1"
1262 resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
1263
1238commander@^2.9.0: 1264commander@^2.9.0:
1239 version "2.13.0" 1265 version "2.13.0"
1240 resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" 1266 resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
@@ -1247,6 +1273,15 @@ concat-map@0.0.1:
1247 version "0.0.1" 1273 version "0.0.1"
1248 resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 1274 resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
1249 1275
1276concat-stream@^1.4.6:
1277 version "1.6.2"
1278 resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
1279 dependencies:
1280 buffer-from "^1.0.0"
1281 inherits "^2.0.3"
1282 readable-stream "^2.2.2"
1283 typedarray "^0.0.6"
1284
1250concat-stream@^1.6.0: 1285concat-stream@^1.6.0:
1251 version "1.6.0" 1286 version "1.6.0"
1252 resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" 1287 resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
@@ -1456,7 +1491,7 @@ date-now@^0.1.4:
1456 version "0.1.4" 1491 version "0.1.4"
1457 resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" 1492 resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
1458 1493
1459debug@^2.2.0, debug@^2.6.8, debug@^2.6.9: 1494debug@^2.1.1, debug@^2.2.0, debug@^2.6.8, debug@^2.6.9:
1460 version "2.6.9" 1495 version "2.6.9"
1461 resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 1496 resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
1462 dependencies: 1497 dependencies:
@@ -1529,7 +1564,7 @@ diffie-hellman@^5.0.0:
1529 miller-rabin "^4.0.0" 1564 miller-rabin "^4.0.0"
1530 randombytes "^2.0.0" 1565 randombytes "^2.0.0"
1531 1566
1532doctrine@1.5.0: 1567doctrine@1.5.0, doctrine@^1.2.2:
1533 version "1.5.0" 1568 version "1.5.0"
1534 resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" 1569 resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
1535 dependencies: 1570 dependencies:
@@ -1708,6 +1743,44 @@ eslint-visitor-keys@^1.0.0:
1708 version "1.0.0" 1743 version "1.0.0"
1709 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" 1744 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
1710 1745
1746eslint@^2.7.0:
1747 version "2.13.1"
1748 resolved "https://registry.yarnpkg.com/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11"
1749 dependencies:
1750 chalk "^1.1.3"
1751 concat-stream "^1.4.6"
1752 debug "^2.1.1"
1753 doctrine "^1.2.2"
1754 es6-map "^0.1.3"
1755 escope "^3.6.0"
1756 espree "^3.1.6"
1757 estraverse "^4.2.0"
1758 esutils "^2.0.2"
1759 file-entry-cache "^1.1.1"
1760 glob "^7.0.3"
1761 globals "^9.2.0"
1762 ignore "^3.1.2"
1763 imurmurhash "^0.1.4"
1764 inquirer "^0.12.0"
1765 is-my-json-valid "^2.10.0"
1766 is-resolvable "^1.0.0"
1767 js-yaml "^3.5.1"
1768 json-stable-stringify "^1.0.0"
1769 levn "^0.3.0"
1770 lodash "^4.0.0"
1771 mkdirp "^0.5.0"
1772 optionator "^0.8.1"
1773 path-is-absolute "^1.0.0"
1774 path-is-inside "^1.0.1"
1775 pluralize "^1.2.1"
1776 progress "^1.1.8"
1777 require-uncached "^1.0.2"
1778 shelljs "^0.6.0"
1779 strip-json-comments "~1.0.1"
1780 table "^3.7.8"
1781 text-table "~0.2.0"
1782 user-home "^2.0.0"
1783
1711eslint@^4.16.0: 1784eslint@^4.16.0:
1712 version "4.17.0" 1785 version "4.17.0"
1713 resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.17.0.tgz#dc24bb51ede48df629be7031c71d9dc0ee4f3ddf" 1786 resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.17.0.tgz#dc24bb51ede48df629be7031c71d9dc0ee4f3ddf"
@@ -1750,6 +1823,13 @@ eslint@^4.16.0:
1750 table "^4.0.1" 1823 table "^4.0.1"
1751 text-table "~0.2.0" 1824 text-table "~0.2.0"
1752 1825
1826espree@^3.1.6:
1827 version "3.5.4"
1828 resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
1829 dependencies:
1830 acorn "^5.5.0"
1831 acorn-jsx "^3.0.0"
1832
1753espree@^3.5.2: 1833espree@^3.5.2:
1754 version "3.5.3" 1834 version "3.5.3"
1755 resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.3.tgz#931e0af64e7fbbed26b050a29daad1fc64799fa6" 1835 resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.3.tgz#931e0af64e7fbbed26b050a29daad1fc64799fa6"
@@ -1778,7 +1858,7 @@ esrecurse@^4.1.0:
1778 estraverse "^4.1.0" 1858 estraverse "^4.1.0"
1779 object-assign "^4.0.1" 1859 object-assign "^4.0.1"
1780 1860
1781estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: 1861estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
1782 version "4.2.0" 1862 version "4.2.0"
1783 resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" 1863 resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
1784 1864
@@ -1816,6 +1896,10 @@ execa@^0.7.0:
1816 signal-exit "^3.0.0" 1896 signal-exit "^3.0.0"
1817 strip-eof "^1.0.0" 1897 strip-eof "^1.0.0"
1818 1898
1899exit-hook@^1.0.0:
1900 version "1.1.1"
1901 resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
1902
1819expand-brackets@^0.1.4: 1903expand-brackets@^0.1.4:
1820 version "0.1.5" 1904 version "0.1.5"
1821 resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" 1905 resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
@@ -1879,12 +1963,26 @@ fastparse@^1.1.1:
1879 version "1.1.1" 1963 version "1.1.1"
1880 resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" 1964 resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
1881 1965
1966figures@^1.3.5:
1967 version "1.7.0"
1968 resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
1969 dependencies:
1970 escape-string-regexp "^1.0.5"
1971 object-assign "^4.1.0"
1972
1882figures@^2.0.0: 1973figures@^2.0.0:
1883 version "2.0.0" 1974 version "2.0.0"
1884 resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" 1975 resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
1885 dependencies: 1976 dependencies:
1886 escape-string-regexp "^1.0.5" 1977 escape-string-regexp "^1.0.5"
1887 1978
1979file-entry-cache@^1.1.1:
1980 version "1.3.1"
1981 resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-1.3.1.tgz#44c61ea607ae4be9c1402f41f44270cbfe334ff8"
1982 dependencies:
1983 flat-cache "^1.2.1"
1984 object-assign "^4.0.1"
1985
1888file-entry-cache@^2.0.0: 1986file-entry-cache@^2.0.0:
1889 version "2.0.0" 1987 version "2.0.0"
1890 resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" 1988 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:
1991 combined-stream "^1.0.5" 2089 combined-stream "^1.0.5"
1992 mime-types "^2.1.12" 2090 mime-types "^2.1.12"
1993 2091
2092front-matter@2.1.2:
2093 version "2.1.2"
2094 resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-2.1.2.tgz#f75983b9f2f413be658c93dfd7bd8ce4078f5cdb"
2095 dependencies:
2096 js-yaml "^3.4.6"
2097
2098fs-extra@^3.0.1:
2099 version "3.0.1"
2100 resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
2101 dependencies:
2102 graceful-fs "^4.1.2"
2103 jsonfile "^3.0.0"
2104 universalify "^0.1.0"
2105
1994fs.realpath@^1.0.0: 2106fs.realpath@^1.0.0:
1995 version "1.0.0" 2107 version "1.0.0"
1996 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 2108 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -2112,7 +2224,7 @@ globals@^11.0.1:
2112 version "11.3.0" 2224 version "11.3.0"
2113 resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0" 2225 resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0"
2114 2226
2115globals@^9.18.0: 2227globals@^9.18.0, globals@^9.2.0:
2116 version "9.18.0" 2228 version "9.18.0"
2117 resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" 2229 resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
2118 2230
@@ -2135,7 +2247,13 @@ globule@^1.0.0:
2135 lodash "~4.17.4" 2247 lodash "~4.17.4"
2136 minimatch "~3.0.2" 2248 minimatch "~3.0.2"
2137 2249
2138graceful-fs@^4.1.2: 2250gonzales-pe-sl@^4.2.3:
2251 version "4.2.3"
2252 resolved "https://registry.yarnpkg.com/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz#6a868bc380645f141feeb042c6f97fcc71b59fe6"
2253 dependencies:
2254 minimist "1.1.x"
2255
2256graceful-fs@^4.1.2, graceful-fs@^4.1.6:
2139 version "4.1.11" 2257 version "4.1.11"
2140 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 2258 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
2141 2259
@@ -2301,7 +2419,7 @@ ieee754@^1.1.4:
2301 version "1.1.8" 2419 version "1.1.8"
2302 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" 2420 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
2303 2421
2304ignore@^3.3.3: 2422ignore@^3.1.2, ignore@^3.3.3:
2305 version "3.3.7" 2423 version "3.3.7"
2306 resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" 2424 resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
2307 2425
@@ -2346,6 +2464,24 @@ ini@~1.3.0:
2346 version "1.3.5" 2464 version "1.3.5"
2347 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" 2465 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
2348 2466
2467inquirer@^0.12.0:
2468 version "0.12.0"
2469 resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
2470 dependencies:
2471 ansi-escapes "^1.1.0"
2472 ansi-regex "^2.0.0"
2473 chalk "^1.0.0"
2474 cli-cursor "^1.0.1"
2475 cli-width "^2.0.0"
2476 figures "^1.3.5"
2477 lodash "^4.3.0"
2478 readline2 "^1.0.1"
2479 run-async "^0.1.0"
2480 rx-lite "^3.1.2"
2481 string-width "^1.0.1"
2482 strip-ansi "^3.0.0"
2483 through "^2.3.6"
2484
2349inquirer@^3.0.6: 2485inquirer@^3.0.6:
2350 version "3.3.0" 2486 version "3.3.0"
2351 resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" 2487 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:
2443 dependencies: 2579 dependencies:
2444 is-extglob "^1.0.0" 2580 is-extglob "^1.0.0"
2445 2581
2582is-my-ip-valid@^1.0.0:
2583 version "1.0.0"
2584 resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
2585
2586is-my-json-valid@^2.10.0:
2587 version "2.17.2"
2588 resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
2589 dependencies:
2590 generate-function "^2.0.0"
2591 generate-object-property "^1.1.0"
2592 is-my-ip-valid "^1.0.0"
2593 jsonpointer "^4.0.0"
2594 xtend "^4.0.0"
2595
2446is-my-json-valid@^2.12.4: 2596is-my-json-valid@^2.12.4:
2447 version "2.17.1" 2597 version "2.17.1"
2448 resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz#3da98914a70a22f0a8563ef1511a246c6fc55471" 2598 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:
2558 version "3.0.2" 2708 version "3.0.2"
2559 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 2709 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
2560 2710
2711js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4:
2712 version "3.11.0"
2713 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
2714 dependencies:
2715 argparse "^1.0.7"
2716 esprima "^4.0.0"
2717
2561js-yaml@^3.9.1: 2718js-yaml@^3.9.1:
2562 version "3.10.0" 2719 version "3.10.0"
2563 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" 2720 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:
2600 version "1.0.1" 2757 version "1.0.1"
2601 resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 2758 resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
2602 2759
2603json-stable-stringify@^1.0.1: 2760json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
2604 version "1.0.1" 2761 version "1.0.1"
2605 resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" 2762 resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
2606 dependencies: 2763 dependencies:
@@ -2614,6 +2771,12 @@ json5@^0.5.0, json5@^0.5.1:
2614 version "0.5.1" 2771 version "0.5.1"
2615 resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" 2772 resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
2616 2773
2774jsonfile@^3.0.0:
2775 version "3.0.1"
2776 resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
2777 optionalDependencies:
2778 graceful-fs "^4.1.6"
2779
2617jsonify@~0.0.0: 2780jsonify@~0.0.0:
2618 version "0.0.0" 2781 version "0.0.0"
2619 resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 2782 resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
@@ -2649,6 +2812,10 @@ kind-of@^4.0.0:
2649 dependencies: 2812 dependencies:
2650 is-buffer "^1.1.5" 2813 is-buffer "^1.1.5"
2651 2814
2815known-css-properties@^0.3.0:
2816 version "0.3.0"
2817 resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4"
2818
2652lazy-cache@^0.2.3: 2819lazy-cache@^0.2.3:
2653 version "0.2.7" 2820 version "0.2.7"
2654 resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" 2821 resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
@@ -2716,6 +2883,10 @@ lodash.camelcase@^4.3.0:
2716 version "4.3.0" 2883 version "4.3.0"
2717 resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" 2884 resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
2718 2885
2886lodash.capitalize@^4.1.0:
2887 version "4.2.1"
2888 resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9"
2889
2719lodash.clonedeep@^4.3.2: 2890lodash.clonedeep@^4.3.2:
2720 version "4.5.0" 2891 version "4.5.0"
2721 resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" 2892 resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
@@ -2728,6 +2899,10 @@ lodash.isplainobject@^4.0.6:
2728 version "4.0.6" 2899 version "4.0.6"
2729 resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 2900 resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
2730 2901
2902lodash.kebabcase@^4.0.0:
2903 version "4.1.1"
2904 resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
2905
2731lodash.memoize@^4.1.2: 2906lodash.memoize@^4.1.2:
2732 version "4.1.2" 2907 version "4.1.2"
2733 resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" 2908 resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -2829,6 +3004,10 @@ meow@^3.7.0:
2829 redent "^1.0.0" 3004 redent "^1.0.0"
2830 trim-newlines "^1.0.0" 3005 trim-newlines "^1.0.0"
2831 3006
3007merge@^1.2.0:
3008 version "1.2.0"
3009 resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
3010
2832micromatch@^2.1.5: 3011micromatch@^2.1.5:
2833 version "2.3.11" 3012 version "2.3.11"
2834 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" 3013 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
@@ -2890,6 +3069,10 @@ minimist@0.0.8:
2890 version "0.0.8" 3069 version "0.0.8"
2891 resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 3070 resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
2892 3071
3072minimist@1.1.x:
3073 version "1.1.3"
3074 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8"
3075
2893minimist@^1.1.3, minimist@^1.2.0: 3076minimist@^1.1.3, minimist@^1.2.0:
2894 version "1.2.0" 3077 version "1.2.0"
2895 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 3078 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -2911,6 +3094,10 @@ ms@2.0.0:
2911 version "2.0.0" 3094 version "2.0.0"
2912 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 3095 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
2913 3096
3097mute-stream@0.0.5:
3098 version "0.0.5"
3099 resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
3100
2914mute-stream@0.0.7: 3101mute-stream@0.0.7:
2915 version "0.0.7" 3102 version "0.0.7"
2916 resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" 3103 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:
3094 dependencies: 3281 dependencies:
3095 wrappy "1" 3282 wrappy "1"
3096 3283
3284onetime@^1.0.0:
3285 version "1.1.0"
3286 resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
3287
3097onetime@^2.0.0: 3288onetime@^2.0.0:
3098 version "2.0.1" 3289 version "2.0.1"
3099 resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" 3290 resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
3100 dependencies: 3291 dependencies:
3101 mimic-fn "^1.0.0" 3292 mimic-fn "^1.0.0"
3102 3293
3103optionator@^0.8.2: 3294optionator@^0.8.1, optionator@^0.8.2:
3104 version "0.8.2" 3295 version "0.8.2"
3105 resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" 3296 resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
3106 dependencies: 3297 dependencies:
@@ -3285,6 +3476,10 @@ pkg-dir@^2.0.0:
3285 dependencies: 3476 dependencies:
3286 find-up "^2.1.0" 3477 find-up "^2.1.0"
3287 3478
3479pluralize@^1.2.1:
3480 version "1.2.1"
3481 resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
3482
3288pluralize@^7.0.0: 3483pluralize@^7.0.0:
3289 version "7.0.0" 3484 version "7.0.0"
3290 resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" 3485 resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
@@ -3559,6 +3754,10 @@ process@^0.11.10:
3559 version "0.11.10" 3754 version "0.11.10"
3560 resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 3755 resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
3561 3756
3757progress@^1.1.8:
3758 version "1.1.8"
3759 resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
3760
3562progress@^2.0.0: 3761progress@^2.0.0:
3563 version "2.0.0" 3762 version "2.0.0"
3564 resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" 3763 resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
@@ -3708,6 +3907,14 @@ readdirp@^2.0.0:
3708 readable-stream "^2.0.2" 3907 readable-stream "^2.0.2"
3709 set-immediate-shim "^1.0.1" 3908 set-immediate-shim "^1.0.1"
3710 3909
3910readline2@^1.0.1:
3911 version "1.0.1"
3912 resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
3913 dependencies:
3914 code-point-at "^1.0.0"
3915 is-fullwidth-code-point "^1.0.0"
3916 mute-stream "0.0.5"
3917
3711redent@^1.0.0: 3918redent@^1.0.0:
3712 version "1.0.0" 3919 version "1.0.0"
3713 resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" 3920 resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@@ -3882,7 +4089,7 @@ require-main-filename@^1.0.1:
3882 version "1.0.1" 4089 version "1.0.1"
3883 resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" 4090 resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
3884 4091
3885require-uncached@^1.0.3: 4092require-uncached@^1.0.2, require-uncached@^1.0.3:
3886 version "1.0.3" 4093 version "1.0.3"
3887 resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" 4094 resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
3888 dependencies: 4095 dependencies:
@@ -3899,6 +4106,13 @@ resolve@^1.5.0:
3899 dependencies: 4106 dependencies:
3900 path-parse "^1.0.5" 4107 path-parse "^1.0.5"
3901 4108
4109restore-cursor@^1.0.1:
4110 version "1.0.1"
4111 resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
4112 dependencies:
4113 exit-hook "^1.0.0"
4114 onetime "^1.0.0"
4115
3902restore-cursor@^2.0.0: 4116restore-cursor@^2.0.0:
3903 version "2.0.0" 4117 version "2.0.0"
3904 resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" 4118 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:
3925 hash-base "^2.0.0" 4139 hash-base "^2.0.0"
3926 inherits "^2.0.1" 4140 inherits "^2.0.1"
3927 4141
4142run-async@^0.1.0:
4143 version "0.1.0"
4144 resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
4145 dependencies:
4146 once "^1.3.0"
4147
3928run-async@^2.2.0: 4148run-async@^2.2.0:
3929 version "2.3.0" 4149 version "2.3.0"
3930 resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" 4150 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:
3941 version "4.0.8" 4161 version "4.0.8"
3942 resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" 4162 resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
3943 4163
4164rx-lite@^3.1.2:
4165 version "3.1.2"
4166 resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
4167
3944safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 4168safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
3945 version "5.1.1" 4169 version "5.1.1"
3946 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 4170 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -3954,6 +4178,25 @@ sass-graph@^2.2.4:
3954 scss-tokenizer "^0.2.3" 4178 scss-tokenizer "^0.2.3"
3955 yargs "^7.0.0" 4179 yargs "^7.0.0"
3956 4180
4181sass-lint@^1.12.1:
4182 version "1.12.1"
4183 resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83"
4184 dependencies:
4185 commander "^2.8.1"
4186 eslint "^2.7.0"
4187 front-matter "2.1.2"
4188 fs-extra "^3.0.1"
4189 glob "^7.0.0"
4190 globule "^1.0.0"
4191 gonzales-pe-sl "^4.2.3"
4192 js-yaml "^3.5.4"
4193 known-css-properties "^0.3.0"
4194 lodash.capitalize "^4.1.0"
4195 lodash.kebabcase "^4.0.0"
4196 merge "^1.2.0"
4197 path-is-absolute "^1.0.0"
4198 util "^0.10.3"
4199
3957sass-loader@^6.0.6: 4200sass-loader@^6.0.6:
3958 version "6.0.6" 4201 version "6.0.6"
3959 resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9" 4202 resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9"
@@ -4027,6 +4270,10 @@ shebang-regex@^1.0.0:
4027 version "1.0.0" 4270 version "1.0.0"
4028 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 4271 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
4029 4272
4273shelljs@^0.6.0:
4274 version "0.6.1"
4275 resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8"
4276
4030signal-exit@^3.0.0, signal-exit@^3.0.2: 4277signal-exit@^3.0.0, signal-exit@^3.0.2:
4031 version "3.0.2" 4278 version "3.0.2"
4032 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 4279 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -4035,6 +4282,10 @@ slash@^1.0.0:
4035 version "1.0.0" 4282 version "1.0.0"
4036 resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 4283 resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
4037 4284
4285slice-ansi@0.0.4:
4286 version "0.0.4"
4287 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
4288
4038slice-ansi@1.0.0: 4289slice-ansi@1.0.0:
4039 version "1.0.0" 4290 version "1.0.0"
4040 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" 4291 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
@@ -4199,6 +4450,10 @@ strip-indent@^1.0.1:
4199 dependencies: 4450 dependencies:
4200 get-stdin "^4.0.1" 4451 get-stdin "^4.0.1"
4201 4452
4453strip-json-comments@~1.0.1:
4454 version "1.0.4"
4455 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
4456
4202strip-json-comments@~2.0.1: 4457strip-json-comments@~2.0.1:
4203 version "2.0.1" 4458 version "2.0.1"
4204 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 4459 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -4244,6 +4499,17 @@ svgo@^0.7.0:
4244 sax "~1.2.1" 4499 sax "~1.2.1"
4245 whet.extend "~0.9.9" 4500 whet.extend "~0.9.9"
4246 4501
4502table@^3.7.8:
4503 version "3.8.3"
4504 resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
4505 dependencies:
4506 ajv "^4.7.0"
4507 ajv-keywords "^1.0.0"
4508 chalk "^1.1.1"
4509 lodash "^4.0.0"
4510 slice-ansi "0.0.4"
4511 string-width "^2.0.0"
4512
4247table@^4.0.1: 4513table@^4.0.1:
4248 version "4.0.2" 4514 version "4.0.2"
4249 resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" 4515 resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
@@ -4395,6 +4661,10 @@ uniqs@^2.0.0:
4395 version "2.0.0" 4661 version "2.0.0"
4396 resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" 4662 resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
4397 4663
4664universalify@^0.1.0:
4665 version "0.1.1"
4666 resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
4667
4398url-loader@^0.6.2: 4668url-loader@^0.6.2:
4399 version "0.6.2" 4669 version "0.6.2"
4400 resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7" 4670 resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
@@ -4410,6 +4680,12 @@ url@^0.11.0:
4410 punycode "1.3.2" 4680 punycode "1.3.2"
4411 querystring "0.2.0" 4681 querystring "0.2.0"
4412 4682
4683user-home@^2.0.0:
4684 version "2.0.0"
4685 resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
4686 dependencies:
4687 os-homedir "^1.0.0"
4688
4413util-deprecate@~1.0.1: 4689util-deprecate@~1.0.1:
4414 version "1.0.2" 4690 version "1.0.2"
4415 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 4691 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"