Fase 3 v1: new qr types, svg export, clipboard, frame text, batch CSV

New static qr types:
- App Link: Android intent:// links (with package + optional browser
  fallback) or a generic custom-scheme URI. iOS Universal Links need no
  special encoding (they're just plain https:// URLs).
- Bluetooth: device name + MAC address. Purely informational, since unlike
  WIFI:/vCard there's no OS-native "scan to pair" convention.

SVG export: already worked (format whitelist/dropdown existed since Fase 1),
verified rather than reimplemented.

Copy-to-clipboard button next to the download button on both qr list tables,
using the Clipboard API against a fetched blob.

Optional frame text label rendered below the qr code via GD after
generation (raster formats only, no-op for svg/eps).

Batch CSV upload (batch_qrcode.php): filename,link rows create dynamic qr
codes with sane defaults, downloadable as a zip. Required refactoring
Qrcode-intchil.php's generation path (previously always redirected/exited
via failure()/success(), which can't run in a loop) into a private
renderAndStore() that throws instead, shared by addQrcode() and the new
addQrcodeBatch(). Qrcode.php's addQrcodeBatch() is a separate, deliberately
duplicated implementation instead, since its generation logic is small
enough that duplication carries less risk than refactoring the working
external-API code path.
This commit is contained in:
2026-07-09 00:03:01 +02:00
parent 6387d24846
commit 4e9fed755c
14 changed files with 698 additions and 21 deletions
+225
View File
@@ -0,0 +1,225 @@
<?php
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php';
if ($_SESSION['type'] === 'user') {
$_SESSION['failure'] = 'The "user" role is read-only and cannot create qr codes.';
header('Location: index.php');
exit;
}
$dynamic_qrcode_instance = new DynamicQrcode();
$results = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
$id_owner = $_SESSION['type'] === 'super' ? ($_POST['id_owner'] ?? '') : $_SESSION['user_id'];
if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
$_SESSION['failure'] = 'Please choose a CSV file to upload.';
header('Location: batch_qrcode.php');
exit;
}
$handle = fopen($_FILES['csv_file']['tmp_name'], 'r');
$rows = [];
if ($handle !== false) {
while (($row = fgetcsv($handle)) !== false) {
$rows[] = $row;
}
fclose($handle);
}
// Skip an optional header row.
if (!empty($rows) && strtolower(trim($rows[0][0] ?? '')) === 'filename') {
array_shift($rows);
}
$successes = [];
$failures = [];
$created_ids = [];
foreach ($rows as $index => $row) {
$line_number = $index + 1;
$filename = $row[0] ?? '';
$link = $row[1] ?? '';
$result = $dynamic_qrcode_instance->addQrcodeBatchRow($filename, $link, $id_owner);
if ($result['ok']) {
$successes[] = $filename;
$created_ids[] = $result['id'];
} else {
$failures[] = ['line' => $line_number, 'filename' => $filename, 'error' => $result['error']];
}
}
$zip_filename = null;
if (!empty($created_ids)) {
$db = getDbInstance();
$files = [];
foreach ($created_ids as $id) {
$db->where('id', $id);
$row = $db->getOne('dynamic_qrcodes');
if ($row !== null) {
$files[] = SAVED_QRCODE_DIRECTORY . $row['qrcode'];
}
}
$zip_filename = 'qrcodes_' . uniqid() . '.zip';
$zip_path = SAVED_QRCODE_DIRECTORY . 'zip/' . $zip_filename;
@unlink($zip_path);
$zip = new ZipArchive();
$zip->open($zip_path, ZipArchive::CREATE);
foreach ($files as $file) {
$content = @file_get_contents($file);
if ($content !== false) {
$zip->addFromString(basename($file), $content);
}
}
$zip->close();
$_SESSION['generated_zips'][] = $zip_filename;
audit_log('batch_qrcode_created', 'dynamic_qrcodes', implode(',', $created_ids));
}
$results = [
'successes' => $successes,
'failures' => $failures,
'zip_filename' => $zip_filename,
];
}
?>
<!DOCTYPE html>
<html lang="en">
<title>Qrcode Generator</title>
<head>
<?php include './includes/head.php'; ?>
</head>
<body class="hold-transition sidebar-mini layout-fixed layout-navbar-fixed layout-footer-fixed">
<div class="wrapper">
<!-- Navbar -->
<?php include './includes/navbar.php'; ?>
<!-- /.navbar -->
<!-- Main Sidebar Container -->
<?php include './includes/sidebar.php'; ?>
<!-- /.Main Sidebar Container -->
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0 text-dark">Batch-create dynamic qr codes</h1>
</div><!-- /.col -->
</div><!-- /.row -->
</div><!-- /.container-fluid -->
</div>
<!-- /.content-header -->
<!-- Flash messages -->
<?php include BASE_PATH.'/includes/flash_messages.php'; ?>
<!-- /.Flash messages -->
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<?php if ($results !== null): ?>
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Result</h3>
</div>
<div class="card-body">
<p><strong><?php echo count($results['successes']); ?></strong> qr code(s) created,
<strong><?php echo count($results['failures']); ?></strong> row(s) failed.</p>
<?php if ($results['zip_filename']): ?>
<a href="qrcode_zip_download.php?file=<?php echo rawurlencode($results['zip_filename']); ?>" class="btn btn-primary">
<i class="fa fa-download"></i> Download all as ZIP
</a>
<?php endif; ?>
<?php if (!empty($results['failures'])): ?>
<table class="table table-striped table-bordered mt-3">
<thead>
<tr>
<th>Line</th>
<th>Filename</th>
<th>Error</th>
</tr>
</thead>
<tbody>
<?php foreach ($results['failures'] as $failure): ?>
<tr>
<td><?php echo (int) $failure['line']; ?></td>
<td><?php echo htmlspecialchars($failure['filename']); ?></td>
<td><?php echo htmlspecialchars($failure['error']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Upload a CSV file</h3>
</div>
<form action="" method="post" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="card-body">
<p>The CSV needs two columns: <code>filename,link</code>. An optional header row
starting with "filename" is skipped automatically. Each row creates one dynamic
qr code (PNG, default colors/size) redirecting to the given link.</p>
<div class="form-group">
<label for="csv_file">CSV file</label>
<input type="file" name="csv_file" id="csv_file" accept=".csv,text/csv" required="required" class="form-control">
</div>
<?php if ($_SESSION['type'] === 'super'): ?>
<div class="form-group">
<label for="id_owner">Owner</label>
<select name="id_owner" class="form-control">
<option value="" selected>All</option>
<?php
require_once BASE_PATH . '/lib/Users/Users.php';
$users_instance = new Users();
$users = $users_instance->getAllUsers();
foreach ($users as $user) {
?>
<option value="<?php echo $user["id"]; ?>"><?php echo htmlspecialchars($user["username"]); ?></option>
<?php
}
?>
</select>
</div>
<?php endif; ?>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">Upload and generate</button>
</div>
</form>
</div>
</div><!--/. container-fluid -->
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
<!-- Footer and scripts -->
<?php include './includes/footer.php'; ?>
</body>
</html>
+20
View File
@@ -500,3 +500,23 @@
return false;
});
})(jQuery)
// Copy a qr code image straight to the clipboard (Fase 3 UX feature).
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.copy-qr-btn').forEach(function (btn) {
btn.addEventListener('click', async function () {
const icon = btn.querySelector('i');
const originalClass = icon.className;
try {
const response = await fetch(btn.getAttribute('data-qr-src'));
const blob = await response.blob();
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
icon.className = 'fa fa-check';
setTimeout(function () { icon.className = originalClass; }, 1500);
} catch (err) {
alert('Could not copy this image to the clipboard (your browser may not support this image format for clipboard access): ' + err.message);
}
});
});
});
+12
View File
@@ -44,6 +44,12 @@
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#twofa" role="tab" aria-controls="custom-tabs-four-settings" aria-selected="false">2FA <i class="fa fa-key"></i></a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#applink" role="tab" aria-controls="custom-tabs-four-settings" aria-selected="false">App Link <i class="fas fa-mobile-alt"></i></a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#bluetooth" role="tab" aria-controls="custom-tabs-four-settings" aria-selected="false">Bluetooth <i class="fab fa-bluetooth-b"></i></a>
</li>
</ul>
</div>
<div class="card-body">
@@ -90,6 +96,12 @@
<div class="tab-pane fade" id="twofa" role="tabpanel" aria-labelledby="custom-tabs-four-profile-tab">
<?php include BASE_PATH . '/forms/static/2fa.php'; ?>
</div>
<div class="tab-pane fade" id="applink" role="tabpanel" aria-labelledby="custom-tabs-four-profile-tab">
<?php include BASE_PATH . '/forms/static/applink.php'; ?>
</div>
<div class="tab-pane fade" id="bluetooth" role="tabpanel" aria-labelledby="custom-tabs-four-profile-tab">
<?php include BASE_PATH . '/forms/static/bluetooth.php'; ?>
</div>
</div>
</div>
</div><!-- /.card -->
+8
View File
@@ -88,6 +88,14 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
<option value="eps">EPS</option>
</select>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="frame_text">Frame text</label>
<input type="text" name="frame_text" value="" placeholder="e.g. Scan me" maxlength="60" class="form-control" id="frame_text">
<small class="form-text text-muted">Optional label rendered below the code. Only applies to PNG/JPEG/GIF, not SVG/EPS.</small>
</div>
</div>
</div>
</div>
+73
View File
@@ -0,0 +1,73 @@
<form class="form" action="static_qrcode.php?type=applink" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<div class="col-sm-12 mb-2">
<div class="row">
<div class="col-6 col-md-3">
<div class="form-group">
<label>Platform *</label>
<select name="platform" id="applink-platform" class="form-control">
<option value="android" selected>Android (intent link)</option>
<option value="generic">Generic (custom scheme)</option>
</select>
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-group">
<label>Scheme *</label>
<input type="text" name="scheme" value="" placeholder="myapp" class="form-control">
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-group">
<label>Path *</label>
<input type="text" name="path" value="" placeholder="open?ref=123" class="form-control">
</div>
</div>
<div class="col-6 col-md-3" id="applink-package-group">
<div class="form-group">
<label>Android package *</label>
<input type="text" name="package" value="" placeholder="com.example.app" class="form-control">
</div>
</div>
<div class="col-6 col-md-3" id="applink-fallback-group">
<div class="form-group">
<label>Fallback URL</label>
<input type="text" name="fallback_url" value="" placeholder="https://play.google.com/store/apps/details?id=..." class="form-control">
</div>
</div>
</div>
</div>
<script>
(function () {
var platformSelect = document.getElementById('applink-platform');
var packageGroup = document.getElementById('applink-package-group');
var fallbackGroup = document.getElementById('applink-fallback-group');
function updateVisibility() {
var isAndroid = platformSelect.value === 'android';
packageGroup.style.display = isAndroid ? '' : 'none';
fallbackGroup.style.display = isAndroid ? '' : 'none';
}
platformSelect.addEventListener('change', updateVisibility);
updateVisibility();
})();
</script>
<div class="col-sm-12 mb-2">
<div class="row">
<div class="col-6 col-md-3">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</div>
</form>
+37
View File
@@ -0,0 +1,37 @@
<form class="form" action="static_qrcode.php?type=bluetooth" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<div class="col-sm-12 mb-2">
<small class="form-text text-muted mb-2">
There is no OS-native "scan to pair" standard for Bluetooth like there is for Wifi, so this
just encodes the device name and address for reference - whoever scans it still pairs
manually via their Bluetooth settings.
</small>
<div class="row">
<div class="col-6 col-md-3">
<div class="form-group">
<label>Device name *</label>
<input type="text" name="device_name" value="" placeholder="" class="form-control">
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-group">
<label>MAC address *</label>
<input type="text" name="mac_address" value="" placeholder="AA:BB:CC:DD:EE:FF" class="form-control">
</div>
</div>
</div>
</div>
<div class="col-sm-12 mb-2">
<div class="row">
<div class="col-6 col-md-3">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</div>
</form>
+3
View File
@@ -89,6 +89,9 @@
<?php endif; ?>
<!-- DOWNLOAD -->
<a href="qrcode_image.php?type=dynamic&id=<?php echo $row['id']; ?>&download=1" class="btn btn-primary"><i class="fa fa-download"></i></a>
<!-- COPY TO CLIPBOARD -->
<button type="button" class="btn btn-secondary copy-qr-btn" data-qr-src="qrcode_image.php?type=dynamic&id=<?php echo $row['id']; ?>" title="Copy image to clipboard"><i class="fa fa-copy"></i></button>
</td>
</tr>
<?php endforeach; ?>
+3
View File
@@ -85,6 +85,9 @@
<?php endif; ?>
<!-- DOWNLOAD -->
<a href="qrcode_image.php?type=static&id=<?php echo $row['id']; ?>&download=1" class="btn btn-primary"><i class="fa fa-download"></i></a>
<!-- COPY TO CLIPBOARD -->
<button type="button" class="btn btn-secondary copy-qr-btn" data-qr-src="qrcode_image.php?type=static&id=<?php echo $row['id']; ?>" title="Copy image to clipboard"><i class="fa fa-copy"></i></button>
</td>
</tr>
<?php endforeach; ?>
+6
View File
@@ -56,6 +56,12 @@
<p>Add new</p>
</a>
</li>
<li class="nav-item">
<a href="./batch_qrcode.php" <?php echo (CURRENT_PAGE == 'batch_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="far fa-circle nav-icon"></i>
<p>Batch create (CSV)</p>
</a>
</li>
<?php endif; ?>
</ul>
</li>
+39
View File
@@ -76,6 +76,45 @@ class DynamicQrcode {
$this->qrcode_instance->addQrcode($input_data, $data_to_db, $data_to_qrcode);
}
/**
* Batch-safe variant used by batch_qrcode.php: creates one dynamic qr code from a
* CSV row (filename + link) with sane defaults, returning a result array
* (['ok' => bool, 'id'|'error' => ...]) instead of redirecting/exiting.
*/
public function addQrcodeBatchRow($filename, $link, $id_owner) {
$filename = trim((string) $filename);
$link = trim((string) $link);
if ($filename === '') {
return ['ok' => false, 'error' => 'Filename is required.'];
}
if ($link === '' || strlen($link) > 500) {
return ['ok' => false, 'error' => 'Link is required and must be at most 500 characters.'];
}
$data_to_db['id_owner'] = $id_owner !== '' ? $id_owner : NULL;
$data_to_db['filename'] = htmlspecialchars($filename, ENT_QUOTES, 'UTF-8');
$data_to_db['created_at'] = date('Y-m-d H:i:s');
$data_to_db['link'] = htmlspecialchars($link, ENT_QUOTES, 'UTF-8');
$data_to_db['created_by'] = $_SESSION['user_id'];
$data_to_db['format'] = 'png';
$data_to_db['identifier'] = randomString(rand(5, 8));
$data_to_db['qrcode'] = $data_to_db['filename'].'.'.$data_to_db['format'];
$data_to_qrcode = READ_PATH.$data_to_db['identifier'];
$input_data = [
'level' => 'L',
'size' => 200,
'foreground' => '#000000',
'background' => '#ffffff',
'frame_text' => '',
];
return $this->qrcode_instance->addQrcodeBatch($input_data, $data_to_db, $data_to_qrcode);
}
/**
* Edit qr code
*
+85 -11
View File
@@ -52,11 +52,11 @@ class Qrcode {
$filename = trim((string) $filename);
if ($filename === '' || strlen($filename) > 45) {
$this->failure('Filename must be between 1 and 45 characters.');
throw new \InvalidArgumentException('Filename must be between 1 and 45 characters.');
}
if (preg_match('#[\\/\\\\]#', $filename) || strpos($filename, '..') !== false || strpos($filename, "\0") !== false) {
$this->failure('Filename cannot contain path separators.');
throw new \InvalidArgumentException('Filename cannot contain path separators.');
}
return $filename;
@@ -66,12 +66,52 @@ class Qrcode {
$format = strtolower((string) $format);
if (!in_array($format, self::ALLOWED_FORMATS, true)) {
$this->failure('Invalid qr code format.');
throw new \InvalidArgumentException('Invalid qr code format.');
}
return $format;
}
/**
* Renders an optional text label below the qr code. Only supported for raster
* formats (png/jpg/jpeg/gif) via GD; a no-op for svg/svgbw/eps.
*/
private function addFrameText($path, $format, $text) {
$text = trim((string) $text);
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
if ($text === '' || !isset($loaders[$format]) || !is_file($path)) {
return;
}
$source = @$loaders[$format]($path);
if ($source === false) {
return;
}
$width = imagesx($source);
$height = imagesy($source);
$padding = 30;
$canvas = imagecreatetruecolor($width, $height + $padding);
$white = imagecolorallocate($canvas, 255, 255, 255);
$black = imagecolorallocate($canvas, 0, 0, 0);
imagefill($canvas, 0, 0, $white);
imagecopy($canvas, $source, 0, 0, 0, 0, $width, $height);
$font = 5;
$text_width = imagefontwidth($font) * strlen($text);
$x = max(0, (int) (($width - $text_width) / 2));
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
imagestring($canvas, $font, $x, $y, $text, $black);
$savers[$format]($canvas, $path);
imagedestroy($source);
imagedestroy($canvas);
}
public function getQrcode($id) {
$db = getDbInstance();
@@ -121,6 +161,36 @@ class Qrcode {
* We save into db the url of qrcode image
*/
public function addQrcode($input_data, $data_to_db, $data_to_qrcode) {
try {
$last_id = $this->renderAndStore($input_data, $data_to_db, $data_to_qrcode);
} catch (\Throwable $e) {
$this->failure($e->getMessage());
}
audit_log('qrcode_created', $this->table, $last_id);
$this->success('Qr code added successfully!');
}
/**
* Batch-safe variant of addQrcode(): generates and stores the qr code but returns a
* result array (['ok' => bool, 'id'|'error' => ...]) instead of redirecting/exiting,
* so batch_qrcode.php can create many codes in one request.
*/
public function addQrcodeBatch($input_data, $data_to_db, $data_to_qrcode) {
try {
$last_id = $this->renderAndStore($input_data, $data_to_db, $data_to_qrcode);
audit_log('qrcode_created', $this->table, $last_id);
return ['ok' => true, 'id' => $last_id];
} catch (\Throwable $e) {
return ['ok' => false, 'error' => $e->getMessage()];
}
}
/**
* Core qr code rendering + storage, shared by addQrcode() and addQrcodeBatch().
* Throws instead of calling failure() so batch processing can catch and continue.
*/
private function renderAndStore($input_data, $data_to_db, $data_to_qrcode) {
$options = $this->setOptions($input_data);
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
@@ -324,9 +394,11 @@ class Qrcode {
}
catch(Exception $e)
{
$this->failure($e->getMessage());
throw new \RuntimeException($e->getMessage());
}
$this->addFrameText($filename, $fileExt, $input_data['frame_text'] ?? '');
// If you want you can customi<e qr code with logo
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
@@ -334,15 +406,13 @@ class Qrcode {
$last_id = $db->insert($this->table, $data_to_db);
}
else
$this->failure('You cannot create a new qr code with an existing name on the server!');
throw new \RuntimeException('You cannot create a new qr code with an existing name on the server!');
if ($last_id){
audit_log('qrcode_created', $this->table, $last_id);
$this->success('Qr code added successfully!');
}
else {
$this->failure('Insert failed: ' . $db->getLastError());
if (!$last_id) {
throw new \RuntimeException('Insert failed: ' . $db->getLastError());
}
return $last_id;
}
/**
@@ -353,7 +423,11 @@ class Qrcode {
$db = getDbInstance();
$old_qrcode = $this->getQrcode($input_data["id"]);
try {
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
} catch (\InvalidArgumentException $e) {
$this->failure($e->getMessage());
}
$data_to_db['qrcode'] = $data_to_db['filename'].'.'.$old_qrcode["format"];
if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$old_qrcode["format"]) || $data_to_db['filename'] == $input_data["old_filename"]){
+108
View File
@@ -57,6 +57,46 @@ class Qrcode {
return $format;
}
/**
* Renders an optional text label below the qr code. Only supported for raster
* formats (png/jpg/jpeg/gif) via GD; a no-op for svg/svgbw/eps.
*/
private function addFrameText($path, $format, $text) {
$text = trim((string) $text);
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
if ($text === '' || !isset($loaders[$format]) || !is_file($path)) {
return;
}
$source = @$loaders[$format]($path);
if ($source === false) {
return;
}
$width = imagesx($source);
$height = imagesy($source);
$padding = 30;
$canvas = imagecreatetruecolor($width, $height + $padding);
$white = imagecolorallocate($canvas, 255, 255, 255);
$black = imagecolorallocate($canvas, 0, 0, 0);
imagefill($canvas, 0, 0, $white);
imagecopy($canvas, $source, 0, 0, 0, 0, $width, $height);
$font = 5;
$text_width = imagefontwidth($font) * strlen($text);
$x = max(0, (int) (($width - $text_width) / 2));
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
imagestring($canvas, $font, $x, $y, $text, $black);
$savers[$format]($canvas, $path);
imagedestroy($source);
imagedestroy($canvas);
}
public function getQrcode($id) {
$db = getDbInstance();
@@ -133,6 +173,8 @@ class Qrcode {
$this->failure($e->getMessage());
}
$this->addFrameText($filename, $data_to_db['format'], $input_data['frame_text'] ?? '');
// If you want you can customi<e qr code with logo
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
@@ -151,6 +193,72 @@ class Qrcode {
}
}
/**
* Batch-safe variant of addQrcode(): generates and stores the qr code but returns a
* result array (['ok' => bool, 'id'|'error' => ...]) instead of redirecting/exiting,
* so batch_qrcode.php can create many codes in one request. Deliberately does not
* reuse addQrcode()/sanitizeFilename()/validateFormat(), since those call failure()
* (redirect + exit) which would abort the whole batch after the first bad row.
*/
public function addQrcodeBatch($input_data, $data_to_db, $data_to_qrcode) {
$filename = trim((string) $data_to_db['filename']);
$format = strtolower((string) $data_to_db['format']);
if ($filename === '' || strlen($filename) > 45) {
return ['ok' => false, 'error' => 'Filename must be between 1 and 45 characters.'];
}
if (preg_match('#[\\/\\\\]#', $filename) || strpos($filename, '..') !== false || strpos($filename, "\0") !== false) {
return ['ok' => false, 'error' => 'Filename cannot contain path separators.'];
}
if (!in_array($format, self::ALLOWED_FORMATS, true)) {
return ['ok' => false, 'error' => 'Invalid qr code format.'];
}
$data_to_db['filename'] = $filename;
$data_to_db['format'] = $format;
$path = SAVED_QRCODE_DIRECTORY.$filename.'.'.$format;
if (file_exists($path)) {
return ['ok' => false, 'error' => 'A qr code with this filename already exists.'];
}
$options = $this->setOptions($input_data);
$url =
'https://api.qrserver.com/v1/create-qr-code/?data='.
$data_to_qrcode.
'&amp;&size='.$options['size'].'x'.$options['size'].
'&ecc='.$options['errorCorrectionLevel'].
'&margin=0&color='.$options['foreground'].
'&bgcolor='.$options['background'].
'&qzone=2'.
'&format='.$format;
$content = @file_get_contents($url);
if ($content === false) {
return ['ok' => false, 'error' => 'Could not generate the qr code image.'];
}
if (@file_put_contents($path, $content) === false) {
return ['ok' => false, 'error' => 'Could not write the qr code file.'];
}
$this->addFrameText($path, $format, $input_data['frame_text'] ?? '');
$db = getDbInstance();
$last_id = $db->insert($this->table, $data_to_db);
if (!$last_id) {
return ['ok' => false, 'error' => 'Insert failed: ' . $db->getLastError()];
}
audit_log('qrcode_created', $this->table, $last_id);
return ['ok' => true, 'id' => $last_id];
}
/**
* Edit qr code
*
+63
View File
@@ -378,6 +378,68 @@ class StaticQrcode {
$this->requiredFieldsError();
}
/**
* create a qr code of type "applink" (mobile app deep link)
* @string platform -> required, "android" (intent:// link with optional fallback) or "generic" (plain custom-scheme URI)
* @string scheme -> required, e.g. "myapp"
* @string path -> required, e.g. "open?ref=123" (without the scheme prefix)
* @string package -> required when platform is "android" (Android package name, e.g. com.example.app)
* @string fallback_url -> optional, Play Store/web fallback used by the Android intent link
*/
public function applinkQrcode($platform, $scheme, $path, $package, $fallback_url)
{
$is_android = $platform === 'android';
if ($scheme != NULL && $path != NULL && (!$is_android || $package != NULL)) {
if ($is_android) {
$this->sData = 'intent://' . $path . '#Intent;scheme=' . $scheme . ';package=' . $package;
if (!empty($fallback_url)) {
$this->sData .= ';S.browser_fallback_url=' . rawurlencode($fallback_url);
}
$this->sData .= ';end';
} else {
$this->sData = $scheme . '://' . $path;
}
$this->sContent = '<strong>Platform:</strong> ' . ($is_android ? 'Android (intent)' : 'Generic') . '<br>';
$this->sContent .= '<strong>Scheme:</strong> ' . $scheme . '<br>';
$this->sContent .= '<strong>Path:</strong> ' . $path;
if ($is_android) {
$this->sContent .= '<br><strong>Package:</strong> ' . $package;
}
if (!empty($fallback_url)) {
$this->sContent .= '<br><strong>Fallback URL:</strong> ' . $fallback_url;
}
$this->addQrcode("applink");
} else {
$this->requiredFieldsError();
}
}
/**
* create a qr code of type "bluetooth" (device pairing info)
* @string device_name -> required
* @string mac_address -> required
*
* Note: unlike WIFI:/vCard there is no OS-native "scan to pair" convention for
* Bluetooth, so this is purely informational - whoever scans it still has to pair
* the device manually via their Bluetooth settings using the name/address shown.
*/
public function bluetoothQrcode($device_name, $mac_address)
{
if ($device_name != NULL && $mac_address != NULL) {
$this->sData = 'BT:N:' . $device_name . ';M:' . $mac_address . ';';
$this->sContent = '<strong>Device name:</strong> ' . $device_name . '<br>' . '<strong>MAC address:</strong> ' . $mac_address;
$this->addQrcode("bluetooth");
} else {
$this->requiredFieldsError();
}
}
public function getQrcode($id) {
return $this->qrcode_instance->getQrcode($id);
}
@@ -410,6 +472,7 @@ class StaticQrcode {
$input_data["foreground"] = $_POST['foreground'];
$input_data["background"] = $_POST['background'];
$input_data["frame_text"] = $_POST['frame_text'] ?? '';
$data_to_qrcode = urlencode($this->sData);
+6
View File
@@ -77,6 +77,12 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
case '2fa': $static_qrcode_instance->twofaQrcode($_POST['algorithms'], $_POST['secret'], rawurlencode($_POST['label']), rawurlencode($_POST['issuer']));
break;
case 'applink': $static_qrcode_instance->applinkQrcode($_POST['platform'], $_POST['scheme'], $_POST['path'], $_POST['package'] ?? '', $_POST['fallback_url'] ?? '');
break;
case 'bluetooth': $static_qrcode_instance->bluetoothQrcode($_POST['device_name'], $_POST['mac_address']);
break;
}
}
?>