Compare commits

...

11 Commits

28 changed files with 875 additions and 2361 deletions

15
.gitignore vendored
View File

@@ -4,4 +4,17 @@
# don't commit all the dependencies fetched via Composer
/vendor/
.idea/
./.idea
./.idea/*
./.crush
/AGENTS.md
/composer.lock
/.idea/copilot.data.migration.agent.xml
/.idea/copilot.data.migration.ask.xml
/.idea/copilot.data.migration.ask2agent.xml
/.idea/copilot.data.migration.edit.xml
/.idea/php.xml
/.idea/phpunit.xml
/PROFILES_README.md
/.idea/vcs.xml
/.idea/workspace.xml

52
.htaccess Normal file
View File

@@ -0,0 +1,52 @@
# This file is - if you set up HUGE correctly - not needed.
# But, for fallback reasons (if you don't route your vhost to /public), it will stay here.
RewriteEngine on
RewriteRule ^(.*) public/$1 [L]
# Everything from is for browser caching and is totally optional
# Deflate Compression by FileType
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/atom_xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/x-shockwave-flash
</IfModule>
# Set browser caching to 1 month
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 month"
ExpiresByType text/javascript "access plus 1 month"
ExpiresByType text/html "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 month"
</IfModule>
<ifmodule mod_headers.c>
# if you want to prevent your site from being embedded into other sites via an iframe (sometimes used for scam), then
# simply uncomment these lines below. you need to have apache rewrite headers activated, usually via
# "a2enmod rewrite headers" on the command line
#Header set X-Frame-Options Deny
#Header always append X-Frame-Options SAMEORIGIN
<filesmatch "\\.(ico|jpe?g|png|gif|swf)$">
Header set Cache-Control "max-age=2592000, public"
</filesmatch>
<filesmatch "\\.(css)$">
Header set Cache-Control "max-age=604800, public"
</filesmatch>
<filesmatch "\\.(js)$">
Header set Cache-Control "max-age=216000, private"
</filesmatch>
</ifmodule>

9
.scrutinizer.yml Normal file
View File

@@ -0,0 +1,9 @@
# This file just tells the wonderful code quality analyzer Scrutinizer (https://scrutinizer-ci.com/g/panique/huge/)
# that we are using external services (Travis) to generate code coverage stats
checks:
php:
code_rating: true
duplication: true
# test coverage is commented out as this does not seem to work properly right now
#tools:
# external_code_coverage: true

38
.travis.yml Normal file
View File

@@ -0,0 +1,38 @@
language: php
php:
- 5.6
- 7.0
- 7.1
- 7.2
- 7.3
- 7.4
# optional, define the environment, in this case useful to test stuff that only runs in development / production
env:
global:
- APPLICATION_ENV=testing
before_install:
- sudo apt-get update > /dev/null
before_script:
- sudo apt-get install apache2
- sudo a2enmod rewrite
# configure apache virtual hosts, create vhost via travis-ci-apache file template
- sudo cp -f travis-ci-apache /etc/apache2/sites-available/default
- sudo sed -e "s?%TRAVIS_BUILD_DIR%?$(pwd)?g" --in-place /etc/apache2/sites-available/default
- sudo service apache2 restart
# composer
- composer self-update
- composer install --prefer-source --no-interaction
# go to tests folder
- cd tests
# run unit tests, create result file
script: ../vendor/bin/phpunit --configuration phpunit.xml --coverage-text --coverage-clover=coverage.clover
# gets tools from Scrutinizer, uploads unit tests results to Scrutinizer (?)
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover

786
README.md
View File

@@ -1,632 +1,220 @@
![HUGE, formerly "php-login" logo](_pictures/huge.png)
# HUGE Installation und Setup
# HUGE
## Schnellstart
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/panique/huge/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/panique/huge/?branch=master)
[![Code Climate](https://codeclimate.com/github/panique/huge/badges/gpa.svg)](https://codeclimate.com/github/panique/huge)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/01a221d168b04b1c94a85813519dab40)](https://www.codacy.com/app/panique/huge?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=panique/huge&amp;utm_campaign=Badge_Grade)
[![Travis CI](https://travis-ci.org/panique/huge.svg?branch=master)](https://travis-ci.org/panique/huge)
[![Dependency Status](https://www.versioneye.com/user/projects/54ca11fbde7924f81a000010/badge.svg?style=flat)](https://www.versioneye.com/user/projects/54ca11fbde7924f81a000010)
[![Support](https://supporterhq.com/api/b/9guz00i6rep05k1mwxyquz30k)](https://supporterhq.com/give/9guz00i6rep05k1mwxyquz30k)
- Voraussetzungen prüfen (siehe unten)
- Abhängigkeiten installieren
- Datenbank anlegen
- Webserver auf `public/` zeigen lassen
- Starten und testen
Just a simple user authentication solution inside a super-simple framework skeleton that works out-of-the-box
(and comes with an auto-installer), using the future-proof official bcrypt password hashing/salting implementation of
PHP 5.5+, plus some nice features that will speed up the time from idea to first usable prototype application
dramatically. Nothing more. This project has its focus on hardcore simplicity. Everything is as simple as possible,
made for smaller projects, typical agency work and quick drafts. If you want to build massive corporate
applications with all the features modern frameworks have, then have a look at [Laravel](http://laravel.com),
[Symfony](http://symfony.com) or [Yii](http://www.yiiframework.com), but if you just want to quickly create something
that just works, then this script might be interesting for you.
Befehle (Beispiele):
HUGE's simple-as-possible architecture was inspired by several conference talks, slides and articles about huge
applications that - surprisingly and intentionally - go back to the basics of programming, using procedural programming,
static classes, extremely simple constructs, not-totally-DRY code etc. while keeping the code extremely readable
([StackOverflow](http://www.dev-metal.com/architecture-stackoverflow/), Wikipedia, SoundCloud).
Some interesting Buzzwords in this context: [KISS](http://en.wikipedia.org/wiki/KISS_principle),
[YAGNI](http://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it), [Feature Creep](https://en.wikipedia.org/wiki/Feature_creep),
[Minimum viable product](https://en.wikipedia.org/wiki/Minimum_viable_product).
#### HUGE has reached "soft End Of Life"
To keep this project stable, secure, clean and minimal I've decided to reduce the development of HUGE to a
minimum. *Don't worry, this is actually a good thing:* New features usually mean new bugs, lots of testing, fixes,
incompatibilities, and for some people even hardcore update stress. As HUGE is a security-critical script new features
are not as important as a stable and secure core, this is why people use it. This means:
- HUGE will not get new features
- but will be maintained, so it will get bugfixes, corrections etc for sure, maybe for years
And to be honest, maintaining a framework for free in my rare free-time is also not what I want to do permanently. :)
Finally a little note: The PHP world has evolved dramatically, we have excellent frameworks with awesome features and
big professional teams behind, very well written documentations and large communities, so there's simply no reason
to put much work into another framework. Instead, please commit to the popular frameworks, then your work will have
much more impact and is used by much more people!
Thanks to everybody around this project, have a wonderful time!
XOXO,
Chris
#### Releases & development
* stable [v3.1](https://github.com/panique/huge/releases/tag/v3.1),
* public beta branch: [master](https://github.com/panique/huge)
* public in-development branch (please commit new code here): [develop](https://github.com/panique/huge/tree/develop)
#### Quick-Index
+ [Features](#features)
+ [Live-Demo](#live-demo)
+ [Support](#support)
+ [Follow the project](#follow)
+ [License](#license)
+ [Requirements](#requirements)
+ [Auto-Installation](#auto-installation)
- [Auto-Installation in Vagrant](#auto-installation-vagrant) (also useful for 100% reproducible installation of HUGE)
- [Auto-Installation in Ubuntu 14.04 LTS server](#auto-installation-ubuntu)
+ [Installation (Ubuntu 14.04 LTS)](#installation)
- [Quick Installation](#quick-installation)
- [Detailed Installation](#detailed-installation)
- [NGINX setup](#nginx-setup)
- [IIS setup](#iis-setup)
+ [Documentation](#documentation)
- [How to use the user roles](#user_roles)
- [How to use the CSRF feature](#csrf)
+ [Community-provided features & feature discussions](#community)
+ [Future of the project, announcing soft EOL](#future)
+ [Why is there no support forum anymore ?](#why-no-support-forum)
+ [Zero tolerance for idiots, trolls and vandals](#zero-tolerance)
+ [Contribute](#contribute)
+ [Code-Quality scanner links](#code-quality)
+ [Report a bug](#bug-report)
### The History of HUGE
Back in 2010/2011 there were no useful login solutions in the PHP world, at least not for non-experts. So I did the worst
mistake every young developer does: Trying to build something by myself without having any clue about security basics.
What made it even worse was: The web was (and is) full of totally broken tutorials about building user authentication
systems, even the biggest companies in the world did this completely wrong (we are talking about SONY, LinkedIn and
Adobe here), and also lots of major framework in all big programming languages (!) used totally outdated and insecure
password saving technologies.
However, in 2012 security expert [Anthony Ferrara](https://github.com/ircmaxell) published a [little PHP library](https://github.com/ircmaxell/password_compat),
allowing extremely secure, modern and correct hashing of passwords in PHP 5.3 and 5.4, usable by every developer without any stress and without any knowledge
about security internals. The script was so awesome that it was written into the core of PHP 5.5, it's the de-facto standard these days.
When this came out I tried to use this naked library to build a fully working out-of-the-box login system for several private and commercial projects,
and put the code on GitHub. Lots of people found this useful, contributed and bugfixed the project, made forks, smaller and larger versions.
The result is this project.
Please note: Now, in 2015, most major frameworks have excellent user authentication logic embedded by default. This was
not the case years ago. So, from today's perspective it might be smarter to chose Laravel, Yii or Symfony for serious
projects. But feel free to try out HUGE, the auto-installer will spin up a fully working installation within minutes and
without any configuration.
And why the name "HUGE" ? It's a nice combination to
[TINY](https://github.com/panique/tiny),
[MINI](https://github.com/panique/mini) and
[MINI2](https://github.com/panique/mini2),
[MINI3](https://github.com/panique/mini3),
which are some of my other older projects. Super-minimal micro frameworks for extremely fast and simple development of simple websites.
### Features <a name="features"></a>
* built with the official PHP password hashing functions, fitting the most modern password hashing/salting web standards
* proper security features, like CSRF blocking (via form tokens), encryption of cookie contents etc.
* users can register, login, logout (with username, email, password)
* password-forget / reset
* remember-me (login via cookie)
* account verification via mail
* captcha
* failed-login-throttling
* user profiles
* account upgrade / downgrade
* simple user types (type 1, type 2, admin)
* supports local avatars and remote Gravatars
* supports native mail and SMTP sending (via PHPMailer and other tools)
* uses PDO for database access for sure, has nice DatabaseFactory (in case your project goes big)
* uses URL rewriting ("beautiful URLs")
* proper split of application and public files (requests only go into /public)
* uses Composer to load external dependencies (PHPMailer, Captcha-Generator, etc.) for sure
* fits PSR-0/1/2/4 coding guidelines
* uses [Post-Redirect-Get pattern](https://en.wikipedia.org/wiki/Post/Redirect/Get) for nice application flow
* masses of comments
* is actively maintained and bug-fixed (however, no big new features as project slowly reaches End of Life)
### Planned features
* A real documentation (currently there's none, but the code is well commented)
### Live-Demo <a name="live-demo"></a>
See a [live demo of older 3.0 version here](http://104.131.8.128) and [the server's phpinfo() here](104.131.8.128/info.php).
### Support the project <a name="support"></a>
There is a lot of work behind this project. I might save you hundreds, maybe thousands of hours of work (calculate that
in developer costs). So when you are earning money by using HUGE, be fair and give something back to open-source.
HUGE is totally free to private and commercial use.
Support the project by renting a server at [DigitalOcean](https://www.digitalocean.com/?refcode=40d978532a20) or just tipping a coffee at BuyMeACoffee.com. Thanks! :)
<a href="https://www.buymeacoffee.com/panique" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
Also feel free to contribute to this project.
### License <a name="license"></a>
Licensed under [MIT](http://www.opensource.org/licenses/mit-license.php).
Totally free for private or commercial projects.
### Requirements <a name="requirements"></a>
Make sure you know the basics of object-oriented programming and MVC, are able to use the command line and have
used Composer before. This script is not for beginners.
* **PHP 5.5+**
* **MySQL 5** database (better use versions 5.5+ as very old versions have a [PDO injection bug](http://stackoverflow.com/q/134099/1114320)
* installed PHP extensions: pdo, gd, openssl (the install guideline shows how to do)
* installed tools on your server: git, curl, composer (the install guideline shows how to do)
* for professional mail sending: an SMTP account (I use [SMTP2GO](http://www.smtp2go.com/?s=devmetal))
* activated mod_rewrite on your server (the install guideline shows how to do)
### Auto-Installations <a name="auto-installation"></a>
Yo, fully automatic. Why ? Because I always hated it to spend days trying to find out how to install a thing.
This will save you masses of time and nerves. Donate a coffee if you like it.
#### Auto-Installation (in Vagrant) <a name="auto-installation-vagrant"></a>
If you are using Vagrant for your development, then simply
1. Add the official Ubuntu 14.04 LTS box to your Vagrant: `vagrant box add ubuntu/trusty64`
2. Move *Vagrantfile* and *bootstrap.sh* (from *_one-click-installation* folder) to a folder where you want to initialize your project.
3. Do `vagrant up` in that folder.
5 minutes later you'll have a fully installed HUGE inside Ubuntu 14.04 LTS. The full code will be auto-synced with
the current folder. MySQL root password and the PHPMyAdmin root password are set to *12345678*. By default
192.168.33.111 is the IP of your new box.
#### Auto-Installation in a naked Ubuntu 14.04 LTS server <a name="auto-installation-ubuntu"></a>
Extremely simple installation in a fresh and naked typical Ubuntu 14.04 LTS server:
Download the installer script
```bash
wget https://raw.githubusercontent.com/panique/huge/master/_one-click-installation/bootstrap.sh
# Composer installieren (macOS über Homebrew)
brew install composer
# Abhängigkeiten holen (im Projektordner)
composer install
# Datenbank & Tabellen anlegen
mysql -u root -p < application/_installation/01-create-database.sql
mysql -u root -p < application/_installation/02-create-table-users.sql
mysql -u root -p < application/_installation/03-create-table-notes.sql
# Rechte für Avatare (je nach OS anpassen)
# Ubuntu/Debian:
sudo chown -R www-data:www-data public/avatars
sudo chmod -R 775 public/avatars
# macOS (Apache Standardnutzer _www):
sudo chown -R _www:_www public/avatars
sudo chmod -R 775 public/avatars
```
Make it executable
---
## Voraussetzungen
- PHP >= 5.5 (laut composer.json)
- Composer
- Apache 2.4 mit `mod_rewrite`
- MySQL/MariaDB
- PHP-Erweiterungen: PDO + pdo_mysql, OpenSSL, mbstring
- Schreibrechte für `public/avatars/`
---
## Projekt beziehen
- Repository klonen oder entpacken
- In den Projektordner wechseln
---
## Abhängigkeiten installieren (Composer)
- Im Projektordner ausführen:
```bash
chmod +x bootstrap.sh
composer install
```
Run it! Give it some minutes to perform all the tasks. And yes, you can thank me later :)
```bash
sudo ./bootstrap.sh
```
### Installation <a name="installation"></a>
---
#### Quick guide: <a name="quick-installation"></a>
## Webserver konfigurieren (Apache)
0. Make sure you have Apache, PHP, MySQL installed. [Tutorial](http://www.dev-metal.com/installsetup-basic-lamp-stack-linux-apache-mysql-php-ubuntu-14-04-lts/).
1. Clone the repo to a folder on your server
2. Activate mod_rewrite, route all traffic to application's /public folder. [Tutorial](http://www.dev-metal.com/enable-mod_rewrite-ubuntu-14-04-lts/).
3. Edit application/config: Set your database credentials
4. Execute SQL statements from application/_installation to setup database tables
5. [Install Composer](http://www.dev-metal.com/install-update-composer-windows-7-ubuntu-debian-centos/),
run `Composer install` on application's root folder to install dependencies
6. Make avatar folder (application/public/avatars) writable
7. For proper email usage: Set SMTP credentials in config file, set EMAIL_USE_SMTP to true
- DocumentRoot auf `.../huge/public` setzen
- `AllowOverride All` aktivieren (damit `.htaccess` greift)
- `mod_rewrite` aktivieren
- Apache neu starten
"Email does not work" ? See the troubleshooting below. TODO
Minimalbeispiel VirtualHost:
#### Detailed guide (Ubuntu 14.04 LTS): <a name="detailed-installation"></a>
This is just a quick guideline for easy setup of a development environment!
Make sure you have Apache, PHP 5.5+ and MySQL installed. [Tutorial here](http://www.dev-metal.com/installsetup-basic-lamp-stack-linux-apache-mysql-php-ubuntu-14-04-lts/).
Nginx will work for sure too, but no install guidelines are available yet.
Edit vhost to make clean URLs possible and route all traffic to /public folder of your project:
```bash
sudo nano /etc/apache2/sites-available/000-default.conf
```
and make the file look like
```
```apacheconf
<VirtualHost *:80>
DocumentRoot "/var/www/html/public"
<Directory "/var/www/html/public">
ServerName huge.local
DocumentRoot "/pfad/zu/huge/public"
<Directory "/pfad/zu/huge/public">
AllowOverride All
Require all granted
</Directory>
SetEnv APPLICATION_ENV development
ErrorLog "${APACHE_LOG_DIR}/huge_error.log"
CustomLog "${APACHE_LOG_DIR}/huge_access.log" combined
</VirtualHost>
```
Enable mod_rewrite and restart apache.
Hinweis: `.htaccess` leitet auf `public/index.php` um.
---
## Datenbank anlegen
- SQL-Skripte in dieser Reihenfolge ausführen (`application/_installation/`):
- `01-create-database.sql`
- `02-create-table-users.sql`
- `03-create-table-notes.sql`
Beispiel in der Shell:
```bash
sudo a2enmod rewrite
service apache2 restart
mysql -u root -p < application/_installation/01-create-database.sql
mysql -u root -p < application/_installation/02-create-table-users.sql
mysql -u root -p < application/_installation/03-create-table-notes.sql
```
Install curl (needed to use git), openssl (needed to clone from GitHub, as github is https only),
PHP GD, the graphic lib (we create captchas and avatars), and git.
---
## Framework-Konfiguration (Entwicklung)
- Datei: `application/config/config.development.php`
- Wichtige Schlüssel:
- URL
- `URL` (Basis-URL, endet mit `/`; auto-detect möglich)
- Pfade
- `PATH_CONTROLLER`, `PATH_VIEW` (meist unverändert)
- Routing
- `DEFAULT_CONTROLLER`, `DEFAULT_ACTION`
- Datenbank
- `DB_TYPE`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS`, `DB_CHARSET`
- Captcha
- `CAPTCHA_WIDTH`, `CAPTCHA_HEIGHT`
- Cookies & Session
- `COOKIE_RUNTIME`, `COOKIE_PATH`, `COOKIE_DOMAIN`, `COOKIE_SECURE`, `COOKIE_HTTP`
- `SESSION_RUNTIME`
- Avatare/Gravatar
- `USE_GRAVATAR`, `GRAVATAR_DEFAULT_IMAGESET`, `GRAVATAR_RATING`
- `AVATAR_SIZE`, `AVATAR_JPEG_QUALITY`, `AVATAR_DEFAULT_IMAGE`
- Ordner `public/avatars/` beschreibbar machen
- Sicherheit
- `ENCRYPTION_KEY`, `HMAC_SALT` (für eigene Installation ändern)
- E-Mail
- `EMAIL_USED_MAILER`, `EMAIL_USE_SMTP`
- `EMAIL_SMTP_HOST`, `EMAIL_SMTP_AUTH`, `EMAIL_SMTP_USERNAME`, `EMAIL_SMTP_PASSWORD`, `EMAIL_SMTP_PORT`, `EMAIL_SMTP_ENCRYPTION`
- `EMAIL_PASSWORD_RESET_*`, `EMAIL_VERIFICATION_*`
---
## Umgebungen (Environment)
- Klasse: `application/core/Environment.php`
- Ermittelt `APPLICATION_ENV` (Fallback: `development`)
- Weitere Datei möglich: `config.production.php`
- Apache-Variable setzen:
```apacheconf
SetEnv APPLICATION_ENV production
```
---
## Verzeichnisrechte
- `public/avatars/` beschreibbar
- Optional Logs/Uploads je nach Bedarf
---
## Starten
- Browser: `http://<host>/`
- Registrierung/Login testen
- Mailversand nur mit korrekt konfiguriertem SMTP
---
## Tests ausführen
- PHPUnit unter `vendor/bin/phpunit`
- Konfiguration: `tests/phpunit.xml`
```bash
sudo apt-get -y install curl
sudo apt-get -y install php5-curl
sudo apt-get -y install openssl
sudo apt-get -y install php5-gd
sudo apt-get -y install git
vendor/bin/phpunit -c tests/phpunit.xml
```
git clone HUGE
---
## Häufige Probleme
- 404 bei allen Routen
- `mod_rewrite` aktivieren
- `AllowOverride All` setzen
- DocumentRoot auf `public/`
- Falsche Links/Assets
- `URL` in der Config prüfen
- Datenbankfehler
- `DB_*`-Werte und Rechte prüfen
- E-Mails kommen nicht an
- `EMAIL_USE_SMTP=true` und SMTP-Daten prüfen
- Avatare fehlen
- Schreibrechte für `public/avatars/`
---
## Option: Vagrant „One-Click-Installation“
- Ordner: `_one-click-installation/`
- Voraussetzungen: Vagrant + VirtualBox
```bash
sudo git clone https://github.com/panique/huge "/var/www/html"
cd _one-click-installation
vagrant up
```
Install Composer
```bash
curl -s https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
```
- Projekt über die VM nutzen (Host/Ports laut Vagrant-Ausgabe)
Go to project folder, load Composer packages (--dev is optional, you know the deal)
```bash
cd /var/www/html
composer install --dev
```
---
Execute the SQL statements. Via phpmyadmin or via the command line for example. 12345678 is the example password.
Note that this is written without a space.
```bash
sudo mysql -h "localhost" -u "root" "-p12345678" < "/var/www/html/application/_installation/01-create-database.sql"
sudo mysql -h "localhost" -u "root" "-p12345678" < "/var/www/html/application/_installation/02-create-table-users.sql"
sudo mysql -h "localhost" -u "root" "-p12345678" < "/var/www/html/application/_installation/03-create-table-notes.sql"
```
## Struktur (Kurz)
Make avatar folder writable (make sure it's the correct path!)
```bash
sudo chown -R www-data "/var/www/html/public/avatars"
```
If this doesn't work for you, then you might try the hard way by setting alternatively
```bash
sudo chmod 0777 -R "/var/www/html/public/avatars"
```
Remove Apache's default demo file
```bash
sudo rm "/var/www/html/index.html"
```
Edit the application's config in application/config/config.development.php and put in your database credentials.
Last part (not needed for a first test): Set your SMTP credentials in the same file and set EMAIL_USE_SMTP to true, so
you can send proper emails. It's highly recommended to use SMTP for mail sending! Native sending via PHP's mail() will
not work in nearly every case (spam blocking). I use [SMTP2GO](http://www.smtp2go.com/?s=devmetal).
Then check your server's IP / domain. Everything should work fine.
#### NGINX setup: <a name="nginx-setup"></a>
This is an untested NGINX setup. Please comment [on the ticket](https://github.com/panique/huge/issues/622) if you see
issues.
```
server {
# your listening port
listen 80;
# your server name
server_name example.com;
# your path to access log files
access_log /srv/www/example.com/logs/access.log;
error_log /srv/www/example.com/logs/error.log;
# your root
root /srv/www/example.com/public_html;
# huge
index index.php;
# huge
location / {
try_files $uri /index.php?url=$uri&$args;
}
# your PHP config
location ~ \.php$ {
try_files $uri = 401;
include /etc/nginx/fastcgi_params;
fastcgi_pass unix:/var/run/php-fastcgi/php-fastcgi.socket;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
```
#### IIS setup: <a name="iis-setup"></a>
Big thanks to razuro for this fine setup: Put this inside your root folder, but don't put any web.config in your public
folder.
```
<?xml version="1.0" encoding="UTF-8"?><configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Imported Rule 1" stopProcessing="true">
<match url="^(.*)$" ignoreCase="false" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
</conditions>
<action type="Rewrite" url="public/index.php?url={R:1}" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
```
Find the original [ticket here](https://github.com/panique/huge/issues/788).
#### Testing with demo users
By default there are two demo users, a normal user and an admin user. For more info on that please have a look on the
user role part of the small documentation block inside this readme.
Normal user: Username is `demo2`, password is `12345678`. The user is already activated.
Admin user (can delete and suspend other users): Username is `demo`, password is `12345678`. The user is already activated.
### What the hell are .travis.yml, .scrutinizer.yml etc. ?
There are several files in the root folder of the project that might be irritating:
- *.htaccess* (optionally) routes all traffic to /public/index.php! If you installed this project correctly, then this
file is not necessary, but as lots of people have problems setting up the vhost correctly, .htaccess it still there
to increase security, even on partly-broken-installations.
- *.scrutinizer.yml* (can be deleted): Configs for the external code quality analyzer Scrutinizer, just used here on
GitHub, you don't need this for your project.
- *.travis.yml* (can be deleted): Same like above. Travis is an external service that creates installations of this
repo after each code change to make sure everything runs fine. Also runs the unit tests. You don't need this inside
your project.
- *composer.json* (important): You should know what this does. ;) This file says what external dependencies are used.
- *travis-ci-apache* (can be deleted): Config file for Travis, see above, so Travis knows how to setup the Apache.
*README* and *CHANGELOG* are self-explaining.
### Documentation <a name="documentation"></a>
A real documentation is in the making. Until then, please have a look at the code and use your IDE's code completion
features to get an idea how things work, it's quite obvious when you look at the controller files, the model files and
how data is shown in the view files. A big sorry that there's no documentation yet, but time is rare and we are all
doing this for free in our free time :)
- TODO: Full documentation
- TODO: Basic examples on how to do things
#### How to use the different user roles <a name="user_roles"></a>
Currently there are two types of users: Normal users and admins. There are exactly the same, but...
1. Admin users can delete and suspend other users, they have an additional button "admin" in the navigation. Admin users
have a value of `7` inside the database table field `user_account_type`. They cannot upgrade or downgrade their accounts
(as this wouldn't make sense).
2. Normal users don't have admin features for sure. But they can upgrade and downgrade their accounts (try it out via
/user/changeUserRole), which is basically a super-simple implementation of the basic-user / premium-user concept.
Normal users have a value of `1` or `2` inside the database table field `user_account_type`. By default all new
registered users are normal users with user role 1 for sure.
See the "Testing with demo users" section of this readme for more info.
There's also a very interesting [pull request adding user roles and user permissions](https://github.com/panique/huge/pull/691),
which is not integrated into the project as it's too advanced and complex. But, this might be exactly what you need,
feel free to try.
#### How to use the CSRF feature <a name="csrf"></a>
To prevent [CSRF attacks](https://en.wikipedia.org/wiki/Cross-site_request_forgery), HUGE does this in the most common
way, by using a security *token* when the user submits critical forms. This means: When PHP renders a form for the user,
the application puts a "random string" inside the form (as a hidden input field), generated via Csrf::makeToken()
(application/core/Csrf.php), which also saves this token to the session. When the form is submitted, the application
checks if the POST request contains exactly the form token that is inside the session.
This CSRF prevention feature is currently implemented on the login form process (see *application/view/login/index.php*)
and user name change form process (see *application/view/user/editUsername.php*), most other forms are not security-
critical and should stay as simple as possible.
So, to do this with a normal form, simply: At your form, before the submit button put:
`<input type="hidden" name="csrf_token" value="<?= Csrf::makeToken(); ?>" />`
Then, in the controller action validate the CSRF token submitted with the form by doing:
```
// check if csrf token is valid
if (!Csrf::isTokenValid()) {
LoginModel::logout();
Redirect::home();
exit();
}
```
A big thanks to OmarElGabry for implementing this!
#### Can a user be logged in from multiple devices ?
In theory: Yes, but this feature didn't work in my tests. As it's an external feature please have a look into the
[according ticket](https://github.com/panique/huge/pull/693) for more.
#### Troubleshooting & Glitches
* In 3.0 and 3.1 a user could log into the application from different devices / browsers / locations. This was intended
behaviour as this is standard in most web applications these days. In 3.2 still feature is "missing" by default, a
user will only be able to log in from one browser at the same time. This is a security improvement, but for sure not
optimal for many developers. The plan is to implement a config switch that will allow / disallow logins from multiple
browsers.
* Using this on a sub-domain ? You might get problems with the cookies in IE11. Fix this by replacing "/" with "./" of
the cookie location COOKIE_PATH inside application/config/config.xxx.php!
Check [ticket #733](https://github.com/panique/huge/issues/733) for more info. Thanks to jahbiuabft for figuring this
out. Update: There's another ticket focusing on the same issue: [ticket #681](https://github.com/panique/huge/issues/681)
### Community-provided features & feature discussions <a name="community"></a>
There are some awesome features or feature ideas build by awesome people, but these features are too special-interest
to go into the main version of HUGE, but have a look into these tickets if you are interested:
- [Caching system](https://github.com/panique/huge/issues/643)
- [ReCaptcha as captcha](https://github.com/panique/huge/issues/665)
- [Internationalization feature](https://github.com/panique/huge/issues/582)
- [Using controller A inside controller B](https://github.com/panique/huge/issues/706)
- [HTML mails](https://github.com/panique/huge/issues/738)
- [Deep user roles / user permission system](https://github.com/panique/huge/pull/691)
### Future of HUGE: Announcing "soft End Of Life" <a name="future"></a>
The idea of this project is and was to provide a super-simple barebone application with a full user authentication
system inside that just works fine and stable. Due to the highly security-related nature of this script any changes
mean a lot of work, lots of testing, catching edge cases etc., and in the end I spent 90% of the time testing and fixing
new features or new features break existing stuff, and doing this is really not what anybody wants to do for free in
the rare free-time :)
To keep the project stable, clean and maintainable, I would kindly announce the "soft-End of Life" for this project,
meaning:
A. HUGE will not get any new features in the future, but ...
B. bugfixes and corrections will be made, probably for years
### Coding guideline behind HUGE
While HUGE was in development, there were 3 main rules that helped me (and probably others) to write minimal, clean
and working code. Might be useful for you too:
1. Reduce features to the bare minimum.
2. Don't implement features that are not needed by most users.
3. Only build everything for the most common use case (like MySQL, not PostGre, NoSQL etc).
As noted in the intro of this README, there are also some powerful concepts that might help you when developing cool
stuff: [KISS](http://en.wikipedia.org/wiki/KISS_principle),
[YAGNI](http://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it), [Feature Creep](https://en.wikipedia.org/wiki/Feature_creep),
[Minimum viable product](https://en.wikipedia.org/wiki/Minimum_viable_product).
#### List of features / ideas provided in tickets / pull requests
To avoid unnecessary work for all of us I would kindly recommend everybody to use HUGE for simple project that only
need the features that already exist, and if you really need a RESTful architecture, migrations, routing, 2FA etc,
then it's easier, cleaner and faster to simply use Laravel, Symfony or Zend.
However, here are the community-suggested possible features, taken from lots of tickets. Feel free to implement them
into your forks of the project:
* OAuth2 implementation (let your users create accounts and login via 3rd party auth, like Facebook, Twitter, GitHub,
etc). As this is a lot of work and would make the project much more complicated it might make sense to do this in a
fork or totally skip it. (see [Ticket #528](https://github.com/panique/huge/issues/528))
* Router (map all URLs to according controller-methods inside one file), [Ticket 727](https://github.com/panique/huge/issues/727)
* RESTful architecture (see [ticket #488](https://github.com/panique/huge/issues/488) for discussion)
* Horizontal MySQL scaling (see [ticket #423](https://github.com/panique/huge/issues/423) for discussion)
* Modules / middleware
* Logging
* Two-Factor-Authentication (see [ticket #732](https://github.com/panique/huge/issues/732))
* Controller-less URLs (see [ticket #704](https://github.com/panique/huge/issues/704))
* Email-re-validation after email change (see [ticket #705](https://github.com/panique/huge/issues/705))
* Connect to multiple databases (see [ticket #702](https://github.com/panique/huge/issues/702))
* A deeper user role system (see [ticket #701](https://github.com/panique/huge/issues/701),
[pull-request #691](https://github.com/panique/huge/pull/691)),
[ticket #603](https://github.com/panique/huge/issues/603)
* How to run without using Composer [ticket #826](https://github.com/panique/huge/issues/826)
### Why is there no support forum (anymore) ? <a name="why-no-support-forum"></a>
There were two (!) support forums for v1 and v2 of this project (HUGE is v3), and both were vandalized by people who
didn't even read the readme and / or the install guidelines. Most asked question was "script does not work plz help"
without giving any useful information (like code or server setup or even the version used). While I'm writing these
lines somebody just asked via Twitter "how to install without Composer". You know what I mean :) - 99% of the questions
were not necessary if the people would had read the guidelines, do a minimal research on their own or would stop making
things so unnecessarily complicated. And even when writing detailed answers most of them still messed it up, resulting
in rants and complaints (for free support for a free software!). It was just frustrating to deal with this every day,
especially when people take it for totally granted that *it's the duty* of open-source developers to give detailed,
free and personal support for every "plz help"-request.
So I decided to completely stop any free support. For serious questions about real problems inside the script please
use the GitHub issues feature.
### Zero tolerance for idiots, trolls and vandals! <a name="zero-tolerance"></a>
Harsh words, but as basically every public internet project gets harassed, vandalized and trolled these days by very
strange people it's necessary: Some simple rules.
1. Respect that this is just a simple script written by unpaid volunteers in their free-time.
This is NOT business-software you've bought for $10.000.
There's no reason to complain (!) about free open-source software. The attitude against free software
is really frustrating these days, people take everything for granted without realizing the work behind it, and the
fact that they get serious software totally for free, saving thousands of dollars. If you don't like it, then don't
use it. If you want a feature, try to take part in the process, maybe even build it by yourself and add it to the
project! Be nice and respectful. Constructive criticism is for sure always welcome!
2. Don't bash, don't hate, don't spam, don't vandalize. Please don't ask for personal free support, don't ask if
somebody could do your work for you. Before you ask something, make sure you've read the README, followed every
tutorial, double-checked the code and tried to solve the problem by yourself.
Trolls and very annoying people will get a permanent ban / block. GitHub has a very powerful anti-abuse team.
### Contribute <a name="contribute"></a>
Please commit only in *develop* branch. The *master* branch will always contain the stable version.
### Code-Quality scanner links <a name="code-quality"></a>
[Scrutinizer (master branch)](https://scrutinizer-ci.com/g/panique/huge/?branch=master),
[Scrutinizer (develop branch)](https://scrutinizer-ci.com/g/panique/huge/?branch=develop),
[Code Climate](https://codeclimate.com/github/panique/huge),
[Codacy](https://www.codacy.com/public/panique/phplogin/dashboard?bid=789836),
[SensioLabs Insight](https://insight.sensiolabs.com/projects/d4f4e3c0-1445-4245-8cb2-d75026c11fa7/analyses/2).
### Found a bug (Responsible Disclosure) ? <a name="bug-report"></a>
Due to the possible consequences when publishing a bug on a public open-source project I'd kindly ask you to send really
big bugs to my email address, not posting this here. If the bug is not interesting for attackers: Feel free to create
an normal GitHub issue.
### Current and further development
See active issues here:
https://github.com/panique/huge/issues?state=open
### Why you should use a favicon.ico in your project :)
Interesting issue: When a user hits your website, the user's browser will also request one or more (!) favicons
(different sizes). If these static files don't exist, your application will start to generate a 404 response and a 404
page for each file. This wastes a lot of server power and is also useless, therefore make sure you always have favicons
or handle this from Apache/nginx level.
HUGE tries to handle this by sending an empty image in the head of the view/_templates/header.php !
More inside this ticket: [Return proper 404 for missing favicon.ico, missing images etc.](https://github.com/panique/huge/issues/530)
More here on Stackflow: [How to prevent favicon.ico requests?](http://stackoverflow.com/questions/1321878/how-to-prevent-favicon-ico-requests),
[Isn't it silly that a tiny favicon requires yet another HTTP request? How to make favicon go into a sprite?](http://stackoverflow.com/questions/5199902/isnt-it-silly-that-a-tiny-favicon-requires-yet-another-http-request-how-to-mak?lq=1).
### Useful links
- [How long will my session last?](http://stackoverflow.com/questions/1516266/how-long-will-my-session-last/1516338#1516338)
- [How to do expire a PHP session after X minutes?](http://stackoverflow.com/questions/520237/how-do-i-expire-a-php-session-after-30-minutes/1270960#1270960)
- [How to use PDO](http://wiki.hashphp.org/PDO_Tutorial_for_MySQL_Developers)
- [A short guideline on how to use the PHP 5.5 password hashing functions and its PHP 5.3 & 5.4 implementations](http://www.dev-metal.com/use-php-5-5-password-hashing-functions/)
- [How to setup latest version of PHP 5.5 on Ubuntu 12.04 LTS](http://www.dev-metal.com/how-to-setup-latest-version-of-php-5-5-on-ubuntu-12-04-lts/)
- [How to setup latest version of PHP 5.5 on Debian Wheezy 7.0/7.1 (and how to fix the GPG key error)](http://www.dev-metal.com/setup-latest-version-php-5-5-debian-wheezy-7-07-1-fix-gpg-key-error/)
- [Notes on password & hashing salting in upcoming PHP versions (PHP 5.5.x & 5.6 etc.)](https://github.com/panique/huge/wiki/Notes-on-password-&-hashing-salting-in-upcoming-PHP-versions-%28PHP-5.5.x-&-5.6-etc.%29)
- [Some basic "benchmarks" of all PHP hash/salt algorithms](https://github.com/panique/huge/wiki/Which-hashing-&-salting-algorithm-should-be-used-%3F)
- [How to prevent PHP sessions being shared between different apache vhosts / different applications](http://www.dev-metal.com/prevent-php-sessions-shared-different-apache-vhosts-different-applications/)
## Interesting links regarding user authentication and application security
- [interesting article about password resets (by Troy Hunt, security expert)](http://www.troyhunt.com/2012/05/everything-you-ever-wanted-to-know.html)
- Password-Free Email Logins: [Ticket & discussion](https://github.com/panique/huge/issues/674), [article](http://techcrunch.com/2015/06/30/blogging-site-medium-rolls-out-password-free-email-logins/?ref=webdesignernews.com)
- Logging in via QR code: [Ticket & discussion](https://github.com/panique/huge/issues/290), [english article](https://www.grc.com/sqrl/sqrl.htm),
[german article](http://www.phpgangsta.de/sesam-oeffne-dich-sicher-einloggen-im-internetcafe),
[repo](https://github.com/PHPGangsta/Sesame), [live-demo](http://sesame.phpgangsta.de/). Big thanks to *PHPGangsta* for writing this!
### My blog
I'm also blogging at **[Dev Metal](http://www.dev-metal.com)**.
- `public/` (Webroot, `.htaccess`, `index.php`)
- `application/controller/` (Controller)
- `application/model/` (Modelle)
- `application/view/` (Views)
- `application/config/` (Konfiguration je Environment)
- `application/_installation/` (SQL-Skripte)
- `vendor/` (Composer-Abhängigkeiten)
- `tests/` (PHPUnit)

View File

@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS `huge`.`user_groups` (
`group_id` TINYINT(1) NOT NULL COMMENT 'numeric user group id, matches users.user_account_type',
`group_name` VARCHAR(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'human readable group name',
PRIMARY KEY (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user groups lookup';
INSERT INTO `huge`.`user_groups` (`group_id`, `group_name`) VALUES
(1, 'Gast'),
(2, 'Benutzer'),
(3, 'Gruppe 3'),
(4, 'Gruppe 4'),
(5, 'Gruppe 5'),
(6, 'Gruppe 6'),
(7, 'Admin')
ON DUPLICATE KEY UPDATE `group_name` = VALUES(`group_name`);

View File

@@ -33,18 +33,19 @@ return array(
* This detects your URL/IP incl. sub-folder automatically. You can also deactivate auto-detection and provide the
* URL manually. This should then look like 'http://192.168.33.44/' ! Note the slash in the end.
*/
'URL' => 'http://' . $_SERVER['HTTP_HOST'] . str_replace('public', '', dirname($_SERVER['SCRIPT_NAME'])),
// 'URL' => 'http://' . $_SERVER['HTTP_HOST'] . str_replace('public', '', dirname($_SERVER['SCRIPT_NAME'])),
'URL' => 'http://localhost:8000/', // Use this for a local environment that doesnt run on a normal webserver.
/**
* Configuration for: Folders
* Usually there's no reason to change this.
*/
'PATH_CONTROLLER' => realpath(dirname(__FILE__) . '/../../') . '/application/controller/',
'PATH_VIEW' => realpath(dirname(__FILE__) . '/../../') . '/application/view/',
'PATH_CONTROLLER' => realpath(dirname(__FILE__).'/../../') . '/application/controller/',
'PATH_VIEW' => realpath(dirname(__FILE__).'/../../') . '/application/view/',
/**
* Configuration for: Avatar paths
* Internal path to save avatars. Make sure this folder is writable. The slash at the end is VERY important!
*/
'PATH_AVATARS' => realpath(dirname(__FILE__) . '/../../') . '/public/avatars/',
'PATH_AVATARS' => realpath(dirname(__FILE__).'/../../') . '/public/avatars/',
'PATH_AVATARS_PUBLIC' => 'avatars/',
/**
* Configuration for: Default controller and action
@@ -62,7 +63,7 @@ return array(
* DB_CHARSET The charset, necessary for security reasons. Check Database.php class for more info.
*/
'DB_TYPE' => 'mysql',
'DB_HOST' => 'localhost',
'DB_HOST' => '127.0.0.1',
'DB_NAME' => 'huge',
'DB_USER' => 'root',
'DB_PASS' => 'root',

View File

@@ -38,7 +38,7 @@ return array(
"FEEDBACK_PASSWORD_REPEAT_WRONG" => "Password and password repeat are not the same.",
"FEEDBACK_PASSWORD_TOO_SHORT" => "Password has a minimum length of 6 characters.",
"FEEDBACK_USERNAME_TOO_SHORT_OR_TOO_LONG" => "Username cannot be shorter than 2 or longer than 64 characters.",
"FEEDBACK_ACCOUNT_SUCCESSFULLY_CREATED" => "Your account has been created successfully and we have sent you an email. Please click the VERIFICATION LINK within that mail.",
"FEEDBACK_ACCOUNT_SUCCESSFULLY_CREATED" => "The account has been created successfully.",
"FEEDBACK_VERIFICATION_MAIL_SENDING_FAILED" => "Sorry, we could not send you an verification mail. Your account has NOT been created.",
"FEEDBACK_ACCOUNT_CREATION_FAILED" => "Sorry, your registration failed. Please go back and try again.",
"FEEDBACK_VERIFICATION_MAIL_SENDING_ERROR" => "Verification mail could not be sent due to: ",

View File

@@ -20,7 +20,9 @@ class AdminController extends Controller
public function index()
{
$this->View->render('admin/index', array(
'users' => UserModel::getPublicProfilesOfAllUsers())
'users' => UserModel::getPublicProfilesOfAllUsers(),
'groups' => GroupModel::getAllGroups()
)
);
}
@@ -32,4 +34,10 @@ class AdminController extends Controller
Redirect::to("admin");
}
public function changeUserGroup()
{
GroupModel::setUserGroup(Request::post('user_id'), Request::post('group_id'));
Redirect::to("admin");
}
}

View File

@@ -0,0 +1,16 @@
<?php
class DirectoryController extends Controller
{
public function __construct()
{
parent::__construct();
}
public function index()
{
$this->View->render('directory/index', [
'users' => UserModel::getUsersWithGroups()
]);
}
}

View File

@@ -22,12 +22,10 @@ class RegisterController extends Controller
*/
public function index()
{
if (LoginModel::isUserLoggedIn()) {
Redirect::home();
} else {
// only admins can access registration; reuse existing admin auth check
Auth::checkAdminAuthentication();
$this->View->render('register/index');
}
}
/**
* Register page action
@@ -35,13 +33,12 @@ class RegisterController extends Controller
*/
public function register_action()
{
$registration_successful = RegistrationModel::registerNewUser();
// enforce admin-only for registration
Auth::checkAdminAuthentication();
if ($registration_successful) {
Redirect::to('login/index');
} else {
Redirect::to('register/index');
}
RegistrationModel::registerNewUser();
Redirect::to('admin/index');
}
/**
@@ -62,13 +59,12 @@ class RegisterController extends Controller
/**
* Generate a captcha, write the characters into $_SESSION['captcha'] and returns a real image which will be used
* like this: <img src="......./login/showCaptcha" />
* IMPORTANT: As this action is called via <img ...> AFTER the real application has finished executing (!), the
* SESSION["captcha"] has no content when the application is loaded. The SESSION["captcha"] gets filled at the
* moment the end-user requests the <img .. >
* Maybe refactor this sometime.
*
* This method is now deprecated as Captcha is no longer used in the registration process.
*/
public function showCaptcha()
{
CaptchaModel::generateAndShowCaptcha();
// Captcha no longer used
Redirect::to('register/index');
}
}

View File

@@ -2,14 +2,13 @@
class Config
{
// this is public to allow better Unit Testing
public static $config;
public static function get($key)
{
if (!self::$config) {
$config_file = '../application/config/config.' . Environment::get() . '.php';
$config_file = __DIR__ . '/../config/config.' . Environment::get() . '.php';
if (!file_exists($config_file)) {
return false;

View File

@@ -32,7 +32,36 @@ class DatabaseFactory
return self::$factory;
}
public function getConnection() {
public function getConnectionWithMySQLI()
{
if (!$this->database) {
// Throw exceptions and prevent also throwing credentials.
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$host = Config::get('DB_HOST');
$user = Config::get('DB_USER');
$pass = Config::get('DB_PASS');
$name = Config::get('DB_NAME');
$port = (int) Config::get('DB_PORT');
$charset = Config::get('DB_CHARSET') ? Config::get('DB_CHARSET') : 'utf8mb4';
$this->database = new mysqli($host, $user, $pass, $name, $port);
// Set charset (important for security + correct encoding)
$this->database->set_charset($charset);
} catch (mysqli_sql_exception $e) {
echo 'Database connection can not be estabilished. Please try again later.' . '<br>';
echo 'Error code: ' . $e->getCode();
exit;
}
}
return $this->database;
}
public function getConnection()
{
if (!$this->database) {
/**
@@ -46,7 +75,9 @@ class DatabaseFactory
$this->database = new PDO(
Config::get('DB_TYPE') . ':host=' . Config::get('DB_HOST') . ';dbname=' .
Config::get('DB_NAME') . ';port=' . Config::get('DB_PORT') . ';charset=' . Config::get('DB_CHARSET'),
Config::get('DB_USER'), Config::get('DB_PASS'), $options
Config::get('DB_USER'),
Config::get('DB_PASS'),
$options
);
} catch (PDOException $e) {
@@ -59,6 +90,7 @@ class DatabaseFactory
exit;
}
}
return $this->database;
}
}

View File

@@ -2,29 +2,16 @@
class Text
{
private static $texts;
public static $texts;
public static function get($key, $data = null)
public static function get($key)
{
// if not $key
if (!$key) {
return null;
}
if ($data) {
foreach ($data as $var => $value) {
${$var} = $value;
}
}
// load config file (this is only done once per application lifecycle)
if (!self::$texts) {
self::$texts = require('../application/config/texts.php');
self::$texts = require(__DIR__ . '/../config/texts.php');
}
// check if array key exists
if (!array_key_exists($key, self::$texts)) {
return null;
return "TEXT NOT FOUND";
}
return self::$texts[$key];

View File

@@ -6,6 +6,17 @@
*/
class View
{
/* Resolve all the deprecation errors happening in the views, since they cant be disabled by error_reporting level in 8.4.*/
public $redirect;
public $users;
public $notes;
public $user_name;
public $user_email;
public $user_gravatar_image_url;
public $user_avatar_file;
public $avatar_file_path;
public $user_account_type;
/**
* simply includes (=shows) the view. this is done from the controller. In the controller, you usually say
* $this->view->render('help/index'); to show (in this example) the view index.php in the folder help.

View File

@@ -0,0 +1,54 @@
<?php
class GroupModel
{
public static function getAllGroups()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT group_id, group_name FROM user_groups ORDER BY group_id";
$query = $database->prepare($sql);
$query->execute();
return $query->fetchAll();
}
public static function getGroupNameById($group_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT group_name FROM user_groups WHERE group_id = :gid LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':gid' => $group_id));
$row = $query->fetch();
return $row ? $row->group_name : null;
}
public static function setUserGroup($userId, $groupId)
{
if (!is_numeric($userId) || !is_numeric($groupId)) {
return false;
}
// Do not allow changing own group via admin UI to prevent lockout
if ((int)$userId === (int)Session::get('user_id')) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_CANT_DELETE_SUSPEND_OWN'));
return false;
}
// Only allow groups that exist in lookup
$database = DatabaseFactory::getFactory()->getConnection();
$check = $database->prepare("SELECT 1 FROM user_groups WHERE group_id = :gid LIMIT 1");
$check->execute([':gid' => $groupId]);
if ($check->rowCount() !== 1) {
return false;
}
$query = $database->prepare("UPDATE users SET user_account_type = :gid WHERE user_id = :uid LIMIT 1");
$query->execute([':gid' => $groupId, ':uid' => $userId]);
if ($query->rowCount() === 1) {
Session::add('feedback_positive', 'Benutzergruppe aktualisiert.');
return true;
}
return false;
}
}

View File

@@ -29,14 +29,13 @@ class NoteModel
*/
public static function getNote($note_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$database = DatabaseFactory::getFactory()->getConnectionWithMySQLI();
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id AND note_id = :note_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => Session::get('user_id'), ':note_id' => $note_id));
// fetch() is the PDO method that gets a single result
return $query->fetch();
return $query;
}
/**

View File

@@ -13,101 +13,68 @@ class RegistrationModel
*
* @return boolean Gives back the success status of the registration
*/
public static function registerNewUser()
public static function registerNewUser($isAdmin = false)
{
// clean the input
$user_name = strip_tags(Request::post('user_name'));
$user_email = strip_tags(Request::post('user_email'));
$user_email_repeat = strip_tags(Request::post('user_email_repeat'));
$user_password_new = Request::post('user_password_new');
$user_password_repeat = Request::post('user_password_repeat');
// Use 'user_password' if provided (admin registration), otherwise 'user_password_new'
$user_password_new = $isAdmin ? Request::post('user_password_new') : Request::post('user_password_new');
$user_password_repeat = $user_password_new; // no repeat field
// stop registration flow if registrationInputValidation() returns false (= anything breaks the input check rules)
$validation_result = self::registrationInputValidation(Request::post('captcha'), $user_name, $user_password_new, $user_password_repeat, $user_email, $user_email_repeat);
if (!$validation_result) {
return false;
}
// validate using existing validators and messages
$valid = true;
if (!self::validateUserName($user_name)) { $valid = false; }
if (!self::validateUserEmail($user_email, $user_email)) { $valid = false; }
if (!self::validateUserPassword($user_password_new, $user_password_repeat)) { $valid = false; }
if (!$valid) { return false; }
// crypt the password with the PHP 5.5's password_hash() function, results in a 60 character hash string.
// @see php.net/manual/en/function.password-hash.php for more, especially for potential options
// hash the password
$user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT);
// make return a bool variable, so both errors can come up at once if needed
$return = true;
// check if username already exists
if (UserModel::doesUsernameAlreadyExist($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN'));
$return = false;
}
// check if email already exists
if (UserModel::doesEmailAlreadyExist($user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_EMAIL_ALREADY_TAKEN'));
$return = false;
}
// if Username or Email were false, return false
if (!$return) return false;
// generate random hash for email verification (40 bytes)
$user_activation_hash = bin2hex(random_bytes(40));
// directly activate user: set empty activation hash
$user_activation_hash = null;
// write user data to database
if (!self::writeNewUserToDatabase($user_name, $user_password_hash, $user_email, time(), $user_activation_hash)) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_CREATION_FAILED'));
return false; // no reason not to return false here
}
// get user_id of the user that has been created, to keep things clean we DON'T use lastInsertId() here
$user_id = UserModel::getUserIdByUsername($user_name);
if (!$user_id) {
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
// send verification email
if (self::sendVerificationEmail($user_id, $user_email, $user_activation_hash)) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_SUCCESSFULLY_CREATED'));
return true;
}
// if verification email sending failed: instantly delete the user
self::rollbackRegistrationByUserId($user_id);
Session::add('feedback_negative', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_FAILED'));
return false;
}
/**
* Validates the registration input
*
* @param $captcha
* @param $user_name
* @param $user_password_new
* @param $user_password_repeat
* @param $user_email
* @param $user_email_repeat
*
* @return bool
*/
public static function registrationInputValidation($captcha, $user_name, $user_password_new, $user_password_repeat, $user_email, $user_email_repeat)
public static function registrationInputValidation($user_name, $user_password_new, $user_email)
{
$return = true;
// perform all necessary checks
if (!CaptchaModel::checkCaptcha($captcha)) {
Session::add('feedback_negative', Text::get('FEEDBACK_CAPTCHA_WRONG'));
if (empty($user_name) || empty($user_password_new) || empty($user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_FIELDS_EMPTY'));
$return = false;
}
// if username, email and password are all correctly validated, but make sure they all run on first sumbit
if (self::validateUserName($user_name) AND self::validateUserEmail($user_email, $user_email_repeat) AND self::validateUserPassword($user_password_new, $user_password_repeat) AND $return) {
return true;
}
// otherwise, return false
return false;
return $return;
}
/**
@@ -181,11 +148,7 @@ class RegistrationModel
return false;
}
if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
}
// no minimum length restriction
return true;
}
@@ -204,16 +167,23 @@ class RegistrationModel
{
$database = DatabaseFactory::getFactory()->getConnection();
// write new users data into database
$sql = "INSERT INTO users (user_name, user_password_hash, user_email, user_creation_timestamp, user_activation_hash, user_provider_type)
VALUES (:user_name, :user_password_hash, :user_email, :user_creation_timestamp, :user_activation_hash, :user_provider_type)";
// write new users data into database; set user_active=1 and user_activation_hash to provided value (can be null)
$sql = "INSERT INTO users (user_name, user_password_hash, user_email, user_creation_timestamp, user_activation_hash, user_provider_type, user_active)
VALUES (:user_name, :user_password_hash, :user_email, :user_creation_timestamp, :user_activation_hash, :user_provider_type, 1)";
$query = $database->prepare($sql);
$query->execute(array(':user_name' => $user_name,
try {
$query->execute(array(
':user_name' => $user_name,
':user_password_hash' => $user_password_hash,
':user_email' => $user_email,
':user_creation_timestamp' => $user_creation_timestamp,
':user_activation_hash' => $user_activation_hash,
':user_provider_type' => 'DEFAULT'));
':user_provider_type' => 'DEFAULT'
));
} catch (PDOException $e) {
// only one feedback message on failure
return false;
}
$count = $query->rowCount();
if ($count == 1) {
return true;

View File

@@ -19,7 +19,7 @@ class UserModel
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_active, user_has_avatar, user_deleted FROM users";
$sql = "SELECT user_id, user_name, user_email, user_active, user_has_avatar, user_deleted, user_account_type FROM users";
$query = $database->prepare($sql);
$query->execute();
@@ -39,11 +39,46 @@ class UserModel
$all_users_profiles[$user->user_id]->user_active = $user->user_active;
$all_users_profiles[$user->user_id]->user_deleted = $user->user_deleted;
$all_users_profiles[$user->user_id]->user_avatar_link = (Config::get('USE_GRAVATAR') ? AvatarModel::getGravatarLinkByEmail($user->user_email) : AvatarModel::getPublicAvatarFilePathOfUser($user->user_has_avatar, $user->user_id));
$all_users_profiles[$user->user_id]->user_account_type = $user->user_account_type;
}
return $all_users_profiles;
}
/**
* Gets list of users including their group name via user_groups lookup.
* @return array
*/
public static function getUsersWithGroups()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT u.user_id, u.user_name, u.user_email, u.user_active, u.user_has_avatar, u.user_deleted, u.user_account_type,
g.group_name
FROM users u
LEFT JOIN user_groups g ON g.group_id = u.user_account_type";
$query = $database->prepare($sql);
$query->execute();
$result = [];
foreach ($query->fetchAll() as $user) {
array_walk_recursive($user, 'Filter::XSSFilter');
$obj = new stdClass();
$obj->user_id = $user->user_id;
$obj->user_name = $user->user_name;
$obj->user_email = $user->user_email;
$obj->user_active = $user->user_active;
$obj->user_deleted = $user->user_deleted;
$obj->user_account_type = $user->user_account_type;
$obj->group_name = $user->group_name;
$obj->user_avatar_link = (Config::get('USE_GRAVATAR') ? AvatarModel::getGravatarLinkByEmail($user->user_email) : AvatarModel::getPublicAvatarFilePathOfUser($user->user_has_avatar, $user->user_id));
$result[] = $obj;
}
return $result;
}
/**
* Gets a user's profile data, according to the given $user_id
* @param int $user_id The user's id

View File

@@ -24,6 +24,9 @@
<li <?php if (View::checkForActiveController($filename, "profile")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>profile/index">Profiles</a>
</li>
<li <?php if (View::checkForActiveController($filename, "directory")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>directory/index">Benutzer</a>
</li>
<?php if (Session::userIsLoggedIn()) { ?>
<li <?php if (View::checkForActiveController($filename, "dashboard")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>dashboard/index">Dashboard</a>
@@ -36,9 +39,6 @@
<li <?php if (View::checkForActiveControllerAndAction($filename, "login/index")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/index">Login</a>
</li>
<li <?php if (View::checkForActiveControllerAndAction($filename, "register/index")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>register/index">Register</a>
</li>
<?php } ?>
</ul>

View File

@@ -22,6 +22,7 @@
<td>User's email</td>
<td>Activated ?</td>
<td>Link to user's profile</td>
<td>Group</td>
<td>suspension Time in days</td>
<td>Soft delete</td>
<td>Submit</td>
@@ -41,6 +42,19 @@
<td>
<a href="<?= Config::get('URL') . 'profile/showProfile/' . $user->user_id; ?>">Profile</a>
</td>
<td>
<form action="<?= Config::get('URL'); ?>admin/changeUserGroup" method="post">
<input type="hidden" name="user_id" value="<?= $user->user_id; ?>" />
<select name="group_id">
<?php foreach ($this->groups as $group) { ?>
<option value="<?= $group->group_id; ?>" <?= (isset($user->user_account_type) && (int)$user->user_account_type === (int)$group->group_id ? 'selected' : '') ?>>
<?= (int)$group->group_id; ?> - <?= htmlspecialchars($group->group_name, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php } ?>
</select>
<input type="submit" value="Save" />
</form>
</td>
<form action="<?= config::get("URL"); ?>admin/actionAccountSettings" method="post">
<td><input type="number" name="suspension" /></td>
<td><input type="checkbox" name="softDelete" <?php if ($user->user_deleted) { ?> checked <?php } ?> /></td>
@@ -53,5 +67,15 @@
<?php } ?>
</table>
</div>
<h3>Register a new user</h3>
<form method="post" action="<?php echo Config::get('URL'); ?>register/register_action">
<input type="text" name="user_name" placeholder="Username" required />
<input type="email" name="user_email" placeholder="Email address" required />
<input type="password" name="user_password_new" placeholder="Password" required autocomplete="off" />
<input type="submit" value="Register User" />
</form>
</div>
</div>

View File

@@ -0,0 +1,54 @@
<div class="container">
<h1>Benutzerverzeichnis</h1>
<div class="box">
<?php $this->renderFeedbackMessages(); ?>
<table id="users-table" class="overview-table" style="width:100%">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Benutzername</td>
<td>Email</td>
<td>Aktiv?</td>
<td>Gruppe</td>
<td>Profil</td>
</tr>
</thead>
<tbody>
<?php foreach ($this->users as $user) { ?>
<tr class="<?= ($user->user_active == 0 ? 'inactive' : 'active'); ?>">
<td><?= $user->user_id; ?></td>
<td class="avatar">
<?php if (isset($user->user_avatar_link)) { ?>
<img src="<?= $user->user_avatar_link; ?>"/>
<?php } ?>
</td>
<td><?= $user->user_name; ?></td>
<td><?= $user->user_email; ?></td>
<td><?= ($user->user_active == 0 ? 'Nein' : 'Ja'); ?></td>
<td><?= htmlspecialchars($user->group_name ?: $user->user_account_type, ENT_QUOTES, 'UTF-8'); ?></td>
<td><a href="<?= Config::get('URL') . 'profile/showProfile/' . $user->user_id; ?>">Profil</a></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<!-- jQuery & DataTables CDN -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/jquery.dataTables.min.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function(){
$('#users-table').DataTable({
pageLength: 10,
order: [[ 0, 'asc' ]],
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.8/i18n/de-DE.json'
}
});
});
</script>

View File

@@ -38,13 +38,6 @@
<a href="<?php echo Config::get('URL'); ?>login/requestPasswordReset">I forgot my password</a>
</div>
</div>
<!-- register box on right side -->
<div class="register-box">
<h2>No account yet ?</h2>
<a href="<?php echo Config::get('URL'); ?>register/index">Register</a>
</div>
</div>
</div>
</div>

View File

@@ -9,30 +9,10 @@
<!-- register form -->
<form method="post" action="<?php echo Config::get('URL'); ?>register/register_action">
<!-- the user name input field uses a HTML5 pattern check -->
<input type="text" pattern="[a-zA-Z0-9]{2,64}" name="user_name" placeholder="Username (letters/numbers, 2-64 chars)" required />
<input type="text" name="user_email" placeholder="email address (a real address)" required />
<input type="text" name="user_email_repeat" placeholder="repeat email address (to prevent typos)" required />
<input type="password" name="user_password_new" pattern=".{6,}" placeholder="Password (6+ characters)" required autocomplete="off" />
<input type="password" name="user_password_repeat" pattern=".{6,}" required placeholder="Repeat your password" autocomplete="off" />
<!-- show the captcha by calling the login/showCaptcha-method in the src attribute of the img tag -->
<img id="captcha" src="<?php echo Config::get('URL'); ?>register/showCaptcha" />
<input type="text" name="captcha" placeholder="Please enter above characters" required />
<!-- quick & dirty captcha reloader -->
<a href="#" style="display: block; font-size: 11px; margin: 5px 0 15px 0; text-align: center"
onclick="document.getElementById('captcha').src = '<?php echo Config::get('URL'); ?>register/showCaptcha?' + Math.random(); return false">Reload Captcha</a>
<input type="submit" value="Register" />
</form>
</div>
</div>
<div class="container">
<p style="display: block; font-size: 11px; color: #999;">
Please note: This captcha will be generated when the img tag requests the captcha-generation
(= a real image) from YOURURL/register/showcaptcha. As this is a client-side triggered request, a
$_SESSION["captcha"] dump will not show the captcha characters. The captcha generation
happens AFTER the request that generates THIS page has been finished.
</p>
</div>

1374
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
// auto-loading the classes (currently only from application/libs) via Composer's PSR-4 auto-loader
// later it might be useful to use a namespace here, but for now let's keep it as simple as possible
require '../vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';
// start our application
new Application();

11
public/router.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
if (php_sapi_name() === 'cli-server') {
$filePath = __DIR__ . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (is_file($filePath)) {
return false;
}
}
$_GET['url'] = isset($_SERVER['PATH_INFO']) ? ltrim($_SERVER['PATH_INFO'], '/') : '';
require __DIR__ . '/index.php';

3
start.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
php -S localhost:8000 -t ./public ./public/router.php