2022-11-23 12:50:12 +01:00
|
|
|
<?php declare(strict_types=1);
|
2022-01-15 19:40:44 +01:00
|
|
|
/**
|
|
|
|
* Hybula Looking Glass
|
|
|
|
*
|
|
|
|
* Provides UI and input for the looking glass backend.
|
|
|
|
*
|
|
|
|
* @copyright 2022 Hybula B.V.
|
|
|
|
* @license Mozilla Public License 2.0
|
|
|
|
* @version 0.1
|
|
|
|
* @since File available since release 0.1
|
|
|
|
* @link https://github.com/hybula/lookingglass
|
|
|
|
*/
|
|
|
|
|
2022-11-23 12:50:12 +01:00
|
|
|
require __DIR__.'/bootstrap.php';
|
2022-01-15 19:40:44 +01:00
|
|
|
|
|
|
|
use Hybula\LookingGlass;
|
|
|
|
|
|
|
|
|
2022-11-23 12:50:50 +01:00
|
|
|
$errorMessage = null;
|
2022-01-15 19:40:44 +01:00
|
|
|
if (!empty($_POST)) {
|
2022-11-23 12:50:50 +01:00
|
|
|
if (!isset($_POST['csrfToken']) || !isset($_SESSION[LookingGlass::SESSION_CSRF]) || ($_POST['csrfToken'] !== $_SESSION[LookingGlass::SESSION_CSRF])) {
|
|
|
|
exitErrorMessage('Missing or incorrect CSRF token.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($_POST['submitForm']) || !isset($_POST['backendMethod']) || !isset($_POST['targetHost'])) {
|
|
|
|
exitErrorMessage('Unsupported POST received.');
|
|
|
|
}
|
2022-01-15 19:40:44 +01:00
|
|
|
|
2022-11-23 12:50:50 +01:00
|
|
|
if (!in_array($_POST['backendMethod'], LG_METHODS)) {
|
|
|
|
exitErrorMessage('Unsupported backend method.');
|
|
|
|
}
|
2022-11-23 11:50:57 +01:00
|
|
|
|
2022-11-23 12:50:50 +01:00
|
|
|
$_SESSION[LookingGlass::SESSION_TARGET_METHOD] = $_POST['backendMethod'];
|
|
|
|
$_SESSION[LookingGlass::SESSION_TARGET_HOST] = $_POST['targetHost'];
|
|
|
|
if (!isset($_POST['checkTerms']) && LG_TERMS) {
|
|
|
|
exitErrorMessage('You must agree with the Terms of Service.');
|
|
|
|
}
|
|
|
|
|
|
|
|
$targetHost = $_POST['targetHost'];
|
|
|
|
if (in_array($_POST['backendMethod'], ['ping', 'mtr', 'traceroute'])) {
|
|
|
|
if (!LookingGlass::isValidIpv4($_POST['targetHost']) &&
|
|
|
|
!$targetHost = LookingGlass::isValidHost($_POST['targetHost'], LookingGlass::IPV4)
|
|
|
|
) {
|
|
|
|
exitErrorMessage('No valid IPv4 provided.');
|
|
|
|
}
|
|
|
|
}
|
2022-11-23 11:50:57 +01:00
|
|
|
|
2022-11-23 12:50:50 +01:00
|
|
|
if (in_array($_POST['backendMethod'], ['ping6', 'mtr6', 'traceroute6'])) {
|
2022-12-27 00:24:31 +01:00
|
|
|
if (!LookingGlass::isValidIpv6($_POST['targetHost']) &&
|
2022-11-23 12:50:50 +01:00
|
|
|
!$targetHost = LookingGlass::isValidHost($_POST['targetHost'],LookingGlass::IPV6)
|
|
|
|
) {
|
|
|
|
exitErrorMessage('No valid IPv6 provided.');
|
2022-01-15 19:40:44 +01:00
|
|
|
}
|
2022-11-23 12:50:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$_SESSION[LookingGlass::SESSION_TARGET_HOST] = $targetHost;
|
|
|
|
$_SESSION[LookingGlass::SESSION_TOS_CHECKED] = true;
|
|
|
|
$_SESSION[LookingGlass::SESSION_CALL_BACKEND] = true;
|
|
|
|
exitNormal();
|
|
|
|
}
|
|
|
|
|
|
|
|
$templateData['session_target'] = $_SESSION[LookingGlass::SESSION_TARGET_HOST] ?? '';
|
|
|
|
$templateData['session_method'] = $_SESSION[LookingGlass::SESSION_TARGET_METHOD] ?? '';
|
|
|
|
$templateData['session_call_backend'] = $_SESSION[LookingGlass::SESSION_CALL_BACKEND] ?? false;
|
|
|
|
$templateData['session_tos_checked'] = isset($_SESSION[LookingGlass::SESSION_TOS_CHECKED]) ? ' checked' : '';
|
|
|
|
|
|
|
|
if (isset($_SESSION[LookingGlass::SESSION_ERROR_MESSAGE])) {
|
|
|
|
$templateData['error_message'] = $_SESSION[LookingGlass::SESSION_ERROR_MESSAGE];
|
|
|
|
unset($_SESSION[LookingGlass::SESSION_ERROR_MESSAGE]);
|
2022-01-15 19:40:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (LG_BLOCK_CUSTOM) {
|
|
|
|
include LG_CUSTOM_PHP;
|
2022-11-23 12:50:12 +01:00
|
|
|
|
|
|
|
ob_start();
|
|
|
|
include LG_CUSTOM_HTML;
|
|
|
|
$templateData['custom_html'] = ob_get_clean();
|
2022-01-15 19:40:44 +01:00
|
|
|
}
|
2022-11-23 12:50:12 +01:00
|
|
|
|
|
|
|
$templateData['csrfToken'] = $_SESSION[LookingGlass::SESSION_CSRF] = bin2hex(random_bytes(12));
|
2022-01-15 19:40:44 +01:00
|
|
|
?>
|
|
|
|
<!doctype html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
|
|
|
<meta content="" name="description">
|
|
|
|
<meta content="Hybula" name="author">
|
2022-11-23 12:50:12 +01:00
|
|
|
<title><?php echo $templateData['title'] ?></title>
|
2022-11-23 21:49:05 +01:00
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php if ($templateData['custom_css']) { echo '<link href="'.$templateData['custom_css'].'" rel="stylesheet">'; } ?>
|
2022-11-23 22:05:42 +01:00
|
|
|
<?php if ($templateData['custom_head']) { echo $templateData['custom_head']; } ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
|
|
|
|
<div class="col-lg-6 mx-auto p-3 py-md-5">
|
|
|
|
|
|
|
|
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
|
|
|
|
<div class="col-8">
|
2022-11-23 12:50:12 +01:00
|
|
|
<a class="d-flex align-items-center text-dark text-decoration-none" href="<?php echo $templateData['logo_url'] ?>" target="_blank">
|
|
|
|
<?php echo $templateData['logo_data'] ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div class="col-4 float-end">
|
|
|
|
<select class="form-select" onchange="window.location = this.options[this.selectedIndex].value">
|
2022-11-23 12:50:12 +01:00
|
|
|
<option selected><?php echo $templateData['current_location'] ?></option>
|
|
|
|
<?php foreach ($templateData['locations'] as $location => $link): ?>
|
|
|
|
<option value="<?php echo $link ?>"><?php echo $location ?></option>
|
|
|
|
<?php endforeach ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
</header>
|
|
|
|
|
|
|
|
<main>
|
|
|
|
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php if (LG_BLOCK_NETWORK): ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
<div class="row mb-5">
|
|
|
|
<div class="card shadow-lg">
|
|
|
|
<div class="card-body p-3">
|
|
|
|
<h1 class="fs-4 card-title mb-4">Network</h1>
|
|
|
|
|
|
|
|
<div class="row mb-3">
|
|
|
|
<div class="col-md-7">
|
|
|
|
<label class="mb-2 text-muted">Location</label>
|
|
|
|
<div class="input-group mb-3">
|
2022-11-23 12:50:12 +01:00
|
|
|
<input type="text" class="form-control" value="<?php echo $templateData['current_location'] ?>" onfocus="this.select()" readonly="">
|
|
|
|
<a class="btn btn-outline-secondary" href="https://www.openstreetmap.org/search?query=<?php echo urlencode($templateData['maps_query']); ?>" target="_blank">Map</a>
|
|
|
|
<?php if (!empty($templateData['locations'])): ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Locations</button>
|
|
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php foreach ($templateData['locations'] as $location => $link): ?>
|
|
|
|
<li><a class="dropdown-item" href="<?php echo $link ?>"><?php echo $location ?></a></li>
|
|
|
|
<?php endforeach ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
</ul>
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php endif ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="col-md-5">
|
|
|
|
<label class="mb-2 text-muted">Facility</label>
|
|
|
|
<div class="input-group">
|
2022-11-23 12:50:12 +01:00
|
|
|
<input type="text" class="form-control" value="<?php echo $templateData['facility'] ?>" onfocus="this.select()" readonly="">
|
|
|
|
<a href="<?php echo $templateData['facility_url'] ?>" class="btn btn-outline-secondary" target="_blank">PeeringDB</a>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="row mb-3">
|
|
|
|
<div class="col-md-3">
|
|
|
|
<label class="mb-2 text-muted">Test IPv4</label>
|
|
|
|
<div class="input-group">
|
2022-11-23 12:50:12 +01:00
|
|
|
<input type="text" class="form-control" value="<?php echo $templateData['ipv4'] ?>" onfocus="this.select()" readonly="">
|
|
|
|
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['ipv4'] ?>', this)">Copy</button>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="col-md-5">
|
|
|
|
<label class="mb-2 text-muted">Test IPv6</label>
|
|
|
|
<div class="input-group">
|
2022-11-23 12:50:12 +01:00
|
|
|
<input type="text" class="form-control" value="<?php echo $templateData['ipv6'] ?>" onfocus="this.select()" readonly="">
|
|
|
|
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['ipv6'] ?>', this)">Copy</button>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
|
|
<label class="mb-2 text-muted">Your IP</label>
|
|
|
|
<div class="input-group">
|
2022-11-23 12:50:12 +01:00
|
|
|
<input type="text" class="form-control" value="<?php echo $templateData['user_ip'] ?>" onfocus="this.select()" readonly="">
|
|
|
|
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['user_ip'] ?>', this)">Copy</button>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php endif ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php if (LG_BLOCK_LOOKINGGLAS): ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
<div class="row pb-5">
|
|
|
|
<div class="card shadow-lg">
|
|
|
|
<div class="card-body p-3">
|
|
|
|
<h1 class="fs-4 card-title mb-4">Looking Glass</h1>
|
|
|
|
<form method="POST" action="/" autocomplete="off">
|
2022-11-23 12:50:12 +01:00
|
|
|
<input type="hidden" name="csrfToken" value="<?php echo $templateData['csrfToken'] ?>">
|
2022-01-15 19:40:44 +01:00
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
<div class="col-md-7 mb-3">
|
|
|
|
<div class="input-group">
|
|
|
|
<span class="input-group-text" id="basic-addon1">Target</span>
|
2022-11-23 12:50:12 +01:00
|
|
|
<input type="text" class="form-control" placeholder="IP address or host..." name="targetHost" value="<?php echo $templateData['session_target'] ?>" required="">
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="col-md-5 mb-3">
|
|
|
|
<div class="input-group">
|
|
|
|
<label class="input-group-text">Method</label>
|
|
|
|
<select class="form-select" name="backendMethod" id="backendMethod">
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php foreach ($templateData['methods'] as $method): ?>
|
|
|
|
<option value="<?php echo $method ?>"<?php if($templateData['session_method'] === $method): ?> selected<?php endif ?>><?php echo $method ?></option>
|
|
|
|
<?php endforeach ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="d-flex align-items-center">
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php if ($templateData['tos']): ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
<div class="form-check">
|
2022-11-23 12:50:12 +01:00
|
|
|
<input type="checkbox" id="checkTerms" name="checkTerms" class="form-check-input"<?php echo $templateData['session_tos_checked'] ?>>
|
|
|
|
<label for="checkTerms" class="form-check-label">I agree with the <a href="<?php echo $templateData['tos'] ?>" target="_blank">Terms of Use</a></label>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php endif ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
<button type="submit" class="btn btn-primary ms-auto" id="executeButton" name="submitForm">
|
|
|
|
Execute
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php if ($templateData['error_message']): ?>
|
2022-11-23 13:07:49 +01:00
|
|
|
<div class="alert alert-danger mt-3" role="alert"><?php echo $templateData['error_message'] ?></div>
|
|
|
|
<?php endif ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
|
|
|
|
<div class="card card-body bg-light mt-4" style="display: none;" id="outputCard">
|
2023-03-01 10:36:51 +01:00
|
|
|
<pre id="outputContent" style="white-space: pre;word-wrap: normal;margin-bottom: 0;padding-bottom: 1rem;"></pre>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php endif ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php if (LG_BLOCK_SPEEDTEST): ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
<div class="row pb-5">
|
|
|
|
<div class="card shadow-lg">
|
|
|
|
<div class="card-body p-3">
|
|
|
|
<h1 class="fs-4 card-title mb-4">Speedtest</h1>
|
|
|
|
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php if ($templateData['speedtest_iperf']): ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
<div class="row mb-3">
|
|
|
|
<div class="col-md-6">
|
2022-11-23 12:50:12 +01:00
|
|
|
<label class="mb-2 text-muted"><?php echo $templateData['speedtest_incoming_label'] ?></label>
|
|
|
|
<p><code><?php echo $templateData['speedtest_incoming_cmd']; ?></code></p>
|
|
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['speedtest_incoming_cmd'] ?>', this)">Copy</button>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
2022-11-23 12:50:12 +01:00
|
|
|
<label class="mb-2 text-muted"><?php echo $templateData['speedtest_outgoing_label'] ?></label>
|
|
|
|
<p><code><?php echo $templateData['speedtest_outgoing_cmd'] ?></code></p>
|
|
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['speedtest_outgoing_cmd'] ?>', this)">Copy</button>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php endif ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
<label class="mb-2 text-muted">Test Files</label>
|
|
|
|
<div class="btn-group input-group mb-3">
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php foreach ($templateData['speedtest_files'] as $file => $link): ?>
|
|
|
|
<a href="<?php echo $link ?>" class="btn btn-outline-secondary"><?php echo $file ?></a>
|
|
|
|
<?php endforeach ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php endif ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
|
2022-11-23 12:50:12 +01:00
|
|
|
<?php echo $templateData['custom_html'] ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
|
|
|
|
</main>
|
|
|
|
<footer class="pt-3 mt-5 my-5 text-muted border-top">
|
|
|
|
Powered by <a href="https://github.com/hybula/lookingglass" target="_blank">Hybula Looking Glass</a>
|
2022-04-19 14:14:16 +02:00
|
|
|
<a href="https://github.com/hybula/lookingglass" target="_blank" class="float-end"><img src="https://img.shields.io/github/stars/hybula/lookingglass?style=social" alt="GitHub"></a>
|
2022-01-15 19:40:44 +01:00
|
|
|
</footer>
|
|
|
|
</div>
|
|
|
|
|
2022-11-23 13:00:07 +01:00
|
|
|
<?php if ($templateData['session_call_backend']): ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
<script type="text/javascript">
|
2022-11-23 13:05:48 +01:00
|
|
|
(function () {
|
|
|
|
const outputContent = document.getElementById('outputContent')
|
|
|
|
const executeButton = document.getElementById('executeButton')
|
|
|
|
const outputCard = document.getElementById('outputCard')
|
2022-11-23 12:04:10 +01:00
|
|
|
|
2022-11-23 13:05:48 +01:00
|
|
|
executeButton.innerText = 'Executing...'
|
|
|
|
executeButton.disabled = true
|
2022-11-23 12:04:10 +01:00
|
|
|
|
2022-11-23 13:05:48 +01:00
|
|
|
outputCard.style.display = 'inherit'
|
2022-11-23 12:04:10 +01:00
|
|
|
|
2022-11-23 13:05:48 +01:00
|
|
|
fetch('/backend.php')
|
|
|
|
.then(async (response) => {
|
|
|
|
// response.body is a ReadableStream
|
|
|
|
const reader = response.body.getReader()
|
|
|
|
const decoder = new TextDecoder()
|
2022-11-23 12:04:10 +01:00
|
|
|
|
2022-11-23 13:05:48 +01:00
|
|
|
for await (const chunk of readChunks(reader)) {
|
|
|
|
const text = decoder.decode(chunk)
|
2022-11-23 12:04:10 +01:00
|
|
|
<?php if(in_array($_SESSION[LookingGlass::SESSION_TARGET_METHOD], [LookingGlass::METHOD_MTR, LookingGlass::METHOD_MTR6])): ?>
|
|
|
|
let splittedText = text.split('---')
|
2022-11-23 13:05:48 +01:00
|
|
|
if (!splittedText[1]) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
outputContent.innerHTML = splittedText[1].trim()
|
2022-11-23 12:04:10 +01:00
|
|
|
<?php else: ?>
|
2022-11-23 13:05:48 +01:00
|
|
|
outputContent.innerHTML = outputContent.innerHTML + text.trim().replace(/<br \/> +/g, '<br />')
|
2022-11-23 12:04:10 +01:00
|
|
|
<?php endif ?>
|
2022-11-23 13:05:48 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
executeButton.innerText = 'Execute'
|
|
|
|
executeButton.disabled = false
|
|
|
|
console.log('Backend ready!')
|
|
|
|
})
|
|
|
|
})()
|
|
|
|
|
|
|
|
// readChunks() reads from the provided reader and yields the results into an async iterable
|
|
|
|
function readChunks(reader) {
|
|
|
|
return {
|
|
|
|
async* [Symbol.asyncIterator]() {
|
|
|
|
let readResult = await reader.read()
|
|
|
|
while (!readResult.done) {
|
|
|
|
yield readResult.value
|
|
|
|
readResult = await reader.read()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2022-01-15 19:40:44 +01:00
|
|
|
</script>
|
2022-11-23 12:04:10 +01:00
|
|
|
<?php endif ?>
|
2022-01-15 19:40:44 +01:00
|
|
|
|
|
|
|
<script type="text/javascript">
|
|
|
|
async function copyToClipboard(text, button) {
|
2022-11-23 13:05:48 +01:00
|
|
|
if (!navigator || !navigator.clipboard || !navigator.clipboard.writeText) {
|
|
|
|
return Promise.reject('The Clipboard API is not available.')
|
2022-11-23 11:51:58 +01:00
|
|
|
}
|
|
|
|
|
2022-11-23 13:05:48 +01:00
|
|
|
button.innerHTML = 'Copied!'
|
|
|
|
await navigator.clipboard.writeText(text)
|
|
|
|
await new Promise(r => setTimeout(r, 2000))
|
|
|
|
button.innerHTML = 'Copy'
|
2022-01-15 19:40:44 +01:00
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
2022-11-23 21:49:05 +01:00
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
|
2022-01-15 19:40:44 +01:00
|
|
|
|
|
|
|
</body>
|
|
|
|
</html>
|