diff options
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 @@ | |||
1 | options: | ||
2 | max-warnings: 0 | ||
3 | rules: | ||
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 | |||
10 | indent_style = space | 10 | indent_style = space |
11 | indent_size = 4 | 11 | indent_size = 4 |
12 | 12 | ||
13 | [*.{htaccess,html,js,json,xml,yml}] | 13 | [*.{htaccess,html,scss,js,json,xml,yml}] |
14 | indent_size = 2 | 14 | indent_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 | ||
31 | cache: | 32 | cache: |
32 | directories: | 33 | directories: |
@@ -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 |
220 | eslint: | 220 | eslint: |
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 | ||
225 | sasslint: | ||
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 | */ | ||
435 | function 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 | ||
2 | namespace Shaarli; | ||
3 | |||
4 | /** | ||
5 | * User login management | ||
6 | */ | ||
7 | class 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 | ||
2 | namespace Shaarli; | ||
3 | |||
4 | /** | ||
5 | * Manages the server-side session | ||
6 | */ | ||
7 | class 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 | ||
2 | namespace Shaarli\Security; | ||
3 | |||
4 | use Shaarli\Config\ConfigManager; | ||
5 | |||
6 | /** | ||
7 | * User login management | ||
8 | */ | ||
9 | class 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 | ||
2 | namespace Shaarli\Security; | ||
3 | |||
4 | use Shaarli\Config\ConfigManager; | ||
5 | |||
6 | /** | ||
7 | * Manages the server-side session | ||
8 | */ | ||
9 | class 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 | ||
12 | body { | 29 | body { |
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 | ||
41 | pre { | 58 | pre { |
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 | 82 | body, | |
67 | body, .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 |
948 | div.awesomplete { | 1123 | div { |
1124 | &.awesomplete { | ||
949 | width: inherit; | 1125 | width: inherit; |
950 | } | ||
951 | 1126 | ||
952 | div.awesomplete > input { | 1127 | > input { |
953 | display: inherit; | 1128 | display: inherit; |
954 | } | 1129 | } |
955 | 1130 | ||
956 | div.awesomplete > ul { | 1131 | > ul { |
957 | z-index: 9999; | 1132 | z-index: 9999; |
1133 | } | ||
1134 | } | ||
958 | } | 1135 | } |
959 | 1136 | ||
960 | .page-form .awesomplete { | 1137 | form { |
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 | ||
972 | form[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 | } |
@@ -78,8 +78,8 @@ require_once 'application/Updater.php'; | |||
78 | use \Shaarli\Languages; | 78 | use \Shaarli\Languages; |
79 | use \Shaarli\ThemeUtils; | 79 | use \Shaarli\ThemeUtils; |
80 | use \Shaarli\Config\ConfigManager; | 80 | use \Shaarli\Config\ConfigManager; |
81 | use \Shaarli\LoginManager; | 81 | use \Shaarli\Security\LoginManager; |
82 | use \Shaarli\SessionManager; | 82 | use \Shaarli\Security\SessionManager; |
83 | 83 | ||
84 | // Ensure the PHP version is supported | 84 | // Ensure the PHP version is supported |
85 | try { | 85 | try { |
@@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') { | |||
101 | // Set default cookie expiration and path. | 101 | // Set default cookie expiration and path. |
102 | session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); | 102 | session_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. | ||
105 | define('INACTIVITY_TIMEOUT', 3600); // in seconds. | ||
106 | // Use cookies to store session. | 104 | // Use cookies to store session. |
107 | ini_set('session.use_cookies', 1); | 105 | ini_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. |
130 | if (! defined('LC_MESSAGES')) { | 130 | if (! 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); |
181 | define('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 | */ |
190 | function 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.) | ||
235 | function 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 | */ | ||
249 | function 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 | */ | ||
266 | function 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. | ||
280 | function isLoggedIn() | 189 | function isLoggedIn() |
281 | { | 190 | { |
282 | global $userIsLoggedIn; | 191 | global $loginManager; |
283 | return $userIsLoggedIn; | 192 | return $loginManager->isLoggedIn(); |
284 | } | 193 | } |
285 | 194 | ||
286 | // Force logout. | ||
287 | function 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. |
300 | if (isset($_POST['login'])) | 198 | if (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 | */ |
385 | function showDailyRSS($conf) { | 290 | function 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 | */ |
487 | function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) | 393 | function 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 | */ |
589 | function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { | 495 | function 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 | */ |
1596 | function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) | 1504 | function 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 | ||
9 | use 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('/(^| ) /m', '$1 ', $description); | 247 | return preg_replace('/(^| ) /m', '$1 ', $description); |
245 | } | 248 | } |
246 | 249 | ||
250 | function reverse_feed_permalink($description) | ||
251 | { | ||
252 | return preg_replace('@— <a href="([^"]+)" title="[^"]+">(\w+)</a>$@im', '— [$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 | |||
6 | require_once 'application/HttpUtils.php'; | ||
7 | |||
8 | /** | ||
9 | * Unitary tests for client_ip_id() | ||
10 | */ | ||
11 | class 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 | ||
2 | require_once 'tests/utils/FakeConfigManager.php'; | ||
3 | |||
4 | // Initialize reference data _before_ PHPUnit starts a session | ||
5 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | ||
6 | ReferenceSessionIdHashes::genAllHashes(); | ||
7 | |||
8 | use \Shaarli\SessionManager; | ||
9 | use \PHPUnit\Framework\TestCase; | ||
10 | |||
11 | |||
12 | /** | ||
13 | * Test coverage for SessionManager | ||
14 | */ | ||
15 | class 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 .= '— <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 | '— <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 .= '— <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>'; | ||
135 | $expected = 'Description... — [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>— <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>'; | ||
145 | $expected = $text; | ||
146 | $text .= '<br>— <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>'; | ||
147 | $expected .= '<br>— [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 |
2 | namespace Shaarli; | 2 | namespace Shaarli\Security; |
3 | 3 | ||
4 | require_once 'tests/utils/FakeConfigManager.php'; | 4 | require_once 'tests/utils/FakeConfigManager.php'; |
5 | use \PHPUnit\Framework\TestCase; | 5 | use \PHPUnit\Framework\TestCase; |
@@ -9,15 +9,54 @@ use \PHPUnit\Framework\TestCase; | |||
9 | */ | 9 | */ |
10 | class LoginManagerTest extends TestCase | 10 | class 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 | ||
2 | require_once 'tests/utils/FakeConfigManager.php'; | ||
3 | |||
4 | // Initialize reference data _before_ PHPUnit starts a session | ||
5 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | ||
6 | ReferenceSessionIdHashes::genAllHashes(); | ||
7 | |||
8 | use \Shaarli\Security\SessionManager; | ||
9 | use \PHPUnit\Framework\TestCase; | ||
10 | |||
11 | |||
12 | /** | ||
13 | * Test coverage for SessionManager | ||
14 | */ | ||
15 | class 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"}·{/if} | 236 | {if="$link_plugin_counter - 1 != $counter"}·{/if} |
237 | {/loop} | 237 | {/loop} |
238 | {/if} | 238 | {/if} |
239 | {if="isLoggedIn()"} | 239 | {if="$is_logged_in"} |
240 | · | 240 | · |
241 | <a href="?delete_link&lf_linkdate={$value.id}&token={$token}" | 241 | <a href="?delete_link&lf_linkdate={$value.id}&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 | · | 10 | · |
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> | 53 | <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a> |
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> |
@@ -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 | ||
33 | acorn@^5.5.0: | ||
34 | version "5.5.3" | ||
35 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" | ||
36 | |||
37 | ajv-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 | |||
33 | ajv-keywords@^2.0.0, ajv-keywords@^2.1.0: | 41 | ajv-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 | ||
37 | ajv@^4.9.1: | 45 | ajv@^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 | ||
77 | ansi-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 | |||
69 | ansi-escapes@^3.0.0: | 81 | ansi-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 | ||
1016 | buffer-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 | |||
1004 | buffer-xor@^1.0.3: | 1020 | buffer-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 | ||
1089 | chalk@^1.1.1, chalk@^1.1.3: | 1105 | chalk@^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 | ||
1159 | cli-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 | |||
1143 | cli-cursor@^2.1.0: | 1165 | cli-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 | ||
1260 | commander@^2.8.1: | ||
1261 | version "2.15.1" | ||
1262 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" | ||
1263 | |||
1238 | commander@^2.9.0: | 1264 | commander@^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 | ||
1276 | concat-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 | |||
1250 | concat-stream@^1.6.0: | 1285 | concat-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 | ||
1459 | debug@^2.2.0, debug@^2.6.8, debug@^2.6.9: | 1494 | debug@^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 | ||
1532 | doctrine@1.5.0: | 1567 | doctrine@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 | ||
1746 | eslint@^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 | |||
1711 | eslint@^4.16.0: | 1784 | eslint@^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 | ||
1826 | espree@^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 | |||
1753 | espree@^3.5.2: | 1833 | espree@^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 | ||
1781 | estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: | 1861 | estraverse@^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 | ||
1899 | exit-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 | |||
1819 | expand-brackets@^0.1.4: | 1903 | expand-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 | ||
1966 | figures@^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 | |||
1882 | figures@^2.0.0: | 1973 | figures@^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 | ||
1979 | file-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 | |||
1888 | file-entry-cache@^2.0.0: | 1986 | file-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 | ||
2092 | front-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 | |||
2098 | fs-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 | |||
1994 | fs.realpath@^1.0.0: | 2106 | fs.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 | ||
2115 | globals@^9.18.0: | 2227 | globals@^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 | ||
2138 | graceful-fs@^4.1.2: | 2250 | gonzales-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 | |||
2256 | graceful-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 | ||
2304 | ignore@^3.3.3: | 2422 | ignore@^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 | ||
2467 | inquirer@^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 | |||
2349 | inquirer@^3.0.6: | 2485 | inquirer@^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 | ||
2582 | is-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 | |||
2586 | is-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 | |||
2446 | is-my-json-valid@^2.12.4: | 2596 | is-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 | ||
2711 | js-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 | |||
2561 | js-yaml@^3.9.1: | 2718 | js-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 | ||
2603 | json-stable-stringify@^1.0.1: | 2760 | json-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 | ||
2774 | jsonfile@^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 | |||
2617 | jsonify@~0.0.0: | 2780 | jsonify@~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 | ||
2815 | known-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 | |||
2652 | lazy-cache@^0.2.3: | 2819 | lazy-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 | ||
2886 | lodash.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 | |||
2719 | lodash.clonedeep@^4.3.2: | 2890 | lodash.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 | ||
2902 | lodash.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 | |||
2731 | lodash.memoize@^4.1.2: | 2906 | lodash.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 | ||
3007 | merge@^1.2.0: | ||
3008 | version "1.2.0" | ||
3009 | resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" | ||
3010 | |||
2832 | micromatch@^2.1.5: | 3011 | micromatch@^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 | ||
3072 | minimist@1.1.x: | ||
3073 | version "1.1.3" | ||
3074 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" | ||
3075 | |||
2893 | minimist@^1.1.3, minimist@^1.2.0: | 3076 | minimist@^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 | ||
3097 | mute-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 | |||
2914 | mute-stream@0.0.7: | 3101 | mute-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 | ||
3284 | onetime@^1.0.0: | ||
3285 | version "1.1.0" | ||
3286 | resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" | ||
3287 | |||
3097 | onetime@^2.0.0: | 3288 | onetime@^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 | ||
3103 | optionator@^0.8.2: | 3294 | optionator@^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 | ||
3479 | pluralize@^1.2.1: | ||
3480 | version "1.2.1" | ||
3481 | resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" | ||
3482 | |||
3288 | pluralize@^7.0.0: | 3483 | pluralize@^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 | ||
3757 | progress@^1.1.8: | ||
3758 | version "1.1.8" | ||
3759 | resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" | ||
3760 | |||
3562 | progress@^2.0.0: | 3761 | progress@^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 | ||
3910 | readline2@^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 | |||
3711 | redent@^1.0.0: | 3918 | redent@^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 | ||
3885 | require-uncached@^1.0.3: | 4092 | require-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 | ||
4109 | restore-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 | |||
3902 | restore-cursor@^2.0.0: | 4116 | restore-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 | ||
4142 | run-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 | |||
3928 | run-async@^2.2.0: | 4148 | run-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 | ||
4164 | rx-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 | |||
3944 | safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: | 4168 | safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: |
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 | ||
4181 | sass-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 | |||
3957 | sass-loader@^6.0.6: | 4200 | sass-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 | ||
4273 | shelljs@^0.6.0: | ||
4274 | version "0.6.1" | ||
4275 | resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" | ||
4276 | |||
4030 | signal-exit@^3.0.0, signal-exit@^3.0.2: | 4277 | signal-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 | ||
4285 | slice-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 | |||
4038 | slice-ansi@1.0.0: | 4289 | slice-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 | ||
4453 | strip-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 | |||
4202 | strip-json-comments@~2.0.1: | 4457 | strip-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 | ||
4502 | table@^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 | |||
4247 | table@^4.0.1: | 4513 | table@^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 | ||
4664 | universalify@^0.1.0: | ||
4665 | version "0.1.1" | ||
4666 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" | ||
4667 | |||
4398 | url-loader@^0.6.2: | 4668 | url-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 | ||
4683 | user-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 | |||
4413 | util-deprecate@~1.0.1: | 4689 | util-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" |