171 Commits

Author SHA1 Message Date
dillard 9cdfbe7a10 Fase 3 feedback round: style preview, icon upload, font choice, optional bitcoin amount, WPA3
- Default the Owner select to the creator's own account instead of "All", so
  superadmin-created codes no longer silently become visible to every admin
  (the underlying NULL-fallback sharing behavior for an explicit "All" choice
  is unchanged).
- Add a live color/precision/size preview swatch next to the preset picker.
- Let the frame text use a chosen DejaVu font + font size instead of a fixed
  GD bitmap font.
- Add an optional self-uploaded icon rendered above the qr code (not embedded
  in it, so scanability is unaffected).
- Make the Bitcoin qr amount optional; a standing wallet address is useful
  without forcing a one-off amount per code.
- Add a WPA3 option to the WiFi qr encryption select.
- Fix a real bug surfaced while testing the preview/style JS: qrcode_options.php
  was included once per static qr type (16 times on one page) and each
  inclusion re-executed <script src="qrcode-style-tools.js">, so every button
  click fired once per type - e.g. saving one preset wrote 16 duplicate rows,
  and every tab except the first ("Text") had dead random-style/preset
  buttons since only the first DOM match ever got a listener. Moved the
  script include to load once per page and rewrote the JS to scope every
  lookup to the triggering element's own tab-pane/form instead of relying on
  getElementById's first-match behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196bLhZhuxwK3MkHuKLe5WB
2026-07-09 23:11:36 +02:00
dillard be9164850a Fase 3 priority 2: presets, random style, qr scanner, PWA
Preset system: qr_presets table (migration 005) plus a presets.php AJAX
endpoint (list/save/delete, CSRF-protected, scoped to the logged-in user's
own id - presets are personal, never shared across accounts). UI/JS lives in
dist/js/qrcode-style-tools.js.

Random style button: client-side only, fills foreground/background with a
random hex color pair (playful randomize, no contrast/scannability
guarantee).

Qr scanner (scan_qrcode.php): camera or image upload, decoded entirely
client-side via html5-qrcode (CDN, pinned to 2.3.8).

PWA: manifest.json + service-worker.js, icons generated from the existing
DynamicQRCode_Original.png glyph. The service worker only caches static
assets (css/js/images) and deliberately never touches PHP pages, since those
carry CSRF tokens and session-specific content that must never be cached.

Fixes a gap found while testing: qrcode_options.php is only a shared partial
for the static qr forms - the dynamic qr form (form_dynamic_add.php) has its
own separate copy of the foreground/background/level/size/filename/format
fields (pre-existing structure, not something introduced here). That meant
frame_text and the new preset/random-style UI never showed up on the
dynamic qr page. Added the same fields there too, verified with a dynamic qr
plus frame text (150x180px, the expected +30px padding).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 00:49:45 +02:00
dillard f7d2de355e 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.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 00:03:01 +02:00
dillard b5ef4ac0cb Admin-scoped user accounts, secure qr code storage, PHP 8.4 upgrade
Admin-scoped users (answers: who can create a 'user' account, only super or
also an admin within their own scope?):
- New owner_admin_id column on users (migration 004). NULL means created by
  super (company-wide, previous behavior); otherwise scoped to that admin's
  own codes.
- Users::addUser/editUser/deleteUser now allow an 'admin' session, but force
  type='user' and owner_admin_id to their own id regardless of submitted
  input. user.php/users.php open up to admins with a restricted UI (no type
  picker, listing limited to their own created users).
- New qr_compute_scope_owner_id()/qr_apply_owner_scope()/qr_has_full_visibility()
  helpers in includes/security.php, replacing the ad-hoc type==='admin' checks
  in index.php, dynamic_qrcodes.php, static_qrcodes.php and bulk_action.php.
  A 'user' account created by an admin is now scoped to that admin's codes
  instead of seeing everything company-wide.

Qr code storage hardening: images were served as plain static files under the
document root with no auth check at all. Storage now lives outside the web
root; qrcode_image.php and qrcode_zip_download.php gate access with the same
permission model as the list pages, and the bulk zip download is bound to the
session that generated it.

PHP 8.4 + chillerlan/php-qrcode 6.0.1: bumped since this is a dockerized app,
so the PHP version shipped doesn't matter to end users. Note: the 6.0.1 tag
itself only requires PHP 8.2 - the earlier "needs 8.4" read was from an
unpinned clone of master, which has since moved past the tag. Fixed along the
way, surfaced by testing on 8.4:
- The hardcoded Imagick build (an old pinned master commit, workaround for
  3.7.0 being broken on PHP 8.3+) no longer compiles on 8.4. Imagick 3.8.1 is
  now a normal stable release, so the workaround is gone.
- config.php had display_errors=On + error_reporting(E_ALL), so PHP 8.4's new
  deprecation notices got dumped straight into the response before
  session_start() could run, breaking login outright. Also an info-disclosure
  risk on its own. Now logged instead of displayed.
- MysqliDb::insertMulti() had an implicit nullable parameter, now explicit.
- includes/auth_validate.php redirected unauthenticated requests but never
  called exit(), so the rest of the script kept running.
- Dockerfile.fpm was missing both git (needed to clone chillerlan/php-qrcode)
  and the imagick extension entirely.

Also removes the unused sample qr code images that shipped in the original
repo; storage now lives outside the document root so they were never going
to be served again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 20:38:58 +02:00
dillard a1ab16d54c Fase 2: read-only user role with per-category view toggles
Adds a third account type 'user' alongside super/admin: no create/edit/delete
rights on qr codes, view access to dynamic/static lists gated per-account by
two admin-controlled toggles (can_view_static, can_view_dynamic), and always
full visibility into the dashboard/reports regardless of those toggles.

- New columns can_view_static/can_view_dynamic on users (migrations/003)
- Users class + form_users.php: 'user' type option with the two toggles
- Access control: dynamic_qrcode.php/static_qrcode.php/bulk_action.php reject
  all mutations for type=user; dynamic_qrcodes.php/static_qrcodes.php enforce
  the view toggle and show all codes (no owner scoping, since 'user' owns none)
- Sidebar and list tables hide add/edit/delete/bulk UI for the read-only role
- index.php dashboard stats are unscoped for both 'super' and 'user'

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:37:25 +02:00
dillard feb5380b28 Security hardening: CSRF, rate limiting, session/password policy, audit log
Fixes critical pre-existing issues found during review: bulk_action.php had no
auth check at all (unauthenticated download/delete of any qrcode) and built a
table name from unwhitelisted user input (SQL injection); the QR generator
classes wrote files from unvalidated filename/format, allowing path traversal
and arbitrary file writes. Also pins chillerlan/php-qrcode to 5.0.5 since
master now requires PHP 8.4, breaking the PHP 8.3 build.

- CSRF tokens on all POST forms and the bulk_action.php JSON endpoint
- Login rate limiting (5 attempts / 15 min) via new login_attempts table
- Hardened sessions: httponly/samesite cookies, 30 min idle timeout,
  session regeneration on login
- Forced password change for the default superadmin/superadmin account
- Server-side validation in Users/DynamicQrcode/Qrcode classes
- Audit log table for auth, user, and qrcode actions
- Checked-in db schema (db/init.sql, migrations/) instead of relying on an
  opaque prebuilt db image
- Production docker-compose with Nginx + php-fpm instead of the PHP dev server

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 15:00:33 +02:00
Giandonato Inverso 268a6a3f65 Remove unnecessary extra information from documentation 2025-09-02 21:56:03 +02:00
Giandonato Inverso 812db2bf9b Delete support section from index.html 2025-09-02 21:53:41 +02:00
Giandonato Inverso 48d7fefd02 Remove donation button from README 2025-09-02 21:52:03 +02:00
Giandonato Inverso 27a5dbd466 Merge pull request #144 from CLAlberto/master
fix(read.php): replace deprecated FILTER_SANITIZE_STRING for PHP 8.3 …
2025-05-08 08:56:07 +02:00
CLAlberto 2c3f2bb030 fix(read.php): replace deprecated FILTER_SANITIZE_STRING for PHP 8.3 compatibility
### Problem

The usage of `FILTER_SANITIZE_STRING` in `read.php` causes a deprecation warning in PHP 8.1 and breaks functionality entirely in PHP 8.3, as the constant was removed.

### Solution

This commit replaces:
```php
filter_input(INPUT_GET, 'id', FILTER_SANITIZE_STRING);



with a safer and future-proof alternative:

$id = filter_input(INPUT_GET, 'id', FILTER_UNSAFE_RAW);
$id = trim(strip_tags($id));


> _Thanks for maintaining this project! Happy to contribute._ 😊
2025-05-06 16:16:22 +02:00
Giandonato Inverso e4d3af69b5 Merge pull request #143 from angelosleebos/patch-1
Make environment variables compatible for other platforms
2025-04-14 12:19:34 +02:00
Giandonato Inverso 938d934b51 Merge pull request #142 from rafinou62/patch-1
Update read.php prevent SQL Injection & XSS attacks
2025-04-14 12:18:32 +02:00
Angelo Sleebos cee6f4d3d5 Make environment variables compatible for other platforms
Make environment variables compatible for other platforms
2025-04-07 01:11:17 +02:00
Raphaël Wanecque 1465ce02ad Update read.php prevent SQL Injection & XSS attacks 2025-03-26 17:04:33 +01:00
giandonato.inverso@edempg.it ba57334142 edit demo url 2025-03-22 15:51:02 +01:00
giandonato.inverso@edempg.it 615d983828 config rollback 2025-03-20 23:38:55 +01:00
giandonato.inverso@edempg.it b245694824 Merge remote-tracking branch 'origin/master' 2025-03-19 23:40:50 +01:00
giandonato.inverso@edempg.it 378968576f bug fix Class Qrcode not found 2025-03-19 23:40:36 +01:00
Giandonato Inverso 0d4368575b Merge pull request #136 from MickGe/patch-1
add cookies secure flags
2025-02-09 16:16:53 +01:00
MickGe 05e80a075a add cookies secure flags 2025-02-07 11:35:24 +01:00
giandonato.inverso@edempg.it 92a1f17dbe updated readme and docs 2025-01-21 22:41:14 +01:00
giandonato.inverso@edempg.it d1505e1e08 updated readme 2025-01-12 23:50:15 +01:00
giandonato.inverso@edempg.it f05c073ad4 Local setup eliminated and documentation updated 2025-01-12 23:48:49 +01:00
giandonato.inverso@edempg.it 79d9ac71a3 Merge remote-tracking branch 'origin/master' 2025-01-12 19:48:31 +01:00
Giandonato Inverso ffc5b64967 Merge pull request #130 from Shineson1001/feature/129-QRCodeGeneratorSwitch
🐛 Global switch for the QR code generator (#129)
2025-01-02 12:04:55 +01:00
Shine 869dd2c799 🐛 Global switch for the QR code generator (#129) 2024-12-31 20:48:56 +01:00
giandonato.inverso@edempg.it 531820e2a8 Revert "Revert "Instead using external API (api.qrserver.com) .. use chillerlan/php-qrcode""
This reverts commit 630bbf3aea.
2024-12-23 18:52:11 +01:00
giandonato.inverso@edempg.it 630bbf3aea Revert "Instead using external API (api.qrserver.com) .. use chillerlan/php-qrcode"
This reverts commit 9b65bb8020.
2024-12-23 18:49:05 +01:00
Giandonato Inverso 8238a81494 Merge pull request #128 from Shineson1001/feature/116-EventIncorrectTimeZone
🐛 Event: Incorrect Time Zone
2024-12-20 20:02:32 +01:00
Shine cc45f0659c 🐛 Event: Incorrect Time Zone
- Add "Time zone" input field.
- 24-Hour time format.
- Set Min-Year and Max-Year dynamically
2024-12-19 22:34:42 +01:00
Giandonato Inverso ad37224890 Merge pull request #127 from Shineson1001/feature/88-SelfHostedQRCodeGenerator
Instead using external API (api.qrserver.com) .. use chillerlan/php-qrcode
2024-12-15 16:35:29 +01:00
Shine 9b65bb8020 Instead using external API (api.qrserver.com) .. use chillerlan/php-qrcode 2024-12-15 14:36:39 +01:00
Giandonato Inverso 97481c2444 Merge pull request #126 from Shineson1001/feature/2FA-QRCodes
Add 2FA QR Code
2024-12-14 09:17:50 +01:00
Shine 8151de4b9a Add 2FA QR Code
Save your 2FA secrets to QR-Code.
2024-12-13 23:42:57 +01:00
Giandonato Inverso 477d7803f0 available plugins 2024-10-20 16:32:58 +02:00
Giandonato Inverso 8cc5294a84 Update README.md 2024-10-20 11:07:12 +02:00
giandonato.inverso@edempg.it e6aac416d9 bug fix in helpers.php 2024-04-21 12:55:03 +02:00
giandonato.inverso@edempg.it e84f853d80 edit readme 2024-04-21 12:38:35 +02:00
giandonato.inverso@edempg.it 7d5a4b889c bug fix 2024-04-18 20:54:04 +02:00
giandonato.inverso@edempg.it c157815ca8 fix in bulk action 2024-04-18 20:44:22 +02:00
giandonato.inverso@edempg.it 2d9b6162e1 readme 2024-04-18 20:35:04 +02:00
giandonato.inverso@edempg.it 096c239715 bug fix in read.php, bump version in footer, NEW: bulk delete 2024-04-18 20:33:20 +02:00
giandonato.inverso@edempg.it 5bdcb8e2bf bug fix database prefix in Qrcode class 2024-03-11 15:43:16 +01:00
giandonato.inverso@edempg.it 98ae82a040 bug fix bulk download 2024-03-04 14:36:18 +01:00
Giandonato Inverso 15b5ece4f9 Merge pull request #94 from tranmh/xss_static_qrcode
Fix Security: Stored Cross Site Scripting for static QR code
2024-03-01 09:58:29 +01:00
Minh Cuong Tran b03238b4c3 Fix Security: Stored Cross Site Scripting for static QR code, see https://github.com/giandonatoinverso/PHP-Dynamic-Qr-code/issues/93 2024-03-01 08:03:31 +01:00
Giandonato Inverso 2287d98455 Merge pull request #92 from tranmh/fix_remove_DATABASE_PREFIX_for_getOne
Inconsistency of using DATABASE_PREFIX with getOne()
2024-02-29 00:25:17 +01:00
Minh Cuong Tran 3e43b72eec Inconsistency of using DATABASE_PREFIX with getOne() 2024-02-28 16:56:39 +01:00
Giandonato Inverso 5488ad0a84 Merge pull request #91 from tranmh/mixed_content_blocked
fix: mixed content blocked for http and https
2024-02-28 14:16:21 +01:00
Minh Cuong Tran 9710ae0673 fix mixed content blocked for http and https: Mixed Content: The page at 'https://localhost/qrcode/dynamic_qrcodes.php' was loaded over HTTPS, but requested an insecure stylesheet 'http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css'. This request has been blocked; the content must be served over HTTPS. 2024-02-28 13:55:44 +01:00
Giandonato Inverso 893b883dbf Merge pull request #90 from tranmh/fix_case_sensitive_filename
Fix case sensitive filename
2024-02-27 13:25:17 +01:00
Minh Cuong Tran 60620e15dd fix: case sensitive for filename 2024-02-27 13:18:52 +01:00
Minh Cuong Tran ec786d2956 fix: case sensitive for filename 2024-02-27 13:18:01 +01:00
Giandonato Inverso a4d8455e5a increased size of column link - dynamic qrcode
increased size of column link - dynamic qrcode
2024-02-14 23:58:00 +01:00
Giandonato Inverso 5ac338945d Update README.md
updated php version requirement
2024-01-03 00:43:50 +01:00
Giandonato Inverso 6e213483a4 Update README.md 2023-10-30 17:02:13 +01:00
Giandonato Inverso 8a926ac0f3 bug fix redirect url with docker installation 2023-10-17 23:24:34 +02:00
Giandonato Inverso 0360176490 bug fix 2023-10-17 00:41:15 +02:00
Giandonato Inverso 57930cf9db bug fix and documentation 2023-10-16 22:57:16 +02:00
Giandonato Inverso 6682a207b2 updated documentation 2023-10-16 22:24:15 +02:00
Giandonato Inverso c326a30afc updated documentation 2023-10-16 20:56:06 +02:00
Giandonato Inverso 28545c2245 Refactoring docker image building, NEW: added docker compose support
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 19:49:29 +02:00
Giandonato Inverso 1085a13d38 readme updated
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 13:15:53 +02:00
giandonato.inverso@edempg.it 13a807ac85 readme updated
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 13:15:07 +02:00
Giandonato Inverso 876b6736b6 refactoring of table database, added script for upgrading to versions >= 2.0, added multi-user support
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 13:10:57 +02:00
Giandonato Inverso e36c26a37a readme updated
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 01:43:56 +02:00
Giandonato Inverso a2ce4b38b6 eliminazione file superflui, spostamento file read.php all'interno del progetto, aggiunta astrazione classe Qrcode, miglioramento download bulk, refactoring generale
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 01:41:27 +02:00
Giandonato Inverso 12e358b87c Bug fix login.php
removed .min extension adminlte js file
2023-09-16 14:51:04 +02:00
Giandonato Inverso 617d08c13d paypal donations 2023-09-05 23:31:38 +02:00
Giandonato Inverso 5612558ad1 Update README.md 2023-09-05 23:29:51 +02:00
giandonato.inverso@edempg.it e827b55f6b Merge remote-tracking branch 'origin/master' 2023-08-22 17:15:09 +02:00
giandonato.inverso@edempg.it 3ea78d6a84 doc update
Signed-off-by: giandonato.inverso@edempg.it <Zannabianca20>
2023-08-22 17:14:52 +02:00
Giandonato Inverso e947153e74 Update Dockerfile 2023-01-25 18:51:10 +01:00
Giandonato Inverso 4e3cfeeaa3 Merge pull request #52 from AyhamAl-Ali/fix/db_prefix
🚀 Fix DB Prefix in `read.php`
2023-01-25 18:50:15 +01:00
Ayham Al-Ali f750d7aaca Fix DB Prefix 2023-01-25 20:47:52 +03:00
Giandonato Inverso c6cb83638a Merge pull request #45 from chilluniverse/patch-1
DATABASE_HOST
2022-10-14 10:36:59 +02:00
Pascal 7e70ca94d8 DATABASE_HOST
In the docker-compose.yml is the Database_Host defined as "mariadb". If Host is not changed in the environment.php to "mariadb" as well the setup will fail
2022-10-14 01:26:06 +02:00
giandonato.inverso@edempg.it 7d7c326795 new version dockerfile 2022-09-28 16:23:44 +02:00
giandonato.inverso@edempg.it 16327a0e0a new version dockerfile 2022-09-28 16:22:18 +02:00
Giandonato Inverso 53955af19c Merge pull request #37 from 0xRenegade/feature/download-multiple-qr-img-at-once
Error Message for no qrcodes selected.
2022-09-24 22:43:16 +02:00
0xRenegade 4aab7d637a Error Message for no qrcodes selected. 2022-09-24 15:40:56 -05:00
Giandonato Inverso a6b2b29352 Merge pull request #36 from 0xRenegade/feature/download-multiple-qr-img-at-once
Feature/download multiple qr img at once
2022-09-24 22:26:36 +02:00
0xRenegade dac89ba0b2 download multiple qrcodes at once feature 2022-09-24 15:14:20 -05:00
0xRenegade f064d8f1ef add base_url() function in helpers, works with https and port number 2022-09-24 14:32:04 -05:00
0xRenegade cccf3ada43 adds in custom.css for, well, custom styles. 2022-09-24 13:09:17 -05:00
Giandonato Inverso b5aec38e45 Merge pull request #35 from 0xRenegade/issue-30/update-docker-yml-for-db-prefix
Issue 30/update docker yml for db prefix
2022-09-24 19:44:31 +02:00
Giandonato Inverso 654d395369 Merge pull request #33 from 0xRenegade/QOL/ignore-env-and-use-example-instead
QOL/ignore env and use example instead
2022-09-24 19:44:23 +02:00
0xRenegade f410322119 update docker-compose.yml for database prefix option 2022-09-24 12:19:40 -05:00
0xRenegade cbc5246163 update docs for this change 2022-09-24 11:52:34 -05:00
0xRenegade d99e8132c9 update main gitignore to add in environment.php 2022-09-24 11:44:02 -05:00
0xRenegade 99a361c0be move env to example, so we don't run into merge conflicts constantly 2022-09-24 11:42:17 -05:00
0xRenegade 64974c6c51 Merge pull request #1 from giandonatoinverso/master
sync master branch with remote
2022-09-24 11:28:55 -05:00
Giandonato Inverso 52e2347644 Merge pull request #31 from 0xRenegade/renegade/general-fixes
general fixes, fixed database prefix during install
2022-09-24 11:53:34 +02:00
0xRenegade 304b808bb6 set static attribute of class rather than non-DRY code 2022-09-24 00:24:03 -05:00
0xRenegade 9f376bd3c9 remove error_log debugging 2022-09-24 00:05:50 -05:00
0xRenegade fca443b6cc updated MysqliDb class functions to handle prefix properly. 2022-09-23 23:44:01 -05:00
0xRenegade 1f64d4cad7 if this feature is accepted in Pull Request, will need to add this back 2022-09-23 23:02:05 -05:00
0xRenegade b7b090f6c5 Merge branch 'renegade/general-fixes' of github.com:0xRenegade/PHP-Dynamic-Qr-code into renegade/general-fixes 2022-09-23 23:01:05 -05:00
0xRenegade 52d890597d some queries are manually entered, which aren't picked up by 'prefix' class attribute in database class 2022-09-23 22:59:53 -05:00
0xRenegade 40ab7b256d Merge branch 'master' into renegade/general-fixes 2022-09-23 21:05:16 -05:00
0xRenegade bbf115c2ae added in prefix by default, oops. 2022-09-23 21:00:41 -05:00
0xRenegade b433a83578 Issue #30: Fixes database_prefix option during install 2022-09-23 20:55:55 -05:00
Giandonato Inverso c005b52211 Update config.php 2022-09-24 03:24:15 +02:00
Giandonato Inverso e61c38473c Update docker-compose.yml 2022-09-24 03:23:41 +02:00
Giandonato Inverso 1243b32de1 Update environment.php 2022-09-24 03:23:20 +02:00
0xRenegade a20810d650 missing scroll bar on documentation page sidebar 2022-09-23 20:09:22 -05:00
Giandonato Inverso c4d5440453 Update add_dynamic_form.php
added support for http url
2022-09-10 11:50:19 +02:00
Giandonato Inverso 975fde9c35 Update MysqliDb.php
fix deprecated implode()
2022-09-05 20:40:26 +02:00
Giandonato Inverso af363723b2 Merge pull request #25 from nirpt/master
docker build now supports app release version code.
2022-08-29 14:09:34 +02:00
nirpt e2ff6e585b Docker build with app version added. 2022-08-29 12:32:20 +02:00
nirpt 389123fe15 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	docker/README.md
2022-08-28 13:22:12 +02:00
Giandonato Inverso a519d7bfe6 Update README.md 2022-08-28 12:33:14 +02:00
Giandonato Inverso a98c89075d doc update
Signed-off-by: giandonato.inverso@edempg.it <Zannabianca20>
2022-08-28 12:04:48 +02:00
Giandonato Inverso 00d7dfdb0b new installation process via script, elimination of data entry form for installation
Signed-off-by: giandonato.inverso@edempg.it <Zannabianca20>
2022-08-28 11:57:31 +02:00
nirpt 08ac31210c Minor refactor and cleanup 2022-08-28 11:29:20 +02:00
Giandonato Inverso fef45c401a configuration file modification, docker environment variable support 2022-08-28 11:24:32 +02:00
Giandonato Inverso 21b779c581 Merge pull request #23 from nirpt/master
docker support added
2022-08-28 10:47:05 +02:00
nirpt 618030e9d3 docker support added 2022-08-28 10:24:07 +02:00
Giandonato Inverso 59cee36c3a Update README.md 2022-06-17 11:49:06 +02:00
Giandonato Inverso 88028393cd Update README.md 2022-06-17 11:48:44 +02:00
Giandonato Inverso de377902be Merge pull request #16 from neoteknic/patch-1
Fix php 8.1 warning in form field
2022-02-22 16:55:19 +01:00
neoteknic ee711a5796 Update filters.php
Fix php 8.1 warning in form field
2022-02-22 16:22:13 +01:00
Giandonato Inverso e599aed16f Add files via upload 2020-09-08 18:43:51 +02:00
Giandonato Inverso 0d77e8f3a4 Add files via upload 2020-09-08 18:42:29 +02:00
Giandonato Inverso 6ed9018086 Add files via upload 2020-09-08 18:41:51 +02:00
Giandonato Inverso 8cee68e091 Add files via upload 2020-09-08 18:30:32 +02:00
Giandonato Inverso 4615be4280 Add files via upload 2020-09-08 18:28:26 +02:00
Giandonato Inverso 6e7b7cfa77 Add files via upload 2020-09-08 18:22:57 +02:00
Giandonato Inverso 74f92ca2fa Add files via upload 2020-09-08 18:20:38 +02:00
Giandonato Inverso 2dc2e2fe16 Add files via upload 2020-09-08 18:17:08 +02:00
Giandonato Inverso 37dc9baa97 Add files via upload 2020-09-08 18:15:54 +02:00
Giandonato Inverso 0337838319 Add files via upload 2020-09-08 18:13:36 +02:00
Giandonato Inverso c544e0c7b5 Add files via upload 2020-09-08 18:11:37 +02:00
Giandonato Inverso 087e3e83b5 Add files via upload 2020-09-08 18:09:19 +02:00
Giandonato Inverso 2ae9f32cb8 Add files via upload 2020-09-08 18:06:28 +02:00
Giandonato Inverso 5924afa3dc Add files via upload 2020-09-08 17:59:49 +02:00
Giandonato Inverso b302e0cce1 Add files via upload 2020-09-08 17:59:21 +02:00
Giandonato Inverso 1f9fa672c6 Update README.md 2020-09-08 17:31:01 +02:00
Giandonato Inverso bd610dc68f Update README.md 2020-09-08 17:29:26 +02:00
Giandonato Inverso dd9d5ae2fb Update README.md 2020-09-08 17:26:41 +02:00
Giandonato Inverso 0e62c29f11 Update README.md 2020-09-08 17:25:38 +02:00
Giandonato Inverso 14610fe212 Update README.md 2020-09-08 17:24:07 +02:00
Giandonato Inverso 8bcb852ea9 Update README.md 2020-09-08 17:23:38 +02:00
Giandonato Inverso 0bfe40eafe Update README.md 2020-09-08 17:23:17 +02:00
Giandonato Inverso 1e6bc3af4e Update README.md 2020-09-08 17:23:02 +02:00
Giandonato Inverso fa9e6730cc Update README.md 2020-09-08 17:22:41 +02:00
Giandonato Inverso 6709776cc5 Update README.md 2020-09-08 17:22:25 +02:00
Giandonato Inverso df04139a05 Update README.md 2020-09-08 17:21:33 +02:00
Giandonato Inverso 4a377b635c Update README.md 2020-09-08 17:21:07 +02:00
Giandonato Inverso 8262550a10 Update README.md 2020-09-08 17:20:39 +02:00
Giandonato Inverso 31ab8efc05 Update README.md 2020-09-08 17:20:20 +02:00
Giandonato Inverso 3ee55c9b17 Update README.md 2020-09-08 17:20:05 +02:00
Giandonato Inverso 93cd20c49d Update README.md 2020-09-08 17:18:41 +02:00
Giandonato Inverso eff10b3ee6 Update README.md 2020-09-08 17:17:36 +02:00
Giandonato Inverso 6de33ea45c Update README.md 2020-09-08 17:14:57 +02:00
Giandonato Inverso 34bfee494e Update README.md 2020-09-08 17:13:33 +02:00
Giandonato Inverso 3f8209a6a6 Update README.md 2020-09-08 17:13:06 +02:00
Giandonato Inverso 2b93634d3a Update README.md 2020-09-08 17:12:50 +02:00
Giandonato Inverso c698fc34f1 Update README.md 2020-09-08 17:12:34 +02:00
Giandonato Inverso 93d362aba2 Update README.md 2020-09-08 17:12:01 +02:00
Giandonato Inverso 8fe055a5b1 Update README.md 2020-09-08 17:11:45 +02:00
Giandonato Inverso 8a64294455 Update README.md 2020-09-08 17:11:27 +02:00
Giandonato Inverso 854b0ca77d Update README.md 2020-09-08 17:10:01 +02:00
Giandonato Inverso 7c707f865c Update README.md 2020-09-08 17:09:36 +02:00
Giandonato Inverso 439d136f54 Update README.md 2020-09-08 17:09:15 +02:00
Giandonato Inverso d0f2d526f1 Update README.md 2020-09-08 17:08:42 +02:00
Giandonato Inverso 9c855a19bb Update README.md 2020-09-08 17:07:06 +02:00
Giandonato Inverso a7e532867a Update README.md 2020-09-08 17:06:25 +02:00
Giandonato Inverso 67e6d85da9 Update README.md 2020-09-08 17:05:43 +02:00
Giandonato Inverso 35c72f6199 Initial commit 2020-09-08 16:56:05 +02:00
72 changed files with 2255 additions and 182 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Kopieer naar .env en pas de waarden aan. .env wordt niet gecommit (zie .gitignore).
# Copy to .env and adjust the values. .env is not committed (see .gitignore).
TYPE=docker
QRCODE_GENERATOR=internal-chillerlan.qrcode
+11 -10
View File
@@ -1,4 +1,4 @@
FROM php:8.3
FROM php:8.4
RUN if [ "$(grep '^VERSION_ID=' /etc/os-release | cut -d '=' -f 2 | tr -d '"')" -eq "9" ]; then \
sed -i -e 's/deb.debian.org/archive.debian.org/g' \
@@ -51,16 +51,12 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update -q \
sockets \
xsl \
zip \
imagick \
" \
&& case "$PHP_VERSION" in \
5.6.*) PHP_EXTENSIONS="$PHP_EXTENSIONS mcrypt mysql";; \
7.0.*|7.1.*) PHP_EXTENSIONS="$PHP_EXTENSIONS mcrypt";; \
esac \
# Install Imagick from master on PHP >= 8.3, because imagick 3.7.0 broke on latest PHP releases and Imagick maintainers don't care to tag a newer release
&& if [ $(php -r 'echo PHP_VERSION_ID;') -lt 80300 ]; then \
PHP_EXTENSIONS="$PHP_EXTENSIONS imagick"; \
else PHP_EXTENSIONS="$PHP_EXTENSIONS https://api.github.com/repos/Imagick/imagick/tarball/28f27044e435a2b203e32675e942eb8de620ee58"; \
fi \
&& install-php-extensions $PHP_EXTENSIONS \
&& if command -v a2enmod; then a2enmod rewrite; fi
@@ -86,10 +82,10 @@ RUN docker-php-ext-install sockets && docker-php-ext-enable sockets
RUN mkdir -p /opt && chmod 777 /opt
WORKDIR /opt
# Vastgezet op 5.0.5 (laatste 5.x-release): vanaf 6.0.0 vereist de library PHP >= 8.4,
# terwijl deze image op PHP 8.3 draait. Een ongepinde clone van master is bovendien
# een reproduceerbaarheids-/supply-chain-risico (build kan zonder waarschuwing breken).
RUN git clone --branch 5.0.5 --depth 1 https://github.com/chillerlan/php-qrcode.git \
# Pinned to a specific release tag instead of an unpinned clone of master, which is a
# reproducibility/supply-chain risk (the build can break silently when upstream moves on,
# as happened when master started requiring PHP 8.4 while this image was still on 8.3).
RUN git clone --branch 6.0.1 --depth 1 https://github.com/chillerlan/php-qrcode.git \
&& chmod -R 777 ./php-qrcode
RUN cp ./php-qrcode/composer.json /var/www/html/composer.json
RUN mkdir -p /var/www/html/test && chmod 777 /var/www/html/test
@@ -100,5 +96,10 @@ WORKDIR /var/www/html
RUN composer update
COPY ./src ./
RUN chmod 755 *;
# Qr code storage lives outside the document root so files can only be reached through
# the authenticated qrcode_image.php / qrcode_zip_download.php endpoints.
RUN mkdir -p /var/www/qrcode-storage/zip && chmod -R 777 /var/www/qrcode-storage
EXPOSE 80
CMD ["php", "-S", "0.0.0.0:80"]
+12 -5
View File
@@ -1,4 +1,4 @@
FROM php:8.3-fpm
FROM php:8.4-fpm
RUN if [ "$(grep '^VERSION_ID=' /etc/os-release | cut -d '=' -f 2 | tr -d '"')" -eq "9" ]; then \
sed -i -e 's/deb.debian.org/archive.debian.org/g' \
@@ -13,6 +13,7 @@ RUN chmod +x /usr/local/bin/install-php-extensions
RUN DEBIAN_FRONTEND=noninteractive apt-get update -q \
&& DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \
curl \
git \
libzip-dev \
libjpeg62-turbo-dev \
libpng-dev \
@@ -21,6 +22,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update -q \
&& install-php-extensions \
gd \
gettext \
imagick \
intl \
mysqli \
opcache \
@@ -41,8 +43,8 @@ RUN cd /opt \
RUN mkdir -p /opt && chmod 777 /opt
WORKDIR /opt
# Zie Dockerfile: vastgezet op 5.0.5, want 6.0.0+ vereist PHP >= 8.4.
RUN git clone --branch 5.0.5 --depth 1 https://github.com/chillerlan/php-qrcode.git \
# See Dockerfile: pinned to a specific release tag instead of an unpinned clone of master.
RUN git clone --branch 6.0.1 --depth 1 https://github.com/chillerlan/php-qrcode.git \
&& chmod -R 777 ./php-qrcode
RUN cp ./php-qrcode/composer.json /var/www/html/composer.json
RUN cp -R ./php-qrcode/src /var/www/html/
@@ -52,8 +54,13 @@ RUN composer update
COPY ./src ./
RUN chown -R www-data:www-data /var/www/html \
&& find /var/www/html -type f -exec chmod 644 {} \; \
&& find /var/www/html -type d -exec chmod 755 {} \; \
&& chmod -R 775 /var/www/html/saved_qrcode
&& find /var/www/html -type d -exec chmod 755 {} \;
# Qr code storage lives outside the document root so files can only be reached through
# the authenticated qrcode_image.php / qrcode_zip_download.php endpoints.
RUN mkdir -p /var/www/qrcode-storage/zip \
&& chown -R www-data:www-data /var/www/qrcode-storage \
&& chmod -R 775 /var/www/qrcode-storage
EXPOSE 9000
CMD ["php-fpm"]
+19 -2
View File
@@ -16,6 +16,9 @@ CREATE TABLE IF NOT EXISTS `users` (
`type` varchar(10) NOT NULL,
`must_change_password` tinyint(1) NOT NULL DEFAULT 0,
`password_changed_at` datetime DEFAULT NULL,
`can_view_static` tinyint(1) NOT NULL DEFAULT 0,
`can_view_dynamic` tinyint(1) NOT NULL DEFAULT 0,
`owner_admin_id` int(25) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ;
@@ -58,7 +61,7 @@ CREATE TABLE IF NOT EXISTS `static_qrcodes` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;
-- Security hardening (Fase 1): rate limiting op login pogingen
-- Security hardening (Fase 1): rate limiting on login attempts
CREATE TABLE IF NOT EXISTS `login_attempts` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
@@ -70,7 +73,7 @@ CREATE TABLE IF NOT EXISTS `login_attempts` (
KEY `ip_attempted_at` (`ip_address`, `attempted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Security hardening (Fase 1): audit log van gevoelige acties
-- Security hardening (Fase 1): audit log of sensitive actions
CREATE TABLE IF NOT EXISTS `audit_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(25) DEFAULT NULL,
@@ -86,6 +89,20 @@ CREATE TABLE IF NOT EXISTS `audit_log` (
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Fase 3 (priority 2): saved color/style presets per user
CREATE TABLE IF NOT EXISTS `qr_presets` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(25) NOT NULL,
`name` varchar(50) NOT NULL,
`foreground` varchar(10) NOT NULL,
`background` varchar(10) NOT NULL,
`level` varchar(1) NOT NULL DEFAULT 'L',
`size` int(10) unsigned NOT NULL DEFAULT 200,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+6 -6
View File
@@ -1,7 +1,7 @@
-- Fase 1 security hardening migratie.
-- Voer uit tegen een bestaande database (gebruikt de originele
-- giandonatoinverso/php-dynamic-qr-code-db image of een oudere init.sql).
-- Kolommen/tabellen worden alleen toegevoegd als ze nog niet bestaan.
-- Fase 1 security hardening migration.
-- Run against an existing database (using the original
-- giandonatoinverso/php-dynamic-qr-code-db image or an older init.sql).
-- Columns/tables are only added if they don't already exist.
SET @db := DATABASE();
@@ -23,8 +23,8 @@ SET @sql := IF(@col_exists = 0,
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- Bestaand superadmin account met het fabriekswachtwoord (superadmin/superadmin)
-- moet bij eerstvolgende login het wachtwoord wijzigen.
-- An existing superadmin account with the factory password (superadmin/superadmin)
-- must change its password on next login.
UPDATE `users`
SET `must_change_password` = 1
WHERE `username` = 'superadmin'
+22
View File
@@ -0,0 +1,22 @@
-- Fase 2: read-only 'user' role with two visibility toggles.
-- type='user' requires no schema change (varchar(10), no enum constraint).
SET @db := DATABASE();
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'can_view_static'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `can_view_static` TINYINT(1) NOT NULL DEFAULT 0',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'can_view_dynamic'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `can_view_dynamic` TINYINT(1) NOT NULL DEFAULT 0',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+14
View File
@@ -0,0 +1,14 @@
-- Option 2: an admin may create their own 'user' accounts within their own scope.
-- owner_admin_id = NULL means: created by super, company-wide (previous behavior).
-- owner_admin_id = <id> means: created by that admin, sees only that admin's own codes.
SET @db := DATABASE();
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'owner_admin_id'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `owner_admin_id` INT(25) DEFAULT NULL',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+14
View File
@@ -0,0 +1,14 @@
-- Fase 3 (priority 2): saved color/style presets per user.
CREATE TABLE IF NOT EXISTS `qr_presets` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(25) NOT NULL,
`name` varchar(50) NOT NULL,
`foreground` varchar(10) NOT NULL,
`background` varchar(10) NOT NULL,
`level` varchar(1) NOT NULL DEFAULT 'L',
`size` int(10) unsigned NOT NULL DEFAULT 200,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+7 -8
View File
@@ -4,11 +4,10 @@ services:
restart: "unless-stopped"
ports:
- "80:80"
# 443 pas openzetten zodra SSL-certificaten zijn gemount (bv. via certbot-volume
# of een losse reverse proxy zoals Caddy/Traefik ervoor). Zie infra-fase van het plan.
# Only open 443 once SSL certificates are mounted (e.g. via a certbot volume,
# or a separate reverse proxy like Caddy/Traefik in front). See the infra phase of the plan.
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode:ro
depends_on:
- php-dynamic-qrcode
networks:
@@ -22,19 +21,19 @@ services:
environment:
TYPE: "docker"
QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}"
BASE_URL: "${BASE_URL:?zet BASE_URL in .env, bv. https://qr.ensembia.com}"
BASE_URL: "${BASE_URL:?set BASE_URL in .env, e.g. https://qr.ensembia.com}"
DATABASE_HOST: "php-dynamic-qrcode-db"
DATABASE_PORT: "3306"
DATABASE_NAME: "${DATABASE_NAME:-qrcode}"
DATABASE_USER: "${DATABASE_USER:-qrcode}"
DATABASE_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}"
DATABASE_PASSWORD: "${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}"
DATABASE_PREFIX: "${DATABASE_PREFIX:-}"
DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}"
depends_on:
php-dynamic-qrcode-db:
condition: service_healthy
volumes:
- php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode
- php_dynamic_qrcode_saved_qrcode_data:/var/www/qrcode-storage
networks:
- php-dynamic-qrcode-network
@@ -45,10 +44,10 @@ services:
- php_dynamic_qrcode_db_data:/var/lib/mysql
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?zet MYSQL_ROOT_PASSWORD in .env}"
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD in .env}"
MYSQL_DATABASE: "${DATABASE_NAME:-qrcode}"
MYSQL_USER: "${DATABASE_USER:-qrcode}"
MYSQL_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}"
MYSQL_PASSWORD: "${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s
+2 -6
View File
@@ -21,12 +21,8 @@ server {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Statisch gegenereerde qrcodes mogen gedownload worden, maar niet als PHP uitgevoerd.
location /saved_qrcode/ {
location ~ \.php$ {
deny all;
}
}
# Generated qr codes are stored outside the document root and are only served
# through the authenticated qrcode_image.php / qrcode_zip_download.php endpoints.
location ~ /\. {
deny all;
+3
View File
@@ -40,6 +40,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
$_SESSION['user_id'] = $row['id'];
$_SESSION['username'] = $row['username'];
$_SESSION['must_change_password'] = !empty($row['must_change_password']);
$_SESSION['can_view_static'] = !empty($row['can_view_static']);
$_SESSION['can_view_dynamic'] = !empty($row['can_view_dynamic']);
$_SESSION['scope_owner_id'] = qr_compute_scope_owner_id($row);
$_SESSION['last_activity'] = time();
audit_log('login_success');
+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>
+25 -11
View File
@@ -35,24 +35,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit();
}
if ($_SESSION['type'] === 'user') {
$view_flag = $type === 'dynamic' ? 'can_view_dynamic' : 'can_view_static';
if (empty($_SESSION[$view_flag] ?? null)) {
http_response_code(403);
echo json_encode(['data' => 'Not allowed to view this qr code type.', 'status' => 403]);
exit();
}
}
foreach ($params as $param) {
$db->where('id', $param);
if ($_SESSION['type'] !== 'super') {
$db->where('id_owner', $_SESSION['user_id']);
$db->orWhere('id_owner', NULL, 'IS');
}
qr_apply_owner_scope($db);
$row = $db->getOne("{$type}_qrcodes");
if ($row !== NULL) {
$files[] = SAVED_QRCODE_FOLDER . $row['qrcode'];
$files[] = SAVED_QRCODE_DIRECTORY . $row['qrcode'];
}
}
$zip = new ZipArchive();
$uniqid = uniqid();
$relative_dir = SAVED_QRCODE_FOLDER . 'zip/qrcodes_' . $uniqid . '.zip';
@unlink($relative_dir);
$url_path = SAVED_QRCODE_URL . 'zip/qrcodes_' . $uniqid . '.zip';
$zip->open($relative_dir, ZipArchive::CREATE);
$zip_filename = 'qrcodes_' . uniqid() . '.zip';
$zip_path = SAVED_QRCODE_DIRECTORY . 'zip/' . $zip_filename;
@unlink($zip_path);
$zip->open($zip_path, ZipArchive::CREATE);
foreach ($files as $file) {
$download_file = @file_get_contents($file, true);
@@ -61,14 +66,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$zip->close();
// Proof-of-generation: only this session may download this specific zip file.
$_SESSION['generated_zips'][] = $zip_filename;
audit_log('bulk_download', $type, implode(',', $params));
echo json_encode([
'data' => $url_path,
'data' => 'qrcode_zip_download.php?file=' . rawurlencode($zip_filename),
'status' => 200
]);
exit();
} else if($json["action"] == "delete") {
if ($_SESSION['type'] === 'user') {
http_response_code(403);
echo json_encode(['data' => 'The "user" role is read-only.', 'status' => 403]);
exit();
}
$params = $json['params'];
if (isset($json['type']) && in_array($json['type'], $allowed_types, true)) {
+9 -5
View File
@@ -2,8 +2,12 @@
//Note: This file should be included first in every php page.
require_once ('environment.php');
// Never display errors/warnings/deprecations in the response body: besides leaking
// internal file paths, it can inject output before session_start() runs and break
// login entirely (seen with PHP 8.4's new deprecation notices). Log them instead.
error_reporting(E_ALL);
ini_set('display_errors', 'On');
ini_set('display_errors', 'Off');
ini_set('log_errors', 'On');
define('BASE_PATH', dirname(dirname(__FILE__)));
define('CURRENT_PAGE', basename($_SERVER['REQUEST_URI']));
define('SCRIPT_NAME', ltrim(dirname($_SERVER['SCRIPT_NAME']), '/'));
@@ -17,10 +21,10 @@ require_once BASE_PATH . '/lib/MysqliDb/MysqliDb.php';
require_once BASE_PATH . '/helpers/helpers.php';
/* SAVED QR CODES */
//You can change the folder where the qr code will be saved
define('SAVED_QRCODE_FOLDER', './saved_qrcode/');
define('SAVED_QRCODE_DIRECTORY', BASE_PATH.'/saved_qrcode/');
define('SAVED_QRCODE_URL', base_url(). SCRIPT_FOLDER .'/saved_qrcode/');
// Storage lives outside the document root so files can only be reached through the
// authenticated qrcode_image.php / qrcode_zip_download.php endpoints, never as a direct
// static URL. See db/migrations and the "saved_qrcode" hardening note in the OSS repo.
define('SAVED_QRCODE_DIRECTORY', dirname(BASE_PATH).'/qrcode-storage/');
//You can change the page name for the redirect and the search parameter (the default is "id")
define('READ_PATH', base_url().'/read.php?id=');
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+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);
}
});
});
});
+249
View File
@@ -0,0 +1,249 @@
// Preset system + random style button for the qr code generation forms (Fase 3, priority 2).
//
// The static qr "add" page stacks all qr-type forms (text/email/.../wifi/bitcoin/...) into
// the DOM at once as Bootstrap tab-panes, and every one of them repeats the same element ids
// (foreground, background, size, random_style_btn, ...). getElementById/getElementsBy* only
// ever finds the *first* of those (the "Text" tab), so every helper below is scoped to the
// specific tab-pane/form the triggering element lives in, and wired up via querySelectorAll
// so every tab gets working listeners, not just the first one.
(function () {
function csrfToken() {
var meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
function scopeOf(el) {
return el.closest('.tab-pane') || el.closest('form') || document;
}
function setColor(scope, id, hex) {
var input = scope.querySelector('#' + id);
if (!input) {
return;
}
input.value = hex;
input.dispatchEvent(new Event('change'));
try {
// Sync the bootstrap-colorpicker widget/swatch if it was initialized on this input.
if (window.jQuery) {
jQuery(input).colorpicker('setValue', hex);
}
} catch (e) {
// Colorpicker not initialized on this page - the raw value above is still correct.
}
}
function randomHexColor() {
var value = Math.floor(Math.random() * 0xFFFFFF).toString(16);
return '#' + ('000000' + value).slice(-6);
}
function updateStylePreview(scope) {
var swatch = scope.querySelector('#style_preview_swatch');
var text = scope.querySelector('#style_preview_text');
if (!swatch || !text) {
return;
}
var foreground = scope.querySelector('#foreground');
var background = scope.querySelector('#background');
var levelSelect = scope.querySelector('select[name="level"]');
var sizeSelect = scope.querySelector('#size');
var fg = foreground ? foreground.value : '#000000';
var bg = background ? background.value : '#ffffff';
swatch.style.background = bg;
swatch.style.borderColor = fg;
var parts = [];
if (levelSelect) {
parts.push('Precision: ' + levelSelect.value);
}
if (sizeSelect) {
parts.push('Size: ' + sizeSelect.value + 'px');
}
text.textContent = parts.join(' · ');
}
function loadPresetsInto(select) {
fetch('presets.php?action=list')
.then(function (response) { return response.json(); })
.then(function (json) {
if (json.status !== 200) {
return;
}
json.data.forEach(function (preset) {
var option = document.createElement('option');
option.value = preset.id;
option.textContent = preset.name;
option.dataset.foreground = preset.foreground;
option.dataset.background = preset.background;
option.dataset.level = preset.level;
option.dataset.size = preset.size;
select.appendChild(option);
});
})
.catch(function () { /* presets are a nice-to-have, fail silently */ });
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('#preset_select').forEach(loadPresetsInto);
document.querySelectorAll('#style_preview').forEach(function (row) {
updateStylePreview(scopeOf(row));
});
document.querySelectorAll('#foreground, #background').forEach(function (input) {
input.addEventListener('change', function () {
updateStylePreview(scopeOf(input));
});
});
document.querySelectorAll('select[name="level"]').forEach(function (select) {
select.addEventListener('change', function () {
updateStylePreview(scopeOf(select));
});
});
document.querySelectorAll('#size').forEach(function (select) {
select.addEventListener('change', function () {
updateStylePreview(scopeOf(select));
});
});
document.querySelectorAll('#random_style_btn').forEach(function (randomBtn) {
randomBtn.addEventListener('click', function () {
var scope = scopeOf(randomBtn);
setColor(scope, 'foreground', randomHexColor());
setColor(scope, 'background', randomHexColor());
updateStylePreview(scope);
});
});
document.querySelectorAll('#preset_select').forEach(function (presetSelect) {
presetSelect.addEventListener('change', function () {
var option = presetSelect.options[presetSelect.selectedIndex];
if (!option.value) {
return;
}
var scope = scopeOf(presetSelect);
setColor(scope, 'foreground', option.dataset.foreground);
setColor(scope, 'background', option.dataset.background);
var levelSelect = scope.querySelector('select[name="level"]');
if (levelSelect) {
levelSelect.value = option.dataset.level;
}
var sizeSelect = scope.querySelector('#size');
if (sizeSelect) {
sizeSelect.value = option.dataset.size;
}
updateStylePreview(scope);
});
});
document.querySelectorAll('#preset_save_btn').forEach(function (saveBtn) {
saveBtn.addEventListener('click', function () {
var scope = scopeOf(saveBtn);
var nameInput = scope.querySelector('#preset_name');
var name = nameInput.value.trim();
if (!name) {
alert('Enter a name for this preset first.');
return;
}
var foreground = scope.querySelector('#foreground').value;
var background = scope.querySelector('#background').value;
var level = scope.querySelector('select[name="level"]').value;
var size = scope.querySelector('#size').value;
var body = new URLSearchParams();
body.set('action', 'save');
body.set('name', name);
body.set('foreground', foreground);
body.set('background', background);
body.set('level', level);
body.set('size', size);
fetch('presets.php', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken() },
body: body
})
.then(function (response) { return response.json(); })
.then(function (json) {
if (json.status !== 200) {
alert('Could not save preset: ' + json.data);
return;
}
// The preset list is shared across every tab, so mirror the new
// option into every preset_select on the page, not just this one.
document.querySelectorAll('#preset_select').forEach(function (select) {
var option = document.createElement('option');
option.value = json.data.id;
option.textContent = json.data.name;
option.dataset.foreground = foreground;
option.dataset.background = background;
option.dataset.level = level;
option.dataset.size = size;
select.appendChild(option);
if (select === scope.querySelector('#preset_select')) {
select.value = option.value;
}
});
nameInput.value = '';
})
.catch(function () { alert('Could not save preset (network error).'); });
});
});
document.querySelectorAll('#preset_delete_btn').forEach(function (deleteBtn) {
deleteBtn.addEventListener('click', function () {
var scope = scopeOf(deleteBtn);
var presetSelect = scope.querySelector('#preset_select');
var option = presetSelect.options[presetSelect.selectedIndex];
if (!option.value) {
return;
}
if (!confirm('Delete preset "' + option.textContent + '"?')) {
return;
}
var body = new URLSearchParams();
body.set('action', 'delete');
body.set('id', option.value);
fetch('presets.php', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken() },
body: body
})
.then(function (response) { return response.json(); })
.then(function (json) {
if (json.status === 200) {
// Remove the matching option from every preset_select on the page.
document.querySelectorAll('#preset_select').forEach(function (select) {
var match = select.querySelector('option[value="' + option.value + '"]');
if (match) {
match.remove();
}
});
} else {
alert('Could not delete preset: ' + json.data);
}
})
.catch(function () { alert('Could not delete preset (network error).'); });
});
});
});
})();
+17 -2
View File
@@ -3,6 +3,12 @@ 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, edit or delete qr codes.';
header('Location: index.php');
exit;
}
$dynamic_qrcode_instance = new DynamicQrcode();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@@ -31,7 +37,7 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["edit"])) {
$dynamic_qrcode_instance->editQrcode($_POST);
}
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"]) && !isset($_POST["del_id"])) {
if(
isset($_POST["foreground"]) &&
isset($_POST["background"]) &&
@@ -39,8 +45,17 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
isset($_POST["filename"]) &&
isset($_POST["format"]) &&
isset($_POST["id_owner"])
)
) {
$icon_upload = qr_handle_icon_upload('icon');
if (!$icon_upload['ok']) {
$_SESSION['failure'] = $icon_upload['error'];
header('Location: ' . basename(__FILE__));
exit;
}
$_POST['icon_tmp_path'] = $icon_upload['path'];
$dynamic_qrcode_instance->addQrcode($_POST);
}
}
?>
<!DOCTYPE html>
+11 -4
View File
@@ -3,6 +3,12 @@ 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' && empty($_SESSION['can_view_dynamic'] ?? null)) {
$_SESSION['failure'] = 'You are not allowed to view dynamic qr codes.';
header('Location: index.php');
exit;
}
$db = getDbInstance();
$dynamic_qrcode = new DynamicQrcode();
@@ -12,10 +18,9 @@ require_once BASE_PATH . '/includes/search_order.php';
$page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 1;
$db->pageLimit = 15;
if($_SESSION['type'] !== 'super') {
$db->where("id_owner", $_SESSION['user_id']);
$db->orWhere ("id_owner", NULL, 'IS');
}
// Scoped to one admin's own codes for an admin (or a 'user' created by that admin);
// full visibility for super and company-wide 'user' accounts.
qr_apply_owner_scope($db);
$rows = $db->arraybuilder()->paginate('dynamic_qrcodes', $page, $select);
$total_pages = $db->totalPages;
@@ -49,6 +54,7 @@ $total_pages = $db->totalPages;
<h1 class="m-0 text-dark">Dynamic Qr codes</h1>
</div><!-- /.col -->
<?php if ($_SESSION['type'] !== 'user'): ?>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item">
@@ -56,6 +62,7 @@ $total_pages = $db->totalPages;
</li>
</ol>
</div><!-- /.col -->
<?php endif; ?>
</div><!-- /.row -->
</div><!-- /.container-fluid -->
</div><!-- /.content-header -->
+84 -3
View File
@@ -35,7 +35,7 @@
<div class="col-6 col-md-3">
<label for="size">Size (px)</label>
<select name="size" class="form-control">
<select name="size" id="size" class="form-control">
<option value="100">100</option>
<option value="200">200</option>
<option value="300">300</option>
@@ -51,6 +51,49 @@
</div>
</div>
<div class="col-sm-12 mb-2">
<div class="row">
<div class="col-6 col-md-3">
<label>&nbsp;</label>
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
<i class="fa fa-dice"></i> Random style
</button>
</div>
<div class="col-6 col-md-3">
<label for="preset_select">Load preset</label>
<div class="input-group">
<select id="preset_select" class="form-control">
<option value="">-- Select --</option>
</select>
<div class="input-group-append">
<button type="button" id="preset_delete_btn" class="btn btn-outline-secondary" title="Delete selected preset"><i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<label for="preset_name">Save as preset</label>
<div class="input-group">
<input type="text" id="preset_name" class="form-control" placeholder="Preset name" maxlength="50">
<div class="input-group-append">
<button type="button" id="preset_save_btn" class="btn btn-outline-secondary"><i class="fa fa-save"></i></button>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<label>Style preview</label>
<div id="style_preview" class="d-flex align-items-center">
<span id="style_preview_swatch" style="display:inline-block;width:38px;height:38px;border:3px solid #000;background:#fff;border-radius:4px;"></span>
<small id="style_preview_text" class="ml-2 text-muted"></small>
</div>
</div>
</div>
</div>
<script src="dist/js/qrcode-style-tools.js?nocache=<?php print rand();?>"></script>
<!-- Its use is not recommended. Read the documentation
<div class="form-group">
<label for="logo">Logo</label>
@@ -94,6 +137,43 @@
<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 class="col-6 col-md-2">
<div class="form-group">
<label for="frame_font">Frame font</label>
<select name="frame_font" id="frame_font" class="form-control">
<option value="sans" selected>Sans</option>
<option value="sans-bold">Sans Bold</option>
<option value="serif">Serif</option>
<option value="serif-bold">Serif Bold</option>
<option value="mono">Monospace</option>
<option value="mono-bold">Monospace Bold</option>
</select>
</div>
</div>
<div class="col-6 col-md-2">
<div class="form-group">
<label for="frame_font_size">Frame font size</label>
<input type="number" name="frame_font_size" id="frame_font_size" value="16" min="8" max="60" class="form-control">
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="icon">Icon above QR code</label>
<input type="file" name="icon" id="icon" accept="image/png,image/jpeg,image/gif" class="form-control-file">
<small class="form-text text-muted">Optional. PNG/JPEG/GIF, max 1MB. Shown above the code (not embedded in it). Only applies to PNG/JPEG/GIF output.</small>
</div>
</div>
</div>
</div>
@@ -104,7 +184,7 @@
<div class="form-group">
<label for="id_owner">Owner *</label>
<select name="id_owner" class="form-control">
<option value="" selected>All</option>
<option value="">All (shared with every admin)</option>
<?php
require_once BASE_PATH . '/lib/Users/Users.php';
@@ -112,8 +192,9 @@
$users = $users_instance->getAllUsers();
foreach ($users as $user) {
$is_self = (int) $user["id"] === (int) $_SESSION["user_id"];
?>
<option value="<?php echo $user["id"];?>"><?php echo $user["username"];?></option>
<option value="<?php echo $user["id"];?>" <?php echo $is_self ? 'selected' : ''; ?>><?php echo $user["username"];?></option>
<?php } ?>
</select>
</div>
+14
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,7 +96,15 @@
<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 -->
<script src="dist/js/qrcode-style-tools.js?nocache=<?php print rand();?>"></script>
</fieldset>
+59
View File
@@ -25,6 +25,7 @@
</div>
</div>
<?php if ($_SESSION['type'] === 'super'): ?>
<div class="col-sm-4">
<label for="user-type">User type *</label>
@@ -38,8 +39,66 @@
<label class="radio">
<input type="radio" name="type" value="admin" required="required" <?php echo ($edit && $user['type'] =='admin') ? "checked": "" ; ?>/> Admin</label>
</div>
<div class="radio">
<label class="radio">
<input type="radio" name="type" value="user" required="required" id="type-user" <?php echo ($edit && $user['type'] =='user') ? "checked": "" ; ?>/> User (read-only)</label>
</div>
</div>
</div>
<div class="col-sm-12 mt-2" id="user-view-toggles">
<label>Visibility for the 'User' role</label>
<div class="form-group">
<div class="icheck-primary d-inline-block mr-4">
<input type="checkbox" name="can_view_static" id="can_view_static" value="1" <?php echo ($edit && !empty($user['can_view_static'])) ? "checked": "" ; ?>>
<label for="can_view_static">Can view static qr codes</label>
</div>
<div class="icheck-primary d-inline-block">
<input type="checkbox" name="can_view_dynamic" id="can_view_dynamic" value="1" <?php echo ($edit && !empty($user['can_view_dynamic'])) ? "checked": "" ; ?>>
<label for="can_view_dynamic">Can view dynamic qr codes</label>
</div>
<small class="form-text text-muted">Only applies to the 'User' type. Reports/statistics are always visible for 'User'.</small>
</div>
</div>
<script>
(function () {
var typeRadios = document.querySelectorAll('input[name="type"]');
var toggles = document.getElementById('user-view-toggles');
function updateToggleVisibility() {
var userSelected = document.getElementById('type-user').checked;
toggles.style.display = userSelected ? '' : 'none';
}
typeRadios.forEach(function (radio) {
radio.addEventListener('change', updateToggleVisibility);
});
updateToggleVisibility();
})();
</script>
<?php else: ?>
<!-- An 'admin' can only create/manage their own read-only 'user' accounts. -->
<input type="hidden" name="type" value="user">
<div class="col-sm-12 mt-2">
<label>Visibility for this user</label>
<div class="form-group">
<div class="icheck-primary d-inline-block mr-4">
<input type="checkbox" name="can_view_static" id="can_view_static" value="1" <?php echo ($edit && !empty($user['can_view_static'])) ? "checked": "" ; ?>>
<label for="can_view_static">Can view static qr codes</label>
</div>
<div class="icheck-primary d-inline-block">
<input type="checkbox" name="can_view_dynamic" id="can_view_dynamic" value="1" <?php echo ($edit && !empty($user['can_view_dynamic'])) ? "checked": "" ; ?>>
<label for="can_view_dynamic">Can view dynamic qr codes</label>
</div>
<small class="form-text text-muted">Reports/statistics are always visible for this account.</small>
</div>
</div>
<?php endif; ?>
<?php if($edit) { ?>
<input type="hidden" name="id" value="<?php echo $user['id'];?>"/>
<input type="hidden" name="edit" value="true"/>
+91 -3
View File
@@ -34,7 +34,7 @@
<div class="col-6 col-md-3">
<label for="size">Size (px)</label>
<select name="size" class="form-control">
<select name="size" id="size" class="form-control">
<option value="100">100</option>
<option value="200">200</option>
<option value="300">300</option>
@@ -55,6 +55,56 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
</div>
</div>
<div class="col-sm-12 mb-2">
<div class="row">
<div class="col-6 col-md-3">
<label>&nbsp;</label>
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
<i class="fa fa-dice"></i> Random style
</button>
</div>
<div class="col-6 col-md-3">
<label for="preset_select">Load preset</label>
<div class="input-group">
<select id="preset_select" class="form-control">
<option value="">-- Select --</option>
</select>
<div class="input-group-append">
<button type="button" id="preset_delete_btn" class="btn btn-outline-secondary" title="Delete selected preset"><i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<label for="preset_name">Save as preset</label>
<div class="input-group">
<input type="text" id="preset_name" class="form-control" placeholder="Preset name" maxlength="50">
<div class="input-group-append">
<button type="button" id="preset_save_btn" class="btn btn-outline-secondary"><i class="fa fa-save"></i></button>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<label>Style preview</label>
<div id="style_preview" class="d-flex align-items-center">
<span id="style_preview_swatch" style="display:inline-block;width:38px;height:38px;border:3px solid #000;background:#fff;border-radius:4px;"></span>
<small id="style_preview_text" class="ml-2 text-muted"></small>
</div>
</div>
</div>
</div>
<!--
Note: no <script src="dist/js/qrcode-style-tools.js"> tag here on purpose. This
partial is included once per qr type on the static "add" page (form_static_add.php),
which stacks all types into the DOM as tab-panes; including the script here would load
and execute it once per type, each execution re-attaching its own listeners to every
button on the page. The script is included exactly once by the pages that use this
partial (form_static_add.php and form_dynamic_add.php).
-->
<!-- Its use is not recommended. Read the documentation
<div class="form-group">
<label for="logo">Logo</label>
@@ -88,6 +138,43 @@ 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 class="col-6 col-md-2">
<div class="form-group">
<label for="frame_font">Frame font</label>
<select name="frame_font" id="frame_font" class="form-control">
<option value="sans" selected>Sans</option>
<option value="sans-bold">Sans Bold</option>
<option value="serif">Serif</option>
<option value="serif-bold">Serif Bold</option>
<option value="mono">Monospace</option>
<option value="mono-bold">Monospace Bold</option>
</select>
</div>
</div>
<div class="col-6 col-md-2">
<div class="form-group">
<label for="frame_font_size">Frame font size</label>
<input type="number" name="frame_font_size" id="frame_font_size" value="16" min="8" max="60" class="form-control">
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="icon">Icon above QR code</label>
<input type="file" name="icon" id="icon" accept="image/png,image/jpeg,image/gif" class="form-control-file">
<small class="form-text text-muted">Optional. PNG/JPEG/GIF, max 1MB. Shown above the code (not embedded in it). Only applies to PNG/JPEG/GIF output.</small>
</div>
</div>
</div>
</div>
@@ -98,7 +185,7 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
<div class="form-group">
<label for="id_owner">Owner *</label>
<select name="id_owner" class="form-control">
<option value="" selected>All</option>
<option value="">All (shared with every admin)</option>
<?php
require_once BASE_PATH . '/lib/Users/Users.php';
@@ -106,8 +193,9 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
$users = $users_instance->getAllUsers();
foreach ($users as $user) {
$is_self = (int) $user["id"] === (int) $_SESSION["user_id"];
?>
<option value="<?php echo $user["id"];?>"><?php echo $user["username"];?></option>
<option value="<?php echo $user["id"];?>" <?php echo $is_self ? 'selected' : ''; ?>><?php echo $user["username"];?></option>
<?php } ?>
</select>
</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>
+1 -1
View File
@@ -14,7 +14,7 @@
<div class="col-6 col-md-3">
<div class="form-group">
<label>Amount *</label>
<label>Amount</label>
<div class="input-group">
<input type="number" name="amount" value="" placeholder="" class="form-control" step="0.0001">
<div class="input-group-append">
+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>
+1
View File
@@ -10,6 +10,7 @@
<label>Encryption *</label>
<select name="encryption" class="form-control">
<option value="WPA" Selected>WPA/WPA2</option>
<option value="WPA3">WPA3</option>
<option value="WEP">WEP</option>
<option value="">None</option>
</select>
+16 -4
View File
@@ -1,4 +1,6 @@
<?php $is_readonly_user = $_SESSION['type'] === 'user'; ?>
<div class="row">
<?php if (!$is_readonly_user): ?>
<div class="col-12" id="bulk-action-div" style="display: none;">
<div id="err-msg"></div>
<div class="bulk-action-wrapper">
@@ -21,13 +23,16 @@
</form>
</div>
</div>
<?php endif; ?>
<div class="col-12">
<div class="card">
<div class="card-body table-responsive p-0">
<table class="table table-striped table-bordered">
<thead>
<tr>
<?php if (!$is_readonly_user): ?>
<th><input type="checkbox" name="bulk-select" value="1"></th>
<?php endif; ?>
<th>ID</th>
<th>Owner</th>
<th>Filename</th>
@@ -42,7 +47,9 @@
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<?php if (!$is_readonly_user): ?>
<td><input type="checkbox" name="action[]" value="<?=$row['id']?>" onchange="updateBulkActionVisibility()"></td>
<?php endif; ?>
<td><?php echo $row['id']; ?></td>
<td>
<?php
@@ -63,12 +70,12 @@
<td><?php echo htmlspecialchars($row['identifier']); ?></td>
<td><?php echo htmlspecialchars($row['link']); ?></td>
<td>
<?php echo '<img src="'.SAVED_QRCODE_FOLDER.htmlspecialchars($row['qrcode']).'" width="100" height="100">'; ?>
<?php echo '<img src="qrcode_image.php?type=dynamic&id='.$row['id'].'" width="100" height="100">'; ?>
</td>
<td><?php echo htmlspecialchars($row['scan']); ?></td>
<td><?php echo htmlspecialchars($row['state']); ?></td>
<td>
<?php if (!$is_readonly_user): ?>
<!-- EDIT -->
<a href="dynamic_qrcode.php?edit=true&id=<?php echo $row['id']; ?>" class="btn btn-primary"><i class="fas fa-edit"></i></a>
@@ -79,9 +86,12 @@
data-target="#delete-modal"
data-del_id="<?php echo $row["id"];?>"
><i class="fas fa-trash"></i></a>
<?php endif; ?>
<!-- DOWNLOAD -->
<a href="<?php echo SAVED_QRCODE_FOLDER.htmlspecialchars($row['qrcode']); ?>" class="btn btn-primary" download><i class="fa fa-download"></i></a>
<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; ?>
@@ -97,6 +107,7 @@
</div><!-- /.col -->
</div><!-- /.row -->
<?php if (!$is_readonly_user): ?>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog">
@@ -122,6 +133,7 @@
</div>
</div>
<!-- /.Delete Confirmation Modal -->
<?php endif; ?>
<script>
const deleteButtons = document.querySelectorAll('.delete_btn');
+16 -4
View File
@@ -1,4 +1,6 @@
<?php $is_readonly_user = $_SESSION['type'] === 'user'; ?>
<div class="row">
<?php if (!$is_readonly_user): ?>
<div class="col-12" id="bulk-action-div" style="display: none;">
<div id="err-msg"></div>
<div class="bulk-action-wrapper">
@@ -21,13 +23,16 @@
</form>
</div>
</div>
<?php endif; ?>
<div class="col-12">
<div class="card">
<div class="card-body table-responsive p-0">
<table class="table table-striped table-bordered">
<thead>
<tr>
<?php if (!$is_readonly_user): ?>
<th><input type="checkbox" name="bulk-select" value="1"></th>
<?php endif; ?>
<th>ID</th>
<th>Owner</th>
<th>Filename</th>
@@ -40,7 +45,9 @@
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<?php if (!$is_readonly_user): ?>
<td><input type="checkbox" name="action[]" value="<?=$row['id']?>" onchange="updateBulkActionVisibility()"></td>
<?php endif; ?>
<td><?php echo $row['id']; ?></td>
<td>
<?php
@@ -61,10 +68,10 @@
<td><?php echo htmlspecialchars($row['type']); ?></td>
<td><?php echo htmlspecialchars_decode($row['content']); ?></td>
<td>
<?php echo '<img src="'.SAVED_QRCODE_FOLDER.htmlspecialchars($row['qrcode']).'" width="100" height="100">'; ?>
<?php echo '<img src="qrcode_image.php?type=static&id='.$row['id'].'" width="100" height="100">'; ?>
</td>
<td>
<?php if (!$is_readonly_user): ?>
<!-- EDIT -->
<a href="static_qrcode.php?edit=true&id=<?php echo $row['id']; ?>" class="btn btn-primary"><i class="fas fa-edit"></i></a>
@@ -75,9 +82,12 @@
data-target="#delete-modal"
data-del_id="<?php echo $row["id"];?>"
><i class="fas fa-trash"></i></a>
<?php endif; ?>
<!-- DOWNLOAD -->
<a href="<?php echo SAVED_QRCODE_FOLDER.htmlspecialchars($row['qrcode']); ?>" class="btn btn-primary" download><i class="fa fa-download"></i></a>
<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; ?>
@@ -93,6 +103,7 @@
</div><!-- /.col -->
</div><!-- /.row -->
<?php if (!$is_readonly_user): ?>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog">
@@ -118,6 +129,7 @@
</div>
</div>
<!-- /.Delete Confirmation Modal -->
<?php endif; ?>
<script>
const deleteButtons = document.querySelectorAll('.delete_btn');
+39
View File
@@ -117,6 +117,45 @@ function paginationLinks($current_page, $total_pages, $base_url) {
return $html;
}
/**
* Handles the optional "icon above the qr code" upload. Validates the actual image
* type (not just the extension/mime the browser claims) and stores the file outside
* the document root, next to the generated qr codes.
*/
function qr_handle_icon_upload($fileKey = 'icon') {
if (!isset($_FILES[$fileKey]) || $_FILES[$fileKey]['error'] === UPLOAD_ERR_NO_FILE) {
return ['ok' => true, 'path' => null];
}
if ($_FILES[$fileKey]['error'] !== UPLOAD_ERR_OK) {
return ['ok' => false, 'error' => 'Icon upload failed.'];
}
if ($_FILES[$fileKey]['size'] > 1024 * 1024) {
return ['ok' => false, 'error' => 'Icon must be smaller than 1MB.'];
}
$info = @getimagesize($_FILES[$fileKey]['tmp_name']);
$allowed = [IMAGETYPE_PNG => 'png', IMAGETYPE_JPEG => 'jpg', IMAGETYPE_GIF => 'gif'];
if ($info === false || !isset($allowed[$info[2]])) {
return ['ok' => false, 'error' => 'Icon must be a PNG, JPEG or GIF image.'];
}
$dir = SAVED_QRCODE_DIRECTORY . 'icons/';
if (!is_dir($dir)) {
mkdir($dir, 0750, true);
}
$destination = $dir . bin2hex(random_bytes(16)) . '.' . $allowed[$info[2]];
if (!move_uploaded_file($_FILES[$fileKey]['tmp_name'], $destination)) {
return ['ok' => false, 'error' => 'Could not store the uploaded icon.'];
}
return ['ok' => true, 'path' => $destination];
}
function base_url() {
require_once(__DIR__ . '/../config/environment.php');
if (defined('BASE_URL') && BASE_URL !== null) {
+2 -1
View File
@@ -3,8 +3,9 @@
//If User is logged in the session['user_logged_in'] will be set to true
//if user is Not Logged in, redirect to login.php page.
if (!isset($_SESSION['user_logged_in'])) {
if (empty($_SESSION['user_logged_in'])) {
header('Location:login.php');
exit;
}
?>
+6
View File
@@ -29,3 +29,9 @@
<script src="plugins/daterangepicker/daterangepicker.js"></script>
<!-- Overlay scrollbar -->
<script type="text/javascript" src="plugins/overlayScrollbars/js/OverlayScrollbars.js"></script>
<!-- PWA service worker -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}
</script>
+3
View File
@@ -2,6 +2,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="csrf-token" content="<?php echo csrf_token(); ?>">
<meta name="theme-color" content="#007bff">
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="dist/img/icon-192.png">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
+43
View File
@@ -143,6 +143,49 @@ function qr_is_login_locked_out($username) {
return $count !== null && $count >= LOGIN_MAX_ATTEMPTS;
}
/**
* Compute the owner-scope for a freshly authenticated user row (see qr_scope_owner_id()
* below for the meaning of the returned value). Call once at login and store the result
* in $_SESSION['scope_owner_id'].
*/
function qr_compute_scope_owner_id($user_row) {
if ($user_row['type'] === 'admin') {
return (int) $user_row['id'];
}
if ($user_row['type'] === 'user' && !empty($user_row['owner_admin_id'])) {
return (int) $user_row['owner_admin_id'];
}
return null;
}
/**
* Owner-scope for the qr code lists/reports, set at login time in $_SESSION['scope_owner_id']:
* - null: full visibility (super, or a company-wide 'user' account created by super)
* - int: restricted to codes owned by this admin id (an admin's own account, or a
* 'user' account created by that admin)
*/
function qr_scope_owner_id() {
return $_SESSION['scope_owner_id'] ?? null;
}
function qr_has_full_visibility() {
return qr_scope_owner_id() === null;
}
/**
* Apply the current session's owner scope to a MysqliDb query builder in place.
* No-op when the session has full visibility.
*/
function qr_apply_owner_scope($db) {
$scope_owner_id = qr_scope_owner_id();
if ($scope_owner_id !== null) {
$db->where('id_owner', $scope_owner_id);
$db->orWhere('id_owner', NULL, 'IS');
}
}
/**
* Audit log
*/
+24
View File
@@ -33,6 +33,15 @@
</p>
</a>
</li>
<li class="nav-item">
<a href="./scan_qrcode.php" <?php echo (CURRENT_PAGE == 'scan_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="nav-icon fas fa-camera"></i>
<p>
Scan qr code
</p>
</a>
</li>
<?php if ($_SESSION['type'] !== 'user' || !empty($_SESSION['can_view_dynamic'])): ?>
<li <?php echo ((substr(CURRENT_PAGE, 0, 19) == 'dynamic_qrcodes.php') || (substr(CURRENT_PAGE, 0, 18) == 'dynamic_qrcode.php')) ? ' class="nav-item has-treeview menu-open"' : ' class="nav-item has-treeview"'; ?>>
<a href="#" <?php echo ((substr(CURRENT_PAGE, 0, 19) == 'dynamic_qrcodes.php') || (substr(CURRENT_PAGE, 0, 18) == 'dynamic_qrcode.php')) ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="nav-icon fa fa-qrcode"></i>
@@ -48,14 +57,24 @@
<p>List all</p>
</a>
</li>
<?php if ($_SESSION['type'] !== 'user'): ?>
<li class="nav-item">
<a href="./dynamic_qrcode.php" <?php echo (CURRENT_PAGE == 'dynamic_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="far fa-circle nav-icon"></i>
<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>
<?php endif; ?>
<?php if ($_SESSION['type'] !== 'user' || !empty($_SESSION['can_view_static'])): ?>
<li <?php echo ((substr(CURRENT_PAGE, 0, 18) == 'static_qrcodes.php') || (substr(CURRENT_PAGE, 0, 17) == 'static_qrcode.php')) ? ' class="nav-item has-treeview menu-open"' : ' class="nav-item has-treeview"'; ?>>
<a href="#" <?php echo ((substr(CURRENT_PAGE, 0, 18) == 'static_qrcodes.php') || (substr(CURRENT_PAGE, 0, 17) == 'static_qrcode.php')) ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="nav-icon fa fa-qrcode"></i>
@@ -71,20 +90,25 @@
<p>List all</p>
</a>
</li>
<?php if ($_SESSION['type'] !== 'user'): ?>
<li class="nav-item">
<a href="./static_qrcode.php" <?php echo (CURRENT_PAGE == 'static_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="far fa-circle nav-icon"></i>
<p>Add new</p>
</a>
</li>
<?php endif; ?>
</ul>
</li>
<?php endif; ?>
<?php if (in_array($_SESSION['type'], ['super', 'admin'], true)): ?>
<li class="nav-item">
<a href="./users.php" <?php echo ((substr(CURRENT_PAGE, 0, 15) == 'users.php') || (substr(CURRENT_PAGE, 0, 14) == 'user.php')) ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="fas fa-users nav-icon"></i>
<p>Users</p>
</a>
</li>
<?php endif; ?>
</ul>
</nav>
<!-- /.sidebar-menu -->
+11 -16
View File
@@ -4,35 +4,30 @@ require_once 'includes/auth_validate.php';
$db = getDbInstance();
// Full visibility (super, or a company-wide 'user' account) vs. scoped to one admin's
// own codes (an admin, or a 'user' account created by that admin).
$is_full_visibility = qr_has_full_visibility();
//Get Dynamic qr code rows
if($_SESSION['type'] !== 'super') {
$db->where("id_owner", $_SESSION['user_id']);
$db->orWhere ("id_owner", NULL, 'IS');
}
qr_apply_owner_scope($db);
$numQrcode_dynamic = $db->getValue("dynamic_qrcodes", "count(*)");
//Get Static qr code rows
if($_SESSION['type'] !== 'super') {
$db->where("id_owner", $_SESSION['user_id']);
$db->orWhere ("id_owner", NULL, 'IS');
}
qr_apply_owner_scope($db);
$numQrcode_static = $db->getValue("static_qrcodes", "count(*)");
$total = $numQrcode_dynamic + $numQrcode_static;
//Get Total scan
if($_SESSION['type'] !== 'super') {
$db->where("id_owner", $_SESSION['user_id']);
$db->orWhere ("id_owner", NULL, 'IS');
}
qr_apply_owner_scope($db);
$numScan = $db->getOne("dynamic_qrcodes", "sum(scan) as numScan");
/* CREATED CHART */
//I initialize the variables that will contain the daily values to 0 otherwise in the foreach loop they will be reset every time
//Get the number of DYNAMIC qr code created in 7 days and total scan
if($_SESSION['type'] !== 'super')
$createdQrcode_dynamic = $db->query("select `created_at`, `scan` from " . DATABASE_PREFIX . "dynamic_qrcodes where `created_at` > curdate()-7 AND (`id_owner`= " . $_SESSION['user_id'] . " OR `id_owner` IS NULL);");
if(!$is_full_visibility)
$createdQrcode_dynamic = $db->query("select `created_at`, `scan` from " . DATABASE_PREFIX . "dynamic_qrcodes where `created_at` > curdate()-7 AND (`id_owner`= " . (int) qr_scope_owner_id() . " OR `id_owner` IS NULL);");
else
$createdQrcode_dynamic = $db->query("select `created_at`, `scan` from ".DATABASE_PREFIX."dynamic_qrcodes where `created_at` > curdate()-7;");
@@ -54,8 +49,8 @@ foreach ($createdQrcode_dynamic as $row) {
/* SCAN CHART */
//Get the number of STATIC qr code created in 7 days
if($_SESSION['type'] !== 'super')
$createdQrcode_static = $db->query("select `created_at` from " . DATABASE_PREFIX . "static_qrcodes where `created_at` > curdate()-7 AND (`id_owner`=" . $_SESSION['user_id'] . " OR `id_owner` IS NULL);");
if(!$is_full_visibility)
$createdQrcode_static = $db->query("select `created_at` from " . DATABASE_PREFIX . "static_qrcodes where `created_at` > curdate()-7 AND (`id_owner`=" . (int) qr_scope_owner_id() . " OR `id_owner` IS NULL);");
else
$createdQrcode_static = $db->query("select `created_at` from ".DATABASE_PREFIX."static_qrcodes where `created_at` > curdate()-7;");
+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
*
+1 -1
View File
@@ -830,7 +830,7 @@ class MysqliDb
* @return bool|array Boolean indicating the insertion failed (false), else return id-array ([int])
* @throws Exception
*/
public function insertMulti($tableName, array $multiInsertData, array $dataKeys = null)
public function insertMulti($tableName, array $multiInsertData, ?array $dataKeys = null)
{
// only auto-commit our inserts, if no transaction is currently running
$autoCommit = (isset($this->_transaction_in_progress) ? !$this->_transaction_in_progress : true);
+184 -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,146 @@ 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;
}
const FRAME_FONT_DIR = '/usr/share/fonts/truetype/dejavu/';
const ALLOWED_FRAME_FONTS = [
'sans' => 'DejaVuSans.ttf',
'sans-bold' => 'DejaVuSans-Bold.ttf',
'serif' => 'DejaVuSerif.ttf',
'serif-bold' => 'DejaVuSerif-Bold.ttf',
'mono' => 'DejaVuSansMono.ttf',
'mono-bold' => 'DejaVuSansMono-Bold.ttf',
];
private static function resolveFrameFont($fontKey) {
$file = self::ALLOWED_FRAME_FONTS[$fontKey] ?? self::ALLOWED_FRAME_FONTS['sans'];
$path = self::FRAME_FONT_DIR . $file;
return is_file($path) ? $path : null;
}
/**
* 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, $fontKey = 'sans', $fontSize = 16) {
$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;
}
$fontFile = self::resolveFrameFont($fontKey);
$fontSize = min(max((int) $fontSize, 8), 60);
$source = @$loaders[$format]($path);
if ($source === false) {
return;
}
$width = imagesx($source);
$height = imagesy($source);
$padding = $fontFile !== null ? $fontSize + 20 : 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);
if ($fontFile !== null && function_exists('imagettftext')) {
$bbox = imagettfbbox($fontSize, 0, $fontFile, $text);
$text_width = abs($bbox[2] - $bbox[0]);
$text_height = abs($bbox[1] - $bbox[7]);
$x = max(0, (int) (($width - $text_width) / 2));
$y = $height + (int) (($padding + $text_height) / 2);
imagettftext($canvas, $fontSize, 0, $x, $y, $black, $fontFile, $text);
} else {
$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);
}
/**
* Renders an optional user-uploaded icon above the qr code (not embedded inside
* it, so scanability is unaffected). Only supported for raster formats via GD.
*/
private function addTopIcon($path, $format, $iconPath) {
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
if (!$iconPath || !is_file($iconPath) || !isset($loaders[$format]) || !is_file($path)) {
return;
}
$iconInfo = @getimagesize($iconPath);
$iconLoaders = [IMAGETYPE_PNG => 'imagecreatefrompng', IMAGETYPE_JPEG => 'imagecreatefromjpeg', IMAGETYPE_GIF => 'imagecreatefromgif'];
if ($iconInfo === false || !isset($iconLoaders[$iconInfo[2]])) {
return;
}
$source = @$loaders[$format]($path);
$icon = @$iconLoaders[$iconInfo[2]]($iconPath);
if ($source === false || $icon === false) {
return;
}
$width = imagesx($source);
$height = imagesy($source);
$iconWidth = imagesx($icon);
$iconHeight = imagesy($icon);
$maxIconHeight = (int) ($height * 0.25);
$scale = min($maxIconHeight / $iconHeight, ($width * 0.6) / $iconWidth, 1);
$targetWidth = max(1, (int) ($iconWidth * $scale));
$targetHeight = max(1, (int) ($iconHeight * $scale));
$margin = 15;
$topPadding = $targetHeight + ($margin * 2);
$canvas = imagecreatetruecolor($width, $height + $topPadding);
$white = imagecolorallocate($canvas, 255, 255, 255);
imagefill($canvas, 0, 0, $white);
$resizedIcon = imagecreatetruecolor($targetWidth, $targetHeight);
imagealphablending($resizedIcon, false);
imagesavealpha($resizedIcon, true);
$transparent = imagecolorallocatealpha($resizedIcon, 0, 0, 0, 127);
imagefill($resizedIcon, 0, 0, $transparent);
imagealphablending($icon, true);
imagecopyresampled($resizedIcon, $icon, 0, 0, 0, 0, $targetWidth, $targetHeight, $iconWidth, $iconHeight);
$x = (int) (($width - $targetWidth) / 2);
imagecopy($canvas, $resizedIcon, $x, $margin, 0, 0, $targetWidth, $targetHeight);
imagecopy($canvas, $source, 0, $topPadding, 0, 0, $width, $height);
$savers[$format]($canvas, $path);
imagedestroy($source);
imagedestroy($icon);
imagedestroy($resizedIcon);
imagedestroy($canvas);
}
public function getQrcode($id) {
$db = getDbInstance();
@@ -121,6 +255,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,7 +488,14 @@ class Qrcode {
}
catch(Exception $e)
{
$this->failure($e->getMessage());
throw new \RuntimeException($e->getMessage());
}
$this->addTopIcon($filename, $fileExt, $input_data['icon_tmp_path'] ?? null);
$this->addFrameText($filename, $fileExt, $input_data['frame_text'] ?? '', $input_data['frame_font'] ?? 'sans', $input_data['frame_font_size'] ?? 16);
if (!empty($input_data['icon_tmp_path'])) {
@unlink($input_data['icon_tmp_path']);
}
// If you want you can customi<e qr code with logo
@@ -334,15 +505,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 +522,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"]){
+212
View File
@@ -57,6 +57,140 @@ class Qrcode {
return $format;
}
const FRAME_FONT_DIR = '/usr/share/fonts/truetype/dejavu/';
const ALLOWED_FRAME_FONTS = [
'sans' => 'DejaVuSans.ttf',
'sans-bold' => 'DejaVuSans-Bold.ttf',
'serif' => 'DejaVuSerif.ttf',
'serif-bold' => 'DejaVuSerif-Bold.ttf',
'mono' => 'DejaVuSansMono.ttf',
'mono-bold' => 'DejaVuSansMono-Bold.ttf',
];
private static function resolveFrameFont($fontKey) {
$file = self::ALLOWED_FRAME_FONTS[$fontKey] ?? self::ALLOWED_FRAME_FONTS['sans'];
$path = self::FRAME_FONT_DIR . $file;
return is_file($path) ? $path : null;
}
/**
* 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, $fontKey = 'sans', $fontSize = 16) {
$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;
}
$fontFile = self::resolveFrameFont($fontKey);
$fontSize = min(max((int) $fontSize, 8), 60);
$source = @$loaders[$format]($path);
if ($source === false) {
return;
}
$width = imagesx($source);
$height = imagesy($source);
$padding = $fontFile !== null ? $fontSize + 20 : 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);
if ($fontFile !== null && function_exists('imagettftext')) {
$bbox = imagettfbbox($fontSize, 0, $fontFile, $text);
$text_width = abs($bbox[2] - $bbox[0]);
$text_height = abs($bbox[1] - $bbox[7]);
$x = max(0, (int) (($width - $text_width) / 2));
$y = $height + (int) (($padding + $text_height) / 2);
imagettftext($canvas, $fontSize, 0, $x, $y, $black, $fontFile, $text);
} else {
$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);
}
/**
* Renders an optional user-uploaded icon above the qr code (not embedded inside
* it, so scanability is unaffected). Only supported for raster formats via GD.
*/
private function addTopIcon($path, $format, $iconPath) {
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
if (!$iconPath || !is_file($iconPath) || !isset($loaders[$format]) || !is_file($path)) {
return;
}
$iconInfo = @getimagesize($iconPath);
$iconLoaders = [IMAGETYPE_PNG => 'imagecreatefrompng', IMAGETYPE_JPEG => 'imagecreatefromjpeg', IMAGETYPE_GIF => 'imagecreatefromgif'];
if ($iconInfo === false || !isset($iconLoaders[$iconInfo[2]])) {
return;
}
$source = @$loaders[$format]($path);
$icon = @$iconLoaders[$iconInfo[2]]($iconPath);
if ($source === false || $icon === false) {
return;
}
$width = imagesx($source);
$height = imagesy($source);
$iconWidth = imagesx($icon);
$iconHeight = imagesy($icon);
$maxIconHeight = (int) ($height * 0.25);
$scale = min($maxIconHeight / $iconHeight, ($width * 0.6) / $iconWidth, 1);
$targetWidth = max(1, (int) ($iconWidth * $scale));
$targetHeight = max(1, (int) ($iconHeight * $scale));
$margin = 15;
$topPadding = $targetHeight + ($margin * 2);
$canvas = imagecreatetruecolor($width, $height + $topPadding);
$white = imagecolorallocate($canvas, 255, 255, 255);
imagefill($canvas, 0, 0, $white);
$resizedIcon = imagecreatetruecolor($targetWidth, $targetHeight);
imagealphablending($resizedIcon, false);
imagesavealpha($resizedIcon, true);
$transparent = imagecolorallocatealpha($resizedIcon, 0, 0, 0, 127);
imagefill($resizedIcon, 0, 0, $transparent);
imagealphablending($icon, true);
imagecopyresampled($resizedIcon, $icon, 0, 0, 0, 0, $targetWidth, $targetHeight, $iconWidth, $iconHeight);
$x = (int) (($width - $targetWidth) / 2);
imagecopy($canvas, $resizedIcon, $x, $margin, 0, 0, $targetWidth, $targetHeight);
imagecopy($canvas, $source, 0, $topPadding, 0, 0, $width, $height);
$savers[$format]($canvas, $path);
imagedestroy($source);
imagedestroy($icon);
imagedestroy($resizedIcon);
imagedestroy($canvas);
}
public function getQrcode($id) {
$db = getDbInstance();
@@ -133,6 +267,13 @@ class Qrcode {
$this->failure($e->getMessage());
}
$this->addTopIcon($filename, $data_to_db['format'], $input_data['icon_tmp_path'] ?? null);
$this->addFrameText($filename, $data_to_db['format'], $input_data['frame_text'] ?? '', $input_data['frame_font'] ?? 'sans', $input_data['frame_font_size'] ?? 16);
if (!empty($input_data['icon_tmp_path'])) {
@unlink($input_data['icon_tmp_path']);
}
// If you want you can customi<e qr code with logo
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
@@ -151,6 +292,77 @@ 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->addTopIcon($path, $format, $input_data['icon_tmp_path'] ?? null);
$this->addFrameText($path, $format, $input_data['frame_text'] ?? '', $input_data['frame_font'] ?? 'sans', $input_data['frame_font_size'] ?? 16);
if (!empty($input_data['icon_tmp_path'])) {
@unlink($input_data['icon_tmp_path']);
}
$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
*
+101 -8
View File
@@ -330,22 +330,49 @@ class StaticQrcode {
/**
* create a qr code of type "bitcoin"
* @string address -> required
* @int amount -> required
* @string amount -> optional (a bitcoin address is useful as a standing QR code,
* not just for one specific payment amount)
* @string label
* @string message
*/
public function bitcoinQrcode($address, $amount, $label, $message)
{
if($address != NULL && $amount != NULL){
$this->sData = 'bitcoin:'.$address.'?amount='.$amount.'&label='.$label.'&message='.$message;
$this->sContent = '<strong>BTC address:</strong> '.$address.'<br>'.'<strong>Amount:</strong> '.$amount.'<br>';
$this->sContent .= '<strong>Label:</strong> '.$label.'<br>'.'<strong>Message:</strong> '.$message;
$address = trim((string) $address);
$amount = trim((string) $amount);
$label = trim((string) $label);
$message = trim((string) $message);
if ($address === '') {
$this->requiredFieldsError();
return;
}
$params = [];
if ($amount !== '') {
$params[] = 'amount=' . rawurlencode($amount);
}
if ($label !== '') {
$params[] = 'label=' . rawurlencode($label);
}
if ($message !== '') {
$params[] = 'message=' . rawurlencode($message);
}
$this->sData = 'bitcoin:' . $address . ($params ? '?' . implode('&', $params) : '');
$this->sContent = '<strong>BTC address:</strong> ' . $address . '<br>';
if ($amount !== '') {
$this->sContent .= '<strong>Amount:</strong> ' . $amount . '<br>';
}
if ($label !== '') {
$this->sContent .= '<strong>Label:</strong> ' . $label . '<br>';
}
if ($message !== '') {
$this->sContent .= '<strong>Message:</strong> ' . $message;
}
$this->addQrcode("bitcoin");
}
else
$this->requiredFieldsError();
}
/**
* create a qr code of type "2FA"
@@ -378,6 +405,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 +499,10 @@ class StaticQrcode {
$input_data["foreground"] = $_POST['foreground'];
$input_data["background"] = $_POST['background'];
$input_data["frame_text"] = $_POST['frame_text'] ?? '';
$input_data["frame_font"] = $_POST['frame_font'] ?? 'sans';
$input_data["frame_font_size"] = $_POST['frame_font_size'] ?? 16;
$input_data["icon_tmp_path"] = $_POST['icon_tmp_path'] ?? null;
$data_to_qrcode = urlencode($this->sData);
+69 -13
View File
@@ -3,7 +3,7 @@ require_once 'config/config.php';
class Users
{
const ALLOWED_TYPES = ['super', 'admin'];
const ALLOWED_TYPES = ['super', 'admin', 'user'];
/**
*
@@ -13,7 +13,7 @@ class Users
}
/**
* Server-side validatie van username/type. Geeft een foutmelding terug (string) of null als geldig.
* Server-side validation of username/type. Returns an error message (string) or null if valid.
*/
private function validateUsernameAndType($username, $type) {
if (!is_string($username) || strlen($username) < 3 || strlen($username) > 50) {
@@ -57,6 +57,24 @@ class Users
return $db->get(DATABASE_PREFIX.'users');
}
/**
* True if the logged-in admin is allowed to manage (edit/delete) this user record.
* Super can always manage everyone.
*/
private function canManage($target_user) {
if ($_SESSION['type'] === 'super') {
return true;
}
if ($_SESSION['type'] === 'admin') {
return $target_user !== null
&& $target_user['type'] === 'user'
&& (int) $target_user['owner_admin_id'] === (int) $_SESSION['user_id'];
}
return false;
}
public function getUser($id) {
$db = getDbInstance();
@@ -70,12 +88,27 @@ class Users
}
/**
* Add user
* Add user.
*
* A 'super' account can create any type freely (owner_admin_id stays NULL: company-wide).
* An 'admin' account can only create their own read-only 'user' accounts
* (type is forced to 'user', owner_admin_id is forced to their own id).
*/
public function addUser($input_data) {
$db = getDbInstance();
$validation_error = $this->validateUsernameAndType($input_data['username'] ?? '', $input_data['type'] ?? '');
$requested_type = $input_data['type'] ?? '';
$owner_admin_id = null;
if ($_SESSION['type'] === 'admin') {
$requested_type = 'user';
$owner_admin_id = $_SESSION['user_id'];
} elseif ($_SESSION['type'] !== 'super') {
header('HTTP/1.1 403 Forbidden', true, 403);
exit('403 Forbidden');
}
$validation_error = $this->validateUsernameAndType($input_data['username'] ?? '', $requested_type);
if ($validation_error !== null) {
$this->failure($validation_error, 'Location: user.php');
}
@@ -86,7 +119,10 @@ class Users
$data_to_db["username"] = $input_data["username"];
$data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
$data_to_db["type"] = $input_data["type"];
$data_to_db["type"] = $requested_type;
$data_to_db['owner_admin_id'] = $owner_admin_id;
$data_to_db['can_view_static'] = !empty($input_data['can_view_static']) ? 1 : 0;
$data_to_db['can_view_dynamic'] = !empty($input_data['can_view_dynamic']) ? 1 : 0;
$db->where('username', $data_to_db['username']);
$db->get('users');
@@ -103,18 +139,30 @@ class Users
}
/**
* Edit user
* Edit user.
*
* An 'admin' may only edit their own 'user' accounts (checked via canManage()) and
* cannot change the type away from 'user'. A 'super' account can edit anyone freely.
*/
public function editUser($input_data) {
$db = getDbInstance();
$db->where('id', $input_data['id']);
$target = $db->getOne('users');
if (!$this->canManage($target)) {
header('HTTP/1.1 403 Forbidden', true, 403);
exit('403 Forbidden');
}
$query_string = http_build_query(array(
'id' => $input_data["id"],
'edit' => "true",
));
$validation_error = $this->validateUsernameAndType($input_data['username'] ?? '', $input_data['type'] ?? '');
$requested_type = $_SESSION['type'] === 'admin' ? 'user' : ($input_data['type'] ?? '');
$validation_error = $this->validateUsernameAndType($input_data['username'] ?? '', $requested_type);
if ($validation_error !== null) {
$this->failure($validation_error, 'Location: user.php?'.$query_string);
}
@@ -123,6 +171,7 @@ class Users
$this->failure('Password must be at least 10 characters long.', 'Location: user.php?'.$query_string);
}
$db = getDbInstance();
$db->where('username', $input_data['username']);
$db->where('id', $input_data["id"], '!=');
$row = $db->getOne('users');
@@ -132,9 +181,11 @@ class Users
}
$data_to_db["username"] = $input_data["username"];
$data_to_db["type"] = $input_data["type"];
$data_to_db["type"] = $requested_type;
$data_to_db['can_view_static'] = !empty($input_data['can_view_static']) ? 1 : 0;
$data_to_db['can_view_dynamic'] = !empty($input_data['can_view_dynamic']) ? 1 : 0;
// Alleen wachtwoord overschrijven als er een nieuwe waarde is opgegeven.
// Only overwrite the password if a new value was submitted.
if (!empty($input_data['password'])) {
$data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
}
@@ -150,13 +201,18 @@ class Users
}
/**
* Delete user
* Delete user.
*
* An 'admin' may only delete their own 'user' accounts; 'super' can delete anyone.
*/
public function deleteUser($id) {
if($_SESSION['type']!='super'){
header('HTTP/1.1 401 Unauthorized', true, 401);
exit("401 Unauthorized");
$db = getDbInstance();
$db->where('id', $id);
$target = $db->getOne('users');
if (!$this->canManage($target)) {
header('HTTP/1.1 403 Forbidden', true, 403);
exit('403 Forbidden');
}
$db = getDbInstance();
+3
View File
@@ -40,6 +40,9 @@ if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token']))
$_SESSION['type'] = $row['type'];
$_SESSION['username'] = $row['username'];
$_SESSION['must_change_password'] = !empty($row['must_change_password']);
$_SESSION['can_view_static'] = !empty($row['can_view_static']);
$_SESSION['can_view_dynamic'] = !empty($row['can_view_dynamic']);
$_SESSION['scope_owner_id'] = qr_compute_scope_owner_id($row);
$_SESSION['last_activity'] = time();
audit_log('login_success_remember');
+14
View File
@@ -0,0 +1,14 @@
{
"name": "Qrcode Generator",
"short_name": "QRcode",
"description": "Self-hosted static and dynamic qr code generator",
"start_url": "index.php",
"scope": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007bff",
"icons": [
{ "src": "dist/img/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "dist/img/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* AJAX endpoint for saved color/style presets (Fase 3, priority 2). Presets are
* personal: scoped to the logged-in user's own id, never shared across accounts.
*/
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
header('Content-Type: application/json');
$action = $_GET['action'] ?? $_POST['action'] ?? '';
if ($action === 'list') {
$db = getDbInstance();
$db->where('user_id', $_SESSION['user_id']);
$db->orderBy('name', 'ASC');
$presets = $db->get('qr_presets', null, ['id', 'name', 'foreground', 'background', 'level', 'size']);
echo json_encode(['status' => 200, 'data' => $presets]);
exit;
}
if ($action === 'save') {
csrf_verify_header_or_die();
$name = trim((string) ($_POST['name'] ?? ''));
$foreground = trim((string) ($_POST['foreground'] ?? ''));
$background = trim((string) ($_POST['background'] ?? ''));
$level = $_POST['level'] ?? 'L';
$size = filter_var($_POST['size'] ?? 200, FILTER_VALIDATE_INT);
if ($name === '' || strlen($name) > 50) {
echo json_encode(['status' => 400, 'data' => 'Preset name must be between 1 and 50 characters.']);
exit;
}
if (!preg_match('/^#?[0-9a-fA-F]{6}$/', $foreground) || !preg_match('/^#?[0-9a-fA-F]{6}$/', $background)) {
echo json_encode(['status' => 400, 'data' => 'Foreground/background must be valid hex colors.']);
exit;
}
if (!in_array($level, ['L', 'M', 'Q', 'H'], true)) {
$level = 'L';
}
if ($size === false) {
$size = 200;
}
$db = getDbInstance();
$last_id = $db->insert('qr_presets', [
'user_id' => $_SESSION['user_id'],
'name' => $name,
'foreground' => $foreground,
'background' => $background,
'level' => $level,
'size' => $size,
'created_at' => date('Y-m-d H:i:s'),
]);
if (!$last_id) {
echo json_encode(['status' => 500, 'data' => 'Could not save preset.']);
exit;
}
echo json_encode(['status' => 200, 'data' => ['id' => $last_id, 'name' => $name]]);
exit;
}
if ($action === 'delete') {
csrf_verify_header_or_die();
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
if (!$id) {
echo json_encode(['status' => 400, 'data' => 'Invalid preset id.']);
exit;
}
$db = getDbInstance();
$db->where('id', $id);
$db->where('user_id', $_SESSION['user_id']);
$deleted = $db->delete('qr_presets');
echo json_encode(['status' => $deleted ? 200 : 404, 'data' => $deleted ? 'Deleted' : 'Preset not found']);
exit;
}
echo json_encode(['status' => 400, 'data' => 'Unknown action']);
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* Authenticated view/download endpoint for a generated qr code image.
* Replaces the previous direct static URL under saved_qrcode/, which any visitor
* could reach without logging in. Enforces the same visibility rules as the list
* pages (dynamic_qrcodes.php / static_qrcodes.php).
*/
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
$type = $_GET['type'] ?? '';
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if (!in_array($type, ['static', 'dynamic'], true) || !$id) {
http_response_code(404);
exit('Not found');
}
if ($_SESSION['type'] === 'user') {
$view_flag = $type === 'dynamic' ? 'can_view_dynamic' : 'can_view_static';
if (empty($_SESSION[$view_flag] ?? null)) {
http_response_code(403);
exit('Forbidden');
}
}
$db = getDbInstance();
$db->where('id', $id);
qr_apply_owner_scope($db);
$row = $db->getOne("{$type}_qrcodes");
if ($row === null) {
http_response_code(404);
exit('Not found');
}
$path = SAVED_QRCODE_DIRECTORY . $row['qrcode'];
if (!is_file($path)) {
http_response_code(404);
exit('Not found');
}
$mime_types = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
'eps' => 'application/postscript',
];
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$mime = $mime_types[$extension] ?? 'application/octet-stream';
$is_download = isset($_GET['download']) && $_GET['download'] === '1';
header('Content-Type: ' . $mime);
header('Content-Length: ' . filesize($path));
header('Cache-Control: private, max-age=0, no-cache');
header('Content-Disposition: ' . ($is_download ? 'attachment' : 'inline') . '; filename="' . basename($path) . '"');
readfile($path);
exit;
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* Authenticated download endpoint for a bulk-export zip generated by bulk_action.php.
* The zip's contents were already permission-filtered when it was built, so this only
* requires a valid session plus proof that this specific file was generated for it.
*/
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
$file = basename($_GET['file'] ?? '');
if (!preg_match('/^qrcodes_[0-9a-f]+\.zip$/', $file)) {
http_response_code(404);
exit('Not found');
}
if (empty($_SESSION['generated_zips']) || !in_array($file, $_SESSION['generated_zips'], true)) {
http_response_code(403);
exit('Forbidden');
}
$path = SAVED_QRCODE_DIRECTORY . 'zip/' . $file;
if (!is_file($path)) {
http_response_code(404);
exit('Not found');
}
header('Content-Type: application/zip');
header('Content-Length: ' . filesize($path));
header('Content-Disposition: attachment; filename="' . $file . '"');
readfile($path);
exit;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

View File
+148
View File
@@ -0,0 +1,148 @@
<?php
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
?>
<!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">Scan a qr code</h1>
</div><!-- /.col -->
</div><!-- /.row -->
</div><!-- /.container-fluid -->
</div>
<!-- /.content-header -->
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Scan from camera or upload an image</h3>
</div>
<div class="card-body">
<p class="text-muted">Decoding happens entirely in your browser - no image is uploaded to the server.</p>
<button type="button" id="start_camera_btn" class="btn btn-primary mb-3">
<i class="fa fa-camera"></i> Start camera
</button>
<button type="button" id="stop_camera_btn" class="btn btn-secondary mb-3" style="display:none;">
<i class="fa fa-stop"></i> Stop camera
</button>
<div id="camera_reader" style="max-width: 500px;"></div>
<div class="form-group mt-3">
<label for="qr_image_input">...or upload an image</label>
<input type="file" id="qr_image_input" accept="image/*" class="form-control">
</div>
<div id="scan_result_wrapper" class="mt-3" style="display:none;">
<label>Decoded content</label>
<div class="input-group">
<input type="text" id="scan_result" class="form-control" readonly>
<div class="input-group-append">
<button type="button" id="copy_result_btn" class="btn btn-outline-secondary"><i class="fa fa-copy"></i></button>
</div>
</div>
</div>
</div>
</div>
</div><!--/. container-fluid -->
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
<!-- Footer and scripts -->
<?php include './includes/footer.php'; ?>
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<script>
(function () {
var resultInput = document.getElementById('scan_result');
var resultWrapper = document.getElementById('scan_result_wrapper');
var html5QrCode = new Html5Qrcode('camera_reader');
var cameraRunning = false;
function showResult(text) {
resultInput.value = text;
resultWrapper.style.display = '';
}
document.getElementById('start_camera_btn').addEventListener('click', function () {
var startBtn = this;
var stopBtn = document.getElementById('stop_camera_btn');
html5QrCode.start(
{ facingMode: 'environment' },
{ fps: 10, qrbox: 250 },
function (decodedText) {
showResult(decodedText);
}
).then(function () {
cameraRunning = true;
startBtn.style.display = 'none';
stopBtn.style.display = '';
}).catch(function (err) {
alert('Could not start the camera: ' + err);
});
});
document.getElementById('stop_camera_btn').addEventListener('click', function () {
var startBtn = document.getElementById('start_camera_btn');
var stopBtn = this;
if (!cameraRunning) {
return;
}
html5QrCode.stop().then(function () {
cameraRunning = false;
startBtn.style.display = '';
stopBtn.style.display = 'none';
});
});
document.getElementById('qr_image_input').addEventListener('change', function (event) {
var file = event.target.files[0];
if (!file) {
return;
}
html5QrCode.scanFile(file, false)
.then(function (decodedText) {
showResult(decodedText);
})
.catch(function (err) {
alert('Could not find a qr code in this image: ' + err);
});
});
document.getElementById('copy_result_btn').addEventListener('click', function () {
navigator.clipboard.writeText(resultInput.value).catch(function () {
resultInput.select();
document.execCommand('copy');
});
});
})();
</script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
// Minimal service worker: only makes the app installable and caches truly static
// assets. Deliberately never caches PHP pages or the presets/bulk_action/qrcode_image
// endpoints - those carry session-specific and CSRF-sensitive content.
const CACHE_NAME = 'qrcode-static-v1';
const STATIC_ASSET_PATTERN = /\.(css|js|png|jpg|jpeg|svg|gif|woff2?|ttf)$/;
self.addEventListener('install', function (event) {
self.skipWaiting();
});
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', function (event) {
const url = new URL(event.request.url);
if (event.request.method !== 'GET' || url.origin !== self.location.origin || !STATIC_ASSET_PATTERN.test(url.pathname)) {
return;
}
event.respondWith(
caches.open(CACHE_NAME).then(function (cache) {
return cache.match(event.request).then(function (cached) {
const fetchPromise = fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
});
return cached || fetchPromise;
});
})
);
});
+21 -1
View File
@@ -3,6 +3,12 @@ require_once 'includes/bootstrap.php';
require_once BASE_PATH.'/includes/auth_validate.php';
require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php';
if ($_SESSION['type'] === 'user') {
$_SESSION['failure'] = 'The "user" role is read-only and cannot create, edit or delete qr codes.';
header('Location: index.php');
exit;
}
$static_qrcode_instance = new StaticQrcode();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@@ -28,7 +34,15 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["edit"])) {
$static_qrcode_instance->editQrcode($_POST);
}
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"]) && !isset($_POST["del_id"])) {
$icon_upload = qr_handle_icon_upload('icon');
if (!$icon_upload['ok']) {
$_SESSION['failure'] = $icon_upload['error'];
header('Location: ' . basename(__FILE__) . '?type=' . urlencode($_GET['type'] ?? ''));
exit;
}
$_POST['icon_tmp_path'] = $icon_upload['path'];
switch($_GET['type']){
case 'text': $static_qrcode_instance->textQrcode($_POST['text']);
break;
@@ -71,6 +85,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;
}
}
?>
+11 -4
View File
@@ -3,6 +3,12 @@ require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php';
if ($_SESSION['type'] === 'user' && empty($_SESSION['can_view_static'] ?? null)) {
$_SESSION['failure'] = 'You are not allowed to view static qr codes.';
header('Location: index.php');
exit;
}
$db = getDbInstance();
$static_qrcode = new StaticQrcode();
@@ -12,10 +18,9 @@ require_once BASE_PATH . '/includes/search_order.php';
$page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 1;
$db->pageLimit = 15;
if($_SESSION['type'] !== 'super') {
$db->where("id_owner", $_SESSION['user_id']);
$db->orWhere ("id_owner", NULL, 'IS');
}
// Scoped to one admin's own codes for an admin (or a 'user' created by that admin);
// full visibility for super and company-wide 'user' accounts.
qr_apply_owner_scope($db);
$rows = $db->arraybuilder()->paginate('static_qrcodes', $page, $select);
$total_pages = $db->totalPages;
@@ -49,6 +54,7 @@ $total_pages = $db->totalPages;
<h1 class="m-0 text-dark">Static Qr codes</h1>
</div><!-- /.col -->
<?php if ($_SESSION['type'] !== 'user'): ?>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item">
@@ -56,6 +62,7 @@ $total_pages = $db->totalPages;
</li>
</ol>
</div><!-- /.col -->
<?php endif; ?>
</div><!-- /.row -->
</div><!-- /.container-fluid -->
</div><!-- /.content-header -->
+9 -2
View File
@@ -5,8 +5,8 @@ require_once BASE_PATH . '/lib/Users/Users.php';
$user_instance = new Users();
if ($_SESSION['type'] !== 'super')
$user_instance->failure('Only a "super admin" account can access the admin listing page', 'Location: index.php');
if (!in_array($_SESSION['type'], ['super', 'admin'], true))
$user_instance->failure('Only "super admin" and "admin" accounts can access the user management page', 'Location: index.php');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
@@ -16,6 +16,13 @@ $edit = false;
if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) {
$edit = true;
$user = $user_instance->getUser($_GET["id"]);
// An admin may only open the edit form for their own 'user' accounts.
if ($_SESSION['type'] === 'admin' && (
$user['type'] !== 'user' || (int) $user['owner_admin_id'] !== (int) $_SESSION['user_id']
)) {
$user_instance->failure('You are not allowed to edit this user', 'Location: users.php');
}
}
if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["del_id"])) {
+9 -2
View File
@@ -6,14 +6,21 @@ require_once BASE_PATH . '/lib/Users/Users.php';
$db = getDbInstance();
$users = new Users();
if ($_SESSION['type'] !== 'super')
$users->failure('Only a "super admin" account can access the admin listing page', 'Location: index.php');
if (!in_array($_SESSION['type'], ['super', 'admin'], true))
$users->failure('Only "super admin" and "admin" accounts can access the user management page', 'Location: index.php');
$select = array('id', 'username', 'type');
$search_fields = array('username');
require_once BASE_PATH . '/includes/search_order.php';
$page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 1;
$db->pageLimit = 15;
// An admin only sees the read-only 'user' accounts they created themselves.
if ($_SESSION['type'] === 'admin') {
$db->where('owner_admin_id', $_SESSION['user_id']);
$db->where('type', 'user');
}
$rows = $db->arraybuilder()->paginate('users', $page, $select);
$total_pages = $db->totalPages;
?>