initial commit

This commit is contained in:
2025-11-24 14:06:57 +01:00
commit 4fce91b055
81 changed files with 7718 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# this is just a demo .gitignore file, put all the files and folders you want to be ignored by git inside
# if you work with NetBeans: ignore NetBeans project files
/nbproject/private/
# don't commit all the dependencies fetched via Composer
/vendor/
.idea/

102
CHANGELOG.md Normal file
View File

@@ -0,0 +1,102 @@
# CHANGE LOG
For the newest (und unstable) version always check the develop branch, for beta check
master branch, for really stable stuff check the releases (the ones that have a real version number :))
## master branch
- [slaveek/panique] [PR](https://github.com/panique/huge/pull/773) [#770] fix for sending user back to last visited page after login
- [slaveek] [PR](https://github.com/panique/huge/pull/815) lots of code styling fixes
- [panique] [#729] Fix, mail sending now returns true or false success status (https://github.com/panique/huge/issues/729)
- [OmarElGabry] [PR](https://github.com/panique/huge/pull/693) session id regeneration in certain situations
- [OmarElGabry] [PR](https://github.com/panique/huge/pull/693) encrypted cookies
- [OmarElGabry] [PR](https://github.com/panique/huge/pull/693) new encryption class
- [OmarElGabry] [PR](https://github.com/panique/huge/pull/693) anti-CSRF feature (used in login and username change forms)
- [josh-bridge] [PR](https://github.com/panique/huge/pull/689) logged-in user can now change password
- [justincdotme] [PR](https://github.com/panique/huge/pull/684) better code for brute-force blocking when logging in
- [panique] soft autoinstaller improvements
- [panique] updated dependencies to current versions
- [Kent55/panique] XSS protection filter
- [FAlbanni] XSS protection with better session/cookie, now only allowed on used domain
- [panique] there's now a simple Favicon and a fallback to avoid browsers hammering the application requesting favicons
- [panique] application has now a page title
- [panique] avatar upload feature can now handle jpg, png, gif
- [panique/tankerkiller125] avatars folder now does not run any PHP code (security improvement)
- [tysonlist] [#657] send user back to last-visited page after successful login (when not being logged in first)
- [sandropons] anti-brute-force feature for login process
- [panique] removed old Facebook texts (as Login-via-Facebook feature was removed since 3.0)
- [oisian/ldmusic] [#608] Deletion / suspension of users, Admin menu
- [panique] [#654](https://github.com/panique/huge/issues/654) little frontend navi bug fixed
- [Dominic28] [PR](https://github.com/panique/huge/pull/645) added checkboxes to request class
- [Dominic28] [PR](https://github.com/panique/huge/pull/644) code style fixes
- [M0ritzWeide] [PR](https://github.com/panique/huge/pull/635) added browser caching
- [modInfo/panique] [PR](https://github.com/panique/huge/pull/647) added missing view table column
## 3.1
Code Quality at Scrutinizer 9.7/10, at Code Climate 3.9/4
**February 2015**
- [panique] several code quality improvements (and line reductions :) ) all over the project
- [PR](https://github.com/panique/huge/pull/620) [owenr88] view rending now possible with multiple view files
- [panique] lots of code refactorings and simplifications all over the project
- [PR](https://github.com/panique/huge/pull/615) [Dominic28] Avatar can now be deleted by the user
- [panique] First Unit tests :)
- [panique] several code quality improvements all over the project
- [panique] avatarModel code improvements
- [panique] renamed AccountType stuff to UserRole, minor changes
## 3.0
Code Quality at Scrutinizer 9.3/10, at Code Climate 3.9/4
**February 2015**
- [panique] removed duplicate code in AccountTypeModel
- [PR](https://github.com/panique/huge/pull/587) [upperwood] Facebook stuff completely removed from SQL
- [panique] tiny text changes
**January 2015**
- [panique] added static Text class (gets the messages etc)
- [panique] added static Environment class (get the environment)
- [panique] added static Config class (gets config easily and according to environment)
- [panique] new styling of the entire project: login/index has new look now
- [panique] massive refactoring of all model classes: lots of methods have been organized into other model classes
- [panique] massive refactoring of all model classes: all methods are static now
- [panique] EXPERIMENTAL: added static database call / DatabaseFactory, rebuild NoteModel with static methods
- [panique] massive refactoring of mail sending, (chose between PHPMailer, SwiftMailer, native / SMTP or no SMTP)
**December 2014**
- [panique] lots of refactorings
- [panique] refactored LoginModel'S login() method / LoginController's login() method
- [panique] removed COOKIE_DOMAIN (cookie is now valid on the domain/IP it has been created on)
- [panique] Abstracting super-globals like $_POST['x'] into Request::post('x')
- [panique] entirely removed all the Facebook stuff [will be replaced by new proper Oauth2 solution soon]
- [panique] lots of code refactorings and cleaning, deletions of duplicate code
- [panique] moving nearly all hardcoded values to config
- [panique] new View handling: you'll have to pass vars to the view renderer now
- [panique] completely removed Facebook login process from controller (incomplete) [will be replaced by new solution]
- [panique] less config, URL/IP is auto-detected now
- [panique] added loadConfig() to load a specific config according to environment setting (fallback: development)
- [panique] added getEnvironment() to fetch (potential) environment setting
- [panique] replaced native super-globals access by wrapper access (Session:get instead of $_SESSION)
- [panique] complete frontend rebuilding (incomplete yet)
- [panique] massive cleaning of all controllers
- [panique] added Session::add() to allow stacking of elements (useful for collecting feedback, errors etc)
- [panique] complete rebuild of model handling
- [panique] View can now render(), renderWithoutHeaderFooter() and renderJSON
- [panique] using Composer's PSR-4 autoloader (in a very basic way currently)
- [panique] DB construction needs now port by default
- [panique] removed (semi-optional) hashing cost factor (as it's redundant usually)
- [panique] email max limit increased to 254/255 (official number)
- [panique] simpler and improved core
- [panique] improved architecture, controllers are now named like "IndexController"
- [panique] moved index.php to /public folder, new .htaccess, new installation guideline
- [panique] MVC naming fixes
- [nerdalertdk] betters paths, automatic paths
- [panique] removed legacy PHP stuff: 5.5.x is now the minimum
- [PR](https://github.com/panique/php-login/pull/503) [Malkleth] allow users to request password reset by inputting email as well as user names
- [PR](https://github.com/panique/php-login/pull/516) [pein0119] cookie runtime calculation fix

632
README.md Normal file
View File

@@ -0,0 +1,632 @@
![HUGE, formerly "php-login" logo](_pictures/huge.png)
# HUGE
[![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&utm_medium=referral&utm_content=panique/huge&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)
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.
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
```
Make it executable
```bash
chmod +x bootstrap.sh
```
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>
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
"Email does not work" ? See the troubleshooting below. TODO
#### 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
```
<VirtualHost *:80>
DocumentRoot "/var/www/html/public"
<Directory "/var/www/html/public">
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
Enable mod_rewrite and restart apache.
```bash
sudo a2enmod rewrite
service apache2 restart
```
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.
```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
```
git clone HUGE
```bash
sudo git clone https://github.com/panique/huge "/var/www/html"
```
Install Composer
```bash
curl -s https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
```
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"
```
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)**.

22
_one-click-installation/Vagrantfile vendored Normal file
View File

@@ -0,0 +1,22 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Every Vagrant virtual environment requires a box to build off of.
config.vm.box = "ubuntu/trusty64"
# Create a private network, which allows host-only access to the machine using a specific IP.
config.vm.network "private_network", ip: "192.168.33.111"
# Share an additional folder to the guest VM. The first argument is the path on the host to the actual folder.
# The second argument is the path on the guest to mount the folder.
config.vm.synced_folder "./", "/var/www/html"
# Define the bootstrap file: A (shell) script that runs after first setup of your box (= provisioning)
config.vm.provision :shell, path: "bootstrap.sh"
end

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Use single quotes instead of double quotes to make it work with special-character passwords
PASSWORD='12345678'
PROJECTFOLDER='myproject'
# Create project folder, written in 3 single mkdir-statements to make sure this runs everywhere without problems
sudo mkdir "/var/www"
sudo mkdir "/var/www/html"
sudo mkdir "/var/www/html/${PROJECTFOLDER}"
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get install -y apache2
sudo apt-get install -y php5
sudo debconf-set-selections <<< "mysql-server mysql-server/root_password password $PASSWORD"
sudo debconf-set-selections <<< "mysql-server mysql-server/root_password_again password $PASSWORD"
sudo apt-get -y install mysql-server
sudo apt-get install php5-mysql
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/dbconfig-install boolean true"
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/app-password-confirm password $PASSWORD"
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/mysql/admin-pass password $PASSWORD"
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/mysql/app-pass password $PASSWORD"
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/reconfigure-webserver multiselect apache2"
sudo apt-get -y install phpmyadmin
# setup hosts file
VHOST=$(cat <<EOF
<VirtualHost *:80>
DocumentRoot "/var/www/html/${PROJECTFOLDER}/public"
<Directory "/var/www/html/${PROJECTFOLDER}/public">
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
EOF
)
echo "${VHOST}" > /etc/apache2/sites-available/000-default.conf
# enable mod_rewrite
sudo a2enmod rewrite
# restart apache
service apache2 restart
# install curl (needed to use git afaik)
sudo apt-get -y install curl
sudo apt-get -y install php5-curl
# install openssl (needed to clone from GitHub, as github is https only)
sudo apt-get -y install openssl
# install PHP GD, the graphic lib (we create captchas and avatars)
sudo apt-get -y install php5-gd
# install git
sudo apt-get -y install git
# git clone HUGE
sudo git clone https://github.com/panique/huge "/var/www/html/${PROJECTFOLDER}"
# install Composer
curl -s https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
# go to project folder, load Composer packages
cd "/var/www/html/${PROJECTFOLDER}"
composer install
# run SQL statements from install folder
sudo mysql -h "localhost" -u "root" "-p${PASSWORD}" < "/var/www/html/${PROJECTFOLDER}/application/_installation/01-create-database.sql"
sudo mysql -h "localhost" -u "root" "-p${PASSWORD}" < "/var/www/html/${PROJECTFOLDER}/application/_installation/02-create-table-users.sql"
sudo mysql -h "localhost" -u "root" "-p${PASSWORD}" < "/var/www/html/${PROJECTFOLDER}/application/_installation/03-create-table-notes.sql"
# writing rights to avatar folder
sudo chown -R www-data "/var/www/html/${PROJECTFOLDER}/public/avatars"
# if this didn't work for you, you can also try the hard way:
#sudo chmod 0777 -R "/var/www/html/${PROJECTFOLDER}/public/avatars"
# remove Apache's default demo file
sudo rm "/var/www/html/index.html"
# final feedback
echo "Voila!"

BIN
_pictures/huge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1 @@
CREATE DATABASE IF NOT EXISTS `huge`;

View File

@@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS `huge`.`users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'auto incrementing user_id of each user, unique index',
`session_id` varchar(48) DEFAULT NULL COMMENT 'stores session cookie id to prevent session concurrency',
`user_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'user''s name, unique',
`user_password_hash` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s password in salted and hashed format',
`user_email` varchar(254) COLLATE utf8_unicode_ci NOT NULL COMMENT 'user''s email, unique',
`user_active` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'user''s activation status',
`user_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'user''s deletion status',
`user_account_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'user''s account type (basic, premium, etc)',
`user_has_avatar` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 if user has a local avatar, 0 if not',
`user_remember_me_token` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s remember-me cookie token',
`user_creation_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of the creation of user''s account',
`user_suspension_timestamp` bigint(20) DEFAULT NULL COMMENT 'Timestamp till the end of a user suspension',
`user_last_login_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of user''s last login',
`user_failed_logins` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'user''s failed login attempts',
`user_last_failed_login` int(10) DEFAULT NULL COMMENT 'unix timestamp of last failed login attempt',
`user_activation_hash` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s email verification hash string',
`user_password_reset_hash` char(80) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s password reset code',
`user_password_reset_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of the password reset request',
`user_provider_type` text COLLATE utf8_unicode_ci,
PRIMARY KEY (`user_id`),
UNIQUE KEY `user_name` (`user_name`),
UNIQUE KEY `user_email` (`user_email`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user data';
INSERT INTO `huge`.`users` (`user_id`, `session_id`, `user_name`, `user_password_hash`, `user_email`, `user_active`, `user_deleted`, `user_account_type`,
`user_has_avatar`, `user_remember_me_token`, `user_creation_timestamp`, `user_suspension_timestamp`, `user_last_login_timestamp`,
`user_failed_logins`, `user_last_failed_login`, `user_activation_hash`, `user_password_reset_hash`,
`user_password_reset_timestamp`, `user_provider_type`) VALUES
(1, NULL, 'demo', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', 'demo@demo.com', 1, 0, 7, 0, NULL, 1422205178, NULL, 1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT'),
(2, NULL, 'demo2', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', 'demo2@demo.com', 1, 0, 1, 0, NULL, 1422205178, NULL, 1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT');

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `huge`.`notes` (
`note_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`note_text` text NOT NULL,
`user_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`note_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user notes';

View File

@@ -0,0 +1,156 @@
<?php
/**
* Configuration for DEVELOPMENT environment
* To create another configuration set just copy this file to config.production.php etc. You get the idea :)
*/
/**
* Configuration for: Error reporting
* Useful to show every little problem during development, but only show hard / no errors in production.
* It's a little bit dirty to put this here, but who cares. For development purposes it's totally okay.
*/
error_reporting(E_ALL);
ini_set("display_errors", 1);
/**
* Configuration for cookie security
* Quote from PHP manual: Marks the cookie as accessible only through the HTTP protocol. This means that the cookie
* won't be accessible by scripting languages, such as JavaScript. This setting can effectively help to reduce identity
* theft through XSS attacks (although it is not supported by all browsers).
*
* @see php.net/manual/en/session.configuration.php#ini.session.cookie-httponly
*/
ini_set('session.cookie_httponly', 1);
/**
* Returns the full configuration.
* This is used by the core/Config class.
*/
return array(
/**
* Configuration for: Base URL
* 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'])),
/**
* 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/',
/**
* 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_PUBLIC' => 'avatars/',
/**
* Configuration for: Default controller and action
*/
'DEFAULT_CONTROLLER' => 'index',
'DEFAULT_ACTION' => 'index',
/**
* Configuration for: Database
* DB_TYPE The used database type. Note that other types than "mysql" might break the db construction currently.
* DB_HOST The mysql hostname, usually localhost or 127.0.0.1
* DB_NAME The database name
* DB_USER The username
* DB_PASS The password
* DB_PORT The mysql port, 3306 by default (?), find out via phpinfo() and look for mysqli.default_port.
* DB_CHARSET The charset, necessary for security reasons. Check Database.php class for more info.
*/
'DB_TYPE' => 'mysql',
'DB_HOST' => 'localhost',
'DB_NAME' => 'huge',
'DB_USER' => 'root',
'DB_PASS' => 'root',
'DB_PORT' => '3306',
'DB_CHARSET' => 'utf8',
/**
* Configuration for: Captcha size
* The currently used Captcha generator (https://github.com/Gregwar/Captcha) also runs without giving a size,
* so feel free to use ->build(); inside CaptchaModel.
*/
'CAPTCHA_WIDTH' => 359,
'CAPTCHA_HEIGHT' => 100,
/**
* Configuration for: Cookies
* 1209600 seconds = 2 weeks
* COOKIE_PATH is the path the cookie is valid on, usually "/" to make it valid on the whole domain.
* @see http://stackoverflow.com/q/9618217/1114320
* @see php.net/manual/en/function.setcookie.php
*
* COOKIE_DOMAIN: The domain where the cookie is valid for. Usually this does not work with "localhost",
* ".localhost", "127.0.0.1", or ".127.0.0.1". If so, leave it as empty string, false or null.
* When using real domains make sure you have a dot (!) in front of the domain, like ".mydomain.com". This is
* strange, but explained here:
* @see http://stackoverflow.com/questions/2285010/php-setcookie-domain
* @see http://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain
* @see http://php.net/manual/en/function.setcookie.php#73107
*
* COOKIE_SECURE: If the cookie will be transferred through secured connection(SSL). It's highly recommended to set it to true if you have secured connection.
* COOKIE_HTTP: If set to true, Cookies that can't be accessed by JS - Highly recommended!
* SESSION_RUNTIME: How long should a session cookie be valid by seconds, 604800 = 1 week.
*/
'COOKIE_RUNTIME' => 1209600,
'COOKIE_PATH' => '/',
'COOKIE_DOMAIN' => "",
'COOKIE_SECURE' => false,
'COOKIE_HTTP' => true,
'SESSION_RUNTIME' => 604800,
/**
* Configuration for: Avatars/Gravatar support
* Set to true if you want to use "Gravatar(s)", a service that automatically gets avatar pictures via using email
* addresses of users by requesting images from the gravatar.com API. Set to false to use own locally saved avatars.
* AVATAR_SIZE set the pixel size of avatars/gravatars (will be 44x44 by default). Avatars are always squares.
* AVATAR_DEFAULT_IMAGE is the default image in public/avatars/
*/
'USE_GRAVATAR' => false,
'GRAVATAR_DEFAULT_IMAGESET' => 'mm',
'GRAVATAR_RATING' => 'pg',
'AVATAR_SIZE' => 44,
'AVATAR_JPEG_QUALITY' => 85,
'AVATAR_DEFAULT_IMAGE' => 'default.jpg',
/**
* Configuration for: Encryption Keys
* ENCRYPTION_KEY, HMAC_SALT: Currently used to encrypt and decrypt publicly visible values, like the user id in
* the cookie. Change these values for increased security, but don't touch if you have no idea what this means.
*/
'ENCRYPTION_KEY' => '6#x0gÊìf^25cL1f$08&',
'HMAC_SALT' => '8qk9c^4L6d#15tM8z7n0%',
/**
* Configuration for: Email server credentials
*
* Here you can define how you want to send emails.
* If you have successfully set up a mail server on your linux server and you know
* what you do, then you can skip this section. Otherwise please set EMAIL_USE_SMTP to true
* and fill in your SMTP provider account data.
*
* EMAIL_USED_MAILER: Check Mail class for alternatives
* EMAIL_USE_SMTP: Use SMTP or not
* EMAIL_SMTP_AUTH: leave this true unless your SMTP service does not need authentication
*/
'EMAIL_USED_MAILER' => 'phpmailer',
'EMAIL_USE_SMTP' => false,
'EMAIL_SMTP_HOST' => 'yourhost',
'EMAIL_SMTP_AUTH' => true,
'EMAIL_SMTP_USERNAME' => 'yourusername',
'EMAIL_SMTP_PASSWORD' => 'yourpassword',
'EMAIL_SMTP_PORT' => 465,
'EMAIL_SMTP_ENCRYPTION' => 'ssl',
/**
* Configuration for: Email content data
*/
'EMAIL_PASSWORD_RESET_URL' => 'login/verifypasswordreset',
'EMAIL_PASSWORD_RESET_FROM_EMAIL' => 'no-reply@example.com',
'EMAIL_PASSWORD_RESET_FROM_NAME' => 'My Project',
'EMAIL_PASSWORD_RESET_SUBJECT' => 'Password reset for PROJECT XY',
'EMAIL_PASSWORD_RESET_CONTENT' => 'Please click on this link to reset your password: ',
'EMAIL_VERIFICATION_URL' => 'register/verify',
'EMAIL_VERIFICATION_FROM_EMAIL' => 'no-reply@example.com',
'EMAIL_VERIFICATION_FROM_NAME' => 'My Project',
'EMAIL_VERIFICATION_SUBJECT' => 'Account activation for PROJECT XY',
'EMAIL_VERIFICATION_CONTENT' => 'Please click on this link to activate your account: ',
);

View File

@@ -0,0 +1,75 @@
<?php
/**
* Texts used in the application.
* These texts are used via Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN').
* Could be extended to i18n etc.
*/
return array(
"FEEDBACK_UNKNOWN_ERROR" => "Unknown error occurred!",
"FEEDBACK_DELETED" => "Your account has been deleted.",
"FEEDBACK_ACCOUNT_SUSPENDED" => "Account Suspended for ",
"FEEDBACK_ACCOUNT_SUSPENSION_DELETION_STATUS" => "This user's suspension / deletion status has been edited.",
"FEEDBACK_ACCOUNT_CANT_DELETE_SUSPEND_OWN" => "You can not delete or suspend your own account.",
"FEEDBACK_ACCOUNT_USER_SUCCESSFULLY_KICKED" => "The selected user has been successfully kicked out of the system (by resetting this user's session)",
"FEEDBACK_PASSWORD_WRONG_3_TIMES" => "You have typed in a wrong password 3 or more times already. Please wait 30 seconds to try again.",
"FEEDBACK_ACCOUNT_NOT_ACTIVATED_YET" => "Your account is not activated yet. Please click on the confirm link in the mail.",
"FEEDBACK_USERNAME_OR_PASSWORD_WRONG" => "The username or password is incorrect. Please try again.",
"FEEDBACK_USER_DOES_NOT_EXIST" => "This user does not exist.",
"FEEDBACK_LOGIN_FAILED" => "Login failed.",
"FEEDBACK_LOGIN_FAILED_3_TIMES" => "Login failed 3 or more times already. Please wait 30 seconds to try again.",
"FEEDBACK_USERNAME_FIELD_EMPTY" => "Username field was empty.",
"FEEDBACK_PASSWORD_FIELD_EMPTY" => "Password field was empty.",
"FEEDBACK_USERNAME_OR_PASSWORD_FIELD_EMPTY" => "Username or password field was empty.",
"FEEDBACK_USERNAME_EMAIL_FIELD_EMPTY" => "Username / email field was empty.",
"FEEDBACK_EMAIL_FIELD_EMPTY" => "Email field was empty.",
"FEEDBACK_EMAIL_REPEAT_WRONG" => "Email and email repeat are not the same",
"FEEDBACK_EMAIL_AND_PASSWORD_FIELDS_EMPTY" => "Email and password fields were empty.",
"FEEDBACK_USERNAME_SAME_AS_OLD_ONE" => "Sorry, that username is the same as your current one. Please choose another one.",
"FEEDBACK_USERNAME_ALREADY_TAKEN" => "Sorry, that username is already taken. Please choose another one.",
"FEEDBACK_USER_EMAIL_ALREADY_TAKEN" => "Sorry, that email is already in use. Please choose another one.",
"FEEDBACK_USERNAME_CHANGE_SUCCESSFUL" => "Your username has been changed successfully.",
"FEEDBACK_USERNAME_AND_PASSWORD_FIELD_EMPTY" => "Username and password fields were empty.",
"FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN" => "Username does not fit the name pattern: only a-Z and numbers are allowed, 2 to 64 characters.",
"FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN" => "Sorry, your chosen email does not fit into the email naming pattern.",
"FEEDBACK_EMAIL_SAME_AS_OLD_ONE" => "Sorry, that email address is the same as your current one. Please choose another one.",
"FEEDBACK_EMAIL_CHANGE_SUCCESSFUL" => "Your email address has been changed successfully.",
"FEEDBACK_CAPTCHA_WRONG" => "The entered captcha security characters were wrong.",
"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_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: ",
"FEEDBACK_VERIFICATION_MAIL_SENDING_SUCCESSFUL" => "A verification mail has been sent successfully.",
"FEEDBACK_ACCOUNT_ACTIVATION_SUCCESSFUL" => "Activation was successful! You can now log in.",
"FEEDBACK_ACCOUNT_ACTIVATION_FAILED" => "Sorry, no such id/verification code combination here! It might be possible that your mail provider (Yahoo? Hotmail?) automatically visits links in emails for anti-scam scanning, so this activation link might been clicked without your action. Please try to log in on the main page.",
"FEEDBACK_AVATAR_UPLOAD_SUCCESSFUL" => "Avatar upload was successful.",
"FEEDBACK_AVATAR_UPLOAD_WRONG_TYPE" => "Only JPEG and PNG files are supported.",
"FEEDBACK_AVATAR_UPLOAD_TOO_SMALL" => "Avatar source file's width/height is too small. Needs to be 100x100 pixel minimum.",
"FEEDBACK_AVATAR_UPLOAD_TOO_BIG" => "Avatar source file is too big. 5 Megabyte is the maximum.",
"FEEDBACK_AVATAR_FOLDER_DOES_NOT_EXIST_OR_NOT_WRITABLE" => "Avatar folder does not exist or is not writable. Please change this via chmod 775 or 777.",
"FEEDBACK_AVATAR_IMAGE_UPLOAD_FAILED" => "Something went wrong with the image upload.",
"FEEDBACK_AVATAR_IMAGE_DELETE_SUCCESSFUL" => "You successfully deleted your avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_NO_FILE" => "You don't have a custom avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_FAILED" => "Something went wrong while deleting your avatar.",
"FEEDBACK_PASSWORD_RESET_TOKEN_FAIL" => "Could not write token to database.",
"FEEDBACK_PASSWORD_RESET_TOKEN_MISSING" => "No password reset token.",
"FEEDBACK_PASSWORD_RESET_MAIL_SENDING_ERROR" => "Password reset mail could not be sent due to: ",
"FEEDBACK_PASSWORD_RESET_MAIL_SENDING_SUCCESSFUL" => "A password reset mail has been sent successfully.",
"FEEDBACK_PASSWORD_RESET_LINK_EXPIRED" => "Your reset link has expired. Please use the reset link within one hour.",
"FEEDBACK_PASSWORD_RESET_COMBINATION_DOES_NOT_EXIST" => "Username/Verification code combination does not exist.",
"FEEDBACK_PASSWORD_RESET_LINK_VALID" => "Password reset validation link is valid. Please change the password now.",
"FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL" => "Password successfully changed.",
"FEEDBACK_PASSWORD_CHANGE_FAILED" => "Sorry, your password changing failed.",
"FEEDBACK_PASSWORD_NEW_SAME_AS_CURRENT" => "New password is the same as the current password.",
"FEEDBACK_PASSWORD_CURRENT_INCORRECT" => "Current password entered was incorrect.",
"FEEDBACK_ACCOUNT_TYPE_CHANGE_SUCCESSFUL" => "Account type change successful",
"FEEDBACK_ACCOUNT_TYPE_CHANGE_FAILED" => "Account type change failed",
"FEEDBACK_NOTE_CREATION_FAILED" => "Note creation failed.",
"FEEDBACK_NOTE_EDITING_FAILED" => "Note editing failed.",
"FEEDBACK_NOTE_DELETION_FAILED" => "Note deletion failed.",
"FEEDBACK_COOKIE_INVALID" => "Your remember-me-cookie is invalid.",
"FEEDBACK_COOKIE_LOGIN_SUCCESSFUL" => "You were successfully logged in via the remember-me-cookie.",
);

View File

@@ -0,0 +1,35 @@
<?php
class AdminController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// special authentication check for the entire controller: Note the check-ADMIN-authentication!
// All methods inside this controller are only accessible for admins (= users that have role type 7)
Auth::checkAdminAuthentication();
}
/**
* This method controls what happens when you move to /admin or /admin/index in your app.
*/
public function index()
{
$this->View->render('admin/index', array(
'users' => UserModel::getPublicProfilesOfAllUsers())
);
}
public function actionAccountSettings()
{
AdminModel::setAccountSuspensionAndDeletionStatus(
Request::post('suspension'), Request::post('softDelete'), Request::post('user_id')
);
Redirect::to("admin");
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* This controller shows an area that's only visible for logged in users (because of Auth::checkAuthentication(); in line 16)
*/
class DashboardController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// this entire controller should only be visible/usable by logged in users, so we put authentication-check here
Auth::checkAuthentication();
}
/**
* This method controls what happens when you move to /dashboard/index in your app.
*/
public function index()
{
$this->View->render('dashboard/index');
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Class Error
* This controller simply contains some methods that can be used to give proper feedback in certain error scenarios,
* like a proper 404 response with an additional HTML page behind when something does not exist.
*/
class ErrorController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* Use this when something is not found. Gives back a proper 404 header response plus a normal page (where you could
* show a well-designed error message or something more useful for your users).
* You can see this in action in action in /core/Application.php -> __construct
*/
public function error404()
{
header('HTTP/1.0 404 Not Found', true, 404);
$this->View->render('error/404');
}
}

View File

@@ -0,0 +1,21 @@
<?php
class IndexController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* Handles what happens when user moves to URL/index/index - or - as this is the default controller, also
* when user moves to /index or enter your application at base level
*/
public function index()
{
$this->View->render('index/index');
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* LoginController
* Controls everything that is authentication-related
*/
class LoginController extends Controller
{
/**
* Construct this object by extending the basic Controller class. The parent::__construct thing is necessary to
* put checkAuthentication in here to make an entire controller only usable for logged-in users (for sure not
* needed in the LoginController).
*/
public function __construct()
{
parent::__construct();
}
/**
* Index, default action (shows the login form), when you do login/index
*/
public function index()
{
// if user is logged in redirect to main-page, if not show the view
if (LoginModel::isUserLoggedIn()) {
Redirect::home();
} else {
$data = array('redirect' => Request::get('redirect') ? Request::get('redirect') : null);
$this->View->render('login/index', $data);
}
}
/**
* The login action, when you do login/login
*/
public function login()
{
// check if csrf token is valid
if (!Csrf::isTokenValid()) {
LoginModel::logout();
Redirect::home();
exit();
}
// perform the login method, put result (true or false) into $login_successful
$login_successful = LoginModel::login(
Request::post('user_name'), Request::post('user_password'), Request::post('set_remember_me_cookie')
);
// check login status: if true, then redirect user to user/index, if false, then to login form again
if ($login_successful) {
if (Request::post('redirect')) {
Redirect::toPreviousViewedPageAfterLogin(ltrim(urldecode(Request::post('redirect')), '/'));
} else {
Redirect::to('user/index');
}
} else {
if (Request::post('redirect')) {
Redirect::to('login?redirect=' . ltrim(urlencode(Request::post('redirect')), '/'));
} else {
Redirect::to('login/index');
}
}
}
/**
* The logout action
* Perform logout, redirect user to main-page
*/
public function logout()
{
LoginModel::logout();
Redirect::home();
exit();
}
/**
* Login with cookie
*/
public function loginWithCookie()
{
// run the loginWithCookie() method in the login-model, put the result in $login_successful (true or false)
$login_successful = LoginModel::loginWithCookie(Request::cookie('remember_me'));
// if login successful, redirect to dashboard/index ...
if ($login_successful) {
Redirect::to('dashboard/index');
} else {
// if not, delete cookie (outdated? attack?) and route user to login form to prevent infinite login loops
LoginModel::deleteCookie();
Redirect::to('login/index');
}
}
/**
* Show the request-password-reset page
*/
public function requestPasswordReset()
{
$this->View->render('login/requestPasswordReset');
}
/**
* The request-password-reset action
* POST-request after form submit
*/
public function requestPasswordReset_action()
{
PasswordResetModel::requestPasswordReset(Request::post('user_name_or_email'), Request::post('captcha'));
Redirect::to('login/index');
}
/**
* Verify the verification token of that user (to show the user the password editing view or not)
* @param string $user_name username
* @param string $verification_code password reset verification token
*/
public function verifyPasswordReset($user_name, $verification_code)
{
// check if this the provided verification code fits the user's verification code
if (PasswordResetModel::verifyPasswordReset($user_name, $verification_code)) {
// pass URL-provided variable to view to display them
$this->View->render('login/resetPassword', array(
'user_name' => $user_name,
'user_password_reset_hash' => $verification_code
));
} else {
Redirect::to('login/index');
}
}
/**
* Set the new password
* Please note that this happens while the user is not logged in. The user identifies via the data provided by the
* password reset link from the email, automatically filled into the <form> fields. See verifyPasswordReset()
* for more. Then (regardless of result) route user to index page (user will get success/error via feedback message)
* POST request !
* TODO this is an _action
*/
public function setNewPassword()
{
PasswordResetModel::setNewPassword(
Request::post('user_name'), Request::post('user_password_reset_hash'),
Request::post('user_password_new'), Request::post('user_password_repeat')
);
Redirect::to('login/index');
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* The note controller: Just an example of simple create, read, update and delete (CRUD) actions.
*/
class NoteController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// VERY IMPORTANT: All controllers/areas that should only be usable by logged-in users
// need this line! Otherwise not-logged in users could do actions. If all of your pages should only
// be usable by logged-in users: Put this line into libs/Controller->__construct
Auth::checkAuthentication();
}
/**
* This method controls what happens when you move to /note/index in your app.
* Gets all notes (of the user).
*/
public function index()
{
$this->View->render('note/index', array(
'notes' => NoteModel::getAllNotes()
));
}
/**
* This method controls what happens when you move to /dashboard/create in your app.
* Creates a new note. This is usually the target of form submit actions.
* POST request.
*/
public function create()
{
NoteModel::createNote(Request::post('note_text'));
Redirect::to('note');
}
/**
* This method controls what happens when you move to /note/edit(/XX) in your app.
* Shows the current content of the note and an editing form.
* @param $note_id int id of the note
*/
public function edit($note_id)
{
$this->View->render('note/edit', array(
'note' => NoteModel::getNote($note_id)
));
}
/**
* This method controls what happens when you move to /note/editSave in your app.
* Edits a note (performs the editing after form submit).
* POST request.
*/
public function editSave()
{
NoteModel::updateNote(Request::post('note_id'), Request::post('note_text'));
Redirect::to('note');
}
/**
* This method controls what happens when you move to /note/delete(/XX) in your app.
* Deletes a note. In a real application a deletion via GET/URL is not recommended, but for demo purposes it's
* totally okay.
* @param int $note_id id of the note
*/
public function delete($note_id)
{
NoteModel::deleteNote($note_id);
Redirect::to('note');
}
}

View File

@@ -0,0 +1,39 @@
<?php
class ProfileController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* This method controls what happens when you move to /overview/index in your app.
* Shows a list of all users.
*/
public function index()
{
$this->View->render('profile/index', array(
'users' => UserModel::getPublicProfilesOfAllUsers())
);
}
/**
* This method controls what happens when you move to /overview/showProfile in your app.
* Shows the (public) details of the selected user.
* @param $user_id int id the the user
*/
public function showProfile($user_id)
{
if (isset($user_id)) {
$this->View->render('profile/showProfile', array(
'user' => UserModel::getPublicProfileOfUser($user_id))
);
} else {
Redirect::home();
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* RegisterController
* Register new user
*/
class RegisterController extends Controller
{
/**
* Construct this object by extending the basic Controller class. The parent::__construct thing is necessary to
* put checkAuthentication in here to make an entire controller only usable for logged-in users (for sure not
* needed in the RegisterController).
*/
public function __construct()
{
parent::__construct();
}
/**
* Register page
* Show the register form, but redirect to main-page if user is already logged-in
*/
public function index()
{
if (LoginModel::isUserLoggedIn()) {
Redirect::home();
} else {
$this->View->render('register/index');
}
}
/**
* Register page action
* POST-request after form submit
*/
public function register_action()
{
$registration_successful = RegistrationModel::registerNewUser();
if ($registration_successful) {
Redirect::to('login/index');
} else {
Redirect::to('register/index');
}
}
/**
* Verify user after activation mail link opened
* @param int $user_id user's id
* @param string $user_activation_verification_code user's verification token
*/
public function verify($user_id, $user_activation_verification_code)
{
if (isset($user_id) && isset($user_activation_verification_code)) {
RegistrationModel::verifyNewUser($user_id, $user_activation_verification_code);
$this->View->render('register/verify');
} else {
Redirect::to('login/index');
}
}
/**
* 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.
*/
public function showCaptcha()
{
CaptchaModel::generateAndShowCaptcha();
}
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* UserController
* Controls everything that is user-related
*/
class UserController extends Controller
{
/**
* Construct this object by extending the basic Controller class.
*/
public function __construct()
{
parent::__construct();
// VERY IMPORTANT: All controllers/areas that should only be usable by logged-in users
// need this line! Otherwise not-logged in users could do actions.
Auth::checkAuthentication();
}
/**
* Show user's PRIVATE profile
*/
public function index()
{
$this->View->render('user/index', array(
'user_name' => Session::get('user_name'),
'user_email' => Session::get('user_email'),
'user_gravatar_image_url' => Session::get('user_gravatar_image_url'),
'user_avatar_file' => Session::get('user_avatar_file'),
'user_account_type' => Session::get('user_account_type')
));
}
/**
* Show edit-my-username page
*/
public function editUsername()
{
$this->View->render('user/editUsername');
}
/**
* Edit user name (perform the real action after form has been submitted)
*/
public function editUsername_action()
{
// check if csrf token is valid
if (!Csrf::isTokenValid()) {
LoginModel::logout();
Redirect::home();
exit();
}
UserModel::editUserName(Request::post('user_name'));
Redirect::to('user/editUsername');
}
/**
* Show edit-my-user-email page
*/
public function editUserEmail()
{
$this->View->render('user/editUserEmail');
}
/**
* Edit user email (perform the real action after form has been submitted)
*/
// make this POST
public function editUserEmail_action()
{
UserModel::editUserEmail(Request::post('user_email'));
Redirect::to('user/editUserEmail');
}
/**
* Edit avatar
*/
public function editAvatar()
{
$this->View->render('user/editAvatar', array(
'avatar_file_path' => AvatarModel::getPublicUserAvatarFilePathByUserId(Session::get('user_id'))
));
}
/**
* Perform the upload of the avatar
* POST-request
*/
public function uploadAvatar_action()
{
AvatarModel::createAvatar();
Redirect::to('user/editAvatar');
}
/**
* Delete the current user's avatar
*/
public function deleteAvatar_action()
{
AvatarModel::deleteAvatar(Session::get("user_id"));
Redirect::to('user/editAvatar');
}
/**
* Show the change-account-type page
*/
public function changeUserRole()
{
$this->View->render('user/changeUserRole');
}
/**
* Perform the account-type changing
* POST-request
*/
public function changeUserRole_action()
{
if (Request::post('user_account_upgrade')) {
// "2" is quick & dirty account type 2, something like "premium user" maybe. you got the idea :)
UserRoleModel::changeUserRole(2);
}
if (Request::post('user_account_downgrade')) {
// "1" is quick & dirty account type 1, something like "basic user" maybe.
UserRoleModel::changeUserRole(1);
}
Redirect::to('user/changeUserRole');
}
/**
* Password Change Page
*/
public function changePassword()
{
$this->View->render('user/changePassword');
}
/**
* Password Change Action
* Submit form, if retured positive redirect to index, otherwise show the changePassword page again
*/
public function changePassword_action()
{
$result = PasswordResetModel::changePassword(
Session::get('user_name'), Request::post('user_password_current'),
Request::post('user_password_new'), Request::post('user_password_repeat')
);
if($result)
Redirect::to('user/index');
else
Redirect::to('user/changePassword');
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* Class Application
* The heart of the application
*/
class Application
{
/** @var mixed Instance of the controller */
private $controller;
/** @var array URL parameters, will be passed to used controller-method */
private $parameters = array();
/** @var string Just the name of the controller, useful for checks inside the view ("where am I ?") */
private $controller_name;
/** @var string Just the name of the controller's method, useful for checks inside the view ("where am I ?") */
private $action_name;
/**
* Start the application, analyze URL elements, call according controller/method or relocate to fallback location
*/
public function __construct()
{
// create array with URL parts in $url
$this->splitUrl();
// creates controller and action names (from URL input)
$this->createControllerAndActionNames();
// does such a controller exist ?
if (file_exists(Config::get('PATH_CONTROLLER') . $this->controller_name . '.php')) {
// load this file and create this controller
// example: if controller would be "car", then this line would translate into: $this->car = new car();
require Config::get('PATH_CONTROLLER') . $this->controller_name . '.php';
$this->controller = new $this->controller_name();
// check are controller and method existing and callable?
if (is_callable(array($this->controller, $this->action_name))) {
if (!empty($this->parameters)) {
// call the method and pass arguments to it
call_user_func_array(array($this->controller, $this->action_name), $this->parameters);
} else {
// if no parameters are given, just call the method without parameters, like $this->index->index();
$this->controller->{$this->action_name}();
}
} else {
// load 404 error page
require Config::get('PATH_CONTROLLER') . 'ErrorController.php';
$this->controller = new ErrorController;
$this->controller->error404();
}
} else {
// load 404 error page
require Config::get('PATH_CONTROLLER') . 'ErrorController.php';
$this->controller = new ErrorController;
$this->controller->error404();
}
}
/**
* Get and split the URL
*/
private function splitUrl()
{
if (Request::get('url')) {
// split URL
$url = trim(Request::get('url'), '/');
$url = filter_var($url, FILTER_SANITIZE_URL);
$url = explode('/', $url);
// put URL parts into according properties
$this->controller_name = isset($url[0]) ? $url[0] : null;
$this->action_name = isset($url[1]) ? $url[1] : null;
// remove controller name and action name from the split URL
unset($url[0], $url[1]);
// rebase array keys and store the URL parameters
$this->parameters = array_values($url);
}
}
/**
* Checks if controller and action names are given. If not, default values are put into the properties.
* Also renames controller to usable name.
*/
private function createControllerAndActionNames()
{
// check for controller: no controller given ? then make controller = default controller (from config)
if (!$this->controller_name) {
$this->controller_name = Config::get('DEFAULT_CONTROLLER');
}
// check for action: no action given ? then make action = default action (from config)
if (!$this->action_name OR (strlen($this->action_name) == 0)) {
$this->action_name = Config::get('DEFAULT_ACTION');
}
// rename controller name to real controller class/file name ("index" to "IndexController")
$this->controller_name = ucwords($this->controller_name) . 'Controller';
}
}

82
application/core/Auth.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
/**
* Class Auth
* Checks if user is logged in, if not then sends the user to "yourdomain.com/login".
* Auth::checkAuthentication() can be used in the constructor of a controller (to make the
* entire controller only visible for logged-in users) or inside a controller-method to make only this part of the
* application available for logged-in users.
*/
class Auth
{
/**
* The normal authentication flow, just check if the user is logged in (by looking into the session).
* If user is not, then he will be redirected to login page and the application is hard-stopped via exit().
*/
public static function checkAuthentication()
{
// initialize the session (if not initialized yet)
Session::init();
// self::checkSessionConcurrency();
// if user is NOT logged in...
// (if user IS logged in the application will not run the code below and therefore just go on)
if (!Session::userIsLoggedIn()) {
// ... then treat user as "not logged in", destroy session, redirect to login page
Session::destroy();
// send the user to the login form page, but also add the current page's URI (the part after the base URL)
// as a parameter argument, making it possible to send the user back to where he/she came from after a
// successful login
header('location: ' . Config::get('URL') . 'login?redirect=' . urlencode($_SERVER['REQUEST_URI']));
// to prevent fetching views via cURL (which "ignores" the header-redirect above) we leave the application
// the hard way, via exit(). @see https://github.com/panique/php-login/issues/453
// this is not optimal and will be fixed in future releases
exit();
}
}
/**
* The admin authentication flow, just check if the user is logged in (by looking into the session) AND has
* user role type 7 (currently there's only type 1 (normal user), type 2 (premium user) and 7 (admin)).
* If user is not, then he will be redirected to login page and the application is hard-stopped via exit().
* Using this method makes only sense in controllers that should only be used by admins.
*/
public static function checkAdminAuthentication()
{
// initialize the session (if not initialized yet)
Session::init();
// self::checkSessionConcurrency();
// if user is not logged in or is not an admin (= not role type 7)
if (!Session::userIsLoggedIn() || Session::get("user_account_type") != 7) {
// ... then treat user as "not logged in", destroy session, redirect to login page
Session::destroy();
header('location: ' . Config::get('URL') . 'login');
// to prevent fetching views via cURL (which "ignores" the header-redirect above) we leave the application
// the hard way, via exit(). @see https://github.com/panique/php-login/issues/453
// this is not optimal and will be fixed in future releases
exit();
}
}
/**
* Detects if there is concurrent session (i.e. another user logged in with the same current user credentials),
* If so, then logout.
*/
public static function checkSessionConcurrency(){
if(Session::userIsLoggedIn()){
if(Session::isConcurrentSessionExists()){
LoginModel::logout();
Redirect::home();
exit();
}
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
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';
if (!file_exists($config_file)) {
return false;
}
self::$config = require $config_file;
}
return self::$config[$key];
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* This is the "base controller class". All other "real" controllers extend this class.
* Whenever a controller is created, we also
* 1. initialize a session
* 2. check if the user is not logged in anymore (session timeout) but has a cookie
*/
class Controller
{
/** @var View View The view object */
public $View;
/**
* Construct the (base) controller. This happens when a real controller is constructed, like in
* the constructor of IndexController when it says: parent::__construct();
*/
public function __construct()
{
// always initialize a session
Session::init();
// check session concurrency
Auth::checkSessionConcurrency();
// user is not logged in but has remember-me-cookie ? then try to login with cookie ("remember me" feature)
if (!Session::userIsLoggedIn() AND Request::cookie('remember_me')) {
header('location: ' . Config::get('URL') . 'login/loginWithCookie');
}
// create a view object to be able to use it inside a controller, like $this->View->render();
$this->View = new View();
}
}

60
application/core/Csrf.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
/**
* Cross Site Request Forgery Class
*
*/
/**
* Instructions:
*
* At your form, before the submit button put:
* <input type="hidden" name="csrf_token" value="<?= Csrf::makeToken(); ?>" />
*
* This validation needed in the controller action method to validate CSRF token submitted with the form:
*
* if (!Csrf::isTokenValid()) {
* LoginModel::logout();
* Redirect::home();
* exit();
* }
*
* To get simpler code it might be better to put the logout, redirect, exit into an own (static) method.
*/
class Csrf
{
/**
* get CSRF token and generate a new one if expired
*
* @access public
* @static static method
* @return string
*/
public static function makeToken()
{
// token is valid for 1 day
$max_time = 60 * 60 * 24;
$stored_time = Session::get('csrf_token_time');
$csrf_token = Session::get('csrf_token');
if ($max_time + $stored_time <= time() || empty($csrf_token)) {
Session::set('csrf_token', md5(uniqid(rand(), true)));
Session::set('csrf_token_time', time());
}
return Session::get('csrf_token');
}
/**
* checks if CSRF token in session is same as in the form submitted
*
* @access public
* @static static method
* @return bool
*/
public static function isTokenValid()
{
$token = Request::post('csrf_token');
return $token === Session::get('csrf_token') && !empty($token);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Class DatabaseFactory
*
* Use it like this:
* $database = DatabaseFactory::getFactory()->getConnection();
*
* That's my personal favourite when creating a database connection.
* It's a slightly modified version of Jon Raphaelson's excellent answer on StackOverflow:
* http://stackoverflow.com/questions/130878/global-or-singleton-for-database-connection
*
* Full quote from the answer:
*
* "Then, in 6 months when your app is super famous and getting dugg and slashdotted and you decide you need more than
* a single connection, all you have to do is implement some pooling in the getConnection() method. Or if you decide
* that you want a wrapper that implements SQL logging, you can pass a PDO subclass. Or if you decide you want a new
* connection on every invocation, you can do do that. It's flexible, instead of rigid."
*
* Thanks! Big up, mate!
*/
class DatabaseFactory
{
private static $factory;
private $database;
public static function getFactory()
{
if (!self::$factory) {
self::$factory = new DatabaseFactory();
}
return self::$factory;
}
public function getConnection() {
if (!$this->database) {
/**
* Check DB connection in try/catch block. Also when PDO is not constructed properly,
* prevent to exposing database host, username and password in plain text as:
* PDO->__construct('mysql:host=127....', 'root', '12345678', Array)
* by throwing custom error message
*/
try {
$options = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING);
$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
);
} catch (PDOException $e) {
// Echo custom message. Echo error code gives you some info.
echo 'Database connection can not be estabilished. Please try again later.' . '<br>';
echo 'Error code: ' . $e->getCode();
// Stop application :(
// No connection, reached limit connections etc. so no point to keep it running
exit;
}
}
return $this->database;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* Encryption and Decryption Class
*
*/
class Encryption
{
/**
* Cipher algorithm
*
* @var string
*/
const CIPHER = 'aes-256-cbc';
/**
* Hash function
*
* @var string
*/
const HASH_FUNCTION = 'sha256';
/**
* constructor for Encryption object.
*
* @access private
*/
private function __construct()
{
}
/**
* Encrypt a string.
*
* @access public
* @static static method
* @param string $plain
* @return string
* @throws Exception If functions don't exists
*/
public static function encrypt($plain)
{
if (!function_exists('openssl_cipher_iv_length') ||
!function_exists('openssl_random_pseudo_bytes') ||
!function_exists('openssl_encrypt')) {
throw new Exception('Encryption function doesn\'t exist');
}
// generate initialization vector,
// this will make $iv different every time,
// so, encrypted string will be also different.
$iv_size = openssl_cipher_iv_length(self::CIPHER);
$iv = openssl_random_pseudo_bytes($iv_size);
// generate key for authentication using ENCRYPTION_KEY & HMAC_SALT
$key = mb_substr(hash(self::HASH_FUNCTION, Config::get('ENCRYPTION_KEY') . Config::get('HMAC_SALT')), 0, 32, '8bit');
// append initialization vector
$encrypted_string = openssl_encrypt($plain, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
$ciphertext = $iv . $encrypted_string;
// apply the HMAC
$hmac = hash_hmac('sha256', $ciphertext, $key);
return $hmac . $ciphertext;
}
/**
* Decrypted a string.
*
* @access public
* @static static method
* @param string $ciphertext
* @return string
* @throws Exception If $ciphertext is empty, or If functions don't exists
*/
public static function decrypt($ciphertext)
{
if (empty($ciphertext)) {
throw new Exception('The String to decrypt can\'t be empty');
}
if (!function_exists('openssl_cipher_iv_length') ||
!function_exists('openssl_decrypt')) {
throw new Exception('Encryption function doesn\'t exist');
}
// generate key used for authentication using ENCRYPTION_KEY & HMAC_SALT
$key = mb_substr(hash(self::HASH_FUNCTION, Config::get('ENCRYPTION_KEY') . Config::get('HMAC_SALT')), 0, 32, '8bit');
// split cipher into: hmac, cipher & iv
$macSize = 64;
$hmac = mb_substr($ciphertext, 0, $macSize, '8bit');
$iv_cipher = mb_substr($ciphertext, $macSize, null, '8bit');
// generate original hmac & compare it with the one in $ciphertext
$originalHmac = hash_hmac('sha256', $iv_cipher, $key);
if (!self::hashEquals($hmac, $originalHmac)) {
return false;
}
// split out the initialization vector and cipher
$iv_size = openssl_cipher_iv_length(self::CIPHER);
$iv = mb_substr($iv_cipher, 0, $iv_size, '8bit');
$cipher = mb_substr($iv_cipher, $iv_size, null, '8bit');
return openssl_decrypt($cipher, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
}
/**
* A timing attack resistant comparison.
*
* @access private
* @static static method
* @param string $hmac The hmac from the ciphertext being decrypted.
* @param string $compare The comparison hmac.
* @return bool
* @see https://github.com/sarciszewski/php-future/blob/bd6c91fb924b2b35a3e4f4074a642868bd051baf/src/Security.php#L36
*/
private static function hashEquals($hmac, $compare)
{
if (function_exists('hash_equals')) {
return hash_equals($hmac, $compare);
}
// if hash_equals() is not available,
// then use the following snippet.
// It's equivalent to hash_equals() in PHP 5.6.
$hashLength = mb_strlen($hmac, '8bit');
$compareLength = mb_strlen($compare, '8bit');
if ($hashLength !== $compareLength) {
return false;
}
$result = 0;
for ($i = 0; $i < $hashLength; $i++) {
$result |= (ord($hmac[$i]) ^ ord($compare[$i]));
}
return $result === 0;
}
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* Class Environment
*
* Extremely simple way to get the environment, everywhere inside your application.
* Extend this the way you want.
*/
class Environment
{
public static function get()
{
// if APPLICATION_ENV constant exists (set in Apache configs)
// then return content of APPLICATION_ENV
// else return "development"
return (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : "development");
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* Class Filter
*
* This is the place to put filters, usually methods that cleans, sorts and, well, filters stuff.
*/
class Filter
{
/**
* The XSS filter: This simply removes "code" from any data, used to prevent Cross-Site Scripting Attacks.
*
* A very simple introduction: Let's say an attackers changes its username from "John" to these lines:
* "<script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>"
* This means, every user's browser would render "John" anymore, instead interpreting this JavaScript code, calling
* the delete.php, in this case inside the project, in worse scenarios something like performing a bank transaction
* or sending your cookie data (containing your remember-me-token) to somebody else.
*
* What is XSS ?
* @see http://phpsecurity.readthedocs.org/en/latest/Cross-Site-Scripting-%28XSS%29.html
*
* Deeper information:
* @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
*
* XSSFilter expects a value, checks if the value is a string, and if so, encodes typical script tag chars to
* harmless HTML (you'll see the code, it wil not be interpreted). Then the method checks if the value is an array,
* or an object and if so, makes sure all its string content is encoded (recursive call on its values).
* Note that this method uses reference to the assed variable, not a copy, meaning you can use this methods like this:
*
* CORRECT: Filter::XSSFilter($myVariable);
* WRONG: $myVariable = Filter::XSSFilter($myVariable);
*
* This works like some other popular PHP functions, for example sort().
* @see http://php.net/manual/en/function.sort.php
*
* @see http://stackoverflow.com/questions/1676897/what-does-it-mean-to-start-a-php-function-with-an-ampersand
* @see http://php.net/manual/en/language.references.pass.php
*
* FYI: htmlspecialchars() does this (from PHP docs):
*
* '&' (ampersand) becomes '&amp;'
* '"' (double quote) becomes '&quot;' when ENT_NOQUOTES is not set.
* "'" (single quote) becomes '&#039;' (or &apos;) only when ENT_QUOTES is set.
* '<' (less than) becomes '&lt;'
* '>' (greater than) becomes '&gt;'
*
* @see http://www.php.net/manual/en/function.htmlspecialchars.php
*
* @param $value The value to be filtered
* @return mixed
*/
public static function XSSFilter(&$value)
{
// if argument is a string, filters that string
if (is_string($value)) {
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
// if argument is an array or an object,
// recursivly filters its content
} else if (is_array($value) || is_object($value)) {
/**
* Make sure the element is passed by reference,
* In PHP 7, foreach does not use the internal array pointer.
* In order to be able to directly modify array elements within the loop
* precede $value with &. In that case the value will be assigned by reference.
* @see http://php.net/manual/en/control-structures.foreach.php
*/
foreach ($value as &$valueInValue) {
self::XSSFilter($valueInValue);
}
}
// other types are untouched
return $value;
}
}

154
application/core/Mail.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
/* Using PHPMailer's namespace */
use PHPMailer\PHPMailer\PHPMailer;
/**
* Class Mail
*
* Handles everything regarding mail-sending.
*/
class Mail
{
/** @var mixed variable to collect errors */
private $error;
/**
* Try to send a mail by using PHP's native mail() function.
* Please note that not PHP itself will send a mail, it's just a wrapper for Linux's sendmail or other mail tools
*
* Good guideline on how to send mails natively with mail():
* @see http://stackoverflow.com/a/24644450/1114320
* @see http://www.php.net/manual/en/function.mail.php
*/
public function sendMailWithNativeMailFunction()
{
// no code yet, so we just return something to make IDEs and code analyzer tools happy
return false;
}
/**
* Try to send a mail by using SwiftMailer.
* Make sure you have loaded SwiftMailer via Composer.
*
* @return bool
*/
public function sendMailWithSwiftMailer()
{
// no code yet, so we just return something to make IDEs and code analyzer tools happy
return false;
}
/**
* Try to send a mail by using PHPMailer.
* Make sure you have loaded PHPMailer via Composer.
* Depending on your EMAIL_USE_SMTP setting this will work via SMTP credentials or via native mail()
*
* @param $user_email
* @param $from_email
* @param $from_name
* @param $subject
* @param $body
*
* @return bool
* @throws Exception
* @throws phpmailerException
*/
public function sendMailWithPHPMailer($user_email, $from_email, $from_name, $subject, $body)
{
$mail = new PHPMailer;
// you should use UTF-8 to avoid encoding issues
$mail->CharSet = 'UTF-8';
// if you want to send mail via PHPMailer using SMTP credentials
if (Config::get('EMAIL_USE_SMTP')) {
// set PHPMailer to use SMTP
$mail->IsSMTP();
// 0 = off, 1 = commands, 2 = commands and data, perfect to see SMTP errors
$mail->SMTPDebug = 0;
// enable SMTP authentication
$mail->SMTPAuth = Config::get('EMAIL_SMTP_AUTH');
// encryption
if (Config::get('EMAIL_SMTP_ENCRYPTION')) {
$mail->SMTPSecure = Config::get('EMAIL_SMTP_ENCRYPTION');
}
// set SMTP provider's credentials
$mail->Host = Config::get('EMAIL_SMTP_HOST');
$mail->Username = Config::get('EMAIL_SMTP_USERNAME');
$mail->Password = Config::get('EMAIL_SMTP_PASSWORD');
$mail->Port = Config::get('EMAIL_SMTP_PORT');
} else {
$mail->IsMail();
}
// fill mail with data
$mail->From = $from_email;
$mail->FromName = $from_name;
$mail->AddAddress($user_email);
$mail->Subject = $subject;
$mail->Body = $body;
// try to send mail, put result status (true/false into $wasSendingSuccessful)
// I'm unsure if mail->send really returns true or false every time, tis method in PHPMailer is quite complex
$wasSendingSuccessful = $mail->Send();
if ($wasSendingSuccessful) {
return true;
} else {
// if not successful, copy errors into Mail's error property
$this->error = $mail->ErrorInfo;
return false;
}
}
/**
* The main mail sending method, this simply calls a certain mail sending method depending on which mail provider
* you've selected in the application's config.
*
* @param $user_email string email
* @param $from_email string sender's email
* @param $from_name string sender's name
* @param $subject string subject
* @param $body string full mail body text
* @return bool the success status of the according mail sending method
*/
public function sendMail($user_email, $from_email, $from_name, $subject, $body)
{
if (Config::get('EMAIL_USED_MAILER') == "phpmailer") {
// returns true if successful, false if not
return $this->sendMailWithPHPMailer(
$user_email, $from_email, $from_name, $subject, $body
);
}
if (Config::get('EMAIL_USED_MAILER') == "swiftmailer") {
return $this->sendMailWithSwiftMailer();
}
if (Config::get('EMAIL_USED_MAILER') == "native") {
return $this->sendMailWithNativeMailFunction();
}
}
/**
* The different mail sending methods write errors to the error property $this->error,
* this method simply returns this error / error array.
*
* @return mixed
*/
public function getError()
{
return $this->error;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Class Redirect
*
* Simple abstraction for redirecting the user to a certain page
*/
class Redirect
{
/**
* To the last visited page before user logged in (useful when people are on a certain page inside your application
* and then want to log in (to edit or comment something for example) and don't to be redirected to the main page).
*
* This is just a bulletproof version of Redirect::to(), redirecting to an ABSOLUTE URL path like
* "http://www.mydomain.com/user/profile", useful as people had problems with the RELATIVE URL path generated
* by Redirect::to() when using HUGE inside sub-folders.
*
* @param $path string
*/
public static function toPreviousViewedPageAfterLogin($path)
{
header('location: http://' . $_SERVER['HTTP_HOST'] . '/' . $path);
}
/**
* To the homepage
*/
public static function home()
{
header("location: " . Config::get('URL'));
}
/**
* To the defined page, uses a relative path (like "user/profile")
*
* Redirects to a RELATIVE path, like "user/profile" (which works very fine unless you are using HUGE inside tricky
* sub-folder structures)
*
* @see https://github.com/panique/huge/issues/770
* @see https://github.com/panique/huge/issues/754
*
* @param $path string
*/
public static function to($path)
{
header("location: " . Config::get('URL') . $path);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* This is under development. Expect changes!
* Class Request
* Abstracts the access to $_GET, $_POST and $_COOKIE, preventing direct access to these super-globals.
* This makes PHP code quality analyzer tools very happy.
* @see http://php.net/manual/en/reserved.variables.request.php
*/
class Request
{
/**
* Gets/returns the value of a specific key of the POST super-global.
* When using just Request::post('x') it will return the raw and untouched $_POST['x'], when using it like
* Request::post('x', true) then it will return a trimmed and stripped $_POST['x'] !
*
* @param mixed $key key
* @param bool $clean marker for optional cleaning of the var
* @return mixed the key's value or nothing
*/
public static function post($key, $clean = false)
{
if (isset($_POST[$key])) {
// we use the Ternary Operator here which saves the if/else block
// @see http://davidwalsh.name/php-shorthand-if-else-ternary-operators
return ($clean) ? trim(strip_tags($_POST[$key])) : $_POST[$key];
}
}
/**
* Returns the state of a checkbox.
*
* @param mixed $key key
* @return mixed state of the checkbox
*/
public static function postCheckbox($key)
{
return isset($_POST[$key]) ? 1 : null;
}
/**
* gets/returns the value of a specific key of the GET super-global
* @param mixed $key key
* @return mixed the key's value or nothing
*/
public static function get($key)
{
if (isset($_GET[$key])) {
return $_GET[$key];
}
}
/**
* gets/returns the value of a specific key of the COOKIE super-global
* @param mixed $key key
* @return mixed the key's value or nothing
*/
public static function cookie($key)
{
if (isset($_COOKIE[$key])) {
return $_COOKIE[$key];
}
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* Session class
*
* handles the session stuff. creates session when no one exists, sets and gets values, and closes the session
* properly (=logout). Not to forget the check if the user is logged in or not.
*/
class Session
{
/**
* starts the session
*/
public static function init()
{
// if no session exist, start the session
if (session_id() == '') {
session_start();
}
}
/**
* sets a specific value to a specific key of the session
*
* @param mixed $key key
* @param mixed $value value
*/
public static function set($key, $value)
{
$_SESSION[$key] = $value;
}
/**
* gets/returns the value of a specific key of the session
*
* @param mixed $key Usually a string, right ?
* @return mixed the key's value or nothing
*/
public static function get($key)
{
if (isset($_SESSION[$key])) {
$value = $_SESSION[$key];
// filter the value for XSS vulnerabilities
return Filter::XSSFilter($value);
}
}
/**
* adds a value as a new array element to the key.
* useful for collecting error messages etc
*
* @param mixed $key
* @param mixed $value
*/
public static function add($key, $value)
{
$_SESSION[$key][] = $value;
}
/**
* deletes the session (= logs the user out)
*/
public static function destroy()
{
session_destroy();
}
/**
* update session id in database
*
* @access public
* @static static method
* @param string $userId
* @param string $sessionId
*/
public static function updateSessionId($userId, $sessionId = null)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET session_id = :session_id WHERE user_id = :user_id";
$query = $database->prepare($sql);
$query->execute(array(':session_id' => $sessionId, ":user_id" => $userId));
}
/**
* checks for session concurrency
*
* This is done as the following:
* UserA logs in with his session id('123') and it will be stored in the database.
* Then, UserB logs in also using the same email and password of UserA from another PC,
* and also store the session id('456') in the database
*
* Now, Whenever UserA performs any action,
* You then check the session_id() against the last one stored in the database('456'),
* If they don't match then log both of them out.
*
* @access public
* @static static method
* @return bool
* @see Session::updateSessionId()
* @see http://stackoverflow.com/questions/6126285/php-stop-concurrent-user-logins
*/
public static function isConcurrentSessionExists()
{
$session_id = session_id();
$userId = Session::get('user_id');
if (isset($userId) && isset($session_id)) {
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT session_id FROM users WHERE user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(":user_id" => $userId));
$result = $query->fetch();
$userSessionId = !empty($result)? $result->session_id: null;
return $session_id !== $userSessionId;
}
return false;
}
/**
* Checks if the user is logged in or not
*
* @return bool user's login status
*/
public static function userIsLoggedIn()
{
return (self::get('user_logged_in') ? true : false);
}
}

32
application/core/Text.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
class Text
{
private static $texts;
public static function get($key, $data = null)
{
// 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');
}
// check if array key exists
if (!array_key_exists($key, self::$texts)) {
return null;
}
return self::$texts[$key];
}
}

177
application/core/View.php Normal file
View File

@@ -0,0 +1,177 @@
<?php
/**
* Class View
* The part that handles all the output
*/
class View
{
/**
* 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.
* Usually the Class and the method are the same like the view, but sometimes you need to show different views.
* @param string $filename Path of the to-be-rendered view, usually folder/file(.php)
* @param array $data Data to be used in the view
*/
public function render($filename, $data = null)
{
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . '_templates/header.php';
require Config::get('PATH_VIEW') . $filename . '.php';
require Config::get('PATH_VIEW') . '_templates/footer.php';
}
/**
* Similar to render, but accepts an array of separate views to render between the header and footer. Use like
* the following: $this->view->renderMulti(array('help/index', 'help/banner'));
* @param array $filenames Array of the paths of the to-be-rendered view, usually folder/file(.php) for each
* @param array $data Data to be used in the view
* @return bool
*/
public function renderMulti($filenames, $data = null)
{
if (!is_array($filenames)) {
self::render($filenames, $data);
return false;
}
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . '_templates/header.php';
foreach($filenames as $filename) {
require Config::get('PATH_VIEW') . $filename . '.php';
}
require Config::get('PATH_VIEW') . '_templates/footer.php';
}
/**
* Same like render(), but does not include header and footer
* @param string $filename Path of the to-be-rendered view, usually folder/file(.php)
* @param mixed $data Data to be used in the view
*/
public function renderWithoutHeaderAndFooter($filename, $data = null)
{
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . $filename . '.php';
}
/**
* Renders pure JSON to the browser, useful for API construction
* @param $data
*/
public function renderJSON($data)
{
header("Content-Type: application/json");
echo json_encode($data);
}
/**
* renders the feedback messages into the view
*/
public function renderFeedbackMessages()
{
// echo out the feedback messages (errors and success messages etc.),
// they are in $_SESSION["feedback_positive"] and $_SESSION["feedback_negative"]
require Config::get('PATH_VIEW') . '_templates/feedback.php';
// delete these messages (as they are not needed anymore and we want to avoid to show them twice
Session::set('feedback_positive', null);
Session::set('feedback_negative', null);
}
/**
* Checks if the passed string is the currently active controller.
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_controller
*
* @return bool Shows if the controller is used or not
*/
public static function checkForActiveController($filename, $navigation_controller)
{
$split_filename = explode("/", $filename);
$active_controller = $split_filename[0];
if ($active_controller == $navigation_controller) {
return true;
}
return false;
}
/**
* Checks if the passed string is the currently active controller-action (=method).
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_action
*
* @return bool Shows if the action/method is used or not
*/
public static function checkForActiveAction($filename, $navigation_action)
{
$split_filename = explode("/", $filename);
$active_action = $split_filename[1];
if ($active_action == $navigation_action) {
return true;
}
return false;
}
/**
* Checks if the passed string is the currently active controller and controller-action.
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_controller_and_action
*
* @return bool
*/
public static function checkForActiveControllerAndAction($filename, $navigation_controller_and_action)
{
$split_filename = explode("/", $filename);
$active_controller = $split_filename[0];
$active_action = $split_filename[1];
$split_filename = explode("/", $navigation_controller_and_action);
$navigation_controller = $split_filename[0];
$navigation_action = $split_filename[1];
if ($active_controller == $navigation_controller AND $active_action == $navigation_action) {
return true;
}
return false;
}
/**
* Converts characters to HTML entities
* This is important to avoid XSS attacks, and attempts to inject malicious code in your page.
*
* @param string $str The string.
* @return string
*/
public function encodeHTML($str)
{
return htmlentities($str, ENT_QUOTES, 'UTF-8');
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* Handles all data manipulation of the admin part
*/
class AdminModel
{
/**
* Sets the deletion and suspension values
*
* @param $suspensionInDays
* @param $softDelete
* @param $userId
*/
public static function setAccountSuspensionAndDeletionStatus($suspensionInDays, $softDelete, $userId)
{
// Prevent to suspend or delete own account.
// If admin suspend or delete own account will not be able to do any action.
if ($userId == Session::get('user_id')) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_CANT_DELETE_SUSPEND_OWN'));
return false;
}
if ($suspensionInDays > 0) {
$suspensionTime = time() + ($suspensionInDays * 60 * 60 * 24);
} else {
$suspensionTime = null;
}
// FYI "on" is what a checkbox delivers by default when submitted. Didn't know that for a long time :)
if ($softDelete == "on") {
$delete = 1;
} else {
$delete = 0;
}
// write the above info to the database
self::writeDeleteAndSuspensionInfoToDatabase($userId, $suspensionTime, $delete);
// if suspension or deletion should happen, then also kick user out of the application instantly by resetting
// the user's session :)
if ($suspensionTime != null OR $delete = 1) {
self::resetUserSession($userId);
}
}
/**
* Simply write the deletion and suspension info for the user into the database, also puts feedback into session
*
* @param $userId
* @param $suspensionTime
* @param $delete
* @return bool
*/
private static function writeDeleteAndSuspensionInfoToDatabase($userId, $suspensionTime, $delete)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_suspension_timestamp = :user_suspension_timestamp, user_deleted = :user_deleted WHERE user_id = :user_id LIMIT 1");
$query->execute(array(
':user_suspension_timestamp' => $suspensionTime,
':user_deleted' => $delete,
':user_id' => $userId
));
if ($query->rowCount() == 1) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_SUSPENSION_DELETION_STATUS'));
return true;
}
}
/**
* Kicks the selected user out of the system instantly by resetting the user's session.
* This means, the user will be "logged out".
*
* @param $userId
* @return bool
*/
private static function resetUserSession($userId)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET session_id = :session_id WHERE user_id = :user_id LIMIT 1");
$query->execute(array(
':session_id' => null,
':user_id' => $userId
));
if ($query->rowCount() == 1) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_USER_SUCCESSFULLY_KICKED'));
return true;
}
}
}

View File

@@ -0,0 +1,258 @@
<?php
class AvatarModel
{
/**
* Gets a gravatar image link from given email address
*
* Gravatar is the #1 (free) provider for email address based global avatar hosting.
* The URL (or image) returns always a .jpg file ! For deeper info on the different parameter possibilities:
* @see http://gravatar.com/site/implement/images/
* @source http://gravatar.com/site/implement/images/php/
*
* This method will return something like http://www.gravatar.com/avatar/79e2e5b48aec07710c08d50?s=80&d=mm&r=g
* Note: the url does NOT have something like ".jpg" ! It works without.
*
* Set the configs inside the application/config/ files.
*
* @param string $email The email address
* @return string
*/
public static function getGravatarLinkByEmail($email)
{
return 'http://www.gravatar.com/avatar/' .
md5(strtolower(trim($email))) .
'?s=' . Config::get('AVATAR_SIZE') . '&d=' . Config::get('GRAVATAR_DEFAULT_IMAGESET') . '&r=' . Config::get('GRAVATAR_RATING');
}
/**
* Gets the user's avatar file path
* @param int $user_has_avatar Marker from database
* @param int $user_id User's id
* @return string Avatar file path
*/
public static function getPublicAvatarFilePathOfUser($user_has_avatar, $user_id)
{
if ($user_has_avatar) {
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . $user_id . '.jpg';
}
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . Config::get('AVATAR_DEFAULT_IMAGE');
}
/**
* Gets the user's avatar file path
* @param $user_id integer The user's id
* @return string avatar picture path
*/
public static function getPublicUserAvatarFilePathByUserId($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_has_avatar FROM users WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_id' => $user_id));
if ($query->fetch()->user_has_avatar) {
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . $user_id . '.jpg';
}
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . Config::get('AVATAR_DEFAULT_IMAGE');
}
/**
* Create an avatar picture (and checks all necessary things too)
* TODO decouple
* TODO total rebuild
*/
public static function createAvatar()
{
// check avatar folder writing rights, check if upload fits all rules
if (self::isAvatarFolderWritable() AND self::validateImageFile()) {
// create a jpg file in the avatar folder, write marker to database
$target_file_path = Config::get('PATH_AVATARS') . Session::get('user_id');
self::resizeAvatarImage($_FILES['avatar_file']['tmp_name'], $target_file_path, Config::get('AVATAR_SIZE'), Config::get('AVATAR_SIZE'));
self::writeAvatarToDatabase(Session::get('user_id'));
Session::set('user_avatar_file', self::getPublicUserAvatarFilePathByUserId(Session::get('user_id')));
Session::add('feedback_positive', Text::get('FEEDBACK_AVATAR_UPLOAD_SUCCESSFUL'));
}
}
/**
* Checks if the avatar folder exists and is writable
*
* @return bool success status
*/
public static function isAvatarFolderWritable()
{
if (is_dir(Config::get('PATH_AVATARS')) AND is_writable(Config::get('PATH_AVATARS'))) {
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_FOLDER_DOES_NOT_EXIST_OR_NOT_WRITABLE'));
return false;
}
/**
* Validates the image
* Only accepts gif, jpg, png types
* @see http://php.net/manual/en/function.image-type-to-mime-type.php
*
* @return bool
*/
public static function validateImageFile()
{
if (!isset($_FILES['avatar_file'])) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_IMAGE_UPLOAD_FAILED'));
return false;
}
// if input file too big (>5MB)
if ($_FILES['avatar_file']['size'] > 5000000) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_TOO_BIG'));
return false;
}
// get the image width, height and mime type
$image_proportions = getimagesize($_FILES['avatar_file']['tmp_name']);
// if input file too small, [0] is the width, [1] is the height
if ($image_proportions[0] < Config::get('AVATAR_SIZE') OR $image_proportions[1] < Config::get('AVATAR_SIZE')) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_TOO_SMALL'));
return false;
}
// if file type is not jpg, gif or png
if (!in_array($image_proportions['mime'], array('image/jpeg', 'image/gif', 'image/png'))) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_WRONG_TYPE'));
return false;
}
return true;
}
/**
* Writes marker to database, saying user has an avatar now
*
* @param $user_id
*/
public static function writeAvatarToDatabase($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_has_avatar = TRUE WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_id' => $user_id));
}
/**
* Resize avatar image (while keeping aspect ratio and cropping it off in a clean way).
* Only works with gif, jpg and png file types. If you want to change this also have a look into
* method validateImageFile() inside this model.
*
* TROUBLESHOOTING: You don't see the new image ? Press F5 or CTRL-F5 to refresh browser cache.
*
* @param string $source_image The location to the original raw image
* @param string $destination The location to save the new image
* @param int $final_width The desired width of the new image
* @param int $final_height The desired height of the new image
*
* @return bool success state
*/
public static function resizeAvatarImage($source_image, $destination, $final_width = 44, $final_height = 44)
{
$imageData = getimagesize($source_image);
$width = $imageData[0];
$height = $imageData[1];
$mimeType = $imageData['mime'];
if (!$width || !$height) {
return false;
}
switch ($mimeType) {
case 'image/jpeg': $myImage = imagecreatefromjpeg($source_image); break;
case 'image/png': $myImage = imagecreatefrompng($source_image); break;
case 'image/gif': $myImage = imagecreatefromgif($source_image); break;
default: return false;
}
// calculating the part of the image to use for thumbnail
if ($width > $height) {
$verticalCoordinateOfSource = 0;
$horizontalCoordinateOfSource = ($width - $height) / 2;
$smallestSide = $height;
} else {
$horizontalCoordinateOfSource = 0;
$verticalCoordinateOfSource = ($height - $width) / 2;
$smallestSide = $width;
}
// copying the part into thumbnail, maybe edit this for square avatars
$thumb = imagecreatetruecolor($final_width, $final_height);
imagecopyresampled($thumb, $myImage, 0, 0, $horizontalCoordinateOfSource, $verticalCoordinateOfSource, $final_width, $final_height, $smallestSide, $smallestSide);
// add '.jpg' to file path, save it as a .jpg file with our $destination_filename parameter
imagejpeg($thumb, $destination . '.jpg', Config::get('AVATAR_JPEG_QUALITY'));
imagedestroy($thumb);
if (file_exists($destination)) {
return true;
}
return false;
}
/**
* Delete a user's avatar
*
* @param int $userId
* @return bool success
*/
public static function deleteAvatar($userId)
{
if (!ctype_digit($userId)) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
// try to delete image, but still go on regardless of file deletion result
self::deleteAvatarImageFile($userId);
$database = DatabaseFactory::getFactory()->getConnection();
$sth = $database->prepare("UPDATE users SET user_has_avatar = 0 WHERE user_id = :user_id LIMIT 1");
$sth->bindValue(":user_id", (int)$userId, PDO::PARAM_INT);
$sth->execute();
if ($sth->rowCount() == 1) {
Session::set('user_avatar_file', self::getPublicUserAvatarFilePathByUserId($userId));
Session::add("feedback_positive", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_SUCCESSFUL"));
return true;
} else {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
}
/**
* Removes the avatar image file from the filesystem
*
* @param integer $userId
* @return bool
*/
public static function deleteAvatarImageFile($userId)
{
// Check if file exists
if (!file_exists(Config::get('PATH_AVATARS') . $userId . ".jpg")) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_NO_FILE"));
return false;
}
// Delete avatar file
if (!unlink(Config::get('PATH_AVATARS') . $userId . ".jpg")) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Class CaptchaModel
*
* This model class handles all the captcha stuff.
* Currently this uses the excellent Captcha generator lib from https://github.com/Gregwar/Captcha
* Have a look there for more options etc.
*/
class CaptchaModel
{
/**
* Generates the captcha, "returns" a real image, this is why there is header('Content-type: image/jpeg')
* Note: This is a very special method, as this is echoes out binary data.
*/
public static function generateAndShowCaptcha()
{
// create a captcha with the CaptchaBuilder lib (loaded via Composer)
$captcha = new Gregwar\Captcha\CaptchaBuilder;
$captcha->build(
Config::get('CAPTCHA_WIDTH'),
Config::get('CAPTCHA_HEIGHT')
);
// write the captcha character into session
Session::set('captcha', $captcha->getPhrase());
// render an image showing the characters (=the captcha)
header('Content-type: image/jpeg');
$captcha->output();
}
/**
* Checks if the entered captcha is the same like the one from the rendered image which has been saved in session
* @param $captcha string The captcha characters
* @return bool success of captcha check
*/
public static function checkCaptcha($captcha)
{
if (Session::get('captcha') && ($captcha == Session::get('captcha'))) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,382 @@
<?php
/**
* LoginModel
*
* The login part of the model: Handles the login / logout stuff
*/
class LoginModel
{
/**
* Login process (for DEFAULT user accounts).
*
* @param $user_name string The user's name
* @param $user_password string The user's password
* @param $set_remember_me_cookie mixed Marker for usage of remember-me cookie feature
*
* @return bool success state
*/
public static function login($user_name, $user_password, $set_remember_me_cookie = null)
{
// we do negative-first checks here, for simplicity empty username and empty password in one line
if (empty($user_name) OR empty($user_password)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_FIELD_EMPTY'));
return false;
}
// checks if user exists, if login is not blocked (due to failed logins) and if password fits the hash
$result = self::validateAndGetUser($user_name, $user_password);
// check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details.
if (!$result) {
//No Need to give feedback here since whole validateAndGetUser controls gives a feedback
return false;
}
// stop the user's login if account has been soft deleted
if ($result->user_deleted == 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_DELETED'));
return false;
}
// stop the user from logging in if user has a suspension, display how long they have left in the feedback.
if ($result->user_suspension_timestamp != null && $result->user_suspension_timestamp - time() > 0) {
$suspensionTimer = Text::get('FEEDBACK_ACCOUNT_SUSPENDED') . round(abs($result->user_suspension_timestamp - time())/60/60, 2) . " hours left";
Session::add('feedback_negative', $suspensionTimer);
return false;
}
// reset the failed login counter for that user (if necessary)
if ($result->user_last_failed_login > 0) {
self::resetFailedLoginCounterOfUser($result->user_name);
}
// save timestamp of this login in the database line of that user
self::saveTimestampOfLoginOfUser($result->user_name);
// if user has checked the "remember me" checkbox, then write token into database and into cookie
if ($set_remember_me_cookie) {
self::setRememberMeInDatabaseAndCookie($result->user_id);
}
// successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true
self::setSuccessfulLoginIntoSession(
$result->user_id, $result->user_name, $result->user_email, $result->user_account_type
);
// return true to make clear the login was successful
// maybe do this in dependence of setSuccessfulLoginIntoSession ?
return true;
}
/**
* Validates the inputs of the users, checks if password is correct etc.
* If successful, user is returned
*
* @param $user_name
* @param $user_password
*
* @return bool|mixed
*/
private static function validateAndGetUser($user_name, $user_password)
{
// brute force attack mitigation: use session failed login count and last failed login for not found users.
// block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago
// (limits user searches in database)
if (Session::get('failed-login-count') >= 3 AND (Session::get('last-failed-login') > (time() - 30))) {
Session::add('feedback_negative', Text::get('FEEDBACK_LOGIN_FAILED_3_TIMES'));
return false;
}
// get all data of that user (to later check if password and password_hash fit)
$result = UserModel::getUserDataByUsername($user_name);
// check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details.
// brute force attack mitigation: reset failed login counter because of found user
if (!$result) {
// increment the user not found count, helps mitigate user enumeration
self::incrementUserNotFoundCounter();
// user does not exist, but we won't to give a potential attacker this details, so we just use a basic feedback message
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG'));
return false;
}
// block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago
if (($result->user_failed_logins >= 3) AND ($result->user_last_failed_login > (time() - 30))) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_WRONG_3_TIMES'));
return false;
}
// if hash of provided password does NOT match the hash in the database: +1 failed-login counter
if (!password_verify($user_password, $result->user_password_hash)) {
self::incrementFailedLoginCounterOfUser($result->user_name);
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG'));
return false;
}
// if user is not active (= has not verified account by verification mail)
if ($result->user_active != 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_NOT_ACTIVATED_YET'));
return false;
}
// reset the user not found counter
self::resetUserNotFoundCounter();
return $result;
}
/**
* Reset the failed-login-count to 0.
* Reset the last-failed-login to an empty string.
*/
private static function resetUserNotFoundCounter()
{
Session::set('failed-login-count', 0);
Session::set('last-failed-login', '');
}
/**
* Increment the failed-login-count by 1.
* Add timestamp to last-failed-login.
*/
private static function incrementUserNotFoundCounter()
{
// Username enumeration prevention: set session failed login count and last failed login for users not found
Session::set('failed-login-count', Session::get('failed-login-count') + 1);
Session::set('last-failed-login', time());
}
/**
* performs the login via cookie (for DEFAULT user account, FACEBOOK-accounts are handled differently)
* TODO add throttling here ?
*
* @param $cookie string The cookie "remember_me"
*
* @return bool success state
*/
public static function loginWithCookie($cookie)
{
// do we have a cookie ?
if (!$cookie) {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
// before list(), check it can be split into 3 strings.
if (count (explode(':', $cookie)) !== 3) {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
// check cookie's contents, check if cookie contents belong together or token is empty
list ($user_id, $token, $hash) = explode(':', $cookie);
// decrypt user id
$user_id = Encryption::decrypt($user_id);
if ($hash !== hash('sha256', $user_id . ':' . $token) OR empty($token) OR empty($user_id)) {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
// get data of user that has this id and this token
$result = UserModel::getUserDataByUserIdAndToken($user_id, $token);
// if user with that id and exactly that cookie token exists in database
if ($result) {
// successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true
self::setSuccessfulLoginIntoSession($result->user_id, $result->user_name, $result->user_email, $result->user_account_type);
// save timestamp of this login in the database line of that user
self::saveTimestampOfLoginOfUser($result->user_name);
// NOTE: we don't set another remember_me-cookie here as the current cookie should always
// be invalid after a certain amount of time, so the user has to login with username/password
// again from time to time. This is good and safe ! ;)
Session::add('feedback_positive', Text::get('FEEDBACK_COOKIE_LOGIN_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
}
/**
* Log out process: delete cookie, delete session
*/
public static function logout()
{
$user_id = Session::get('user_id');
self::deleteCookie($user_id);
Session::destroy();
Session::updateSessionId($user_id);
}
/**
* The real login process: The user's data is written into the session.
* Cheesy name, maybe rename. Also maybe refactoring this, using an array.
*
* @param $user_id
* @param $user_name
* @param $user_email
* @param $user_account_type
*/
public static function setSuccessfulLoginIntoSession($user_id, $user_name, $user_email, $user_account_type)
{
Session::init();
// remove old and regenerate session ID.
// It's important to regenerate session on sensitive actions,
// and to avoid fixated session.
// e.g. when a user logs in
session_regenerate_id(true);
$_SESSION = array();
Session::set('user_id', $user_id);
Session::set('user_name', $user_name);
Session::set('user_email', $user_email);
Session::set('user_account_type', $user_account_type);
Session::set('user_provider_type', 'DEFAULT');
// get and set avatars
Session::set('user_avatar_file', AvatarModel::getPublicUserAvatarFilePathByUserId($user_id));
Session::set('user_gravatar_image_url', AvatarModel::getGravatarLinkByEmail($user_email));
// finally, set user as logged-in
Session::set('user_logged_in', true);
// update session id in database
Session::updateSessionId($user_id, session_id());
// set session cookie setting manually,
// Why? because you need to explicitly set session expiry, path, domain, secure, and HTTP.
// @see https://www.owasp.org/index.php/PHP_Security_Cheat_Sheet#Cookies
setcookie(session_name(), session_id(), time() + Config::get('SESSION_RUNTIME'), Config::get('COOKIE_PATH'),
Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
}
/**
* Increments the failed-login counter of a user
*
* @param $user_name
*/
public static function incrementFailedLoginCounterOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_failed_logins = user_failed_logins+1, user_last_failed_login = :user_last_failed_login
WHERE user_name = :user_name OR user_email = :user_name
LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name, ':user_last_failed_login' => time() ));
}
/**
* Resets the failed-login counter of a user back to 0
*
* @param $user_name
*/
public static function resetFailedLoginCounterOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_failed_logins = 0, user_last_failed_login = NULL
WHERE user_name = :user_name AND user_failed_logins != 0
LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name));
}
/**
* Write timestamp of this login into database (we only write a "real" login via login form into the database,
* not the session-login on every page request
*
* @param $user_name
*/
public static function saveTimestampOfLoginOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_last_login_timestamp = :user_last_login_timestamp
WHERE user_name = :user_name LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name, ':user_last_login_timestamp' => time()));
}
/**
* Write remember-me token into database and into cookie
* Maybe splitting this into database and cookie part ?
*
* @param $user_id
*/
public static function setRememberMeInDatabaseAndCookie($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
// generate 64 char random string
$random_token_string = hash('sha256', mt_rand());
// write that token into database
$sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_remember_me_token' => $random_token_string, ':user_id' => $user_id));
// generate cookie string that consists of user id, random string and combined hash of both
// never expose the original user id, instead, encrypt it.
$cookie_string_first_part = Encryption::encrypt($user_id) . ':' . $random_token_string;
$cookie_string_hash = hash('sha256', $user_id . ':' . $random_token_string);
$cookie_string = $cookie_string_first_part . ':' . $cookie_string_hash;
// set cookie, and make it available only for the domain created on (to avoid XSS attacks, where the
// attacker could steal your remember-me cookie string and would login itself).
// If you are using HTTPS, then you should set the "secure" flag (the second one from right) to true, too.
// @see http://www.php.net/manual/en/function.setcookie.php
setcookie('remember_me', $cookie_string, time() + Config::get('COOKIE_RUNTIME'), Config::get('COOKIE_PATH'),
Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
}
/**
* Deletes the cookie
* It's necessary to split deleteCookie() and logout() as cookies are deleted without logging out too!
* Sets the remember-me-cookie to ten years ago (3600sec * 24 hours * 365 days * 10).
* that's obviously the best practice to kill a cookie @see http://stackoverflow.com/a/686166/1114320
*
* @param string $user_id
*/
public static function deleteCookie($user_id = null)
{
// is $user_id was set, then clear remember_me token in database
if (isset($user_id)) {
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_remember_me_token' => null, ':user_id' => $user_id));
}
// delete remember_me cookie in browser
setcookie('remember_me', false, time() - (3600 * 24 * 3650), Config::get('COOKIE_PATH'),
Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
}
/**
* Returns the current state of the user's login
*
* @return bool user's login status
*/
public static function isUserLoggedIn()
{
return Session::userIsLoggedIn();
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* NoteModel
* This is basically a simple CRUD (Create/Read/Update/Delete) demonstration.
*/
class NoteModel
{
/**
* Get all notes (notes are just example data that the user has created)
* @return array an array with several objects (the results)
*/
public static function getAllNotes()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => Session::get('user_id')));
// fetchAll() is the PDO method that gets all result rows
return $query->fetchAll();
}
/**
* Get a single note
* @param int $note_id id of the specific note
* @return object a single object (the result)
*/
public static function getNote($note_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$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();
}
/**
* Set a note (create a new one)
* @param string $note_text note text that will be created
* @return bool feedback (was the note created properly ?)
*/
public static function createNote($note_text)
{
if (!$note_text || strlen($note_text) == 0) {
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_CREATION_FAILED'));
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "INSERT INTO notes (note_text, user_id) VALUES (:note_text, :user_id)";
$query = $database->prepare($sql);
$query->execute(array(':note_text' => $note_text, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
// default return
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_CREATION_FAILED'));
return false;
}
/**
* Update an existing note
* @param int $note_id id of the specific note
* @param string $note_text new text of the specific note
* @return bool feedback (was the update successful ?)
*/
public static function updateNote($note_id, $note_text)
{
if (!$note_id || !$note_text) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE notes SET note_text = :note_text WHERE note_id = :note_id AND user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':note_id' => $note_id, ':note_text' => $note_text, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_EDITING_FAILED'));
return false;
}
/**
* Delete a specific note
* @param int $note_id id of the note
* @return bool feedback (was the note deleted properly ?)
*/
public static function deleteNote($note_id)
{
if (!$note_id) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "DELETE FROM notes WHERE note_id = :note_id AND user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':note_id' => $note_id, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
// default return
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_DELETION_FAILED'));
return false;
}
}

View File

@@ -0,0 +1,365 @@
<?php
/**
* Class PasswordResetModel
*
* Handles all the stuff that is related to the password-reset process
*/
class PasswordResetModel
{
/**
* Perform the necessary actions to send a password reset mail
*
* @param $user_name_or_email string Username or user's email
* @param $captcha string Captcha string
*
* @return bool success status
*/
public static function requestPasswordReset($user_name_or_email, $captcha)
{
if (!CaptchaModel::checkCaptcha($captcha)) {
Session::add('feedback_negative', Text::get('FEEDBACK_CAPTCHA_WRONG'));
return false;
}
if (empty($user_name_or_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_EMAIL_FIELD_EMPTY'));
return false;
}
// check if that username exists
$result = UserModel::getUserDataByUserNameOrEmail($user_name_or_email);
if (!$result) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
return false;
}
// generate integer-timestamp (to see when exactly the user (or an attacker) requested the password reset mail)
// generate random hash for email password reset verification (40 bytes)
$temporary_timestamp = time();
$user_password_reset_hash = bin2hex(random_bytes(40));
// set token (= a random hash string and a timestamp) into database ...
$token_set = self::setPasswordResetDatabaseToken($result->user_name, $user_password_reset_hash, $temporary_timestamp);
if (!$token_set) {
return false;
}
// ... and send a mail to the user, containing a link with username and token hash string
$mail_sent = self::sendPasswordResetMail($result->user_name, $user_password_reset_hash, $result->user_email);
if ($mail_sent) {
return true;
}
// default return
return false;
}
/**
* Set password reset token in database (for DEFAULT user accounts)
*
* @param string $user_name username
* @param string $user_password_reset_hash password reset hash
* @param int $temporary_timestamp timestamp
*
* @return bool success status
*/
public static function setPasswordResetDatabaseToken($user_name, $user_password_reset_hash, $temporary_timestamp)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_password_reset_hash = :user_password_reset_hash, user_password_reset_timestamp = :user_password_reset_timestamp
WHERE user_name = :user_name AND user_provider_type = :provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_reset_hash' => $user_password_reset_hash, ':user_name' => $user_name,
':user_password_reset_timestamp' => $temporary_timestamp, ':provider_type' => 'DEFAULT'
));
// check if exactly one row was successfully changed
if ($query->rowCount() == 1) {
return true;
}
// fallback
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_FAIL'));
return false;
}
/**
* Send the password reset mail
*
* @param string $user_name username
* @param string $user_password_reset_hash password reset hash
* @param string $user_email user email
*
* @return bool success status
*/
public static function sendPasswordResetMail($user_name, $user_password_reset_hash, $user_email)
{
// create email body
$body = Config::get('EMAIL_PASSWORD_RESET_CONTENT') . ' ' . Config::get('URL') .
Config::get('EMAIL_PASSWORD_RESET_URL') . '/' . urlencode($user_name) . '/' . urlencode($user_password_reset_hash);
// create instance of Mail class, try sending and check
$mail = new Mail;
$mail_sent = $mail->sendMail($user_email, Config::get('EMAIL_PASSWORD_RESET_FROM_EMAIL'),
Config::get('EMAIL_PASSWORD_RESET_FROM_NAME'), Config::get('EMAIL_PASSWORD_RESET_SUBJECT'), $body
);
if ($mail_sent) {
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_RESET_MAIL_SENDING_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_MAIL_SENDING_ERROR') . $mail->getError() );
return false;
}
/**
* Verifies the password reset request via the verification hash token (that's only valid for one hour)
* @param string $user_name Username
* @param string $verification_code Hash token
* @return bool Success status
*/
public static function verifyPasswordReset($user_name, $verification_code)
{
$database = DatabaseFactory::getFactory()->getConnection();
// check if user-provided username + verification code combination exists
$sql = "SELECT user_id, user_password_reset_timestamp
FROM users
WHERE user_name = :user_name
AND user_password_reset_hash = :user_password_reset_hash
AND user_provider_type = :user_provider_type
LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_reset_hash' => $verification_code, ':user_name' => $user_name,
':user_provider_type' => 'DEFAULT'
));
// if this user with exactly this verification hash code does NOT exist
if ($query->rowCount() != 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_COMBINATION_DOES_NOT_EXIST'));
return false;
}
// get result row (as an object)
$result_user_row = $query->fetch();
// 3600 seconds are 1 hour
$timestamp_one_hour_ago = time() - 3600;
// if password reset request was sent within the last hour (this timeout is for security reasons)
if ($result_user_row->user_password_reset_timestamp > $timestamp_one_hour_ago) {
// verification was successful
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_RESET_LINK_VALID'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_LINK_EXPIRED'));
return false;
}
}
/**
* Writes the new password to the database
*
* @param string $user_name username
* @param string $user_password_hash
* @param string $user_password_reset_hash
*
* @return bool
*/
public static function saveNewUserPassword($user_name, $user_password_hash, $user_password_reset_hash)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_password_hash = :user_password_hash, user_password_reset_hash = NULL,
user_password_reset_timestamp = NULL
WHERE user_name = :user_name AND user_password_reset_hash = :user_password_reset_hash
AND user_provider_type = :user_provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_hash' => $user_password_hash, ':user_name' => $user_name,
':user_password_reset_hash' => $user_password_reset_hash, ':user_provider_type' => 'DEFAULT'
));
// if one result exists, return true, else false. Could be written even shorter btw.
return ($query->rowCount() == 1 ? true : false);
}
/**
* Set the new password (for DEFAULT user, FACEBOOK-users don't have a password)
* Please note: At this point the user has already pre-verified via verifyPasswordReset() (within one hour),
* so we don't need to check again for the 60min-limit here. In this method we authenticate
* via username & password-reset-hash from (hidden) form fields.
*
* @param string $user_name
* @param string $user_password_reset_hash
* @param string $user_password_new
* @param string $user_password_repeat
*
* @return bool success state of the password reset
*/
public static function setNewPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)
{
// validate the password
if (!self::validateResetPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)) {
return false;
}
// crypt the password (with the PHP 5.5+'s password_hash() function, result is a 60 character hash string)
$user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT);
// write the password to database (as hashed and salted string), reset user_password_reset_hash
if (self::saveNewUserPassword($user_name, $user_password_hash, $user_password_reset_hash)) {
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CHANGE_FAILED'));
return false;
}
}
/**
* Validate the password submission
*
* @param $user_name
* @param $user_password_reset_hash
* @param $user_password_new
* @param $user_password_repeat
*
* @return bool
*/
public static function validateResetPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)
{
if (empty($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_FIELD_EMPTY'));
return false;
} else if (empty($user_password_reset_hash)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_MISSING'));
return false;
} else if (empty($user_password_new) || empty($user_password_repeat)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY'));
return false;
} else if ($user_password_new !== $user_password_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG'));
return false;
} else if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
}
return true;
}
/**
* Writes the new password to the database
*
* @param string $user_name
* @param string $user_password_hash
*
* @return bool
*/
public static function saveChangedPassword($user_name, $user_password_hash)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_password_hash = :user_password_hash
WHERE user_name = :user_name
AND user_provider_type = :user_provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_hash' => $user_password_hash, ':user_name' => $user_name,
':user_provider_type' => 'DEFAULT'
));
// if one result exists, return true, else false. Could be written even shorter btw.
return ($query->rowCount() == 1 ? true : false);
}
/**
* Validates fields, hashes new password, saves new password
*
* @param string $user_name
* @param string $user_password_current
* @param string $user_password_new
* @param string $user_password_repeat
*
* @return bool
*/
public static function changePassword($user_name, $user_password_current, $user_password_new, $user_password_repeat)
{
// validate the passwords
if (!self::validatePasswordChange($user_name, $user_password_current, $user_password_new, $user_password_repeat)) {
return false;
}
// crypt the password (with the PHP 5.5+'s password_hash() function, result is a 60 character hash string)
$user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT);
// write the password to database (as hashed and salted string)
if (self::saveChangedPassword($user_name, $user_password_hash)) {
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CHANGE_FAILED'));
return false;
}
}
/**
* Validates current and new passwords
*
* @param string $user_name
* @param string $user_password_current
* @param string $user_password_new
* @param string $user_password_repeat
*
* @return bool
*/
public static function validatePasswordChange($user_name, $user_password_current, $user_password_new, $user_password_repeat)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_password_hash, user_failed_logins FROM users WHERE user_name = :user_name LIMIT 1;";
$query = $database->prepare($sql);
$query->execute(array(
':user_name' => $user_name
));
$user = $query->fetch();
if ($query->rowCount() == 1) {
$user_password_hash = $user->user_password_hash;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
return false;
}
if (!password_verify($user_password_current, $user_password_hash)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CURRENT_INCORRECT'));
return false;
} else if (empty($user_password_new) || empty($user_password_repeat)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY'));
return false;
} else if ($user_password_new !== $user_password_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG'));
return false;
} else if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
} else if ($user_password_current == $user_password_new){
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_NEW_SAME_AS_CURRENT'));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,293 @@
<?php
/**
* Class RegistrationModel
*
* Everything registration-related happens here.
*/
class RegistrationModel
{
/**
* Handles the entire registration process for DEFAULT users (not for people who register with
* 3rd party services, like facebook) and creates a new user in the database if everything is fine
*
* @return boolean Gives back the success status of the registration
*/
public static function registerNewUser()
{
// 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');
// 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;
}
// 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
$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));
// 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)
{
$return = true;
// perform all necessary checks
if (!CaptchaModel::checkCaptcha($captcha)) {
Session::add('feedback_negative', Text::get('FEEDBACK_CAPTCHA_WRONG'));
$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;
}
/**
* Validates the username
*
* @param $user_name
* @return bool
*/
public static function validateUserName($user_name)
{
if (empty($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_FIELD_EMPTY'));
return false;
}
// if username is too short (2), too long (64) or does not fit the pattern (aZ09)
if (!preg_match('/^[a-zA-Z0-9]{2,64}$/', $user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN'));
return false;
}
return true;
}
/**
* Validates the email
*
* @param $user_email
* @param $user_email_repeat
* @return bool
*/
public static function validateUserEmail($user_email, $user_email_repeat)
{
if (empty($user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_FIELD_EMPTY'));
return false;
}
if ($user_email !== $user_email_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_REPEAT_WRONG'));
return false;
}
// validate the email with PHP's internal filter
// side-fact: Max length seems to be 254 chars
// @see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
if (!filter_var($user_email, FILTER_VALIDATE_EMAIL)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN'));
return false;
}
return true;
}
/**
* Validates the password
*
* @param $user_password_new
* @param $user_password_repeat
* @return bool
*/
public static function validateUserPassword($user_password_new, $user_password_repeat)
{
if (empty($user_password_new) OR empty($user_password_repeat)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY'));
return false;
}
if ($user_password_new !== $user_password_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG'));
return false;
}
if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
}
return true;
}
/**
* Writes the new user's data to the database
*
* @param $user_name
* @param $user_password_hash
* @param $user_email
* @param $user_creation_timestamp
* @param $user_activation_hash
*
* @return bool
*/
public static function writeNewUserToDatabase($user_name, $user_password_hash, $user_email, $user_creation_timestamp, $user_activation_hash)
{
$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)";
$query = $database->prepare($sql);
$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'));
$count = $query->rowCount();
if ($count == 1) {
return true;
}
return false;
}
/**
* Deletes the user from users table. Currently used to rollback a registration when verification mail sending
* was not successful.
*
* @param $user_id
*/
public static function rollbackRegistrationByUserId($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("DELETE FROM users WHERE user_id = :user_id");
$query->execute(array(':user_id' => $user_id));
}
/**
* Sends the verification email (to confirm the account).
* The construction of the mail $body looks weird at first, but it's really just a simple string.
*
* @param int $user_id user's id
* @param string $user_email user's email
* @param string $user_activation_hash user's mail verification hash string
*
* @return boolean gives back true if mail has been sent, gives back false if no mail could been sent
*/
public static function sendVerificationEmail($user_id, $user_email, $user_activation_hash)
{
$body = Config::get('EMAIL_VERIFICATION_CONTENT') . Config::get('URL') . Config::get('EMAIL_VERIFICATION_URL')
. '/' . urlencode($user_id) . '/' . urlencode($user_activation_hash);
$mail = new Mail;
$mail_sent = $mail->sendMail($user_email, Config::get('EMAIL_VERIFICATION_FROM_EMAIL'),
Config::get('EMAIL_VERIFICATION_FROM_NAME'), Config::get('EMAIL_VERIFICATION_SUBJECT'), $body
);
if ($mail_sent) {
Session::add('feedback_positive', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_ERROR') . $mail->getError() );
return false;
}
}
/**
* checks the email/verification code combination and set the user's activation status to true in the database
*
* @param int $user_id user id
* @param string $user_activation_verification_code verification token
*
* @return bool success status
*/
public static function verifyNewUser($user_id, $user_activation_verification_code)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_active = 1, user_activation_hash = NULL
WHERE user_id = :user_id AND user_activation_hash = :user_activation_hash LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id, ':user_activation_hash' => $user_activation_verification_code));
if ($query->rowCount() == 1) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_ACTIVATION_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_ACTIVATION_FAILED'));
return false;
}
}

View File

@@ -0,0 +1,343 @@
<?php
/**
* UserModel
* Handles all the PUBLIC profile stuff. This is not for getting data of the logged in user, it's more for handling
* data of all the other users. Useful for display profile information, creating user lists etc.
*/
class UserModel
{
/**
* Gets an array that contains all the users in the database. The array's keys are the user ids.
* Each array element is an object, containing a specific user's data.
* The avatar line is built using Ternary Operators, have a look here for more:
* @see http://davidwalsh.name/php-shorthand-if-else-ternary-operators
*
* @return array The profiles of all users
*/
public static function getPublicProfilesOfAllUsers()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_active, user_has_avatar, user_deleted FROM users";
$query = $database->prepare($sql);
$query->execute();
$all_users_profiles = array();
foreach ($query->fetchAll() as $user) {
// all elements of array passed to Filter::XSSFilter for XSS sanitation, have a look into
// application/core/Filter.php for more info on how to use. Removes (possibly bad) JavaScript etc from
// the user's values
array_walk_recursive($user, 'Filter::XSSFilter');
$all_users_profiles[$user->user_id] = new stdClass();
$all_users_profiles[$user->user_id]->user_id = $user->user_id;
$all_users_profiles[$user->user_id]->user_name = $user->user_name;
$all_users_profiles[$user->user_id]->user_email = $user->user_email;
$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));
}
return $all_users_profiles;
}
/**
* Gets a user's profile data, according to the given $user_id
* @param int $user_id The user's id
* @return mixed The selected user's profile
*/
public static function getPublicProfileOfUser($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_active, user_has_avatar, user_deleted
FROM users WHERE user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id));
$user = $query->fetch();
if ($query->rowCount() == 1) {
if (Config::get('USE_GRAVATAR')) {
$user->user_avatar_link = AvatarModel::getGravatarLinkByEmail($user->user_email);
} else {
$user->user_avatar_link = AvatarModel::getPublicAvatarFilePathOfUser($user->user_has_avatar, $user->user_id);
}
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
}
// all elements of array passed to Filter::XSSFilter for XSS sanitation, have a look into
// application/core/Filter.php for more info on how to use. Removes (possibly bad) JavaScript etc from
// the user's values
array_walk_recursive($user, 'Filter::XSSFilter');
return $user;
}
/**
* @param $user_name_or_email
*
* @return mixed
*/
public static function getUserDataByUserNameOrEmail($user_name_or_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id, user_name, user_email FROM users
WHERE (user_name = :user_name_or_email OR user_email = :user_name_or_email)
AND user_provider_type = :provider_type LIMIT 1");
$query->execute(array(':user_name_or_email' => $user_name_or_email, ':provider_type' => 'DEFAULT'));
return $query->fetch();
}
/**
* Checks if a username is already taken
*
* @param $user_name string username
*
* @return bool
*/
public static function doesUsernameAlreadyExist($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id FROM users WHERE user_name = :user_name LIMIT 1");
$query->execute(array(':user_name' => $user_name));
if ($query->rowCount() == 0) {
return false;
}
return true;
}
/**
* Checks if a email is already used
*
* @param $user_email string email
*
* @return bool
*/
public static function doesEmailAlreadyExist($user_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id FROM users WHERE user_email = :user_email LIMIT 1");
$query->execute(array(':user_email' => $user_email));
if ($query->rowCount() == 0) {
return false;
}
return true;
}
/**
* Writes new username to database
*
* @param $user_id int user id
* @param $new_user_name string new username
*
* @return bool
*/
public static function saveNewUserName($user_id, $new_user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_name = :user_name WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_name' => $new_user_name, ':user_id' => $user_id));
if ($query->rowCount() == 1) {
return true;
}
return false;
}
/**
* Writes new email address to database
*
* @param $user_id int user id
* @param $new_user_email string new email address
*
* @return bool
*/
public static function saveNewEmailAddress($user_id, $new_user_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_email = :user_email WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_email' => $new_user_email, ':user_id' => $user_id));
$count = $query->rowCount();
if ($count == 1) {
return true;
}
return false;
}
/**
* Edit the user's name, provided in the editing form
*
* @param $new_user_name string The new username
*
* @return bool success status
*/
public static function editUserName($new_user_name)
{
// new username same as old one ?
if ($new_user_name == Session::get('user_name')) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_SAME_AS_OLD_ONE'));
return false;
}
// username cannot be empty and must be azAZ09 and 2-64 characters
if (!preg_match("/^[a-zA-Z0-9]{2,64}$/", $new_user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN'));
return false;
}
// clean the input, strip usernames longer than 64 chars (maybe fix this ?)
$new_user_name = substr(strip_tags($new_user_name), 0, 64);
// check if new username already exists
if (self::doesUsernameAlreadyExist($new_user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN'));
return false;
}
$status_of_action = self::saveNewUserName(Session::get('user_id'), $new_user_name);
if ($status_of_action) {
Session::set('user_name', $new_user_name);
Session::add('feedback_positive', Text::get('FEEDBACK_USERNAME_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
}
/**
* Edit the user's email
*
* @param $new_user_email
*
* @return bool success status
*/
public static function editUserEmail($new_user_email)
{
// email provided ?
if (empty($new_user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_FIELD_EMPTY'));
return false;
}
// check if new email is same like the old one
if ($new_user_email == Session::get('user_email')) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_SAME_AS_OLD_ONE'));
return false;
}
// user's email must be in valid email format, also checks the length
// @see http://stackoverflow.com/questions/21631366/php-filter-validate-email-max-length
// @see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
if (!filter_var($new_user_email, FILTER_VALIDATE_EMAIL)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN'));
return false;
}
// strip tags, just to be sure
$new_user_email = substr(strip_tags($new_user_email), 0, 254);
// check if user's email already exists
if (self::doesEmailAlreadyExist($new_user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_EMAIL_ALREADY_TAKEN'));
return false;
}
// write to database, if successful ...
// ... then write new email to session, Gravatar too (as this relies to the user's email address)
if (self::saveNewEmailAddress(Session::get('user_id'), $new_user_email)) {
Session::set('user_email', $new_user_email);
Session::set('user_gravatar_image_url', AvatarModel::getGravatarLinkByEmail($new_user_email));
Session::add('feedback_positive', Text::get('FEEDBACK_EMAIL_CHANGE_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
/**
* Gets the user's id
*
* @param $user_name
*
* @return mixed
*/
public static function getUserIdByUsername($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id FROM users WHERE user_name = :user_name AND user_provider_type = :provider_type LIMIT 1";
$query = $database->prepare($sql);
// DEFAULT is the marker for "normal" accounts (that have a password etc.)
// There are other types of accounts that don't have passwords etc. (FACEBOOK)
$query->execute(array(':user_name' => $user_name, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch()->user_id;
}
/**
* Gets the user's data
*
* @param $user_name string User's name
*
* @return mixed Returns false if user does not exist, returns object with user's data when user exists
*/
public static function getUserDataByUsername($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_password_hash, user_active,user_deleted, user_suspension_timestamp, user_account_type,
user_failed_logins, user_last_failed_login
FROM users
WHERE (user_name = :user_name OR user_email = :user_name)
AND user_provider_type = :provider_type
LIMIT 1";
$query = $database->prepare($sql);
// DEFAULT is the marker for "normal" accounts (that have a password etc.)
// There are other types of accounts that don't have passwords etc. (FACEBOOK)
$query->execute(array(':user_name' => $user_name, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch();
}
/**
* Gets the user's data by user's id and a token (used by login-via-cookie process)
*
* @param $user_id
* @param $token
*
* @return mixed Returns false if user does not exist, returns object with user's data when user exists
*/
public static function getUserDataByUserIdAndToken($user_id, $token)
{
$database = DatabaseFactory::getFactory()->getConnection();
// get real token from database (and all other data)
$query = $database->prepare("SELECT user_id, user_name, user_email, user_password_hash, user_active,
user_account_type, user_has_avatar, user_failed_logins, user_last_failed_login
FROM users
WHERE user_id = :user_id
AND user_remember_me_token = :user_remember_me_token
AND user_remember_me_token IS NOT NULL
AND user_provider_type = :provider_type LIMIT 1");
$query->execute(array(':user_id' => $user_id, ':user_remember_me_token' => $token, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch();
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* Class UserRoleModel
*
* This class contains everything that is related to up- and downgrading accounts.
*/
class UserRoleModel
{
/**
* Upgrades / downgrades the user's account. Currently it's just the field user_account_type in the database that
* can be 1 or 2 (maybe "basic" or "premium"). Put some more complex stuff in here, maybe a pay-process or whatever
* you like.
*
* @param $type
*
* @return bool
*/
public static function changeUserRole($type)
{
if (!$type) {
return false;
}
// save new role to database
if (self::saveRoleToDatabase($type)) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_TYPE_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_TYPE_CHANGE_FAILED'));
return false;
}
}
/**
* Writes the new account type marker to the database and to the session
*
* @param $type
*
* @return bool
*/
public static function saveRoleToDatabase($type)
{
// if $type is not 1 or 2
if (!in_array($type, [1, 2])) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_account_type = :new_type WHERE user_id = :user_id LIMIT 1");
$query->execute(array(
':new_type' => $type,
':user_id' => Session::get('user_id')
));
if ($query->rowCount() == 1) {
// set account type in session
Session::set('user_account_type', $type);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,19 @@
<?php
// get the feedback (they are arrays, to make multiple positive/negative messages possible)
$feedback_positive = Session::get('feedback_positive');
$feedback_negative = Session::get('feedback_negative');
// echo out positive messages
if (isset($feedback_positive)) {
foreach ($feedback_positive as $feedback) {
echo '<div class="feedback success">'.$feedback.'</div>';
}
}
// echo out negative messages
if (isset($feedback_negative)) {
foreach ($feedback_negative as $feedback) {
echo '<div class="feedback error">'.$feedback.'</div>';
}
}

View File

@@ -0,0 +1,7 @@
<div class="footer"></div>
</div><!-- close class="wrapper" -->
<!-- the support button on the top right -->
<a class="support-button" href="https://affiliates.a2hosting.com/idevaffiliate.php?id=4471&url=579" target="_blank"></a>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!doctype html>
<html>
<head>
<title>HUGE</title>
<!-- META -->
<meta charset="utf-8">
<!-- send empty favicon fallback to prevent user's browser hitting the server for lots of favicon requests resulting in 404s -->
<link rel="icon" href="data:;base64,=">
<!-- CSS -->
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" />
</head>
<body>
<!-- wrapper, to center website -->
<div class="wrapper">
<!-- logo -->
<div class="logo"></div>
<!-- navigation -->
<ul class="navigation">
<li <?php if (View::checkForActiveController($filename, "index")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>index/index">Index</a>
</li>
<li <?php if (View::checkForActiveController($filename, "profile")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>profile/index">Profiles</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>
</li>
<li <?php if (View::checkForActiveController($filename, "note")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>note/index">My Notes</a>
</li>
<?php } else { ?>
<!-- for not logged in users -->
<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>
<!-- my account -->
<ul class="navigation right">
<?php if (Session::userIsLoggedIn()) : ?>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/index">My Account</a>
<ul class="navigation-submenu">
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/changeUserRole">Change account type</a>
</li>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/editAvatar">Edit your avatar</a>
</li>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/editusername">Edit my username</a>
</li>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/edituseremail">Edit my email</a>
</li>
<li <?php if (View::checkForActiveController($filename, "user")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>user/changePassword">Change Password</a>
</li>
<li <?php if (View::checkForActiveController($filename, "login")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/logout">Logout</a>
</li>
</ul>
</li>
<?php if (Session::get("user_account_type") == 7) : ?>
<li <?php if (View::checkForActiveController($filename, "admin")) {
echo ' class="active" ';
} ?> >
<a href="<?php echo Config::get('URL'); ?>admin/">Admin</a>
</li>
<?php endif; ?>
<?php endif; ?>
</ul>

View File

@@ -0,0 +1,57 @@
<div class="container">
<h1>Admin/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<div>
This controller/action/view shows a list of all users in the system. with the ability to soft delete a user
or suspend a user.
</div>
<div>
<table class="overview-table">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Username</td>
<td>User's email</td>
<td>Activated ?</td>
<td>Link to user's profile</td>
<td>suspension Time in days</td>
<td>Soft delete</td>
<td>Submit</td>
</tr>
</thead>
<?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 ? 'No' : 'Yes'); ?></td>
<td>
<a href="<?= Config::get('URL') . 'profile/showProfile/' . $user->user_id; ?>">Profile</a>
</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>
<td>
<input type="hidden" name="user_id" value="<?= $user->user_id; ?>" />
<input type="submit" />
</td>
</form>
</tr>
<?php } ?>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<div class="container">
<h1>DashboardController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is an area that's only visible for logged in users. Try to log out, an go to /dashboard/ again. You'll
be redirected to /index/ as you are not logged in. You can protect a whole section in your app within the
according controller by placing <i>Auth::handleLogin();</i> into the constructor.
<p>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<div class="container">
<h1>404 - Page not found</h1>
<div class="box">
<p class="red-text">This page does not exist.</p>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<div class="container">
<h1>IndexController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is the homepage. As no real URL-route (like /register/index) is provided, the app uses the default
controller and the default action, defined in application/config/config.php, by default it's
IndexController and index()-method. So, the app will load application/controller/IndexController.php and
run index() from that file. Easy. That index()-method (= the action) has just one line of code inside
($this->view->render('index/index');) that loads application/view/index/index.php, which is basically
this text you are reading right now.
</p>
</div>
</div>

View File

@@ -0,0 +1,50 @@
<div class="container">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="login-page-box">
<div class="table-wrapper">
<!-- login box on left side -->
<div class="login-box">
<h2>Login here</h2>
<form action="<?php echo Config::get('URL'); ?>login/login" method="post">
<input type="text" name="user_name" placeholder="Username or email" required />
<input type="password" name="user_password" placeholder="Password" required />
<label for="set_remember_me_cookie" class="remember-me-label">
<input type="checkbox" name="set_remember_me_cookie" class="remember-me-checkbox" />
Remember me for 2 weeks
</label>
<!-- when a user navigates to a page that's only accessible for logged a logged-in user, then
the user is sent to this page here, also having the page he/she came from in the URL parameter
(have a look). This "where did you came from" value is put into this form to sent the user back
there after being logged in successfully.
Simple but powerful feature, big thanks to @tysonlist. -->
<?php if (!empty($this->redirect)) { ?>
<input type="hidden" name="redirect" value="<?php echo $this->encodeHTML($this->redirect); ?>" />
<?php } ?>
<!--
set CSRF token in login form, although sending fake login requests mightn't be interesting gap here.
If you want to get deeper, check these answers:
1. natevw's http://stackoverflow.com/questions/6412813/do-login-forms-need-tokens-against-csrf-attacks?rq=1
2. http://stackoverflow.com/questions/15602473/is-csrf-protection-necessary-on-a-sign-up-form?lq=1
3. http://stackoverflow.com/questions/13667437/how-to-add-csrf-token-to-login-form?lq=1
-->
<input type="hidden" name="csrf_token" value="<?= Csrf::makeToken(); ?>" />
<input type="submit" class="login-submit-button" value="Log in"/>
</form>
<div class="link-forgot-my-password">
<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

@@ -0,0 +1,35 @@
<div class="container">
<h1>Request a password reset</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<!-- request password reset form box -->
<form method="post" action="<?php echo Config::get('URL'); ?>login/requestPasswordReset_action">
<label for="user_name_or_email">
Enter your username or email and you'll get a mail with instructions:
<input type="text" name="user_name_or_email" required />
</label>
<!-- 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" /><br/>
<input type="text" name="captcha" placeholder="Enter captcha above" required />
<!-- quick & dirty captcha reloader -->
<a href="#" style="display: block; font-size: 11px; margin: 5px 0 15px 0;"
onclick="document.getElementById('captcha').src = '<?php echo Config::get('URL'); ?>register/showCaptcha?' + Math.random(); return false">Reload Captcha</a>
<input type="submit" value="Send me a password-reset mail" />
</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>

View File

@@ -0,0 +1,27 @@
<div class="container">
<h1>LoginController/resetPassword</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Set new password</h2>
<p>FYI: ... Idenfitication process works via password-reset-token (hidden input field)</p>
<!-- new password form box -->
<form method="post" action="<?php echo Config::get('URL'); ?>login/setNewPassword" name="new_password_form">
<input type='hidden' name='user_name' value='<?php echo $this->user_name; ?>' />
<input type='hidden' name='user_password_reset_hash' value='<?php echo $this->user_password_reset_hash; ?>' />
<label for="reset_input_password_new">New password (min. 6 characters)</label>
<input id="reset_input_password_new" class="reset_input" type="password"
name="user_password_new" pattern=".{6,}" required autocomplete="off" />
<label for="reset_input_password_repeat">Repeat new password</label>
<input id="reset_input_password_repeat" class="reset_input" type="password"
name="user_password_repeat" pattern=".{6,}" required autocomplete="off" />
<input type="submit" name="submit_new_password" value="Submit new password" />
</form>
<a href="<?php echo Config::get('URL'); ?>login/index">Back to Login Page</a>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="container">
<h1>NoteController/edit/:note_id</h1>
<div class="box">
<h2>Edit a note</h2>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<?php if ($this->note) { ?>
<form method="post" action="<?php echo Config::get('URL'); ?>note/editSave">
<label>Change text of note: </label>
<!-- we use htmlentities() here to prevent user input with " etc. break the HTML -->
<input type="hidden" name="note_id" value="<?php echo htmlentities($this->note->note_id); ?>" />
<input type="text" name="note_text" value="<?php echo htmlentities($this->note->note_text); ?>" />
<input type="submit" value='Change' />
</form>
<?php } else { ?>
<p>This note does not exist.</p>
<?php } ?>
</div>
</div>

View File

@@ -0,0 +1,44 @@
<div class="container">
<h1>NoteController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is just a simple CRUD implementation. Creating, reading, updating and deleting things.
</p>
<p>
<form method="post" action="<?php echo Config::get('URL');?>note/create">
<label>Text of new note: </label><input type="text" name="note_text" />
<input type="submit" value='Create this note' autocomplete="off" />
</form>
</p>
<?php if ($this->notes) { ?>
<table class="note-table">
<thead>
<tr>
<td>Id</td>
<td>Note</td>
<td>EDIT</td>
<td>DELETE</td>
</tr>
</thead>
<tbody>
<?php foreach($this->notes as $key => $value) { ?>
<tr>
<td><?= $value->note_id; ?></td>
<td><?= htmlentities($value->note_text); ?></td>
<td><a href="<?= Config::get('URL') . 'note/edit/' . $value->note_id; ?>">Edit</a></td>
<td><a href="<?= Config::get('URL') . 'note/delete/' . $value->note_id; ?>">Delete</a></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } else { ?>
<div>No notes yet. Create some !</div>
<?php } ?>
</div>
</div>

View File

@@ -0,0 +1,44 @@
<div class="container">
<h1>ProfileController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<div>
This controller/action/view shows a list of all users in the system. You could use the underlying code to
build things that use profile information of one or multiple/all users.
</div>
<div>
<table class="overview-table">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Username</td>
<td>User's email</td>
<td>Activated ?</td>
<td>Link to user's profile</td>
</tr>
</thead>
<?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 ? 'No' : 'Yes'); ?></td>
<td>
<a href="<?= Config::get('URL') . 'profile/showProfile/' . $user->user_id; ?>">Profile</a>
</td>
</tr>
<?php } ?>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,41 @@
<div class="container">
<h1>ProfileController/showProfile/:id</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<div>This controller/action/view shows all public information about a certain user.</div>
<?php if ($this->user) { ?>
<div>
<table class="overview-table">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Username</td>
<td>User's email</td>
<td>Activated ?</td>
</tr>
</thead>
<tbody>
<tr class="<?= ($this->user->user_active == 0 ? 'inactive' : 'active'); ?>">
<td><?= $this->user->user_id; ?></td>
<td class="avatar">
<?php if (isset($this->user->user_avatar_link)) { ?>
<img src="<?= $this->user->user_avatar_link; ?>" />
<?php } ?>
</td>
<td><?= $this->user->user_name; ?></td>
<td><?= $this->user->user_email; ?></td>
<td><?= ($this->user->user_active == 0 ? 'No' : 'Yes'); ?></td>
</tr>
</tbody>
</table>
</div>
<?php } ?>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<div class="container">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<!-- login box on left side -->
<div class="login-box" style="width: 50%; display: block;">
<h2>Register a new account</h2>
<!-- 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>

View File

@@ -0,0 +1,12 @@
<div class="container">
<h1>Verification</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<a href="<?php echo Config::get('URL'); ?>">Go back to home page</a>
</div>
</div>

View File

@@ -0,0 +1,25 @@
<div class="container">
<h1>UserController/changePassword</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Set new password</h2>
<!-- new password form box -->
<form method="post" action="<?php echo Config::get('URL'); ?>user/changePassword_action" name="new_password_form">
<label for="change_input_password_current">Enter Current Password:</label>
<p><input id="change_input_password_current" class="reset_input" type='password'
name='user_password_current' pattern=".{6,}" required autocomplete="off" /></p>
<label for="change_input_password_new">New password (min. 6 characters)</label>
<p><input id="change_input_password_new" class="reset_input" type="password"
name="user_password_new" pattern=".{6,}" required autocomplete="off" /></p>
<label for="change_input_password_repeat">Repeat new password</label>
<p><input id="change_input_password_repeat" class="reset_input" type="password"
name="user_password_repeat" pattern=".{6,}" required autocomplete="off" /></p>
<input type="submit" name="submit_new_password" value="Submit new password" />
</form>
</div>
</div>

View File

@@ -0,0 +1,31 @@
<div class="container">
<h1>UserController/changeUserRole</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change account type</h2>
<p>
This page is a basic implementation of the upgrade-process.
User can click on that button to upgrade their accounts from
"basic account" to "premium account". This script simple offers
a click-able button that will upgrade/downgrade the account instantly.
In a real world application you would implement something like a
pay-process.
</p>
<p>
Please note: This whole process has been renamed from AccountType (v3.0) to UserRole (v3.1).
</p>
<h2>Currently your account type is: <?php echo Session::get('user_account_type'); ?></h2>
<!-- basic implementation for two account types: type 1 and type 2 -->
<form action="<?php echo Config::get('URL'); ?>user/changeUserRole_action" method="post">
<?php if (Session::get('user_account_type') == 1) { ?>
<input type="submit" name="user_account_upgrade" value="Upgrade my account (to Premium User)" />
<?php } else if (Session::get('user_account_type') == 2) { ?>
<input type="submit" name="user_account_downgrade" value="Downgrade my account (to Basic User)" />
<?php } ?>
</form>
</div>
</div>

View File

@@ -0,0 +1,28 @@
<div class="container">
<h1>Edit your avatar</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h3>Upload an Avatar</h3>
<div class="feedback info">
If you still see the old picture after uploading a new one: Hard-Reload the page with F5! Your browser doesn't
realize there's a new image as new and old one have the same filename.
</div>
<form action="<?php echo Config::get('URL'); ?>user/uploadAvatar_action" method="post" enctype="multipart/form-data">
<label for="avatar_file">Select an avatar image from your hard-disk (will be scaled to 44x44 px, only .jpg currently):</label>
<input type="file" name="avatar_file" required />
<!-- max size 5 MB (as many people directly upload high res pictures from their digital cameras) -->
<input type="hidden" name="MAX_FILE_SIZE" value="5000000" />
<input type="submit" value="Upload image" />
</form>
</div>
<div class="box">
<h3>Delete your avatar</h3>
<p>Click this link to delete your (local) avatar: <a href="<?php echo Config::get('URL'); ?>user/deleteAvatar_action">Delete your avatar</a>
</div>
</div>

View File

@@ -0,0 +1,17 @@
<div class="container">
<h1>UserController/editUserEmail</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change your email address</h2>
<form action="<?php echo Config::get('URL'); ?>user/editUserEmail_action" method="post">
<label>
New email address: <input type="text" name="user_email" required />
</label>
<input type="submit" value="Submit" />
</form>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class="container">
<h1>UserController/editUsername</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change your username</h2>
<form action="<?php echo Config::get('URL'); ?>user/editUserName_action" method="post">
<!-- btw http://stackoverflow.com/questions/774054/should-i-put-input-tag-inside-label-tag -->
<label>
New username: <input type="text" name="user_name" required />
</label>
<!-- set CSRF token at the end of the form -->
<input type="hidden" name="csrf_token" value="<?= Csrf::makeToken(); ?>" />
<input type="submit" value="Submit" />
</form>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<div class="container">
<h1>UserController/showProfile</h1>
<div class="box">
<h2>Your profile</h2>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div>Your username: <?= $this->user_name; ?></div>
<div>Your email: <?= $this->user_email; ?></div>
<div>Your avatar image:
<?php if (Config::get('USE_GRAVATAR')) { ?>
Your gravatar pic (on gravatar.com): <img src='<?= $this->user_gravatar_image_url; ?>' />
<?php } else { ?>
Your avatar pic (saved locally): <img src='<?= $this->user_avatar_file; ?>' />
<?php } ?>
</div>
<div>Your account type is: <?= $this->user_account_type; ?></div>
</div>
</div>

17
composer.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "panique/huge",
"type": "project",
"description": "A full-feature user authentication / login system embedded into a simple but powerful MVC framework structure",
"keywords": ["login", "auth", "user", "authentication", "mvc", "membership"],
"homepage": "https://github.com/panique/huge",
"license": "MIT",
"require-dev": {
"php": ">=5.5.0",
"phpmailer/phpmailer": "~6.0",
"gregwar/captcha": "~1.1",
"phpunit/phpunit": "4.8.*|5.7.*"
},
"autoload": {
"psr-4": { "": ["application/core/", "application/model/"] }
}
}

1374
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
public/.htaccess Normal file
View File

@@ -0,0 +1,23 @@
# Necessary to prevent problems when using a controller named "index" and having a root index.php
# more here: http://httpd.apache.org/docs/2.2/content-negotiation.html
Options -MultiViews
# Activates URL rewriting (like myproject.com/controller/action/1/2/3)
RewriteEngine On
# Prevent people from looking directly into folders
Options -Indexes
# If the following conditions are true, then rewrite the URL:
# If the requested filename is not a directory,
RewriteCond %{REQUEST_FILENAME} !-d
# and if the requested filename is not a regular file that exists,
RewriteCond %{REQUEST_FILENAME} !-f
# and if the requested filename is not a symbolic link,
RewriteCond %{REQUEST_FILENAME} !-l
# then rewrite the URL in the following way:
# Take the whole request filename and provide it as the value of a
# "url" query parameter to index.php. Append any query string from
# the original URL as further query parameters (QSA), and stop
# processing this .htaccess file (L).
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]

9
public/avatars/.htaccess Executable file
View File

@@ -0,0 +1,9 @@
# this disallows direct access to the folder listing
# and disallows access to any executables files (that users may upload)
# the script allows only .jpg/.png uploads, but we never know...
Options -Indexes
Options -ExecCGI
AddHandler cgi-script .php .php3 .php4 .phtml .pl .py .jsp .asp .htm .shtml .sh .cgi
# completely disable the PHP engine inside THIS folder, so what ever an attacker will do, PHP code will never run here
# more info on this: http://www.electrictoolbox.com/disable-php-apache-htaccess/
php_flag engine off

BIN
public/avatars/default.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

268
public/css/style.css Normal file

File diff suppressed because one or more lines are too long

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

17
public/index.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
/**
* A super-simple user-authentication solution, embedded into a small framework.
*
* HUGE
*
* @link https://github.com/panique/huge
* @license http://opensource.org/licenses/MIT MIT License
*/
// 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';
// start our application
new Application();

44
tests/core/ConfigTest.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
class ConfigTest extends PHPUnit_Framework_TestCase
{
/*
* Create fake values, necessary to run the tests
*/
public function setUp()
{
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['SCRIPT_NAME'] = 'index.php';
Config::$config = null;
}
/**
* Reset everything
*/
public function tearDown()
{
putenv('APPLICATION_ENV=');
Config::$config = null;
}
/**
* Checks if the correct config file for an EXISTING environment / config is called.
*/
public function testGetDefaultEnvironment()
{
// manually set environment to "development"
putenv('APPLICATION_ENV=development');
// now get the default action to see if the correct config file (for development) is called
$this->assertEquals('index', Config::get('DEFAULT_ACTION'));
}
public function testGetFailingEnvironment()
{
// manually set environment to "foobar" (and non-existing environment)
putenv('APPLICATION_ENV=foobar');
// call for environment should return false because config.foobar.php does not exist
$this->assertEquals(false, Config::get('DEFAULT_ACTION'));
}
}

View File

@@ -0,0 +1,23 @@
<?php
class EnvironmentTest extends PHPUnit_Framework_TestCase
{
public function testGetDefault()
{
// call for environment should return "development"
$this->assertEquals('development', Environment::get());
}
public function testGetDevelopment()
{
putenv('APPLICATION_ENV=development');
// call for environment should return "development"
$this->assertEquals('development', Environment::get());
}
public function testGetProduction()
{
putenv('APPLICATION_ENV=production');
$this->assertEquals('production', Environment::get());
}
}

262
tests/core/FilterTest.php Normal file
View File

@@ -0,0 +1,262 @@
<?php
class FilterTest extends PHPUnit_Framework_TestCase
{
/**
* When string argument contains bad code the encoded (and therefore un-dangerous) string should be returned
*/
public function testXSSFilterWithBadCodeInString_byref()
{
$codeBefore = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
Filter::XSSFilter($codeBefore);
$this->assertEquals($codeAfter, $codeBefore);
}
/**
* When string argument contains bad code the encoded (and therefore un-dangerous) string should be returned
*/
public function testXSSFilterWithBadCodeInString_return()
{
$codeBefore = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$this->assertEquals($codeAfter, Filter::XSSFilter($codeBefore));
}
public function testXSSFilterWithArrayOfBadCode_byref()
{
$codeBefore1 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeBefore2 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$badArray = [$codeBefore1, $codeBefore2];
Filter::XSSFilter($badArray);
$this->assertEquals($codeAfter, $badArray[0]);
$this->assertEquals($codeAfter, $badArray[1]);
}
public function testXSSFilterWithArrayOfBadCode_return()
{
$codeBefore1 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeBefore2 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$badArray = [$codeBefore1, $codeBefore2];
$this->assertEquals($codeAfter, Filter::XSSFilter($badArray)[1]);
}
public function testXSSFilterWithAssociativeArrayOfBadCode()
{
$codeBefore1 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeBefore2 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$badArray = ['foo' => $codeBefore1, 'bar' => $codeBefore2];
Filter::XSSFilter($badArray);
$this->assertEquals($codeAfter, $badArray['foo']);
$this->assertEquals($codeAfter, $badArray['bar']);
}
public function testXSSFilterWithSimpleObject_byref()
{
$codeBefore = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$integerBefore = 123;
$integerAfter = 123;
$object = new stdClass();
$object->int = $integerBefore;
$object->str = 'foo';
$object->badstr = $codeBefore;
Filter::XSSFilter($object);
$this->assertEquals('foo', $object->str);
$this->assertEquals($integerAfter, $object->int);
$this->assertEquals($codeAfter, $object->badstr);
}
public function testXSSFilterWithSimpleObject_return()
{
$codeBefore = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$integerBefore = 123;
$integerAfter = 123;
$object = new stdClass();
$object->str = 'foo';
$object->badstr = $codeBefore;
$this->assertEquals($codeAfter, Filter::XSSFilter($object)->badstr);
}
public function testXSSFilterWithObjectContainingArray_byref()
{
$codeBefore1 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeBefore2 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$badArray = ['foo' => 'bar', 'bad1' => $codeBefore1, 'bad2' => $codeBefore2];
$object = new stdClass();
$object->badArray = $badArray;
Filter::XSSFilter($object);
$this->assertEquals('bar', $object->badArray['foo']);
$this->assertEquals($codeAfter, $object->badArray['bad1']);
$this->assertEquals($codeAfter, $object->badArray['bad2']);
}
public function testXSSFilterWithObjectContainingArray_return()
{
$codeBefore = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$badArray = ['foo' => 'bar', 'bad' => $codeBefore];
$object = new stdClass();
$object->badArray = $badArray;
$this->assertEquals($codeAfter, Filter::XSSFilter($object)->badArray['bad']);
}
public function testXSSFilterWithObjectContainingObject_byref()
{
$codeBefore1 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeBefore2 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$object = new stdClass();
$object->badStr = $codeBefore1;
$childObject = new stdClass();
$childObject->badStr = $codeBefore2;
$object->badObject = $childObject;
Filter::XSSFilter($object);
$this->assertEquals($codeAfter, $object->badStr);
$this->assertEquals($codeAfter, $object->badObject->badStr);
}
public function testXSSFilterWithObjectContainingObject_return()
{
$codeBefore = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$object = new stdClass();
$childObject = new stdClass();
$childObject->badStr = $codeBefore;
$object->badObject = $childObject;
$this->assertEquals($codeAfter, Filter::XSSFilter($object)->badObject->badStr);
}
/**
* For every type other than strings or arrays, the method should return the untouched passed argument
*/
public function testXSSFilterWithNonStringOrArrayArguments()
{
$integerBefore = 123;
$integerAfter = 123;
$arrayBefore = [1, 2, 3];
$arrayAfter = [1, 2, 3];
$floatsBefore = 17.001;
$floatsAfter = 17.001;
$null = null;
Filter::XSSFilter($integerBefore);
Filter::XSSFilter($arrayBefore);
Filter::XSSFilter($floatsBefore);
Filter::XSSFilter($null);
$this->assertEquals($integerAfter, $integerBefore);
$this->assertEquals($arrayBefore, $arrayAfter);
$this->assertEquals($floatsBefore, $floatsAfter);
$this->assertNull($null);
}
/**
* For every type other than strings or arrays, the method should return the untouched passed argument
*/
public function testXSSFilterWithNonStringOrArrayArguments_return()
{
$integerBefore = 123;
$integerAfter = 123;
$arrayBefore = [1, 2, 3];
$arrayAfter = [1, 2, 3];
$floatsBefore = 17.001;
$floatsAfter = 17.001;
$null = null;
$this->assertEquals($integerAfter, Filter::XSSFilter($integerBefore));
$this->assertEquals($arrayBefore, Filter::XSSFilter($arrayBefore));
$this->assertEquals($floatsBefore, Filter::XSSFilter($floatsBefore));
$this->assertNull(Filter::XSSFilter($null));
}
/**
* For every type other than strings or arrays, the method should return the untouched passed argument
*/
public function testXSSFilterWithNonStringOrArrayArguments_byref()
{
$integerBefore = 123;
$integerAfter = 123;
$arrayBefore = [1, 2, 3];
$arrayAfter = [1, 2, 3];
$floatsBefore = 17.001;
$floatsAfter = 17.001;
$null = null;
Filter::XSSFilter($integerBefore);
Filter::XSSFilter($arrayBefore);
Filter::XSSFilter($floatsBefore);
Filter::XSSFilter($null);
$this->assertEquals($integerAfter, $integerBefore);
$this->assertEquals($arrayBefore, $arrayAfter);
$this->assertEquals($floatsBefore, $floatsAfter);
$this->assertNull($null);
}
public function testXSSFilterWithComplexArrayOfBadCode()
{
$codeBefore1 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeBefore2 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeBefore3 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeBefore4 = "Hello <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>";
$codeAfter = 'Hello &lt;script&gt;var http = new XMLHttpRequest(); http.open(&#039;POST&#039;, &#039;example.com/my_account/delete.php&#039;, true);&lt;/script&gt;';
$badObject = new stdClass();
$badObject->badstr = $codeBefore4;
$badArray = [
'foo',
$codeBefore1,
'bar',
[
'foo' => $codeBefore2,
'bar' => $codeBefore3
],
$badObject
];
Filter::XSSFilter($badArray);
$this->assertEquals('foo', $badArray[0]);
$this->assertEquals($codeAfter, $badArray[1]);
$this->assertEquals('bar', $badArray[2]);
$this->assertEquals($codeAfter, $badArray[3]['foo']);
$this->assertEquals($codeAfter, $badArray[3]['bar']);
$this->assertEquals($codeAfter, $badArray[4]->badstr);
}
}

View File

@@ -0,0 +1,55 @@
<?php
class RequestTest extends PHPUnit_Framework_TestCase
{
/**
* Testing the post() method of the Request class
*/
public function testPost()
{
$_POST["test"] = 22;
$this->assertEquals(22, Request::post('test'));
$this->assertEquals(null, Request::post('not_existing_key'));
// test trim & strip_tags: Method is used with second argument "true", triggering a cleaning of the input
$_POST["attacker_string"] = ' <script>alert("yo!");</script> ';
$this->assertEquals('alert("yo!");', Request::post('attacker_string', true));
}
/**
* Testing the postCheckbox() method of the Request class
*/
public function testPostCheckbox()
{
// Weird side-fact: a checked checkbox that has no manually set value will mostly contain 'on' as the default
// value in most modern browsers btw, so it makes sense to test this
$_POST['checkboxName'] = 'on';
$this->assertEquals(1, Request::postCheckbox('checkboxName'));
$_POST['checkboxName'] = 1;
$this->assertEquals(1, Request::postCheckbox('checkboxName'));
$_POST['checkboxName'] = null;
$this->assertEquals(null, Request::postCheckbox('checkboxName'));
}
/**
* Testing the get() method of the Request class
*/
public function testGet()
{
$_GET["test"] = 33;
$this->assertEquals(33, Request::get('test'));
$this->assertEquals(null, Request::get('not_existing_key'));
}
/**
* Testing the cookie() method of the Request class
*/
public function testCookie()
{
$_COOKIE["test"] = 44;
$this->assertEquals(44, Request::cookie('test'));
$this->assertEquals(null, Request::cookie('not_existing_key'));
}
}

28
tests/core/TextTest.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
class TextTest extends PHPUnit_Framework_TestCase
{
/**
* When argument is existing key, then existing value should be returned
*/
public function testGet()
{
$this->assertEquals("The username or password is incorrect. Please try again.", Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG'));
}
/**
* When argument is null, should return null
*/
public function testGetWithNullKey()
{
$this->assertEquals(null, Text::get(null));
}
/**
* When key does not exist in text data file, should return null
*/
public function testGetWithNonExistingKey()
{
$this->assertEquals(null, Text::get('XXX'));
}
}

24
tests/phpunit.xml Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="../vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite name="Core Suite">
<directory>./core/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../application/core</directory>
<directory suffix=".php">../application/model</directory>
<directory suffix=".php">../application/controller</directory>
</whitelist>
</filter>
</phpunit>

7
travis-ci-apache Normal file
View File

@@ -0,0 +1,7 @@
<VirtualHost *:80>
DocumentRoot "%TRAVIS_BUILD_DIR%/public"
<Directory "%TRAVIS_BUILD_DIR%/public">
AllowOverride All
Require all granted
</Directory>
</VirtualHost>