Compare commits
10 Commits
195de75b78
...
66f40e5e97
Author | SHA1 | Date | |
---|---|---|---|
66f40e5e97 | |||
02f46cc08c | |||
d60230f975 | |||
d03c0a5331 | |||
dbd27b266d | |||
f96616bcef | |||
eeb73eccd4 | |||
d3a537aa6c | |||
801bbebf4f | |||
86abf587f6 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -14,9 +14,10 @@ storage/logs
|
|||||||
# Testing stuff
|
# Testing stuff
|
||||||
/docker-compose.yml
|
/docker-compose.yml
|
||||||
scratch
|
scratch
|
||||||
|
storage.bak
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
tkr.tgz
|
tkr.tgz
|
||||||
|
|
||||||
# Test logs
|
# Test logs
|
||||||
storage/prerequisite-check.log
|
storage/prerequisite-check.log
|
||||||
|
231
README.md
231
README.md
@ -2,95 +2,77 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
A lightweight, HTML-only status feed for self-hosted personal websites. Written in PHP. Heavily inspired by [status.cafe](https://status.cafe).
|
A simple, HTML-only status feed for self-hosted personal websites. Written in PHP. Heavily inspired by [status.cafe](https://status.cafe).
|
||||||
|
|
||||||
## Screenshots
|
## LLM Disclosure
|
||||||
|
|
||||||
### Mobile
|
I used Claude for guidance on some portions of this. I limited it to things that I couldn't find good answers to otherwise, and I used it as a learning tool. I've adapted any LLM-generated code suggestions, but if you strongly object to LLMs and don't want any LLM-assisted code on your site, then you shouldn't use this.
|
||||||
|
|
||||||
<img src="https://subcultureofone.org/images/tkr/tkr-logged-out-mobile-v4.png"
|
## Requirements
|
||||||
alt="tkr logged out view - mobile"
|
|
||||||
width="40%" height="40%">
|
|
||||||
<img src="https://subcultureofone.org/images/tkr/tkr-logged-in-mobile-v4.png"
|
|
||||||
alt="tkr logged in view - mobile"
|
|
||||||
width="40%" height="40%">
|
|
||||||
|
|
||||||
### Desktop
|
* A server running Linux
|
||||||
|
* A web server, such as apache or nginx
|
||||||
<img src="https://subcultureofone.org/images/tkr/tkr-logged-out-desktop-v4.png"
|
* Web server configs are in the `examples` directory
|
||||||
alt="tkr logged in view - desktop"
|
* PHP 8.2+
|
||||||
width="60%" height="60%">
|
* Required extensions:
|
||||||
|
* [PDO](https://www.php.net/pdo)
|
||||||
<img src="https://subcultureofone.org/images/tkr/tkr-logged-in-desktop-v4.png"
|
* [PDO_SQLITE](https://www.php.net/pdo-sqlite)
|
||||||
alt="tkr logged in view - desktop"
|
* Recommended extensions:
|
||||||
width="60%" height="60%">
|
* [mbstring](https://www.php.net/mbstring)
|
||||||
|
* [fileinfo](https://www.php.net/fileinfo)
|
||||||
## Features
|
|
||||||
|
|
||||||
* HTML and CSS implementation. No Javascript.
|
|
||||||
* Accessible by default
|
|
||||||
* RSS `/feed/rss` and Atom `/feed/atom` feeds
|
|
||||||
* CSS uploads for custom theming
|
|
||||||
* Custom emoji to personalize moods (unicode only)
|
|
||||||
|
|
||||||
I'm trying to make sure that the HTML is both semantically valid and accessible, but I have a lot to learn about both. If you see something I should fix, please let me know!
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
* A web server with PHP support, such as:
|
|
||||||
* Apache with mod_php
|
|
||||||
* nginx and php-fpm
|
|
||||||
* PHP 8.2+ with the PDO and PDO_SQLITE extensions
|
|
||||||
* The PDO and PDO_SQLITE extensions are usually included by default
|
|
||||||
* This might work with earlier PHP versions, but I've only tested 8.2
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Download the latest tkr archive from [the packages page](https://gitea.subcultureofone.org/greg/tkr/packages)
|
1. Get the latest package from https://gitea.subcultureofone.org/greg/tkr/packages
|
||||||
1. Copy the `.tgz` file to your server and extract it
|
1. Copy it to your web server
|
||||||
1. Copy the `tkr` directory to the location you want to serve it from
|
1. Set up the directory permissions:
|
||||||
* on debian-based systems, `/var/www/tkr` is recommended
|
* `tkr/storage` must be writable by the web server account
|
||||||
1. Make the `storage` directory writable by the web server account.
|
* `www-data` on debian-based systems
|
||||||
```sh
|
* `apache` on redhat-based systems
|
||||||
chown www-data:www-data /path/to/tkr/storage
|
* All other `tkr` directories should be *readable* by the web server account.
|
||||||
chmod 0770 /path/to/tkr/storage
|
* For example, if you're on debian:
|
||||||
```
|
```sh
|
||||||
1. Add the necessary web server configuration.
|
# Make 'root' the owner of everything under 'tkr'
|
||||||
* Examples for common scenarios can be found in the [examples](./examples) directory.
|
chown -R root:root tkr
|
||||||
* Apache VPS, subdomain (e.g. `https://tkr.your-domain.com`): [examples/apache/vps/root](./examples/apache/vps/root)
|
|
||||||
* Apache VPS, subfolder (e.g. `https://your-domain.com/tkr`): [examples/apache/vps/subfolder](./examples/apache/vps/subfolder)
|
|
||||||
* Nginx VPS, subdomain (e.g. `https://tkr.your-domain.com`): [examples/nginx/root](./examples/nginx/root)
|
|
||||||
* Nginx VPS, subfolder (e.g. `https://your-domain.com/tkr`): [examples/nginx/subfolder](./examples/nginx/subfolder)
|
|
||||||
* Any values that need to be configured for your environment are labeled with `CONFIG`.
|
|
||||||
* The SSL configurations are basic, but should work. For more robust SSL configurations, see https://ssl-config.mozilla.org
|
|
||||||
|
|
||||||
|
# Make 'www-data' the owner of everything under 'tkr/storage'
|
||||||
|
chown -R www-data:www-data tkr/storage
|
||||||
|
|
||||||
## Initial configuration
|
# Make 'tkr/storage' writable bt 'www-data'
|
||||||
|
chmod 0770 tkr/storage
|
||||||
|
```
|
||||||
|
1. Configure your web server for your environment and serving model:
|
||||||
|
* There are example web server configs for common scenarios in the `examples` directory.
|
||||||
|
* apache shared hosting (e.g. everything served from `public_html`, .htaccess only)
|
||||||
|
* apache VPS
|
||||||
|
* subfolder: e.g. https://example.com/tkr
|
||||||
|
* subdomain: e.g. https://tke.example.com
|
||||||
|
* nginx VPS
|
||||||
|
* subfolder: e.g. https://example.com/tkr
|
||||||
|
* subdomain: e.g. https://tke.example.com
|
||||||
|
* Expose only the `public` folder if possible
|
||||||
|
* The VPS examples are configured this way.
|
||||||
|
* If this isn't possible (e.g. shared hosting), it's okay. There are `.htaccess` files included to block access to other directories in shared hosting environments.
|
||||||
|
1. Run the installation:
|
||||||
|
* Browser-based:
|
||||||
|
* Navigate to the tkr site (e.g. https://my-domain.org/tkr or https://tkr.my-domain.org)
|
||||||
|
* You'll be presented with the setup page.
|
||||||
|
* Complete the required fields.
|
||||||
|
* Command-line:
|
||||||
|
* ssh to your web server
|
||||||
|
* run `php /path/to/tkr/tkr-setup.php`
|
||||||
|
|
||||||
1. Run `php tkr/prerequisites.php`. This will confirm that:
|
If any prerequisites are missing (required PHP extensions, directory permissions)
|
||||||
1. PHP 8.2+ is installed
|
|
||||||
1. All required PHP extensions are installed
|
## Features
|
||||||
1. PDO
|
|
||||||
1. PDO::sqlite
|
* RSS `/rss` and Atom `/atom` feeds
|
||||||
1. All required directories exist
|
* HTML and CSS implementation. No Javascript.
|
||||||
1. The `tkr/storage` directory exists and is writable
|
* Accessible HTML, with strict accessibility settng to aid tab navigation
|
||||||
1. If `tkr/storage` is writable, then it will create the required subdirectories
|
* CSS uploads for custom theming
|
||||||
1. `tkr/storage/db`
|
* Custom emoji to personalize moods
|
||||||
1. `tkr/storage/upload`
|
|
||||||
1. The script will write a summary to stdout and will save a log at `tkr/storage/prerequisite-check.log`
|
I'm trying to make sure that the HTML is both semantically valid and accessible, but I have a lot to learn about both. If you see something I should fix, please let me know!
|
||||||
1. Edit `config/init.php` to set the domain and base path correctly for your configuration.
|
|
||||||
* subdirectory installation (e.g. https://my-domain.com/tkr)
|
|
||||||
```
|
|
||||||
'base_url' => 'https://my-domain.com',
|
|
||||||
'base_path' => '/tkr/',
|
|
||||||
```
|
|
||||||
* subdomain installation (e.g. https://tkr.my-domain.com)
|
|
||||||
```
|
|
||||||
'base_url' => 'https://tkr.my-domain.com',
|
|
||||||
'base_path' => '/',
|
|
||||||
```
|
|
||||||
1. Browse to your tkr URL. You'll be presented with the setup screen to complete initial configuration.
|
|
||||||

|
|
||||||
|
|
||||||
### Server configuration notes
|
### Server configuration notes
|
||||||
|
|
||||||
@ -108,8 +90,7 @@ There is an `.htaccess` file in the `tkr/` root directory. It's designed for the
|
|||||||
* `tkr/storage`
|
* `tkr/storage`
|
||||||
* `tkr/templates`
|
* `tkr/templates`
|
||||||
|
|
||||||
|
### Docker compose (local development)
|
||||||
### Docker compose
|
|
||||||
|
|
||||||
The [docker](./docker) directory contains docker-compose.yml files and web server configs for some different server configurations. For simplicity, these do not use SSL.
|
The [docker](./docker) directory contains docker-compose.yml files and web server configs for some different server configurations. For simplicity, these do not use SSL.
|
||||||
|
|
||||||
@ -117,44 +98,62 @@ To run tkr locally on your machine, copy the docker-compose file you're interest
|
|||||||
|
|
||||||
## Accessibility Note
|
## Accessibility Note
|
||||||
|
|
||||||
The "Strict Accessibility" setting (enabled by default) addes `tabindex="0"` to all `<a>` tags to force them to get tab focus. This isn't strictly best practice. The `<a>` tag should get tab focus by default. But I've learned that some browsers (at least Safari and Vivaldi) disable this in their default configurations, making accessibility an opt-in feature.
|
The "Strict Accessibility" setting (enabled by default) adds `tabindex="0"` to all `<a>` tags to force them to get tab focus. This isn't strictly best practice. The `<a>` tag should get tab focus by default. But I've learned that some browsers (at least Safari and Vivaldi) disable this in their default configurations, making accessibility an opt-in feature.
|
||||||
|
|
||||||
If you'd like to revert to the standard behavior, toggle this setting off. But know that people who navigate by keyboard may have to reconfigure their browser settings in order to select hyperlinks.
|
If you'd like to revert to the standard behavior, toggle this setting off. But know that people who navigate by keyboard may have to reconfigure their browser settings in order to select hyperlinks.
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
Ticks are stored in files on the filesystem under `/tkr/storage/ticks`. This directory must be writable by the web server user and so SHOULD NOT be served by the web server. If you set your document root to `/tkr/public/`, then you'll be fine.
|
tkr stores data at `tkr/storage`. This directory must be writable by the web server account and so *should not* be served by the web server. If you made `tkr/public` your document root, then you're fine. If you can't, then the .htaccess file in that directory will block access if you're running apache.
|
||||||
|
|
||||||
The file structure is `YYYY/MM/DD.txt`. That is, each day's ticks are located in a file whose full path is `/tkr/storage/ticks/YEAR/MONTH/DAY.txt`. This is to prevent any single file from getting too large.
|
There are 3 subdirectories:
|
||||||
|
|
||||||
Each entry takes the form `TIMESTAMP|TICK`, where `TIMESTAMP` is the time that the entry was made and `TICK` is the text of the entry.
|
* `tkr/storage/db`:
|
||||||
|
* Contains a sqlite database: `tkr.sqlite`.
|
||||||
|
* The database contains site settings, user profile data, ticks, and custom emoji.
|
||||||
|
* `tkr/storage/logs`:
|
||||||
|
* Runtime log files.
|
||||||
|
* Logs rotate after 1,000 lines.
|
||||||
|
* The last 5 log files are retained.
|
||||||
|
* `tkr/storage/uploads`:
|
||||||
|
* Stores custom CSS for personalized theming
|
||||||
|
|
||||||
For illustration, here's a sample from the file `/tkr/storage/ticks/2025/05/25` on my test system.
|
### SQLite Database Note
|
||||||
|
|
||||||
```sh
|
|
||||||
# cat /tkr/ticks/2025/05/25.txt
|
|
||||||
23:27:37|some stuff
|
|
||||||
23:27:45|some more, stuff
|
|
||||||
```
|
|
||||||
|
|
||||||
### SQLite Database
|
|
||||||
|
|
||||||
tkr stores profile information, custom emojis, and uploaded css metadata in a SQLite database located at `tkr/storage/db/tkr.sqlite`.
|
|
||||||
|
|
||||||
You don't have to do any database setup. The database is automatically created and initialized on first run.
|
You don't have to do any database setup. The database is automatically created and initialized on first run.
|
||||||
|
|
||||||
## FAQ
|
## Backup and restore
|
||||||
|
|
||||||
### Why don't I see the right IPs in the logs?
|
tkr is completely self-contained. To back up tkr, just zip up the `tkr` directory and copy it to your backup location. To restore it, unzip it in the new location.
|
||||||
|
|
||||||
This can happen for a few reasons. Some common ones are:
|
Technically, you only need back up and restore the `tkr/storage` directory if you want to minimize the size of the backup, but the app is pretty small (~40 Kb compressed).
|
||||||
|
|
||||||
**Docker Development:** If running via Docker, you may see `192.168.65.1` (Docker Desktop gateway). This is normal for development.
|
## Screenshots
|
||||||
|
|
||||||
**Behind a Proxy/CDN:** If you're behind Cloudflare (with proxy enabled), load balancers, or other proxies, all requests may appear to come from the proxy's IP addresses.
|
### Mobile
|
||||||
|
|
||||||
- **For accurate IP logging:** Configure your web server to trust proxy headers. See your proxy provider's documentation for the required nginx/Apache configuration.
|
<img src="https://subcultureofone.org/images/tkr/tkr-logged-out-mobile-1.0.png"
|
||||||
|
alt="tkr logged out view - mobile"
|
||||||
|
width="40%" height="40%">
|
||||||
|
<img src="https://subcultureofone.org/images/tkr/tkr-logged-in-mobile-1.0.png"
|
||||||
|
alt="tkr logged in view - mobile"
|
||||||
|
width="40%" height="40%">
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
|
||||||
|
<img src="https://subcultureofone.org/images/tkr/tkr-logged-out-desktop-1.0.png"
|
||||||
|
alt="tkr logged out view - desktop"
|
||||||
|
width="60%" height="60%">
|
||||||
|
|
||||||
|
<img src="https://subcultureofone.org/images/tkr/tkr-logged-in-desktop-1.0.png"
|
||||||
|
alt="tkr logged in view - desktop"
|
||||||
|
width="60%" height="60%">
|
||||||
|
|
||||||
|
### Setup validation failure example
|
||||||
|
|
||||||
|
<img src="https://subcultureofone.org/images/tkr/tkr-setup-failure-example-1.0.png"
|
||||||
|
alt="tkr setup validation failure"
|
||||||
|
width="60%" height="60%">
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
@ -162,30 +161,4 @@ It's been a lot of fun to get back to building something. I'm grateful to the pe
|
|||||||
|
|
||||||
* [armaina](https://armaina.com) - Armaina's a talented artist (check out the site!) who had the original idea for a self-hosted PHP version of status.cafe. That sounded like a fun project so I thought I'd see if I could manage it. This project doesn't exist without Armaina. Thank you!
|
* [armaina](https://armaina.com) - Armaina's a talented artist (check out the site!) who had the original idea for a self-hosted PHP version of status.cafe. That sounded like a fun project so I thought I'd see if I could manage it. This project doesn't exist without Armaina. Thank you!
|
||||||
* [status.cafe](https://status.cafe) - The technological inspiration. Unless you really want to self-host, you should use status.cafe instead! I took a lot of inspiration from its design and then I made the CSS way heavier and probably lost some of the soul along the way.
|
* [status.cafe](https://status.cafe) - The technological inspiration. Unless you really want to self-host, you should use status.cafe instead! I took a lot of inspiration from its design and then I made the CSS way heavier and probably lost some of the soul along the way.
|
||||||
* [32-bit cafe](https://32bit.cafe) - I started in technology as a hobbyist and idealist. Then I became a professional. The decades since have sucked the joy and the hope out of technology. 32-bit cafe reminded me that they're both still there.
|
* [32-bit cafe](https://32bit.cafe) - I started in technology as a hobbyist and idealist. Then I became a professional. The decades since have sucked the joy and the hope out of technology. 32-bit cafe reminded me that they're both still there.
|
||||||
|
|
||||||
## Tentative 0.y.z releases to 1.0
|
|
||||||
|
|
||||||
I'd like to alternate beteen architecture and feature releases between here and 1.0. This is my current thinking, but these may change.
|
|
||||||
|
|
||||||
### 0.7.5 (architecture improvements)
|
|
||||||
* Add linting and tests
|
|
||||||
* Add artifact build pipeline
|
|
||||||
|
|
||||||
### 0.8.0 (features and enhancements)
|
|
||||||
* Support microformats
|
|
||||||
* Support h-feed and JSON
|
|
||||||
|
|
||||||
### 0.8.5 (architecture improvements)
|
|
||||||
* Add docker build and deployment
|
|
||||||
|
|
||||||
### 0.9.0 (features and enhancements)
|
|
||||||
* Allow customization of time zone and time display for ticks
|
|
||||||
|
|
||||||
### 0.9.5 (architecture enhancements)
|
|
||||||
* Improve exception handling
|
|
||||||
* Add logging, including log viewer screen
|
|
||||||
|
|
||||||
### 1.0.0
|
|
||||||
* Polish README and other docs
|
|
||||||
* Set up dedicated webpage
|
|
@ -5,15 +5,23 @@ declare(strict_types=1);
|
|||||||
// - define paths
|
// - define paths
|
||||||
// - set up autoloader
|
// - set up autoloader
|
||||||
|
|
||||||
|
// Set a couple ini settings for security
|
||||||
|
ini_set('allow_url_fopen', 0); // don't allow remote files to be read
|
||||||
|
ini_set('expose_php', 0); // don't advertise the PHP version
|
||||||
|
|
||||||
// Define all the important paths
|
// Define all the important paths
|
||||||
define('APP_ROOT', dirname(dirname(__FILE__)));
|
define('APP_ROOT', dirname(dirname(__FILE__)));
|
||||||
|
// Root-level directories
|
||||||
define('CONFIG_DIR', APP_ROOT . '/config');
|
define('CONFIG_DIR', APP_ROOT . '/config');
|
||||||
|
define('PUBLIC_DIR', APP_ROOT . '/public');
|
||||||
define('SRC_DIR', APP_ROOT . '/src');
|
define('SRC_DIR', APP_ROOT . '/src');
|
||||||
define('STORAGE_DIR', APP_ROOT . '/storage');
|
define('STORAGE_DIR', APP_ROOT . '/storage');
|
||||||
define('TEMPLATES_DIR', APP_ROOT . '/templates');
|
// Storage subdirectories
|
||||||
define('DATA_DIR', STORAGE_DIR . '/db');
|
|
||||||
define('DB_FILE', DATA_DIR . '/tkr.sqlite');
|
|
||||||
define('CSS_UPLOAD_DIR', STORAGE_DIR . '/upload/css');
|
define('CSS_UPLOAD_DIR', STORAGE_DIR . '/upload/css');
|
||||||
|
define('DATA_DIR', STORAGE_DIR . '/db');
|
||||||
|
define('TEMPLATES_DIR', APP_ROOT . '/templates');
|
||||||
|
// Database file
|
||||||
|
define('DB_FILE', DATA_DIR . '/tkr.sqlite');
|
||||||
|
|
||||||
// Janky autoloader function
|
// Janky autoloader function
|
||||||
// This is a bit more consistent with current frameworks
|
// This is a bit more consistent with current frameworks
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE settings
|
||||||
|
ADD COLUMN tick_delete_hours INTEGER NULL;
|
22
docker/apache/bad-configs/.htaccess
Normal file
22
docker/apache/bad-configs/.htaccess
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Basic .htaccess for tkr on shared hosting
|
||||||
|
# For use with included docker-compose.yml
|
||||||
|
|
||||||
|
# Enable mod_rewrite
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Set directory index
|
||||||
|
DirectoryIndex public/index.php
|
||||||
|
|
||||||
|
# Block access to sensitive directories
|
||||||
|
RewriteRule ^(storage|src|templates|config)(/.*)?$ - [F,L]
|
||||||
|
|
||||||
|
# Block access to hidden files
|
||||||
|
RewriteRule ^\..*$ - [F,L]
|
||||||
|
|
||||||
|
# Block access to setup script
|
||||||
|
RewriteRule ^tkr-setup\.php$ - [F,L]
|
||||||
|
|
||||||
|
# Route everything else through public/index.php
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ public/index.php [L]
|
22
docker/apache/bad-configs/docker-compose.yml
Normal file
22
docker/apache/bad-configs/docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
apache-php-no-sqlite:
|
||||||
|
image: debian:bookworm
|
||||||
|
container_name: apache-no-sqlite-test
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./config:/var/www/html/tkr/config
|
||||||
|
- ./public:/var/www/html/tkr/public
|
||||||
|
- ./src:/var/www/html/tkr/src
|
||||||
|
- ./storage:/var/www/html/tkr/storage
|
||||||
|
- ./templates:/var/www/html/tkr/templates
|
||||||
|
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
|
||||||
|
- ./docker/apache/shared-hosting/.htaccess:/var/www/html/tkr/.htaccess
|
||||||
|
command: >
|
||||||
|
bash -c "apt-get update &&
|
||||||
|
apt-get install -y apache2 php libapache2-mod-php &&
|
||||||
|
a2enmod rewrite headers expires php8.2 &&
|
||||||
|
sed -i 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf &&
|
||||||
|
chown -R www-data:www-data /var/www/html/tkr/storage &&
|
||||||
|
apache2ctl -D FOREGROUND"
|
||||||
|
restart: unless-stopped
|
@ -1,49 +1,22 @@
|
|||||||
# Example Apache VirtualHost
|
# Basic .htaccess for tkr on shared hosting
|
||||||
# for serving tkr as a subdirectory path
|
# For use with included docker-compose.yml
|
||||||
# on shared hosting via .htaccess
|
|
||||||
#
|
|
||||||
# e.g. http://www.my-domain.com/tkr
|
|
||||||
#
|
|
||||||
# This should work without modification if you extract the app
|
|
||||||
# to /tkr from your web document root
|
|
||||||
|
|
||||||
# Enable mod_rewrite
|
# Enable mod_rewrite
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
|
||||||
# Security headers
|
# Set directory index
|
||||||
Header always set X-Frame-Options "SAMEORIGIN"
|
|
||||||
Header always set X-XSS-Protection "1; mode=block"
|
|
||||||
Header always set X-Content-Type-Options "nosniff"
|
|
||||||
|
|
||||||
# Directory index
|
|
||||||
DirectoryIndex public/index.php
|
DirectoryIndex public/index.php
|
||||||
|
|
||||||
# Security: Block direct access to .php files (except through rewrites)
|
# Block access to sensitive directories
|
||||||
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
RewriteRule ^(storage|src|templates|config)(/.*)?$ - [F,L]
|
||||||
RewriteRule ^.*$ - [R=404,L]
|
|
||||||
|
|
||||||
# Security: Block access to sensitive directories
|
# Block access to hidden files
|
||||||
RewriteRule ^(storage|src|templates|examples|config)(/.*)?$ - [F,L]
|
|
||||||
|
|
||||||
# Security: Block access to hidden files
|
|
||||||
RewriteRule ^\..*$ - [F,L]
|
RewriteRule ^\..*$ - [F,L]
|
||||||
|
|
||||||
# Cache CSS files for 1 hour
|
# Block access to setup script
|
||||||
<FilesMatch "\.css$">
|
RewriteRule ^tkr-setup\.php$ - [F,L]
|
||||||
Header set Cache-Control "public, max-age=3600"
|
|
||||||
</FilesMatch>
|
|
||||||
|
|
||||||
# Serve the one static file that exists: css/tkr.css
|
# Route everything else through public/index.php
|
||||||
# (Pass requests to css/custom/ through to the PHP app)
|
|
||||||
RewriteCond %{REQUEST_URI} !^/css/custom/
|
|
||||||
RewriteRule ^css/tkr\.css$ public/css/tkr.css [L]
|
|
||||||
|
|
||||||
# 404 all other static files (images, js, fonts, etc.)
|
|
||||||
# so those requests don't hit the PHP app
|
|
||||||
# (this is to reduce load on the PHP app from bots and scanners)
|
|
||||||
RewriteRule \.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$ - [R=404,L]
|
|
||||||
|
|
||||||
# Everything else goes to the front controller
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
RewriteRule ^(.*)$ public/index.php [L]
|
RewriteRule ^(.*)$ public/index.php [L]
|
||||||
|
@ -10,6 +10,7 @@ services:
|
|||||||
- ./src:/var/www/html/tkr/src
|
- ./src:/var/www/html/tkr/src
|
||||||
- ./storage:/var/www/html/tkr/storage
|
- ./storage:/var/www/html/tkr/storage
|
||||||
- ./templates:/var/www/html/tkr/templates
|
- ./templates:/var/www/html/tkr/templates
|
||||||
|
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
|
||||||
- ./docker/apache/shared-hosting/.htaccess:/var/www/html/tkr/.htaccess
|
- ./docker/apache/shared-hosting/.htaccess:/var/www/html/tkr/.htaccess
|
||||||
command: >
|
command: >
|
||||||
bash -c "a2enmod rewrite headers expires &&
|
bash -c "a2enmod rewrite headers expires &&
|
||||||
|
@ -10,6 +10,7 @@ services:
|
|||||||
- ./src:/var/www/tkr/src
|
- ./src:/var/www/tkr/src
|
||||||
- ./storage:/var/www/tkr/storage
|
- ./storage:/var/www/tkr/storage
|
||||||
- ./templates:/var/www/tkr/templates
|
- ./templates:/var/www/tkr/templates
|
||||||
|
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
|
||||||
- ./docker/apache/vps/root/tkr.my-domain.com.conf:/etc/apache2/sites-enabled/tkr.my-domain.com.conf
|
- ./docker/apache/vps/root/tkr.my-domain.com.conf:/etc/apache2/sites-enabled/tkr.my-domain.com.conf
|
||||||
command: >
|
command: >
|
||||||
bash -c "a2enmod rewrite headers expires &&
|
bash -c "a2enmod rewrite headers expires &&
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
# Example Apache VirtualHost
|
# Basic Apache VirtualHost for tkr
|
||||||
# for serving tkr as a subdomain root without SSL
|
# For use with included docker-compose.yml
|
||||||
# e.g. http://tkr.my-domain.com/
|
|
||||||
#
|
# HTTP - redirect to HTTPS
|
||||||
# NOTE: Do not use in production.
|
|
||||||
# This is provided for docker compose
|
|
||||||
# (The included docker-compose file will mount it in the container image)
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ServerName localhost
|
ServerName localhost
|
||||||
DocumentRoot /var/www/tkr/public
|
DocumentRoot /var/www/tkr/public
|
||||||
|
|
||||||
# Security headers
|
# Main directory - route everything through index.php
|
||||||
Header always set X-Frame-Options "SAMEORIGIN"
|
<Directory "/var/www/tkr/public">
|
||||||
Header always set X-XSS-Protection "1; mode=block"
|
AllowOverride None
|
||||||
Header always set X-Content-Type-Options "nosniff"
|
Require all granted
|
||||||
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
# Block access to sensitive directories
|
# Block access to sensitive directories
|
||||||
<Directory "/var/www/tkr/storage">
|
<Directory "/var/www/tkr/storage">
|
||||||
@ -22,59 +24,13 @@
|
|||||||
<Directory "/var/www/tkr/src">
|
<Directory "/var/www/tkr/src">
|
||||||
Require all denied
|
Require all denied
|
||||||
</Directory>
|
</Directory>
|
||||||
<Directory "/var/www/tkr/templates">
|
|
||||||
Require all denied
|
|
||||||
</Directory>
|
|
||||||
<Directory "/var/www/tkr/config">
|
<Directory "/var/www/tkr/config">
|
||||||
Require all denied
|
Require all denied
|
||||||
</Directory>
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/templates">
|
||||||
# Block access to hidden files
|
|
||||||
<DirectoryMatch "^\.|/\.">
|
|
||||||
Require all denied
|
Require all denied
|
||||||
</DirectoryMatch>
|
|
||||||
|
|
||||||
# Cache CSS files
|
|
||||||
<LocationMatch "\.css$">
|
|
||||||
Header set Cache-Control "public, max-age=3600"
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
# Serve static CSS file
|
|
||||||
Alias /css/tkr.css /var/www/tkr/public/css/tkr.css
|
|
||||||
|
|
||||||
# 404 all non-css static files (images, js, fonts, etc.)
|
|
||||||
# so those requests don't hit the PHP app
|
|
||||||
# (this is to reduce load on the PHP app from bots and scanners)
|
|
||||||
<LocationMatch "\.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$">
|
|
||||||
<RequireAll>
|
|
||||||
Require all denied
|
|
||||||
</RequireAll>
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
# Enable rewrite engine
|
|
||||||
<Directory "/var/www/tkr/public">
|
|
||||||
Options -Indexes
|
|
||||||
AllowOverride None
|
|
||||||
Require all granted
|
|
||||||
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
# Block direct PHP access
|
|
||||||
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
|
||||||
RewriteRule ^.*$ - [R=404,L]
|
|
||||||
|
|
||||||
# Serve the one static file that exists: css/tkr.css
|
|
||||||
# (Pass requests to css/custom/ through to the PHP app)
|
|
||||||
RewriteCond %{REQUEST_URI} !^/css/custom/
|
|
||||||
RewriteRule ^css/tkr\.css$ css/tkr.css [L]
|
|
||||||
|
|
||||||
# Everything else to front controller
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.*)$ index.php [L]
|
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
# Error and access logs
|
|
||||||
ErrorLog ${APACHE_LOG_DIR}/tkr_error.log
|
ErrorLog ${APACHE_LOG_DIR}/tkr_error.log
|
||||||
CustomLog ${APACHE_LOG_DIR}/tkr_access.log combined
|
CustomLog ${APACHE_LOG_DIR}/tkr_access.log combined
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
@ -10,6 +10,7 @@ services:
|
|||||||
- ./src:/var/www/tkr/src
|
- ./src:/var/www/tkr/src
|
||||||
- ./storage:/var/www/tkr/storage
|
- ./storage:/var/www/tkr/storage
|
||||||
- ./templates:/var/www/tkr/templates
|
- ./templates:/var/www/tkr/templates
|
||||||
|
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
|
||||||
- ./docker/apache/vps/subfolder/my-domain.com.conf:/etc/apache2/sites-enabled/my-domain.com.conf
|
- ./docker/apache/vps/subfolder/my-domain.com.conf:/etc/apache2/sites-enabled/my-domain.com.conf
|
||||||
command: >
|
command: >
|
||||||
bash -c "a2enmod rewrite headers expires &&
|
bash -c "a2enmod rewrite headers expires &&
|
||||||
|
@ -1,72 +1,38 @@
|
|||||||
# Example Apache VirtualHost
|
# Basic Apache config for tkr in subfolder
|
||||||
# for serving tkr as a subdirectory path without SSL
|
# e.g. https://your-domain.com/tkr
|
||||||
# e.g. http://www.my-domain.com/tkr
|
# For use with included docker-compose.yml
|
||||||
#
|
|
||||||
# NOTE: Do not use in production.
|
# Alias for tkr subfolder
|
||||||
# This is provided for docker compose
|
|
||||||
# (The included docker-compose file will mount it in the container image)
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ServerName localhost
|
ServerName localhost
|
||||||
DocumentRoot /var/www/html
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
Header always set X-Frame-Options "SAMEORIGIN"
|
|
||||||
Header always set X-XSS-Protection "1; mode=block"
|
|
||||||
Header always set X-Content-Type-Options "nosniff"
|
|
||||||
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
||||||
|
|
||||||
# tkr Application at /tkr
|
|
||||||
# NOTE: If you change the directory name,
|
|
||||||
# remember to update all instances of /var/www/tkr in this file to match
|
|
||||||
Alias /tkr /var/www/tkr/public
|
Alias /tkr /var/www/tkr/public
|
||||||
|
|
||||||
# Block access to sensitive TKR directories
|
<Directory "/var/www/tkr/public">
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
# Front controller pattern
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ /tkr/index.php [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Block access to sensitive directories
|
||||||
<Directory "/var/www/tkr/storage">
|
<Directory "/var/www/tkr/storage">
|
||||||
Require all denied
|
Require all denied
|
||||||
</Directory>
|
</Directory>
|
||||||
<Directory "/var/www/tkr/src">
|
<Directory "/var/www/tkr/src">
|
||||||
Require all denied
|
Require all denied
|
||||||
</Directory>
|
</Directory>
|
||||||
<Directory "/var/www/tkr/templates">
|
|
||||||
Require all denied
|
|
||||||
</Directory>
|
|
||||||
<Directory "/var/www/tkr/config">
|
<Directory "/var/www/tkr/config">
|
||||||
Require all denied
|
Require all denied
|
||||||
</Directory>
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/templates">
|
||||||
# 404 all non-css static files in /tkr (images, js, fonts, etc.)
|
Require all denied
|
||||||
# so those requests don't hit the PHP app
|
|
||||||
# (this is to reduce load on the PHP app from bots and scanners)
|
|
||||||
<LocationMatch "^/tkr/.*\.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$">
|
|
||||||
<RequireAll>
|
|
||||||
Require all denied
|
|
||||||
</RequireAll>
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
# tkr application directory
|
|
||||||
<Directory "/var/www/tkr/public">
|
|
||||||
Options -Indexes
|
|
||||||
AllowOverride None
|
|
||||||
Require all granted
|
|
||||||
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
# Block direct PHP access
|
|
||||||
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
|
||||||
RewriteRule ^.*$ - [R=404,L]
|
|
||||||
|
|
||||||
# Serve the one static file that exists: css/tkr.css
|
|
||||||
# (Pass requests to css/custom/ through to the PHP app)
|
|
||||||
RewriteCond %{REQUEST_URI} !^/tkr/css/custom/
|
|
||||||
RewriteRule ^css/tkr\.css$ css/tkr.css [L]
|
|
||||||
|
|
||||||
# Send everything else to the front controller
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.*)$ index.php [L]
|
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
# Error and access logs
|
# Error and access logs
|
||||||
ErrorLog ${APACHE_LOG_DIR}/my-domain_error.log
|
ErrorLog ${APACHE_LOG_DIR}/tkr_error.log
|
||||||
CustomLog ${APACHE_LOG_DIR}/my-domain_access.log combined
|
CustomLog ${APACHE_LOG_DIR}/tkr_access.log combined
|
||||||
</VirtualHost>
|
</VirtualHost>
|
@ -20,6 +20,7 @@ services:
|
|||||||
- ./src:/var/www/tkr/src
|
- ./src:/var/www/tkr/src
|
||||||
- ./storage:/var/www/tkr/storage
|
- ./storage:/var/www/tkr/storage
|
||||||
- ./templates:/var/www/tkr/templates
|
- ./templates:/var/www/tkr/templates
|
||||||
|
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
|
||||||
command: >
|
command: >
|
||||||
sh -c "
|
sh -c "
|
||||||
chown -R www-data:www-data /var/www/tkr/storage &&
|
chown -R www-data:www-data /var/www/tkr/storage &&
|
||||||
|
@ -1,100 +1,45 @@
|
|||||||
# Example nginx config
|
# Basic nginx config for tkr
|
||||||
# for serving tkr as a subdomain without SSL
|
# Replace "your-domain.com" with your actual domain
|
||||||
# e.g. http://tkr.my-domain.com/
|
# Replace "/var/www/tkr" with your installation path
|
||||||
#
|
|
||||||
# NOTE: Do not use in production.
|
# HTTP - redirect to HTTPS
|
||||||
# This is provided for docker compose
|
|
||||||
# (The included docker-compose file will mount it in the container image)
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80 default_server;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
root /var/www/tkr/public;
|
root /var/www/tkr/public;
|
||||||
index index.php;
|
index index.php;
|
||||||
|
|
||||||
# Security headers
|
# Block access to sensitive directories
|
||||||
# The first rule is to prevent including in a frame on a different domain.
|
location ~ ^/(storage|src|templates|config) {
|
||||||
# Remove it if you want to do that.
|
deny all;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
return 404;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
}
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
|
|
||||||
# Deny access to hidden files
|
# Block access to hidden files
|
||||||
location ~ /\. {
|
location ~ /\. {
|
||||||
deny all;
|
deny all;
|
||||||
access_log off;
|
|
||||||
log_not_found off;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# PHP routing - everything goes through index.php
|
# Handle PHP files
|
||||||
location / {
|
location ~ \.php$ {
|
||||||
# Cache static files
|
|
||||||
# Note that I don't actually serve most of this (just css)
|
|
||||||
# but this prevents requests for static content from getting to the PHP handler.
|
|
||||||
#
|
|
||||||
# I've excluded /css/custom so that requests for uploaded css can be handled by the PHP app.
|
|
||||||
# That lets me store uploaded content outside of the document root,
|
|
||||||
# so it isn't served directly.
|
|
||||||
|
|
||||||
# CSS files - 1 hour cache
|
|
||||||
location ~* ^/(?!css/custom/).+\.css$ {
|
|
||||||
expires 1h;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Other static assets - 1 year cache
|
|
||||||
location ~* ^/.+\.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# index.php is the entry point
|
|
||||||
# It needs to be sent to php-fpm
|
|
||||||
# But if someone tries to directly access index.php, that file will throw a 404
|
|
||||||
# so bots and scanners can't tell this is a php app
|
|
||||||
location = /index.php {
|
|
||||||
# If you're running php-fpm on the same server as nginx,
|
|
||||||
# then change this to the local php-fpm socket
|
|
||||||
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
|
||||||
fastcgi_pass php:9000;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
|
||||||
include fastcgi_params;
|
|
||||||
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Block attempts to access all other .php files directly
|
|
||||||
# (these are bots and scanners)
|
|
||||||
location ~ ^/.+\.php$ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# forward other requests to the fallback block,
|
|
||||||
# which sends them to php-fpm for handling
|
|
||||||
try_files $uri $uri/ @tkr_fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fallback for /tkr routing - all non-file requests (e.g. /login) go to index.php
|
|
||||||
location @tkr_fallback {
|
|
||||||
# If you're running php-fpm on the same server as nginx,
|
|
||||||
# then change this to the local php-fpm socket
|
|
||||||
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
|
||||||
fastcgi_pass php:9000;
|
fastcgi_pass php:9000;
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Front controller pattern - route everything else through index.php
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ @tkr;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle PHP requests
|
||||||
|
location @tkr {
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
|
include fastcgi_params;
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Deny access to sensitive directories
|
|
||||||
location ~ ^/(storage|src|templates|uploads|config) {
|
|
||||||
deny all;
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ services:
|
|||||||
- ./src:/var/www/tkr/src
|
- ./src:/var/www/tkr/src
|
||||||
- ./storage:/var/www/tkr/storage
|
- ./storage:/var/www/tkr/storage
|
||||||
- ./templates:/var/www/tkr/templates
|
- ./templates:/var/www/tkr/templates
|
||||||
|
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
|
||||||
command: >
|
command: >
|
||||||
sh -c "
|
sh -c "
|
||||||
chown -R www-data:www-data /var/www/tkr/storage &&
|
chown -R www-data:www-data /var/www/tkr/storage &&
|
||||||
|
@ -1,101 +1,47 @@
|
|||||||
# Example nginx config
|
# Basic nginx config for tkr in subfolder
|
||||||
# for serving tkr as a subdfolder without SSL
|
# e.g. https://your-domain.com/tkr
|
||||||
# e.g. http://my-domain.com/tkr
|
# For use with included docker-compose.yml
|
||||||
#
|
|
||||||
# NOTE: Do not use in production.
|
|
||||||
# This is provided for docker compose
|
|
||||||
# (The included docker-compose file will mount it in the container image)
|
|
||||||
server {
|
server {
|
||||||
listen 80 default_server;
|
listen 80 default_server;
|
||||||
listen [::]:80 default_server;
|
|
||||||
|
|
||||||
# replace localhost with your subdomain
|
|
||||||
# e.g. tkr.my-domain.com
|
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
root /var/www/html;
|
root /var/www/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# Security headers
|
|
||||||
# The first rule is to prevent including in a frame on a different domain.
|
|
||||||
# Remove it if you want to do that.
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
|
|
||||||
# Deny access to hidden files
|
|
||||||
location ~ /\. {
|
|
||||||
deny all;
|
|
||||||
access_log off;
|
|
||||||
log_not_found off;
|
|
||||||
}
|
|
||||||
|
|
||||||
# PHP routing - everything under /tkr goes through index.php
|
|
||||||
location /tkr {
|
location /tkr {
|
||||||
alias /var/www/tkr/public;
|
alias /var/www/tkr/public;
|
||||||
index index.php;
|
index index.php;
|
||||||
|
|
||||||
# Cache static files
|
# Block access to sensitive directories
|
||||||
# Note that I don't actually serve most of this (just css)
|
location ~ ^/tkr/(storage|src|templates|config) {
|
||||||
# but this prevents requests for static content from getting to the PHP handler.
|
deny all;
|
||||||
#
|
|
||||||
# I've excluded /css/custom so that requests for uploaded css can be handled by the PHP app.
|
|
||||||
# That lets me store uploaded content outside of the document root,
|
|
||||||
# so it isn't served directly.
|
|
||||||
|
|
||||||
# CSS files - 1 hour cache
|
|
||||||
location ~* ^/tkr/(?!css/custom/).+\.css$ {
|
|
||||||
expires 1h;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Other static assets - 1 year cache
|
|
||||||
location ~* ^/tkr/.+\.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# index.php is the entry point
|
|
||||||
# It needs to be sent to php-fpm
|
|
||||||
# But if someone tries to directly access index.php, that file will throw a 404
|
|
||||||
# so bots and scanners can't tell this is a php app
|
|
||||||
location = /tkr/index.php {
|
|
||||||
fastcgi_pass php:9000;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
|
||||||
include fastcgi_params;
|
|
||||||
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Block attempts to access all other .php files directly
|
|
||||||
# (these are bots and scanners)
|
|
||||||
location ~ ^/tkr/.+\.php$ {
|
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# forward other requests to the fallback block,
|
# Block access to hidden files
|
||||||
# which sends them to php-fpm for handling
|
location ~ /\. {
|
||||||
try_files $uri $uri/ @tkr_fallback;
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle PHP files
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Front controller pattern
|
||||||
|
# Send everything else to index.php
|
||||||
|
try_files $uri $uri/ @tkr;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fallback for /tkr routing - all non-file requests (e.g. /login) go to index.php
|
location @tkr {
|
||||||
location @tkr_fallback {
|
|
||||||
fastcgi_pass php:9000;
|
fastcgi_pass php:9000;
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Deny access to sensitive directories
|
|
||||||
location ~ ^/tkr/(storage|src|templates|uploads|config) {
|
|
||||||
deny all;
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,49 +1,23 @@
|
|||||||
# Example Apache VirtualHost
|
# Basic .htaccess for tkr on shared hosting
|
||||||
# for serving tkr as a subdirectory path
|
# Place this file inside the tkr directory
|
||||||
# on shared hosting via .htaccess
|
# e.g. if tkr is at /public_html/tkr/, this goes in /public_html/tkr/
|
||||||
#
|
|
||||||
# e.g. http://www.my-domain.com/tkr
|
|
||||||
#
|
|
||||||
# This should work without modification if you extract the app
|
|
||||||
# to /tkr from your web document root
|
|
||||||
|
|
||||||
# Enable mod_rewrite
|
# Enable mod_rewrite
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
|
||||||
# Security headers
|
# Set directory index
|
||||||
Header always set X-Frame-Options "SAMEORIGIN"
|
|
||||||
Header always set X-XSS-Protection "1; mode=block"
|
|
||||||
Header always set X-Content-Type-Options "nosniff"
|
|
||||||
|
|
||||||
# Directory index
|
|
||||||
DirectoryIndex public/index.php
|
DirectoryIndex public/index.php
|
||||||
|
|
||||||
# Security: Block direct access to .php files (except through rewrites)
|
# Block access to sensitive directories
|
||||||
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
RewriteRule ^(storage|src|templates|config)(/.*)?$ - [F,L]
|
||||||
RewriteRule ^.*$ - [R=404,L]
|
|
||||||
|
|
||||||
# Security: Block access to sensitive directories
|
# Block access to hidden files
|
||||||
RewriteRule ^(storage|src|templates|examples|config)(/.*)?$ - [F,L]
|
|
||||||
|
|
||||||
# Security: Block access to hidden files
|
|
||||||
RewriteRule ^\..*$ - [F,L]
|
RewriteRule ^\..*$ - [F,L]
|
||||||
|
|
||||||
# Cache CSS files for 1 hour
|
# Block access to setup script
|
||||||
<FilesMatch "\.css$">
|
RewriteRule ^tkr-setup\.php$ - [F,L]
|
||||||
Header set Cache-Control "public, max-age=3600"
|
|
||||||
</FilesMatch>
|
|
||||||
|
|
||||||
# Serve the one static file that exists: css/tkr.css
|
# Route everything else through the front controller
|
||||||
# (Pass requests to css/custom/ through to the PHP app)
|
|
||||||
RewriteCond %{REQUEST_URI} !^/css/custom/
|
|
||||||
RewriteRule ^css/tkr\.css$ public/css/tkr.css [L]
|
|
||||||
|
|
||||||
# 404 all other static files (images, js, fonts, etc.)
|
|
||||||
# so those requests don't hit the PHP app
|
|
||||||
# (this is to reduce load on the PHP app from bots and scanners)
|
|
||||||
RewriteRule \.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$ - [R=404,L]
|
|
||||||
|
|
||||||
# Everything else goes to the front controller
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
RewriteRule ^(.*)$ public/index.php [L]
|
RewriteRule ^(.*)$ public/index.php [L]
|
||||||
|
@ -1,39 +1,34 @@
|
|||||||
# Example Apache VirtualHost
|
# Basic Apache VirtualHost for tkr
|
||||||
# for serving tkr as a subdomain root with SSL
|
# Replace "your-domain.com" with your actual domain
|
||||||
# e.g. https://tkr.my-domain.com/
|
# Replace "/var/www/tkr" with your installation path
|
||||||
#
|
|
||||||
# Use SSL in production.
|
# HTTP - redirect to HTTPS
|
||||||
# This is a minimal SSL confiuration
|
|
||||||
# For more robust SSL configuration, refer to https://ssl-config.mozilla.org/
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
ServerName your-domain.com
|
||||||
ServerName localhost
|
Redirect permanent / https://your-domain.com/
|
||||||
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
|
||||||
DocumentRoot /var/www/tkr/public
|
|
||||||
# Redirect HTTP to HTTPS
|
|
||||||
Redirect permanent / https://tkr.my-domain.com/
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
||||||
|
# HTTPS
|
||||||
<VirtualHost *:443>
|
<VirtualHost *:443>
|
||||||
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
ServerName your-domain.com
|
||||||
ServerName localhost
|
|
||||||
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
|
||||||
DocumentRoot /var/www/tkr/public
|
DocumentRoot /var/www/tkr/public
|
||||||
|
|
||||||
# SSL Configuration
|
# SSL Configuration (using Let's Encrypt)
|
||||||
SSLEngine on
|
SSLEngine on
|
||||||
|
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
|
||||||
# Assumes you're using letsencrypt for cert generation
|
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
|
||||||
# Replace with the actual paths to your cert and key
|
|
||||||
SSLCertificateFile /etc/letsencrypt/live/tkr.my-domain.com/fullchain.pem
|
# Main directory - route everything through index.php
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/tkr.my-domain.com/privkey.pem
|
<Directory "/var/www/tkr/public">
|
||||||
|
AllowOverride None
|
||||||
# Security headers
|
Require all granted
|
||||||
Header always set X-Frame-Options "SAMEORIGIN"
|
|
||||||
Header always set X-XSS-Protection "1; mode=block"
|
RewriteEngine On
|
||||||
Header always set X-Content-Type-Options "nosniff"
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
# Block access to sensitive directories
|
# Block access to sensitive directories
|
||||||
<Directory "/var/www/tkr/storage">
|
<Directory "/var/www/tkr/storage">
|
||||||
Require all denied
|
Require all denied
|
||||||
@ -41,59 +36,13 @@
|
|||||||
<Directory "/var/www/tkr/src">
|
<Directory "/var/www/tkr/src">
|
||||||
Require all denied
|
Require all denied
|
||||||
</Directory>
|
</Directory>
|
||||||
<Directory "/var/www/tkr/templates">
|
|
||||||
Require all denied
|
|
||||||
</Directory>
|
|
||||||
<Directory "/var/www/tkr/config">
|
<Directory "/var/www/tkr/config">
|
||||||
Require all denied
|
Require all denied
|
||||||
</Directory>
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/templates">
|
||||||
# Block access to hidden files
|
|
||||||
<DirectoryMatch "^\.|/\.">
|
|
||||||
Require all denied
|
Require all denied
|
||||||
</DirectoryMatch>
|
|
||||||
|
|
||||||
# Cache CSS files
|
|
||||||
<LocationMatch "\.css$">
|
|
||||||
Header set Cache-Control "public, max-age=3600"
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
# Serve static CSS file
|
|
||||||
Alias /css/tkr.css /var/www/tkr/public/css/tkr.css
|
|
||||||
|
|
||||||
# 404 all non-css static files (images, js, fonts, etc.)
|
|
||||||
# so those requests don't hit the PHP app
|
|
||||||
# (this is to reduce load on the PHP app from bots and scanners)
|
|
||||||
<LocationMatch "\.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$">
|
|
||||||
<RequireAll>
|
|
||||||
Require all denied
|
|
||||||
</RequireAll>
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
# Enable rewrite engine
|
|
||||||
<Directory "/var/www/tkr/public">
|
|
||||||
Options -Indexes
|
|
||||||
AllowOverride None
|
|
||||||
Require all granted
|
|
||||||
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
# Block direct PHP access
|
|
||||||
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
|
||||||
RewriteRule ^.*$ - [R=404,L]
|
|
||||||
|
|
||||||
# Serve the one static file that exists: css/tkr.css
|
|
||||||
# (Pass requests to css/custom/ through to the PHP app)
|
|
||||||
RewriteCond %{REQUEST_URI} !^/css/custom/
|
|
||||||
RewriteRule ^css/tkr\.css$ css/tkr.css [L]
|
|
||||||
|
|
||||||
# Everything else to front controller
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.*)$ index.php [L]
|
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
# Error and access logs
|
|
||||||
ErrorLog ${APACHE_LOG_DIR}/tkr_error.log
|
ErrorLog ${APACHE_LOG_DIR}/tkr_error.log
|
||||||
CustomLog ${APACHE_LOG_DIR}/tkr_access.log combined
|
CustomLog ${APACHE_LOG_DIR}/tkr_access.log combined
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
@ -1,91 +1,31 @@
|
|||||||
# Example Apache VirtualHost
|
# Basic Apache config for tkr in subfolder
|
||||||
# for serving tkr as a subdirectory path with SSL
|
# e.g. https://your-domain.com/tkr
|
||||||
# e.g. https://www.my-domain.com/tkr
|
# Add this to your existing VirtualHost configuration
|
||||||
#
|
|
||||||
# Use SSL in production.
|
|
||||||
# This is a minimal SSL confiuration
|
|
||||||
# For more robust SSL configuration, refer to https://ssl-config.mozilla.org/
|
|
||||||
<VirtualHost *:80>
|
|
||||||
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
|
||||||
ServerName localhost
|
|
||||||
# CONFIG: Replace with your subdomain, e.g. tkr.my-domain.com
|
|
||||||
DocumentRoot /var/www/tkr
|
|
||||||
# Redirect HTTP to HTTPS
|
|
||||||
Redirect permanent / https://my-domain.com/
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
<VirtualHost *:443>
|
# Alias for tkr subfolder
|
||||||
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
Alias /tkr /var/www/tkr/public
|
||||||
ServerName localhost
|
|
||||||
# CONFIG: Replace with your subdomain, e.g. tkr.my-domain.com
|
|
||||||
DocumentRoot /var/www/tkr/
|
|
||||||
|
|
||||||
# SSL Configuration
|
<Directory "/var/www/tkr/public">
|
||||||
SSLEngine on
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
# Front controller pattern
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ /tkr/index.php [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
# Assumes you're using letsencrypt for cert generation
|
# Block access to sensitive directories
|
||||||
# Replace with the actual paths to your cert and key
|
<Directory "/var/www/tkr/storage">
|
||||||
SSLCertificateFile /etc/letsencrypt/live/my-domain.com/fullchain.pem
|
Require all denied
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/my-domain.com/privkey.pem
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/src">
|
||||||
# Security headers
|
Require all denied
|
||||||
Header always set X-Frame-Options "SAMEORIGIN"
|
</Directory>
|
||||||
Header always set X-XSS-Protection "1; mode=block"
|
<Directory "/var/www/tkr/config">
|
||||||
Header always set X-Content-Type-Options "nosniff"
|
Require all denied
|
||||||
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/templates">
|
||||||
# tkr Application at /tkr
|
Require all denied
|
||||||
# NOTE: If you change the directory name,
|
</Directory>
|
||||||
# remember to update all instances of /var/www/tkr in this file to match
|
|
||||||
Alias /tkr /var/www/tkr/public
|
|
||||||
|
|
||||||
# Block access to sensitive TKR directories
|
|
||||||
<Directory "/var/www/tkr/storage">
|
|
||||||
Require all denied
|
|
||||||
</Directory>
|
|
||||||
<Directory "/var/www/tkr/src">
|
|
||||||
Require all denied
|
|
||||||
</Directory>
|
|
||||||
<Directory "/var/www/tkr/templates">
|
|
||||||
Require all denied
|
|
||||||
</Directory>
|
|
||||||
<Directory "/var/www/tkr/config">
|
|
||||||
Require all denied
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
# 404 all non-css static files in /tkr (images, js, fonts, etc.)
|
|
||||||
# so those requests don't hit the PHP app
|
|
||||||
# (this is to reduce load on the PHP app from bots and scanners)
|
|
||||||
<LocationMatch "^/tkr/.*\.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$">
|
|
||||||
<RequireAll>
|
|
||||||
Require all denied
|
|
||||||
</RequireAll>
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
# tkr application directory
|
|
||||||
<Directory "/var/www/tkr/public">
|
|
||||||
Options -Indexes
|
|
||||||
AllowOverride None
|
|
||||||
Require all granted
|
|
||||||
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
# Block direct PHP access
|
|
||||||
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
|
||||||
RewriteRule ^.*$ - [R=404,L]
|
|
||||||
|
|
||||||
# Serve the one static file that exists: css/tkr.css
|
|
||||||
# (Pass requests to css/custom/ through to the PHP app)
|
|
||||||
RewriteCond %{REQUEST_URI} !^/tkr/css/custom/
|
|
||||||
RewriteRule ^css/tkr\.css$ css/tkr.css [L]
|
|
||||||
|
|
||||||
# Send everything else to the front controller
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.*)$ index.php [L]
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
# Error and access logs
|
|
||||||
ErrorLog ${APACHE_LOG_DIR}/my-domain_error.log
|
|
||||||
CustomLog ${APACHE_LOG_DIR}/my-domain_access.log combined
|
|
||||||
</VirtualHost>
|
|
@ -1,117 +1,51 @@
|
|||||||
# Example nginx config
|
# Basic nginx config for tkr
|
||||||
# for serving tkr as a subdomain with SSL
|
# Replace "your-domain.com" with your actual domain
|
||||||
# e.g. https://tkr.my-domain.com/
|
# Replace "/var/www/tkr" with your installation path
|
||||||
#
|
|
||||||
# Use SSL in production.
|
# HTTP - redirect to HTTPS
|
||||||
# This is a minimal SSL confiuration
|
server {
|
||||||
# For more robust SSL configuration, refer to https://ssl-config.mozilla.org/
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
# CONFIG: replace "localhost" with your subdomain (e.g. tkr.my-domain.com)
|
server_name your-domain.com;
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
# CONFIG:
|
|
||||||
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
|
||||||
root /var/www/tkr/public;
|
root /var/www/tkr/public;
|
||||||
index index.php;
|
index index.php;
|
||||||
|
|
||||||
# CONFIG:
|
# SSL Configuration (using Let's Encrypt)
|
||||||
# Assumes you're using letsencrypt for cert generation
|
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
||||||
# Replace with the actual paths to your cert and key
|
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
||||||
ssl_certificate /etc/letsencrypt/live/tkr.my-domain.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/tkr.my-domain.com/privkey.pem;
|
# Block access to sensitive directories
|
||||||
|
location ~ ^/(storage|src|templates|config) {
|
||||||
# Security headers
|
deny all;
|
||||||
# The first rule is to prevent including in a frame on a different domain.
|
return 404;
|
||||||
# Remove it if you want to do that.
|
}
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
# Block access to hidden files
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
|
|
||||||
# Deny access to hidden files
|
|
||||||
location ~ /\. {
|
location ~ /\. {
|
||||||
deny all;
|
deny all;
|
||||||
access_log off;
|
|
||||||
log_not_found off;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# PHP routing - everything goes through index.php
|
# Front controller pattern - route everything through index.php
|
||||||
location / {
|
location / {
|
||||||
# Cache static files
|
try_files $uri $uri/ @tkr;
|
||||||
# Note that I don't actually serve most of this (just css)
|
|
||||||
# but this prevents requests for static content from getting to the PHP handler.
|
|
||||||
#
|
|
||||||
# I've excluded /css/custom so that requests for uploaded css can be handled by the PHP app.
|
|
||||||
# That lets me store uploaded content outside of the document root,
|
|
||||||
# so it isn't served directly.
|
|
||||||
location ~* ^/(?!css/custom/).+\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# index.php is the entry point
|
|
||||||
# It needs to be sent to php-fpm
|
|
||||||
# But if someone tries to directly access index.php, that file will throw a 404
|
|
||||||
# so bots and scanners can't tell this is a php app
|
|
||||||
location = /index.php {
|
|
||||||
# CONFIG:
|
|
||||||
# If you're running php-fpm on the same server as nginx,
|
|
||||||
# then change this to the local php-fpm socket
|
|
||||||
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
|
||||||
fastcgi_pass php:9000;
|
|
||||||
|
|
||||||
# CONFIG:
|
|
||||||
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
|
||||||
include fastcgi_params;
|
|
||||||
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Block attempts to access all other .php files directly
|
|
||||||
# (these are bots and scanners)
|
|
||||||
location ~ ^/.+\.php$ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# forward other requests to the fallback block,
|
|
||||||
# which sends them to php-fpm for handling
|
|
||||||
try_files $uri $uri/ @tkr_fallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fallback for /tkr routing - all non-file requests (e.g. /login) go to index.php
|
# Handle PHP requests
|
||||||
location @tkr_fallback {
|
location @tkr {
|
||||||
# CONFIG:
|
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # Adjust PHP version as needed
|
||||||
# If you're running php-fpm on the same server as nginx,
|
|
||||||
# then change this to the local php-fpm socket
|
|
||||||
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
|
||||||
fastcgi_pass php:9000;
|
|
||||||
|
|
||||||
# CONFIG:
|
|
||||||
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Deny access to sensitive directories
|
|
||||||
location ~ ^/(storage|src|templates|uploads|config) {
|
|
||||||
deny all;
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
}
|
||||||
|
@ -1,118 +1,38 @@
|
|||||||
# Example nginx config
|
# Basic nginx config for tkr in subfolder
|
||||||
# for serving tkr as a subdfolder with SSL
|
# e.g. https://your-domain.com/tkr
|
||||||
# e.g. https://my-domain.com/tkr
|
# Add this location block to your existing server configuration
|
||||||
#
|
|
||||||
# Use SSL in production.
|
|
||||||
# This is a minimal SSL confiuration
|
|
||||||
# For more robust SSL configuration, refer to https://ssl-config.mozilla.org/
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
listen [::]:443 ssl;
|
|
||||||
|
|
||||||
# CONFIG: Replace localhost with your domain e.g. my-domain.com
|
location /tkr {
|
||||||
server_name localhost;
|
alias /var/www/tkr/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
# CONFIG:
|
# Handle PHP files
|
||||||
# Assumes you're using letsencrypt for cert generation
|
location ~ \.php$ {
|
||||||
# Replace with the actual paths to your cert and key
|
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # Adjust PHP version/socket as needed
|
||||||
ssl_certificate /etc/letsencrypt/live/my-domain.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/my-domain.com/privkey.pem;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
# The first rule is to prevent including in a frame on a different domain.
|
|
||||||
# Remove it if you want to do that.
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
|
|
||||||
# Deny access to hidden files
|
|
||||||
location ~ /\. {
|
|
||||||
deny all;
|
|
||||||
access_log off;
|
|
||||||
log_not_found off;
|
|
||||||
}
|
|
||||||
|
|
||||||
# PHP routing - everything under /tkr goes through index.php
|
|
||||||
location /tkr {
|
|
||||||
# CONFIG:
|
|
||||||
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
|
||||||
alias /var/www/tkr/public;
|
|
||||||
index index.php;
|
|
||||||
|
|
||||||
# Cache static files
|
|
||||||
# Note that I don't actually serve most of this (just css)
|
|
||||||
# but this prevents requests for static content from getting to the PHP handler.
|
|
||||||
#
|
|
||||||
# I've excluded /css/custom so that requests for uploaded css can be handled by the PHP app.
|
|
||||||
# That lets me store uploaded content outside of the document root,
|
|
||||||
# so it isn't served directly.
|
|
||||||
location ~* ^/tkr/(?!css/custom/).+\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# index.php is the entry point
|
|
||||||
# It needs to be sent to php-fpm
|
|
||||||
# But if someone tries to directly access index.php, that file will throw a 404
|
|
||||||
# so bots and scanners can't tell this is a php app
|
|
||||||
location = /tkr/index.php {
|
|
||||||
# CONFIG:
|
|
||||||
# If you're running php-fpm on the same server as nginx,
|
|
||||||
# then change this to the local php-fpm socket
|
|
||||||
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
|
||||||
fastcgi_pass php:9000;
|
|
||||||
|
|
||||||
# CONFIG:
|
|
||||||
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
|
||||||
include fastcgi_params;
|
|
||||||
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Block attempts to access all other .php files directly
|
|
||||||
# (these are bots and scanners)
|
|
||||||
location ~ ^/tkr/.+\.php$ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# forward other requests to the fallback block,
|
|
||||||
# which sends them to php-fpm for handling
|
|
||||||
try_files $uri $uri/ @tkr_fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fallback for /tkr routing - all non-file requests (e.g. /login) go to index.php
|
|
||||||
location @tkr_fallback {
|
|
||||||
# CONFIG:
|
|
||||||
# If you're running php-fpm on the same server as nginx,
|
|
||||||
# then change this to the local php-fpm socket
|
|
||||||
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
|
||||||
fastcgi_pass php:9000;
|
|
||||||
|
|
||||||
# CONFIG:
|
|
||||||
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Deny access to sensitive directories
|
# Block access to sensitive directories
|
||||||
location ~ ^/tkr/(storage|src|templates|uploads|config) {
|
location ~ ^/tkr/(storage|src|templates|config) {
|
||||||
deny all;
|
deny all;
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Block access to hidden files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Front controller pattern
|
||||||
|
try_files $uri $uri/ @tkr;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
location @tkr {
|
||||||
listen 80 default_server;
|
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # Adjust PHP version as needed
|
||||||
listen [::]:80 default_server;
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
|
include fastcgi_params;
|
||||||
return 301 https://$host$request_uri;
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
}
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
}
|
49
examples/production/apache/.htaccess
Normal file
49
examples/production/apache/.htaccess
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Example Apache VirtualHost
|
||||||
|
# for serving tkr as a subdirectory path
|
||||||
|
# on shared hosting via .htaccess
|
||||||
|
#
|
||||||
|
# e.g. http://www.my-domain.com/tkr
|
||||||
|
#
|
||||||
|
# This should work without modification if you extract the app
|
||||||
|
# to /tkr from your web document root
|
||||||
|
|
||||||
|
# Enable mod_rewrite
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
|
||||||
|
# Directory index
|
||||||
|
DirectoryIndex public/index.php
|
||||||
|
|
||||||
|
# Security: Block direct access to .php files (except through rewrites)
|
||||||
|
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
||||||
|
RewriteRule ^.*$ - [R=404,L]
|
||||||
|
|
||||||
|
# Security: Block access to sensitive directories
|
||||||
|
RewriteRule ^(storage|src|templates|examples|config)(/.*)?$ - [F,L]
|
||||||
|
|
||||||
|
# Security: Block access to hidden files
|
||||||
|
RewriteRule ^\..*$ - [F,L]
|
||||||
|
|
||||||
|
# Cache CSS files for 1 hour
|
||||||
|
<FilesMatch "\.css$">
|
||||||
|
Header set Cache-Control "public, max-age=3600"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Serve the one static file that exists: css/default.css
|
||||||
|
# (Pass requests to css/custom/ through to the PHP app)
|
||||||
|
RewriteCond %{REQUEST_URI} !^/css/custom/
|
||||||
|
RewriteRule ^css/default\.css$ public/css/default.css [L]
|
||||||
|
|
||||||
|
# 404 all other static files (images, js, fonts, etc.)
|
||||||
|
# so those requests don't hit the PHP app
|
||||||
|
# (this is to reduce load on the PHP app from bots and scanners)
|
||||||
|
RewriteRule \.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$ - [R=404,L]
|
||||||
|
|
||||||
|
# Everything else goes to the front controller
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ public/index.php [L]
|
91
examples/production/apache/subfolder.conf
Normal file
91
examples/production/apache/subfolder.conf
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Example Apache VirtualHost
|
||||||
|
# for serving tkr as a subdirectory path with SSL
|
||||||
|
# e.g. https://www.my-domain.com/tkr
|
||||||
|
#
|
||||||
|
# Use SSL in production.
|
||||||
|
# This is a minimal SSL confiuration
|
||||||
|
# For more robust SSL configuration, refer to https://ssl-config.mozilla.org/
|
||||||
|
<VirtualHost *:80>
|
||||||
|
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
||||||
|
ServerName localhost
|
||||||
|
# CONFIG: Replace with your subdomain, e.g. tkr.my-domain.com
|
||||||
|
DocumentRoot /var/www/tkr
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
Redirect permanent / https://my-domain.com/
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
<VirtualHost *:443>
|
||||||
|
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
||||||
|
ServerName localhost
|
||||||
|
# CONFIG: Replace with your subdomain, e.g. tkr.my-domain.com
|
||||||
|
DocumentRoot /var/www/tkr/
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
SSLEngine on
|
||||||
|
|
||||||
|
# Assumes you're using letsencrypt for cert generation
|
||||||
|
# Replace with the actual paths to your cert and key
|
||||||
|
SSLCertificateFile /etc/letsencrypt/live/my-domain.com/fullchain.pem
|
||||||
|
SSLCertificateKeyFile /etc/letsencrypt/live/my-domain.com/privkey.pem
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
|
||||||
|
# tkr Application at /tkr
|
||||||
|
# NOTE: If you change the directory name,
|
||||||
|
# remember to update all instances of /var/www/tkr in this file to match
|
||||||
|
Alias /tkr /var/www/tkr/public
|
||||||
|
|
||||||
|
# Block access to sensitive TKR directories
|
||||||
|
<Directory "/var/www/tkr/storage">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/src">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/templates">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/config">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# 404 all non-css static files in /tkr (images, js, fonts, etc.)
|
||||||
|
# so those requests don't hit the PHP app
|
||||||
|
# (this is to reduce load on the PHP app from bots and scanners)
|
||||||
|
<LocationMatch "^/tkr/.*\.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$">
|
||||||
|
<RequireAll>
|
||||||
|
Require all denied
|
||||||
|
</RequireAll>
|
||||||
|
</LocationMatch>
|
||||||
|
|
||||||
|
# tkr application directory
|
||||||
|
<Directory "/var/www/tkr/public">
|
||||||
|
Options -Indexes
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Block direct PHP access
|
||||||
|
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
||||||
|
RewriteRule ^.*$ - [R=404,L]
|
||||||
|
|
||||||
|
# Serve the one static file that exists: css/default.css
|
||||||
|
# (Pass requests to css/custom/ through to the PHP app)
|
||||||
|
RewriteCond %{REQUEST_URI} !^/tkr/css/custom/
|
||||||
|
RewriteRule ^css/default\.css$ css/tkr.css [L]
|
||||||
|
|
||||||
|
# Send everything else to the front controller
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Error and access logs
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/my-domain_error.log
|
||||||
|
CustomLog ${APACHE_LOG_DIR}/my-domain_access.log combined
|
||||||
|
</VirtualHost>
|
99
examples/production/apache/tkr.my-domain.com.conf
Normal file
99
examples/production/apache/tkr.my-domain.com.conf
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Example Apache VirtualHost
|
||||||
|
# for serving tkr as a subdomain root with SSL
|
||||||
|
# e.g. https://tkr.my-domain.com/
|
||||||
|
#
|
||||||
|
# Use SSL in production.
|
||||||
|
# This is a minimal SSL confiuration
|
||||||
|
# For more robust SSL configuration, refer to https://ssl-config.mozilla.org/
|
||||||
|
<VirtualHost *:80>
|
||||||
|
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
||||||
|
ServerName localhost
|
||||||
|
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
||||||
|
DocumentRoot /var/www/tkr/public
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
Redirect permanent / https://tkr.my-domain.com/
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
<VirtualHost *:443>
|
||||||
|
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
||||||
|
ServerName localhost
|
||||||
|
# CONFIG: Replace localhost with your subdomain, e.g. tkr.my-domain.com
|
||||||
|
DocumentRoot /var/www/tkr/public
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
SSLEngine on
|
||||||
|
|
||||||
|
# Assumes you're using letsencrypt for cert generation
|
||||||
|
# Replace with the actual paths to your cert and key
|
||||||
|
SSLCertificateFile /etc/letsencrypt/live/tkr.my-domain.com/fullchain.pem
|
||||||
|
SSLCertificateKeyFile /etc/letsencrypt/live/tkr.my-domain.com/privkey.pem
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
|
||||||
|
# Block access to sensitive directories
|
||||||
|
<Directory "/var/www/tkr/storage">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/src">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/templates">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
<Directory "/var/www/tkr/config">
|
||||||
|
Require all denied
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Block access to hidden files
|
||||||
|
<DirectoryMatch "^\.|/\.">
|
||||||
|
Require all denied
|
||||||
|
</DirectoryMatch>
|
||||||
|
|
||||||
|
# Cache CSS files
|
||||||
|
<LocationMatch "\.css$">
|
||||||
|
Header set Cache-Control "public, max-age=3600"
|
||||||
|
</LocationMatch>
|
||||||
|
|
||||||
|
# Serve static CSS file
|
||||||
|
Alias /css/tkr.css /var/www/tkr/public/css/tkr.css
|
||||||
|
|
||||||
|
# 404 all non-css static files (images, js, fonts, etc.)
|
||||||
|
# so those requests don't hit the PHP app
|
||||||
|
# (this is to reduce load on the PHP app from bots and scanners)
|
||||||
|
<LocationMatch "\.(js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|mp3|mp4|avi|mov)$">
|
||||||
|
<RequireAll>
|
||||||
|
Require all denied
|
||||||
|
</RequireAll>
|
||||||
|
</LocationMatch>
|
||||||
|
|
||||||
|
# Enable rewrite engine
|
||||||
|
<Directory "/var/www/tkr/public">
|
||||||
|
Options -Indexes
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Block direct PHP access
|
||||||
|
RewriteCond %{THE_REQUEST} \s/[^?\s]*\.php[\s?] [NC]
|
||||||
|
RewriteRule ^.*$ - [R=404,L]
|
||||||
|
|
||||||
|
# Serve the one static file that exists: css/default.css
|
||||||
|
# (Pass requests to css/custom/ through to the PHP app)
|
||||||
|
RewriteCond %{REQUEST_URI} !^/css/custom/
|
||||||
|
RewriteRule ^css/default\.css$ css/default.css [L]
|
||||||
|
|
||||||
|
# Everything else to front controller
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Error and access logs
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/tkr_error.log
|
||||||
|
CustomLog ${APACHE_LOG_DIR}/tkr_access.log combined
|
||||||
|
</VirtualHost>
|
117
examples/production/nginx/nginx.conf
Normal file
117
examples/production/nginx/nginx.conf
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# Example nginx config
|
||||||
|
# for serving tkr as a subdomain with SSL
|
||||||
|
# e.g. https://tkr.my-domain.com/
|
||||||
|
#
|
||||||
|
# Use SSL in production.
|
||||||
|
# This is a minimal SSL confiuration
|
||||||
|
# For more robust SSL configuration, refer to https://ssl-config.mozilla.org/
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
# CONFIG: replace "localhost" with your subdomain (e.g. tkr.my-domain.com)
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# CONFIG:
|
||||||
|
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
||||||
|
root /var/www/tkr/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
# CONFIG:
|
||||||
|
# Assumes you're using letsencrypt for cert generation
|
||||||
|
# Replace with the actual paths to your cert and key
|
||||||
|
ssl_certificate /etc/letsencrypt/live/tkr.my-domain.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/tkr.my-domain.com/privkey.pem;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
# The first rule is to prevent including in a frame on a different domain.
|
||||||
|
# Remove it if you want to do that.
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
|
||||||
|
# Deny access to hidden files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# PHP routing - everything goes through index.php
|
||||||
|
location / {
|
||||||
|
# Cache static files
|
||||||
|
# Note that I don't actually serve most of this (just css)
|
||||||
|
# but this prevents requests for static content from getting to the PHP handler.
|
||||||
|
#
|
||||||
|
# I've excluded /css/custom so that requests for uploaded css can be handled by the PHP app.
|
||||||
|
# That lets me store uploaded content outside of the document root,
|
||||||
|
# so it isn't served directly.
|
||||||
|
location ~* ^/(?!css/custom/).+\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# index.php is the entry point
|
||||||
|
# It needs to be sent to php-fpm
|
||||||
|
# But if someone tries to directly access index.php, that file will throw a 404
|
||||||
|
# so bots and scanners can't tell this is a php app
|
||||||
|
location = /index.php {
|
||||||
|
# CONFIG:
|
||||||
|
# If you're running php-fpm on the same server as nginx,
|
||||||
|
# then change this to the local php-fpm socket
|
||||||
|
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
|
||||||
|
# CONFIG:
|
||||||
|
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Block attempts to access all other .php files directly
|
||||||
|
# (these are bots and scanners)
|
||||||
|
location ~ ^/.+\.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# forward other requests to the fallback block,
|
||||||
|
# which sends them to php-fpm for handling
|
||||||
|
try_files $uri $uri/ @tkr_fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback for /tkr routing - all non-file requests (e.g. /login) go to index.php
|
||||||
|
location @tkr_fallback {
|
||||||
|
# CONFIG:
|
||||||
|
# If you're running php-fpm on the same server as nginx,
|
||||||
|
# then change this to the local php-fpm socket
|
||||||
|
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
|
||||||
|
# CONFIG:
|
||||||
|
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to sensitive directories
|
||||||
|
location ~ ^/(storage|src|templates|uploads|config) {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
118
examples/production/nginx/subfolder.conf
Normal file
118
examples/production/nginx/subfolder.conf
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# Example nginx config
|
||||||
|
# for serving tkr as a subdfolder with SSL
|
||||||
|
# e.g. https://my-domain.com/tkr
|
||||||
|
#
|
||||||
|
# Use SSL in production.
|
||||||
|
# This is a minimal SSL confiuration
|
||||||
|
# For more robust SSL configuration, refer to https://ssl-config.mozilla.org/
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
# CONFIG: Replace localhost with your domain e.g. my-domain.com
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# CONFIG:
|
||||||
|
# Assumes you're using letsencrypt for cert generation
|
||||||
|
# Replace with the actual paths to your cert and key
|
||||||
|
ssl_certificate /etc/letsencrypt/live/my-domain.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/my-domain.com/privkey.pem;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
# The first rule is to prevent including in a frame on a different domain.
|
||||||
|
# Remove it if you want to do that.
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
|
||||||
|
# Deny access to hidden files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# PHP routing - everything under /tkr goes through index.php
|
||||||
|
location /tkr {
|
||||||
|
# CONFIG:
|
||||||
|
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
||||||
|
alias /var/www/tkr/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
# Cache static files
|
||||||
|
# Note that I don't actually serve most of this (just css)
|
||||||
|
# but this prevents requests for static content from getting to the PHP handler.
|
||||||
|
#
|
||||||
|
# I've excluded /css/custom so that requests for uploaded css can be handled by the PHP app.
|
||||||
|
# That lets me store uploaded content outside of the document root,
|
||||||
|
# so it isn't served directly.
|
||||||
|
location ~* ^/tkr/(?!css/custom/).+\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# index.php is the entry point
|
||||||
|
# It needs to be sent to php-fpm
|
||||||
|
# But if someone tries to directly access index.php, that file will throw a 404
|
||||||
|
# so bots and scanners can't tell this is a php app
|
||||||
|
location = /tkr/index.php {
|
||||||
|
# CONFIG:
|
||||||
|
# If you're running php-fpm on the same server as nginx,
|
||||||
|
# then change this to the local php-fpm socket
|
||||||
|
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
|
||||||
|
# CONFIG:
|
||||||
|
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Block attempts to access all other .php files directly
|
||||||
|
# (these are bots and scanners)
|
||||||
|
location ~ ^/tkr/.+\.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# forward other requests to the fallback block,
|
||||||
|
# which sends them to php-fpm for handling
|
||||||
|
try_files $uri $uri/ @tkr_fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback for /tkr routing - all non-file requests (e.g. /login) go to index.php
|
||||||
|
location @tkr_fallback {
|
||||||
|
# CONFIG:
|
||||||
|
# If you're running php-fpm on the same server as nginx,
|
||||||
|
# then change this to the local php-fpm socket
|
||||||
|
# e.g. fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
|
||||||
|
# CONFIG:
|
||||||
|
# replace "/var/www/tkr" with the directory you extracted the .zip file to (if different)
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/tkr/public/index.php;
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to sensitive directories
|
||||||
|
location ~ ^/tkr/(storage|src|templates|uploads|config) {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
@ -12,6 +12,9 @@
|
|||||||
--color-flash-success: darkgreen;
|
--color-flash-success: darkgreen;
|
||||||
--color-flash-success-bg: honeydew;
|
--color-flash-success-bg: honeydew;
|
||||||
--color-flash-success-border-left: forestgreen;
|
--color-flash-success-border-left: forestgreen;
|
||||||
|
--color-flash-warning: darkgoldenrod;
|
||||||
|
--color-flash-warning-bg: lightgoldenrodyellow;
|
||||||
|
--color-flash-warning-border-left: gold;
|
||||||
--color-mood-border: darkslateblue;
|
--color-mood-border: darkslateblue;
|
||||||
--color-mood-hover: lightsteelblue;
|
--color-mood-hover: lightsteelblue;
|
||||||
--color-mood-selected: lightblue;
|
--color-mood-selected: lightblue;
|
||||||
@ -324,6 +327,12 @@ summary:focus,
|
|||||||
color: var(--color-flash-error);
|
color: var(--color-flash-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flash-warning {
|
||||||
|
background-color: var(--color-flash-warning-bg);
|
||||||
|
border-left-color: var(--color-flash-warning-border-left);
|
||||||
|
color: var(--color-flash-warning);
|
||||||
|
}
|
||||||
|
|
||||||
.fieldset-items {
|
.fieldset-items {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -370,6 +379,11 @@ time {
|
|||||||
font-size: 1.0em;
|
font-size: 1.0em;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.tick-meta {
|
||||||
|
color: var(--color-log-muted);
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
.tick-pagination a {
|
.tick-pagination a {
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
|
@ -42,8 +42,15 @@ if (!$prerequisites->validateApplication()) {
|
|||||||
// Get the working database connection from prerequisites
|
// Get the working database connection from prerequisites
|
||||||
$db = $prerequisites->getDatabase();
|
$db = $prerequisites->getDatabase();
|
||||||
|
|
||||||
|
// Apply any pending database migrations
|
||||||
|
if (!$prerequisites->applyMigrations($db)){
|
||||||
|
$prerequisites->generateWebSummary();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if setup is complete (user exists and URL is configured)
|
// Check if setup is complete (user exists and URL is configured)
|
||||||
if (!(preg_match('/tkr-setup$/', $path))) {
|
// Skip the setup check for the default css
|
||||||
|
if (!(preg_match('/tkr-setup$/', $path) || preg_match('/default.css$/', $path))) {
|
||||||
try {
|
try {
|
||||||
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
|
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
|
||||||
$settings = (new SettingsModel($db))->get();
|
$settings = (new SettingsModel($db))->get();
|
||||||
@ -66,6 +73,10 @@ if (!(preg_match('/tkr-setup$/', $path))) {
|
|||||||
echo "<p>Please check your installation or contact your hosting provider.</p>";
|
echo "<p>Please check your installation or contact your hosting provider.</p>";
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// we're heading to setup. the base path hasn't been set. autodetect it
|
||||||
|
$autodetected = Util::getAutodetectedUrl();
|
||||||
|
$basePath = $autodetected['basePath'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -86,8 +97,11 @@ Session::start();
|
|||||||
Session::generateCsrfToken();
|
Session::generateCsrfToken();
|
||||||
|
|
||||||
// Remove the base path from the URL
|
// Remove the base path from the URL
|
||||||
if (strpos($path, $app['settings']->basePath) === 0) {
|
// If basePath isn't already set (i.e. we're not autodetecting it en route to tkr-setup),
|
||||||
$path = substr($path, strlen($app['settings']->basePath));
|
// set it to the value from settings
|
||||||
|
$basePath ??= $app['settings']->basePath;
|
||||||
|
if (strpos($path, $basePath) === 0) {
|
||||||
|
$path = substr($path, strlen($basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip the trailing slash from the resulting route
|
// strip the trailing slash from the resulting route
|
||||||
@ -100,11 +114,11 @@ Log::debug("Path requested: {$path}");
|
|||||||
// if this is a POST and we aren't in setup,
|
// if this is a POST and we aren't in setup,
|
||||||
// make sure there's a valid session
|
// make sure there's a valid session
|
||||||
// if not, redirect to /login or die as appropriate
|
// if not, redirect to /login or die as appropriate
|
||||||
if ($method === 'POST' && $path != 'setup') {
|
if ($method === 'POST' && $path != 'tkr-setup') {
|
||||||
if ($path != 'login'){
|
if ($path != 'login'){
|
||||||
if (!Session::isValid($_POST['csrf_token'])) {
|
if (!Session::isValid($_POST['csrf_token'])) {
|
||||||
// Invalid session - redirect to /login
|
// Invalid session - redirect to /login
|
||||||
Log::info('Attempt to POST with invalid session. Redirecting to login.');
|
Log::warning('Attempt to POST with invalid session. Redirecting to login.');
|
||||||
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, 'login'));
|
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, 'login'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class AdminController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->saveSettings($_POST, false);
|
$result = $this->saveSettings($_POST, false);
|
||||||
header('Location: ' . $_SERVER['PHP_SELF']);
|
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ class AdminController extends Controller {
|
|||||||
// for setup, we don't care if they're logged in
|
// for setup, we don't care if they're logged in
|
||||||
// (because they can't be until setup is complete)
|
// (because they can't be until setup is complete)
|
||||||
$result = $this->saveSettings($_POST, true);
|
$result = $this->saveSettings($_POST, true);
|
||||||
header('Location: ' . $_SERVER['PHP_SELF']);
|
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +91,7 @@ class AdminController extends Controller {
|
|||||||
$itemsPerPage = (int) ($postData['items_per_page'] ?? 25);
|
$itemsPerPage = (int) ($postData['items_per_page'] ?? 25);
|
||||||
$strictAccessibility = isset($postData['strict_accessibility']);
|
$strictAccessibility = isset($postData['strict_accessibility']);
|
||||||
$logLevel = (int) ($postData['log_level'] ?? 0);
|
$logLevel = (int) ($postData['log_level'] ?? 0);
|
||||||
|
$tickDeleteHours = (int) ($postData['tick_delete_hours'] ?? 1);
|
||||||
|
|
||||||
// Password
|
// Password
|
||||||
$password = $postData['password'] ?? '';
|
$password = $postData['password'] ?? '';
|
||||||
@ -152,6 +153,7 @@ class AdminController extends Controller {
|
|||||||
$app['settings']->itemsPerPage = $itemsPerPage;
|
$app['settings']->itemsPerPage = $itemsPerPage;
|
||||||
$app['settings']->strictAccessibility = $strictAccessibility;
|
$app['settings']->strictAccessibility = $strictAccessibility;
|
||||||
$app['settings']->logLevel = $logLevel;
|
$app['settings']->logLevel = $logLevel;
|
||||||
|
$app['settings']->tickDeleteHours = $tickDeleteHours;
|
||||||
|
|
||||||
// Save site settings and reload config from database
|
// Save site settings and reload config from database
|
||||||
$app['settings'] = $app['settings']->save();
|
$app['settings'] = $app['settings']->save();
|
||||||
|
@ -39,7 +39,7 @@ class AuthController extends Controller {
|
|||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to create login session for {$username}: " . $e->getMessage());
|
Log::error("Failed to create login session for {$username}: " . $e->getMessage());
|
||||||
Session::setFlashMessage('error', 'Login failed - session error');
|
Session::setFlashMessage('error', 'Login failed - session error');
|
||||||
header('Location: ' . $_SERVER['PHP_SELF']);
|
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -47,13 +47,13 @@ class AuthController extends Controller {
|
|||||||
|
|
||||||
// Set a flash message and reload the login page
|
// Set a flash message and reload the login page
|
||||||
Session::setFlashMessage('error', 'Invalid username or password');
|
Session::setFlashMessage('error', 'Invalid username or password');
|
||||||
header('Location: ' . $_SERVER['PHP_SELF']);
|
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Database error during login for {$username}: " . $e->getMessage());
|
Log::error("Database error during login for {$username}: " . $e->getMessage());
|
||||||
Session::setFlashMessage('error', 'Login temporarily unavailable');
|
Session::setFlashMessage('error', 'Login temporarily unavailable');
|
||||||
header('Location: ' . $_SERVER['PHP_SELF']);
|
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,32 +20,63 @@ class CssController extends Controller {
|
|||||||
global $app;
|
global $app;
|
||||||
$cssModel = new CssModel($app['db']);
|
$cssModel = new CssModel($app['db']);
|
||||||
$filename = "$baseFilename.css";
|
$filename = "$baseFilename.css";
|
||||||
|
Log::debug("Attempting to serve custom css: {$filename}");
|
||||||
|
|
||||||
|
// Make sure the file exists in the database
|
||||||
$cssRow = $cssModel->getByFilename($filename);
|
$cssRow = $cssModel->getByFilename($filename);
|
||||||
|
|
||||||
if (!$cssRow){
|
if (!$cssRow){
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
exit("CSS file not found: $filename");
|
$msg = "Custom css file not in database: {$filename}";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure the file exists on the filesystem and is readable
|
||||||
$filePath = CSS_UPLOAD_DIR . "/$filename";
|
$filePath = CSS_UPLOAD_DIR . "/$filename";
|
||||||
|
|
||||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
exit("CSS file not found: $filePath");
|
$msg = "Custom css file not found or not readable: {$filePath}";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This shouldn't be possible, but I'm being extra paranoid
|
// Make sure the file has a .css extension
|
||||||
// about user input
|
|
||||||
$ext = strToLower(pathinfo($filename, PATHINFO_EXTENSION));
|
$ext = strToLower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
if($ext != 'css'){
|
if($ext != 'css'){
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit("Invalid file type requested: $ext");
|
$msg = "Invalid file type requested: {$ext}";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we get here, serve the file
|
||||||
|
Log::debug("Serving custom css: {$filename}");
|
||||||
header('Content-type: text/css');
|
header('Content-type: text/css');
|
||||||
header('Cache-control: public, max-age=3600');
|
header('Cache-control: public, max-age=3600');
|
||||||
|
readfile($filePath);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serveDefaultCss(){
|
||||||
|
$filePath = PUBLIC_DIR . '/css/default.css';
|
||||||
|
Log::debug("Serving default css: {$filePath}");
|
||||||
|
|
||||||
|
// Make sure the default CSS file exists and is readable
|
||||||
|
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||||
|
http_response_code(404);
|
||||||
|
$msg = "Default CSS file not found";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the file
|
||||||
|
header('Content-type: text/css');
|
||||||
|
header('Cache-control: public, max-age=3600');
|
||||||
readfile($filePath);
|
readfile($filePath);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@ -64,7 +95,7 @@ class CssController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// redirect after handling to avoid resubmitting form
|
// redirect after handling to avoid resubmitting form
|
||||||
header('Location: ' . $_SERVER['PHP_SELF']);
|
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,18 +105,24 @@ class CssController extends Controller {
|
|||||||
// Don't try to delete the default theme.
|
// Don't try to delete the default theme.
|
||||||
if (!$_POST['selectCssFile']){
|
if (!$_POST['selectCssFile']){
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit("Cannot delete default theme");
|
$msg = "Cannot delete default theme.";
|
||||||
|
Log::warning($msg);
|
||||||
|
Session::setFlashMessage('warning', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the data for the selected CSS file
|
// Get the data for the selected CSS file
|
||||||
$cssId = $_POST['selectCssFile'];
|
$cssId = (int) $_POST['selectCssFile'];
|
||||||
$cssModel = new CssModel($app['db']);
|
$cssModel = new CssModel($app['db']);
|
||||||
$cssRow = $cssModel->getById($cssId);
|
$cssRow = $cssModel->getById($cssId);
|
||||||
|
|
||||||
// exit if the requested file isn't in the database
|
// exit if the requested file isn't in the database
|
||||||
if (!$cssRow){
|
if (!$cssRow){
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit("No entry found for css id $cssId");
|
$msg = "No entry found for css id {$cssId}.";
|
||||||
|
Log::warning($msg);
|
||||||
|
Session::setFlashMessage('warning', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the filename
|
// get the filename
|
||||||
@ -93,8 +130,11 @@ class CssController extends Controller {
|
|||||||
|
|
||||||
// delete the file from the database
|
// delete the file from the database
|
||||||
if (!$cssModel->delete($cssId)){
|
if (!$cssModel->delete($cssId)){
|
||||||
http_response_code(400);
|
http_response_code(500);
|
||||||
exit("Error deleting theme");
|
$msg = "Error deleting theme {$cssId}.";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the full path to the file
|
// Build the full path to the file
|
||||||
@ -103,33 +143,42 @@ class CssController extends Controller {
|
|||||||
// Exit if the file doesn't exist or isn't readable
|
// Exit if the file doesn't exist or isn't readable
|
||||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
exit("CSS file not found: $filePath");
|
$msg = "CSS file not found: {$filePath}";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the file
|
// Delete the file
|
||||||
if (!unlink($filePath)){
|
if (!unlink($filePath)){
|
||||||
http_response_code(400);
|
http_response_code(500);
|
||||||
exit("Error deleting file: $filePath");
|
$msg = "Error deleting file: {$filePath}";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the theme back to default
|
// Set the theme back to default
|
||||||
try {
|
try {
|
||||||
$app['settings']->cssId = null;
|
$app['settings']->cssId = null;
|
||||||
$app['settings'] = $app['settings']->save();
|
$app['settings'] = $app['settings']->save();
|
||||||
Session::setFlashMessage('success', 'Theme ' . $cssFilename . ' deleted.');
|
$msg = "Theme {$cssFilename} deleted.";
|
||||||
|
Log::debug($msg);
|
||||||
|
Session::setFlashMessage('success', $msg);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to update config after deleting theme: " . $e->getMessage());
|
$msg = "Failed to update config after deleting theme.";
|
||||||
Session::setFlashMessage('error', 'Theme deleted but failed to update settings');
|
Log::error($msg . ' ' . $e->getMessage());
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleSetTheme() {
|
private function handleSetTheme(): void {
|
||||||
global $app;
|
global $app;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($_POST['selectCssFile']){
|
if ($_POST['selectCssFile']){
|
||||||
// Set custom theme
|
// Set custom theme
|
||||||
$app['settings']->cssId = $_POST['selectCssFile'];
|
$app['settings']->cssId = (int)($_POST['selectCssFile']);
|
||||||
} else {
|
} else {
|
||||||
// Set default theme
|
// Set default theme
|
||||||
$app['settings']->cssId = null;
|
$app['settings']->cssId = null;
|
||||||
@ -144,7 +193,7 @@ class CssController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleUpload() {
|
private function handleUpload(): void {
|
||||||
try {
|
try {
|
||||||
// Check if file was uploaded
|
// Check if file was uploaded
|
||||||
if (!isset($_FILES['uploadCssFile']) || $_FILES['uploadCssFile']['error'] !== UPLOAD_ERR_OK) {
|
if (!isset($_FILES['uploadCssFile']) || $_FILES['uploadCssFile']['error'] !== UPLOAD_ERR_OK) {
|
||||||
@ -199,12 +248,11 @@ class CssController extends Controller {
|
|||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Set error flash message
|
// Set error flash message
|
||||||
// Todo - don't do a global catch like this. Subclass Exception.
|
|
||||||
Session::setFlashMessage('error', 'Upload exception: ' . $e->getMessage());
|
Session::setFlashMessage('error', 'Upload exception: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateCssContent($content) {
|
private function validateCssContent($content): void {
|
||||||
// Remove comments
|
// Remove comments
|
||||||
$content = preg_replace('/\/\*.*?\*\//s', '', $content);
|
$content = preg_replace('/\/\*.*?\*\//s', '', $content);
|
||||||
|
|
||||||
@ -225,7 +273,7 @@ class CssController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function scanForMaliciousContent($content, $fileName) {
|
private function scanForMaliciousContent($content, $fileName): void {
|
||||||
// Check for suspicious patterns
|
// Check for suspicious patterns
|
||||||
$suspiciousPatterns = [
|
$suspiciousPatterns = [
|
||||||
'/javascript:/i',
|
'/javascript:/i',
|
||||||
@ -264,12 +312,11 @@ class CssController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateSafeFileName($originalName) {
|
private function generateSafeFileName($originalName): string {
|
||||||
// Remove path information and dangerous characters
|
// Remove path information and dangerous characters
|
||||||
$fileName = basename($originalName);
|
$fileName = basename($originalName);
|
||||||
$fileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
|
$fileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
|
||||||
|
|
||||||
return $fileName;
|
return $fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,10 @@ declare(strict_types=1);
|
|||||||
$emojiModel = new EmojiModel($app['db']);
|
$emojiModel = new EmojiModel($app['db']);
|
||||||
$emojiList = $emojiModel->getAll();
|
$emojiList = $emojiModel->getAll();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to load emoji list: " . $e->getMessage());
|
|
||||||
$emojiList = [];
|
$emojiList = [];
|
||||||
Session::setFlashMessage('error', 'Failed to load custom emoji');
|
$msg = "Failed to load emoji list.";
|
||||||
|
Log::error($msg . " " . $e->getMessage());
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
$vars = [
|
$vars = [
|
||||||
@ -49,23 +50,31 @@ declare(strict_types=1);
|
|||||||
// TODO - log a warning if mbstring isn't loaded
|
// TODO - log a warning if mbstring isn't loaded
|
||||||
$charCount = mb_strlen($emoji, 'UTF-8');
|
$charCount = mb_strlen($emoji, 'UTF-8');
|
||||||
if ($charCount !== 1) {
|
if ($charCount !== 1) {
|
||||||
// TODO - handle error
|
$msg = "Emoji must be a single UTF-8 encoded character.";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log::warning("mbstring extension not loaded. Skipping emoji character count validation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the emoji is actually an emoji
|
// Validate the emoji is actually an emoji
|
||||||
$emojiPattern = '/^[\x{1F000}-\x{1F9FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}\x{1F900}-\x{1F9FF}\x{1FA70}-\x{1FAFF}]$/u';
|
$emojiPattern = '/^[\x{1F000}-\x{1F9FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}\x{1F900}-\x{1F9FF}\x{1FA70}-\x{1FAFF}]$/u';
|
||||||
|
|
||||||
if (!preg_match($emojiPattern, $emoji)) {
|
if (!preg_match($emojiPattern, $emoji)) {
|
||||||
// TODO - handle error
|
$msg = "Character is not a valid emoji.";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// emojis should have more bytes than characters
|
// emojis should have more bytes than characters
|
||||||
$byteCount = strlen($emoji);
|
$byteCount = strlen($emoji);
|
||||||
if ($byteCount <= 1) {
|
if ($byteCount <= 1) {
|
||||||
// TODO - handle error
|
$msg = "Character is not a valid emoji (too few bytes).";
|
||||||
|
Log::error($msg);
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +85,7 @@ declare(strict_types=1);
|
|||||||
global $app;
|
global $app;
|
||||||
|
|
||||||
if (!$this->isValidEmoji($emoji)){
|
if (!$this->isValidEmoji($emoji)){
|
||||||
Session::setFlashMessage('error', 'Invalid emoji format');
|
// exceptions are handled in isValidEmoji
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,10 +93,14 @@ declare(strict_types=1);
|
|||||||
try {
|
try {
|
||||||
$emojiModel = new EmojiModel($app['db']);
|
$emojiModel = new EmojiModel($app['db']);
|
||||||
$emojiModel->add($emoji, $description);
|
$emojiModel->add($emoji, $description);
|
||||||
Session::setFlashMessage('success', 'Emoji added successfully');
|
$msg = "Emoji added: {$emoji} - {$description}";
|
||||||
|
|
||||||
|
Log::debug($msg);
|
||||||
|
Session::setFlashMessage('success', $msg);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to add emoji: " . $e->getMessage());
|
$msg = "Failed to add emoji.";
|
||||||
Session::setFlashMessage('error', 'Failed to add emoji');
|
Log::error($msg . " " . $e->getMessage());
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,10 +113,14 @@ declare(strict_types=1);
|
|||||||
try {
|
try {
|
||||||
$emojiModel = new EmojiModel($app['db']);
|
$emojiModel = new EmojiModel($app['db']);
|
||||||
$emojiModel->delete($ids);
|
$emojiModel->delete($ids);
|
||||||
Session::setFlashMessage('success', 'Emoji deleted successfully');
|
$msg = "Emoji deleted.";
|
||||||
|
|
||||||
|
Log::debug($msg);
|
||||||
|
Session::setFlashMessage('success', $msg);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Failed to delete emoji: " . $e->getMessage());
|
$msg = "Failed to delete emoji.";
|
||||||
Session::setFlashMessage('error', 'Failed to delete emoji');
|
Log::error($msg . " " . $e->getMessage());
|
||||||
|
Session::setFlashMessage('error', $msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ class HomeController extends Controller {
|
|||||||
// renders the homepage view.
|
// renders the homepage view.
|
||||||
public function index(){
|
public function index(){
|
||||||
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
||||||
$data = $this->getHomeData($page);
|
$vars = $this->getHomeData($page);
|
||||||
$this->render("home.php", $data);
|
$this->render("home.php", $vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHomeData(int $page): array {
|
public function getHomeData(int $page): array {
|
||||||
|
@ -104,7 +104,7 @@ class LogController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function parseLogLine(string $line): ?array {
|
private function parseLogLine(string $line): ?array {
|
||||||
// Parse format: [2025-01-31 08:30:15] DEBUG: 192.168.1.100 [GET feed/rss] - message
|
// Parse format: [2025-01-31 08:30:15] DEBUG: 192.168.1.100 [GET admin/settings] - message
|
||||||
$pattern = '/^\[([^\]]+)\] (\w+): ([^\s]+)(?:\s+\[([^\]]+)\])? - (.+)$/';
|
$pattern = '/^\[([^\]]+)\] (\w+): ([^\s]+)(?:\s+\[([^\]]+)\])? - (.+)$/';
|
||||||
|
|
||||||
if (preg_match($pattern, $line, $matches)) {
|
if (preg_match($pattern, $line, $matches)) {
|
||||||
|
@ -2,22 +2,27 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
class TickController extends Controller{
|
class TickController extends Controller{
|
||||||
public function index(int $id){
|
public function index(string $id){
|
||||||
|
// This is because my router is too simplistic to cleanly handle type casting,
|
||||||
|
// so I just accept a sting here and cast it to an int immediately.
|
||||||
|
$id = (int) $id;
|
||||||
global $app;
|
global $app;
|
||||||
|
$vars = ['settings' => $app['settings']];
|
||||||
|
|
||||||
Log::debug("Fetching tick with ID: {$id}");
|
Log::debug("Fetching tick with ID: {$id}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$tickModel = new TickModel($app['db'], $app['settings']);
|
$tickModel = new TickModel($app['db'], $app['settings']);
|
||||||
$vars = $tickModel->get($id);
|
$tick = $tickModel->get($id);
|
||||||
|
|
||||||
if (empty($vars) || !isset($vars['tick'])) {
|
if (empty($tick) || !isset($tick['tick'])) {
|
||||||
Log::warning("Tick not found for ID: {$id}");
|
Log::warning("Tick not found for ID: {$id}");
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo '<h1>404 - Tick Not Found</h1>';
|
$this->render('tick-404.php', $vars);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$vars = array_merge($tick, $vars);
|
||||||
Log::info("Successfully loaded tick {$id}: " . substr($vars['tick'], 0, 50) . (strlen($vars['tick']) > 50 ? '...' : ''));
|
Log::info("Successfully loaded tick {$id}: " . substr($vars['tick'], 0, 50) . (strlen($vars['tick']) > 50 ? '...' : ''));
|
||||||
$this->render('tick.php', $vars);
|
$this->render('tick.php', $vars);
|
||||||
|
|
||||||
@ -30,32 +35,32 @@ class TickController extends Controller{
|
|||||||
|
|
||||||
public function handleDelete(string $id){
|
public function handleDelete(string $id){
|
||||||
global $app;
|
global $app;
|
||||||
|
|
||||||
$id = (int) $id;
|
$id = (int) $id;
|
||||||
Log::debug("Attempting to delete tick with ID: {$id}");
|
Log::debug("Attempting to delete tick with ID: {$id}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$tickModel = new TickModel($app['db'], $app['settings']);
|
$tickModel = new TickModel($app['db'], $app['settings']);
|
||||||
|
|
||||||
// TickModel->delete() handles validation and sets flash messages:
|
// TickModel->delete() handles validation and sets flash messages:
|
||||||
// - "Tick not found" if tick doesn't exist
|
// - "Tick not found" if tick doesn't exist
|
||||||
// - "Tick is too old to delete" if outside deletion window
|
// - "Tick is too old to delete" if outside deletion window
|
||||||
// - "Deleted: '{content}'" on success
|
// - "Deleted: '{content}'" on success
|
||||||
$success = $tickModel->delete($id);
|
$success = $tickModel->delete($id);
|
||||||
|
|
||||||
if ($success) {
|
if ($success) {
|
||||||
Log::info("Successfully deleted tick {$id}");
|
Log::info("Successfully deleted tick {$id}");
|
||||||
} else {
|
} else {
|
||||||
Log::warning("Failed to delete tick {$id}");
|
Log::warning("Failed to delete tick {$id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error("Exception while deleting tick {$id}: " . $e->getMessage());
|
Log::error("Exception while deleting tick {$id}: " . $e->getMessage());
|
||||||
Session::setFlashMessage('error', 'An error occurred while deleting the tick');
|
Session::setFlashMessage('error', 'An error occurred while deleting the tick');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to homepage
|
// Redirect back to homepage
|
||||||
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
|
header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
|
||||||
exit();
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -18,7 +18,7 @@ class AtomGenerator extends FeedGenerator {
|
|||||||
Log::debug("Building Atom feed for " . $this->settings->siteTitle);
|
Log::debug("Building Atom feed for " . $this->settings->siteTitle);
|
||||||
$feedTitle = Util::escape_xml($this->settings->siteTitle . " Atom Feed");
|
$feedTitle = Util::escape_xml($this->settings->siteTitle . " Atom Feed");
|
||||||
$siteUrl = Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath));
|
$siteUrl = Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath));
|
||||||
$feedUrl = Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath, 'feed/atom'));
|
$feedUrl = Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath, 'atom'));
|
||||||
$updated = date(DATE_ATOM, strtotime($this->ticks[0]['timestamp'] ?? 'now'));
|
$updated = date(DATE_ATOM, strtotime($this->ticks[0]['timestamp'] ?? 'now'));
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
@ -41,7 +41,7 @@ class AtomGenerator extends FeedGenerator {
|
|||||||
$tickUrl = Util::escape_xml($siteUrl . $tickPath);
|
$tickUrl = Util::escape_xml($siteUrl . $tickPath);
|
||||||
$tickTime = date(DATE_ATOM, strtotime($tick['timestamp']));
|
$tickTime = date(DATE_ATOM, strtotime($tick['timestamp']));
|
||||||
$tickTitle = Util::escape_xml($tick['tick']);
|
$tickTitle = Util::escape_xml($tick['tick']);
|
||||||
$tickContent = Util::linkify($tickTitle);
|
$tickContent = Util::escape_xml(Util::linkify(Util::escape_html($tick['tick'])));
|
||||||
?>
|
?>
|
||||||
<entry>
|
<entry>
|
||||||
<title><?= $tickTitle ?></title>
|
<title><?= $tickTitle ?></title>
|
||||||
|
@ -23,7 +23,7 @@ class RssGenerator extends FeedGenerator {
|
|||||||
<channel>
|
<channel>
|
||||||
<title><?php echo Util::escape_xml($this->settings->siteTitle . ' RSS Feed') ?></title>
|
<title><?php echo Util::escape_xml($this->settings->siteTitle . ' RSS Feed') ?></title>
|
||||||
<link><?php echo Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath))?></link>
|
<link><?php echo Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath))?></link>
|
||||||
<atom:link href="<?php echo Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath, 'feed/rss'))?>"
|
<atom:link href="<?php echo Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath, 'rss'))?>"
|
||||||
rel="self"
|
rel="self"
|
||||||
type="application/rss+xml" />
|
type="application/rss+xml" />
|
||||||
<description><?php echo Util::escape_xml($this->settings->siteDescription) ?></description>
|
<description><?php echo Util::escape_xml($this->settings->siteDescription) ?></description>
|
||||||
@ -35,10 +35,11 @@ class RssGenerator extends FeedGenerator {
|
|||||||
$tickUrl = Util::escape_xml($this->buildTickUrl($tick['id']));
|
$tickUrl = Util::escape_xml($this->buildTickUrl($tick['id']));
|
||||||
$tickDate = date(DATE_RSS, strtotime($tick['timestamp']));
|
$tickDate = date(DATE_RSS, strtotime($tick['timestamp']));
|
||||||
$tickTitle = Util::escape_xml($tick['tick']);
|
$tickTitle = Util::escape_xml($tick['tick']);
|
||||||
$tickDescription = Util::linkify($tickTitle);
|
$tickDescription = Util::escape_xml(Util::linkify(Util::escape_html($tick['tick'])));
|
||||||
|
Log::debug("RSS item: {$tickDescription}");
|
||||||
?>
|
?>
|
||||||
<item>
|
<item>
|
||||||
<title><?php echo $tickTitle ?></title>
|
<title><?php echo $tickTitle; ?></title>
|
||||||
<link><?php echo $tickUrl; ?></link>
|
<link><?php echo $tickUrl; ?></link>
|
||||||
<description><?php echo $tickDescription; ?></description>
|
<description><?php echo $tickDescription; ?></description>
|
||||||
<pubDate><?php echo $tickDate; ?></pubDate>
|
<pubDate><?php echo $tickDate; ?></pubDate>
|
||||||
|
@ -407,7 +407,7 @@ class Prerequisites {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createDatabase() {
|
private function createDatabase(): bool {
|
||||||
$dbFile = $this->baseDir . '/storage/db/tkr.sqlite';
|
$dbFile = $this->baseDir . '/storage/db/tkr.sqlite';
|
||||||
|
|
||||||
// Test database connection (will create file if needed)
|
// Test database connection (will create file if needed)
|
||||||
@ -442,7 +442,7 @@ class Prerequisites {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyMigrations($db) {
|
public function applyMigrations($db): bool {
|
||||||
try {
|
try {
|
||||||
$migrator = new Migrator($db);
|
$migrator = new Migrator($db);
|
||||||
$migrator->migrate();
|
$migrator->migrate();
|
||||||
@ -452,8 +452,6 @@ class Prerequisites {
|
|||||||
true,
|
true,
|
||||||
'All database migrations applied successfully'
|
'All database migrations applied successfully'
|
||||||
);
|
);
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->addCheck(
|
$this->addCheck(
|
||||||
'Database Migrations',
|
'Database Migrations',
|
||||||
@ -463,6 +461,8 @@ class Prerequisites {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate system requirements that can't be fixed by the script
|
// Validate system requirements that can't be fixed by the script
|
||||||
@ -511,6 +511,8 @@ class Prerequisites {
|
|||||||
|
|
||||||
// Create missing application components
|
// Create missing application components
|
||||||
public function createMissing(): bool {
|
public function createMissing(): bool {
|
||||||
|
// If we're calling this, there were likely setup validation errors
|
||||||
|
$currentErrors = count($this->errors);
|
||||||
$this->log("=== tkr setup started at " . date('Y-m-d H:i:s') . " ===", true);
|
$this->log("=== tkr setup started at " . date('Y-m-d H:i:s') . " ===", true);
|
||||||
|
|
||||||
if ($this->isCli) {
|
if ($this->isCli) {
|
||||||
@ -526,8 +528,8 @@ class Prerequisites {
|
|||||||
$this->generateCliSummary($results);
|
$this->generateCliSummary($results);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return true only if no errors occurred
|
// Return true only if no NEW errors occurred
|
||||||
return count($this->errors) === 0;
|
return count($this->errors) === $currentErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,8 +15,8 @@ class Router {
|
|||||||
['admin/emoji', 'EmojiController'],
|
['admin/emoji', 'EmojiController'],
|
||||||
['admin/emoji', 'EmojiController@handlePost', ['POST']],
|
['admin/emoji', 'EmojiController@handlePost', ['POST']],
|
||||||
['admin/logs', 'LogController'],
|
['admin/logs', 'LogController'],
|
||||||
['feed/rss', 'FeedController@rss'],
|
['rss', 'FeedController@rss'],
|
||||||
['feed/atom', 'FeedController@atom'],
|
['atom', 'FeedController@atom'],
|
||||||
['login', 'AuthController@showLogin'],
|
['login', 'AuthController@showLogin'],
|
||||||
['login', 'AuthController@handleLogin', ['POST']],
|
['login', 'AuthController@handleLogin', ['POST']],
|
||||||
['logout', 'AuthController@handleLogout', ['GET', 'POST']],
|
['logout', 'AuthController@handleLogout', ['GET', 'POST']],
|
||||||
@ -27,6 +27,7 @@ class Router {
|
|||||||
['tick/{id}', 'TickController'],
|
['tick/{id}', 'TickController'],
|
||||||
['tick/{id}/delete', 'TickController@handleDelete', ['POST']],
|
['tick/{id}/delete', 'TickController@handleDelete', ['POST']],
|
||||||
['css/custom/{filename}.css', 'CssController@serveCustomCss'],
|
['css/custom/{filename}.css', 'CssController@serveCustomCss'],
|
||||||
|
['css/default.css', 'CssController@serveDefaultCss'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,15 @@ class Session {
|
|||||||
// global $_SESSION associative array
|
// global $_SESSION associative array
|
||||||
public static function start(): void{
|
public static function start(): void{
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
// Cookie security settings
|
||||||
|
ini_set('session.cookie_httponly', 1);
|
||||||
|
ini_set('session.cookie_samesite', 'Strict');
|
||||||
|
|
||||||
|
// Enable secure cookie flag if HTTPS is being used
|
||||||
|
if (($_SERVER['HTTPS'] ?? 'off') === 'on') {
|
||||||
|
ini_set('session.cookie_secure', 1);
|
||||||
|
}
|
||||||
|
|
||||||
$existingSessionId = $_COOKIE['PHPSESSID'] ?? null;
|
$existingSessionId = $_COOKIE['PHPSESSID'] ?? null;
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
@ -58,7 +67,6 @@ class Session {
|
|||||||
// valid types are:
|
// valid types are:
|
||||||
// - success
|
// - success
|
||||||
// - error
|
// - error
|
||||||
// - info
|
|
||||||
// - warning
|
// - warning
|
||||||
public static function setFlashMessage(string $type, string $message): void {
|
public static function setFlashMessage(string $type, string $message): void {
|
||||||
if (!isset($_SESSION['flash'][$type])){
|
if (!isset($_SESSION['flash'][$type])){
|
||||||
|
@ -125,6 +125,13 @@ class Util {
|
|||||||
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php';
|
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php';
|
||||||
$basePath = dirname($scriptName);
|
$basePath = dirname($scriptName);
|
||||||
|
|
||||||
|
// Handle shared hosting scenario where document root can't be set to public/
|
||||||
|
// If script name ends with /public/index.php, we need to go up one directory
|
||||||
|
if (str_ends_with($scriptName, '/public/index.php')) {
|
||||||
|
$basePath = dirname($basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure base path always has a trailing /
|
||||||
if ($basePath === '/' || $basePath === '.' || $basePath === '') {
|
if ($basePath === '/' || $basePath === '.' || $basePath === '') {
|
||||||
$basePath = '/';
|
$basePath = '/';
|
||||||
} else {
|
} else {
|
||||||
@ -132,10 +139,7 @@ class Util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct full URL
|
// Construct full URL
|
||||||
$fullUrl = $baseUrl;
|
$fullUrl = $baseUrl . $basePath;
|
||||||
if ($basePath !== '/') {
|
|
||||||
$fullUrl .= ltrim($basePath, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'baseUrl' => $baseUrl,
|
'baseUrl' => $baseUrl,
|
||||||
|
@ -12,8 +12,7 @@ class SettingsModel {
|
|||||||
public ?int $cssId = null;
|
public ?int $cssId = null;
|
||||||
public bool $strictAccessibility = true;
|
public bool $strictAccessibility = true;
|
||||||
public ?int $logLevel = null;
|
public ?int $logLevel = null;
|
||||||
// not currently configurable
|
public ?int $tickDeleteHours = null;
|
||||||
public int $tickDeleteHours = 1;
|
|
||||||
|
|
||||||
public function __construct(private PDO $db) {}
|
public function __construct(private PDO $db) {}
|
||||||
|
|
||||||
@ -28,7 +27,8 @@ class SettingsModel {
|
|||||||
items_per_page,
|
items_per_page,
|
||||||
css_id,
|
css_id,
|
||||||
strict_accessibility,
|
strict_accessibility,
|
||||||
log_level
|
log_level,
|
||||||
|
tick_delete_hours
|
||||||
FROM settings WHERE id=1");
|
FROM settings WHERE id=1");
|
||||||
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
@ -41,7 +41,8 @@ class SettingsModel {
|
|||||||
$c->itemsPerPage = (int) $row['items_per_page'];
|
$c->itemsPerPage = (int) $row['items_per_page'];
|
||||||
$c->cssId = (int) $row['css_id'];
|
$c->cssId = (int) $row['css_id'];
|
||||||
$c->strictAccessibility = (bool) $row['strict_accessibility'];
|
$c->strictAccessibility = (bool) $row['strict_accessibility'];
|
||||||
$c->logLevel = $row['log_level'];
|
$c->logLevel = (int) ($row['log_level'] ?? 2);
|
||||||
|
$c->tickDeleteHours = (int) ($row['tick_delete_hours'] ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $c;
|
return $c;
|
||||||
@ -51,6 +52,7 @@ class SettingsModel {
|
|||||||
$settingsCount = (int) $this->db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
|
$settingsCount = (int) $this->db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
|
||||||
|
|
||||||
if ($settingsCount === 0){
|
if ($settingsCount === 0){
|
||||||
|
Log::debug('Initializing settings');
|
||||||
$stmt = $this->db->prepare("INSERT INTO settings (
|
$stmt = $this->db->prepare("INSERT INTO settings (
|
||||||
id,
|
id,
|
||||||
site_title,
|
site_title,
|
||||||
@ -60,10 +62,12 @@ class SettingsModel {
|
|||||||
items_per_page,
|
items_per_page,
|
||||||
css_id,
|
css_id,
|
||||||
strict_accessibility,
|
strict_accessibility,
|
||||||
log_level
|
log_level,
|
||||||
|
tick_delete_hours
|
||||||
)
|
)
|
||||||
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)");
|
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
} else {
|
} else {
|
||||||
|
Log::debug('Updating settings');
|
||||||
$stmt = $this->db->prepare("UPDATE settings SET
|
$stmt = $this->db->prepare("UPDATE settings SET
|
||||||
site_title=?,
|
site_title=?,
|
||||||
site_description=?,
|
site_description=?,
|
||||||
@ -72,10 +76,21 @@ class SettingsModel {
|
|||||||
items_per_page=?,
|
items_per_page=?,
|
||||||
css_id=?,
|
css_id=?,
|
||||||
strict_accessibility=?,
|
strict_accessibility=?,
|
||||||
log_level=?
|
log_level=?,
|
||||||
|
tick_delete_hours=?
|
||||||
WHERE id=1");
|
WHERE id=1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log::debug("Site title: " . $this->siteTitle);
|
||||||
|
Log::debug("Site description: " . $this->siteDescription);
|
||||||
|
Log::debug("Base URL: " . $this->baseUrl);
|
||||||
|
Log::debug("Base path: " . $this->basePath);
|
||||||
|
Log::debug("Items per page: " . $this->itemsPerPage);
|
||||||
|
Log::debug("CSS ID: " . $this->cssId);
|
||||||
|
Log::debug("Strict accessibility: " . $this->strictAccessibility);
|
||||||
|
Log::debug("Log level: " . $this->logLevel);
|
||||||
|
Log::debug("Tick delete window: " . $this->tickDeleteHours);
|
||||||
|
|
||||||
$stmt->execute([$this->siteTitle,
|
$stmt->execute([$this->siteTitle,
|
||||||
$this->siteDescription,
|
$this->siteDescription,
|
||||||
$this->baseUrl,
|
$this->baseUrl,
|
||||||
@ -83,7 +98,8 @@ class SettingsModel {
|
|||||||
$this->itemsPerPage,
|
$this->itemsPerPage,
|
||||||
$this->cssId,
|
$this->cssId,
|
||||||
$this->strictAccessibility,
|
$this->strictAccessibility,
|
||||||
$this->logLevel
|
$this->logLevel,
|
||||||
|
$this->tickDeleteHours
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->get();
|
return $this->get();
|
||||||
|
@ -7,14 +7,14 @@ class TickModel {
|
|||||||
public function getPage(int $limit, int $offset = 0): array {
|
public function getPage(int $limit, int $offset = 0): array {
|
||||||
$stmt = $this->db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?");
|
$stmt = $this->db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?");
|
||||||
$stmt->execute([$limit, $offset]);
|
$stmt->execute([$limit, $offset]);
|
||||||
|
|
||||||
$ticks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$ticks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
return array_map(function($tick) {
|
return array_map(function($tick) {
|
||||||
$tickTime = new DateTimeImmutable($tick['timestamp'], new DateTimeZone('UTC'));
|
$tickTime = new DateTimeImmutable($tick['timestamp'], new DateTimeZone('UTC'));
|
||||||
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
||||||
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
|
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
|
||||||
|
|
||||||
$tick['can_delete'] = $hoursSinceCreation <= $this->settings->tickDeleteHours;
|
$tick['can_delete'] = $hoursSinceCreation <= $this->settings->tickDeleteHours;
|
||||||
return $tick;
|
return $tick;
|
||||||
}, $ticks);
|
}, $ticks);
|
||||||
@ -41,7 +41,6 @@ class TickModel {
|
|||||||
return [
|
return [
|
||||||
'tickTime' => $row['timestamp'],
|
'tickTime' => $row['timestamp'],
|
||||||
'tick' => $row['tick'],
|
'tick' => $row['tick'],
|
||||||
'settings' => $this->settings,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,26 +49,26 @@ class TickModel {
|
|||||||
$stmt = $this->db->prepare("SELECT tick, timestamp FROM tick WHERE id=?");
|
$stmt = $this->db->prepare("SELECT tick, timestamp FROM tick WHERE id=?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($row === false || empty($row)) {
|
if ($row === false || empty($row)) {
|
||||||
Session::setFlashMessage('error', 'Tick not found');
|
Session::setFlashMessage('error', 'Tick not found');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check deletion window
|
// Check deletion window
|
||||||
$tickTime = new DateTimeImmutable($row['timestamp'], new DateTimeZone('UTC'));
|
$tickTime = new DateTimeImmutable($row['timestamp'], new DateTimeZone('UTC'));
|
||||||
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
||||||
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
|
$hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
|
||||||
|
|
||||||
if ($hoursSinceCreation > $this->settings->tickDeleteHours) {
|
if ($hoursSinceCreation > $this->settings->tickDeleteHours) {
|
||||||
Session::setFlashMessage('error', 'Tick is too old to delete');
|
Session::setFlashMessage('error', 'Tick is too old to delete');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete and set success message
|
// Delete and set success message
|
||||||
$stmt = $this->db->prepare("DELETE FROM tick WHERE id=?");
|
$stmt = $this->db->prepare("DELETE FROM tick WHERE id=?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
Session::setFlashMessage('success', "Deleted: '{$row['tick']}'");
|
Session::setFlashMessage('success', "Deleted: '{$row['tick']}'");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -23,14 +23,14 @@ class TicksView {
|
|||||||
$relativeTime = Util::relative_time($tick['timestamp']);
|
$relativeTime = Util::relative_time($tick['timestamp']);
|
||||||
?>
|
?>
|
||||||
<li class="tick" tabindex="0">
|
<li class="tick" tabindex="0">
|
||||||
<?php if ($tick['can_delete']): ?>
|
<?php if (Session::isLoggedIn() && $tick['can_delete']): ?>
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="<?= Util::buildRelativeUrl($settings->basePath, "tick/{$tick['id']}/delete") ?>"
|
action="<?= Util::buildRelativeUrl($settings->basePath, "tick/{$tick['id']}/delete") ?>"
|
||||||
class="delete-tick-form">
|
class="delete-tick-form">
|
||||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
||||||
<button type="submit" class="delete-tick-button">🗑️</button>
|
<button type="submit" class="delete-tick-button">🗑️</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
<time datetime="<?php echo $datetime->format('c') ?>"><?php echo Util::escape_html($relativeTime) ?></time>
|
<time datetime="<?php echo $datetime->format('c') ?>"><?php echo Util::escape_html($relativeTime) ?></time>
|
||||||
<span class="tick-text"><?php echo Util::linkify(Util::escape_html($tick['tick'])) ?></span>
|
<span class="tick-text"><?php echo Util::linkify(Util::escape_html($tick['tick'])) ?></span>
|
||||||
</li>
|
</li>
|
||||||
|
@ -19,11 +19,11 @@
|
|||||||
<link rel="alternate"
|
<link rel="alternate"
|
||||||
type="application/rss+xml"
|
type="application/rss+xml"
|
||||||
title="<?php echo Util::escape_html($settings->siteTitle) ?> RSS Feed"
|
title="<?php echo Util::escape_html($settings->siteTitle) ?> RSS Feed"
|
||||||
href="<?php echo Util::escape_html($settings->baseUrl . $settings->basePath)?>feed/rss/">
|
href="<?php echo Util::escape_html($settings->baseUrl . $settings->basePath)?>rss/">
|
||||||
<link rel="alternate"
|
<link rel="alternate"
|
||||||
type="application/atom+xml"
|
type="application/atom+xml"
|
||||||
title="<?php echo Util::escape_html($settings->siteTitle) ?> Atom Feed"
|
title="<?php echo Util::escape_html($settings->siteTitle) ?> Atom Feed"
|
||||||
href="<?php echo Util::escape_html($settings->baseUrl . $settings->basePath)?>feed/atom/">
|
href="<?php echo Util::escape_html($settings->baseUrl . $settings->basePath)?>atom/">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<?php include TEMPLATES_DIR . '/partials/navbar.php'?>
|
<?php include TEMPLATES_DIR . '/partials/navbar.php'?>
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
<?php /** @var SettingsModel $settings */ ?>
|
<?php /** @var SettingsModel $settings */ ?>
|
||||||
<?php /** @var UserModel $user */ ?>
|
<?php /** @var UserModel $user */ ?>
|
||||||
<?php /** @var isSetup bool */ ?>
|
<?php /** @var isSetup bool */ ?>
|
||||||
<h1><?php if ($isSetup): ?>Setup<?php else: ?>Admin<?php endif; ?></h1>
|
<?php
|
||||||
|
$title = $isSetup ? 'Setup' : 'Admin';
|
||||||
|
$urlPath = $isSetup ? 'tkr-setup' : 'admin'
|
||||||
|
?>
|
||||||
|
<h1><?php echo $title ?></h1>
|
||||||
<main>
|
<main>
|
||||||
<form
|
<form
|
||||||
action="<?php echo Util::buildRelativeUrl($settings->basePath, ($isSetup ? 'setup' : 'admin')) ?>"
|
action="<?php echo Util::buildRelativeUrl($settings->basePath, $urlPath) ?>"
|
||||||
method="post">
|
method="post">
|
||||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
<input type="hidden" name="csrf_token" value="<?php echo Util::escape_html($_SESSION['csrf_token']) ?>">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>User settings</legend>
|
<legend>User settings</legend>
|
||||||
<div class="fieldset-items">
|
<div class="fieldset-items">
|
||||||
@ -14,19 +18,19 @@
|
|||||||
<input type="text"
|
<input type="text"
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
value="<?= Util::escape_html($user->username) ?>"
|
value="<?php echo Util::escape_html($user->username) ?>"
|
||||||
required>
|
required>
|
||||||
<label for="display_name">Display name <span class=required>*</span></label>
|
<label for="display_name">Display name <span class=required>*</span></label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="display_name"
|
id="display_name"
|
||||||
name="display_name"
|
name="display_name"
|
||||||
value="<?= Util::escape_html($user->displayName) ?>"
|
value="<?php echo Util::escape_html($user->displayName) ?>"
|
||||||
required>
|
required>
|
||||||
<label for="website">Website </label>
|
<label for="website">Website </label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="website"
|
id="website"
|
||||||
name="website"
|
name="website"
|
||||||
value="<?= Util::escape_html($user->website) ?>">
|
value="<?php echo Util::escape_html($user->website) ?>">
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@ -36,43 +40,48 @@
|
|||||||
<input type="text"
|
<input type="text"
|
||||||
id="site_title"
|
id="site_title"
|
||||||
name="site_title"
|
name="site_title"
|
||||||
value="<?= Util::escape_html($settings->siteTitle) ?>"
|
value="<?php echo Util::escape_html($settings->siteTitle) ?>"
|
||||||
required>
|
required>
|
||||||
<label for="site_description">Description <span class=required>*</span></label>
|
<label for="site_description">Description <span class=required>*</span></label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="site_description"
|
id="site_description"
|
||||||
name="site_description"
|
name="site_description"
|
||||||
value="<?= Util::escape_html($settings->siteDescription) ?>">
|
value="<?php echo Util::escape_html($settings->siteDescription) ?>">
|
||||||
<label for="base_url">Base URL <span class=required>*</span></label>
|
<label for="base_url">Base URL <span class=required>*</span></label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="base_url"
|
id="base_url"
|
||||||
name="base_url"
|
name="base_url"
|
||||||
value="<?= Util::escape_html($settings->baseUrl) ?>"
|
value="<?php echo Util::escape_html($settings->baseUrl) ?>"
|
||||||
required>
|
required>
|
||||||
<label for="base_path">Base path <span class=required>*</span></label>
|
<label for="base_path">Base path <span class=required>*</span></label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="base_path"
|
id="base_path"
|
||||||
name="base_path"
|
name="base_path"
|
||||||
value="<?= Util::escape_html($settings->basePath) ?>"
|
value="<?php echo Util::escape_html($settings->basePath) ?>"
|
||||||
required>
|
required>
|
||||||
<label for="items_per_page">Ticks per page (max 50) <span class=required>*</span></label>
|
<label for="items_per_page">Ticks per page (max 50) <span class=required>*</span></label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="items_per_page"
|
id="items_per_page"
|
||||||
name="items_per_page"
|
name="items_per_page"
|
||||||
value="<?= $settings->itemsPerPage ?>" min="1" max="50"
|
value="<?php echo $settings->itemsPerPage ?>" min="1" max="50"
|
||||||
required>
|
required>
|
||||||
|
<label for="tick_delete_hours">Tick delete window (hours)</label>
|
||||||
|
<input type="number"
|
||||||
|
id="tick_delete_hours"
|
||||||
|
name="tick_delete_hours"
|
||||||
|
value="<?php echo ($settings->tickDeleteHours ?? 1) ?>" min="1">
|
||||||
<label for="strict_accessibility">Strict accessibility</label>
|
<label for="strict_accessibility">Strict accessibility</label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
id="strict_accessibility"
|
id="strict_accessibility"
|
||||||
name="strict_accessibility"
|
name="strict_accessibility"
|
||||||
value="1"
|
value="1"
|
||||||
<?php if ($settings->strictAccessibility): ?> checked <?php endif; ?>>
|
<?php if ($settings->strictAccessibility): ?> checked <?php endif; ?>>
|
||||||
<label for="strict_accessibility">Log Level</label>
|
<label for="log_level">Log Level</label>
|
||||||
<select id="log_level" name="log_level">
|
<select id="log_level" name="log_level">
|
||||||
<option value="1" <?= ($settings->logLevel ?? 2) == 1 ? 'selected' : '' ?>>DEBUG</option>
|
<option value="1" <?php echo ($settings->logLevel ?? 2) == 1 ? 'selected' : '' ?>>DEBUG</option>
|
||||||
<option value="2" <?= ($settings->logLevel ?? 2) == 2 ? 'selected' : '' ?>>INFO</option>
|
<option value="2" <?php echo ($settings->logLevel ?? 2) == 2 ? 'selected' : '' ?>>INFO</option>
|
||||||
<option value="3" <?= ($settings->logLevel ?? 2) == 3 ? 'selected' : '' ?>>WARNING</option>
|
<option value="3" <?php echo ($settings->logLevel ?? 2) == 3 ? 'selected' : '' ?>>WARNING</option>
|
||||||
<option value="4" <?= ($settings->logLevel ?? 2) == 4 ? 'selected' : '' ?>>ERROR</option>
|
<option value="4" <?php echo ($settings->logLevel ?? 2) == 4 ? 'selected' : '' ?>>ERROR</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
<summary aria-haspopup="true">feeds</summary>
|
<summary aria-haspopup="true">feeds</summary>
|
||||||
<div class="dropdown-items">
|
<div class="dropdown-items">
|
||||||
<a <?php if($settings->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($settings->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html(Util::buildRelativeUrl($settings->basePath, 'feed/rss')) ?>">rss</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($settings->basePath, 'rss')) ?>">rss</a>
|
||||||
<a <?php if($settings->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($settings->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html(Util::buildRelativeUrl($settings->basePath, 'feed/atom')) ?>">atom</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($settings->basePath, 'atom')) ?>">atom</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<?php if (!Session::isLoggedIn()): ?>
|
<?php if (!Session::isLoggedIn()): ?>
|
||||||
|
4
templates/partials/tick-404.php
Normal file
4
templates/partials/tick-404.php
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="not-found-container">
|
||||||
|
<h1>Tick Not Found</h1>
|
||||||
|
<p>The tick you're looking for has been deleted or never existed.</p>
|
||||||
|
</div>
|
@ -1,4 +1,14 @@
|
|||||||
<?php /** @var Date $tickTime */ ?>
|
<?php /** @var Date $tickTime */ ?>
|
||||||
<?php /** @var string $tick */ ?>
|
<?php /** @var string $tick */ ?>
|
||||||
<h1>Tick from <?= $tickTime; ?></h1>
|
<?php $displayTime = DateTimeImmutable::createFromformat('Y-m-d H:i:s', $tickTime) ?>
|
||||||
<p><?= Util::linkify(Util::escape_html($tick)) ?></p>
|
<div class="tick-container">
|
||||||
|
<article class="tick">
|
||||||
|
<header class="tick-header">
|
||||||
|
<h1>Tick</h1>
|
||||||
|
<p class="tick-meta">Posted on <time class="tick-meta" datetime="<?= $displayTime->format('c') ?>"><?= $displayTime->format('F j, Y \a\t g:i A') ?></time> UTC</p>
|
||||||
|
</header>
|
||||||
|
<div class="tick-text">
|
||||||
|
<?= Util::linkify(Util::escape_html($tick)) ?>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
@ -22,6 +22,7 @@ class AdminControllerTest extends TestCase
|
|||||||
$this->settings->baseUrl = 'https://example.com';
|
$this->settings->baseUrl = 'https://example.com';
|
||||||
$this->settings->basePath = '/tkr';
|
$this->settings->basePath = '/tkr';
|
||||||
$this->settings->itemsPerPage = 10;
|
$this->settings->itemsPerPage = 10;
|
||||||
|
$this->settings->tickDeleteHours = 2;
|
||||||
|
|
||||||
$this->user = new UserModel($this->mockPdo);
|
$this->user = new UserModel($this->mockPdo);
|
||||||
$this->user->username = 'testuser';
|
$this->user->username = 'testuser';
|
||||||
@ -119,7 +120,8 @@ class AdminControllerTest extends TestCase
|
|||||||
'items_per_page' => 15,
|
'items_per_page' => 15,
|
||||||
'css_id' => null,
|
'css_id' => null,
|
||||||
'strict_accessibility' => true,
|
'strict_accessibility' => true,
|
||||||
'log_level' => 2
|
'log_level' => 2,
|
||||||
|
'tick_delete_hours' => 3
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'username' => 'newuser',
|
'username' => 'newuser',
|
||||||
@ -153,7 +155,8 @@ class AdminControllerTest extends TestCase
|
|||||||
'base_path' => '/updated',
|
'base_path' => '/updated',
|
||||||
'items_per_page' => 15,
|
'items_per_page' => 15,
|
||||||
'strict_accessibility' => 'on',
|
'strict_accessibility' => 'on',
|
||||||
'log_level' => 2
|
'log_level' => 2,
|
||||||
|
'tick_delete_hours' => 3
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = $controller->saveSettings($postData, false);
|
$result = $controller->saveSettings($postData, false);
|
||||||
@ -177,7 +180,8 @@ class AdminControllerTest extends TestCase
|
|||||||
'items_per_page' => 10,
|
'items_per_page' => 10,
|
||||||
'css_id' => null,
|
'css_id' => null,
|
||||||
'strict_accessibility' => true,
|
'strict_accessibility' => true,
|
||||||
'log_level' => 2
|
'log_level' => 2,
|
||||||
|
'tick_delete_hours' => 3
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'username' => 'testuser',
|
'username' => 'testuser',
|
||||||
|
@ -62,7 +62,7 @@ class TickControllerTest extends TestCase
|
|||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$controller = new TickController();
|
$controller = new TickController();
|
||||||
$controller->index(123);
|
$controller->index("123");
|
||||||
|
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
@ -96,12 +96,12 @@ class TickControllerTest extends TestCase
|
|||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$controller = new TickController();
|
$controller = new TickController();
|
||||||
$controller->index(999);
|
$controller->index("999");
|
||||||
|
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
// Should return 404 error
|
// Should return 404 error
|
||||||
$this->assertStringContainsString('404 - Tick Not Found', $output);
|
$this->assertStringContainsString('Tick Not Found', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexWithEmptyTickData(): void
|
public function testIndexWithEmptyTickData(): void
|
||||||
@ -125,12 +125,12 @@ class TickControllerTest extends TestCase
|
|||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$controller = new TickController();
|
$controller = new TickController();
|
||||||
$controller->index(456);
|
$controller->index("456");
|
||||||
|
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
// Should return 404 error for empty data
|
// Should return 404 error for empty data
|
||||||
$this->assertStringContainsString('404 - Tick Not Found', $output);
|
$this->assertStringContainsString('Tick Not Found', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexWithDatabaseException(): void
|
public function testIndexWithDatabaseException(): void
|
||||||
@ -145,7 +145,7 @@ class TickControllerTest extends TestCase
|
|||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$controller = new TickController();
|
$controller = new TickController();
|
||||||
$controller->index(123);
|
$controller->index("123");
|
||||||
|
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class AtomGeneratorTest extends TestCase
|
|||||||
$this->assertStringContainsString('<title>Test Site Atom Feed</title>', $xml);
|
$this->assertStringContainsString('<title>Test Site Atom Feed</title>', $xml);
|
||||||
$this->assertStringContainsString('<link rel="alternate" href="https://example.com/tkr/"/>', $xml);
|
$this->assertStringContainsString('<link rel="alternate" href="https://example.com/tkr/"/>', $xml);
|
||||||
$this->assertStringContainsString('<link rel="self"', $xml);
|
$this->assertStringContainsString('<link rel="self"', $xml);
|
||||||
$this->assertStringContainsString('href="https://example.com/tkr/feed/atom"', $xml);
|
$this->assertStringContainsString('href="https://example.com/tkr/atom"', $xml);
|
||||||
$this->assertStringContainsString('<id>https://example.com/tkr/</id>', $xml);
|
$this->assertStringContainsString('<id>https://example.com/tkr/</id>', $xml);
|
||||||
$this->assertStringContainsString('<author>', $xml);
|
$this->assertStringContainsString('<author>', $xml);
|
||||||
$this->assertStringContainsString('<name>Test Site</name>', $xml);
|
$this->assertStringContainsString('<name>Test Site</name>', $xml);
|
||||||
@ -69,7 +69,7 @@ class AtomGeneratorTest extends TestCase
|
|||||||
$this->assertStringContainsString('<title>Test Site Atom Feed</title>', $xml);
|
$this->assertStringContainsString('<title>Test Site Atom Feed</title>', $xml);
|
||||||
$this->assertStringContainsString('<link rel="alternate" href="https://example.com/tkr/"/>', $xml);
|
$this->assertStringContainsString('<link rel="alternate" href="https://example.com/tkr/"/>', $xml);
|
||||||
$this->assertStringContainsString('<link rel="self"', $xml);
|
$this->assertStringContainsString('<link rel="self"', $xml);
|
||||||
$this->assertStringContainsString('href="https://example.com/tkr/feed/atom"', $xml);
|
$this->assertStringContainsString('href="https://example.com/tkr/atom"', $xml);
|
||||||
$this->assertStringContainsString('<id>https://example.com/tkr/</id>', $xml);
|
$this->assertStringContainsString('<id>https://example.com/tkr/</id>', $xml);
|
||||||
$this->assertStringContainsString('<author>', $xml);
|
$this->assertStringContainsString('<author>', $xml);
|
||||||
$this->assertStringContainsString('<name>Test Site</name>', $xml);
|
$this->assertStringContainsString('<name>Test Site</name>', $xml);
|
||||||
|
@ -34,7 +34,7 @@ class RssGeneratorTest extends TestCase
|
|||||||
$this->assertStringContainsString('<rss version="2.0"', $xml);
|
$this->assertStringContainsString('<rss version="2.0"', $xml);
|
||||||
$this->assertStringContainsString('<title>Test Site RSS Feed</title>', $xml);
|
$this->assertStringContainsString('<title>Test Site RSS Feed</title>', $xml);
|
||||||
$this->assertStringContainsString('<link>https://example.com/tkr/</link>', $xml);
|
$this->assertStringContainsString('<link>https://example.com/tkr/</link>', $xml);
|
||||||
$this->assertStringContainsString('<atom:link href="https://example.com/tkr/feed/rss"', $xml);
|
$this->assertStringContainsString('<atom:link href="https://example.com/tkr/rss"', $xml);
|
||||||
$this->assertStringContainsString('<channel>', $xml);
|
$this->assertStringContainsString('<channel>', $xml);
|
||||||
$this->assertStringContainsString('<item>', $xml);
|
$this->assertStringContainsString('<item>', $xml);
|
||||||
$this->assertStringContainsString('</item>', $xml);
|
$this->assertStringContainsString('</item>', $xml);
|
||||||
@ -66,7 +66,7 @@ class RssGeneratorTest extends TestCase
|
|||||||
$this->assertStringContainsString('<rss version="2.0"', $xml);
|
$this->assertStringContainsString('<rss version="2.0"', $xml);
|
||||||
$this->assertStringContainsString('<title>Test Site RSS Feed</title>', $xml);
|
$this->assertStringContainsString('<title>Test Site RSS Feed</title>', $xml);
|
||||||
$this->assertStringContainsString('<link>https://example.com/tkr/</link>', $xml);
|
$this->assertStringContainsString('<link>https://example.com/tkr/</link>', $xml);
|
||||||
$this->assertStringContainsString('<atom:link href="https://example.com/tkr/feed/rss"', $xml);
|
$this->assertStringContainsString('<atom:link href="https://example.com/tkr/rss"', $xml);
|
||||||
$this->assertStringContainsString('<channel>', $xml);
|
$this->assertStringContainsString('<channel>', $xml);
|
||||||
$this->assertStringContainsString('</channel>', $xml);
|
$this->assertStringContainsString('</channel>', $xml);
|
||||||
$this->assertStringEndsWith('</rss>' . "\n", $xml);
|
$this->assertStringEndsWith('</rss>' . "\n", $xml);
|
||||||
|
@ -177,6 +177,7 @@ try {
|
|||||||
// Create/update settings
|
// Create/update settings
|
||||||
$settingsModel = new SettingsModel($db);
|
$settingsModel = new SettingsModel($db);
|
||||||
$settingsModel->siteTitle = $siteTitle;
|
$settingsModel->siteTitle = $siteTitle;
|
||||||
|
$settingsModel->siteDescription = $siteTitle;
|
||||||
$settingsModel->baseUrl = $baseUrl;
|
$settingsModel->baseUrl = $baseUrl;
|
||||||
$settingsModel->basePath = $basePath;
|
$settingsModel->basePath = $basePath;
|
||||||
$settings = $settingsModel->save();
|
$settings = $settingsModel->save();
|
||||||
@ -184,7 +185,7 @@ try {
|
|||||||
// Create admin user
|
// Create admin user
|
||||||
$userModel = new UserModel($db);
|
$userModel = new UserModel($db);
|
||||||
$userModel->username = $adminUsername;
|
$userModel->username = $adminUsername;
|
||||||
$userModel->display_name = $adminUsername;
|
$userModel->displayName = $adminUsername;
|
||||||
$userModel->website = '';
|
$userModel->website = '';
|
||||||
$userModel->mood = '';
|
$userModel->mood = '';
|
||||||
$user = $userModel->save();
|
$user = $userModel->save();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user