Compare commits
156 Commits
v0.9
..
ba57334142
| Author | SHA1 | Date | |
|---|---|---|---|
| ba57334142 | |||
| 615d983828 | |||
| b245694824 | |||
| 378968576f | |||
| 0d4368575b | |||
| 05e80a075a | |||
| 92a1f17dbe | |||
| d1505e1e08 | |||
| f05c073ad4 | |||
| 79d9ac71a3 | |||
| ffc5b64967 | |||
| 869dd2c799 | |||
| 531820e2a8 | |||
| 630bbf3aea | |||
| 8238a81494 | |||
| cc45f0659c | |||
| ad37224890 | |||
| 9b65bb8020 | |||
| 97481c2444 | |||
| 8151de4b9a | |||
| 477d7803f0 | |||
| 8cc5294a84 | |||
| e6aac416d9 | |||
| e84f853d80 | |||
| 7d5a4b889c | |||
| c157815ca8 | |||
| 2d9b6162e1 | |||
| 096c239715 | |||
| 5bdcb8e2bf | |||
| 98ae82a040 | |||
| 15b5ece4f9 | |||
| b03238b4c3 | |||
| 2287d98455 | |||
| 3e43b72eec | |||
| 5488ad0a84 | |||
| 9710ae0673 | |||
| 893b883dbf | |||
| 60620e15dd | |||
| ec786d2956 | |||
| a4d8455e5a | |||
| 5ac338945d | |||
| 6e213483a4 | |||
| 8a926ac0f3 | |||
| 0360176490 | |||
| 57930cf9db | |||
| 6682a207b2 | |||
| c326a30afc | |||
| 28545c2245 | |||
| 1085a13d38 | |||
| 13a807ac85 | |||
| 876b6736b6 | |||
| e36c26a37a | |||
| a2ce4b38b6 | |||
| 12e358b87c | |||
| 617d08c13d | |||
| 5612558ad1 | |||
| e827b55f6b | |||
| 3ea78d6a84 | |||
| e947153e74 | |||
| 4e3cfeeaa3 | |||
| f750d7aaca | |||
| c6cb83638a | |||
| 7e70ca94d8 | |||
| 7d7c326795 | |||
| 16327a0e0a | |||
| 53955af19c | |||
| 4aab7d637a | |||
| a6b2b29352 | |||
| dac89ba0b2 | |||
| f064d8f1ef | |||
| cccf3ada43 | |||
| b5aec38e45 | |||
| 654d395369 | |||
| f410322119 | |||
| cbc5246163 | |||
| d99e8132c9 | |||
| 99a361c0be | |||
| 64974c6c51 | |||
| 52e2347644 | |||
| 304b808bb6 | |||
| 9f376bd3c9 | |||
| fca443b6cc | |||
| 1f64d4cad7 | |||
| b7b090f6c5 | |||
| 52d890597d | |||
| 40ab7b256d | |||
| bbf115c2ae | |||
| b433a83578 | |||
| c005b52211 | |||
| e61c38473c | |||
| 1243b32de1 | |||
| a20810d650 | |||
| c4d5440453 | |||
| 975fde9c35 | |||
| af363723b2 | |||
| e2ff6e585b | |||
| 389123fe15 | |||
| a519d7bfe6 | |||
| a98c89075d | |||
| 00d7dfdb0b | |||
| 08ac31210c | |||
| fef45c401a | |||
| 21b779c581 | |||
| 618030e9d3 | |||
| 59cee36c3a | |||
| 88028393cd | |||
| de377902be | |||
| ee711a5796 | |||
| e599aed16f | |||
| 0d77e8f3a4 | |||
| 6ed9018086 | |||
| 8cee68e091 | |||
| 4615be4280 | |||
| 6e7b7cfa77 | |||
| 74f92ca2fa | |||
| 2dc2e2fe16 | |||
| 37dc9baa97 | |||
| 0337838319 | |||
| c544e0c7b5 | |||
| 087e3e83b5 | |||
| 2ae9f32cb8 | |||
| 5924afa3dc | |||
| b302e0cce1 | |||
| 1f9fa672c6 | |||
| bd610dc68f | |||
| dd9d5ae2fb | |||
| 0e62c29f11 | |||
| 14610fe212 | |||
| 8bcb852ea9 | |||
| 0bfe40eafe | |||
| 1e6bc3af4e | |||
| fa9e6730cc | |||
| 6709776cc5 | |||
| df04139a05 | |||
| 4a377b635c | |||
| 8262550a10 | |||
| 31ab8efc05 | |||
| 3ee55c9b17 | |||
| 93cd20c49d | |||
| eff10b3ee6 | |||
| 6de33ea45c | |||
| 34bfee494e | |||
| 3f8209a6a6 | |||
| 2b93634d3a | |||
| c698fc34f1 | |||
| 93d362aba2 | |||
| 8fe055a5b1 | |||
| 8a64294455 | |||
| 854b0ca77d | |||
| 7c707f865c | |||
| 439d136f54 | |||
| d0f2d526f1 | |||
| 9c855a19bb | |||
| a7e532867a | |||
| 67e6d85da9 | |||
| 35c72f6199 |
@@ -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
|
|
||||||
@@ -2,4 +2,3 @@
|
|||||||
.project
|
.project
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
[](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).
|
|
||||||
@@ -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 */;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,92 +1,66 @@
|
|||||||
<?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'];
|
||||||
|
|
||||||
// Voorkom session fixation: nieuwe sessie-id na een geslaagde login.
|
|
||||||
session_regenerate_id(true);
|
|
||||||
|
|
||||||
$_SESSION['user_logged_in'] = TRUE;
|
|
||||||
$_SESSION['type'] = $row['type'];
|
|
||||||
$_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'];
|
$user_id = $row['id'];
|
||||||
|
|
||||||
if ($remember)
|
if (password_verify($password, $db_password))
|
||||||
{
|
{
|
||||||
$series_id = randomString(16);
|
$_SESSION['user_logged_in'] = TRUE;
|
||||||
$remember_token = getSecureRandomToken(20);
|
$_SESSION['type'] = $row['type'];
|
||||||
$encryted_remember_token = password_hash($remember_token,PASSWORD_DEFAULT);
|
$_SESSION['user_id'] = $row['id'];
|
||||||
|
|
||||||
$expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days'));
|
if ($remember)
|
||||||
$expires = strtotime($expiry_time);
|
{
|
||||||
$is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
$series_id = randomString(16);
|
||||||
|
$remember_token = getSecureRandomToken(20);
|
||||||
|
$encryted_remember_token = password_hash($remember_token,PASSWORD_DEFAULT);
|
||||||
|
|
||||||
$cookie_options = [
|
$expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days'));
|
||||||
'expires' => $expires,
|
$expires = strtotime($expiry_time);
|
||||||
'path' => '/',
|
|
||||||
'secure' => $is_https,
|
|
||||||
'httponly' => true,
|
|
||||||
'samesite' => 'Lax',
|
|
||||||
];
|
|
||||||
|
|
||||||
setcookie('series_id', $series_id, $cookie_options);
|
setcookie('series_id', $series_id, $expires, '/');
|
||||||
setcookie('remember_token', $remember_token, $cookie_options);
|
setcookie('remember_token', $remember_token, $expires, '/');
|
||||||
|
|
||||||
$db = getDbInstance();
|
$db = getDbInstance();
|
||||||
$db->where ('id',$user_id);
|
$db->where ('id',$user_id);
|
||||||
|
|
||||||
$update_remember = array(
|
$update_remember = array(
|
||||||
'series_id'=> $series_id,
|
'series_id'=> $series_id,
|
||||||
'remember_token' => $encryted_remember_token,
|
'remember_token' => $encryted_remember_token,
|
||||||
'expires' =>$expiry_time
|
'expires' =>$expiry_time
|
||||||
);
|
);
|
||||||
$db->update('users', $update_remember);
|
$db->update('users', $update_remember);
|
||||||
|
}
|
||||||
|
// Authentication successfull redirect user
|
||||||
|
header('Location: index.php');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$_SESSION['login_failure'] = 'Invalid username or password';
|
||||||
|
header('Location: login.php');
|
||||||
}
|
}
|
||||||
// Authentication successfull redirect user
|
|
||||||
header('Location: index.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;
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
@@ -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>
|
|
||||||
@@ -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=');
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 755 B |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -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).'); });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 31 KiB |
@@ -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> </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>
|
||||||
@@ -120,70 +77,34 @@
|
|||||||
<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="frame_text">Frame text</label>
|
<label for="filename">Filename *</label>
|
||||||
<input type="text" name="frame_text" value="" placeholder="e.g. Scan me" maxlength="60" class="form-control" id="frame_text">
|
<input type="text" name="filename" value="" placeholder="My first Qrcode" class="form-control error" required="required" id="filename">
|
||||||
<small class="form-text text-muted">Optional label rendered below the code. Only applies to PNG/JPEG/GIF, not SVG/EPS.</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-6 col-md-2">
|
<div class="col-6 col-md-1">
|
||||||
<div class="form-group">
|
<label for="format">Format *</label>
|
||||||
<label for="frame_font">Frame font</label>
|
<select name="format" class="form-control" required="required">
|
||||||
<select name="frame_font" id="frame_font" class="form-control">
|
<option value="png" selected>PNG</option>
|
||||||
<option value="sans" selected>Sans</option>
|
<option value="gif">GIF</option>
|
||||||
<option value="sans-bold">Sans Bold</option>
|
<option value="jpeg">JPEG</option>
|
||||||
<option value="serif">Serif</option>
|
<option value="jpg">JPG</option>
|
||||||
<option value="serif-bold">Serif Bold</option>
|
<option value="svg">SVG</option>
|
||||||
<option value="mono">Monospace</option>
|
<option value="eps">EPS</option>
|
||||||
<option value="mono-bold">Monospace Bold</option>
|
</select>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if($_SESSION['type'] === 'super') { ?>
|
||||||
<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="form-group">
|
|
||||||
<label for="filename">Filename *</label>
|
|
||||||
<input type="text" name="filename" value="" placeholder="My first Qrcode" class="form-control error" required="required" id="filename">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-6 col-md-1">
|
|
||||||
<label for="format">Format *</label>
|
|
||||||
<select name="format" class="form-control" required="required">
|
|
||||||
<option value="png" selected>PNG</option>
|
|
||||||
<option value="gif">GIF</option>
|
|
||||||
<option value="jpeg">JPEG</option>
|
|
||||||
<option value="jpg">JPG</option>
|
|
||||||
<option value="svg">SVG</option>
|
|
||||||
<option value="eps">EPS</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($_SESSION['type'] === 'super') { ?>
|
|
||||||
<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>
|
||||||
<?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 } ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -116,53 +66,13 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
|||||||
<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="frame_text">Frame text</label>
|
<label for="filename">Filename *</label>
|
||||||
<input type="text" name="frame_text" value="" placeholder="e.g. Scan me" maxlength="60" class="form-control" id="frame_text">
|
<input type="text" name="filename" value="" placeholder="My first Qrcode" class="form-control error" required="required" id = "filename">
|
||||||
<small class="form-text text-muted">Optional label rendered below the code. Only applies to PNG/JPEG/GIF, not SVG/EPS.</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-6 col-md-2">
|
<div class="col-6 col-md-1">
|
||||||
<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="col-sm-4">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="filename">Filename *</label>
|
|
||||||
<input type="text" name="filename" value="" placeholder="My first Qrcode" class="form-control error" required="required" id="filename">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-6 col-md-1">
|
|
||||||
<label for="format">Format</label>
|
<label for="format">Format</label>
|
||||||
<select name="format" class="form-control">
|
<select name="format" class="form-control">
|
||||||
<option value="png">PNG</option>
|
<option value="png">PNG</option>
|
||||||
@@ -177,31 +87,34 @@ 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-4">
|
<div class="col-sm-12 mb-2">
|
||||||
<div class="form-group">
|
<div class="row">
|
||||||
<label for="id_owner">Owner *</label>
|
<div class="col-sm-4">
|
||||||
<select name="id_owner" id="id_owner" class="form-control">
|
<div class="form-group">
|
||||||
<option value="">All (shared with every admin)</option>
|
<label for="id_owner">Owner *</label>
|
||||||
<?php
|
<select name="id_owner" class="form-control">
|
||||||
|
<option value="" selected>All</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
require_once BASE_PATH . '/lib/Users/Users.php';
|
require_once BASE_PATH . '/lib/Users/Users.php';
|
||||||
$users_instance = new Users();
|
$users_instance = new Users();
|
||||||
$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 $user["username"];?></option>
|
||||||
<option value="<?php echo $user["id"];?>" <?php echo $is_self ? 'selected' : ''; ?>><?php echo $user["username"];?></option>
|
<?php } ?>
|
||||||
<?php } ?>
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php } else { ?>
|
|
||||||
<input type="hidden" name="id_owner" value="<?php echo $_SESSION["user_id"];?>"/>
|
|
||||||
<?php } ?>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<?php } else { ?>
|
||||||
|
<input type="hidden" name="id_owner" value="<?php echo $_SESSION["user_id"];?>"/>
|
||||||
|
<?php } ?>
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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,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,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,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">
|
||||||
|
|||||||
@@ -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,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,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,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,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,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,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,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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">×</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,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">×</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');
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
@@ -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();
|
|
||||||
@@ -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>
|
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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,33 +135,29 @@ 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>
|
|
||||||
|
|
||||||
<div class="info-box-content">
|
<div class="info-box-content">
|
||||||
<span class="info-box-text">Dynamic Qr codes</span>
|
<span class="info-box-text">Dynamic Qr codes</span>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="info-box-content">
|
<div class="info-box-content">
|
||||||
<span class="info-box-text">Static QR codes</span>
|
<span class="info-box-text">Static QR codes</span>
|
||||||
<span class="info-box-number"><?php echo $numQrcode_static; ?></span>
|
<span class="info-box-number"><?php echo $numQrcode_static; ?></span>
|
||||||
</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">
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
|
||||||
'&&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"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,48 +330,21 @@ 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->addQrcode("bitcoin");
|
||||||
|
}
|
||||||
|
else
|
||||||
$this->requiredFieldsError();
|
$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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
|
||||||
$data_to_db['can_view_static'] = !empty($input_data['can_view_static']) ? 1 : 0;
|
$data_to_db["type"] = $input_data["type"];
|
||||||
$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);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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']);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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';
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 558 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 539 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 895 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |