initial commit
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal 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
102
CHANGELOG.md
Normal 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
632
README.md
Normal file
@@ -0,0 +1,632 @@
|
||||

|
||||
|
||||
# HUGE
|
||||
|
||||
[](https://scrutinizer-ci.com/g/panique/huge/?branch=master)
|
||||
[](https://codeclimate.com/github/panique/huge)
|
||||
[](https://www.codacy.com/app/panique/huge?utm_source=github.com&utm_medium=referral&utm_content=panique/huge&utm_campaign=Badge_Grade)
|
||||
[](https://travis-ci.org/panique/huge)
|
||||
[](https://www.versioneye.com/user/projects/54ca11fbde7924f81a000010)
|
||||
[](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
22
_one-click-installation/Vagrantfile
vendored
Normal 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
|
||||
87
_one-click-installation/bootstrap.sh
Normal file
87
_one-click-installation/bootstrap.sh
Normal 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
BIN
_pictures/huge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
1
application/_installation/01-create-database.sql
Normal file
1
application/_installation/01-create-database.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE DATABASE IF NOT EXISTS `huge`;
|
||||
31
application/_installation/02-create-table-users.sql
Normal file
31
application/_installation/02-create-table-users.sql
Normal 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');
|
||||
6
application/_installation/03-create-table-notes.sql
Normal file
6
application/_installation/03-create-table-notes.sql
Normal 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';
|
||||
156
application/config/config.development.php
Normal file
156
application/config/config.development.php
Normal 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: ',
|
||||
);
|
||||
75
application/config/texts.php
Normal file
75
application/config/texts.php
Normal 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.",
|
||||
);
|
||||
35
application/controller/AdminController.php
Normal file
35
application/controller/AdminController.php
Normal 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");
|
||||
}
|
||||
}
|
||||
26
application/controller/DashboardController.php
Normal file
26
application/controller/DashboardController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
28
application/controller/ErrorController.php
Normal file
28
application/controller/ErrorController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
21
application/controller/IndexController.php
Normal file
21
application/controller/IndexController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
148
application/controller/LoginController.php
Normal file
148
application/controller/LoginController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
77
application/controller/NoteController.php
Normal file
77
application/controller/NoteController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
39
application/controller/ProfileController.php
Normal file
39
application/controller/ProfileController.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
74
application/controller/RegisterController.php
Normal file
74
application/controller/RegisterController.php
Normal 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();
|
||||
}
|
||||
}
|
||||
157
application/controller/UserController.php
Normal file
157
application/controller/UserController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
106
application/core/Application.php
Normal file
106
application/core/Application.php
Normal 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
82
application/core/Auth.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
application/core/Config.php
Normal file
23
application/core/Config.php
Normal 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];
|
||||
}
|
||||
}
|
||||
34
application/core/Controller.php
Normal file
34
application/core/Controller.php
Normal 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
60
application/core/Csrf.php
Normal 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);
|
||||
}
|
||||
}
|
||||
64
application/core/DatabaseFactory.php
Normal file
64
application/core/DatabaseFactory.php
Normal 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;
|
||||
}
|
||||
}
|
||||
146
application/core/Encryption.php
Normal file
146
application/core/Encryption.php
Normal 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;
|
||||
}
|
||||
}
|
||||
18
application/core/Environment.php
Normal file
18
application/core/Environment.php
Normal 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");
|
||||
}
|
||||
}
|
||||
77
application/core/Filter.php
Normal file
77
application/core/Filter.php
Normal 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 '&'
|
||||
* '"' (double quote) becomes '"' when ENT_NOQUOTES is not set.
|
||||
* "'" (single quote) becomes ''' (or ') only when ENT_QUOTES is set.
|
||||
* '<' (less than) becomes '<'
|
||||
* '>' (greater than) becomes '>'
|
||||
*
|
||||
* @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
154
application/core/Mail.php
Normal 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;
|
||||
}
|
||||
}
|
||||
48
application/core/Redirect.php
Normal file
48
application/core/Redirect.php
Normal 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);
|
||||
}
|
||||
}
|
||||
64
application/core/Request.php
Normal file
64
application/core/Request.php
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
135
application/core/Session.php
Normal file
135
application/core/Session.php
Normal 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
32
application/core/Text.php
Normal 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
177
application/core/View.php
Normal 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');
|
||||
}
|
||||
}
|
||||
95
application/model/AdminModel.php
Normal file
95
application/model/AdminModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
258
application/model/AvatarModel.php
Normal file
258
application/model/AvatarModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
46
application/model/CaptchaModel.php
Normal file
46
application/model/CaptchaModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
382
application/model/LoginModel.php
Normal file
382
application/model/LoginModel.php
Normal 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();
|
||||
}
|
||||
}
|
||||
120
application/model/NoteModel.php
Normal file
120
application/model/NoteModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
365
application/model/PasswordResetModel.php
Normal file
365
application/model/PasswordResetModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
293
application/model/RegistrationModel.php
Normal file
293
application/model/RegistrationModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
343
application/model/UserModel.php
Normal file
343
application/model/UserModel.php
Normal 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();
|
||||
}
|
||||
}
|
||||
65
application/model/UserRoleModel.php
Normal file
65
application/model/UserRoleModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
19
application/view/_templates/feedback.php
Normal file
19
application/view/_templates/feedback.php
Normal 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>';
|
||||
}
|
||||
}
|
||||
7
application/view/_templates/footer.php
Normal file
7
application/view/_templates/footer.php
Normal 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>
|
||||
79
application/view/_templates/header.php
Normal file
79
application/view/_templates/header.php
Normal 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>
|
||||
57
application/view/admin/index.php
Normal file
57
application/view/admin/index.php
Normal 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>
|
||||
15
application/view/dashboard/index.php
Normal file
15
application/view/dashboard/index.php
Normal 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>
|
||||
6
application/view/error/404.php
Normal file
6
application/view/error/404.php
Normal 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>
|
||||
18
application/view/index/index.php
Normal file
18
application/view/index/index.php
Normal 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>
|
||||
50
application/view/login/index.php
Normal file
50
application/view/login/index.php
Normal 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>
|
||||
35
application/view/login/requestPasswordReset.php
Normal file
35
application/view/login/requestPasswordReset.php
Normal 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>
|
||||
27
application/view/login/resetPassword.php
Normal file
27
application/view/login/resetPassword.php
Normal 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>
|
||||
22
application/view/note/edit.php
Normal file
22
application/view/note/edit.php
Normal 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>
|
||||
44
application/view/note/index.php
Normal file
44
application/view/note/index.php
Normal 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>
|
||||
44
application/view/profile/index.php
Normal file
44
application/view/profile/index.php
Normal 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>
|
||||
41
application/view/profile/showProfile.php
Normal file
41
application/view/profile/showProfile.php
Normal 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>
|
||||
38
application/view/register/index.php
Normal file
38
application/view/register/index.php
Normal 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>
|
||||
12
application/view/register/verify.php
Normal file
12
application/view/register/verify.php
Normal 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>
|
||||
25
application/view/user/changePassword.php
Normal file
25
application/view/user/changePassword.php
Normal 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>
|
||||
31
application/view/user/changeUserRole.php
Normal file
31
application/view/user/changeUserRole.php
Normal 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>
|
||||
28
application/view/user/editAvatar.php
Normal file
28
application/view/user/editAvatar.php
Normal 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>
|
||||
17
application/view/user/editUserEmail.php
Normal file
17
application/view/user/editUserEmail.php
Normal 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>
|
||||
20
application/view/user/editUsername.php
Normal file
20
application/view/user/editUsername.php
Normal 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>
|
||||
21
application/view/user/index.php
Normal file
21
application/view/user/index.php
Normal 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
17
composer.json
Normal 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
1374
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
public/.htaccess
Normal file
23
public/.htaccess
Normal 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
9
public/avatars/.htaccess
Executable 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
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
268
public/css/style.css
Normal file
File diff suppressed because one or more lines are too long
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
17
public/index.php
Normal file
17
public/index.php
Normal 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
44
tests/core/ConfigTest.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
23
tests/core/EnvironmentTest.php
Normal file
23
tests/core/EnvironmentTest.php
Normal 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
262
tests/core/FilterTest.php
Normal 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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
$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 <script>var http = new XMLHttpRequest(); http.open('POST', 'example.com/my_account/delete.php', true);</script>';
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
||||
55
tests/core/RequestTest.php
Normal file
55
tests/core/RequestTest.php
Normal 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
28
tests/core/TextTest.php
Normal 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
24
tests/phpunit.xml
Normal 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
7
travis-ci-apache
Normal file
@@ -0,0 +1,7 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot "%TRAVIS_BUILD_DIR%/public"
|
||||
<Directory "%TRAVIS_BUILD_DIR%/public">
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
Reference in New Issue
Block a user