Rejigger the CAPTCHA a bunch, more functionality without JS

This commit is contained in:
Floorb 2022-08-25 01:51:54 -04:00
parent 0f519a8ced
commit 86afab0458
21 changed files with 312 additions and 68 deletions

View file

@ -67,10 +67,7 @@ function captcha($color, $mul, $allowed) : array {
} }
$captcha_config['code'] = ''; $captcha_config['code'] = '';
$length = rand($captcha_config['min_length'], $captcha_config['max_length']);
while (strlen($captcha_config['code']) < $length) {
$captcha_config['code'] .= substr($captcha_config['characters'], rand() % (strlen($captcha_config['characters'])), 1);
}
return $captcha_config; return $captcha_config;
} }
@ -97,3 +94,32 @@ if (!function_exists('hex2rgb')) {
return null; return null;
} }
} }
function setupCaptcha() : string {
global $captcha_config;
global $redis;
$code = '';
for ($i = 0; $i < 5; $i++) {
$code .= substr($captcha_config['allowed'], rand() % (strlen($captcha_config['allowed'])), 1);
}
$token = pp_random_password();
$redis->setex('captcha/' . md5($token), 600, $code);
return $token;
}
function checkCaptcha(string $token, string $answer) : bool {
global $redis;
$redis_answer = $redis->get('captcha/' . $token);
if (!$redis_answer) {
return false;
}
$redis->del('captcha/' . $token);
return $redis_answer === $answer;
}

View file

@ -237,3 +237,64 @@ function addToSitemap(Paste $paste, $priority, $changefreq) {
function paste_protocol() : string { function paste_protocol() : string {
return !empty($_SERVER['HTTPS']) ? 'https://' : 'http://'; return !empty($_SERVER['HTTPS']) ? 'https://' : 'http://';
} }
/* get rid of unintended wildcards in a parameter to LIKE queries; not a security issue, just unexpected behaviour. */
function escapeLikeQuery(string $query) : string {
return str_replace(['\\', '_', '%'], ['\\\\', '\\_', '\\%'], $query);
}
function paginate(int $current_page, int $per_page, int $total_records) : string {
$first_page = 0;
$last_page = floor($total_records / $per_page);
$window = 2;
if ($first_page == $last_page) {
// Do something?
}
$_page_button = function(int $page, string $text, bool $disabled = false) use ($current_page) : string {
/* We need to update the 'page' parameter in the request URI, or add it if it doesn't exist. */
$request_uri = parse_url($_SERVER['REQUEST_URI']);
parse_str((string) @$request_uri['query'], $parsed_query);
$parsed_query['page'] = (string) $page;
$page_uri = ((string) @$request_uri['path']) . '?' . http_build_query($parsed_query);
$selected_class = $current_page == $page ? ' paginator__button--selected' : '';
$disabled_text = $disabled ? ' aria-disabled="true"' : '';
return sprintf("<a type=\"button\" class=\"paginator__button$selected_class\" href=\"%s\"%s>%s</a>", $page_uri, $disabled_text, $text);
};
$html = '';
/* First and last page the main paginator will show */
$first_page_show = max(($current_page - $window), $first_page);
$last_page_show = min(($current_page + $window), $last_page);
/* Whether to show the first and last pages in existence at the ends of the paginator */
$show_first_page = (abs($first_page - $current_page)) > ($window);
$show_last_page = (abs($last_page - $current_page)) > ($window);
$prev_button_disabled = $current_page == $first_page ? 'disabled' : '';
$next_button_disabled = $current_page == $last_page ? 'disabled' : '';
$html .= $_page_button($current_page - 1, 'Previous', $prev_button_disabled);
if ($show_first_page) {
$html .= $_page_button($first_page, $first_page);
$html .= '<span class="ellipsis">…</span>';
}
for ($i = $first_page_show; $i <= $last_page_show; $i++) {
$html .= $_page_button($i, $i);
}
if ($show_last_page) {
$html .= '<span class="ellipsis">…</span>';
$html .= $_page_button($last_page, $last_page);
}
$html .= $_page_button($current_page + 1, 'Next', $next_button_disabled);
return $html;
}

View file

@ -5,6 +5,8 @@ const setupSignupModal = () => {
const signupButton = $('[data-target~="#signin"],[data-target~="#signup"]'); const signupButton = $('[data-target~="#signin"],[data-target~="#signup"]');
if (signupButton) { if (signupButton) {
signupButton.href = 'javascript:void(0)';
signupButton.addEventListener('click', () => { signupButton.addEventListener('click', () => {
$('.modal').classList.add('is-active'); $('.modal').classList.add('is-active');
}); });
@ -77,6 +79,23 @@ const globalSetup = () => {
preloader.remove(); preloader.remove();
main.id = ''; main.id = '';
} }
const captchaContainer = $('.captcha_container');
if (captchaContainer) {
const refreshElement = captchaContainer.querySelector('a');
const imageElement = captchaContainer.querySelector('img');
if (refreshElement && imageElement) {
refreshElement.addEventListener('click', () => {
imageElement.src = imageElement.src.split('?')[0] + '?rand=' + Math.random();
});
}
}
Array.prototype.forEach.call($('.js-hidden'), (elem) => {
toggleEl(elem);
});
} }
export { globalSetup }; export { globalSetup };

View file

@ -4,11 +4,6 @@ require_once(__DIR__ . '/../includes/common.php');
use PonePaste\Models\Tag; use PonePaste\Models\Tag;
/* get rid of unintended wildcards in a parameter to LIKE queries; not a security issue, just unexpected behaviour. */
function escapeLikeQuery(string $query) : string {
return str_replace(['\\', '_', '%'], ['\\\\', '\\_', '\\%'], $query);
}
header('Content-Type: application/json'); header('Content-Type: application/json');
if (empty($_GET['tag'])) { if (empty($_GET['tag'])) {
@ -23,6 +18,8 @@ $results = Tag::select('name')
->fetchAll() ->fetchAll()
->toArray(); ->toArray();
/* we want to ensure the tag name that the user input is always returned,
* even if that tag doesn't actually exist yet. */
$tags[] = ['name' => $tag_name]; $tags[] = ['name' => $tag_name];
echo json_encode($tags); echo json_encode($tags);

View file

@ -5,6 +5,43 @@ require_once(__DIR__ . '/../includes/common.php');
use PonePaste\Models\Paste; use PonePaste\Models\Paste;
$per_page = 20;
$current_page = 0;
$filter_value = '';
if (!empty($_GET['page'])) {
$current_page = max(0, intval($_GET['page']));
}
if (!empty($_GET['per_page'])) {
$per_page = max(1, min(100, intval($_GET['per_page'])));
}
if (!empty($_GET['q'])) {
$filter_value = $_GET['q'];
}
$pastes = Paste::with([
'user' => function($q) {
$q->select('users.id', 'username');
},
'tags' => function($q) {
$q->select('tags.id', 'name', 'slug');
}])
->select('id', 'user_id', 'title', 'created_at')
->where('visible', Paste::VISIBILITY_PUBLIC)
->whereRaw("((expiry IS NULL) OR ((expiry != 'SELF') AND (expiry > NOW())))");
if (!empty($filter_value)) {
$pastes = $pastes->where('title', 'LIKE', '%' . escapeLikeQuery($filter_value) . '%');
}
$total_results = $pastes->count();
$pastes = $pastes->limit($per_page)->offset($per_page * $current_page);
$pastes = $pastes->get();
// Temp count for untagged pastes // Temp count for untagged pastes
$total_untagged = Paste::doesntHave('tags')->count(); $total_untagged = Paste::doesntHave('tags')->count();

View file

@ -413,6 +413,8 @@ const setupSignupModal = () => {
const signupButton = $('[data-target~="#signin"],[data-target~="#signup"]'); const signupButton = $('[data-target~="#signin"],[data-target~="#signup"]');
if (signupButton) { if (signupButton) {
signupButton.href = 'javascript:void(0)';
signupButton.addEventListener('click', () => { signupButton.addEventListener('click', () => {
$('.modal').classList.add('is-active'); $('.modal').classList.add('is-active');
}); });
@ -485,6 +487,23 @@ const globalSetup = () => {
preloader.remove(); preloader.remove();
main.id = ''; main.id = '';
} }
const captchaContainer = $('.captcha_container');
if (captchaContainer) {
const refreshElement = captchaContainer.querySelector('a');
const imageElement = captchaContainer.querySelector('img');
if (refreshElement && imageElement) {
refreshElement.addEventListener('click', () => {
imageElement.src = imageElement.src.split('?')[0] + '?rand=' + Math.random();
});
}
}
Array.prototype.forEach.call($('.js-hidden'), (elem) => {
toggleEl(elem);
});
}; };
whenReady(() => { whenReady(() => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -144,6 +144,8 @@ const setupSignupModal = () => {
const signupButton = $('[data-target~="#signin"],[data-target~="#signup"]'); const signupButton = $('[data-target~="#signin"],[data-target~="#signup"]');
if (signupButton) { if (signupButton) {
signupButton.href = 'javascript:void(0)';
signupButton.addEventListener('click', () => { signupButton.addEventListener('click', () => {
$('.modal').classList.add('is-active'); $('.modal').classList.add('is-active');
}); });
@ -216,6 +218,23 @@ const globalSetup = () => {
preloader.remove(); preloader.remove();
main.id = ''; main.id = '';
} }
const captchaContainer = $('.captcha_container');
if (captchaContainer) {
const refreshElement = captchaContainer.querySelector('a');
const imageElement = captchaContainer.querySelector('img');
if (refreshElement && imageElement) {
refreshElement.addEventListener('click', () => {
imageElement.src = imageElement.src.split('?')[0] + '?rand=' + Math.random();
});
}
}
Array.prototype.forEach.call($('.js-hidden'), (elem) => {
toggleEl(elem);
});
}; };
whenReady(globalSetup); whenReady(globalSetup);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -413,6 +413,8 @@ const setupSignupModal = () => {
const signupButton = $('[data-target~="#signin"],[data-target~="#signup"]'); const signupButton = $('[data-target~="#signin"],[data-target~="#signup"]');
if (signupButton) { if (signupButton) {
signupButton.href = 'javascript:void(0)';
signupButton.addEventListener('click', () => { signupButton.addEventListener('click', () => {
$('.modal').classList.add('is-active'); $('.modal').classList.add('is-active');
}); });
@ -485,6 +487,23 @@ const globalSetup = () => {
preloader.remove(); preloader.remove();
main.id = ''; main.id = '';
} }
const captchaContainer = $('.captcha_container');
if (captchaContainer) {
const refreshElement = captchaContainer.querySelector('a');
const imageElement = captchaContainer.querySelector('img');
if (refreshElement && imageElement) {
refreshElement.addEventListener('click', () => {
imageElement.src = imageElement.src.split('?')[0] + '?rand=' + Math.random();
});
}
}
Array.prototype.forEach.call($('.js-hidden'), (elem) => {
toggleEl(elem);
});
}; };
const getUserInfo = () => { const getUserInfo = () => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,17 @@ define('IN_PONEPASTE', 1);
require_once(__DIR__ . '/../includes/common.php'); require_once(__DIR__ . '/../includes/common.php');
require_once(__DIR__ . '/../includes/captcha.php'); require_once(__DIR__ . '/../includes/captcha.php');
if (empty($_GET['t'])) {
die('No token provided.');
}
$captcha_token = 'captcha/' . md5($_GET['t']);
$captcha_code = $redis->get($captcha_token);
if (!$captcha_code) {
die('No token provided.');
}
$captcha_config = captcha($captcha_config['colour'], $captcha_config['multiple'], $captcha_config['allowed']); $captcha_config = captcha($captcha_config['colour'], $captcha_config['multiple'], $captcha_config['allowed']);
// Pick random background, get info, and start captcha // Pick random background, get info, and start captcha
@ -28,7 +39,7 @@ if (!file_exists($font)) {
// Set the font size // Set the font size
$font_size = rand($captcha_config['min_font_size'], $captcha_config['max_font_size']); $font_size = rand($captcha_config['min_font_size'], $captcha_config['max_font_size']);
$text_box_size = imagettfbbox($font_size, $angle, $font, $captcha_config['code']); $text_box_size = imagettfbbox($font_size, $angle, $font, $captcha_code);
// Determine text position // Determine text position
$box_width = (int) abs($text_box_size[6] - $text_box_size[2]); $box_width = (int) abs($text_box_size[6] - $text_box_size[2]);
@ -44,11 +55,11 @@ $text_pos_y = rand($text_pos_y_min, $text_pos_y_max);
if ($captcha_config['shadow']) { if ($captcha_config['shadow']) {
$shadow_color = hex2rgb($captcha_config['shadow_color']); $shadow_color = hex2rgb($captcha_config['shadow_color']);
$shadow_color = imagecolorallocate($captcha, $shadow_color['r'], $shadow_color['g'], $shadow_color['b']); $shadow_color = imagecolorallocate($captcha, $shadow_color['r'], $shadow_color['g'], $shadow_color['b']);
imagettftext($captcha, $font_size, $angle, $text_pos_x + $captcha_config['shadow_offset_x'], $text_pos_y + $captcha_config['shadow_offset_y'], $shadow_color, $font, $captcha_config['code']); imagettftext($captcha, $font_size, $angle, $text_pos_x + $captcha_config['shadow_offset_x'], $text_pos_y + $captcha_config['shadow_offset_y'], $shadow_color, $font, $captcha_code);
} }
// Draw text // Draw text
imagettftext($captcha, $font_size, $angle, $text_pos_x, $text_pos_y, $color, $font, $captcha_config['code']); imagettftext($captcha, $font_size, $angle, $text_pos_x, $text_pos_y, $color, $font, $captcha_code);
// Output image // Output image
header("Content-type: image/png"); header("Content-type: image/png");

View file

@ -14,9 +14,8 @@ function verifyCaptcha() : string|bool {
global $current_user; global $current_user;
if ($captcha_config['enabled'] && !$current_user) { if ($captcha_config['enabled'] && !$current_user) {
$scode = strtolower(trim($_POST['scode'])); if (empty($_POST['captcha_answer']) ||
$cap_code = strtolower($_SESSION['captcha']['code']); !checkCaptcha($_POST['captcha_token'], strtolower(trim($_POST['captcha_answer'])))) {
if ($cap_code !== $scode) {
return 'Wrong CAPTCHA.'; return 'Wrong CAPTCHA.';
} }
} }

View file

@ -202,6 +202,13 @@ button.button--no-style {
cursor: not-allowed; cursor: not-allowed;
} }
.paginator__button[aria-disabled="true"] {
display: inline-block;
pointer-events: none;
cursor: none;
color: #33333388;
}
.table_filterer { .table_filterer {
color: #fff; color: #fff;
background: #3298dc; background: #3298dc;
@ -219,8 +226,9 @@ button.button--no-style {
font-weight: 400; font-weight: 400;
background: #fff; background: #fff;
border: 1px solid #bdc4c9; border: 1px solid #bdc4c9;
width: 80%; width: 75%;
box-shadow: inset 0 1px 0 #f1f0f1; box-shadow: inset 0 1px 0 #f1f0f1;
flex-grow: 4;
} }
.rules h2 { .rules h2 {
@ -233,3 +241,18 @@ button.button--no-style {
margin-right: 0.5rem; margin-right: 0.5rem;
font-size: 1.25rem; font-size: 1.25rem;
} }
.captcha_container {
background-color: #f5f5f5;
border-radius: 4px;
position: relative;
padding: 1.25rem 2.5rem 1.25rem 1.5rem;
}
.captcha_container img {
border-radius: 4px;
}
.captcha_container i {
color: black;
}

View file

@ -65,20 +65,23 @@
<h1 class="title is-5">This pastebin is private.</h1> <h1 class="title is-5">This pastebin is private.</h1>
<?php else: ?> <?php else: ?>
<h1 class="title is-4">Pastes Archive</h1> <h1 class="title is-4">Pastes Archive</h1>
<div class="table_filterer"> <form class="table_filterer" method="GET">
<label><i class="fa fa-search"></i> <label><i class="fa fa-search"></i>
<input class="search" type="search" name="search" placeholder="Filter..."/> <input class="search" type="search" name="q" placeholder="Filter..." value="<?= $filter_value; ?>" />
</label> </label>
Show&nbsp; <label>
<select name="per_page"> Show&nbsp;
<option value="10">10</option> <select name="per_page">
<option value="25">25</option> <option value="10">10</option>
<option value="50">50</option> <option value="25">25</option>
<option value="100">100</option> <option value="50">50</option>
</select> <option value="100">100</option>
&nbsp;per page </select>
</div> &nbsp;per page
<table id="archive" class="table is-fullwidth is-hoverable hidden"> </label>
<button type="submit" class="button js-hidden">Search</button>
</form>
<table id="archive" class="table is-fullwidth is-hoverable">
<thead> <thead>
<tr class="paginator__sort"> <tr class="paginator__sort">
<th data-sort-field="title">Title</th> <th data-sort-field="title">Title</th>
@ -87,7 +90,13 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- Filled by DataTables --> <?php foreach ($pastes as $paste): ?>
<tr>
<td><?= pp_html_escape($paste->title) ?></td>
<td><?= pp_html_escape($paste->user->username) ?></td>
<td><?= tagsToHtml($paste->tags) ?></td>
</tr>
<?php endforeach; ?>
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
@ -97,10 +106,12 @@
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
<div class="loading_container"> <div class="loading_container is-hidden">
</div> </div>
<div class="paginator"></div> <div class="paginator">
<?= paginate($current_page, $per_page, $total_results) ?>
</div>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View file

@ -116,7 +116,7 @@ $flashes = getFlashes();
<span>Events</span> <span>Events</span>
</a> </a>
<?php endif; ?> <?php endif; ?>
<a class="button is-info modal-button" data-target="#signin">Sign In</a> <a class="button is-info modal-button" data-target="#signin" href="/login?login">Sign In / Up</a>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@ -210,12 +210,15 @@ $flashes = getFlashes();
</label> </label>
</div> </div>
<div class="field"> <div class="field">
<div class="notification"> <div class="captcha_container">
<span class="tags are-large"><img src="<?= $_SESSION['captcha']['image_src'] ?>" alt="CAPTCHA" class="imagever" /></span> <img src="/captcha?t=<?= setupCaptcha() ?>" alt="CAPTCHA Image" />
<input type="text" class="input" name="scode" value="" <span id="captcha_refresh" style="height: 100%;">
placeholder="Enter the CAPTCHA"> <a href="javascript:void(0)">
<p class="is-size-6 has-text-grey-light has-text-left mt-2">and press <i class="fa fa-refresh" style="height: 100%;"></i>
"Enter"</p> </a>
</span>
<input type="text" class="input" name="scode" placeholder="Enter the CAPTCHA" />
<p class="is-size-6 has-text-grey-light has-text-left mt-2">and press "Enter"</p>
</div> </div>
</div> </div>
</div> </div>
@ -316,7 +319,7 @@ $flashes = getFlashes();
</script> </script>
<script nonce="D4rkm0d3"> <script nonce="D4rkm0d3">
const toggleSwitch = document.querySelector('.theme-switch input[type="checkbox"]'); const toggleSwitch = document.querySelector('.theme-switch input[type="checkbox"]');
const currentTheme = localStorage.getItem('theme'); const currentTheme = localStorage.getItem('theme') || "<?= @$_COOKIE['theme'] ?>";
if (currentTheme) { if (currentTheme) {
document.documentElement.setAttribute('data-theme', currentTheme); document.documentElement.setAttribute('data-theme', currentTheme);
@ -338,7 +341,5 @@ $flashes = getFlashes();
toggleSwitch.addEventListener('change', switchTheme, false); toggleSwitch.addEventListener('change', switchTheme, false);
</script> </script>
</body> </body>
</html> </html>

View file

@ -161,20 +161,19 @@
</div> </div>
</div> </div>
</form> </form>
<?php } else { ?>
<div class="columns">
<div class="column">
<h1 class="title is-4">Where to?</h1>
<a href="/login">Login</a><br/>
<a href="/register">Register</a> <br/>
<a href="/forgot">Forgot Password</a><br/>
</div>
<div class="column">
</div>
<div class="column">
</div>
</div>
<?php } ?> <?php } ?>
<div class="columns">
<div class="column">
<h1 class="title is-4">Where to?</h1>
<a href="/login">Login</a><br/>
<a href="/register">Register</a> <br/>
<a href="/forgot">Forgot Password</a><br/>
</div>
<div class="column">
</div>
<div class="column">
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -295,12 +295,15 @@
<!-- CAPTCHA --> <!-- CAPTCHA -->
<?php if ($captcha_config['enabled'] && $current_user === null): ?> <?php if ($captcha_config['enabled'] && $current_user === null): ?>
<div class="is-one-quarter"> <div class="is-one-quarter">
<div class="notification"> <div class="captcha_container">
<span class="tags are-large"><img src="<?= $_SESSION['captcha']['image_src'] ?>" alt="CAPTCHA" class="imagever" /></span> <img src="/captcha?t=<?= setupCaptcha() ?>" alt="CAPTCHA Image" />
<input type="text" class="input" name="scode" value="" <span id="captcha_refresh" style="height: 100%;">
placeholder="Enter the CAPTCHA" /> <a href="javascript:void(0)">
<p class="is-size-6 has-text-grey-light has-text-left mt-2">and press <i class="fa fa-refresh" style="height: 100%;"></i>
"Enter"</p> </a>
</span>
<input type="text" class="input" name="scode" placeholder="Enter the CAPTCHA" />
<p class="is-size-6 has-text-grey-light has-text-left mt-2">and press "Enter"</p>
</div> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>