From 4e9fed755c8c3777f2c07cb12c6b2f40acebf0b1 Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Thu, 9 Jul 2026 00:03:01 +0200 Subject: [PATCH] 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. --- src/batch_qrcode.php | 225 ++++++++++++++++++++++++ src/dist/js/custom.js | 22 ++- src/forms/form_static_add.php | 14 +- src/forms/qrcode_options.php | 8 + src/forms/static/applink.php | 73 ++++++++ src/forms/static/bluetooth.php | 37 ++++ src/forms/table_dynamic.php | 3 + src/forms/table_static.php | 3 + src/includes/sidebar.php | 6 + src/lib/DynamicQrcode/DynamicQrcode.php | 43 ++++- src/lib/Qrcode/Qrcode-intchil.php | 102 +++++++++-- src/lib/Qrcode/Qrcode.php | 114 +++++++++++- src/lib/StaticQrcode/StaticQrcode.php | 63 +++++++ src/static_qrcode.php | 6 + 14 files changed, 698 insertions(+), 21 deletions(-) create mode 100644 src/batch_qrcode.php create mode 100644 src/forms/static/applink.php create mode 100644 src/forms/static/bluetooth.php diff --git a/src/batch_qrcode.php b/src/batch_qrcode.php new file mode 100644 index 0000000..4201b7d --- /dev/null +++ b/src/batch_qrcode.php @@ -0,0 +1,225 @@ + $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, + ]; +} +?> + + + Qrcode Generator + + + + +
+ + + + + + + + + +
+ +
+
+
+
+

Batch-create dynamic qr codes

+
+
+
+
+ + + + + + + +
+
+ + +
+
+

Result

+
+
+

qr code(s) created, + row(s) failed.

+ + + + Download all as ZIP + + + + + + + + + + + + + + + + + + + + + +
LineFilenameError
+ +
+
+ + +
+
+

Upload a CSV file

+
+
+ +
+

The CSV needs two columns: filename,link. 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.

+ +
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+
+
+ + + + + diff --git a/src/dist/js/custom.js b/src/dist/js/custom.js index e0bffbd..bb1eab4 100644 --- a/src/dist/js/custom.js +++ b/src/dist/js/custom.js @@ -499,4 +499,24 @@ return false; }); -})(jQuery) \ No newline at end of file +})(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); + } + }); + }); +}); diff --git a/src/forms/form_static_add.php b/src/forms/form_static_add.php index c9e3705..1d9bf90 100644 --- a/src/forms/form_static_add.php +++ b/src/forms/form_static_add.php @@ -44,6 +44,12 @@ + +
@@ -88,7 +94,13 @@
- + +
+ +
+
diff --git a/src/forms/qrcode_options.php b/src/forms/qrcode_options.php index 219d86c..9f8da2d 100644 --- a/src/forms/qrcode_options.php +++ b/src/forms/qrcode_options.php @@ -88,6 +88,14 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") { + +
+
+ + + Optional label rendered below the code. Only applies to PNG/JPEG/GIF, not SVG/EPS. +
+
diff --git a/src/forms/static/applink.php b/src/forms/static/applink.php new file mode 100644 index 0000000..c939515 --- /dev/null +++ b/src/forms/static/applink.php @@ -0,0 +1,73 @@ +
+ + + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + + + +
+
+ + + +
+
+
+ +
+
+
+ +
diff --git a/src/forms/static/bluetooth.php b/src/forms/static/bluetooth.php new file mode 100644 index 0000000..a2368a7 --- /dev/null +++ b/src/forms/static/bluetooth.php @@ -0,0 +1,37 @@ +
+ + + +
+ + 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. + +
+ +
+
+ + +
+
+ +
+
+ + +
+
+
+
+ +
+
+
+ +
+
+
+ +
diff --git a/src/forms/table_dynamic.php b/src/forms/table_dynamic.php index 7ccb5d0..5acb57d 100644 --- a/src/forms/table_dynamic.php +++ b/src/forms/table_dynamic.php @@ -89,6 +89,9 @@ + + + diff --git a/src/forms/table_static.php b/src/forms/table_static.php index d551bd3..60d3879 100644 --- a/src/forms/table_static.php +++ b/src/forms/table_static.php @@ -85,6 +85,9 @@ + + + diff --git a/src/includes/sidebar.php b/src/includes/sidebar.php index 45acde4..2eec17d 100644 --- a/src/includes/sidebar.php +++ b/src/includes/sidebar.php @@ -56,6 +56,12 @@

Add new

+ diff --git a/src/lib/DynamicQrcode/DynamicQrcode.php b/src/lib/DynamicQrcode/DynamicQrcode.php index f2c61a8..ad3dccc 100644 --- a/src/lib/DynamicQrcode/DynamicQrcode.php +++ b/src/lib/DynamicQrcode/DynamicQrcode.php @@ -75,10 +75,49 @@ 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 - * + * */ public function editQrcode($input_data) { $this->validateLink($input_data['link'] ?? ''); diff --git a/src/lib/Qrcode/Qrcode-intchil.php b/src/lib/Qrcode/Qrcode-intchil.php index 6dd3b69..630e9f2 100644 --- a/src/lib/Qrcode/Qrcode-intchil.php +++ b/src/lib/Qrcode/Qrcode-intchil.php @@ -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,25 +394,25 @@ 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 customiaddLogo($data_to_db['qrcode'], $options['optionlogo']); - + $db = getDbInstance(); $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!'); - - if ($last_id){ - audit_log('qrcode_created', $this->table, $last_id); - $this->success('Qr code added successfully!'); - } - else { - $this->failure('Insert failed: ' . $db->getLastError()); + throw new \RuntimeException('You cannot create a new qr code with an existing name on the server!'); + + 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"]); - $data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']); + 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"]){ diff --git a/src/lib/Qrcode/Qrcode.php b/src/lib/Qrcode/Qrcode.php index 1bbd71c..d9f2f46 100644 --- a/src/lib/Qrcode/Qrcode.php +++ b/src/lib/Qrcode/Qrcode.php @@ -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(); @@ -125,14 +165,16 @@ class Qrcode { $content = file_get_contents($url); $filename = SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$data_to_db['format']; - + try{ file_put_contents($filename, $content); } catch(Exception $e){ $this->failure($e->getMessage()); } - + + $this->addFrameText($filename, $data_to_db['format'], $input_data['frame_text'] ?? ''); + // If you want you can customiaddLogo($data_to_db['qrcode'], $options['optionlogo']); @@ -151,9 +193,75 @@ 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. + '&&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 - * + * */ public function editQrcode($input_data, $data_to_db) { $db = getDbInstance(); diff --git a/src/lib/StaticQrcode/StaticQrcode.php b/src/lib/StaticQrcode/StaticQrcode.php index 2323750..c14c042 100644 --- a/src/lib/StaticQrcode/StaticQrcode.php +++ b/src/lib/StaticQrcode/StaticQrcode.php @@ -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 = 'Platform: ' . ($is_android ? 'Android (intent)' : 'Generic') . '
'; + $this->sContent .= 'Scheme: ' . $scheme . '
'; + $this->sContent .= 'Path: ' . $path; + + if ($is_android) { + $this->sContent .= '
Package: ' . $package; + } + + if (!empty($fallback_url)) { + $this->sContent .= '
Fallback URL: ' . $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 = 'Device name: ' . $device_name . '
' . 'MAC address: ' . $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); diff --git a/src/static_qrcode.php b/src/static_qrcode.php index 9568329..d738dd1 100644 --- a/src/static_qrcode.php +++ b/src/static_qrcode.php @@ -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; } } ?>