156 Commits

Author SHA1 Message Date
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
111 changed files with 478 additions and 3732 deletions
-15
View File
@@ -1,15 +0,0 @@
# Copy to .env and adjust the values. .env is not committed (see .gitignore).
TYPE=docker
QRCODE_GENERATOR=internal-chillerlan.qrcode
BASE_URL=http://localhost
DATABASE_HOST=qrforge-db
DATABASE_PORT=3306
DATABASE_NAME=qrcode
DATABASE_USER=qrcode
DATABASE_PASSWORD=change-me-to-a-strong-password
DATABASE_PREFIX=
DATABASE_CHARSET=utf8
MYSQL_ROOT_PASSWORD=change-me-to-a-strong-root-password
-1
View File
@@ -2,4 +2,3 @@
.project .project
.idea .idea
.DS_Store .DS_Store
.env
+7 -11
View File
@@ -1,4 +1,4 @@
FROM php:8.4 FROM php:8.3
RUN if [ "$(grep '^VERSION_ID=' /etc/os-release | cut -d '=' -f 2 | tr -d '"')" -eq "9" ]; then \ 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' \ sed -i -e 's/deb.debian.org/archive.debian.org/g' \
@@ -51,12 +51,16 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update -q \
sockets \ sockets \
xsl \ xsl \
zip \ zip \
imagick \
" \ " \
&& case "$PHP_VERSION" in \ && case "$PHP_VERSION" in \
5.6.*) PHP_EXTENSIONS="$PHP_EXTENSIONS mcrypt mysql";; \ 5.6.*) PHP_EXTENSIONS="$PHP_EXTENSIONS mcrypt mysql";; \
7.0.*|7.1.*) PHP_EXTENSIONS="$PHP_EXTENSIONS mcrypt";; \ 7.0.*|7.1.*) PHP_EXTENSIONS="$PHP_EXTENSIONS mcrypt";; \
esac \ 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 \ && install-php-extensions $PHP_EXTENSIONS \
&& if command -v a2enmod; then a2enmod rewrite; fi && if command -v a2enmod; then a2enmod rewrite; fi
@@ -82,10 +86,7 @@ RUN docker-php-ext-install sockets && docker-php-ext-enable sockets
RUN mkdir -p /opt && chmod 777 /opt RUN mkdir -p /opt && chmod 777 /opt
WORKDIR /opt WORKDIR /opt
# Pinned to a specific release tag instead of an unpinned clone of master, which is a RUN git clone https://github.com/chillerlan/php-qrcode.git \
# 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 && chmod -R 777 ./php-qrcode
RUN cp ./php-qrcode/composer.json /var/www/html/composer.json RUN cp ./php-qrcode/composer.json /var/www/html/composer.json
RUN mkdir -p /var/www/html/test && chmod 777 /var/www/html/test RUN mkdir -p /var/www/html/test && chmod 777 /var/www/html/test
@@ -96,10 +97,5 @@ WORKDIR /var/www/html
RUN composer update RUN composer update
COPY ./src ./ COPY ./src ./
RUN chmod 755 *; 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 EXPOSE 80
CMD ["php", "-S", "0.0.0.0:80"] CMD ["php", "-S", "0.0.0.0:80"]
-66
View File
@@ -1,66 +0,0 @@
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' \
-e 's/security.debian.org/archive.debian.org/g' \
-e '/stretch-updates/d' /etc/apt/sources.list; \
fi
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
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 \
libfreetype6-dev \
zip unzip \
&& install-php-extensions \
gd \
gettext \
imagick \
intl \
mysqli \
opcache \
pdo_mysql \
sockets \
zip
# Install Composer.
ENV PATH=$PATH:/root/composer/vendor/bin \
COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_HOME=/root/composer
RUN cd /opt \
&& curl -sSL https://getcomposer.org/installer > composer-setup.php \
&& curl -sSL https://composer.github.io/installer.sha384sum > composer-setup.sha384sum \
&& sha384sum --check composer-setup.sha384sum \
&& php composer-setup.php --install-dir=/usr/local/bin --filename=composer --2 \
&& rm /opt/composer-setup.php /opt/composer-setup.sha384sum
RUN mkdir -p /opt && chmod 777 /opt
WORKDIR /opt
# 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/
WORKDIR /var/www/html
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 {} \;
# 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"]
-14
View File
@@ -1,14 +0,0 @@
FROM nginx:1.27-alpine
# The php-dynamic-qrcode container builds the full app (incl. composer/vendor) into its
# own image at /var/www/html. nginx runs as a separate container and has no access to
# that filesystem, so it needs its own copy of just the static assets it serves directly
# via try_files - everything else (*.php) is proxied to php-fpm regardless. Without this,
# every static asset request (CSS/JS/manifest) falls through nginx's try_files to
# index.php, which requires a login and silently redirects there instead of serving the
# file.
COPY src/dist /var/www/html/dist
COPY src/plugins /var/www/html/plugins
COPY src/manifest.json /var/www/html/manifest.json
COPY src/service-worker.js /var/www/html/service-worker.js
COPY src/favicon.ico /var/www/html/favicon.ico
-1
View File
@@ -1,7 +1,6 @@
MIT License MIT License
Copyright (c) 2020 Giandonato Inverso Copyright (c) 2020 Giandonato Inverso
Copyright (c) 2026 Dillard Blom
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+58 -61
View File
@@ -1,70 +1,67 @@
<p align="center"><img src="src/dist/img/brand/logo.svg" alt="QRForge" width="320"></p> <p align="center"><img src="https://www.giandonatoinverso.it/qrcode/dist/img/DynamicQRCode_Original.png"></p>
<p align="center"><strong>Self-hosted, open-source QR code generator.</strong></p> **PHP Dynamic Qr code** is a script that allows the generation and saving of dynamic and static QR codes. It has a clean, responsive, and user-friendly design. It is based on [AdminLte](https://adminlte.io/), the "Best open source admin dashboard & control panel theme. Built on top of Bootstrap" and [Core PHP Admin Panel](https://github.com/chetans9/core-php-admin-panel), a simple Admin Panel written in core PHP that contains an implementation of general features you might need in your website admin panel like: record management (CRUD), secure authentication, pagination, filters.
**QRForge** creates and manages static and dynamic QR codes from a clean, [LIVE DEMO](https://qrcode.giandonatoinverso.dev/)
responsive control panel. It's a security-hardened, actively maintained fork
of the original [PHP Dynamic Qr code](https://github.com/giandonatoinverso/PHP-Dynamic-Qr-code)
project by Giandonato Inverso, built on [AdminLTE](https://adminlte.io/).
- **Try it free:** [qr.ensembia.com](https://qr.ensembia.com) - fully functional OSS test username: admin
instance. Self-service signup isn't live yet, so log in with the temporary shared demo
account `admin` / `admin` in the meantime. password: admin
- **Commercial VIP edition** (self-service create-rights, logo-embedded QR codes):
[www.qrforge.eu](https://www.qrforge.eu). [DOCUMENTATION](https://giandonatoinverso.it/qrcode/documentation)
- **Self-host it yourself:** this repository, MIT-licensed.
[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UEYVHYZYCGYYN)
# Features # Features
- Dynamic QR codes with a database-backed URL shortener - **#1 Dynamic Qr code generator on GitHub with a database to store Qr codes**
- Create, edit, delete, enable/disable the redirect - Create unlimited Qr codes
- Download any time, bulk download/delete - Docker compose support
- Batch-generate from a CSV file - Control panel with 2 access levels
- 16 static QR code types: Text, Email, Phone, SMS, WhatsApp, Skype, Location, - Multi-account
vCard, Event/calendar, Bookmark, WiFi (incl. WPA3), PayPal, Bitcoin, 2FA, - Dashboard with advanced statistics on Qr codes created and on scans
App Link (Android intent / universal links), Bluetooth - Bulk download, bulk delete
- QR code styling: 6 export formats, foreground/background color, 4 precision - Dynamic Qr code
levels, 10 sizes, optional label text below the code (custom font + size), - Create, modify, and delete Qr codes
optional icon shown above the code, save/load your own style presets - You can download your Qr codes when you want
- Address search (OpenStreetMap Nominatim) for Location QR codes - URL shortener with redirect
- Built-in QR scanner (camera or image upload, decodes entirely client-side) - Enable or disable the link redirect
- Installable as a PWA - Static Qr code
- Role-based access: `super` (full access + user management), `admin` - Text QR Code
(scoped to their own codes and sub-users), `user` (read-only, with - Email QR Code
optional per-account create rights and view toggles set by an admin) - Phone QR Code
- Dashboard with QR/scan statistics and a 7-day activity chart - Sms QR Code
- CSRF protection, login rate limiting, session hardening, audit log - Whatsapp QR Code
- Docker Compose setup, both a dev image and a production Nginx + PHP-FPM image - Skype QR Code
- Location QR Code
- Vcard QR Code
- Event/calendar QR Code
- Bookmark QR Code
- Wifi QR Code
- Paypal QR Code
- Bitcoin QR Code
- Customization of Qr codes
- 6 formats for images
- Foreground color
- Background color
- 4 levels of precision
- 10 sizes
- Responsive bootstrap-based design
- Easy to understand and expand code
- Full OOP with classes and well-documented
# What is included ## What is included
- PHP 8.4 application source - PHP files
- Database schema + migrations - .sql file with sample data
- Docker Compose files (dev and production) - JS files
- CSS/JS assets - CSS files
- Docker compose file
# Setup with Docker Compose ## Setup with docker compose
1. download docker-compose.yml file
1. Clone this repository. 2. Start docker stack
2. Copy `.env.example` to `.env` and set a real `DATABASE_PASSWORD` / ```bash
`MYSQL_ROOT_PASSWORD`. docker compose build --no-cache && docker compose up -d
3. Start the stack: ```
```bash 3. Open your browser at http://localhost:80 and login with (username: superadmin, password: superadmin)
docker compose up -d --build
```
4. Open `http://localhost` and log in with `superadmin` / `superadmin`. You'll
be required to set a new password on first login.
For a production deployment behind a reverse proxy, use
`docker-compose.prod.yml` (Nginx + PHP-FPM) instead of the dev stack.
# Credits
- Originally forked from [PHP Dynamic Qr code](https://github.com/giandonatoinverso/PHP-Dynamic-Qr-code)
by Giandonato Inverso.
- QR code rendering powered by [chillerlan/php-qrcode](https://github.com/chillerlan/php-qrcode).
- Admin panel UI built on [AdminLTE](https://adminlte.io/).
# License
MIT - see [LICENSE](LICENSE).
-108
View File
@@ -1,108 +0,0 @@
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
CREATE TABLE IF NOT EXISTS `users` (
`id` int(25) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`series_id` varchar(60) DEFAULT NULL,
`remember_token` varchar(255) DEFAULT NULL,
`expires` datetime DEFAULT NULL,
`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 ;
-- Default super admin account. Credentials: superadmin / superadmin
-- must_change_password=1 forces a password change on first login (see Fase 1 hardening).
INSERT INTO `users` (`id`, `username`, `password`, `series_id`, `remember_token`, `expires`, `type`, `must_change_password`, `password_changed_at`) VALUES
(1, 'superadmin', '$2y$10$xpZc5KC.aU2XHkcqhuZGFuAnqmtL4Unt8MysOyylceq.19XIyoZpG', NULL, NULL, NULL, 'super', 1, NULL);
CREATE TABLE IF NOT EXISTS `dynamic_qrcodes` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`id_owner` int(25) NULL DEFAULT NULL,
`filename` varchar(45) NOT NULL,
`format` varchar(45) DEFAULT NULL,
`identifier` longtext,
`link` varchar(500) DEFAULT NULL,
`qrcode` varchar(60) DEFAULT NULL,
`scan` int(11) NOT NULL DEFAULT '0',
`state` varchar(20) NOT NULL DEFAULT 'enable',
`created_by` int(10) unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_by` int(10) unsigned NOT NULL DEFAULT '0',
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ;
CREATE TABLE IF NOT EXISTS `static_qrcodes` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`id_owner` int(25) NULL DEFAULT NULL,
`filename` varchar(45) CHARACTER SET utf8 NOT NULL,
`format` varchar(45) DEFAULT NULL,
`type` varchar(45) CHARACTER SET utf8 DEFAULT NULL,
`content` mediumtext CHARACTER SET utf8,
`qrcode` varchar(60) CHARACTER SET utf8 DEFAULT NULL,
`state` varchar(20) CHARACTER SET utf8 NOT NULL DEFAULT 'enable',
`created_by` int(10) unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_by` int(10) unsigned NOT NULL DEFAULT '0',
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;
-- 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,
`ip_address` varchar(45) NOT NULL,
`success` tinyint(1) NOT NULL DEFAULT 0,
`attempted_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `username_attempted_at` (`username`, `attempted_at`),
KEY `ip_attempted_at` (`ip_address`, `attempted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 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,
`username` varchar(50) DEFAULT NULL,
`action` varchar(50) NOT NULL,
`target_type` varchar(30) DEFAULT NULL,
`target_id` varchar(50) DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `created_at` (`created_at`),
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 */;
-57
View File
@@ -1,57 +0,0 @@
-- 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();
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'must_change_password'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `must_change_password` 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 = 'password_changed_at'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `password_changed_at` DATETIME DEFAULT NULL',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 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'
AND `password` = '$2y$10$xpZc5KC.aU2XHkcqhuZGFuAnqmtL4Unt8MysOyylceq.19XIyoZpG';
CREATE TABLE IF NOT EXISTS `login_attempts` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`ip_address` varchar(45) NOT NULL,
`success` tinyint(1) NOT NULL DEFAULT 0,
`attempted_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `username_attempted_at` (`username`, `attempted_at`),
KEY `ip_attempted_at` (`ip_address`, `attempted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `audit_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(25) DEFAULT NULL,
`username` varchar(50) DEFAULT NULL,
`action` varchar(50) NOT NULL,
`target_type` varchar(30) DEFAULT NULL,
`target_id` varchar(50) DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `created_at` (`created_at`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-22
View File
@@ -1,22 +0,0 @@
-- 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
@@ -1,14 +0,0 @@
-- 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
@@ -1,14 +0,0 @@
-- 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;
-67
View File
@@ -1,67 +0,0 @@
services:
nginx:
build:
context: .
dockerfile: Dockerfile.nginx
restart: "unless-stopped"
ports:
- "80:80"
# 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
depends_on:
- qrforge-app
networks:
- qrforge-network
qrforge-app:
build:
context: .
dockerfile: Dockerfile.fpm
restart: "unless-stopped"
environment:
TYPE: "docker"
QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}"
BASE_URL: "${BASE_URL:?set BASE_URL in .env, e.g. https://qr.ensembia.com}"
DATABASE_HOST: "qrforge-db"
DATABASE_PORT: "3306"
DATABASE_NAME: "${DATABASE_NAME:-qrcode}"
DATABASE_USER: "${DATABASE_USER:-qrcode}"
DATABASE_PASSWORD: "${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}"
DATABASE_PREFIX: "${DATABASE_PREFIX:-}"
DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}"
depends_on:
qrforge-db:
condition: service_healthy
volumes:
- qrforge_qrcode_storage:/var/www/qrcode-storage
networks:
- qrforge-network
qrforge-db:
image: "mysql:8.0"
restart: "unless-stopped"
volumes:
- qrforge_db_data:/var/lib/mysql
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
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:?set DATABASE_PASSWORD in .env}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 10
networks:
- qrforge-network
volumes:
qrforge_db_data:
qrforge_qrcode_storage:
networks:
qrforge-network:
driver: bridge
+30 -35
View File
@@ -1,53 +1,48 @@
version: "3.2"
services: services:
qrforge-app: php-dynamic-qrcode:
build: image: "giandonatoinverso/php-dynamic-qr-code:latest"
context: .
dockerfile: Dockerfile
restart: "unless-stopped" restart: "unless-stopped"
environment: environment:
TYPE: "${TYPE:-docker}" TYPE: "docker"
QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}" QRCODE_GENERATOR: "internal-chillerlan.qrcode"
BASE_URL: "${BASE_URL:-http://localhost}" BASE_URL: "https://mydomain.com"
DATABASE_HOST: "qrforge-db" DATABASE_HOST: "php-dynamic-qrcode-db"
DATABASE_PORT: "3306" DATABASE_PORT: "3306"
DATABASE_NAME: "${DATABASE_NAME:-qrcode}" DATABASE_NAME: "qrcode"
DATABASE_USER: "${DATABASE_USER:-qrcode}" DATABASE_USER: "qrcode"
DATABASE_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}" DATABASE_PASSWORD: "changeme"
DATABASE_PREFIX: "${DATABASE_PREFIX:-}" DATABASE_PREFIX: ""
DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}" DATABASE_CHARSET: "utf8"
ports: ports:
- "80:80" - 80:80
depends_on: depends_on:
qrforge-db: - php-dynamic-qrcode-db
condition: service_healthy
volumes: volumes:
- qrforge_qrcode_storage:/var/www/qrcode-storage - php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode
networks: networks:
- qrforge-network - php-dynamic-qrcode-network
qrforge-db: php-dynamic-qrcode-db:
image: "mysql:8.0" image: "giandonatoinverso/php-dynamic-qr-code-db:latest"
restart: "unless-stopped" restart: "unless-stopped"
volumes: volumes:
- qrforge_db_data:/var/lib/mysql - php_dynamic_qrcode_db_data:/var/lib/mysql
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro ports:
- '13306:3306'
environment: environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?zet MYSQL_ROOT_PASSWORD in .env}" MYSQL_ROOT_PASSWORD: "changeme"
MYSQL_DATABASE: "${DATABASE_NAME:-qrcode}" MYSQL_DATABASE: "qrcode"
MYSQL_USER: "${DATABASE_USER:-qrcode}" MYSQL_USER: "qrcode"
MYSQL_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}" MYSQL_PASSWORD: "changeme"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 10
networks: networks:
- qrforge-network - php-dynamic-qrcode-network
volumes: volumes:
qrforge_db_data: php_dynamic_qrcode_db_data:
qrforge_qrcode_storage: php_dynamic_qrcode_config_data:
php_dynamic_qrcode_saved_qrcode_data:
networks: networks:
qrforge-network: php-dynamic-qrcode-network:
driver: bridge driver: bridge
+21
View File
@@ -267,6 +267,27 @@ The first shows a weekly report of the number of qr codes created (dynamic and s
You can also customize the redirect page and increase the timer You can also customize the redirect page and increase the timer
</p> </p>
</div> </div>
<div id="extra">
<h2>EXTRA</h2>
<p>My script is in constant development and I hope to expand it from time to time with more and more useful features, so stay tuned for the updates.<br> With the first version, in the classes that realize the 2 types of qr code, an additional method not mentioned in the above documentation called <strong>addLogo()</strong> is included.
To add this functionality you need to delete the comment characters inside the class and add the input fields to the forms for the user to upload the logo.
However, this feature is not recommended as it can cause different QR code scanning errors depending on the scanner applications.
</p>
</div>
<div id="support">
<h2>Support</h2>
<div class="wrapper">
<div class="alert alert-success alert-dismissible" role="alert">
If you have any question please feel free to email me at <strong><a href="mailto:hello@giandonatoinverso.dev?Subject=Dynamic%20Qrcode" target="_top">hello@giandonatoinverso.dev</a></strong>
</div>
<p>Please don't forget to rate my script on GitHub.
<br>
<br>Thank You, <b><br><br> Giandonato Inverso</b></p>
</div>
</div> </div>
</div> </div>
<!-- Main Panel End --> <!-- Main Panel End -->
-30
View File
@@ -1,30 +0,0 @@
server {
listen 80;
server_name _;
root /var/www/html;
index index.php;
client_max_body_size 20m;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "same-origin" always;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass qrforge-app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# 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;
}
}
-102
View File
@@ -1,102 +0,0 @@
<?php
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
?>
<!DOCTYPE html>
<html lang="en">
<title>About - QRForge</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">About</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-body text-center">
<img src="dist/img/brand/logo-stacked.svg" alt="QRForge" style="max-width: 220px; margin: 20px 0;">
<p class="text-muted">Self-hosted static and dynamic QR code generator. Version 3.0.</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Commercial version</h3>
</div>
<div class="card-body">
<p>
This is the free, open-source (MIT) edition of QRForge. It runs unmodified
as a live, fully functional try-out at
<a href="https://qr.ensembia.com" target="_blank">qr.ensembia.com</a> -
self-service signup isn't live yet, so log in with the temporary shared
demo account <code>admin</code> / <code>admin</code> in the meantime.
</p>
<p>
The commercial VIP edition (paid create-rights and logo-embedded QR codes)
is a separate product built on the same OSS core - see
<a href="https://www.qrforge.eu" target="_blank">www.qrforge.eu</a> for
pricing and details.
</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Self-hosting</h3>
</div>
<div class="card-body">
<p>
Prefer to run your own instance? QRForge is MIT-licensed and available on
GitHub:
<a href="https://github.com/dillardblom/QRForge-selfhosted" target="_blank">github.com/dillardblom/QRForge-selfhosted</a>.
</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Credits</h3>
</div>
<div class="card-body">
<p>
Originally forked from
<a href="https://github.com/giandonatoinverso/PHP-Dynamic-Qr-code" target="_blank">PHP Qrcode Generator by Giandonato Inverso</a>.
</p>
<p>
QR code rendering is powered by the
<a href="https://github.com/chillerlan/php-qrcode" target="_blank">chillerlan/php-qrcode</a>
library.
</p>
</div>
</div>
</div><!--/. container-fluid -->
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
<!-- Footer and scripts -->
<?php include './includes/footer.php'; ?>
</body>
</html>
+16 -42
View File
@@ -1,53 +1,31 @@
<?php <?php
require_once 'includes/bootstrap.php';
require_once 'config/config.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') if ($_SERVER['REQUEST_METHOD'] === 'POST')
{ {
csrf_verify_or_die();
$username = filter_input(INPUT_POST, 'username'); $username = filter_input(INPUT_POST, 'username');
$password = filter_input(INPUT_POST, 'password'); $password = filter_input(INPUT_POST, 'password');
$remember = filter_input(INPUT_POST, 'remember'); $remember = filter_input(INPUT_POST, 'remember');
if (!$username || !$password) {
$_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php');
exit;
}
if (qr_is_login_locked_out($username)) {
$_SESSION['login_failure'] = 'Too many failed login attempts. Try again in 15 minutes.';
header('Location: login.php');
exit;
}
// Get DB instance. // Get DB instance.
$db = getDbInstance(); $db = getDbInstance();
$db->where('username', $username); $db->where('username', $username);
$row = $db->getOne('users'); $row = $db->getOne('users');
if ($db->count >= 1 && password_verify($password, $row['password'])) if ($db->count >= 1)
{ {
qr_record_login_attempt($username, true); $db_password = $row['password'];
$user_id = $row['id'];
// Voorkom session fixation: nieuwe sessie-id na een geslaagde login.
session_regenerate_id(true);
if (password_verify($password, $db_password))
{
$_SESSION['user_logged_in'] = TRUE; $_SESSION['user_logged_in'] = TRUE;
$_SESSION['type'] = $row['type']; $_SESSION['type'] = $row['type'];
$_SESSION['user_id'] = $row['id']; $_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');
$user_id = $row['id'];
if ($remember) if ($remember)
{ {
@@ -57,18 +35,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
$expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days')); $expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days'));
$expires = strtotime($expiry_time); $expires = strtotime($expiry_time);
$is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$cookie_options = [ setcookie('series_id', $series_id, $expires, '/');
'expires' => $expires, setcookie('remember_token', $remember_token, $expires, '/');
'path' => '/',
'secure' => $is_https,
'httponly' => true,
'samesite' => 'Lax',
];
setcookie('series_id', $series_id, $cookie_options);
setcookie('remember_token', $remember_token, $cookie_options);
$db = getDbInstance(); $db = getDbInstance();
$db->where ('id',$user_id); $db->where ('id',$user_id);
@@ -82,11 +51,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
} }
// Authentication successfull redirect user // Authentication successfull redirect user
header('Location: index.php'); header('Location: index.php');
}
else
{
$_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php');
}
exit; exit;
} }
else else
{ {
qr_record_login_attempt($username, false);
$_SESSION['login_failure'] = 'Invalid username or password'; $_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php'); header('Location: login.php');
exit; exit;
-225
View File
@@ -1,225 +0,0 @@
<?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>QRForge</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>
+23 -51
View File
@@ -1,14 +1,9 @@
<?php <?php
require_once 'includes/bootstrap.php'; session_start();
require_once BASE_PATH . '/includes/auth_validate.php'; require_once 'config/config.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php'; require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php';
require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php'; require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php';
header('Content-Type: application/json');
csrf_verify_header_or_die();
$allowed_types = ['dynamic', 'static'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db = getDbInstance(); $db = getDbInstance();
$json = json_decode(file_get_contents('php://input'), true); $json = json_decode(file_get_contents('php://input'), true);
@@ -17,8 +12,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$params = $json['params']; $params = $json['params'];
$files = []; $files = [];
if (isset($json['type']) && in_array($json['type'], $allowed_types, true)) { if (isset($json['type'])) {
$type = $json['type']; $type = filter_var($json['type'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
} else { } else {
echo json_encode([ echo json_encode([
'data' => 'Type action field in the request.', 'data' => 'Type action field in the request.',
@@ -35,29 +30,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit(); 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) { foreach ($params as $param) {
$db->where('id', $param); $row = $db->where('id', $param);
qr_apply_owner_scope($db);
$row = $db->getOne("{$type}_qrcodes"); $row = $db->getOne("{$type}_qrcodes");
if ($row !== NULL) { @$files[] = SAVED_QRCODE_FOLDER . $row['qrcode'];
$files[] = SAVED_QRCODE_DIRECTORY . $row['qrcode'];
}
} }
$zip = new ZipArchive(); $zip = new ZipArchive();
$zip_filename = 'qrcodes_' . uniqid() . '.zip'; $uniqid = uniqid();
$zip_path = SAVED_QRCODE_DIRECTORY . 'zip/' . $zip_filename; $relative_dir = SAVED_QRCODE_FOLDER . 'zip/qrcodes_' . $uniqid . '.zip';
@unlink($zip_path); @unlink($relative_dir);
$zip->open($zip_path, ZipArchive::CREATE); $url_path = SAVED_QRCODE_URL . 'zip/qrcodes_' . $uniqid . '.zip';
$zip->open($relative_dir, ZipArchive::CREATE);
foreach ($files as $file) { foreach ($files as $file) {
$download_file = @file_get_contents($file, true); $download_file = @file_get_contents($file, true);
@@ -66,27 +50,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$zip->close(); $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([ echo json_encode([
'data' => 'qrcode_zip_download.php?file=' . rawurlencode($zip_filename), 'data' => $url_path,
'status' => 200 'status' => 200
]); ]);
exit(); exit();
} else if($json["action"] == "delete") { } 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']; $params = $json['params'];
$files = [];
if (isset($json['type']) && in_array($json['type'], $allowed_types, true)) { if (isset($json['type'])) {
$type = $json['type']; $type = filter_var($json['type'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
} else { } else {
echo json_encode([ echo json_encode([
'data' => 'Type action field in the request.', 'data' => 'Type action field in the request.',
@@ -105,15 +79,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if($type == "dynamic") if($type == "dynamic")
$instance = new DynamicQrcode(); $instance = new DynamicQrcode();
else else if($type == "static")
$instance = new StaticQrcode(); $instance = new StaticQrcode();
else
die("Type not allowed");
foreach ($params as $param) { foreach ($params as $param) {
$a = 0;
$instance->deleteQrcode($param, true); $instance->deleteQrcode($param, true);
} }
audit_log('bulk_delete', $type, implode(',', $params));
echo json_encode([ echo json_encode([
'action' => "delete", 'action' => "delete",
'data' => "Qrcode deleted", 'data' => "Qrcode deleted",
@@ -121,12 +96,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
]); ]);
exit(); exit();
} else { } else
echo json_encode(['data' => 'Action not allowed', 'status' => 400]); exit("Action not allowed");
exit();
}
} else { } else {
http_response_code(405); exit('Direct access to this script not allowed.');
echo json_encode(['data' => 'Direct access to this script not allowed.', 'status' => 405]);
exit();
} }
?>
-99
View File
@@ -1,99 +0,0 @@
<?php
require_once 'includes/bootstrap.php';
if (empty($_SESSION['user_logged_in'])) {
header('Location: login.php');
exit;
}
$forced = !empty($_SESSION['must_change_password']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
$current_password = $_POST['current_password'] ?? '';
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
$db = getDbInstance();
$db->where('id', $_SESSION['user_id']);
$user = $db->getOne('users');
if ($user === NULL || !password_verify($current_password, $user['password'])) {
$_SESSION['failure'] = 'Current password is incorrect.';
} elseif (strlen($new_password) < 10) {
$_SESSION['failure'] = 'New password must be at least 10 characters long.';
} elseif ($new_password !== $confirm_password) {
$_SESSION['failure'] = 'New password and confirmation do not match.';
} elseif ($new_password === $current_password) {
$_SESSION['failure'] = 'New password must be different from the current password.';
} else {
$db = getDbInstance();
$db->where('id', $_SESSION['user_id']);
$db->update('users', [
'password' => password_hash($new_password, PASSWORD_DEFAULT),
'must_change_password' => 0,
'password_changed_at' => date('Y-m-d H:i:s'),
]);
$_SESSION['must_change_password'] = false;
audit_log('password_changed');
$_SESSION['success'] = 'Password updated successfully.';
header('Location: index.php');
exit;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<title>Change password - QRForge</title>
<?php include './includes/head.php'; ?>
<body class="login-page" style="min-height: 512.391px;">
<div class="login-box">
<div class="login-logo">
<img src="dist/img/brand/logo.svg" alt="QRForge" style="max-width: 260px;">
</div>
<div class="card">
<div class="card-body login-card-body">
<p class="login-box-msg">
<?php echo $forced
? 'You must change your password before continuing.'
: 'Change your password'; ?>
</p>
<?php include './includes/flash_messages.php'; ?>
<form method="POST" action="change_password.php">
<?php echo csrf_field(); ?>
<div class="input-group mb-3">
<input type="password" name="current_password" class="form-control" placeholder="Current password" required="required" autocomplete="current-password">
</div>
<div class="input-group mb-3">
<input type="password" name="new_password" class="form-control" placeholder="New password (min. 10 characters)" required="required" minlength="10" autocomplete="new-password">
</div>
<div class="input-group mb-3">
<input type="password" name="confirm_password" class="form-control" placeholder="Confirm new password" required="required" minlength="10" autocomplete="new-password">
</div>
<div class="row">
<div class="col-12">
<button type="submit" class="btn btn-primary btn-block">Update password</button>
</div>
</div>
</form>
<?php if (!$forced): ?>
<p class="mt-3 text-center"><a href="index.php">Back to dashboard</a></p>
<?php endif; ?>
</div>
</div>
</div>
<script src="../../plugins/jquery/jquery.min.js"></script>
<script src="../../plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="../../dist/js/adminlte.js"></script>
</body>
</html>
+5 -9
View File
@@ -2,12 +2,8 @@
//Note: This file should be included first in every php page. //Note: This file should be included first in every php page.
require_once ('environment.php'); 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); error_reporting(E_ALL);
ini_set('display_errors', 'Off'); ini_set('display_errors', 'On');
ini_set('log_errors', 'On');
define('BASE_PATH', dirname(dirname(__FILE__))); define('BASE_PATH', dirname(dirname(__FILE__)));
define('CURRENT_PAGE', basename($_SERVER['REQUEST_URI'])); define('CURRENT_PAGE', basename($_SERVER['REQUEST_URI']));
define('SCRIPT_NAME', ltrim(dirname($_SERVER['SCRIPT_NAME']), '/')); define('SCRIPT_NAME', ltrim(dirname($_SERVER['SCRIPT_NAME']), '/'));
@@ -21,10 +17,10 @@ require_once BASE_PATH . '/lib/MysqliDb/MysqliDb.php';
require_once BASE_PATH . '/helpers/helpers.php'; require_once BASE_PATH . '/helpers/helpers.php';
/* SAVED QR CODES */ /* SAVED QR CODES */
// Storage lives outside the document root so files can only be reached through the //You can change the folder where the qr code will be saved
// authenticated qrcode_image.php / qrcode_zip_download.php endpoints, never as a direct define('SAVED_QRCODE_FOLDER', './saved_qrcode/');
// static URL. See db/migrations and the "saved_qrcode" hardening note in the OSS repo. define('SAVED_QRCODE_DIRECTORY', BASE_PATH.'/saved_qrcode/');
define('SAVED_QRCODE_DIRECTORY', dirname(BASE_PATH).'/qrcode-storage/'); define('SAVED_QRCODE_URL', base_url(). SCRIPT_FOLDER .'/saved_qrcode/');
//You can change the page name for the redirect and the search parameter (the default is "id") //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='); define('READ_PATH', base_url().'/read.php?id=');
+29 -12
View File
@@ -1,18 +1,35 @@
<?php <?php
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| UNIFIED ENVIRONMENT CONFIGURATION | DOCKER INSTALLATION
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
define('DATABASE_HOST', getenv('DATABASE_HOST') ?: 'localhost'); if(is_string(Getenv('TYPE')) && Getenv('TYPE') == "docker") {
define('DATABASE_PORT', filter_var(getenv('DATABASE_PORT'), FILTER_VALIDATE_INT) ?: 3306); define('DATABASE_HOST', Getenv('DATABASE_HOST'));
define('DATABASE_NAME', getenv('DATABASE_NAME') ?: 'qrcode'); define('DATABASE_PORT', filter_var(Getenv('DATABASE_PORT'), FILTER_VALIDATE_INT));
define('DATABASE_USER', getenv('DATABASE_USER') ?: 'root'); define('DATABASE_NAME', Getenv('DATABASE_NAME'));
define('DATABASE_PASSWORD', getenv('DATABASE_PASSWORD') ?: 'root'); define('DATABASE_USER', Getenv('DATABASE_USER'));
define('DATABASE_PREFIX', getenv('DATABASE_PREFIX') !== false ? getenv('DATABASE_PREFIX') : 'qr_'); define('DATABASE_PASSWORD', Getenv('DATABASE_PASSWORD'));
define('DATABASE_CHARSET', getenv('DATABASE_CHARSET') ?: 'utf8'); define('DATABASE_PREFIX', Getenv('DATABASE_PREFIX'));
define('DATABASE_CHARSET', Getenv('DATABASE_CHARSET'));
define('TYPE', Getenv('TYPE'));
define('BASE_URL', Getenv('BASE_URL'));
define("QRCODE_GENERATOR", Getenv('QRCODE_GENERATOR'));
} else {
define('DATABASE_HOST', "localhost");
define('DATABASE_PORT', "3306");
define('DATABASE_NAME', "qrcode");
define('DATABASE_USER', "root");
define('DATABASE_PASSWORD', "root");
define('DATABASE_PREFIX', "qr_");
define('DATABASE_CHARSET', "utf8");
define("QRCODE_GENERATOR", "external-api.qrserver.com"); // external-api.qrserver.com => https://api.qrserver.com/v1/create-qr-code/?data= // internal-chillerlan.qrcode => https://github.com/chillerlan/php-qrcode
}
/*
|--------------------------------------------------------------------------
| INSTALLATION WITHOUT CONTAINER
|--------------------------------------------------------------------------
*/
define('TYPE', getenv('TYPE') ?: 'local'); ?>
define('BASE_URL', getenv('BASE_URL') ?: 'http://localhost');
define('QRCODE_GENERATOR', getenv('QRCODE_GENERATOR') ?: 'external-api.qrserver.com'); // opties: external-api.qrserver.com of internal-chillerlan.qrcode
-17
View File
@@ -7,20 +7,3 @@
display: none; display: none;
color: red; color: red;
} }
/* List-page qr code thumbnails: fixed box, but object-fit:contain instead of
width/height attrs so taller images (e.g. icon-above-qr) don't get squashed. */
.qr-thumb-link {
display: inline-block;
padding: 0;
border: 0;
background: none;
cursor: pointer;
}
.qr-thumb-img {
width: 100px;
height: 100px;
object-fit: contain;
background: #fff;
border: 1px solid #dee2e6;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 755 B

-11
View File
@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 151.74 151.74" role="img" aria-labelledby="qfi-title qfi-desc">
<title id="qfi-title">QRForge icon</title>
<desc id="qfi-desc">QR-block mark with outbound arrow tile.</desc>
<g transform="translate(-8.95,-2.57)">
<rect id="block-top-left" x="5.24" y="16.81" width="42.29" height="40.70" rx="2.5" fill="#334155" />
<rect id="block-mid" x="59.57" y="68.79" width="24.90" height="23.46" rx="2.5" fill="#334155" />
<rect id="block-bottom-right" x="96.08" y="103.32" width="40.59" height="31.68" rx="2.5" fill="#334155" />
<path id="block-bottom-left-L" d="M 5.24,71.29 Q 5.24,68.79 7.74,68.79 L 45.03,68.79 Q 47.53,68.79 47.53,71.29 L 47.53,103.32 Q 47.53,103.32 47.53,103.32 L 81.97,103.32 Q 84.47,103.32 84.47,105.82 L 84.47,132.50 Q 84.47,135.00 81.97,135.00 L 7.74,135.00 Q 5.24,135.00 5.24,132.50 Z" fill="#334155" />
<path id="arrow-tile" d="M 105.68,6.94 Q 104.67,5.88 106.13,5.88 L 145.89,5.88 Q 148.39,5.88 148.39,8.38 L 148.39,44.14 Q 148.39,46.64 146.42,45.11 L 133.00,34.67 Q 133.00,34.67 133.00,34.67 L 112.50,56.87 Q 112.50,56.87 112.50,56.87 L 134.08,56.87 Q 136.58,56.87 136.58,59.37 L 136.58,89.50 Q 136.58,92.00 134.08,92.00 L 98.63,92.00 Q 96.13,92.00 96.13,89.50 L 96.13,56.87 Q 96.13,56.87 96.13,56.87 L 62.10,56.87 Q 59.60,56.87 59.60,54.37 L 59.60,19.60 Q 59.60,17.10 62.10,17.10 L 93.63,17.10 Q 96.13,17.10 96.13,19.60 L 96.13,41.00 Q 96.13,41.00 96.13,41.00 L 97.00,42.17 Q 97.00,42.17 97.00,42.17 L 117.19,22.73 Q 118.99,21.00 117.27,19.18 L 107.68,9.06 Q 106.68,8.00 105.68,6.94 Z" fill="#2563EB" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-11
View File
@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 151.74 151.74" role="img" aria-labelledby="qfi-title qfi-desc">
<title id="qfi-title">QRForge icon</title>
<desc id="qfi-desc">QR-block mark with outbound arrow tile.</desc>
<g transform="translate(-8.95,-2.57)">
<rect id="block-top-left" x="5.24" y="16.81" width="42.29" height="40.70" rx="2.5" fill="#FFFFFF" />
<rect id="block-mid" x="59.57" y="68.79" width="24.90" height="23.46" rx="2.5" fill="#FFFFFF" />
<rect id="block-bottom-right" x="96.08" y="103.32" width="40.59" height="31.68" rx="2.5" fill="#FFFFFF" />
<path id="block-bottom-left-L" d="M 5.24,71.29 Q 5.24,68.79 7.74,68.79 L 45.03,68.79 Q 47.53,68.79 47.53,71.29 L 47.53,103.32 Q 47.53,103.32 47.53,103.32 L 81.97,103.32 Q 84.47,103.32 84.47,105.82 L 84.47,132.50 Q 84.47,135.00 81.97,135.00 L 7.74,135.00 Q 5.24,135.00 5.24,132.50 Z" fill="#FFFFFF" />
<path id="arrow-tile" d="M 105.68,6.94 Q 104.67,5.88 106.13,5.88 L 145.89,5.88 Q 148.39,5.88 148.39,8.38 L 148.39,44.14 Q 148.39,46.64 146.42,45.11 L 133.00,34.67 Q 133.00,34.67 133.00,34.67 L 112.50,56.87 Q 112.50,56.87 112.50,56.87 L 134.08,56.87 Q 136.58,56.87 136.58,59.37 L 136.58,89.50 Q 136.58,92.00 134.08,92.00 L 98.63,92.00 Q 96.13,92.00 96.13,89.50 L 96.13,56.87 Q 96.13,56.87 96.13,56.87 L 62.10,56.87 Q 59.60,56.87 59.60,54.37 L 59.60,19.60 Q 59.60,17.10 62.10,17.10 L 93.63,17.10 Q 96.13,17.10 96.13,19.60 L 96.13,41.00 Q 96.13,41.00 96.13,41.00 L 97.00,42.17 Q 97.00,42.17 97.00,42.17 L 117.19,22.73 Q 118.99,21.00 117.27,19.18 L 107.68,9.06 Q 106.68,8.00 105.68,6.94 Z" fill="#2563EB" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-11
View File
@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 151.74 151.74" role="img" aria-labelledby="qfi-title qfi-desc">
<title id="qfi-title">QRForge icon</title>
<desc id="qfi-desc">QR-block mark with outbound arrow tile.</desc>
<g transform="translate(-8.95,-2.57)">
<rect id="block-top-left" x="5.24" y="16.81" width="42.29" height="40.70" rx="2.5" fill="#334155" />
<rect id="block-mid" x="59.57" y="68.79" width="24.90" height="23.46" rx="2.5" fill="#334155" />
<rect id="block-bottom-right" x="96.08" y="103.32" width="40.59" height="31.68" rx="2.5" fill="#334155" />
<path id="block-bottom-left-L" d="M 5.24,71.29 Q 5.24,68.79 7.74,68.79 L 45.03,68.79 Q 47.53,68.79 47.53,71.29 L 47.53,103.32 Q 47.53,103.32 47.53,103.32 L 81.97,103.32 Q 84.47,103.32 84.47,105.82 L 84.47,132.50 Q 84.47,135.00 81.97,135.00 L 7.74,135.00 Q 5.24,135.00 5.24,132.50 Z" fill="#334155" />
<path id="arrow-tile" d="M 105.68,6.94 Q 104.67,5.88 106.13,5.88 L 145.89,5.88 Q 148.39,5.88 148.39,8.38 L 148.39,44.14 Q 148.39,46.64 146.42,45.11 L 133.00,34.67 Q 133.00,34.67 133.00,34.67 L 112.50,56.87 Q 112.50,56.87 112.50,56.87 L 134.08,56.87 Q 136.58,56.87 136.58,59.37 L 136.58,89.50 Q 136.58,92.00 134.08,92.00 L 98.63,92.00 Q 96.13,92.00 96.13,89.50 L 96.13,56.87 Q 96.13,56.87 96.13,56.87 L 62.10,56.87 Q 59.60,56.87 59.60,54.37 L 59.60,19.60 Q 59.60,17.10 62.10,17.10 L 93.63,17.10 Q 96.13,17.10 96.13,19.60 L 96.13,41.00 Q 96.13,41.00 96.13,41.00 L 97.00,42.17 Q 97.00,42.17 97.00,42.17 L 117.19,22.73 Q 118.99,21.00 117.27,19.18 L 107.68,9.06 Q 106.68,8.00 105.68,6.94 Z" fill="#2563EB" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-12
View File
@@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236.20 246.50" role="img" aria-labelledby="qfs-title qfs-desc">
<title id="qfs-title">QRForge stacked logo</title>
<desc id="qfs-desc">Stacked QRForge logo.</desc>
<g transform="translate(33.28,14.75)">
<rect id="block-top-left" x="5.24" y="16.81" width="42.29" height="40.70" rx="2.5" fill="#334155" />
<rect id="block-mid" x="59.57" y="68.79" width="24.90" height="23.46" rx="2.5" fill="#334155" />
<rect id="block-bottom-right" x="96.08" y="103.32" width="40.59" height="31.68" rx="2.5" fill="#334155" />
<path id="block-bottom-left-L" d="M 5.24,71.29 Q 5.24,68.79 7.74,68.79 L 45.03,68.79 Q 47.53,68.79 47.53,71.29 L 47.53,103.32 Q 47.53,103.32 47.53,103.32 L 81.97,103.32 Q 84.47,103.32 84.47,105.82 L 84.47,132.50 Q 84.47,135.00 81.97,135.00 L 7.74,135.00 Q 5.24,135.00 5.24,132.50 Z" fill="#334155" />
<path id="arrow-tile" d="M 105.68,6.94 Q 104.67,5.88 106.13,5.88 L 145.89,5.88 Q 148.39,5.88 148.39,8.38 L 148.39,44.14 Q 148.39,46.64 146.42,45.11 L 133.00,34.67 Q 133.00,34.67 133.00,34.67 L 112.50,56.87 Q 112.50,56.87 112.50,56.87 L 134.08,56.87 Q 136.58,56.87 136.58,59.37 L 136.58,89.50 Q 136.58,92.00 134.08,92.00 L 98.63,92.00 Q 96.13,92.00 96.13,89.50 L 96.13,56.87 Q 96.13,56.87 96.13,56.87 L 62.10,56.87 Q 59.60,56.87 59.60,54.37 L 59.60,19.60 Q 59.60,17.10 62.10,17.10 L 93.63,17.10 Q 96.13,17.10 96.13,19.60 L 96.13,41.00 Q 96.13,41.00 96.13,41.00 L 97.00,42.17 Q 97.00,42.17 97.00,42.17 L 117.19,22.73 Q 118.99,21.00 117.27,19.18 L 107.68,9.06 Q 106.68,8.00 105.68,6.94 Z" fill="#2563EB" />
</g>
<text x="118.10" y="203.67" text-anchor="middle" font-family="Inter, Manrope, Geist, Arial, sans-serif" font-size="48.39" font-weight="700" letter-spacing="-3.43" fill="#334155">QRForge</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-12
View File
@@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5.24 5.88 573.81 145.12" role="img" aria-labelledby="qf-title qf-desc">
<title id="qf-title">QRForge logo</title>
<desc id="qf-desc">QRForge wordmark with a QR-block mark and outbound arrow tile.</desc>
<g id="icon">
<rect id="block-top-left" x="5.24" y="16.81" width="42.29" height="40.70" rx="2.5" fill="#334155" />
<rect id="block-mid" x="59.57" y="68.79" width="24.90" height="23.46" rx="2.5" fill="#334155" />
<rect id="block-bottom-right" x="96.08" y="103.32" width="40.59" height="31.68" rx="2.5" fill="#334155" />
<path id="block-bottom-left-L" d="M 5.24,71.29 Q 5.24,68.79 7.74,68.79 L 45.03,68.79 Q 47.53,68.79 47.53,71.29 L 47.53,103.32 Q 47.53,103.32 47.53,103.32 L 81.97,103.32 Q 84.47,103.32 84.47,105.82 L 84.47,132.50 Q 84.47,135.00 81.97,135.00 L 7.74,135.00 Q 5.24,135.00 5.24,132.50 Z" fill="#334155" />
<path id="arrow-tile" d="M 105.68,6.94 Q 104.67,5.88 106.13,5.88 L 145.89,5.88 Q 148.39,5.88 148.39,8.38 L 148.39,44.14 Q 148.39,46.64 146.42,45.11 L 133.00,34.67 Q 133.00,34.67 133.00,34.67 L 112.50,56.87 Q 112.50,56.87 112.50,56.87 L 134.08,56.87 Q 136.58,56.87 136.58,59.37 L 136.58,89.50 Q 136.58,92.00 134.08,92.00 L 98.63,92.00 Q 96.13,92.00 96.13,89.50 L 96.13,56.87 Q 96.13,56.87 96.13,56.87 L 62.10,56.87 Q 59.60,56.87 59.60,54.37 L 59.60,19.60 Q 59.60,17.10 62.10,17.10 L 93.63,17.10 Q 96.13,17.10 96.13,19.60 L 96.13,41.00 Q 96.13,41.00 96.13,41.00 L 97.00,42.17 Q 97.00,42.17 97.00,42.17 L 117.19,22.73 Q 118.99,21.00 117.27,19.18 L 107.68,9.06 Q 106.68,8.00 105.68,6.94 Z" fill="#2563EB" />
</g>
<text id="wordmark" x="177" y="101" font-family="Inter, Manrope, Geist, Arial, sans-serif" font-size="103" font-weight="700" letter-spacing="-7.3" fill="#334155">QRForge</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

-23
View File
@@ -470,9 +470,6 @@
data: JSON.stringify(data), data: JSON.stringify(data),
dataType: "json", dataType: "json",
contentType: 'application/json', contentType: 'application/json',
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
success: (res) => { success: (res) => {
if (res.status == 200) { if (res.status == 200) {
if(data["action"] === "download") { if(data["action"] === "download") {
@@ -500,23 +497,3 @@
return false; return false;
}); });
})(jQuery) })(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);
}
});
});
});
-69
View File
@@ -1,69 +0,0 @@
(function () {
document.addEventListener('DOMContentLoaded', function () {
var searchInput = document.getElementById('location_search');
var searchBtn = document.getElementById('location_search_btn');
var resultsBox = document.getElementById('location_search_results');
var latInput = document.getElementById('latitude');
var lngInput = document.getElementById('longitude');
if (!searchInput || !searchBtn || !resultsBox || !latInput || !lngInput) {
return;
}
function clearResults() {
resultsBox.innerHTML = '';
}
function doSearch() {
var query = searchInput.value.trim();
if (!query) {
return;
}
clearResults();
resultsBox.innerHTML = '<div class="list-group-item">Searching...</div>';
fetch('https://nominatim.openstreetmap.org/search?format=json&limit=5&q=' + encodeURIComponent(query))
.then(function (response) { return response.json(); })
.then(function (results) {
clearResults();
if (!results.length) {
resultsBox.innerHTML = '<div class="list-group-item">No results found.</div>';
return;
}
results.forEach(function (place) {
var item = document.createElement('button');
item.type = 'button';
item.className = 'list-group-item list-group-item-action';
item.textContent = place.display_name;
item.addEventListener('click', function () {
latInput.value = place.lat;
lngInput.value = place.lon;
searchInput.value = place.display_name;
clearResults();
});
resultsBox.appendChild(item);
});
})
.catch(function () {
resultsBox.innerHTML = '<div class="list-group-item text-danger">Search failed (network error).</div>';
});
}
searchBtn.addEventListener('click', doSearch);
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
doSearch();
}
});
document.addEventListener('click', function (e) {
if (e.target !== searchInput && !resultsBox.contains(e.target)) {
clearResults();
}
});
});
})();
-258
View File
@@ -1,258 +0,0 @@
// 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) {
// bootstrap-colorpicker sets the value and fires 'change' via jQuery's
// synthetic .trigger(), which a native addEventListener never sees - so the
// preview only updates via jQuery's event binding, not the native one below.
if (window.jQuery) {
jQuery(input).on('change', function () {
updateStylePreview(scopeOf(input));
});
} else {
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).'); });
});
});
});
})();
+5 -24
View File
@@ -1,20 +1,11 @@
<?php <?php
require_once 'includes/bootstrap.php'; session_start();
require_once 'config/config.php';
require_once BASE_PATH.'/includes/auth_validate.php'; require_once BASE_PATH.'/includes/auth_validate.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.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(); $dynamic_qrcode_instance = new DynamicQrcode();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
}
$edit = false; $edit = false;
if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) { if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) {
$edit = true; $edit = true;
@@ -37,7 +28,7 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["edit"])) {
$dynamic_qrcode_instance->editQrcode($_POST); $dynamic_qrcode_instance->editQrcode($_POST);
} }
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"]) && !isset($_POST["del_id"])) { if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
if( if(
isset($_POST["foreground"]) && isset($_POST["foreground"]) &&
isset($_POST["background"]) && isset($_POST["background"]) &&
@@ -45,22 +36,13 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"]) && !isset($_P
isset($_POST["filename"]) && isset($_POST["filename"]) &&
isset($_POST["format"]) && isset($_POST["format"]) &&
isset($_POST["id_owner"]) 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); $dynamic_qrcode_instance->addQrcode($_POST);
}
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<title>QRForge</title> <title>Qrcode Generator</title>
<head> <head>
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; ?>
</head> </head>
@@ -101,7 +83,6 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"]) && !isset($_P
<h3 class="card-title">Enter the requested data</h3> <h3 class="card-title">Enter the requested data</h3>
</div> </div>
<form class="form" action="" method="post" id="dynamic_form" enctype="multipart/form-data"> <form class="form" action="" method="post" id="dynamic_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="card-body"> <div class="card-body">
<?php <?php
if($edit) if($edit)
+7 -13
View File
@@ -1,14 +1,9 @@
<?php <?php
require_once 'includes/bootstrap.php'; session_start();
require_once 'config/config.php';
require_once BASE_PATH . '/includes/auth_validate.php'; require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.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(); $db = getDbInstance();
$dynamic_qrcode = new DynamicQrcode(); $dynamic_qrcode = new DynamicQrcode();
@@ -18,9 +13,10 @@ require_once BASE_PATH . '/includes/search_order.php';
$page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 1; $page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 1;
$db->pageLimit = 15; $db->pageLimit = 15;
// Scoped to one admin's own codes for an admin (or a 'user' created by that admin); if($_SESSION['type'] !== 'super') {
// full visibility for super and company-wide 'user' accounts. $db->where("id_owner", $_SESSION['user_id']);
qr_apply_owner_scope($db); $db->orWhere ("id_owner", NULL, 'IS');
}
$rows = $db->arraybuilder()->paginate('dynamic_qrcodes', $page, $select); $rows = $db->arraybuilder()->paginate('dynamic_qrcodes', $page, $select);
$total_pages = $db->totalPages; $total_pages = $db->totalPages;
@@ -29,7 +25,7 @@ $total_pages = $db->totalPages;
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<title>QRForge</title> <title>Qrcode Generator</title>
<head> <head>
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; ?>
</head> </head>
@@ -54,7 +50,6 @@ $total_pages = $db->totalPages;
<h1 class="m-0 text-dark">Dynamic Qr codes</h1> <h1 class="m-0 text-dark">Dynamic Qr codes</h1>
</div><!-- /.col --> </div><!-- /.col -->
<?php if ($_SESSION['type'] !== 'user'): ?>
<div class="col-sm-6"> <div class="col-sm-6">
<ol class="breadcrumb float-sm-right"> <ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
@@ -62,7 +57,6 @@ $total_pages = $db->totalPages;
</li> </li>
</ol> </ol>
</div><!-- /.col --> </div><!-- /.col -->
<?php endif; ?>
</div><!-- /.row --> </div><!-- /.row -->
</div><!-- /.container-fluid --> </div><!-- /.container-fluid -->
</div><!-- /.content-header --> </div><!-- /.content-header -->
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

+12 -92
View File
@@ -35,7 +35,7 @@
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
<label for="size">Size (px)</label> <label for="size">Size (px)</label>
<select name="size" id="size" class="form-control"> <select name="size" class="form-control">
<option value="100">100</option> <option value="100">100</option>
<option value="200">200</option> <option value="200">200</option>
<option value="300">300</option> <option value="300">300</option>
@@ -51,49 +51,6 @@
</div> </div>
</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 <!-- Its use is not recommended. Read the documentation
<div class="form-group"> <div class="form-group">
<label for="logo">Logo</label> <label for="logo">Logo</label>
@@ -117,52 +74,12 @@
</div> </div>
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
<div class="row">
<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>
<div class="col-sm-12 mb-2">
<div class="row"> <div class="row">
<div class="col-sm-4"> <div class="col-sm-4">
<div class="form-group"> <div class="form-group">
<label for="filename">Filename *</label> <label for="filename">Filename *</label>
<input type="text" name="filename" value="" placeholder="My first Qrcode" class="form-control error" required="required" id="filename"> <input type="text" name="filename" value="" placeholder="My first Qrcode" class="form-control error" required="required" id="filename">
</div> </div>
</div> </div>
@@ -177,13 +94,17 @@
<option value="eps">EPS</option> <option value="eps">EPS</option>
</select> </select>
</div> </div>
</div>
</div>
<?php if ($_SESSION['type'] === 'super') { ?> <?php if($_SESSION['type'] === 'super') { ?>
<div class="col-sm-12 mb-2">
<div class="row">
<div class="col-sm-4"> <div class="col-sm-4">
<div class="form-group"> <div class="form-group">
<label for="id_owner">Owner *</label> <label for="id_owner">Owner *</label>
<select name="id_owner" id="id_owner" class="form-control"> <select name="id_owner" class="form-control">
<option value="">All (shared with every admin)</option> <option value="" selected>All</option>
<?php <?php
require_once BASE_PATH . '/lib/Users/Users.php'; require_once BASE_PATH . '/lib/Users/Users.php';
@@ -191,16 +112,15 @@
$users = $users_instance->getAllUsers(); $users = $users_instance->getAllUsers();
foreach ($users as $user) { foreach ($users as $user) {
$is_self = (int) $user["id"] === (int) $_SESSION["user_id"];
?> ?>
<option value="<?php echo $user["id"];?>" <?php echo $is_self ? 'selected' : ''; ?>><?php echo $user["username"];?></option> <option value="<?php echo $user["id"];?>"><?php echo $user["username"];?></option>
<?php } ?> <?php } ?>
</select> </select>
</div> </div>
</div> </div>
</div>
</div>
<?php } else { ?> <?php } else { ?>
<input type="hidden" name="id_owner" value="<?php echo $_SESSION["user_id"];?>"/> <input type="hidden" name="id_owner" value="<?php echo $_SESSION["user_id"];?>"/>
<?php } ?> <?php } ?>
</div>
</div>
</fieldset> </fieldset>
-14
View File
@@ -44,12 +44,6 @@
<li class="nav-item"> <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> <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>
<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> </ul>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -96,15 +90,7 @@
<div class="tab-pane fade" id="twofa" role="tabpanel" aria-labelledby="custom-tabs-four-profile-tab"> <div class="tab-pane fade" id="twofa" role="tabpanel" aria-labelledby="custom-tabs-four-profile-tab">
<?php include BASE_PATH . '/forms/static/2fa.php'; ?> <?php include BASE_PATH . '/forms/static/2fa.php'; ?>
</div> </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> </div>
</div><!-- /.card --> </div><!-- /.card -->
<script src="dist/js/qrcode-style-tools.js?nocache=<?php print rand();?>"></script>
</fieldset> </fieldset>
+1 -60
View File
@@ -20,12 +20,11 @@
<span class="input-group-text"><i class="fa fa-lock"></i></span> <span class="input-group-text"><i class="fa fa-lock"></i></span>
</div> </div>
<input type="password" name="password" placeholder="<?php echo ($edit) ? 'Leave blank to keep current password' : 'Password'; ?>" class="form-control" <?php echo ($edit) ? '' : 'required="required"'; ?> minlength="10" autocomplete="off"> <input type="password" name="password" placeholder="Password" class="form-control" required="required" autocomplete="off">
</div> </div>
</div> </div>
</div> </div>
<?php if ($_SESSION['type'] === 'super'): ?>
<div class="col-sm-4"> <div class="col-sm-4">
<label for="user-type">User type *</label> <label for="user-type">User type *</label>
@@ -39,66 +38,8 @@
<label class="radio"> <label class="radio">
<input type="radio" name="type" value="admin" required="required" <?php echo ($edit && $user['type'] =='admin') ? "checked": "" ; ?>/> Admin</label> <input type="radio" name="type" value="admin" required="required" <?php echo ($edit && $user['type'] =='admin') ? "checked": "" ; ?>/> Admin</label>
</div> </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>
</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) { ?> <?php if($edit) { ?>
<input type="hidden" name="id" value="<?php echo $user['id'];?>"/> <input type="hidden" name="id" value="<?php echo $user['id'];?>"/>
<input type="hidden" name="edit" value="true"/> <input type="hidden" name="edit" value="true"/>
+15 -102
View File
@@ -34,7 +34,7 @@
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
<label for="size">Size (px)</label> <label for="size">Size (px)</label>
<select name="size" id="size" class="form-control"> <select name="size" class="form-control">
<option value="100">100</option> <option value="100">100</option>
<option value="200">200</option> <option value="200">200</option>
<option value="300">300</option> <option value="300">300</option>
@@ -55,56 +55,6 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
</div> </div>
</div> </div>
<div class="col-sm-12 mb-2">
<div class="row">
<div class="col-6 col-md-3">
<label>Randomize</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 <!-- Its use is not recommended. Read the documentation
<div class="form-group"> <div class="form-group">
<label for="logo">Logo</label> <label for="logo">Logo</label>
@@ -112,53 +62,13 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
</div> </div>
--> -->
<div class="col-sm-12 mb-2">
<div class="row">
<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>
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
<div class="row"> <div class="row">
<div class="col-sm-4"> <div class="col-sm-4">
<div class="form-group"> <div class="form-group">
<label for="filename">Filename *</label> <label for="filename">Filename *</label>
<input type="text" name="filename" value="" placeholder="My first Qrcode" class="form-control error" required="required" id="filename"> <input type="text" name="filename" value="" placeholder="My first Qrcode" class="form-control error" required="required" id = "filename">
</div> </div>
</div> </div>
@@ -178,13 +88,17 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
<option value="eps">EPS</option> <option value="eps">EPS</option>
</select> </select>
</div> </div>
</div>
</div>
<?php if ($_SESSION['type'] === 'super') { ?> <?php if($_SESSION['type'] === 'super') { ?>
<div class="col-sm-12 mb-2">
<div class="row">
<div class="col-sm-4"> <div class="col-sm-4">
<div class="form-group"> <div class="form-group">
<label for="id_owner">Owner *</label> <label for="id_owner">Owner *</label>
<select name="id_owner" id="id_owner" class="form-control"> <select name="id_owner" class="form-control">
<option value="">All (shared with every admin)</option> <option value="" selected>All</option>
<?php <?php
require_once BASE_PATH . '/lib/Users/Users.php'; require_once BASE_PATH . '/lib/Users/Users.php';
@@ -192,16 +106,15 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
$users = $users_instance->getAllUsers(); $users = $users_instance->getAllUsers();
foreach ($users as $user) { foreach ($users as $user) {
$is_self = (int) $user["id"] === (int) $_SESSION["user_id"];
?> ?>
<option value="<?php echo $user["id"];?>" <?php echo $is_self ? 'selected' : ''; ?>><?php echo $user["username"];?></option> <option value="<?php echo $user["id"];?>"><?php echo $user["username"];?></option>
<?php } ?> <?php } ?>
</select> </select>
</div> </div>
</div> </div>
<?php } else { ?>
<input type="hidden" name="id_owner" value="<?php echo $_SESSION["user_id"];?>"/>
<?php } ?>
</div> </div>
</div> </div>
<?php } else { ?>
<input type="hidden" name="id_owner" value="<?php echo $_SESSION["user_id"];?>"/>
<?php } ?>
<br> <br>
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=2fa" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=2fa" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
-73
View File
@@ -1,73 +0,0 @@
<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 -2
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=bitcoin" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=bitcoin" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
@@ -14,7 +13,7 @@
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
<div class="form-group"> <div class="form-group">
<label>Amount</label> <label>Amount *</label>
<div class="input-group"> <div class="input-group">
<input type="number" name="amount" value="" placeholder="" class="form-control" step="0.0001"> <input type="number" name="amount" value="" placeholder="" class="form-control" step="0.0001">
<div class="input-group-append"> <div class="input-group-append">
-37
View File
@@ -1,37 +0,0 @@
<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
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=bookmark" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=bookmark" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=email" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=email" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=event" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=event" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-4"> <div class="col-sm-4">
+6 -22
View File
@@ -1,32 +1,17 @@
<form class="form" action="static_qrcode.php?type=location" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=location" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-8"> <div class="col-sm-4">
<div class="form-group" style="position: relative;"> <div class="form-group">
<label for="location_search">Search address</label> <label>Latitude *</label>
<div class="input-group"> <input type="text" name="latitude" value="" placeholder="40.7127753" class="form-control">
<input type="text" id="location_search" class="form-control" placeholder="Search for an address or place...">
<div class="input-group-append">
<button type="button" id="location_search_btn" class="btn btn-outline-secondary"><i class="fa fa-search"></i></button>
</div>
</div>
<div id="location_search_results" class="list-group" style="position:absolute;z-index:1000;width:100%;"></div>
<small class="form-text text-muted">Looks up coordinates via OpenStreetMap Nominatim (your search leaves this server and goes to nominatim.openstreetmap.org). Or enter coordinates directly below.</small>
</div> </div>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<div class="form-group"> <div class="form-group">
<label for="latitude">Latitude *</label> <label>Longitude *</label>
<input type="text" name="latitude" id="latitude" value="" placeholder="40.7127753" class="form-control"> <input type="text" name="longitude" value="" placeholder="-74.0059728" class="form-control">
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="longitude">Longitude *</label>
<input type="text" name="longitude" id="longitude" value="" placeholder="-74.0059728" class="form-control">
</div> </div>
</div> </div>
@@ -38,5 +23,4 @@
</div> </div>
</div> </div>
<script src="dist/js/location-search.js"></script>
</form> </form>
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=paypal" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=paypal" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=phone" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=phone" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=skype" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=skype" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-4"> <div class="col-sm-4">
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=sms" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=sms" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=text" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=text" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-6"> <div class="col-sm-6">
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=vcard" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=vcard" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<!-- First row --> <!-- First row -->
-1
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=whatsapp" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=whatsapp" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
-2
View File
@@ -1,5 +1,4 @@
<form class="form" action="static_qrcode.php?type=wifi" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=wifi" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
@@ -10,7 +9,6 @@
<label>Encryption *</label> <label>Encryption *</label>
<select name="encryption" class="form-control"> <select name="encryption" class="form-control">
<option value="WPA" Selected>WPA/WPA2</option> <option value="WPA" Selected>WPA/WPA2</option>
<option value="WPA3">WPA3</option>
<option value="WEP">WEP</option> <option value="WEP">WEP</option>
<option value="">None</option> <option value="">None</option>
</select> </select>
+4 -48
View File
@@ -1,11 +1,8 @@
<?php $is_readonly_user = $_SESSION['type'] === 'user'; ?>
<div class="row"> <div class="row">
<?php if (!$is_readonly_user): ?>
<div class="col-12" id="bulk-action-div" style="display: none;"> <div class="col-12" id="bulk-action-div" style="display: none;">
<div id="err-msg"></div> <div id="err-msg"></div>
<div class="bulk-action-wrapper"> <div class="bulk-action-wrapper">
<form id="bulk-action" action="bulk_action.php" method="POST"> <form id="bulk-action" action="bulk_action.php" method="POST">
<?php echo csrf_field(); ?>
<div class="col-sm-12 mb-2" style="margin-left: 10px"> <div class="col-sm-12 mb-2" style="margin-left: 10px">
<div class="row"> <div class="row">
<div class="col-5 col-md-2"> <div class="col-5 col-md-2">
@@ -23,16 +20,13 @@
</form> </form>
</div> </div>
</div> </div>
<?php endif; ?>
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-body table-responsive p-0"> <div class="card-body table-responsive p-0">
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<?php if (!$is_readonly_user): ?>
<th><input type="checkbox" name="bulk-select" value="1"></th> <th><input type="checkbox" name="bulk-select" value="1"></th>
<?php endif; ?>
<th>ID</th> <th>ID</th>
<th>Owner</th> <th>Owner</th>
<th>Filename</th> <th>Filename</th>
@@ -47,9 +41,7 @@
<tbody> <tbody>
<?php foreach ($rows as $row): ?> <?php foreach ($rows as $row): ?>
<tr> <tr>
<?php if (!$is_readonly_user): ?>
<td><input type="checkbox" name="action[]" value="<?=$row['id']?>" onchange="updateBulkActionVisibility()"></td> <td><input type="checkbox" name="action[]" value="<?=$row['id']?>" onchange="updateBulkActionVisibility()"></td>
<?php endif; ?>
<td><?php echo $row['id']; ?></td> <td><?php echo $row['id']; ?></td>
<td> <td>
<?php <?php
@@ -70,16 +62,12 @@
<td><?php echo htmlspecialchars($row['identifier']); ?></td> <td><?php echo htmlspecialchars($row['identifier']); ?></td>
<td><?php echo htmlspecialchars($row['link']); ?></td> <td><?php echo htmlspecialchars($row['link']); ?></td>
<td> <td>
<a href="#" class="qr-thumb-link" data-toggle="modal" data-target="#preview-modal" <?php echo '<img src="'.SAVED_QRCODE_FOLDER.htmlspecialchars($row['qrcode']).'" width="100" height="100">'; ?>
data-qr-src="qrcode_image.php?type=dynamic&id=<?php echo $row['id']; ?>"
data-qr-name="<?php echo htmlspecialchars($row['filename']); ?>">
<img src="qrcode_image.php?type=dynamic&id=<?php echo $row['id']; ?>" class="qr-thumb-img" alt="QR code for <?php echo htmlspecialchars($row['filename']); ?>">
</a>
</td> </td>
<td><?php echo htmlspecialchars($row['scan']); ?></td> <td><?php echo htmlspecialchars($row['scan']); ?></td>
<td><?php echo htmlspecialchars($row['state']); ?></td> <td><?php echo htmlspecialchars($row['state']); ?></td>
<td> <td>
<?php if (!$is_readonly_user): ?>
<!-- EDIT --> <!-- EDIT -->
<a href="dynamic_qrcode.php?edit=true&id=<?php echo $row['id']; ?>" class="btn btn-primary"><i class="fas fa-edit"></i></a> <a href="dynamic_qrcode.php?edit=true&id=<?php echo $row['id']; ?>" class="btn btn-primary"><i class="fas fa-edit"></i></a>
@@ -90,12 +78,9 @@
data-target="#delete-modal" data-target="#delete-modal"
data-del_id="<?php echo $row["id"];?>" data-del_id="<?php echo $row["id"];?>"
><i class="fas fa-trash"></i></a> ><i class="fas fa-trash"></i></a>
<?php endif; ?>
<!-- DOWNLOAD -->
<a href="qrcode_image.php?type=dynamic&id=<?php echo $row['id']; ?>&download=1" class="btn btn-primary"><i class="fa fa-download"></i></a>
<!-- COPY TO CLIPBOARD --> <!-- DOWNLOAD -->
<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> <a href="<?php echo SAVED_QRCODE_FOLDER.htmlspecialchars($row['qrcode']); ?>" class="btn btn-primary" download><i class="fa fa-download"></i></a>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -111,12 +96,10 @@
</div><!-- /.col --> </div><!-- /.col -->
</div><!-- /.row --> </div><!-- /.row -->
<?php if (!$is_readonly_user): ?>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div class="modal fade" id="delete-modal" role="dialog"> <div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<form action="dynamic_qrcode.php" method="POST"> <form action="dynamic_qrcode.php" method="POST">
<?php echo csrf_field(); ?>
<!-- Modal content --> <!-- Modal content -->
<div class="modal-content"> <div class="modal-content">
@@ -137,33 +120,6 @@
</div> </div>
</div> </div>
<!-- /.Delete Confirmation Modal --> <!-- /.Delete Confirmation Modal -->
<?php endif; ?>
<!-- Qr Code Preview Modal -->
<div class="modal fade" id="preview-modal" role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="preview-modal-title">QR code preview</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body text-center">
<img id="preview-modal-img" src="" alt="" style="max-width:100%;max-height:70vh;">
</div>
</div>
</div>
</div>
<!-- /.Qr Code Preview Modal -->
<script>
document.querySelectorAll('.qr-thumb-link').forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
document.getElementById('preview-modal-img').src = link.getAttribute('data-qr-src');
document.getElementById('preview-modal-title').textContent = link.getAttribute('data-qr-name');
});
});
</script>
<script> <script>
const deleteButtons = document.querySelectorAll('.delete_btn'); const deleteButtons = document.querySelectorAll('.delete_btn');
+4 -48
View File
@@ -1,11 +1,8 @@
<?php $is_readonly_user = $_SESSION['type'] === 'user'; ?>
<div class="row"> <div class="row">
<?php if (!$is_readonly_user): ?>
<div class="col-12" id="bulk-action-div" style="display: none;"> <div class="col-12" id="bulk-action-div" style="display: none;">
<div id="err-msg"></div> <div id="err-msg"></div>
<div class="bulk-action-wrapper"> <div class="bulk-action-wrapper">
<form id="bulk-action" action="bulk_action.php" method="POST"> <form id="bulk-action" action="bulk_action.php" method="POST">
<?php echo csrf_field(); ?>
<div class="col-sm-12 mb-2" style="margin-left: 10px"> <div class="col-sm-12 mb-2" style="margin-left: 10px">
<div class="row"> <div class="row">
<div class="col-5 col-md-2"> <div class="col-5 col-md-2">
@@ -23,16 +20,13 @@
</form> </form>
</div> </div>
</div> </div>
<?php endif; ?>
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-body table-responsive p-0"> <div class="card-body table-responsive p-0">
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<?php if (!$is_readonly_user): ?>
<th><input type="checkbox" name="bulk-select" value="1"></th> <th><input type="checkbox" name="bulk-select" value="1"></th>
<?php endif; ?>
<th>ID</th> <th>ID</th>
<th>Owner</th> <th>Owner</th>
<th>Filename</th> <th>Filename</th>
@@ -45,9 +39,7 @@
<tbody> <tbody>
<?php foreach ($rows as $row): ?> <?php foreach ($rows as $row): ?>
<tr> <tr>
<?php if (!$is_readonly_user): ?>
<td><input type="checkbox" name="action[]" value="<?=$row['id']?>" onchange="updateBulkActionVisibility()"></td> <td><input type="checkbox" name="action[]" value="<?=$row['id']?>" onchange="updateBulkActionVisibility()"></td>
<?php endif; ?>
<td><?php echo $row['id']; ?></td> <td><?php echo $row['id']; ?></td>
<td> <td>
<?php <?php
@@ -68,14 +60,10 @@
<td><?php echo htmlspecialchars($row['type']); ?></td> <td><?php echo htmlspecialchars($row['type']); ?></td>
<td><?php echo htmlspecialchars_decode($row['content']); ?></td> <td><?php echo htmlspecialchars_decode($row['content']); ?></td>
<td> <td>
<a href="#" class="qr-thumb-link" data-toggle="modal" data-target="#preview-modal" <?php echo '<img src="'.SAVED_QRCODE_FOLDER.htmlspecialchars($row['qrcode']).'" width="100" height="100">'; ?>
data-qr-src="qrcode_image.php?type=static&id=<?php echo $row['id']; ?>"
data-qr-name="<?php echo htmlspecialchars($row['filename']); ?>">
<img src="qrcode_image.php?type=static&id=<?php echo $row['id']; ?>" class="qr-thumb-img" alt="QR code for <?php echo htmlspecialchars($row['filename']); ?>">
</a>
</td> </td>
<td> <td>
<?php if (!$is_readonly_user): ?>
<!-- EDIT --> <!-- EDIT -->
<a href="static_qrcode.php?edit=true&id=<?php echo $row['id']; ?>" class="btn btn-primary"><i class="fas fa-edit"></i></a> <a href="static_qrcode.php?edit=true&id=<?php echo $row['id']; ?>" class="btn btn-primary"><i class="fas fa-edit"></i></a>
@@ -86,12 +74,9 @@
data-target="#delete-modal" data-target="#delete-modal"
data-del_id="<?php echo $row["id"];?>" data-del_id="<?php echo $row["id"];?>"
><i class="fas fa-trash"></i></a> ><i class="fas fa-trash"></i></a>
<?php endif; ?>
<!-- DOWNLOAD -->
<a href="qrcode_image.php?type=static&id=<?php echo $row['id']; ?>&download=1" class="btn btn-primary"><i class="fa fa-download"></i></a>
<!-- COPY TO CLIPBOARD --> <!-- DOWNLOAD -->
<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> <a href="<?php echo SAVED_QRCODE_FOLDER.htmlspecialchars($row['qrcode']); ?>" class="btn btn-primary" download><i class="fa fa-download"></i></a>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -107,12 +92,10 @@
</div><!-- /.col --> </div><!-- /.col -->
</div><!-- /.row --> </div><!-- /.row -->
<?php if (!$is_readonly_user): ?>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div class="modal fade" id="delete-modal" role="dialog"> <div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<form action="static_qrcode.php" method="POST"> <form action="static_qrcode.php" method="POST">
<?php echo csrf_field(); ?>
<!-- Modal content --> <!-- Modal content -->
<div class="modal-content"> <div class="modal-content">
@@ -133,33 +116,6 @@
</div> </div>
</div> </div>
<!-- /.Delete Confirmation Modal --> <!-- /.Delete Confirmation Modal -->
<?php endif; ?>
<!-- Qr Code Preview Modal -->
<div class="modal fade" id="preview-modal" role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="preview-modal-title">QR code preview</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body text-center">
<img id="preview-modal-img" src="" alt="" style="max-width:100%;max-height:70vh;">
</div>
</div>
</div>
</div>
<!-- /.Qr Code Preview Modal -->
<script>
document.querySelectorAll('.qr-thumb-link').forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
document.getElementById('preview-modal-img').src = link.getAttribute('data-qr-src');
document.getElementById('preview-modal-title').textContent = link.getAttribute('data-qr-name');
});
});
</script>
<script> <script>
const deleteButtons = document.querySelectorAll('.delete_btn'); const deleteButtons = document.querySelectorAll('.delete_btn');
-1
View File
@@ -47,7 +47,6 @@
<div class="modal fade" id="delete-modal" role="dialog"> <div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<form action="user.php" method="POST"> <form action="user.php" method="POST">
<?php echo csrf_field(); ?>
<!-- Modal content --> <!-- Modal content -->
<div class="modal-content"> <div class="modal-content">
-39
View File
@@ -117,45 +117,6 @@ function paginationLinks($current_page, $total_pages, $base_url) {
return $html; 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() { function base_url() {
require_once(__DIR__ . '/../config/environment.php'); require_once(__DIR__ . '/../config/environment.php');
if (defined('BASE_URL') && BASE_URL !== null) { if (defined('BASE_URL') && BASE_URL !== null) {
+1 -2
View File
@@ -3,9 +3,8 @@
//If User is logged in the session['user_logged_in'] will be set to true //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 user is Not Logged in, redirect to login.php page.
if (empty($_SESSION['user_logged_in'])) { if (!isset($_SESSION['user_logged_in'])) {
header('Location:login.php'); header('Location:login.php');
exit;
} }
?> ?>
-14
View File
@@ -1,14 +0,0 @@
<?php
/**
* Centrale bootstrap voor elke entrypoint: config laden, sessie starten met
* verharde instellingen, sessie-timeout en verplichte wachtwoordwijziging afdwingen.
*
* Vervangt de losse "session_start(); require_once 'config/config.php';" aanroepen.
*/
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/security.php';
qr_session_start();
qr_enforce_session_timeout();
qr_enforce_password_change();
+2 -9
View File
@@ -6,10 +6,9 @@
<!-- Main Footer --> <!-- Main Footer -->
<footer class="main-footer text-sm"> <footer class="main-footer text-sm">
<strong><a href="https://www.qrforge.eu" target="_blank">QRForge</a></strong> - <strong><a href="https://github.com/giandonatoinverso/PHP-Dynamic-Qr-code" target="_blank">PHP Qrcode Generator</a> by </strong> Giandonato Inverso
<a href="./about.php">About</a> / credits
<div class="float-right d-none d-sm-inline-block"> <div class="float-right d-none d-sm-inline-block">
<b>Version</b> 3.0 <b>Version</b> 2.3.0
</div> </div>
</footer> </footer>
</div> </div>
@@ -30,9 +29,3 @@
<script src="plugins/daterangepicker/daterangepicker.js"></script> <script src="plugins/daterangepicker/daterangepicker.js"></script>
<!-- Overlay scrollbar --> <!-- Overlay scrollbar -->
<script type="text/javascript" src="plugins/overlayScrollbars/js/OverlayScrollbars.js"></script> <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>
-7
View File
@@ -1,13 +1,6 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="csrf-token" content="<?php echo csrf_token(); ?>">
<meta name="theme-color" content="#2563EB">
<link rel="manifest" href="manifest.json">
<link rel="icon" href="dist/img/brand/favicon.svg" type="image/svg+xml">
<link rel="icon" href="dist/img/brand/favicon-32.png" sizes="32x32" type="image/png">
<link rel="icon" href="dist/img/brand/favicon-16.png" sizes="16x16" type="image/png">
<link rel="apple-touch-icon" href="dist/img/brand/apple-touch-icon.png">
<!-- Font Awesome Icons --> <!-- Font Awesome Icons -->
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css"> <link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
+6 -2
View File
@@ -16,10 +16,14 @@
</a> </a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a href="./change_password.php" class="dropdown-item"> <!--<a href="#" class="dropdown-item">
<i class="fas fa-key"></i> Change password <i class="fas fa-user"></i> Profile
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a href="#" class="dropdown-item">
<i class="fa fa-cog"></i> Settings
</a>-->
<div class="dropdown-divider"></div>
<a href="./logout.php" class="dropdown-item"> <a href="./logout.php" class="dropdown-item">
<i class="fas fa-sign-out-alt"></i> Logout <i class="fas fa-sign-out-alt"></i> Logout
</a> </a>
-210
View File
@@ -1,210 +0,0 @@
<?php
/**
* Fase 1 security hardening: sessiebeheer, CSRF, rate limiting, audit log.
* Wordt geladen via includes/bootstrap.php, dat als eerste in elke entrypoint hoort te staan.
*/
define('SESSION_IDLE_TIMEOUT', 30 * 60); // 30 minuten inactiviteit -> uitloggen
define('LOGIN_MAX_ATTEMPTS', 5);
define('LOGIN_LOCKOUT_WINDOW', 15 * 60); // 15 minuten
/**
* Start de sessie met verharde cookie-instellingen. Moet vóór elke output aangeroepen worden.
*/
function qr_session_start() {
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
$is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
ini_set('session.gc_maxlifetime', (string) SESSION_IDLE_TIMEOUT);
ini_set('session.use_strict_mode', '1');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => $is_https,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
function qr_client_ip() {
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
/**
* Logt de gebruiker uit als de sessie te lang inactief is geweest.
*/
function qr_enforce_session_timeout() {
if (empty($_SESSION['user_logged_in'])) {
return;
}
$now = time();
if (isset($_SESSION['last_activity']) && ($now - $_SESSION['last_activity']) > SESSION_IDLE_TIMEOUT) {
$_SESSION = [];
$_SESSION['login_failure'] = 'Je sessie is verlopen wegens inactiviteit. Log opnieuw in.';
header('Location: login.php');
exit;
}
$_SESSION['last_activity'] = $now;
}
/**
* Stuurt ingelogde gebruikers met een verplichte wachtwoordwijziging naar change_password.php,
* behalve op de wijzigingspagina en logout zelf.
*/
function qr_enforce_password_change() {
if (empty($_SESSION['user_logged_in']) || empty($_SESSION['must_change_password'])) {
return;
}
$current_script = basename(parse_url($_SERVER['SCRIPT_NAME'], PHP_URL_PATH));
$exempt = ['change_password.php', 'logout.php'];
if (in_array($current_script, $exempt, true)) {
return;
}
header('Location: change_password.php');
exit;
}
/**
* CSRF-bescherming
*/
function csrf_token() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field() {
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8') . '">';
}
function csrf_is_valid($token) {
return isset($_SESSION['csrf_token']) && is_string($token) && hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Voor klassieke form-POSTs: verwacht een verborgen veld "csrf_token".
*/
function csrf_verify_or_die() {
if (!csrf_is_valid($_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('403 Forbidden: invalid or missing CSRF token.');
}
}
/**
* Voor JSON/AJAX-endpoints (bv. bulk_action.php): verwacht header X-CSRF-Token.
*/
function csrf_verify_header_or_die() {
$token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!csrf_is_valid($token)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['data' => 'Invalid or missing CSRF token', 'status' => 403]);
exit;
}
}
/**
* Rate limiting op login
*/
function qr_record_login_attempt($username, $success) {
$db = getDbInstance();
$db->insert('login_attempts', [
'username' => $username,
'ip_address' => qr_client_ip(),
'success' => $success ? 1 : 0,
'attempted_at' => date('Y-m-d H:i:s'),
]);
}
function qr_is_login_locked_out($username) {
$db = getDbInstance();
$window_start = date('Y-m-d H:i:s', time() - LOGIN_LOCKOUT_WINDOW);
$db->where('username', $username);
$db->where('success', 0);
$db->where('attempted_at', $window_start, '>=');
$count = $db->getValue('login_attempts', 'count(*)');
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.
*
* Uses a single raw, parenthesized condition rather than where()+orWhere() -
* the previous two-call form produced "WHERE id = ? AND id_owner = ? OR id_owner
* IS NULL" whenever a caller had already added its own where('id', ...) (e.g.
* qrcode_image.php, bulk_action.php), and AND binds tighter than OR in SQL, so
* the OR silently detached from the id filter and matched *any* id_owner-NULL
* row instead of the one actually requested.
*/
function qr_apply_owner_scope($db) {
$scope_owner_id = qr_scope_owner_id();
if ($scope_owner_id !== null) {
$db->where('(id_owner = ' . (int) $scope_owner_id . ' OR id_owner IS NULL)');
}
}
/**
* Audit log
*/
function audit_log($action, $target_type = null, $target_id = null) {
$db = getDbInstance();
$db->insert('audit_log', [
'user_id' => $_SESSION['user_id'] ?? null,
'username' => $_SESSION['username'] ?? null,
'action' => $action,
'target_type' => $target_type,
'target_id' => $target_id !== null ? (string) $target_id : null,
'ip_address' => qr_client_ip(),
'user_agent' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
'created_at' => date('Y-m-d H:i:s'),
]);
}
+6 -36
View File
@@ -2,9 +2,9 @@
<aside class="main-sidebar sidebar-dark-primary elevation-4"> <aside class="main-sidebar sidebar-dark-primary elevation-4">
<!-- Brand Logo --> <!-- Brand Logo -->
<a href="./index.php" class="brand-link"> <a href="./index.php" class="brand-link">
<img src="dist/img/brand/icon-inverse.svg" alt="QRForge" class="brand-image" <img src="dist/img/Symbol_WhiteBlue.png" alt="Logo" class="brand-image"
style="opacity: .8"> style="opacity: .8">
<span class="brand-text font-weight-light">QRForge</span> <span class="brand-text font-weight-light">Qrcode Generator</span>
</a> </a>
<!-- Sidebar --> <!-- Sidebar -->
@@ -33,15 +33,6 @@
</p> </p>
</a> </a>
</li> </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"'; ?>> <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"'; ?>> <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> <i class="nav-icon fa fa-qrcode"></i>
@@ -53,28 +44,18 @@
<ul class="nav nav-treeview"> <ul class="nav nav-treeview">
<li class="nav-item"> <li class="nav-item">
<a href="./dynamic_qrcodes.php" <?php echo ((substr(CURRENT_PAGE, 0, 19) == 'dynamic_qrcodes.php')) ? ' class="nav-link active"' : ' class="nav-link"'; ?>> <a href="./dynamic_qrcodes.php" <?php echo ((substr(CURRENT_PAGE, 0, 19) == 'dynamic_qrcodes.php')) ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="fas fa-angle-right nav-icon"></i> <i class="far fa-circle nav-icon"></i>
<p>List all</p> <p>List all</p>
</a> </a>
</li> </li>
<?php if ($_SESSION['type'] !== 'user'): ?>
<li class="nav-item"> <li class="nav-item">
<a href="./dynamic_qrcode.php" <?php echo (CURRENT_PAGE == 'dynamic_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>> <a href="./dynamic_qrcode.php" <?php echo (CURRENT_PAGE == 'dynamic_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="fas fa-angle-right nav-icon"></i> <i class="far fa-circle nav-icon"></i>
<p>Add new</p> <p>Add new</p>
</a> </a>
</li> </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="fas fa-angle-right nav-icon"></i>
<p>Batch create (CSV)</p>
</a>
</li>
<?php endif; ?>
</ul> </ul>
</li> </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"'; ?>> <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"'; ?>> <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> <i class="nav-icon fa fa-qrcode"></i>
@@ -86,35 +67,24 @@
<ul class="nav nav-treeview"> <ul class="nav nav-treeview">
<li class="nav-item"> <li class="nav-item">
<a href="./static_qrcodes.php" <?php echo ((substr(CURRENT_PAGE, 0, 18) == 'static_qrcodes.php')) ? ' class="nav-link active"' : ' class="nav-link"'; ?>> <a href="./static_qrcodes.php" <?php echo ((substr(CURRENT_PAGE, 0, 18) == 'static_qrcodes.php')) ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="fas fa-angle-right nav-icon"></i> <i class="far fa-circle nav-icon"></i>
<p>List all</p> <p>List all</p>
</a> </a>
</li> </li>
<?php if ($_SESSION['type'] !== 'user'): ?>
<li class="nav-item"> <li class="nav-item">
<a href="./static_qrcode.php" <?php echo (CURRENT_PAGE == 'static_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>> <a href="./static_qrcode.php" <?php echo (CURRENT_PAGE == 'static_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="fas fa-angle-right nav-icon"></i> <i class="far fa-circle nav-icon"></i>
<p>Add new</p> <p>Add new</p>
</a> </a>
</li> </li>
<?php endif; ?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php if (in_array($_SESSION['type'], ['super', 'admin'], true)): ?>
<li class="nav-item"> <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"'; ?>> <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> <i class="fas fa-users nav-icon"></i>
<p>Users</p> <p>Users</p>
</a> </a>
</li> </li>
<?php endif; ?>
<li class="nav-item">
<a href="./about.php" <?php echo (CURRENT_PAGE == 'about.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
<i class="fas fa-info-circle nav-icon"></i>
<p>About</p>
</a>
</li>
</ul> </ul>
</nav> </nav>
<!-- /.sidebar-menu --> <!-- /.sidebar-menu -->
+28 -17
View File
@@ -1,33 +1,48 @@
<?php <?php
require_once 'includes/bootstrap.php'; //Use httponly flag
ini_set('session.cookie_httponly', 1);
//Use only cookies
ini_set('session.use_only_cookies', 1);
//Use secure flag
ini_set('session.cookie_secure', 1);
session_start();
require_once './config/config.php';
require_once 'includes/auth_validate.php'; require_once 'includes/auth_validate.php';
$db = getDbInstance(); $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 //Get Dynamic qr code rows
qr_apply_owner_scope($db); if($_SESSION['type'] !== 'super') {
$db->where("id_owner", $_SESSION['user_id']);
$db->orWhere ("id_owner", NULL, 'IS');
}
$numQrcode_dynamic = $db->getValue("dynamic_qrcodes", "count(*)"); $numQrcode_dynamic = $db->getValue("dynamic_qrcodes", "count(*)");
//Get Static qr code rows //Get Static qr code rows
qr_apply_owner_scope($db); if($_SESSION['type'] !== 'super') {
$db->where("id_owner", $_SESSION['user_id']);
$db->orWhere ("id_owner", NULL, 'IS');
}
$numQrcode_static = $db->getValue("static_qrcodes", "count(*)"); $numQrcode_static = $db->getValue("static_qrcodes", "count(*)");
$total = $numQrcode_dynamic + $numQrcode_static; $total = $numQrcode_dynamic + $numQrcode_static;
//Get Total scan //Get Total scan
qr_apply_owner_scope($db); if($_SESSION['type'] !== 'super') {
$db->where("id_owner", $_SESSION['user_id']);
$db->orWhere ("id_owner", NULL, 'IS');
}
$numScan = $db->getOne("dynamic_qrcodes", "sum(scan) as numScan"); $numScan = $db->getOne("dynamic_qrcodes", "sum(scan) as numScan");
/* CREATED CHART */ /* 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 //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 //Get the number of DYNAMIC qr code created in 7 days and total scan
if(!$is_full_visibility) 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`= " . (int) qr_scope_owner_id() . " OR `id_owner` IS NULL);"); $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);");
else else
$createdQrcode_dynamic = $db->query("select `created_at`, `scan` from ".DATABASE_PREFIX."dynamic_qrcodes where `created_at` > curdate()-7;"); $createdQrcode_dynamic = $db->query("select `created_at`, `scan` from ".DATABASE_PREFIX."dynamic_qrcodes where `created_at` > curdate()-7;");
@@ -49,8 +64,8 @@ foreach ($createdQrcode_dynamic as $row) {
/* SCAN CHART */ /* SCAN CHART */
//Get the number of STATIC qr code created in 7 days //Get the number of STATIC qr code created in 7 days
if(!$is_full_visibility) if($_SESSION['type'] !== 'super')
$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);"); $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);");
else else
$createdQrcode_static = $db->query("select `created_at` from ".DATABASE_PREFIX."static_qrcodes where `created_at` > curdate()-7;"); $createdQrcode_static = $db->query("select `created_at` from ".DATABASE_PREFIX."static_qrcodes where `created_at` > curdate()-7;");
@@ -70,7 +85,7 @@ foreach ($createdQrcode_static as $row) {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<title>QRForge</title> <title>Qr Code Generator</title>
<head> <head>
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; ?>
</head> </head>
@@ -120,7 +135,6 @@ foreach ($createdQrcode_static as $row) {
</div><!-- /.col --> </div><!-- /.col -->
<div class="col-12 col-sm-6 col-md-3"> <div class="col-12 col-sm-6 col-md-3">
<a href="dynamic_qrcodes.php" class="d-block">
<div class="info-box mb-3 bg-success"> <div class="info-box mb-3 bg-success">
<span class="info-box-icon"><i class="fa fa-qrcode"></i></span> <span class="info-box-icon"><i class="fa fa-qrcode"></i></span>
@@ -129,14 +143,12 @@ foreach ($createdQrcode_static as $row) {
<span class="info-box-number"><?php echo $numQrcode_dynamic; ?></span> <span class="info-box-number"><?php echo $numQrcode_dynamic; ?></span>
</div><!-- /.info-box-content --> </div><!-- /.info-box-content -->
</div> </div>
</a>
</div><!-- /.col --> </div><!-- /.col -->
<!-- fix for small devices only --> <!-- fix for small devices only -->
<div class="clearfix hidden-md-up"></div> <div class="clearfix hidden-md-up"></div>
<div class="col-12 col-sm-6 col-md-3"> <div class="col-12 col-sm-6 col-md-3">
<a href="static_qrcodes.php" class="d-block">
<div class="info-box mb-3 bg-danger"> <div class="info-box mb-3 bg-danger">
<span class="info-box-icon"><i class="fa fa-qrcode"></i></span> <span class="info-box-icon"><i class="fa fa-qrcode"></i></span>
@@ -146,7 +158,6 @@ foreach ($createdQrcode_static as $row) {
</div><!-- /.info-box-content --> </div><!-- /.info-box-content -->
</div> </div>
</a>
</div><!-- /.col --> </div><!-- /.col -->
<div class="col-12 col-sm-6 col-md-3"> <div class="col-12 col-sm-6 col-md-3">
-54
View File
@@ -56,8 +56,6 @@ class DynamicQrcode {
* We save into db the url of qrcode image * We save into db the url of qrcode image
*/ */
public function addQrcode($input_data) { public function addQrcode($input_data) {
$this->validateLink($input_data['link'] ?? '');
if($input_data['id_owner'] != "") if($input_data['id_owner'] != "")
$data_to_db['id_owner'] = $input_data['id_owner']; $data_to_db['id_owner'] = $input_data['id_owner'];
else else
@@ -76,52 +74,11 @@ class DynamicQrcode {
$this->qrcode_instance->addQrcode($input_data, $data_to_db, $data_to_qrcode); $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 * Edit qr code
* *
*/ */
public function editQrcode($input_data) { public function editQrcode($input_data) {
$this->validateLink($input_data['link'] ?? '');
if($input_data['id_owner'] != "") if($input_data['id_owner'] != "")
$data_to_db['id_owner'] = $input_data['id_owner']; $data_to_db['id_owner'] = $input_data['id_owner'];
else else
@@ -160,17 +117,6 @@ class DynamicQrcode {
} }
/**
* Server-side validatie van de redirect-link (verplicht, max. 500 tekens per kolomdefinitie).
*/
private function validateLink($link) {
$link = trim((string) $link);
if ($link === '' || strlen($link) > 500) {
$this->failure('Link is required and must be at most 500 characters.');
}
}
/** /**
* Flash message Failure process * Flash message Failure process
*/ */
+1 -1
View File
@@ -830,7 +830,7 @@ class MysqliDb
* @return bool|array Boolean indicating the insertion failed (false), else return id-array ([int]) * @return bool|array Boolean indicating the insertion failed (false), else return id-array ([int])
* @throws Exception * @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 // only auto-commit our inserts, if no transaction is currently running
$autoCommit = (isset($this->_transaction_in_progress) ? !$this->_transaction_in_progress : true); $autoCommit = (isset($this->_transaction_in_progress) ? !$this->_transaction_in_progress : true);
+7 -224
View File
@@ -20,8 +20,6 @@ class Qrcode {
private string $table; private string $table;
private string $redirect_url; private string $redirect_url;
const ALLOWED_FORMATS = ['png', 'gif', 'jpeg', 'jpg', 'svg', 'svgbw', 'eps'];
/** /**
* *
*/ */
@@ -45,174 +43,10 @@ class Qrcode {
{ {
} }
/**
* Voorkomt path traversal / arbitrary file write via een gemanipuleerde bestandsnaam.
*/
private function sanitizeFilename($filename) {
$filename = trim((string) $filename);
if ($filename === '' || strlen($filename) > 45) {
throw new \InvalidArgumentException('Filename must be between 1 and 45 characters.');
}
if (preg_match('#[\\/\\\\]#', $filename) || strpos($filename, '..') !== false || strpos($filename, "\0") !== false) {
throw new \InvalidArgumentException('Filename cannot contain path separators.');
}
return $filename;
}
private function validateFormat($format) {
$format = strtolower((string) $format);
if (!in_array($format, self::ALLOWED_FORMATS, true)) {
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.625);
$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) { public function getQrcode($id) {
require_once BASE_PATH . '/includes/security.php';
$db = getDbInstance(); $db = getDbInstance();
$db->where('id', $id); $db->where('id', $id);
qr_apply_owner_scope($db);
$result = $db->getOne($this->table); $result = $db->getOne($this->table);
if($result !== NULL) if($result !== NULL)
@@ -258,41 +92,8 @@ class Qrcode {
* We save into db the url of qrcode image * We save into db the url of qrcode image
*/ */
public function addQrcode($input_data, $data_to_db, $data_to_qrcode) { 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); $options = $this->setOptions($input_data);
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
$data_to_db['format'] = $this->validateFormat($data_to_db['format']);
$outputInterface = QRGdImagePNG::class; $outputInterface = QRGdImagePNG::class;
$imageFormat = strtolower($data_to_db['format']); $imageFormat = strtolower($data_to_db['format']);
$fileExt = $imageFormat; $fileExt = $imageFormat;
@@ -491,14 +292,7 @@ class Qrcode {
} }
catch(Exception $e) catch(Exception $e)
{ {
throw new \RuntimeException($e->getMessage()); $this->failure($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 // If you want you can customi<e qr code with logo
@@ -508,13 +302,14 @@ class Qrcode {
$last_id = $db->insert($this->table, $data_to_db); $last_id = $db->insert($this->table, $data_to_db);
} }
else else
throw new \RuntimeException('You cannot create a new qr code with an existing name on the server!'); $this->failure('You cannot create a new qr code with an existing name on the server!');
if (!$last_id) { if ($last_id){
throw new \RuntimeException('Insert failed: ' . $db->getLastError()); $this->success('Qr code added successfully!');
}
else {
$this->failure('Insert failed: ' . $db->getLastError());
} }
return $last_id;
} }
/** /**
@@ -525,16 +320,10 @@ class Qrcode {
$db = getDbInstance(); $db = getDbInstance();
$old_qrcode = $this->getQrcode($input_data["id"]); $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"]; $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"]){ if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$old_qrcode["format"]) || $data_to_db['filename'] == $input_data["old_filename"]){
$db->where('id', $input_data["id"]); $db->where('id', $input_data["id"]);
qr_apply_owner_scope($db);
$stat = $db->update($this->table, $data_to_db); $stat = $db->update($this->table, $data_to_db);
try{ try{
@@ -548,7 +337,6 @@ class Qrcode {
$this->failure('You cannot edit a qr code with an existing name on the server!'); $this->failure('You cannot edit a qr code with an existing name on the server!');
if ($stat){ if ($stat){
audit_log('qrcode_updated', $this->table, $input_data['id']);
$this->success('Qr code updated successfully!'); $this->success('Qr code updated successfully!');
} }
else { else {
@@ -567,13 +355,8 @@ class Qrcode {
$qrcode = $this->getQrcode($id); $qrcode = $this->getQrcode($id);
$db->where('id', $id); $db->where('id', $id);
qr_apply_owner_scope($db);
$status = $db->delete($this->table); $status = $db->delete($this->table);
if ($status) {
audit_log('qrcode_deleted', $this->table, $id);
}
try{ try{
unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]); unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]);
} }
-256
View File
@@ -5,8 +5,6 @@ class Qrcode {
private string $table; private string $table;
private string $redirect_url; private string $redirect_url;
const ALLOWED_FORMATS = ['png', 'gif', 'jpeg', 'jpg', 'svg', 'eps'];
/** /**
* *
*/ */
@@ -30,174 +28,10 @@ class Qrcode {
{ {
} }
/**
* Voorkomt path traversal / arbitrary file write via een gemanipuleerde bestandsnaam.
*/
private function sanitizeFilename($filename) {
$filename = trim((string) $filename);
if ($filename === '' || strlen($filename) > 45) {
$this->failure('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.');
}
return $filename;
}
private function validateFormat($format) {
$format = strtolower((string) $format);
if (!in_array($format, self::ALLOWED_FORMATS, true)) {
$this->failure('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.625);
$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) { public function getQrcode($id) {
require_once BASE_PATH . '/includes/security.php';
$db = getDbInstance(); $db = getDbInstance();
$db->where('id', $id); $db->where('id', $id);
qr_apply_owner_scope($db);
$result = $db->getOne($this->table); $result = $db->getOne($this->table);
if($result !== NULL) if($result !== NULL)
@@ -245,9 +79,6 @@ class Qrcode {
public function addQrcode($input_data, $data_to_db, $data_to_qrcode) { public function addQrcode($input_data, $data_to_db, $data_to_qrcode) {
$options = $this->setOptions($input_data); $options = $this->setOptions($input_data);
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
$data_to_db['format'] = $this->validateFormat($data_to_db['format']);
if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$data_to_db['format'])){ if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$data_to_db['format'])){
$url = $url =
'https://api.qrserver.com/v1/create-qr-code/?data='. 'https://api.qrserver.com/v1/create-qr-code/?data='.
@@ -270,13 +101,6 @@ class Qrcode {
$this->failure($e->getMessage()); $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 // If you want you can customi<e qr code with logo
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']); //$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
@@ -287,7 +111,6 @@ class Qrcode {
$this->failure('You cannot create a new qr code with an existing name on the server!'); $this->failure('You cannot create a new qr code with an existing name on the server!');
if ($last_id){ if ($last_id){
audit_log('qrcode_created', $this->table, $last_id);
$this->success('Qr code added successfully!'); $this->success('Qr code added successfully!');
} }
else { else {
@@ -295,77 +118,6 @@ 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 * Edit qr code
* *
@@ -374,12 +126,10 @@ class Qrcode {
$db = getDbInstance(); $db = getDbInstance();
$old_qrcode = $this->getQrcode($input_data["id"]); $old_qrcode = $this->getQrcode($input_data["id"]);
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
$data_to_db['qrcode'] = $data_to_db['filename'].'.'.$old_qrcode["format"]; $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"]){ if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$old_qrcode["format"]) || $data_to_db['filename'] == $input_data["old_filename"]){
$db->where('id', $input_data["id"]); $db->where('id', $input_data["id"]);
qr_apply_owner_scope($db);
$stat = $db->update($this->table, $data_to_db); $stat = $db->update($this->table, $data_to_db);
try{ try{
@@ -393,7 +143,6 @@ class Qrcode {
$this->failure('You cannot edit a qr code with an existing name on the server!'); $this->failure('You cannot edit a qr code with an existing name on the server!');
if ($stat){ if ($stat){
audit_log('qrcode_updated', $this->table, $input_data['id']);
$this->success('Qr code updated successfully!'); $this->success('Qr code updated successfully!');
} }
else { else {
@@ -412,13 +161,8 @@ class Qrcode {
$qrcode = $this->getQrcode($id); $qrcode = $this->getQrcode($id);
$db->where('id', $id); $db->where('id', $id);
qr_apply_owner_scope($db);
$status = $db->delete($this->table); $status = $db->delete($this->table);
if ($status) {
audit_log('qrcode_deleted', $this->table, $id);
}
try{ try{
unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]); unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]);
} }
+8 -101
View File
@@ -330,49 +330,22 @@ class StaticQrcode {
/** /**
* create a qr code of type "bitcoin" * create a qr code of type "bitcoin"
* @string address -> required * @string address -> required
* @string amount -> optional (a bitcoin address is useful as a standing QR code, * @int amount -> required
* not just for one specific payment amount)
* @string label * @string label
* @string message * @string message
*/ */
public function bitcoinQrcode($address, $amount, $label, $message) public function bitcoinQrcode($address, $amount, $label, $message)
{ {
$address = trim((string) $address); if($address != NULL && $amount != NULL){
$amount = trim((string) $amount); $this->sData = 'bitcoin:'.$address.'?amount='.$amount.'&label='.$label.'&message='.$message;
$label = trim((string) $label); $this->sContent = '<strong>BTC address:</strong> '.$address.'<br>'.'<strong>Amount:</strong> '.$amount.'<br>';
$message = trim((string) $message); $this->sContent .= '<strong>Label:</strong> '.$label.'<br>'.'<strong>Message:</strong> '.$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"); $this->addQrcode("bitcoin");
} }
else
$this->requiredFieldsError();
}
/** /**
* create a qr code of type "2FA" * create a qr code of type "2FA"
@@ -405,68 +378,6 @@ class StaticQrcode {
$this->requiredFieldsError(); $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) { public function getQrcode($id) {
return $this->qrcode_instance->getQrcode($id); return $this->qrcode_instance->getQrcode($id);
} }
@@ -499,10 +410,6 @@ class StaticQrcode {
$input_data["foreground"] = $_POST['foreground']; $input_data["foreground"] = $_POST['foreground'];
$input_data["background"] = $_POST['background']; $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); $data_to_qrcode = urlencode($this->sData);
+17 -121
View File
@@ -3,8 +3,6 @@ require_once 'config/config.php';
class Users class Users
{ {
const ALLOWED_TYPES = ['super', 'admin', 'user'];
/** /**
* *
*/ */
@@ -12,25 +10,6 @@ class Users
{ {
} }
/**
* 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) {
return 'Username must be between 3 and 50 characters.';
}
if (!preg_match('/^[a-zA-Z0-9._-]+$/', $username)) {
return 'Username may only contain letters, numbers, dots, underscores and hyphens.';
}
if (!in_array($type, self::ALLOWED_TYPES, true)) {
return 'Invalid user type.';
}
return null;
}
/** /**
* *
*/ */
@@ -57,24 +36,6 @@ class Users
return $db->get(DATABASE_PREFIX.'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) { public function getUser($id) {
$db = getDbInstance(); $db = getDbInstance();
@@ -88,41 +49,14 @@ 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) { public function addUser($input_data) {
$db = getDbInstance(); $db = getDbInstance();
$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');
}
if (!isset($input_data['password']) || strlen($input_data['password']) < 10) {
$this->failure('Password must be at least 10 characters long.', 'Location: user.php');
}
$data_to_db["username"] = $input_data["username"]; $data_to_db["username"] = $input_data["username"];
$data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT); $data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
$data_to_db["type"] = $requested_type; $data_to_db["type"] = $input_data["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->where('username', $data_to_db['username']);
$db->get('users'); $db->get('users');
@@ -132,97 +66,59 @@ class Users
$last_id = $db->insert('users', $data_to_db); $last_id = $db->insert('users', $data_to_db);
if ($last_id) { if ($last_id)
audit_log('user_created', 'user', $last_id);
$this->success('User added successfully'); $this->success('User added successfully');
} }
}
/** /**
* 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) { public function editUser($input_data) {
$db = getDbInstance(); $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",
));
$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);
}
if (isset($input_data['password']) && strlen($input_data['password']) > 0 && strlen($input_data['password']) < 10) {
$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('username', $input_data['username']);
$db->where('id', $input_data["id"], '!='); $db->where('id', $input_data["id"], '!=');
$row = $db->getOne('users'); $row = $db->getOne('users');
if (!empty($row['username'])) { if (!empty($row['username'])) {
$query_string = http_build_query(array(
'id' => $input_data["id"],
'edit' => "true",
));
$this->failure('Username already exists', 'Location: user.php?'.$query_string); $this->failure('Username already exists', 'Location: user.php?'.$query_string);
} }
$data_to_db["username"] = $input_data["username"]; $data_to_db["username"] = $input_data["username"];
$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;
// 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); $data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
} $data_to_db["type"] = $input_data["type"];
$db->where('id', $input_data["id"]); $db->where('id', $input_data["id"]);
$stat = $db->update('users', $data_to_db); $stat = $db->update('users', $data_to_db);
if ($stat) { if ($stat)
audit_log('user_updated', 'user', $input_data['id']);
$this->success('User updated successfully!'); $this->success('User updated successfully!');
} else else
$this->failure('Failed to update User: ' . $db->getLastError()); $this->failure('Failed to update User: ' . $db->getLastError());
} }
/** /**
* Delete user. * Delete user
* *
* An 'admin' may only delete their own 'user' accounts; 'super' can delete anyone.
*/ */
public function deleteUser($id) { public function deleteUser($id) {
$db = getDbInstance(); if($_SESSION['type']!='super'){
$db->where('id', $id); header('HTTP/1.1 401 Unauthorized', true, 401);
$target = $db->getOne('users'); exit("401 Unauthorized");
if (!$this->canManage($target)) {
header('HTTP/1.1 403 Forbidden', true, 403);
exit('403 Forbidden');
} }
$db = getDbInstance(); $db = getDbInstance();
$db->where('id', $id); $db->where('id', $id);
$stat = $db->delete('users'); $stat = $db->delete('users');
if ($stat) { if ($stat)
audit_log('user_deleted', 'user', $id);
$this->info('User deleted successfully!'); $this->info('User deleted successfully!');
} else else
$this->failure('Unable to delete user'); $this->failure('Unable to delete user');
} }
+4 -16
View File
@@ -1,12 +1,12 @@
<?php <?php
require_once 'includes/bootstrap.php'; session_start();
require_once 'config/config.php';
$token = bin2hex(openssl_random_pseudo_bytes(16)); $token = bin2hex(openssl_random_pseudo_bytes(16));
// If User has already logged in, redirect to dashboard page. // If User has already logged in, redirect to dashboard page.
if (isset($_SESSION['user_logged_in']) && $_SESSION['user_logged_in'] === TRUE) if (isset($_SESSION['user_logged_in']) && $_SESSION['user_logged_in'] === TRUE)
{ {
header('Location: index.php'); header('Location: index.php');
exit;
} }
// If user has previously selected "remember me option": // If user has previously selected "remember me option":
@@ -33,20 +33,9 @@ if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token']))
exit; exit;
} }
session_regenerate_id(true);
$_SESSION['user_logged_in'] = TRUE; $_SESSION['user_logged_in'] = TRUE;
$_SESSION['user_id'] = $row['id']; $_SESSION['user_id'] = $row['id'];
$_SESSION['type'] = $row['type']; $_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');
header('Location: index.php'); header('Location: index.php');
exit; exit;
} }
@@ -68,13 +57,13 @@ if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token']))
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<title>Login - QRForge</title> <title>Login - Qrcode Generator</title>
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; ?>
<body class="login-page" style="min-height: 512.391px;"> <body class="login-page" style="min-height: 512.391px;">
<div class="login-box"> <div class="login-box">
<div class="login-logo"> <div class="login-logo">
<img src="dist/img/brand/logo.svg" alt="QRForge" style="max-width: 260px;"> <img src="dist/img/DynamicQRCode_Original.png" style="width: 95%; height: 95%">
</div> </div>
<div class="card"> <div class="card">
@@ -82,7 +71,6 @@ if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token']))
<p class="login-box-msg">Sign in to start your session</p> <p class="login-box-msg">Sign in to start your session</p>
<form method="POST" action="authenticate.php"> <form method="POST" action="authenticate.php">
<?php echo csrf_field(); ?>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="text" name="username" class="form-control" placeholder="Username" required="required"> <input type="text" name="username" class="form-control" placeholder="Username" required="required">
<div class="input-group-append"> <div class="input-group-append">
+4 -14
View File
@@ -1,20 +1,10 @@
<?php <?php
require_once 'includes/bootstrap.php'; require_once './config/config.php';
session_start();
if (!empty($_SESSION['user_logged_in'])) {
audit_log('logout');
}
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy(); session_destroy();
if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token'])) {
if(isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token'])){
clearAuthCookie(); clearAuthCookie();
} }
header('Location:index.php'); header('Location:index.php');
-14
View File
@@ -1,14 +0,0 @@
{
"name": "QRForge",
"short_name": "QRForge",
"description": "Self-hosted static and dynamic qr code generator",
"start_url": "index.php",
"scope": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563EB",
"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
@@ -1,89 +0,0 @@
<?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
@@ -1,63 +0,0 @@
<?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
@@ -1,34 +0,0 @@
<?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;
+8 -31
View File
@@ -1,47 +1,24 @@
<?php <?php
include 'config/config.php'; include 'config/config.php';
if ($_SERVER["REQUEST_METHOD"] !== "GET" || !isset($_GET['id'])) { if($_SERVER["REQUEST_METHOD"] !== "GET" || !isset($_GET['id']))
die("Method not allowed. Check id parameter"); die("Method not allowed. Check id parameter");
}
// Validation and sanitization of the input UPDATE to php 8.3
$id = filter_input(INPUT_GET, 'id', FILTER_UNSAFE_RAW);
$id = trim(strip_tags($id));
if (!$id) {
die("Invalid ID parameter");
}
$db = getDbInstance(); $db = getDbInstance();
// Using prepared statements to avoid SQL injections $db->where("identifier", $_GET['id']);
$db->where("identifier", $id);
$qrcode = $db->getOne("dynamic_qrcodes"); $qrcode = $db->getOne("dynamic_qrcodes");
if (!$qrcode) { $data = array (
die("QR code not found");
}
$data = array(
'scan' => $db->inc(1) 'scan' => $db->inc(1)
); );
$db->where("identifier", $_GET['id']);
$db->update ('dynamic_qrcodes', $data);
$db->where("identifier", $id); if($qrcode['state'] == 'enable'){
if (!$db->update('dynamic_qrcodes', $data)) { echo '<meta http-equiv="refresh" content="0; URL='.$qrcode['link'].'" />';
die("Failed to update scan count");
}
if ($qrcode['state'] == 'enable') {
// Validation and escaping of the URL to avoid XSS attacks
$link = filter_var($qrcode['link'], FILTER_VALIDATE_URL);
if ($link) {
echo '<meta http-equiv="refresh" content="0; URL=' . htmlspecialchars($link, ENT_QUOTES, 'UTF-8') . '" />';
echo 'Loading...'; // You can include a custom page to display during the redirect echo 'Loading...'; // You can include a custom page to display during the redirect
} else {
echo 'Invalid URL';
} }
} else { else
echo 'Disabled link'; echo 'Disabled link';
}
?> ?>
Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Some files were not shown because too many files have changed in this diff Show More