Compare commits

...

No commits in common. "gh-pages" and "dev" have entirely different histories.

78 changed files with 8550 additions and 1612 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
config/config.php

26
.travis.yml Normal file
View file

@ -0,0 +1,26 @@
language: php
php:
- '5.4'
- '5.5'
- '5.6'
- '7.0'
- hhvm
- nightly
matrix:
allow_failures:
- php: hhvm
- php: nightly
services:
- mysql
before_install:
# Create example schema
- mysql -u root -e "CREATE USER 'vmail'@'localhost' IDENTIFIED BY 'vmail';"
- mysql -u root -e "CREATE DATABASE IF NOT EXISTS vmail;"
- mysql -u root -e "GRANT ALL PRIVILEGES ON vmail.* TO 'vmail'@'localhost'"
- mysql -u root -e "CREATE TABLE vmail.domains (id int(10) unsigned NOT NULL AUTO_INCREMENT, domain varchar(128) NOT NULL, PRIMARY KEY (domain), UNIQUE KEY id (id)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;"
- mysql -u root -e "CREATE TABLE vmail.users (id int(10) unsigned NOT NULL AUTO_INCREMENT, username varchar(128) NOT NULL DEFAULT '', domain varchar(128) NOT NULL DEFAULT '', password varchar(128) NOT NULL DEFAULT '', mailbox_limit int(10) NOT NULL DEFAULT '128', max_user_redirects int(10) NOT NULL DEFAULT '0', PRIMARY KEY (username,domain), UNIQUE KEY id (id)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;"
- mysql -u root -e "CREATE TABLE vmail.aliases (id int(10) unsigned NOT NULL AUTO_INCREMENT, source varchar(128) NOT NULL, destination text NOT NULL, multi_source varchar(32) DEFAULT NULL, is_created_by_user int(1) NOT NULL DEFAULT '0', PRIMARY KEY (source), UNIQUE KEY id (id)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;"
# Copy the example config
- cp config/config.php.example config/config.php
notifications:
email: false

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Thomas Leister
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

382
README.md Normal file
View file

@ -0,0 +1,382 @@
:exclamation: This project **isn't maintained** anymore. New maintainers welcome, just contact [@ohartl](https://github.com/ohartl) . Please consider using alternatives like https://github.com/Andreas-Bresch/vmailManage/ :exclamation:
# WebMUM - Web Mailserver User Manager
[![Build Status](https://travis-ci.org/ohartl/webmum.svg)](https://travis-ci.org/ohartl/webmum)
***WebMUM is not compatible with the [new Mailserver-HowTo](https://thomas-leister.de/allgemein/sicherer-mailserver-dovecot-postfix-virtuellen-benutzern-mysql-ubuntu-server-xenial/)!,*** but we will try to implement the changes for the release of version 1.0.0.
WebMUM is a web frontend based on PHP which helps you to manage e-mail server via MySQL. This software is licensed under the MIT license.
This project is currently developed and managed by [ohartl](https://github.com/ohartl) and together with the [contributes](https://github.com/ohartl/webmum/graphs/contributors).
Founder of this project is [ThomasLeister](https://github.com/ThomasLeister), a passionate [blogger](https://thomas-leister.de/) specialized topics like linux, open-source, servers etc.
Feel free to send in issues and pull requests, your support for this project is much appreciated!
## Installation
Clone the WebMUM Repository to your webserver's virtual host root directory:
```bash
git clone https://github.com/ohartl/webmum
```
A update / upgrade guide can be found [here](#update--upgrade-webmum).
### Webserver
Now configure your webserver. URL rewriting to index.php is required.
#### Nginx
Nginx config examples following, but you still need to change domain and path in config as explained [here](#paths).
With subdirectory `webmum/` in URL (e.g. `http://mydomain.tld/webmum/`):
```nginx
server {
listen 80;
server_name mydomain.tld;
root /var/www;
index index.html index.php;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location /webmum {
try_files $uri $uri/ /webmum/index.php?$args;
}
# protect the codebase by denying direct access
location ^~ /webmum/include/php {
deny all;
return 403;
}
location ^~ /webmum/config {
deny all;
return 403;
}
}
```
Without subdirectory in URL (e.g. `http://webmum.mydomain.tld/`):
```nginx
server {
listen 80;
server_name webmum.mydomain.tld;
root /var/www/webmum;
index index.html index.php;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
# protect the codebase by denying direct access
location ^~ /include/php {
deny all;
return 403;
}
location ^~ /config {
deny all;
return 403;
}
}
```
#### Apache
Apache config examples following, but you still need to change domain and path in config as explained [here](#paths).
Please note: mod_rewrite must be enabled for URL rewriting:
```bash
sudo a2enmod rewrite
```
With subdirectory `webmum/` in URL (e.g. `http://mydomain.tld/webmum/`):
```apache
<VirtualHost *:80>
ServerName domain.tld
DocumentRoot /var/www/domain.tld
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^\/webmum/(.*)\.css$ /webmum/$1.css [L]
RewriteRule ^\/webmum/(.*)$ /webmum/index.php [L,QSA]
</VirtualHost>
```
Without subdirectory in URL (e.g. `http://webmum.mydomain.tld/`):
```apache
<VirtualHost *:80>
ServerName webmum.domain.tld
DocumentRoot /var/www/domain.tld/webmum
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule (.*)\.css$ $1.css [L]
RewriteRule ^(.*)$ /index.php [L,QSA]
</VirtualHost>
```
Access to the codebase is denied with a `.htaccess` file, that can be found in `/include/php`.
## WebMUM Configuration
Configure WebMUM via the configuration file at `config/config.inc.php`.
### MySQL
At first the database access has to be configured under the config key `mysql`.
Check if you've got the same database schema as configured in the config key `schema`.
### Mailbox limit (Optional)
If you want to use your "mailbox_limit" column to limit the size of your users' mailboxes, just enable mailbox limit in the options.
```php
'options' => array(
...
'enable_mailbox_limits' => true,
...
),
```
WebMUM will then show a new field "Mailbox limit" in the frontend.
### Multiple source redirect support (Optional)
As mailservers can only process a single source address for redirects the database table for aliases / redirects can only hold a single source address in a row.
WebMum will, if you enabled the multiple source redirect support, do some magic so there is only a single address in a row even though multiple addresses where entered.
To make this work another column in the database table is required, which holds an identifier for the list of source addresses, so they can be edited like normal redirects.
By default you can only redirect a single address to a single or multiple destinations.
If you want to enable support for redirecting multiple source addresses to a destination, just enable it in the options:
```php
'options' => array(
...
'enable_multi_source_redirects' => true,
...
),
```
And add the following column to your database table for aliases / redirects:
```sql
ALTER TABLE `aliases` ADD COLUMN `multi_source` VARCHAR(32) NULL DEFAULT NULL;
```
WebMUM will then show a larger field for source addresses in the frontend and you can not list emails in source field.
### Admin domain limits (Optional)
If you share your mailserver with others, host their domains and they should be able to manage their domains, but not all domains on that mailserver then this is the right option for you.
You have to add that user to the `admins` array in your configuration and enable admin domain limits in the options:
```php
'options' => array(
...
'enable_admin_domain_limits' => true,
...
),
```
also you have to make an entry in the `admin_domain_limits` array, for example `peter@his.tld` should be able to manage his domains `his.tld` and `his-company.tld` then configure the following:
```php
'admin_domain_limits' => array(
'peter@his.tld' => array('his.tld', 'his-company.tld'),
);
```
Admins that have been listed in `admin_domain_limits` don't have access to the "Manage domains" pages, otherwise they could delete domains they are managing, but maybe someone else owns.
### Users redirects (Optional)
If you want to enable some users to create redirects on their own, either limited by a maximum number of redirects or unlimited, this is the right option for you.
You have to enable this feature in the options:
```php
'options' => array(
...
'enable_user_redirects' => true,
...
),
```
And add the following columns to your database tables for aliases / redirects and users:
```sql
ALTER TABLE `aliases` ADD COLUMN `is_created_by_user` INT(1) NOT NULL DEFAULT '0';
```
Note: By choosing a default value for the `max_user_redirects` column on the users table, you can set the default state of user redirects for new users. `0` = unlimited, `-1` = disabled and a number larger than 0 will limit user redirects.
```sql
ALTER TABLE `users` ADD COLUMN `max_user_redirects` INT(10) NOT NULL DEFAULT '-1';
```
### Paths
The `base_url` is the URL your WebMUM installation is accessible from outside, this also includes subdirectories if you installed it in a subdirectory for that specific domain.
```php
'base_url' => 'http://localhost/webmum',
```
In the example above, WebMUM is located in a subdirectory named "webmum/". If your WebMUM installation is directly accessible from a domain (has its own domain), then set the `base_url` to something like this:
```php
'base_url' => 'http://webmum.mydomain.tld',
```
### Admin e-mail address
Only users with one of the specified email addresses will have access to the administrator's dashboard and will be able to create, edit and delete users, domains and redirects.
```php
'admins' = array(
'admin@domain.tld',
);
```
Admin email accounts must exist in the virtual user database on your own server. (=> an e-mail account on a foreign server won't give you access!). You can then login into the admin dashboard with that e-mail address and the corresponding password.
### Minimal required password length
```php
'password' => array(
...
'min_length' => 8,
...
),
```
### Logfile
When logging is enabled, WebMUM will write messages into a file "webmum.log" in a specified directory (e.g. when a login attempt fails).
Enable logging by setting it to enabled in the options:
```php
'options' => array(
...
'enable_logging' => true,
...
),
```
... and set a log path where the PHP user has permission to write the log file:
```php
'log_path' => '/var/www/webmum/log/',
```
"Login-failed-messages" have the following scheme:
```
Dec 19 13:00:19: WebMUM login failed for IP 127.0.0.1
```
#### Fail2Ban support
If you want to use **Fail2Ban** with WebMUM, the filter has to be:
```
[Definition]
failregex = ^(.*)\: WebMUM login failed for IP <HOST>$
```
### Validate that source addresses of redirects must be from the managed domains only
```php
'options' => array(
...
'enable_validate_aliases_source_domain' => true,
...
),
```
### Frontend options
Choose delimiter between multiple email addresses: comma, semicolon or new line separated.
**Tip:** new line is helpful for long lists of addresses.
```php
'frontend_options' => array(
// Separator for email lists
'email_separator_text' => ', ', // possible values: ', ' (default), '; ', PHP_EOL (newline)
'email_separator_form' => ',', // possible values: ',' (default), ';', PHP_EOL (newline)
),
```
The input for addresses can be separated by `,`, `;`, `:`, `|`, `newline` and combinations since all of them will result in a valid list of addresses in database, magic.
## Update / Upgrade WebMUM
If you cloned WebMUM into your filesystem via `git clone https://github.com/ohartl/webmum`:
```bash
git stash
git pull origin master
git stash pop
```
... and you are ready to go. Git might complain about conflicting files - you will have to resolve the merge conflict manually then.
If you downloaded WebMUM as a ZIP package, you have to update WebMUM manually.
**After every update:**
Please check if your config.inc.php fits the current requirements by comparing your version of the file with the config.inc.php in the repository.
## FAQ
### Which password hash algorithm does WebMUM use?
By default WebMUM uses the `SHA-512` hash algorithm for passwords. You can also choose between the alternatives `SHA-256` or `BLOWFISH` in the config.
```php
'password' => array(
...
'hash_algorithm' => 'SHA-512', // Supported algorithms: SHA-512, SHA-256, BLOWFISH
...
),
```
### "login/ cannot be found"
Webserver rewrites have to be enabled on your server, because WebMUM does not use real URLs for the frontend, but virtual URLs based on URL rewriting.
When rewriting fails, you receive a 404 error message.

1
config/.htaccess Normal file
View file

@ -0,0 +1 @@
Deny from all

211
config/config.php.example Normal file
View file

@ -0,0 +1,211 @@
<?php
//////////////////////////////////////////////////////////////////////////
// //
// DO NOT EDIT THIS FILE! //
// //
// Instead, copy this config file to config.php and make your changes //
// in the copied version. This is just a template! //
// //
//////////////////////////////////////////////////////////////////////////
return array(
/******************************************************
* URL to your WebMUM installation.
*/
'base_url' => 'http://localhost/webmum',
/******************************************************
* MySQL database connection settings
*/
'mysql' => array(
'host' => 'localhost',
'user' => 'vmail',
'password' => 'vmail',
'database' => 'vmail',
),
/******************************************************
* Database schema mapping
*/
'schema' => array(
// Table names
'tables' => array(
// Example:
// 'table-keyword' => 'actual-table-name',
'users' => 'users',
'domains' => 'domains',
'aliases' => 'aliases',
),
'attributes' => array(
// Example:
// 'table-keyword' => array(
// 'attribute-keyword' => 'actual-attribute-name',
// ...
// ),
// Users table columns
'users' => array(
'id' => 'id',
'username' => 'username',
'domain' => 'domain',
'password' => 'password',
'mailbox_limit' => 'mailbox_limit', // (Optional see 'options.enable_mailbox_limits')
'max_user_redirects' => 'max_user_redirects', // (Optional see 'options.enable_user_redirects')
),
// Domains table columns
'domains' => array(
'id' => 'id',
'domain' => 'domain',
),
// Aliases table columns
'aliases' => array(
'id' => 'id',
'source' => 'source',
'destination' => 'destination',
'multi_source' => 'multi_source', // (Optional see 'options.enable_multi_source_redirects')
'is_created_by_user' => 'is_created_by_user', // (Optional see 'options.enable_user_redirects')
),
),
),
/******************************************************
* General options
*/
'options' => array(
/**
* Enable mailbox limits. (Default false == off)
*
* Needs a new db attribute in users table with INT(10).
* (see 'schema.attributes.users.mailbox_limit')
*/
'enable_mailbox_limits' => false,
/**
* Enable validating that the source addresses are ending with domain from domains. (Default true == on)
*/
'enable_validate_aliases_source_domain' => true,
/**
* Enable multi source redirects. (Default false == off)
*
* Needs a new db attribute in aliases table with VARCHAR(32).
* (see 'schema.attributes.aliases.multi_source')
*/
'enable_multi_source_redirects' => false,
/**
* Enable limited admin domain access. (Default false == off)
*
* Limitations can be configured under 'admin_domain_limits'.
*/
'enable_admin_domain_limits' => false,
/**
* Enable users can create own redirects. (Default false == off)
*
* Needs two new db attributes in users table with INT(10) and aliases table with INT(1) + DEFAULT 0
* (see 'schema.attributes.users.max_user_redirects' and 'schema.attributes.aliases.is_created_by_user')
*
* A maximum number of redirects per user can be configured.
*/
'enable_user_redirects' => false,
/**
* Enable logging for failed login attempts. (Default false == off)
*
* You can monitor the logfile with fail2ban and ban attackers' IP-addresses.
* Path to logfile can be configured under 'log_path'.
*/
'enable_logging' => false,
),
/******************************************************
* Admin e-mail addresses
*
* Users with these e-mail addresses will have admin access,
* you can limit their access with the 'options.enable_admin_domain_limits' feature
*/
'admins' => array(
'admin@domain.tld',
),
/******************************************************
* Limited admin domain access (only used if 'options.enable_admin_domain_limits' is true)
*
* Unlisted admins have access to every domain, the admin is limited to listed domains only!
* Unlisted domains are not accessible by that admin.
* Note that listed admins cannot create new domains!
*/
'admin_domain_limits' => array(
// Example:
// 'low_rank_admin@domain.tld' => array('his-domain.tld', 'shared-domain.tld'),
),
/******************************************************
* Password
*/
'password' => array(
// Algorithm used for password encryption
'hash_algorithm' => 'SHA-512', // Supported algorithms: SHA-512, SHA-256, BLOWFISH
// Minimum length for passwords
'min_length' => 8,
),
/******************************************************
* Log file path (only used if 'options.enable_logging' is true)
*
* Make sure that PHP has permission to create the log directory and webmum.log (write permissions for php user)
*/
'log_path' => '/var/www/webmum/log/',
/******************************************************
* Frontend options
*/
'frontend_options' => array(
// Separator for email lists
'email_separator_text' => ', ', // possible values: ', ' (default), '; ', PHP_EOL (newline)
'email_separator_form' => ',', // possible values: ',' (default), ';', PHP_EOL (newline)
),
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

433
include/css/style.css Normal file
View file

@ -0,0 +1,433 @@
body {
font-family: arial, serif;
font-size: 12px;
margin: 0;
background-color: white;
}
hr {
border: none;
border-bottom: 1px solid #ccc;
margin: 20px 0;
}
hr.invisible {
border-color: transparent;
}
.text-fail {
color: #d90000;
}
.text-warning {
color: #ADA900;
}
.text-success {
color: #39AD00;
}
#header {
position: relative;
height: 50px;
width: 100%;
background: rgba(15, 15, 15, 1) linear-gradient(rgba(63, 63, 63, 1), rgba(15, 15, 15, 1));
color: white;
line-height: 50px;
box-sizing: border-box;
padding-left: 20px;
padding-right: 20px;
}
#header div.title {
float: left;
height: 50px;
width: auto;
}
#header div.title a {
font-size: 15px;
color: white;
text-decoration: none;
}
#header div.title a:hover {
text-decoration: underline;
}
#header div.header-menu {
float: left;
padding-left: 100px;
}
#header div.header-button {
float: left;
height: 50px;
margin-right: 30px;
color: white;
}
#header div.header-button a {
color: white;
text-decoration: none;
}
#header div.header-button a:hover {
text-decoration: underline;
}
#content {
height: auto;
min-height: calc(100vh - 150px);
padding: 20px;
background-color: white;
}
#content h1 {
color: rgba(62, 59, 59, 1);
}
#content .sub-header {
font-weight: normal;
font-size: .9em;
color: #999;
padding: 5px 0 0 5px;
}
#content a {
color: blue;
text-decoration: none;
}
#content a:hover {
text-decoration: underline;
}
#content .form {
margin: 25px 0;
}
#content .form hr {
margin: 5px 0 15px;
}
#content .form .input-group, #content .form .buttons {
padding-bottom: 10px;
}
#content .form .input-group > label {
font-weight: bold;
line-height: 22px;
font-size: 13px;
}
#content .form .input-group > .input-info {
padding-bottom: 5px;
color: #999;
}
#content .form .input-group > .input-group {
padding-left: 25px;
}
#content .form .input-group > .input-group:first-of-type {
padding-top: 10px;
}
#content .form .input-group > .input-group > label {
font-size: 12px;
}
#content .form .input {
}
#content .form .input input, #content .form .input textarea, #content .form .input select {
background: #fefefe;
border: 1px solid rgba(200, 200, 200, 1);
border-radius: 3px;
margin-bottom: 5px;
padding: 0 10px;
box-shadow: inset 1px 1px 3px rgba(230, 230, 230, 1);
}
#content .form .input input:focus, #content .form .input input:focus, #content .form .input select:focus {
border: 1px solid rgba(137, 137, 137, 1);
}
#content .form .input input {
min-width: 180px;
}
#content .form .input input[type="number"] {
padding-left: 15px;
padding-right: 0;
min-width: 70px;
width: 70px;
}
#content .form .input input[type="checkbox"],
#content .form .input input[type="radio"] {
border: none;
box-shadow: none;
min-width: inherit;
vertical-align: middle;
margin: 6px 0 8px 5px;
cursor: pointer;
}
#content .form .input input[type="checkbox"]+label,
#content .form .input input[type="radio"]+label {
padding-left: 3px;
margin-right: 15px;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#content .form .input textarea {
min-height: 150px;
min-width: 400px;
line-height: 18px !important;
padding-top: 8px;
padding-bottom: 8px;
}
#content .form .input input,
#content .form .input textarea,
#content .form .input.input-labeled.input-labeled-left > *:first-child,
#content .form .input.input-labeled.input-labeled-right > *:last-child {
line-height: 33px;
}
#content .form .input select {
padding: 8px 10px 9px;
min-width: 200px;
}
#content .form .input select option[value=""] {
color: #ccc;
}
#content .form .input.input-labeled.input-labeled-left > *:first-child,
#content .form .input.input-labeled.input-labeled-right > *:last-child {
background: #eee;
border: 1px solid rgba(200, 200, 200, 1);
border-radius: 3px;
display: inline-block;
margin: 0 0 0 3px;
padding: 0 10px;
position: absolute;
}
#content .form .input.input-action > *:first-child,
#content .form .input.input-labeled > *:first-child {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
margin-right: -4px;
}
#content .form .input.input-action > *:last-child,
#content .form .input.input-labeled > *:last-child {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
#content .buttons {
margin: 15px 0;
}
#content .form .buttons {
margin: 0;
}
#content .buttons.buttons-horizontal .button {
display: block;
margin-top: 10px;
}
#content .buttons.buttons-horizontal .button:first-child {
margin-top: 0;
}
#content .button {
background: #dddddd;
background: linear-gradient(#ffffff, #eaeaea);
border: 1px solid rgba(200, 200, 200, 1);
border-radius: 3px;
font-family: arial, serif;
transition: all 0.2s;
height: auto;
min-width: 200px;
width: 200px;
line-height: 31px;
font-size: 13px;
text-align: center;
color: rgba(57, 57, 57, 1);
text-decoration: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#content a.button {
display: inline-block;
}
#content .button:hover {
box-shadow: 1px 1px 4px #dbdbdb;
text-decoration: none;
cursor: pointer;
}
#content .button.button-large,
#content .buttons.button-large .button {
min-width: 300px;
width: 300px;
font-size: 15px;
line-height: 45px;
color: rgba(57, 57, 57, 1);
}
#content .button.button-primary,
#content .buttons.button-primary .button {
background: #666;
background: linear-gradient(#999, #666);
border-color: #444;
color: #fff;
}
#content .button.button-primary:hover,
#content .button.button-primary:active,
#content .buttons.button-primary .button:hover,
#content .buttons.button-primary .button:active {
background: #777;
background: linear-gradient(#777777, #444);
border-color: #333;
color: #fff;
}
#content .button.button-disabled {
background: #f1f1f1;
border-color: #f1f1f1;
box-shadow: none;
cursor: not-allowed;
color: #bbb;
}
#content .table {
margin: 25px 0;
border-collapse: collapse;
border: none;
}
#content .table-compact {
margin: 5px 0;
}
#content .table thead th {
line-height: 38px;
padding: 2px 15px 0;
border: 1px solid rgba(179, 176, 176, 1);
background: #eeeeee;
background: linear-gradient(#ffffff, #eaeaea);
font-size: 13px;
}
#content .table tfoot th {
line-height: 33px;
text-align: left;
padding: 0 10px;
font-weight: normal;
color: #999;
}
#content .table tbody td {
line-height: 21px;
padding: 9px 10px;
border: 1px solid rgba(179, 176, 176, 1);
}
#content .table tbody > tr:hover {
background-color: rgba(234, 234, 234, 1);
}
#content .table tbody > tr.warning {
background-color: #fcf897;
}
#content .table a {
color: rgb(148, 148, 255);
}
#content .table tbody > tr:hover a {
color: blue;
}
#content .notifications {
}
#content .notification {
height: auto;
width: 100%;
margin: 15px 0;
text-align: center;
border: 1px solid;
border-radius: 3px;
padding: 15px 10px;
box-sizing: border-box;
}
#content .notification.notification-fail {
background-color: #fcacac;
border-color: red;
}
#content .notification.notification-warning {
background-color: #fcf897;
border-color: #ffe600;
}
#content .notification.notification-success {
background-color: rgba(182, 255, 183, 1);
border-color: green;
}
#footer {
position: relative;
width: 100%;
background-color: white;
padding: 15px 20px;
box-sizing: border-box;
color: grey;
}
#footer ul {
list-style: none;
}
#footer li {
display: inline-block;
}
#footer li:not(:last-child):after {
content: '|';
color: #444444;
padding: 0 6px;
}
#footer a {
color: #888888;
}
#footer a:hover {
color: #666666;
}

1
include/php/.htaccess Normal file
View file

@ -0,0 +1 @@
Deny from all

View file

@ -0,0 +1,263 @@
<?php
class Auth
{
const SESSION_IDENTIFIER = 'uid';
/**
* @var User|null
*/
private static $loggedInUser = null;
/**
* Init Authentication
*/
public static function init()
{
static::loginUserViaSession();
}
/**
* Check whether the user is logged in or not.
*
* @return bool
*/
public static function isLoggedIn()
{
return !is_null(static::$loggedInUser);
}
/**
* Get the currently logged in user.
*
* @return null|User
*/
public static function getUser()
{
return static::$loggedInUser;
}
/**
* @param AbstractModel $user
*/
private static function loginUserByModel($user)
{
static::$loggedInUser = $user;
}
/**
* Checks session for logged in user, validates the login and finally logs him in.
*/
private static function loginUserViaSession()
{
global $_SESSION;
if(isset($_SESSION[static::SESSION_IDENTIFIER])
&& !empty($_SESSION[static::SESSION_IDENTIFIER])
){
$userId = $_SESSION[static::SESSION_IDENTIFIER];
/** @var User $user */
$user = User::find($userId);
// check if user still exists in database
if(!is_null($user)){
static::loginUserByModel($user);
}
}
}
/**
* Login user with provided credentials and save login in session
*
* @param string $email
* @param string $password
*
* @return bool
*/
public static function login($email, $password)
{
$email = strtolower($email);
$emailInParts = explode("@", $email);
if(count($emailInParts) !== 2){
return false;
}
$username = $emailInParts[0];
$domain = $emailInParts[1];
/** @var User $user */
$user = User::findWhereFirst(
array(
array(User::attr('username'), $username),
array(User::attr('domain'), $domain),
)
);
// Check if user exists
if(!is_null($user)){
if(static::checkPasswordByHash($password, $user->getPasswordHash())){
static::loginUserByModel($user);
$_SESSION[static::SESSION_IDENTIFIER] = $user->getId();
return true;
}
}
return false;
}
/**
* @return void
*/
public static function logout()
{
unset($_SESSION[static::SESSION_IDENTIFIER]);
static::$loggedInUser = null;
if(session_status() === PHP_SESSION_ACTIVE){
session_destroy();
}
}
/**
* Check if current user has a certain role, but User::ROLE_ADMIN will have access to all
*
* @param string $requiredRole
*
* @return bool
*/
public static function hasPermission($requiredRole)
{
if(static::isLoggedIn()){
$user = static::getUser();
return $user->getRole() === $requiredRole
|| $user->getRole() === User::ROLE_ADMIN;
}
return false;
}
/**
* Checks the new password entered by user on certain criteria, and throws an exception if its invalid.
*
* @param string $password
* @param string $passwordRepeated
*
* @throws AuthException Codes explained below
* 2: One password field is empty
* 3: Passwords aren't equal
* 4: Passwort is too snort
*/
public static function validateNewPassword($password, $passwordRepeated)
{
// Check if one passwort input is empty
if(empty($password)){
throw new AuthException("First password field was'nt filled out.", 2);
}
if(empty($passwordRepeated)){
throw new AuthException("Repeat password field was'nt filled out.", 2);
}
// Check if password are equal
if($password !== $passwordRepeated){
throw new AuthException("The repeated password must be equal to the first one.", 3);
}
// Check if password length is okay
if(Config::has('password.min_length')
&& strlen($password) < Config::get('password.min_length')
){
throw new AuthException("Passwords must be at least ".Config::get('password.min_length')." characters long.", 4);
}
}
/**
* @param string $password
* @param string $hash
*
* @return bool
*/
public static function checkPasswordByHash($password, $hash)
{
return crypt($password, $hash) === $hash;
}
/**
* @return string
*/
private static function getPasswordSchemaPrefix()
{
$map = array(
'SHA-256' => '$5$rounds=5000$',
'BLOWFISH' => '$2a$09$',
'SHA-512' => '$6$rounds=5000$',
);
$key = Config::get('password.hash_algorithm', 'SHA-512');
if(!isset($map[$key])){
$key = 'SHA-512';
}
return $map[$key];
}
/**
* @param string $password
*
* @return string
*/
public static function generatePasswordHash($password)
{
if(function_exists('mt_rand')){
mt_srand(time());
$num = mt_rand(1, 100000);
}
else{
srand(time());
$num = rand(1, 100000);
}
$salt = base64_encode($num);
$schemaPrefix = static::getPasswordSchemaPrefix();
$hash = crypt($password, $schemaPrefix.$salt.'$');
return $hash;
}
/**
* @param string $userId
* @param $password
*/
public static function changeUserPassword($userId, $password)
{
$passwordHash = static::generatePasswordHash($password);
/** @var User $user */
$user = User::find($userId);
if(!is_null($user)){
$user->setPasswordHash($passwordHash);
$user->save();
}
}
}

View file

@ -0,0 +1,6 @@
<?php
class AuthException extends Exception
{
}

View file

@ -0,0 +1,117 @@
<?php
class Config
{
/**
* @var array
*/
protected static $config = array();
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* @codeCoverageIgnore
*/
private function __clone()
{
}
/**
* @param array $configArray
*/
public static function init($configArray)
{
static::set(null, $configArray);
}
/**
* Set a config value using "dot" notation.
*
* @param string $key
* @param mixed $value
* @return array
*/
public static function set($key, $value)
{
if(is_null($key)) return static::$config = $value;
$keys = explode('.', $key);
$array =& static::$config;
while(count($keys) > 1){
$key = array_shift($keys);
if(!isset($array[$key]) || !is_array($array[$key])){
$array[$key] = array();
}
$array =& $array[$key];
}
$array[array_shift($keys)] = $value;
return $array;
}
/**
* Get a config value using "dot" notation.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public static function get($key, $default = null)
{
if(is_null($key)) return static::$config;
if(isset(static::$config[$key])) return static::$config[$key];
$pointer = static::$config;
foreach(explode('.', $key) as $segment){
if(!is_array($pointer) || !array_key_exists($segment, $pointer)){
return $default;
}
$pointer = $pointer[$segment];
}
return $pointer;
}
/**
* Check if a config value exists using "dot" notation.
*
* @param string $key
* @return bool
*/
public static function has($key)
{
if(empty(static::$config) || is_null($key)) return false;
if(array_key_exists($key, static::$config)) return true;
$pointer = static::$config;
foreach(explode('.', $key) as $segment){
if(!is_array($pointer) || !array_key_exists($segment, $pointer)){
return false;
}
$pointer = $pointer[$segment];
}
return true;
}
}

View file

@ -0,0 +1,524 @@
<?php
class Database
{
/**
* @var Database
*/
protected static $instance = null;
/**
* @var mysqli
*/
protected $db;
/**
* @var string
*/
protected $config;
/**
* @var string
*/
protected $lastQuery;
/**
* @param string $host
* @param string $user
* @param string $password
* @param string $database
*
* @throws Exception
*/
protected function __construct($host, $user, $password, $database)
{
if(!static::isInitialized()){
$this->config = $database;
try{
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$this->db = new mysqli($host, $user, $password, $database);
}
catch(mysqli_sql_exception $e){
throw new Exception('Unable to connect to the database.', 0, $e);
}
}
}
protected function __clone()
{
}
/**
* @return Database
*
* @throws Exception
*/
public static function getInstance()
{
if(!static::isInitialized()){
throw new Exception('Database must be initialized before using it (see Database::init).');
}
return static::$instance;
}
/**
* @param Database $instance
*
* @codeCoverageIgnore
*/
protected static function setInstance($instance)
{
static::$instance = $instance;
}
/**
* @param string $host
* @param string $user
* @param string $password
* @param string $database
*
* @throws InvalidArgumentException
* @throws Exception
*
* @codeCoverageIgnore
*/
public static function init($host, $user = null, $password = null, $database = null)
{
if(!static::isInitialized()){
if(is_array($host)){
$database = isset($host['database']) ? $host['database'] : $database;
$password = isset($host['password']) ? $host['password'] : $password;
$user = isset($host['user']) ? $host['user'] : $user;
$host = isset($host['host']) ? $host['host'] : $host;
}
if(is_null($host) || is_null($user) || is_null($password) || is_null($database)){
throw new InvalidArgumentException('Missing parameters for database initialization.');
}
static::setInstance(
new static($host, $user, $password, $database)
);
}
}
/**
* @return bool
*/
public static function isInitialized()
{
return !is_null(static::$instance);
}
/**
* Execute query
*
* @param string $query
*
* @return bool|mysqli_result
*
* @throws DatabaseException
*/
public function query($query)
{
$this->lastQuery = $query;
$result = $this->db->query($query);
if($this->db->errno !== 0){
$ex = new DatabaseException('There was an error running the query ['.$this->db->error.']');
if(!is_null($this->lastQuery)){
$ex->setQuery($this->lastQuery);
}
throw $ex;
}
return $result;
}
/**
* @return mixed
*/
public function getInsertId()
{
return $this->db->insert_id;
}
/**
* Escape string
*
* @param string $input
*
* @return string
*/
public function escape($input)
{
return $this->db->real_escape_string($input);
}
/**
* @param string $table
* @param array $conditions
* @param string $conditionConnector
* @param null $orderBy
* @param int $limit
*
* @return bool|mysqli_result
*
* @throws DatabaseException
*/
public function select($table, $conditions = array(), $conditionConnector = 'AND', $orderBy = null, $limit = 0)
{
return $this->query(
sprintf(
"SELECT * FROM `%s` %s%s%s",
$table,
static::helperWhere($conditions, $conditionConnector),
static::helperOrderBy($orderBy),
static::helperLimit($limit)
)
);
}
/**
* Insert into table
*
* @param string $table
* @param array $values
*
* @return mixed
*
* @throws DatabaseException
*/
public function insert($table, $values)
{
if(count($values) === 0){
return null;
}
$this->query(
sprintf(
"INSERT INTO `%s` (%s) VALUES %s",
$table,
static::helperAttributeList(array_keys($values)),
static::helperValueList(array_values($values))
)
);
return $this->getInsertId();
}
/**
* Update table
*
* @param string $table
* @param array $values
* @param array $conditions
* @param string $conditionConnector
*
* @throws DatabaseException
*/
public function update($table, $values, $conditions = array(), $conditionConnector = 'AND')
{
if(count($values) === 0){
return;
}
$sqlValues = array();
foreach($values as $attribute => $value){
$sqlValues[] = array($attribute, '=', $value);
}
$this->query(
sprintf(
"UPDATE `%s` SET %s %s",
$table,
static::helperConditionList($sqlValues, ','),
static::helperWhere($conditions, $conditionConnector)
)
);
}
/**
* Count in table
*
* @param string $table
* @param string $byAttribute
* @param array $conditions
* @param string $conditionConnector
*
* @return int
*
* @throws DatabaseException
*/
public function count($table, $byAttribute, $conditions = array(), $conditionConnector = 'AND')
{
$result = $this->query(
sprintf(
"SELECT COUNT(`%s`) FROM `%s` %s",
$byAttribute,
$table,
static::helperWhere($conditions, $conditionConnector)
)
);
return intval($result->fetch_array(MYSQLI_NUM)[0]);
}
/**
* @param string $table
* @param string $attribute
* @param mixed $value
*
* @throws DatabaseException
*/
public function delete($table, $attribute, $value)
{
$sql = sprintf(
"DELETE FROM `%s` %s",
$table,
static::helperWhere(array($attribute, $value))
);
$this->query($sql);
}
/**
* @param string $potentialKeyword
*
* @return bool
*/
protected static function isKeyword($potentialKeyword)
{
return in_array(
strtoupper($potentialKeyword),
array('AS', 'ASC', 'DESC')
);
}
/**
* @param array $attributes
*
* @return string
*/
public static function helperAttributeList($attributes)
{
$sqlAttributes = array();
foreach($attributes as $attribute){
if(is_string($attribute)){ // raw
$sqlAttributes[] = $attribute;
continue;
}
if(!is_array($attribute)){
$attribute = array($attribute);
}
$sqlPieces = array();
for($i = 0; $i < count($attribute); ++$i){
if(static::isKeyword($attribute[$i])){
$sqlPieces[] = sprintf("%s", $attribute[$i]);
}
elseif(isset($attribute[$i + 1]) && !static::isKeyword($attribute[$i + 1])){
$sqlPieces[] = sprintf("`%s`.`%s`", $attribute[$i], $attribute[++$i]);
}
else{
$sqlPieces[] = sprintf("`%s`", $attribute[$i]);
}
}
$sqlAttributes[] = implode(" ", $sqlPieces);
}
return sprintf(
"%s",
implode(', ', $sqlAttributes)
);
}
/**
* @param mixed $value
*
* @return string
*/
public static function helperValue($value)
{
if(is_null($value) || (is_string($value) && strtoupper($value) === 'NULL')){
return "NULL";
}
elseif(is_array($value)){
return static::helperValueList($value);
}
return sprintf(
"'%s'",
static::getInstance()->escape($value)
);
}
/**
* @param array $values
*
* @return string
*/
public static function helperValueList($values)
{
$sqlValues = array();
foreach($values as $val){
$sqlValues[] = static::helperValue($val);
}
return sprintf(
"(%s)",
implode(', ', $sqlValues)
);
}
/**
* @param array $conditions
* array('attr', '=', '3') => "`attr` = '3'"
* array(
* array('`attr` = '3') (raw SQL) => `attr` = '3'
* array('attr', 3) => `attr` = '3'
* array('attr', '=', '3') => `attr` = '3'
* array('attr', '<=', 3) => `attr` <= '3'
* array('attr', 'LIKE', '%asd') => `attr` LIKE '%asd'
* array('attr', 'IS', null) => `attr` IS NULL
* array('attr', 'IS NOT', null) => `attr` IS NOT NULL
* )
* @param string $conditionConnector AND, OR
*
* @return string
*/
public static function helperConditionList($conditions, $conditionConnector = 'AND')
{
// detect non nested array
if(count($conditions) > 0 && !is_array($conditions[0])){
$conditions = array($conditions);
}
$conditionConnector = strtoupper($conditionConnector);
if(in_array($conditionConnector, array('AND', 'OR'))){
$conditionConnector = " ".$conditionConnector;
}
$values = array();
foreach($conditions as $val){
switch(count($val)){
case 1:
// raw
$values[] = $val;
break;
case 2:
$v = static::helperValue($val[1]);
$values[] = sprintf("`%s` = %s", $val[0], $v);
break;
case 3:
$v = static::helperValue($val[2]);
$values[] = sprintf("`%s` %s %s", $val[0], strtoupper($val[1]), $v);
break;
}
}
return implode($conditionConnector." ", $values);
}
/**
* @param array $conditions
* @param string $conditionConnector AND, OR
*
* @return string
*/
public static function helperWhere($conditions, $conditionConnector = 'AND')
{
if(count($conditions) > 0){
return sprintf(
" WHERE %s",
static::helperConditionList($conditions, $conditionConnector)
);
}
return "";
}
/**
* @param array|null $orderBy Examples below:
* null => ""
* array() => ""
* array('attr1' => 'asc', 'attr2' => 'desc') => " ORDER BY `attr1` ASC, `attr2` DESC"
* array('attr1') => " ORDER BY `attr1` ASC"
*
* @return string
*/
public static function helperOrderBy($orderBy = null)
{
if(is_null($orderBy) || count($orderBy) === 0){
return "";
}
$values = array();
foreach($orderBy as $key => $val){
if(is_int($key)){
$values[] = array($val);
}
else{
$values[] = array($key, strtoupper($val));
}
}
return sprintf(
" ORDER BY %s",
static::helperAttributeList($values)
);
}
/**
* @param int|array $limit
* 0 => ""
* 3 => " LIMIT 3"
* array(3, 4) => " LIMIT 3,4"
*
* @return string
*/
public static function helperLimit($limit = 0)
{
if(is_array($limit) && count($limit) == 2){
$limit = $limit[0].",".$limit[1];
}
if(is_string($limit) || (is_int($limit) && $limit > 0)){
return sprintf(
" LIMIT %s",
$limit
);
}
return "";
}
}

View file

@ -0,0 +1,31 @@
<?php
class DatabaseException extends Exception
{
/** @var string */
protected $query;
/**
* Set the executed SQL query
*
* @param string $query
*
* @return $this
*/
public function setQuery($query)
{
$this->query = $query;
return $this;
}
/**
* Get the executed SQL query
*
* @return string
*/
public function getQuery()
{
return $this->query;
}
}

View file

@ -0,0 +1,146 @@
<?php
class Message
{
const TYPE_FAIL = 'fail';
const TYPE_ERROR = 'fail';
const TYPE_WARNING = 'warning';
const TYPE_SUCCESS = 'success';
/**
* @var Message
*/
protected static $instance;
/**
* Holds all messages
*
* @var array
*/
protected $messages = array();
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* @codeCoverageIgnore
*/
private function __clone()
{
}
/**
* @return Message
*
* @codeCoverageIgnore
*/
public static function getInstance()
{
if(is_null(static::$instance)){
static::$instance = new static();
}
return static::$instance;
}
/**
* Add a new message
*
* @param string $type Supported types: success, fail, info
* @param string $text
*/
public function add($type, $text)
{
if(!in_array($type, array(static::TYPE_FAIL, static::TYPE_ERROR, static::TYPE_WARNING, static::TYPE_SUCCESS))){
throw new InvalidArgumentException;
}
$this->messages[] = array(
'type' => $type,
'message' => $text,
);
}
/**
* Add a new success message
*
* @param string $text
*/
public function fail($text)
{
$this->add(static::TYPE_FAIL, $text);
}
/**
* Add a new success message
*
* @param string $text
*/
public function error($text)
{
$this->add(static::TYPE_ERROR, $text);
}
/**
* Add a new success message
*
* @param string $text
*/
public function warning($text)
{
$this->add(static::TYPE_WARNING, $text);
}
/**
* Add a new success message
*
* @param string $text
*/
public function success($text)
{
$this->add(static::TYPE_SUCCESS, $text);
}
/**
* Render all messages
*
* @param null|string $type null = render all
*
* @return string
*/
public function render($type = null)
{
$out = '';
if(count($this->messages) > 0){
$out .= '<div class="notifications">';
foreach($this->messages as $message){
if(is_null($type) || $type == $message['type']){
$out .= '<div class="notification notification-'.$message['type'].'">'.$message['message'].'</div>';
}
}
$out .= '</div>';
}
return $out;
}
}

View file

@ -0,0 +1,299 @@
<?php
class Router
{
const METHOD_GET = 'GET';
const METHOD_POST = 'POST';
/**
* @var array
*/
private static $routes = array();
/**
* @var array
*/
private static $errorPages = array(
404 => 'include/php/template/error/not-found.php',
403 => 'include/php/template/error/not-allowed.php'
);
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* @codeCoverageIgnore
*/
private function __clone()
{
}
/**
* @param array $routes
*/
public static function init($routes)
{
static::$routes = $routes;
}
/**
* @param string $method
*
* @return bool
*/
protected static function isValidMethod($method)
{
return in_array(
$method,
array(
static::METHOD_GET,
static::METHOD_POST
)
);
}
/**
* @param string|array $methods
* @param string $pattern
* @param callable|array|string $routeConfig
* @param array $permission
*
* @throws Exception
*/
public static function addRoute($methods, $pattern, $routeConfig, $permission = null)
{
if(!is_array($methods)){
$methods = array($methods);
}
$config = array(
'pattern' => $pattern,
'config' => $routeConfig,
'permission' => $permission,
);
foreach($methods as $method){
$method = strtoupper($method);
if(!static::isValidMethod($method)){
throw new Exception('Unsupported HTTP method "'.$method.'".');
}
if(!isset(static::$routes[$method])){
static::$routes[$method] = array();
}
static::$routes[$method][] = $config;
}
}
/**
* @param string $pattern
* @param callable|string $routeConfig
* @param array $permission
*/
public static function addGet($pattern, $routeConfig, $permission = null)
{
static::addRoute(static::METHOD_GET, $pattern, $routeConfig, $permission);
}
/**
* @param string $pattern
* @param callable|string $routeConfig
* @param array $permission
*/
public static function addPost($pattern, $routeConfig, $permission = null)
{
static::addRoute(static::METHOD_POST, $pattern, $routeConfig, $permission);
}
/**
* @param string $pattern
* @param callable|string $routeConfig
* @param array $permission
*/
public static function addMixed($pattern, $routeConfig, $permission = null)
{
static::addRoute(array(static::METHOD_GET, static::METHOD_POST), $pattern, $routeConfig, $permission);
}
/**
* @param string $url
* @param string $method
*
* @return string
*
* @throws Exception
*/
public static function execute($url, $method = self::METHOD_GET)
{
$method = strtoupper($method);
if(!static::isValidMethod($method) && !isset(self::$routes[$method])){
throw new Exception('Unsupported HTTP method "'.$method.'".');
}
if(isset(self::$routes[$method])){
foreach(self::$routes[$method] as $route){
if(rtrim($route['pattern'], '/') === rtrim($url, '/')){
if(!is_null($route['permission'])){
if(!Auth::isLoggedIn() || !Auth::hasPermission($route['permission'])){
return static::loadAndBufferOutput(static::$errorPages[403]);
}
}
return static::resolveRouteConfig($route['config']);
}
}
}
return static::loadAndBufferOutput(static::$errorPages[404]);
}
/**
* @return string
*/
public static function executeCurrentRequest()
{
return static::execute(
static::getCurrentUrlPath(),
isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : static::METHOD_GET
);
}
/**
* @param int $errorNumber
*
* @return string|null
*
* @codeCoverageIgnore
*/
public static function displayError($errorNumber)
{
$errorPage = isset(static::$errorPages[$errorNumber])
? static::loadAndBufferOutput(static::$errorPages[$errorNumber])
: '';
echo Router::loadAndBufferOutput(
'include/php/template/layout.php',
array(
'content' => $errorPage,
)
);
exit;
}
/**
* @param bool $removeGetParameters
*
* @return string
*/
public static function getCurrentUrlPath($removeGetParameters = true)
{
$baseUrl = parse_url(Config::get('base_url'));
$basePath = isset($baseUrl['path']) ? rtrim($baseUrl['path'], '/') : '';
$url = $_SERVER['REQUEST_URI'];
if($removeGetParameters){
$url = preg_replace('/\?.*/', '', $url); // Trim GET Parameters
}
// Trim all leading slashes
$url = rtrim($url, '/');
if(!empty($basePath) && ($basePathPos = strpos($url, $basePath)) === 0){
$url = substr($url, strlen($basePath));
}
return $url;
}
/**
* @param array $config
*
* @return string
*/
public static function resolveRouteConfig($config)
{
if(is_string($config)){
if(file_exists($config)){
return static::loadAndBufferOutput($config);
}
}
elseif(is_callable($config) && $config instanceof Closure){
return $config();
}
return static::loadAndBufferOutput(static::$errorPages[404]);
}
/**
* @param string $file
* @param array $variables
*
* @return string
*/
public static function loadAndBufferOutput($file, $variables = array())
{
ob_start();
extract($variables);
require $file;
return ob_get_clean();
}
/**
* Generate full url
*
* @param string $url
*
* @return string
*/
public static function url($url = '')
{
return rtrim(
sprintf(
'%s/%s',
rtrim(Config::get('base_url'), '/'),
trim($url, '/')
),
'/'
);
}
/**
* Redirect user to an url
*
* @param string $url
*
* @codeCoverageIgnore
*/
public static function redirect($url)
{
header("Location: ".static::url($url));
exit;
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* Register automatic loading for dependency injection
*/
spl_autoload_register(function($class){
if(file_exists('include/php/models/'.$class.'.php')){
include_once 'include/php/models/'.$class.'.php';
}
elseif(file_exists('include/php/classes/'.$class.'.php')){
include_once 'include/php/classes/'.$class.'.php';
}
});
/**
* Start session as the very first thing
*/
session_start();
session_regenerate_id();
/**
* Load some global accessible functions
*/
require_once 'include/php/global.inc.php';
/**
* Setting up
*/
if(file_exists('config/config.php') && !isset($_SESSION['installer'])){
/**
* Loading config
*/
$configValues = require_once 'config/config.php';
if(!is_array($configValues)){
throw new Exception('Config must return an array of config values.');
}
Config::init($configValues);
/**
* Establish database connection
*/
Database::init(Config::get('mysql'));
/**
* Initialize Authentication (Login User if in session)
*/
Auth::init();
/**
* Setup routes
*/
require_once 'include/php/routes.inc.php';
}
else{
/**
* Switching to install mode
*/
define('INSTALLER_ENABLED', true);
}

166
include/php/global.inc.php Normal file
View file

@ -0,0 +1,166 @@
<?php
/**
* Add message to logfile
*
* @param string $text
*
* @throws Exception
*/
function writeLog($text)
{
if(Config::get('options.enable_logging', false) && Config::has('log_path')){
$logDestination = realpath(Config::get('log_path')).DIRECTORY_SEPARATOR."webmum.log";
if(is_writable(Config::get('log_path'))){
if($logfile = fopen($logDestination, "a")){
fwrite($logfile, date('M d H:i:s').": ".$text."\n");
fclose($logfile);
}
else{
throw new Exception('Unable to create or open logfile "'.$logDestination.'" in root directory!');
}
}
else{
throw new Exception('Directory "'.Config::get('log_path').'" isn\'t writable');
}
}
}
/**
* Split comma, semicolon or newline separated list of emails to string
*
* @param string $input
*
* @return array
*/
function stringToEmails($input)
{
$list = explode(
'|',
str_replace(
array(' ', ',', ';', "\r\n", "\r", "\n", '|', ':'),
'|',
$input
)
);
foreach($list as $i => &$email){
if(empty($email)){
unset($list[$i]);
}
}
$emails = array_values(
array_unique(
array_map(
'formatEmail',
$list
)
)
);
asort($emails);
return $emails;
}
/**
* List of emails to comma or $glue separated list string
*
* @param array $list
* @param string $glue
*
* @return string
*/
function emailsToString($list, $glue = ',')
{
if(is_string($list)){
return $list;
}
return implode($glue, $list);
}
/**
* Format single email address
*
* @param string $input
*
* @return string
*/
function formatEmail($input)
{
return strtolower(trim($input));
}
/**
* Format email addresses (single, multiple in separated list, or array of email addresses)
*
* @param string|array $input
* @param string $glue
*
* @return string
*/
function formatEmails($input, $glue)
{
if(!is_array($input)){
$input = stringToEmails($input);
}
return emailsToString($input, $glue);
}
/**
* Format email addresses for text output (not in an input field)
*
* @param string|array $input
* @return string
*/
function formatEmailsText($input)
{
return formatEmails(
$input,
str_replace(PHP_EOL, '<br>', Config::get('frontend_options.email_separator_text', ', '))
);
}
/**
* Format email addresses for form output (in an input field)
*
* @param string|array $input
* @return string
*/
function formatEmailsForm($input)
{
return strip_tags(
formatEmails(
$input,
Config::get('frontend_options.email_separator_form', ',')
)
);
}
/**
* @param string $textPattern
* @param int|mixed $value
* @return string
*/
function textValue($textPattern, $value)
{
$text = str_replace(
array('_', ':val:', ':value:'),
$value,
$textPattern
);
if(is_numeric($value) && $value > 1){
$text .= 's';
}
return $text;
}

View file

@ -0,0 +1,457 @@
<?php
abstract class AbstractModel
{
/**
* Db table for find methods
*
* @var string
*/
public static $table;
/**
* Db id attribute for find methods
*
* @var string
*/
public static $idAttribute;
/**
* Mapping model attributes and database attributes for saving
*
* @var array
*/
protected static $attributeDbAttributeMapping = null;
/**
* Initialize Model
*/
protected static function initModel()
{
}
/**
* Get mapped db attribute
*
* @param string $name
* @return string
*/
public static function attr($name)
{
static::initModel();
if(isset(static::$attributeDbAttributeMapping[$name])){
return static::$attributeDbAttributeMapping[$name];
}
return false;
}
/**
* Format or do other things before saving
*
* @param array $data
*
* @return array
*/
protected function preSave($data)
{
return $data;
}
/**
* Hold all data from a model
*
* @var mixed
*/
protected $data = array();
/**
* Constructor.
*
* @param array $data
*/
protected function __construct($data)
{
static::initModel();
if(isset($data[static::$idAttribute])){
$id = is_numeric($data[static::$idAttribute]) && strpos($data[static::$idAttribute], ',') === false
? intval($data[static::$idAttribute])
: $data[static::$idAttribute];
$this->setId($id);
}
}
/**
* Create a model from data
*
* @param array $data
*
* @return static|null The Model
*/
public static function create($data)
{
if(count($data) > 0){
return new static($data);
}
return null;
}
/**
* Create a model collection from data
*
* @param array $multiData
*
* @return ModelCollection|static[]
*/
public static function createMultiple($multiData = array())
{
$collection = new ModelCollection();
foreach($multiData as $data){
$model = static::create($data);
if(!is_null($model)){
if(is_null($model->getId())){
$collection->add($model);
}
else{
$collection->add($model, $model->getId());
}
}
}
return $collection;
}
/**
* @see create
*
* @param array $data
*
* @return AbstractModel|null
*/
public static function createAndSave($data)
{
$model = static::create($data);
if(!is_null($model)){
$model->save();
return $model;
}
return null;
}
/**
* @see createMultiple
*
* @param array $multiData
*
* @return ModelCollection|static[]
*/
public static function createMultipleAndSave($multiData = array())
{
$collection = new ModelCollection();
foreach($multiData as $data){
$model = static::createAndSave($data);
if(!is_null($model)){
$collection->add($model);
}
}
return $collection;
}
/**
* Create a model from mysqli result
*
* @param mysqli_result $result
*
* @return static|null
*/
public static function createFromDbResult($result)
{
if($result->num_rows === 0){
return null;
}
return static::create($result->fetch_assoc());
}
/**
* Create a model collection from mysqli result
*
* @param mysqli_result $result
*
* @return ModelCollection|static[]
*/
public static function createMultipleFromDbResult($result)
{
$rows = array();
while($row = $result->fetch_assoc()){
$rows[] = $row;
}
return static::createMultiple($rows);
}
/**
* @param string $attribute
* @param mixed $value
*/
public function setAttribute($attribute, $value)
{
$this->data[$attribute] = $value;
}
/**
* @param string $attribute
*
* @return mixed|null
*/
public function getAttribute($attribute)
{
if(isset($this->data[$attribute])){
$value = $this->data[$attribute];
if(is_array($value)){
return array_map('strip_tags', $value);
}
elseif(is_string($value)){
return strip_tags($value);
}
return $value;
}
return null;
}
/**
* @return mixed
*/
public function getId()
{
return $this->getAttribute('id');
}
/**
* @param mixed $value
*/
protected function setId($value)
{
$this->setAttribute('id', $value);
}
/**
* Find all models by raw sql
*
* @param $sql
* @param null|string $useSpecificModel
*
* @return ModelCollection|static[]
*/
public static function findAllRaw($sql, $useSpecificModel = null)
{
$result = Database::getInstance()->query($sql);
if(is_null($useSpecificModel)){
return static::createMultipleFromDbResult($result);
}
elseif(class_exists($useSpecificModel)){
return call_user_func_array(array($useSpecificModel, 'createMultipleFromDbResult'), array($result));
}
return new ModelCollection();
}
/**
* Find a model by raw sql
*
* @param $sql
* @param null|string $useSpecificModel
*
* @return AbstractModel
*/
public static function findRaw($sql, $useSpecificModel = null)
{
$result = Database::getInstance()->query($sql);
if(is_null($useSpecificModel)){
return static::createFromDbResult($result);
}
elseif(class_exists($useSpecificModel)){
return call_user_func_array(array($useSpecificModel, 'createFromDbResult'), array($result));
}
return null;
}
/**
* Find models by a condition
*
* @param array $conditions see helperConditionArray
* @param string $conditionConnector see helperConditionArray
* @param array|null $orderBy
* @param int $limit see helperLimit
*
* @return ModelCollection|static[]|AbstractModel|null
*/
public static function findWhere($conditions = array(), $conditionConnector = 'AND', $orderBy = null, $limit = 0)
{
static::initModel();
$result = Database::getInstance()->select(static::$table, $conditions, $conditionConnector, $orderBy, $limit);
if($limit === 1){
return static::createFromDbResult($result);
}
return static::createMultipleFromDbResult($result);
}
/**
* Find all models
*
* @param array|null $orderBy see helperOrderBy
*
* @return ModelCollection|static[]
*/
public static function findAll($orderBy = null)
{
return static::findWhere(array(), 'AND', $orderBy);
}
/**
* Find first model matching a condition
*
* @param array $conditions see helperConditionArray
* @param string $conditionConnector see helperConditionArray
* @param array|null $orderBy
*
* @return AbstractModel|null
*/
public static function findWhereFirst($conditions = array(), $conditionConnector = 'AND', $orderBy = null)
{
return static::findWhere($conditions, $conditionConnector, $orderBy, 1);
}
/**
* Find a model by id
*
* @param mixed $id
*
* @return AbstractModel|null
*/
public static function find($id)
{
static::initModel();
return static::findWhereFirst(array(static::$idAttribute, $id));
}
/**
* Save model data to database
*/
public function save()
{
$data = $this->preSave($this->data);
$values = array();
foreach(static::$attributeDbAttributeMapping as $attribute => $sqlAttribute){
if($sqlAttribute === static::$idAttribute){
continue;
}
$values[$sqlAttribute] = $data[$attribute];
}
if(is_null($this->getId())){
$insertId = Database::getInstance()->insert(static::$table, $values);
$this->setId(intval($insertId));
}
else{
Database::getInstance()->update(static::$table, $values, array(static::$idAttribute, $this->getId()));
}
}
/**
* Delete model from database
*
* @return bool
*/
public function delete()
{
if(!is_null($this->getId())){
Database::getInstance()->delete(static::$table, static::$idAttribute, $this->getId());
return true;
}
return false;
}
/**
* Count models by a condition
*
* @param array $conditions see helperConditionArray
* @param string $conditionConnector see helperConditionArray
*
* @return int
*/
public static function countWhere($conditions = array(), $conditionConnector = 'AND')
{
static::initModel();
return Database::getInstance()->count(static::$table, static::$idAttribute, $conditions, $conditionConnector);
}
/**
* Count all models
*
* @return int
*/
public static function count()
{
return static::countWhere();
}
}

View file

@ -0,0 +1,6 @@
<?php
abstract class AbstractMultiRedirect extends AbstractRedirect
{
}

View file

@ -0,0 +1,398 @@
<?php
abstract class AbstractRedirect extends AbstractModel
{
use DomainLimitTrait;
/**
* Db table for find methods
*
* @var string
*/
public static $table;
/**
* Db id attribute for find methods
*
* @var string
*/
public static $idAttribute;
/**
* Mapping model attributes and database attributes for saving
*
* @var array
*/
protected static $attributeDbAttributeMapping = null;
/**
* @var ModelCollection
*/
protected $conflictingUsers = null;
/**
* @inheritdoc
*/
protected static function initModel()
{
if(is_null(static::$attributeDbAttributeMapping)){
static::$table = Config::get('schema.tables.aliases', 'aliases');
static::$idAttribute = Config::get('schema.attributes.aliases.id', 'id');
static::$attributeDbAttributeMapping = array(
'id' => Config::get('schema.attributes.aliases.id', 'id'),
'source' => Config::get('schema.attributes.aliases.source', 'source'),
'destination' => Config::get('schema.attributes.aliases.destination', 'destination'),
'multi_hash' => Config::get('schema.attributes.aliases.multi_source', 'multi_source'),
);
if(Config::get('options.enable_user_redirects', false)){
static::$attributeDbAttributeMapping['is_created_by_user'] = Config::get('schema.attributes.aliases.is_created_by_user', 'is_created_by_user');
}
}
}
/**
* @inheritdoc
*/
protected function preSave($data)
{
$data = parent::preSave($data);
$data['source'] = emailsToString($data['source']);
$data['destination'] = emailsToString($data['destination']);
if(Config::get('options.enable_user_redirects', false)){
$data['is_created_by_user'] = $data['is_created_by_user'] ? 1 : 0;
}
return $data;
}
/**
* @inheritdoc
*/
protected function __construct($data)
{
parent::__construct($data);
$source = stringToEmails($data[static::attr('source')]);
$destination = stringToEmails($data[static::attr('destination')]);
if(get_called_class() === 'Alias' || get_called_class() === 'Redirect'){
$source = $source[0];
}
if(get_called_class() === 'Alias' || get_called_class() === 'MultiAlias'){
$destination = $destination[0];
}
$this->setSource($source);
$this->setDestination($destination);
if(Config::get('options.enable_multi_source_redirects', false)){
$this->setMultiHash($data[static::attr('multi_hash')]);
}
if(Config::get('options.enable_user_redirects', false)){
$this->setIsCreatedByUser($data[static::attr('is_created_by_user')]);
}
}
/**
* @inheritdoc
*/
public static function create($data)
{
if(get_called_class() !== 'AbstractRedirect'){
return parent::create($data);
}
$hasMultipleSources = array_key_exists(static::attr('source'), $data)
&& strpos($data[static::attr('source')], ',') !== false;
$hasMultipleDestinations = array_key_exists(static::attr('destination'), $data)
&& strpos($data[static::attr('destination')], ',') !== false;
if(Config::get('options.enable_multi_source_redirects', false) && $hasMultipleSources
){
if($hasMultipleDestinations){
return MultiRedirect::create($data);
}
else{
return MultiAlias::create($data);
}
}
else{
if($hasMultipleDestinations){
return Redirect::create($data);
}
else{
return Alias::create($data);
}
}
}
/**
* @return array|string
*/
public function getSource()
{
return $this->getAttribute('source');
}
/**
* @param string|array $value
*/
public function setSource($value)
{
if(is_array($value)){
$this->setAttribute('source', array_map('strtolower', $value));
}
else{
$this->setAttribute('source', strtolower($value));
}
}
/**
* @return array|string
*/
public function getDestination()
{
return $this->getAttribute('destination');
}
/**
* @param string|array $value
*/
public function setDestination($value)
{
if(is_array($value)){
$this->setAttribute('destination', array_map('strtolower', $value));
}
else{
$this->setAttribute('destination', strtolower($value));
}
}
/**
* @return string
*/
public function getMultiHash()
{
return $this->getAttribute('multi_hash');
}
/**
* @param string $value
*/
public function setMultiHash($value)
{
$this->setAttribute('multi_hash', $value);
}
/**
* @return bool
*/
public function isCreatedByUser()
{
return $this->getAttribute('is_created_by_user');
}
/**n
* @param bool $value
*/
public function setIsCreatedByUser($value)
{
$this->setAttribute('is_created_by_user', $value ? true : false);
}
/**
* @return array
*/
protected function getDomain()
{
$sources = $this->getSource();
if(is_string($sources)){
$sources = array($sources);
}
$domains = array();
foreach($sources as $source){
$emailParts = explode('@', $source);
if(count($emailParts) === 2){
$domains[] = $emailParts[1];
}
}
return array_unique($domains);
}
/**
* @return ModelCollection
*/
public function getConflictingUsers()
{
if(is_null($this->conflictingUsers)){
$sources = $this->getSource();
if(is_string($sources)){
$sources = array($sources);
}
$this->conflictingUsers = new ModelCollection();
foreach($sources as $source){
$user = User::findByEmail($source);
if(!is_null($user)){
$this->conflictingUsers->add($user);
}
}
}
return $this->conflictingUsers;
}
/**
* @param string $template
*
* @return array|string
*/
public function getConflictingMarkedSource($template = "<u>%email%</u>")
{
$conflictingUsers = $this->getConflictingUsers();
$sources = $this->getSource();
if(is_string($sources)){
$sources = array($sources);
}
foreach($conflictingUsers as $user){
if(($key = array_search($user->getEmail(), $sources)) !== false){
$sources[$key] = str_replace('%email%', $sources[$key], $template);
}
}
return $sources;
}
/**
* @inheritdoc
*/
public static function findAll($orderBy = null)
{
if(is_null($orderBy)){
$orderBy = array(static::attr('source'));
}
return parent::findAll($orderBy);
}
/**
* @return string
*/
private static function generateRedirectBaseQuery()
{
if(Config::get('options.enable_multi_source_redirects', false)){
return "SELECT r.* FROM (
SELECT
GROUP_CONCAT(g.`".static::$idAttribute."` ORDER BY g.`".static::$idAttribute."` SEPARATOR ',') AS `".static::$idAttribute."`,
GROUP_CONCAT(g.`".static::attr('source')."` SEPARATOR ',') AS `".static::attr('source')."`,
g.`".static::attr('destination')."`,
g.`".static::attr('multi_hash')."`
".(Config::get('options.enable_user_redirects', false) ? ", g.`".static::attr('is_created_by_user')."`" : "")."
FROM `".static::$table."` AS g
WHERE g.`".static::attr('multi_hash')."` IS NOT NULL
GROUP BY g.`".static::attr('multi_hash')."`
UNION
SELECT
s.`".static::$idAttribute."`,
s.`".static::attr('source')."`,
s.`".static::attr('destination')."`,
s.`".static::attr('multi_hash')."`
".(Config::get('options.enable_user_redirects', false) ? ", s.`".static::attr('is_created_by_user')."`" : "")."
FROM `".static::$table."` AS s
WHERE s.`".static::attr('multi_hash')."` IS NULL
) AS r";
}
else{
return "SELECT * FROM `".static::$table."`";
}
}
public static function findMultiAll($orderBy = null)
{
static::initModel();
if(is_null($orderBy)){
$orderBy = array(static::attr('source'));
}
$sql = static::generateRedirectBaseQuery()
.Database::helperOrderBy($orderBy);
return static::findAllRaw($sql);
}
public static function findMultiWhere($conditions = array(), $conditionConnector = 'AND', $orderBy = null, $limit = 0)
{
$sql = static::generateRedirectBaseQuery()
.Database::helperWhere($conditions, $conditionConnector)
.Database::helperOrderBy($orderBy)
.Database::helperLimit($limit);
if($limit === 1){
return static::findRaw($sql);
}
return static::findAllRaw($sql);
}
public static function findMultiWhereFirst($conditions = array(), $conditionConnector = 'AND', $orderBy = null)
{
return static::findMultiWhere($conditions, $conditionConnector, $orderBy, 1);
}
public static function findMulti($id)
{
static::initModel();
return static::findMultiWhereFirst(array(static::$idAttribute, $id));
}
/**
* @param array|User|null $limitedBy
*
* @return ModelCollection|static[]
*/
public static function getMultiByLimitedDomains($limitedBy = null)
{
return static::filterModelCollectionByLimitedDomains(static::findMultiAll(), $limitedBy);
}
}

View file

@ -0,0 +1,10 @@
<?php
/**
* @method string getSource()
* @method string getDestination()
*/
class Alias extends Redirect
{
}

View file

@ -0,0 +1,103 @@
<?php
class Domain extends AbstractModel
{
use DomainLimitTrait;
/**
* Db table for find methods
*
* @var string
*/
public static $table;
/**
* Db id attribute for find methods
*
* @var string
*/
public static $idAttribute;
/**
* Mapping model attributes and database attributes for saving
*
* @var array
*/
protected static $attributeDbAttributeMapping = null;
/**
* @inheritdoc
*/
protected static function initModel()
{
if(is_null(static::$attributeDbAttributeMapping)){
static::$table = Config::get('schema.tables.domains', 'domains');
static::$idAttribute = Config::get('schema.attributes.domains.id', 'id');
static::$attributeDbAttributeMapping = array(
'id' => Config::get('schema.attributes.domains.id', 'id'),
'domain' => Config::get('schema.attributes.domains.domain', 'domain'),
);
}
}
/**
* @inheritdoc
*/
protected function __construct($data)
{
parent::__construct($data);
$this->setDomain($data[static::attr('domain')]);
}
/**
* @return string
*/
public function getDomain()
{
return $this->getAttribute('domain');
}
/**
* @param string $value
*/
public function setDomain($value)
{
$this->setAttribute('domain', strtolower($value));
}
/**
* @return int
*/
public function countUsers()
{
return User::countWhere(
array(User::attr('domain'), $this->getDomain())
);
}
/**
* @return int
*/
public function countRedirects()
{
return AbstractRedirect::countWhere(
array(
array(AbstractRedirect::attr('source'), 'LIKE', "%@{$this->getDomain()}"),
array(AbstractRedirect::attr('destination'), 'LIKE', "%@{$this->getDomain()}")
),
'OR'
);
}
}

View file

@ -0,0 +1,69 @@
<?php
trait DomainLimitTrait
{
/**
* @param array|User|null $limitedBy
*
* @return bool
*/
public function isInLimitedDomains($limitedBy = null)
{
if(!Config::get('options.enable_admin_domain_limits', false)) {
return true;
}
if(is_null($limitedBy)){
return static::isInLimitedDomains(Auth::getUser());
}
elseif($limitedBy instanceof User) {
/** @var User $limitedBy */
return !$limitedBy->isDomainLimited() || static::isInLimitedDomains($limitedBy->getDomainLimits());
}
if(!is_array($limitedBy)){
throw new InvalidArgumentException;
}
/** @var string|array|string[] $domain */
$domain = $this->getDomain();
if(is_string($domain)) {
return in_array($domain, $limitedBy);
}
foreach($domain as $d){
if(!in_array($d, $limitedBy)) {
return false;
}
}
return true;
}
/**
* @param ModelCollection|static[] $collection
* @param array|User|null $limitedBy
*
* @return ModelCollection|static[]
*/
protected static function filterModelCollectionByLimitedDomains($collection, $limitedBy = null)
{
return $collection->searchAll(function($model) use ($limitedBy){
/** @var static $model */
//var_dump($model->isInLimitedDomains($limitedBy), $model->getDomain());
return $model->isInLimitedDomains($limitedBy);
});
}
/**
* @param array|User|null $limitedBy
*
* @return ModelCollection|static[]
*/
public static function getByLimitedDomains($limitedBy = null)
{
return static::filterModelCollectionByLimitedDomains(static::findAll(), $limitedBy);
}
}

View file

@ -0,0 +1,278 @@
<?php
class ModelCollection implements Iterator, ArrayAccess, Countable
{
/**
* @var array|AbstractModel[]
*/
private $models = array();
/**
* Constructor.
*
* @param array|AbstractModel[] $array
*/
public function __construct($array = array())
{
if($this->isNumericArray($array)){
foreach($array as $model){
$this->add($model);
}
}
else{
foreach($array as $key => $model){
$this->add($model, $key);
}
}
}
/**
* @param array $array
*
* @return bool
*/
protected function isNumericArray($array)
{
return array_keys($array) === range(0, count($array) - 1)
&& count(array_filter($array, 'is_string')) === 0;
}
/**
* Adds a model to the collection,
* but won't replace if it exists with that key
*
* @param AbstractModel $model
* @param mixed|null $key
*/
public function add($model, $key = null)
{
if(is_null($model) || !($model instanceof AbstractModel)){
return;
}
if(is_null($key)){
$this->models[] = $model;
}
elseif(!$this->has($key)){
$this->models[$key] = $model;
}
}
/**
* Replace a model with given key
*
* @param AbstractModel $model
* @param mixed $key
*/
public function replace($model, $key)
{
if(is_null($model) || !($model instanceof AbstractModel)){
return;
}
$model[$key] = $model;
}
/**
* Delete a model by key
*
* @param mixed $key
*/
public function delete($key)
{
if($this->has($key)){
unset($this->models[$key]);
}
}
/**
* Check if collection has a model by key
*
* @param mixed $key
*
* @return bool
*/
public function has($key)
{
return isset($this->models[$key]);
}
/**
* Get a model from the collection by key
*
* @param mixed $key
*
* @return AbstractModel|null
*/
public function get($key)
{
if($this->has($key)){
return $this->models[$key];
}
return null;
}
/**
* Search a model in collection with a condition
*
* @param callable $callable Gives back if the search matches
*
* @return AbstractModel|null
*/
public function search($callable)
{
if(is_callable($callable)){
foreach($this->models as $model){
if($callable($model)){
return $model;
}
}
}
return null;
}
/**
* Search all models in collection with a condition
*
* @param callable $callable Gives back if the search matches
*
* @return static
*/
public function searchAll($callable)
{
$collection = new static;
if(is_callable($callable)){
foreach($this->models as $model){
if($callable($model)){
$collection->add($model);
}
}
}
return $collection;
}
/**
* Convert models to an array of strings
*
* @param callable $callable Gives back a string for a model
*
* @return array|string[]
*/
public function toStringArray($callable)
{
$strings = array();
if(is_callable($callable)){
foreach($this->models as $model){
$strings[] = $callable($model);
}
}
return $strings;
}
/**
* @inheritdoc
*/
public function current()
{
return current($this->models);
}
/**
* @inheritdoc
*/
public function next()
{
return next($this->models);
}
/**
* @inheritdoc
*/
public function key()
{
return key($this->models);
}
/**
* @inheritdoc
*/
public function valid()
{
return $this->current() !== false;
}
/**
* @inheritdoc
*/
public function rewind()
{
reset($this->models);
}
/**
* @inheritdoc
*/
public function offsetExists($offset)
{
return $this->has($offset);
}
/**
* @inheritdoc
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* @inheritdoc
*/
public function offsetSet($offset, $value)
{
$this->add($value, $offset);
}
/**
* @inheritdoc
*/
public function offsetUnset($offset)
{
$this->delete($offset);
}
/**
* @inheritdoc
*/
public function count()
{
return count($this->models);
}
}

View file

@ -0,0 +1,10 @@
<?php
/**
* @method array getSource()
* @method string getDestination()
*/
class MultiAlias extends AbstractMultiRedirect
{
}

View file

@ -0,0 +1,10 @@
<?php
/**
* @method array getSource()
* @method array getDestination()
*/
class MultiRedirect extends AbstractMultiRedirect
{
}

View file

@ -0,0 +1,10 @@
<?php
/**
* @method string getSource()
* @method array getDestination()
*/
class Redirect extends AbstractRedirect
{
}

467
include/php/models/User.php Normal file
View file

@ -0,0 +1,467 @@
<?php
class User extends AbstractModel
{
use DomainLimitTrait;
/**
* Db table for find methods
*
* @var string
*/
public static $table;
/**
* Db id attribute for find methods
*
* @var string
*/
public static $idAttribute;
/**
* Mapping model attributes and database attributes for saving
*
* @var array
*/
protected static $attributeDbAttributeMapping = null;
const ROLE_USER = 'user';
const ROLE_ADMIN = 'admin';
/**
* @var AbstractRedirect
*/
protected $conflictingRedirect = null;
/**
* @var ModelCollection|AbstractRedirect[]
*/
protected $redirects = null;
/**
* @inheritdoc
*/
protected static function initModel()
{
if(is_null(static::$attributeDbAttributeMapping)){
static::$table = Config::get('schema.tables.users', 'users');
static::$idAttribute = Config::get('schema.attributes.users.id', 'id');
static::$attributeDbAttributeMapping = array(
'id' => Config::get('schema.attributes.users.id', 'id'),
'username' => Config::get('schema.attributes.users.username', 'username'),
'domain' => Config::get('schema.attributes.users.domain', 'domain'),
'password_hash' => Config::get('schema.attributes.users.password', 'password'),
);
if(Config::get('options.enable_mailbox_limits', false)){
static::$attributeDbAttributeMapping['mailbox_limit'] = Config::get('schema.attributes.users.mailbox_limit');
}
if(Config::get('options.enable_user_redirects', false)){
static::$attributeDbAttributeMapping['max_user_redirects'] = Config::get('schema.attributes.users.max_user_redirects');
}
}
}
/**
* @inheritdoc
*/
protected function __construct($data)
{
parent::__construct($data);
$this->setUsername($data[static::attr('username')]);
$this->setDomain($data[static::attr('domain')]);
$this->setPasswordHash($data[static::attr('password_hash')]);
if(Config::get('options.enable_mailbox_limits', false)){
$this->setMailboxLimit($data[static::attr('mailbox_limit')]);
}
if(Config::get('options.enable_user_redirects', false)){
$this->setMaxUserRedirects($data[static::attr('max_user_redirects')]);
}
$this->setAttribute('role', static::getRoleByEmail($this->getEmail()));
}
/**
* @return string
*/
public function getUsername()
{
return $this->getAttribute('username');
}
/**
* @param string $value
*/
public function setUsername($value)
{
$this->setAttribute('username', strtolower($value));
}
/**
* @return string
*/
public function getDomain()
{
return $this->getAttribute('domain');
}
/**
* @param string $value
*/
public function setDomain($value)
{
$this->setAttribute('domain', strtolower($value));
}
/**
* @return string
*/
public function getEmail()
{
return $this->getUsername().'@'.$this->getDomain();
}
/**
* @return string
*/
public function getPasswordHash()
{
return $this->getAttribute('password_hash');
}
/**
* @param string $value
*/
public function setPasswordHash($value)
{
$this->setAttribute('password_hash', $value);
}
/**
* @return int
*/
public function getMailboxLimit()
{
return $this->getAttribute('mailbox_limit');
}
/**
* @param int $value
*/
public function setMailboxLimit($value)
{
$this->setAttribute('mailbox_limit', intval($value));
}
/**
* @return int
*/
public function getMaxUserRedirects()
{
return $this->getAttribute('max_user_redirects');
}
/**
* @param int $value
*/
public function setMaxUserRedirects($value)
{
$this->setAttribute('max_user_redirects', intval($value));
}
/**
* @param string $attr
* @param mixed $default
*
* @return mixed
*
* @throws Exception
*/
protected static function getAttributeDefaultValue($attr, $default)
{
static::initModel();
$sql = "SELECT DEFAULT(".static::attr($attr).") FROM `".static::$table."` LIMIT 1";
try {
$result = Database::getInstance()->query($sql);
if($result->num_rows === 1){
$row = $result->fetch_array();
return $row[0];
}
}
catch(Exception $e) {
if (strpos($e->getMessage(), 'doesn\'t have a default') !== false) {
throw new Exception('Database table "'.static::$table.'" is missing a default value for attribute "'.static::attr($attr).'".');
}
return $default;
}
return $default;
}
/**
* Get mailbox limit default via database default value
*
* @return int
*/
public static function getMailboxLimitDefault()
{
if(Config::get('options.enable_mailbox_limits', false)){
return intval(static::getAttributeDefaultValue('mailbox_limit', 0));
}
return 0;
}
/**
* Get max user redirects default via database default value
*
* @return int
*/
public static function getMaxUserRedirectsDefault()
{
if(Config::get('options.enable_user_redirects', false)){
return intval(static::getAttributeDefaultValue('max_user_redirects', 0));
}
return 0;
}
/**
* @return string
*/
public function getRole()
{
return $this->getAttribute('role');
}
/**
* @param string $email
*
* @return string
*/
private static function getRoleByEmail($email)
{
if(in_array($email, Config::get('admins', array()))){
return static::ROLE_ADMIN;
}
return static::ROLE_USER;
}
/**
* Is user limited by domain limits?
*
* @return bool
*/
public function isDomainLimited()
{
$adminDomainLimits = Config::get('admin_domain_limits', array());
return Config::get('options.enable_admin_domain_limits', false)
&& is_array($adminDomainLimits) && isset($adminDomainLimits[$this->getEmail()]);
}
/**
* Get domain limits, returns an empty array if user has no limits or ADMIN_DOMAIN_LIMITS_ENABLED is disabled
*
* @return array
*/
public function getDomainLimits()
{
if($this->isDomainLimited()){
$adminDomainLimits = Config::get('admin_domain_limits', array());
if(!is_array($adminDomainLimits[$this->getEmail()])){
throw new InvalidArgumentException('Config value of admin domain limits for email "'.$this->getEmail().'" needs to be of type array.');
}
return $adminDomainLimits[$this->getEmail()];
}
return array();
}
/**
* @return bool
*/
public function isAllowedToCreateUserRedirects()
{
return $this->getMaxUserRedirects() >= 0;
}
/**
* @return bool
*/
public function canCreateUserRedirects()
{
if(!$this->isAllowedToCreateUserRedirects()
|| (
$this->getMaxUserRedirects() > 0
&& $this->getSelfCreatedRedirects()->count() >= $this->getMaxUserRedirects()
)
){
return false;
}
return true;
}
/**
* @return AbstractRedirect
*/
public function getConflictingRedirect()
{
if(is_null($this->conflictingRedirect)){
$this->conflictingRedirect = AbstractRedirect::findWhereFirst(
array(AbstractRedirect::attr('source'), $this->getEmail())
);
}
return $this->conflictingRedirect;
}
/**
* @return ModelCollection|AbstractRedirect[]
*/
public function getRedirects()
{
if(is_null($this->redirects)){
$this->redirects = AbstractRedirect::findMultiWhere(
array(AbstractRedirect::attr('destination'), 'LIKE', '%'.$this->getEmail().'%')
);
}
return $this->redirects;
}
/**
* @return ModelCollection|AbstractRedirect[]
*/
public function getAnonymizedRedirects()
{
$redirects = $this->getRedirects();
foreach($redirects as $redirect){
$emails = $redirect->getDestination();
if(is_array($emails) && count($emails) > 1){
$redirect->setDestination(array($this->getEmail(), '&hellip;'));
}
}
return $redirects;
}
/**
* @return ModelCollection|AbstractRedirect[]
*/
public function getSelfCreatedRedirects()
{
$redirects = $this->getRedirects();
return $redirects->searchAll(
function($redirect) {
/** @var AbstractRedirect $redirect */
return $redirect->isCreatedByUser();
}
);
}
/**
* Change this users password, throws Exception if password is invalid.
*
* @param string $password
* @param string $passwordRepeated
*
* @throws AuthException
*/
public function changePassword($password, $passwordRepeated)
{
Auth::validateNewPassword($password, $passwordRepeated);
$passwordHash = Auth::generatePasswordHash($password);
$this->setPasswordHash($passwordHash);
$this->save();
}
/**
* @inheritdoc
*/
public static function findAll($orderBy = null)
{
if(is_null($orderBy)){
$orderBy = array(static::attr('domain'), static::attr('username'));
}
return parent::findAll($orderBy);
}
/**
* @param string $email
*
* @return User|null
*/
public static function findByEmail($email)
{
$emailInParts = explode("@", $email);
if(count($emailInParts) !== 2){
return null;
}
$username = $emailInParts[0];
$domain = $emailInParts[1];
return static::findWhereFirst(
array(
array(static::attr('username'), $username),
array(static::attr('domain'), $domain)
)
);
}
}

View file

@ -0,0 +1,55 @@
<?php
if(Auth::getUser()->isDomainLimited()){
Router::displayError(403);
}
if(isset($_POST['domain'])){
$inputDomain = $_POST['domain'];
if(!empty($inputDomain)){
$existingDomain = Domain::findWhere(array(Domain::attr('domain'), $inputDomain));
if(!is_null($existingDomain)){
Domain::createAndSave(
array(
Domain::attr('domain') => $inputDomain,
)
);
// Created domain successfull, redirect to overview
Router::redirect("admin/listdomains/?created=1");
}
else{
Message::getInstance()->fail("Domain already exists in database.");
}
}
else{
Message::getInstance()->fail("Empty domain couldn't be created.");
}
}
?>
<h1>Create new domain</h1>
<?php echo Message::getInstance()->render(); ?>
<div class="buttons">
<a class="button" href="<?php echo Router::url('admin/listdomains'); ?>">&#10092; Back to domain list</a>
</div>
<form class="form" action="" method="post" autocomplete="off">
<div class="input-group">
<label>Domain</label>
<div class="input">
<input type="text" name="domain" placeholder="domain.tld" autofocus required/>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Create domain</button>
</div>
</form>

View file

@ -0,0 +1,94 @@
<?php
if(Auth::getUser()->isDomainLimited()){
Router::displayError(403);
}
if(!isset($_GET['id'])){
// Domain id not set, redirect to overview
Router::redirect("admin/listdomains");
}
$id = $_GET['id'];
/** @var Domain $domain */
$domain = Domain::find($id);
if(is_null($domain)){
// Domain doesn't exist, redirect to overview
Router::redirect("admin/listdomains");
}
if(!$domain->isInLimitedDomains()){
Router::redirect("admin/listdomains/?missing-permission=1");
}
// Delete domain
if(isset($_POST['confirm'])){
$confirm = $_POST['confirm'];
if($confirm === "yes"){
// Check if admin domain is affected
$isAdminDomain = false;
foreach(Config::get('admins', array()) as $admin){
$parts = explode("@", $admin);
if(count($parts) === 2 && $parts[2] === $domain->getDomain()){
$isAdminDomain = true;
break;
}
}
if(!$isAdminDomain){
$users = User::findWhere(array(User::attr('domain'), $domain->getDomain()));
/** @var User $user */
foreach($users as $user){
$user->delete();
}
$domain->delete();
// Delete domain successfull, redirect to overview
Router::redirect("admin/listdomains/?deleted=1");
}
else{
// Cannot delete domain with admin emails, redirect to overview
Router::redirect("admin/listdomains/?adm_del=1");
}
}
else{
// Choose to not delete domain, redirect to overview
Router::redirect("admin/listdomains");
}
}
?>
<h1>Delete domain "<?php echo $domain->getDomain() ?>"?</h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('admin/listdomains'); ?>">&#10092; Back to domain list</a>
</div>
<form class="form" action="" method="post" autocomplete="off">
<div class="input-group">
<label>All mailboxes matching the domain will be deleted from the user database!</label>
<div class="input-info">Mailbox directories in the filesystem won't be affected.</div>
</div>
<div class="input-group">
<label for="confirm">Do you realy want to delete this domain?</label>
<div class="input">
<select name="confirm" autofocus required>
<option value="no">No!</option>
<option value="yes">Yes!</option>
</select>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Delete</button>
</div>
</form>

View file

@ -0,0 +1,87 @@
<?php
if(!isset($_GET['id'])){
// Redirect id not set, redirect to overview
Router::redirect("admin/listredirects");
}
$id = $_GET['id'];
/** @var AbstractRedirect $redirect */
$redirect = AbstractRedirect::findMulti($id);
if(is_null($redirect)){
// Redirect doesn't exist, redirect to overview
Router::redirect("admin/listredirects");
}
if(!$redirect->isInLimitedDomains()){
Router::redirect("admin/listredirects/?missing-permission=1");
}
if(isset($_POST['confirm'])){
$confirm = $_POST['confirm'];
if($confirm === "yes"){
if ($redirect instanceof AbstractMultiRedirect){
// Get single source rows of multi source redirect/alias instead
$hash = $redirect->getMultiHash();
$singleRedirects = AbstractRedirect::findWhere(array(AbstractRedirect::attr('multi_hash'), $hash));
/** @var AbstractRedirect $redirectToDelete */
foreach($singleRedirects as $redirectToDelete){
$redirectToDelete->delete();
}
}
else {
$redirect->delete();
}
// Delete redirect successfull, redirect to overview
Router::redirect("admin/listredirects/?deleted=1");
}
else{
// Choose to not delete redirect, redirect to overview
Router::redirect("admin/listredirects");
}
}
else{
?>
<h1>Delete redirection?</h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('admin/listredirects'); ?>">&#10092; Back to redirect list</a>
</div>
<form class="form" action="" method="post" autocomplete="off">
<div class="input-group">
<label>Source</label>
<div class="input-info"><?php echo formatEmailsText($redirect->getSource()); ?></div>
</div>
<div class="input-group">
<label>Destination</label>
<div class="input-info"><?php echo formatEmailsText($redirect->getDestination()); ?></div>
</div>
<div class="input-group">
<label for="confirm">Do you realy want to delete this redirect?</label>
<div class="input">
<select name="confirm" autofocus required>
<option value="no">No!</option>
<option value="yes">Yes!</option>
</select>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Delete</button>
</div>
</form>
<?php
}
?>

View file

@ -0,0 +1,150 @@
<?php
if(!isset($_GET['id'])){
// Redirect id not set, redirect to overview
Router::redirect('admin/listredirects');
}
$id = $_GET['id'];
/** @var User $user */
$user = User::find($id);
if(is_null($user)){
// User doesn't exist, redirect to overview
Router::redirect('admin/listusers');
}
if(!$user->isInLimitedDomains()){
Router::redirect('admin/listusers/?missing-permission=1');
}
// Delete user
if(isset($_POST['confirm'])){
$confirm = $_POST['confirm'];
if($confirm === 'yes'){
// Check if admin is affected
if(!in_array($user->getEmail(), Config::get('admins', array()))){
// Delete redirects of this user
if(isset($_POST['delete_redirects']) && $_POST['delete_redirects'] === 'yes'
&& isset($_POST['selected_redirects']) && is_array($_POST['selected_redirects'])
){
$redirectMultiIds = $_POST['selected_redirects'];
foreach($redirectMultiIds as $redirectMultiId){
$redirectIds = explode(',', $redirectMultiId);
foreach($redirectIds as $redirectId){
// Note: No Multi* selected, so there is only Alias & Redirect
$redirects = AbstractRedirect::findWhere(
array(
array(AbstractRedirect::attr('id'), $redirectId),
array(AbstractRedirect::attr('destination'), 'LIKE', '%'.$user->getEmail().'%')
)
);
/** @var AbstractRedirect $redirect */
foreach($redirects as $redirect){
if($redirect instanceof Alias) {
$redirect->delete();
}
elseif($redirect instanceof Redirect) {
$redirect->setDestination(
array_diff(
$redirect->getDestination(),
array($user->getEmail())
)
);
$redirect->save();
}
}
}
}
}
$user->delete();
// Delete user successful, redirect to overview
Router::redirect('admin/listusers/?deleted=1');
}
else{
// Admin tried to delete himself, redirect to overview
Router::redirect('admin/listusers/?adm_del=1');
}
}
else{
// Choose to not delete user, redirect to overview
Router::redirect('admin/listusers');
}
}
$redirects = $user->getAnonymizedRedirects();
?>
<h1>Delete user "<?php echo $user->getEmail() ?>"?</h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('admin/listusers'); ?>">&#10092; Back to user list</a>
</div>
<form class="form" action="" method="post" autocomplete="off">
<div class="input-group">
<label>The user's mailbox will be deleted from the database only!</label>
<div class="input-info">The mailbox in the filesystem won't be affected.</div>
</div>
<div class="input-group">
<label>Redirects to this user:</label>
<?php if($redirects->count() > 0): ?>
<div class="input-info">Do you also want to delete the following redirects to this user?</div>
<table class="table table-compact">
<thead>
<tr>
<th></th>
<th>Source</th>
<th>Destination</th>
<tr>
</thead>
<tbody>
<?php foreach($redirects as $redirect): /** @var AbstractRedirect $redirect */ ?>
<tr>
<td><input type="checkbox" name="selected_redirects[]" value="<?php echo $redirect->getId(); ?>" checked></td>
<td><?php echo formatEmailsText($redirect->getSource()); ?></td>
<td><?php echo formatEmailsText($redirect->getDestination()); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="input">
<label>
<select name="delete_redirects" required>
<option value="no">Don't delete the redirects.</option>
<option value="yes">Yes, delete the selected redirects!</option>
</select>
</label>
</div>
<?php else: ?>
<div class="input-info">There are currently no redirects to this user.</div>
<?php endif; ?>
</div>
<div class="input-group">
<label for="confirm">Do you realy want to delete this user?</label>
<div class="input">
<label>
<select name="confirm" autofocus required>
<option value="no">No!</option>
<option value="yes">Yes!</option>
</select>
</label>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Delete</button>
</div>
</form>

View file

@ -0,0 +1,290 @@
<?php
$id = null;
$redirect = null;
if(isset($_GET['id'])){
$id = $_GET['id'];
/** @var AbstractRedirect $redirect */
$redirect = AbstractRedirect::findMulti($id);
if(is_null($redirect)){
// Redirect doesn't exist, redirect to overview
Router::redirect("admin/listredirects");
}
if(!$redirect->isInLimitedDomains()){
Router::redirect("admin/listredirects/?missing-permission=1");
}
}
if(isset($_POST['savemode'])){
$savemode = $_POST['savemode'];
$inputSources = stringToEmails($_POST['source']);
$inputDestinations = stringToEmails($_POST['destination']);
// validate emails
$emailErrors = array();
// basic email validation isn't working 100% correct though
foreach(array_merge($inputSources, $inputDestinations) as $email){
if(strpos($email, '@') === false){
$emailErrors[$email] = "Address \"{$email}\" isn't a valid email address.";
}
}
// validate source emails are on domains
if(Config::get('options.enable_validate_aliases_source_domain', true)){
$domains = Domain::getByLimitedDomains();
foreach($inputSources as $email){
if(isset($emailErrors[$email])){
continue;
}
$emailParts = explode('@', $email);
$searchResult = $domains->search(
function($domain) use ($emailParts){
/** @var Domain $domain */
return $domain->getDomain() === $emailParts[1];
}
);
if(is_null($searchResult)){
$emailErrors[$email] = "Domain of source address \"{$email}\" not in your domains.";
}
}
}
// validate no redirect loops
foreach(array_intersect($inputSources, $inputDestinations) as $email){
$emailErrors[$email] = "Address \"{$email}\" cannot be in source and destination in same redirect.";
}
if(count($emailErrors) > 0){
Message::getInstance()->fail(implode("<br>", $emailErrors));
}
else{
if(count($emailErrors) === 0 && $savemode === "edit" && !is_null($redirect)){
if(count($inputSources) > 0 && count($inputDestinations) > 0){
if(Config::get('options.enable_multi_source_redirects', false) && $redirect instanceof AbstractMultiRedirect){
$existingRedirectsToEdit = AbstractRedirect::findWhere(
array(AbstractRedirect::attr('multi_hash'), $redirect->getMultiHash())
);
}
else{
$existingRedirectsToEdit = AbstractRedirect::findWhere(
array(AbstractRedirect::attr('id'), $redirect->getId())
);
}
$emailsToCheck = $inputSources;
foreach($existingRedirectsToEdit as $r){
$key = array_search($r->getSource(), $emailsToCheck);
if($key !== false){
unset($emailsToCheck[$key]);
}
}
if(count($emailsToCheck) > 0){
$existingRedirectsOther = AbstractRedirect::findWhere(
array(
array(AbstractRedirect::attr('source'), 'IN', $emailsToCheck)
)
);
}
else{
$existingRedirectsOther = null;
}
if(!is_null($existingRedirectsOther) && $existingRedirectsOther->count() > 0){
$errorMessages = array();
/** @var AbstractRedirect $existingRedirect */
foreach($existingRedirectsOther as $id => $existingRedirect){
if(!$existingRedirectsToEdit->has($id)){
$errorMessages[] = "Source address \"{$existingRedirect->getSource()}\" is already redirected to some destination.";
}
}
Message::getInstance()->fail(implode("<br>", $errorMessages));
}
else{
// multi source handling
$hash = (count($inputSources) === 1) ? null : md5(emailsToString($inputSources));
foreach($inputSources as $sourceAddress){
$sourceAddress = formatEmail($sourceAddress);
/** @var AbstractRedirect $thisRedirect */
$thisRedirect = $existingRedirectsToEdit->search(
function($model) use ($sourceAddress){
/** @var AbstractRedirect $model */
return $model->getSource() === $sourceAddress;
}
);
if(!is_null($thisRedirect)){
// edit existing source
$thisRedirect->setSource($sourceAddress);
$thisRedirect->setDestination($inputDestinations);
$thisRedirect->setMultiHash($hash);
// Don't set 'isCreatedByUser' here, it will overwrite redirects created by user
$thisRedirect->save();
$existingRedirectsToEdit->delete($thisRedirect->getId()); // mark updated
}
else{
$data = array(
AbstractRedirect::attr('source') => $sourceAddress,
AbstractRedirect::attr('destination') => emailsToString($inputDestinations),
AbstractRedirect::attr('multi_hash') => $hash,
);
if(Config::get('options.enable_user_redirects', false)){
$data[AbstractRedirect::attr('is_created_by_user')] = false;
}
AbstractRedirect::createAndSave($data);
}
}
// Delete none updated redirect
foreach($existingRedirectsToEdit as $redirect){
$redirect->delete();
}
// Edit successfull, redirect to overview
Router::redirect("admin/listredirects/?edited=1");
}
}
else{
Message::getInstance()->fail("Redirect couldn't be edited. Fill out all fields.");
}
}
else if(count($emailErrors) === 0 && $savemode === "create"){
if(count($inputSources) > 0 && count($inputDestinations) > 0){
$existingRedirects = AbstractRedirect::findWhere(
array(AbstractRedirect::attr('source'), 'IN', $inputSources)
);
if($existingRedirects->count() > 0){
$errorMessages = array();
/** @var AbstractRedirect $existingRedirect */
foreach($existingRedirects as $existingRedirect){
$errorMessages[] = "Source address \"{$existingRedirect->getSource()}\" is already redirected to some destination.";
}
Message::getInstance()->fail(implode("<br>", $errorMessages));
}
else{
$inputDestination = emailsToString($inputDestinations);
$hash = (count($inputSources) === 1) ? null : md5(emailsToString($inputSources));
foreach($inputSources as $inputSource){
$data = array(
AbstractRedirect::attr('source') => $inputSource,
AbstractRedirect::attr('destination') => $inputDestination,
AbstractRedirect::attr('multi_hash') => $hash,
);
if(Config::get('options.enable_user_redirects', false)){
$data[AbstractRedirect::attr('is_created_by_user')] = false;
}
$a = AbstractRedirect::createAndSave($data);
}
// Redirect created, redirect to overview
Router::redirect("admin/listredirects/?created=1");
}
}
else{
Message::getInstance()->fail("Redirect couldn't be created. Fill out all fields.");
}
}
}
}
// Select mode
$mode = "create";
if(isset($_GET['id'])){
$mode = "edit";
}
$domains = Domain::getByLimitedDomains();
?>
<h1><?php echo ($mode === "create") ? 'Create' : 'Edit'; ?> Redirect</h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('admin/listredirects'); ?>">&#10092; Back to redirects list</a>
</div>
<div class="notification">
Please note that mailservers will prefer to deliver mails to redirects over mailboxes.<br>
So make sure you don't accidentally override a mailbox with a redirect.
</div>
<?php echo Message::getInstance()->render(); ?>
<?php if(Config::get('options.enable_validate_aliases_source_domain', true) && Auth::getUser()->isDomainLimited() && $domains->count() === 0): ?>
<div class="notification notification-fail">
You are listed for limited access to domains, but it seems there are no domains listed you can access.
</div>
<?php else: ?>
<form class="form" action="" method="post" autocomplete="off">
<input name="savemode" type="hidden" value="<?php echo $mode; ?>"/>
<div class="input-group">
<div class="input-info">Enter single or multiple addresses separated by comma, semicolon or newline.</div>
</div>
<div class="input-group">
<label for="source">Source</label>
<div class="input-info">
<?php if($domains->count() > 0): ?>
<?php if(Auth::getUser()->isDomainLimited()): ?>
You can create redirects for source addresses from these domains only:
<?php else: ?>
You can create redirects for every domain you want,<br>
but here's a list of domains managed by WebMUM:
<?php endif; ?>
<ul>
<?php foreach($domains as $domain): /** @var Domain $domain */ ?>
<li><?php echo $domain->getDomain(); ?></li>
<?php endforeach; ?>
</ul>
<?php else: ?>
There are no domains managed by WebMUM yet.
<?php endif; ?>
</div>
<div class="input">
<?php if(Config::get('options.enable_multi_source_redirects', false)): ?>
<textarea name="source" placeholder="Source" required autofocus><?php echo formatEmailsForm(isset($_POST['source']) ? $_POST['source'] : (is_null($redirect) ? '' : $redirect->getSource())); ?></textarea>
<?php else: ?>
<input type="text" name="source" placeholder="Source (single address)" required autofocus value="<?php echo formatEmailsForm(isset($_POST['source']) ? $_POST['source'] : (is_null($redirect) ? '' : $redirect->getSource())); ?>"/>
<?php endif; ?>
</div>
</div>
<div class="input-group">
<label for="destination">Destination</label>
<div class="input">
<textarea name="destination" placeholder="Destination" required><?php echo formatEmailsForm(isset($_POST['destination']) ? $_POST['destination'] : (is_null($redirect) ? '' : $redirect->getDestination())); ?></textarea>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Save settings</button>
</div>
</form>
<?php endif; ?>

View file

@ -0,0 +1,295 @@
<?php
$mailboxLimitDefault = User::getMailboxLimitDefault();
$canCreateUserRedirectsDefault = false;
$maxUserRedirectsDefault = User::getMaxUserRedirectsDefault();
$saveMode = (isset($_POST['savemode']) && in_array($_POST['savemode'], array('edit', 'create')))
? $_POST['savemode']
: null;
if(!is_null($saveMode)){
$inputPassword = isset($_POST['password']) ? $_POST['password'] : null;
$inputPasswordRepeated = isset($_POST['password_repeat']) ? $_POST['password_repeat'] : null;
$inputMailboxLimit = null;
if(Config::get('options.enable_mailbox_limits', false)){
$inputMailboxLimit = isset($_POST['mailbox_limit']) ? intval($_POST['mailbox_limit']) : $mailboxLimitDefault;
if(!$inputMailboxLimit === 0 && empty($inputMailboxLimit)){
$inputMailboxLimit = $mailboxLimitDefault;
}
}
$inputMaxUserRedirects = null;
if(Config::get('options.enable_user_redirects', false)){
$inputMaxUserRedirects = isset($_POST['max_user_redirects']) ? intval($_POST['max_user_redirects']) : $maxUserRedirectsDefault;
if(!$inputMaxUserRedirects === 0 && empty($inputMaxUserRedirects)){
$inputMaxUserRedirects = $maxUserRedirectsDefault;
}
if(isset($_POST['user_redirects']) && $_POST['user_redirects'] === 'no'){
$inputMaxUserRedirects = -1;
}
}
if($saveMode === 'edit'){
// Edit mode entered
if(!isset($_POST['id'])){
// User id not set, redirect to overview
Router::redirect("admin/listusers");
}
$inputId = $_POST['id'];
/** @var User $userToEdit */
$userToEdit = User::find($inputId);
if(is_null($userToEdit)){
// User doesn't exist, redirect to overview
Router::redirect("admin/listusers");
}
if(!$userToEdit->isInLimitedDomains()){
Router::redirect("admin/listusers/?missing-permission=1");
}
if(!is_null($inputMailboxLimit)){
$userToEdit->setMailboxLimit($inputMailboxLimit);
}
if(!is_null($inputMaxUserRedirects)){
$userToEdit->setMaxUserRedirects($inputMaxUserRedirects);
}
$passwordError = false;
// Is there a changed password?
if(!empty($inputPassword) || !empty($inputPasswordRepeated)){
try{
$userToEdit->changePassword($inputPassword, $inputPasswordRepeated);
}
catch(AuthException $passwordInvalidException){
Message::getInstance()->fail($passwordInvalidException->getMessage());
$passwordError = true;
}
}
$userToEdit->save();
if(!$passwordError){
// Edit user successfull, redirect to overview
Router::redirect("admin/listusers/?edited=1");
}
}
else if($saveMode === 'create'){
// Create mode entered
$inputUsername = isset($_POST['username']) ? $_POST['username'] : null;
$inputDomain = isset($_POST['domain']) ? $_POST['domain'] : null;
if(!empty($inputUsername)
&& !empty($inputDomain)
&& (!empty($inputPassword) || !empty($inputPasswordRepeated))
){
/** @var Domain $selectedDomain */
$selectedDomain = Domain::findWhereFirst(
array(Domain::attr('domain'), $inputDomain)
);
if(!is_null($selectedDomain)){
if(!$selectedDomain->isInLimitedDomains()){
Router::redirect("admin/listusers/?missing-permission=1");
}
/** @var User $user */
$user = User::findWhereFirst(
array(
array(User::attr('username'), $inputUsername),
array(User::attr('domain'), $selectedDomain->getDomain()),
)
);
// Check if user already exists
if(is_null($user)){
try{
// Check password then go on an insert user first
Auth::validateNewPassword($inputPassword, $inputPasswordRepeated);
$data = array(
User::attr('username') => $inputUsername,
User::attr('domain') => $selectedDomain->getDomain(),
User::attr('password_hash') => Auth::generatePasswordHash($inputPassword)
);
if(!is_null($inputMailboxLimit)){
$data[User::attr('mailbox_limit')] = $inputMailboxLimit;
}
if(!is_null($inputMaxUserRedirects)){
$data[User::attr('max_user_redirects')] = $inputMaxUserRedirects;
}
/** @var User $user */
$user = User::createAndSave($data);
// Redirect user to user list
Router::redirect("admin/listusers/?created=1");
}
catch(AuthException $passwordInvalidException){
Message::getInstance()->fail($passwordInvalidException->getMessage());
}
}
else{
Message::getInstance()->fail("User already exists in database.");
}
}
else{
Message::getInstance()->fail("The selected domain doesn't exist.");
}
}
else{
var_dump($_POST);
// Fields missing
Message::getInstance()->fail("Not all fields were filled out.");
}
}
}
// Select mode
$mode = "create";
if(isset($_GET['id'])){
$mode = "edit";
$id = $_GET['id'];
/** @var User $user */
$user = User::find($id);
if(is_null($user)){
// User doesn't exist, redirect to overview
Router::redirect("admin/listusers");
}
if(!$user->isInLimitedDomains()){
Router::redirect("admin/listusers/?missing-permission=1");
}
}
?>
<h1><?php echo ($mode === "create") ? "Create User" : "Edit user \"{$user->getEmail()}\""; ?></h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('admin/listusers'); ?>">&#10092; Back to user list</a>
</div>
<form class="form" action="" method="post" autocomplete="off">
<input type="hidden" name="savemode" value="<?php echo $mode; ?>"/>
<?php if($mode === "edit"): ?>
<input type="hidden" name="id" value="<?php echo $user->getId(); ?>"/>
<?php endif; ?>
<?php echo Message::getInstance()->render(); ?>
<?php if($mode === "edit"): ?>
<div class="input-group">
<label>Username and Group cannot be edited</label>
<div class="input-info">To rename or move a mailbox, you have to move in the filesystem first and create a new user here after.</div>
</div>
<?php else:
/** @var ModelCollection $domains */
$domains = Domain::getByLimitedDomains();
?>
<div class="input-group">
<label for="username">Username</label>
<div class="input">
<input type="text" name="username" placeholder="Username" value="<?php echo isset($_POST['username']) ? strip_tags($_POST['username']) : ''; ?>" autofocus required/>
</div>
</div>
<div class="input-group">
<label for="domain">Domain</label>
<div class="input">
<select name="domain" required>
<option value="">-- Select a domain --</option>
<?php foreach($domains as $domain): /** @var Domain $domain */ ?>
<option value="<?php echo $domain->getDomain(); ?>" <?php echo ((isset($_POST['domain']) && $_POST['domain'] === $domain->getDomain()) || ($mode === "create" && $domains->count() === 1)) ? 'selected' : ''; ?>>
<?php echo $domain->getDomain(); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<?php endif; ?>
<div class="input-group">
<label for="password">Password</label>
<?php if(Config::has('password.min_length')): ?>
<div class="input-info">The new password must be at least <?php echo Config::get('password.min_length'); ?> characters long.</div>
<?php endif; ?>
<div class="input input-action">
<input type="password" name="password" placeholder="New password" <?php echo ($mode === "create") ? 'required' : ''; ?> minlength="<?php echo Config::get('password.min_length', 0); ?>"/>
<button type="button" class="button" onclick="pass=generatePassword();this.form.password.value=pass;this.form.password_repeat.value=pass;this.form.password.type='text';this.form.password_repeat.type='text'">Generate password</button>
</div>
<div class="input">
<input type="password" name="password_repeat" placeholder="Repeat password" <?php echo ($mode === "create") ? 'required' : ''; ?> minlength="<?php echo Config::get('password.min_length', 0); ?>"/>
</div>
</div>
<?php if(Config::get('options.enable_mailbox_limits', false)): ?>
<div class="input-group">
<label>Mailbox limit</label>
<div class="input-info">The default limit is <?php echo $mailboxLimitDefault; ?> MB. Limit set to 0 means no limit in size.</div>
<div class="input input-labeled input-labeled-right">
<input name="mailbox_limit" type="number" value="<?php echo isset($_POST['mailbox_limit']) ? strip_tags($_POST['mailbox_limit']) : ((isset($user) && Config::get('options.enable_mailbox_limits', false)) ? $user->getMailboxLimit() : $mailboxLimitDefault); ?>" placeholder="Mailbox limit in MB" min="0" required/>
<span class="input-label">MB</span>
</div>
</div>
<?php endif; ?>
<?php if(Config::get('options.enable_user_redirects', false)):
$canCreateUserRedirects = isset($_POST['user_redirects'])
? $_POST['user_redirects'] === 'yes'
: (isset($user) ? $user->isAllowedToCreateUserRedirects() : $canCreateUserRedirectsDefault);
$maxUserRedirects = !$canCreateUserRedirects
? $maxUserRedirectsDefault
: (
isset($_POST['max_user_redirects'])
? strip_tags($_POST['max_user_redirects'])
: (
isset($user)
? $user->getMaxUserRedirects()
: $maxUserRedirectsDefault
)
)
?>
<div class="input-group">
<label>User can create redirects to himself?</label>
<div class="input-info">The maximum number of redirects can be limited by the limit redirects setting.</div>
<div class="input">
<select name="user_redirects" autofocus required>
<option value="no" <?php echo !$canCreateUserRedirects ? 'selected' : ''; ?>>Disabled</option>
<option value="yes" <?php echo $canCreateUserRedirects ? 'selected' : ''; ?>>Yes, creating redirects is allowed</option>
</select>
</div>
<div class="input-group">
<label>Limit redirects</label>
<div class="input-info">The default limit is "<?php echo $maxUserRedirectsDefault; ?>". Set to "0" means unlimited redirects.</div>
<div class="input input-labeled input-labeled-right">
<input name="max_user_redirects" type="number" value="<?php echo $maxUserRedirects; ?>" placeholder="Mailbox limit in MB" min="0" required/>
<span class="input-label">Redirects</span>
</div>
</div>
</div>
<?php endif; ?>
<div class="buttons">
<button type="submit" class="button button-primary">Save settings</button>
</div>
</form>

View file

@ -0,0 +1,66 @@
<?php
if(Auth::getUser()->isDomainLimited()){
Router::displayError(403);
}
if(isset($_GET['deleted']) && $_GET['deleted'] == "1"){
Message::getInstance()->success("Domain deleted successfully.");
}
else if(isset($_GET['created']) && $_GET['created'] == "1"){
Message::getInstance()->success("Domain created successfully.");
}
else if(isset($_GET['adm_del']) && $_GET['adm_del'] == "1"){
Message::getInstance()->fail("Domain couldn't be deleted because admin account would be affected.");
}
else if(isset($_GET['missing-permission']) && $_GET['missing-permission'] == "1"){
Message::getInstance()->fail("You don't have the permission to delete that domain.");
}
$domains = Domain::findAll();
?>
<h1>Domains</h1>
<?php if(!Auth::getUser()->isDomainLimited()): ?>
<div class="buttons">
<a class="button" href="<?php echo Router::url('admin/createdomain'); ?>">Create new domain</a>
</div>
<?php endif; ?>
<?php echo Message::getInstance()->render(); ?>
<?php if($domains->count() > 0): ?>
<table class="table">
<thead>
<tr>
<th>Domain</th>
<th>User count</th>
<th>Redirect count</th>
<th></th>
<tr>
</thead>
<tbody>
<?php foreach($domains as $domain): /** @var Domain $domain */ ?>
<tr>
<td><?php echo $domain->getDomain(); ?></td>
<td><?php echo $domain->countUsers(); ?></td>
<td><?php echo $domain->countRedirects(); ?></td>
<td>
<a href="<?php echo Router::url('admin/deletedomain/?id='.$domain->getId()); ?>">[Delete]</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th><?php echo textValue('_ domain', $domains->count()); ?></th>
</tr>
</tfoot>
</table>
<?php else: ?>
<div class="notification notification-warning">
There are currently no domains created you can manage.
</div>
<?php endif; ?>

View file

@ -0,0 +1,86 @@
<?php
if(isset($_GET['deleted']) && $_GET['deleted'] == "1"){
Message::getInstance()->success("Redirect deleted successfully.");
}
else if(isset($_GET['created']) && $_GET['created'] == "1"){
Message::getInstance()->success("Redirect created successfully.");
}
else if(isset($_GET['edited']) && $_GET['edited'] == "1"){
Message::getInstance()->success("Redirect edited successfully.");
}
else if(isset($_GET['missing-permission']) && $_GET['missing-permission'] == "1"){
Message::getInstance()->fail("You don't have the permission to edit/delete redirects of that domain.");
}
$redirects = AbstractRedirect::getMultiByLimitedDomains();
?>
<h1>Redirects</h1>
<?php if(!(Auth::getUser()->isDomainLimited() && count(Domain::getByLimitedDomains()) === 0)): ?>
<div class="buttons">
<a class="button" href="<?php echo Router::url('admin/editredirect'); ?>">Create new redirect</a>
</div>
<?php else: ?>
<div class="notification notification-warning">
You are listed for limited access to domains, but it seems there are no domains listed you can access.
</div>
<?php endif; ?>
<?php echo Message::getInstance()->render(); ?>
<?php if($redirects->count() > 0): ?>
<table class="table">
<thead>
<tr>
<th>Source</th>
<th>Destination</th>
<?php if(Config::get('options.enable_user_redirects', false)): ?>
<th>Created by user</th>
<?php endif; ?>
<th></th>
<th></th>
<tr>
</thead>
<tbody>
<?php foreach($redirects as $redirect): /** @var AbstractRedirect $redirect */ ?>
<tr<?php echo $redirect->getConflictingUsers()->count() > 0 ? ' class="warning"' : ''; ?>>
<td>
<?php if($redirect->getConflictingUsers()->count() > 0): ?>
<strong><?php echo $redirect->getConflictingUsers()->count() === 1 ? 'The marked redirect overrides a mailbox.' : 'The marked redirects override mailboxes.'; ?></strong><br>
<?php endif; ?>
<?php echo formatEmailsText($redirect->getConflictingMarkedSource()); ?>
</td>
<td><?php echo formatEmailsText($redirect->getDestination()); ?></td>
<?php if(Config::get('options.enable_user_redirects', false)): ?>
<td><?php echo $redirect->isCreatedByUser() ? 'Yes' : 'No'; ?></td>
<?php endif; ?>
<td>
<a href="<?php echo Router::url('admin/editredirect/?id='.$redirect->getId()); ?>">[Edit]</a>
</td>
<td>
<a href="<?php echo Router::url('admin/deleteredirect/?id='.$redirect->getId()); ?>">[Delete]</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th><?php echo textValue('_ redirect', $redirects->count()); ?></th>
<?php if(Config::get('options.enable_user_redirects', false)):
$userRedirectsCount = AbstractRedirect::countWhere(
array(AbstractRedirect::attr('is_created_by_user'), 1)
);
?>
<th></th>
<th><?php echo textValue('_ user redirect', $userRedirectsCount); ?></th>
<?php endif; ?>
</tr>
</tfoot>
</table>
<?php elseif(!(Auth::getUser()->isDomainLimited() && count(Domain::getByLimitedDomains()) === 0)): ?>
<div class="notification notification-warning">
There are currently no redirects created you can manage.
</div>
<?php endif; ?>

View file

@ -0,0 +1,102 @@
<?php
if(isset($_GET['deleted']) && $_GET['deleted'] == "1"){
Message::getInstance()->success("User deleted successfully.");
}
else if(isset($_GET['created']) && $_GET['created'] == "1"){
Message::getInstance()->success("User created successfully.");
}
else if(isset($_GET['edited']) && $_GET['edited'] == "1"){
Message::getInstance()->success("User edited successfully.");
}
else if(isset($_GET['adm_del']) && $_GET['adm_del'] == "1"){
Message::getInstance()->fail("Admin user cannot be deleted.");
}
else if(isset($_GET['missing-permission']) && $_GET['missing-permission'] == "1"){
Message::getInstance()->fail("You don't have the permission to edit/delete users of that domain.");
}
$users = User::getByLimitedDomains();
?>
<h1>List of all mailbox accounts</h1>
<?php if(!(Auth::getUser()->isDomainLimited() && count(Domain::getByLimitedDomains()) === 0)): ?>
<div class="buttons">
<a class="button button-small" href="<?php echo Router::url('admin/edituser'); ?>">Create new user</a>
</div>
<?php else: ?>
<div class="notification notification-warning">
You are listed for limited access to domains, but it seems there are no domains listed you can access.
</div>
<?php endif; ?>
<?php echo Message::getInstance()->render(); ?>
<?php if($users->count() > 0): ?>
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Domain</th>
<?php if(Config::get('options.enable_mailbox_limits', false)): ?>
<th>Mailbox Limit</th>
<?php endif; ?>
<th>Redirect count</th>
<?php if(Config::get('options.enable_user_redirects', false)): ?>
<th>User Redirects</th>
<?php endif; ?>
<th>Role</th>
<th></th>
<th></th>
<tr>
</thead>
<tbody>
<?php foreach($users as $user): /** @var User $user */ ?>
<tr<?php echo !is_null($user->getConflictingRedirect()) ? ' class="warning"' : ''; ?>>
<td>
<?php if(!is_null($user->getConflictingRedirect())): ?>
<strong>This mailbox is overridden by a redirect.</strong><br>
<?php endif; ?>
<?php echo $user->getUsername(); ?>
</td>
<td><?php echo $user->getDomain(); ?></td>
<?php if(Config::get('options.enable_mailbox_limits', false)): ?>
<td style="text-align: right"><?php echo ($user->getMailboxLimit() > 0) ? $user->getMailboxLimit().' MB' : 'No limit'; ?></td>
<?php endif; ?>
<td style="text-align: right">
<?php echo $user->getRedirects()->count(); ?>
</td>
<?php if(Config::get('options.enable_user_redirects', false)): ?>
<td>
<?php if($user->getMaxUserRedirects() < 0): ?>
Not Allowed
<?php elseif($user->getMaxUserRedirects() > 0): ?>
Limited (<?php echo $user->getMaxUserRedirects(); ?>)
<?php else: ?>
Unlimited
<?php endif; ?>
</td>
<?php endif; ?>
<td><?php echo ($user->getRole() === User::ROLE_ADMIN) ? 'Admin' : 'User'; ?></td>
<td>
<a href="<?php echo Router::url('admin/edituser/?id='.$user->getId()); ?>">[Edit]</a>
</td>
<td>
<a href="<?php echo Router::url('admin/deleteuser/?id='.$user->getId()); ?>">[Delete]</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th><?php echo textValue('_ user', $users->count()); ?></th>
</tr>
</tfoot>
</table>
<?php elseif(!(Auth::getUser()->isDomainLimited() && count(Domain::getByLimitedDomains()) === 0)): ?>
<div class="notification notification-warning">
There are currently no users created you can manage.
</div>
<?php endif; ?>

View file

@ -0,0 +1,11 @@
<h1>Admin Dashboard</h1>
<div class="buttons buttons-horizontal button-large">
<a class="button" href="<?php echo Router::url('admin/listusers'); ?>">Manage users</a>
<?php if(!Auth::getUser()->isDomainLimited()): ?>
<a class="button" href="<?php echo Router::url('admin/listdomains'); ?>">Manage domains</a>
<?php endif; ?>
<a class="button" href="<?php echo Router::url('admin/listredirects'); ?>">Manage redirects</a>
</div>

View file

@ -0,0 +1,51 @@
<?php
// If user is already logged in, redirect to start.
if(Auth::isLoggedIn()){
Router::redirect("private");
}
if(isset($_POST['email']) && isset($_POST['password'])){
if(empty($_POST['email']) || empty($_POST['password'])){
Message::getInstance()->fail('Please fill out both email and password fields.');
}
else {
// Start login
if(Auth::login($_POST['email'], $_POST['password'])){
Router::redirect("private");
}
// If login isn't successful
else{
//Log error message
writeLog("WebMUM login failed for IP ".$_SERVER['REMOTE_ADDR']);
Message::getInstance()->fail("Sorry, but we cannot log you in with this combination of email and password, there might be a typo.");
}
}
}
?>
<h1>Login</h1>
<?php echo Message::getInstance()->render(); ?>
<form class="form" action="" method="post">
<div class="input-group">
<label>Email address</label>
<div class="input">
<input type="text" name="email" placeholder="Your email address" value="<?php echo isset($_POST['email']) ? $_POST['email'] : ''; ?>" autofocus required/><br>
</div>
</div>
<div class="input-group">
<label>Password</label>
<div class="input">
<input type="password" name="password" placeholder="Your password" required/>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Log in</button>
</div>
</form>

View file

@ -0,0 +1,42 @@
<?php
if(isset($_POST['password']) && isset($_POST['password_repeat'])){
try{
Auth::getUser()->changePassword($_POST['password'], $_POST['password_repeat']);
Message::getInstance()->success("Password changed successfully!");
}
catch(AuthException $passwordInvalidException){
Message::getInstance()->fail($passwordInvalidException->getMessage());
}
}
?>
<h1>Change password</h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('private'); ?>">&#10092; Back to personal dashboard</a>
</div>
<?php echo Message::getInstance()->render(); ?>
<form class="form" action="" method="post" autocomplete="off">
<div class="input-group">
<label for="password">Password</label>
<?php if(Config::has('password.min_length')): ?>
<div class="input-info">Your new password must be at least <?php echo Config::get('password.min_length'); ?> characters long.</div>
<?php endif; ?>
<div class="input input-action">
<input type="password" name="password" placeholder="New password" required minlength="<?php echo Config::get('password.min_length', 0); ?>" autofocus/>
<button type="button" class="button" onclick="pass=generatePassword();this.form.password.value=pass;this.form.password_repeat.value=pass;this.form.password.type='text';this.form.password_repeat.type='text'">Generate password</button>
</div>
<div class="input">
<input type="password" name="password_repeat" placeholder="Repeat password" required minlength="<?php echo Config::get('password.min_length', 0); ?>"/>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Change password</button>
</div>
</form>

View file

@ -0,0 +1,133 @@
<?php
if(!Config::get('options.enable_user_redirects', false)
|| !Auth::getUser()->canCreateUserRedirects()
){
Router::redirect('private/redirects');
}
if(isset($_POST['source'])){
$destination = Auth::getUser()->getEmail();
$domain = Auth::getUser()->getDomain();
$inputSources = stringToEmails($_POST['source']);
// validate emails
$emailErrors = array();
// basic email validation isn't working 100% correct though
foreach($inputSources as $email){
if(strpos($email, '@') === false){
$emailErrors[$email] = "Address \"{$email}\" isn't a valid email address.";
}
}
// validate source emails are on domains
if(Config::get('options.enable_validate_aliases_source_domain', true)){
$domains = Domain::getByLimitedDomains();
foreach($inputSources as $email){
if(isset($emailErrors[$email])){
continue;
}
$emailParts = explode('@', $email);
if($emailParts[1] != $domain){
$emailErrors[$email] = "Domain of source address \"{$email}\" must be \"{$domain}\".";
}
}
}
// validate no redirect loops
if(in_array($destination, $inputSources)){
$emailErrors[$destination] = "Address \"{$destination}\" cannot be in source and destination in same redirect.";
}
if(count($emailErrors) > 0){
Message::getInstance()->fail(implode("<br>", $emailErrors));
}
elseif(count($inputSources) !== 1){
Message::getInstance()->fail("Only one email address as source.");
}
else{
if(count($inputSources) > 0){
$existingRedirects = AbstractRedirect::findWhere(
array(AbstractRedirect::attr('source'), 'IN', $inputSources)
);
if($existingRedirects->count() > 0){
$errorMessages = array();
/** @var AbstractRedirect $existingRedirect */
foreach($existingRedirects as $existingRedirect){
$errorMessages[] = "Source address \"{$existingRedirect->getSource()}\" is already redirected to some destination.";
}
Message::getInstance()->fail(implode("<br>", $errorMessages));
}
else{
foreach($inputSources as $inputSource){
$data = array(
AbstractRedirect::attr('source') => $inputSource,
AbstractRedirect::attr('destination') => $destination,
AbstractRedirect::attr('multi_hash') => null,
AbstractRedirect::attr('is_created_by_user') => true,
);
$a = Alias::createAndSave($data);
}
// Redirect created, redirect to overview
Router::redirect('private/redirects');
}
}
else{
Message::getInstance()->fail("Redirect couldn't be created. Fill out all fields.");
}
}
}
$domains = Domain::getByLimitedDomains();
?>
<h1>Create Redirect</h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('private/redirects'); ?>">&#10092; Back to your redirects</a>
</div>
<?php echo Message::getInstance()->render(); ?>
<form class="form" action="" method="post" autocomplete="off">
<div class="input-group">
<label for="source">Source</label>
<div class="input-info">
<?php if($domains->count() > 0): ?>
You can only create redirects with this domain:
<ul>
<li><?php echo Auth::getUser()->getDomain(); ?></li>
</ul>
<?php else: ?>
There are no domains managed by WebMUM yet.
<?php endif; ?>
</div>
<div class="input">
<input type="text" name="source" placeholder="Source address" required autofocus value="<?php echo formatEmailsForm(isset($_POST['source']) ? $_POST['source'] : ''); ?>"/>
</div>
</div>
<div class="input-group">
<label for="destination">Destination</label>
<div class="input">
<?php echo formatEmailsText(Auth::getUser()->getEmail()); ?>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Create redirect</button>
</div>
</form>

View file

@ -0,0 +1,82 @@
<?php
if(!Config::get('options.enable_user_redirects', false)
|| !Auth::getUser()->isAllowedToCreateUserRedirects()
){
Router::redirect('private/redirects');
}
if(!isset($_GET['id'])){
// Redirect id not set, redirect to overview
Router::redirect('private/redirects');
}
$id = $_GET['id'];
/** @var AbstractRedirect $redirect */
$redirect = AbstractRedirect::findMultiWhereFirst(
array(
array(AbstractRedirect::attr('id'), $id),
array(AbstractRedirect::attr('is_created_by_user'), true),
array(AbstractRedirect::attr('destination'), Auth::getUser()->getEmail()),
)
);
if(is_null($redirect)){
// Redirect doesn't exist, redirect to overview
Router::redirect('private/redirects');
}
if(isset($_POST['confirm'])){
$confirm = $_POST['confirm'];
if($confirm === "yes"){
$redirect->delete();
// Delete redirect successfull, redirect to overview
Router::redirect('private/redirects/?deleted=1');
}
else{
// Choose to not delete redirect, redirect to overview
Router::redirect('private/redirects');
}
}
else{
?>
<h1>Delete redirection?</h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('private/redirects'); ?>">&#10092; Back to your redirects</a>
</div>
<form class="form" action="" method="post" autocomplete="off">
<div class="input-group">
<label>Source</label>
<div class="input-info"><?php echo formatEmailsText($redirect->getSource()); ?></div>
</div>
<div class="input-group">
<label>Destination</label>
<div class="input-info"><?php echo formatEmailsText($redirect->getDestination()); ?></div>
</div>
<div class="input-group">
<label for="confirm">Do you realy want to delete this redirection?</label>
<div class="input">
<select name="confirm" autofocus required>
<option value="no">No!</option>
<option value="yes">Yes!</option>
</select>
</div>
</div>
<div class="buttons">
<button type="submit" class="button button-primary">Delete</button>
</div>
</form>
<?php
}
?>

View file

@ -0,0 +1,13 @@
<h1>Welcome to your dashboard!</h1>
<p>
Please choose an action.
</p>
<div class="buttons buttons-horizontal button-large">
<a class="button" href="<?php echo Router::url('private/changepass'); ?>">Change your password</a>
</div>
<div class="buttons buttons-horizontal button-large">
<a class="button" href="<?php echo Router::url('private/redirects'); ?>">Redirects to your mailbox</a>
</div>

View file

@ -0,0 +1,88 @@
<?php
$user = Auth::getUser();
$activateUserRedirects = Config::get('options.enable_user_redirects', false) && $user->isAllowedToCreateUserRedirects();
$redirects = $user->getAnonymizedRedirects();
$userRedirectsCount = $user->getSelfCreatedRedirects()->count();
?>
<h1>Redirects to your mailbox</h1>
<div class="buttons">
<a class="button" href="<?php echo Router::url('private'); ?>">&#10092; Back to personal dashboard</a>
<?php if($activateUserRedirects): ?>
<?php if($user->canCreateUserRedirects()): ?>
<a class="button" href="<?php echo Router::url('private/redirect/create'); ?>">Create new redirect</a>
<?php else: ?>
<a class="button button-disabled" title="You reached your user redirect limit of <?php echo $user->getMaxUserRedirects(); ?>.">Create new redirect</a>
<?php endif; ?>
<?php endif; ?>
</div>
<?php echo Message::getInstance()->render(); ?>
<?php if($activateUserRedirects): ?>
<div class="notifications notification">
You are allowed to create <strong><?php echo $user->getMaxUserRedirects() === 0 ? 'unlimited user redirects' : textValue('up to _ user redirect', $user->getMaxUserRedirects()); ?></strong> on your own.
<?php if($user->getMaxUserRedirects() > 0): ?>
<?php if($user->canCreateUserRedirects()): ?>
<br><br>You can still create <strong><?php echo textValue('_ more user redirect', $user->getMaxUserRedirects() - $userRedirectsCount); ?></strong>.
<?php else: ?>
<br><br>You cannot create anymore redirects as your limit is reached.
<br>Consider deleting unused redirects or ask an admin to extend your limit.
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if($redirects->count() > 0): ?>
<table class="table">
<thead>
<tr>
<th>Source</th>
<th>Destination</th>
<?php if($activateUserRedirects): ?>
<th>Created by you</th>
<th></th>
<?php endif; ?>
<tr>
</thead>
<tbody>
<?php foreach($redirects as $redirect): /** @var AbstractRedirect $redirect */ ?>
<tr>
<td><?php echo formatEmailsText($redirect->getSource()); ?></td>
<td><?php echo formatEmailsText($redirect->getDestination()); ?></td>
<?php if($activateUserRedirects): ?>
<td><?php echo $redirect->isCreatedByUser() ? 'Yes' : 'No'; ?></td>
<td>
<?php if($redirect->isCreatedByUser()): ?>
<a href="<?php echo Router::url('private/redirect/delete/?id='.$redirect->getId()); ?>">[Delete]</a>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th><?php echo textValue('_ redirect', $redirects->count()); ?></th>
<?php if($activateUserRedirects): ?>
<th></th>
<th>
<?php if($user->getMaxUserRedirects() === 0): ?>
<?php echo textValue('_ user redirect', $userRedirectsCount); ?>
<?php else: ?>
<?php echo $userRedirectsCount.textValue(' of _ user redirect', $user->getMaxUserRedirects()); ?>
<?php endif; ?>
</th>
<?php endif; ?>
</tr>
</tfoot>
</table>
<?php else: ?>
<div class="notification notification-warning">
There are currently no redirects to your mailbox.
</div>
<?php endif; ?>

View file

@ -0,0 +1,17 @@
<?php
if(Auth::isLoggedIn()){
Router::redirect("private");
}
?>
<h1>WebMUM</h1>
<p>
WebMUM is an easy to use web interface for managing user accounts on your e-mail server with a MySQL user backend.<br/>
Users of your server can log in here to change their passwords.
</p>
<div class="buttons buttons-horizontal">
<a class="button" href="<?php echo Router::url('login'); ?>">Log in</a>
</div>

View file

@ -0,0 +1,45 @@
<?php
// Home
Router::addGet('/', 'include/php/pages/start.php');
/**
* Auth
*/
Router::addMixed('/login', 'include/php/pages/login.php');
Router::addGet('/logout', function(){
Auth::logout();
Router::redirect('/');
return;
});
/**
* Private area
*/
Router::addGet('/private', 'include/php/pages/private/start.php', User::ROLE_USER);
Router::addMixed('/private/changepass', 'include/php/pages/private/changepass.php', User::ROLE_USER);
Router::addGet('/private/redirects', 'include/php/pages/private/yourredirects.php', User::ROLE_USER);
if(Config::get('options.enable_user_redirects', false)){
Router::addMixed('/private/redirect/create', 'include/php/pages/private/createredirect.php', User::ROLE_USER);
Router::addMixed('/private/redirect/delete', 'include/php/pages/private/deleteredirect.php', User::ROLE_USER);
}
/**
* Admin area
*/
Router::addGet('/admin', 'include/php/pages/admin/start.php', User::ROLE_ADMIN);
// Users / Mailboxes
Router::addGet('/admin/listusers', 'include/php/pages/admin/listusers.php', User::ROLE_ADMIN);
Router::addMixed('/admin/edituser', 'include/php/pages/admin/edituser.php', User::ROLE_ADMIN);
Router::addMixed('/admin/deleteuser', 'include/php/pages/admin/deleteuser.php', User::ROLE_ADMIN);
// Domains
Router::addGet('/admin/listdomains', 'include/php/pages/admin/listdomains.php', User::ROLE_ADMIN);
Router::addMixed('/admin/deletedomain', 'include/php/pages/admin/deletedomain.php', User::ROLE_ADMIN);
Router::addMixed('/admin/createdomain', 'include/php/pages/admin/createdomain.php', User::ROLE_ADMIN);
// Redirects
Router::addGet('/admin/listredirects', 'include/php/pages/admin/listredirects.php', User::ROLE_ADMIN);
Router::addMixed('/admin/editredirect', 'include/php/pages/admin/editredirect.php', User::ROLE_ADMIN);
Router::addMixed('/admin/deleteredirect', 'include/php/pages/admin/deleteredirect.php', User::ROLE_ADMIN);

View file

@ -0,0 +1,5 @@
<h1>Not allowed!</h1>
<p>
Sorry, you aren't allowed to access this page.
</p>

View file

@ -0,0 +1,5 @@
<h1>This page does not exist.</h1>
<p>
Sorry, the page you requested couldn't be found.
</p>

View file

@ -0,0 +1,54 @@
<!doctype html>
<html>
<head>
<title>WebMUM</title>
<link rel=stylesheet href="<?php echo Router::url('include/css/style.css'); ?>" type="text/css" media=screen>
<script type="text/javascript">
function generatePassword() {
var length = <?php echo Config::get('password.min_length', 8) + 1; ?>,
charset = "abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#",
retVal = "";
for (var i = 0, n = charset.length; i < length; ++i) {
retVal += charset.charAt(Math.floor(Math.random() * n));
}
return retVal;
}
</script>
</head>
<body>
<div id="header">
<div class="title"><a href="<?php echo Router::url('/'); ?>">WebMUM - Web Mailserver User Manager</a></div>
<div class="header-menu">
<?php if(Auth::hasPermission(User::ROLE_ADMIN)): ?>
<div class="header-button">
<a href="<?php echo Router::url('admin'); ?>">[Admin Dashboard]</a>
</div>
<?php endif; ?>
<?php if(Auth::hasPermission(User::ROLE_USER)): ?>
<div class="header-button">
<a href="<?php echo Router::url('private'); ?>">[Personal Dashboard]</a>
</div>
<?php endif; ?>
<?php if(Auth::isLoggedIn()): ?>
<div class="header-button">
Logged in as <?php echo Auth::getUser()->getEmail(); ?>
<a href="<?php echo Router::url('logout'); ?>">[Logout]</a>
</div>
<?php endif; ?>
</div>
</div>
<div id="content">
<?php echo $content; ?>
</div>
<div id="footer">
<ul>
<li>Powered by WebMUM (<a target="_blank" href="https://git.io/vwXhh">https://github.com/ohartl/webmum</a>).</li>
<li>Developed by Oliver Hartl, Thomas Leister and contributors.</li>
<li>License: MIT</li>
</ul>
</div>
</body>
</html>

View file

@ -1,372 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link href='https://fonts.googleapis.com/css?family=Architects+Daughter' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="stylesheets/stylesheet.css" media="screen">
<link rel="stylesheet" type="text/css" href="stylesheets/github-light.css" media="screen">
<link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print">
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<title>Webmum by ohartl</title>
</head>
<body>
<header>
<div class="inner">
<h1>Webmum</h1>
<h2>Web Mailsystem User Manager</h2>
<a href="https://github.com/ohartl/webmum" class="button"><small>View project on</small> GitHub</a>
</div>
</header>
<div id="content-wrapper">
<div class="inner clearfix">
<section id="main-content">
<h1>
<a id="webmum---web-mailserver-user-manager" class="anchor" href="#webmum---web-mailserver-user-manager" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>WebMUM - Web Mailserver User Manager</h1>
<p><a href="https://travis-ci.org/ohartl/webmum"><img src="https://travis-ci.org/ohartl/webmum.svg" alt="Build Status"></a></p>
<p><strong><em>WebMUM is not compatible with the <a href="https://thomas-leister.de/allgemein/sicherer-mailserver-dovecot-postfix-virtuellen-benutzern-mysql-ubuntu-server-xenial/">new Mailserver-HowTo</a>!,</em></strong> but we will try to implement the changes for the release of version 1.0.0.</p>
<p>WebMUM is a web frontend based on PHP which helps you to manage e-mail server via MySQL. This software is licensed under the MIT license.</p>
<p>This project is currently developed and managed by <a href="https://github.com/ohartl">ohartl</a> and together with the <a href="https://github.com/ohartl/webmum/graphs/contributors">contributes</a>.
Founder of this project is <a href="https://github.com/ThomasLeister">ThomasLeister</a>, a passionate <a href="https://thomas-leister.de/">blogger</a> specialized topics like linux, open-source, servers etc.</p>
<p>Feel free to send in issues and pull requests, your support for this project is much appreciated!</p>
<h2>
<a id="installation" class="anchor" href="#installation" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Installation</h2>
<p>Clone the WebMUM Repository to your webserver's virtual host root directory:</p>
<div class="highlight highlight-source-shell"><pre>git clone https://github.com/ohartl/webmum</pre></div>
<p>A update / upgrade guide can be found <a href="#update--upgrade-webmum">here</a>.</p>
<h3>
<a id="webserver" class="anchor" href="#webserver" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Webserver</h3>
<p>Now configure your webserver. URL rewriting to index.php is required.</p>
<h4>
<a id="nginx" class="anchor" href="#nginx" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Nginx</h4>
<p>Nginx config examples following, but you still need to change domain and path in config as explained <a href="#paths">here</a>.</p>
<p>With subdirectory <code>webmum/</code> in URL (e.g. <code>http://mydomain.tld/webmum/</code>):</p>
<div class="highlight highlight-source-nginx"><pre><span class="pl-k">server</span> {
<span class="pl-k">listen</span> <span class="pl-s">80</span>;
<span class="pl-k">server_name</span> mydomain.tld;
<span class="pl-k">root</span> /var/www;
<span class="pl-k">index</span> index.html index.php;
<span class="pl-k">location</span> ~ <span class="pl-sr">\.php$ </span>{
<span class="pl-k">fastcgi_pass</span> 127.0.0.1:9000;
<span class="pl-k">fastcgi_index</span> index.php;
<span class="pl-k">fastcgi_param</span> SCRIPT_FILENAME <span class="pl-smi">$document_root$fastcgi_script_name</span>;
<span class="pl-k">include</span> fastcgi_params;
}
<span class="pl-k">location</span> <span class="pl-en">/webmum </span>{
<span class="pl-k">try_files</span> <span class="pl-smi">$uri</span> <span class="pl-smi">$uri</span>/ /webmum/index.php?<span class="pl-smi">$args</span>;
}
<span class="pl-c"># protect the codebase by denying direct access</span>
<span class="pl-k">location</span> ^~ <span class="pl-sr">/webmum/include/php </span>{
<span class="pl-k">deny</span><span class="pl-c1"> all</span>;
<span class="pl-c1">return</span> <span class="pl-s">403</span>;
}
<span class="pl-k">location</span> ^~ <span class="pl-sr">/webmum/config </span>{
<span class="pl-k">deny</span><span class="pl-c1"> all</span>;
<span class="pl-c1">return</span> <span class="pl-s">403</span>;
}
}</pre></div>
<p>Without subdirectory in URL (e.g. <code>http://webmum.mydomain.tld/</code>):</p>
<div class="highlight highlight-source-nginx"><pre><span class="pl-k">server</span> {
<span class="pl-k">listen</span> <span class="pl-s">80</span>;
<span class="pl-k">server_name</span> webmum.mydomain.tld;
<span class="pl-k">root</span> /var/www/webmum;
<span class="pl-k">index</span> index.html index.php;
<span class="pl-k">location</span> ~ <span class="pl-sr">\.php$ </span>{
<span class="pl-k">fastcgi_pass</span> 127.0.0.1:9000;
<span class="pl-k">fastcgi_index</span> index.php;
<span class="pl-k">fastcgi_param</span> SCRIPT_FILENAME <span class="pl-smi">$document_root$fastcgi_script_name</span>;
<span class="pl-k">include</span> fastcgi_params;
}
<span class="pl-k">location</span> <span class="pl-en">/ </span>{
<span class="pl-k">try_files</span> <span class="pl-smi">$uri</span> <span class="pl-smi">$uri</span>/ /index.php?<span class="pl-smi">$args</span>;
}
<span class="pl-c"># protect the codebase by denying direct access</span>
<span class="pl-k">location</span> ^~ <span class="pl-sr">/include/php </span>{
<span class="pl-k">deny</span><span class="pl-c1"> all</span>;
<span class="pl-c1">return</span> <span class="pl-s">403</span>;
}
<span class="pl-k">location</span> ^~ <span class="pl-sr">/config </span>{
<span class="pl-k">deny</span><span class="pl-c1"> all</span>;
<span class="pl-c1">return</span> <span class="pl-s">403</span>;
}
}</pre></div>
<h4>
<a id="apache" class="anchor" href="#apache" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Apache</h4>
<p>Apache config examples following, but you still need to change domain and path in config as explained <a href="#paths">here</a>.</p>
<p>Please note: mod_rewrite must be enabled for URL rewriting:</p>
<div class="highlight highlight-source-shell"><pre>sudo a2enmod rewrite</pre></div>
<p>With subdirectory <code>webmum/</code> in URL (e.g. <code>http://mydomain.tld/webmum/</code>):</p>
<div class="highlight highlight-source-apache-config"><pre>&lt;<span class="pl-ent">VirtualHost</span> *:80&gt;
<span class="pl-c1">ServerName</span> domain.tld
<span class="pl-c1">DocumentRoot</span> /var/www/domain.tld
<span class="pl-c1">RewriteEngine</span> on
<span class="pl-c1">RewriteCond</span> <span class="pl-c1">%{REQUEST_FILENAME}</span> <span class="pl-s">!-d</span>
<span class="pl-c1">RewriteCond</span> <span class="pl-c1">%{REQUEST_FILENAME}</span> <span class="pl-s">!-f</span>
<span class="pl-c1">RewriteRule</span> <span class="pl-sr">^\/webmum/(.*)\.css$</span> <span class="pl-s">/webmum/$1.css</span> <span class="pl-sr">[L]</span>
<span class="pl-c1">RewriteRule</span> <span class="pl-sr">^\/webmum/(.*)$</span> <span class="pl-s">/webmum/index.php</span> <span class="pl-sr">[L,QSA]</span>
&lt;/<span class="pl-ent">VirtualHost</span>&gt;</pre></div>
<p>Without subdirectory in URL (e.g. <code>http://webmum.mydomain.tld/</code>):</p>
<div class="highlight highlight-source-apache-config"><pre>&lt;<span class="pl-ent">VirtualHost</span> *:80&gt;
<span class="pl-c1">ServerName</span> webmum.domain.tld
<span class="pl-c1">DocumentRoot</span> /var/www/domain.tld/webmum
<span class="pl-c1">RewriteEngine</span> on
<span class="pl-c1">RewriteCond</span> <span class="pl-c1">%{REQUEST_FILENAME}</span> <span class="pl-s">!-d</span>
<span class="pl-c1">RewriteCond</span> <span class="pl-c1">%{REQUEST_FILENAME}</span> <span class="pl-s">!-f</span>
<span class="pl-c1">RewriteRule</span> <span class="pl-sr">(.*)\.css$</span> <span class="pl-s">$1.css</span> <span class="pl-sr">[L]</span>
<span class="pl-c1">RewriteRule</span> <span class="pl-sr">^(.*)$</span> <span class="pl-s">/index.php</span> <span class="pl-sr">[L,QSA]</span>
&lt;/<span class="pl-ent">VirtualHost</span>&gt;</pre></div>
<p>Access to the codebase is denied with a <code>.htaccess</code> file, that can be found in <code>/include/php</code>.</p>
<h2>
<a id="webmum-configuration" class="anchor" href="#webmum-configuration" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>WebMUM Configuration</h2>
<p>Configure WebMUM via the configuration file at <code>config/config.inc.php</code>.</p>
<h3>
<a id="mysql" class="anchor" href="#mysql" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>MySQL</h3>
<p>At first the database access has to be configured under the config key <code>mysql</code>.</p>
<p>Check if you've got the same database schema as configured in the config key <code>schema</code>.</p>
<h3>
<a id="mailbox-limit-optional" class="anchor" href="#mailbox-limit-optional" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Mailbox limit (Optional)</h3>
<p>If you want to use your "mailbox_limit" column to limit the size of your users' mailboxes, just enable mailbox limit in the options.</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>options<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>enable_mailbox_limits<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">true</span>,</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1">),</span></pre></div>
<p>WebMUM will then show a new field "Mailbox limit" in the frontend.</p>
<h3>
<a id="multiple-source-redirect-support-optional" class="anchor" href="#multiple-source-redirect-support-optional" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Multiple source redirect support (Optional)</h3>
<p>As mailservers can only process a single source address for redirects the database table for aliases / redirects can only hold a single source address in a row.
WebMum will, if you enabled the multiple source redirect support, do some magic so there is only a single address in a row even though multiple addresses where entered.
To make this work another column in the database table is required, which holds an identifier for the list of source addresses, so they can be edited like normal redirects.</p>
<p>By default you can only redirect a single address to a single or multiple destinations.
If you want to enable support for redirecting multiple source addresses to a destination, just enable it in the options:</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>options<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>enable_multi_source_redirects<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">true</span>,</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1">),</span></pre></div>
<p>And add the following column to your database table for aliases / redirects:</p>
<div class="highlight highlight-source-sql"><pre><span class="pl-k">ALTER</span> <span class="pl-k">TABLE</span> <span class="pl-s"><span class="pl-pds">`</span>aliases<span class="pl-pds">`</span></span> ADD COLUMN <span class="pl-s"><span class="pl-pds">`</span>multi_source<span class="pl-pds">`</span></span> <span class="pl-k">VARCHAR</span>(<span class="pl-c1">32</span>) <span class="pl-k">NULL</span> DEFAULT <span class="pl-k">NULL</span>;</pre></div>
<p>WebMUM will then show a larger field for source addresses in the frontend and you can not list emails in source field.</p>
<h3>
<a id="admin-domain-limits-optional" class="anchor" href="#admin-domain-limits-optional" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Admin domain limits (Optional)</h3>
<p>If you share your mailserver with others, host their domains and they should be able to manage their domains, but not all domains on that mailserver then this is the right option for you.
You have to add that user to the <code>admins</code> array in your configuration and enable admin domain limits in the options:</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>options<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>enable_admin_domain_limits<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">true</span>,</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1">),</span></pre></div>
<p>also you have to make an entry in the <code>admin_domain_limits</code> array, for example <code>peter@his.tld</code> should be able to manage his domains <code>his.tld</code> and <code>his-company.tld</code> then configure the following:</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>admin_domain_limits<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>peter@his.tld<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(<span class="pl-s"><span class="pl-pds">'</span>his.tld<span class="pl-pds">'</span></span>, <span class="pl-s"><span class="pl-pds">'</span>his-company.tld<span class="pl-pds">'</span></span>),</span>
<span class="pl-s1">);</span></pre></div>
<p>Admins that have been listed in <code>admin_domain_limits</code> don't have access to the "Manage domains" pages, otherwise they could delete domains they are managing, but maybe someone else owns.</p>
<h3>
<a id="paths" class="anchor" href="#paths" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Paths</h3>
<p>The <code>base_url</code> is the URL your WebMUM installation is accessible from outside, this also includes subdirectories if you installed it in a subdirectory for that specific domain.</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>base_url<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-s"><span class="pl-pds">'</span>http://localhost/webmum<span class="pl-pds">'</span></span>,</span></pre></div>
<p>In the example above, WebMUM is located in a subdirectory named "webmum/". If your WebMUM installation is directly accessible from a domain (has its own domain), then set the <code>FRONTEND_BASE_PATH</code> to something like this:</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>base_url<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-s"><span class="pl-pds">'</span>http://webmum.mydomain.tld<span class="pl-pds">'</span></span>,</span></pre></div>
<h3>
<a id="admin-e-mail-address" class="anchor" href="#admin-e-mail-address" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Admin e-mail address</h3>
<p>Only users with one of the specified email addresses will have access to the administrator's dashboard and will be able to create, edit and delete users, domains and redirects.</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>admins<span class="pl-pds">'</span></span> <span class="pl-k">=</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>admin@domain.tld<span class="pl-pds">'</span></span>,</span>
<span class="pl-s1">);</span></pre></div>
<p>Admin email accounts must exist in the virtual user database on your own server. (=&gt; an e-mail account on a foreign server won't give you access!). You can then login into the admin dashboard with that e-mail address and the corresponding password.</p>
<h3>
<a id="minimal-required-password-length" class="anchor" href="#minimal-required-password-length" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Minimal required password length</h3>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>password<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>min_length<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">8</span>,</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1">),</span></pre></div>
<h3>
<a id="logfile" class="anchor" href="#logfile" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Logfile</h3>
<p>When logging is enabled, WebMUM will write messages into a file "webmum.log" in a specified directory (e.g. when a login attempt fails).</p>
<p>Enable logging by setting it to enabled in the options:</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>options<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>enable_logging<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">true</span>,</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1">),</span></pre></div>
<p>... and set a log path where the PHP user has permission to write the log file:</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>log_path<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-s"><span class="pl-pds">'</span>/var/www/webmum/log/<span class="pl-pds">'</span></span>,</span></pre></div>
<p>"Login-failed-messages" have the following scheme:</p>
<pre><code>Dec 19 13:00:19: WebMUM login failed for IP 127.0.0.1
</code></pre>
<h4>
<a id="fail2ban-support" class="anchor" href="#fail2ban-support" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Fail2Ban support</h4>
<p>If you want to use <strong>Fail2Ban</strong> with WebMUM, the filter has to be:</p>
<pre><code>[Definition]
failregex = ^(.*)\: WebMUM login failed for IP &lt;HOST&gt;$
</code></pre>
<h3>
<a id="validate-that-source-addresses-of-redirects-must-be-from-the-managed-domains-only" class="anchor" href="#validate-that-source-addresses-of-redirects-must-be-from-the-managed-domains-only" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Validate that source addresses of redirects must be from the managed domains only</h3>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>options<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>enable_validate_aliases_source_domain<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">true</span>,</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1">),</span></pre></div>
<h3>
<a id="frontend-options" class="anchor" href="#frontend-options" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Frontend options</h3>
<p>Choose delimiter between multiple email addresses: comma, semicolon or new line separated.</p>
<p><strong>Tip:</strong> new line is helpful for long lists of addresses.</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>frontend_options<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-c">// Separator for email lists</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>email_separator_text<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-s"><span class="pl-pds">'</span>, <span class="pl-pds">'</span></span>, <span class="pl-c">// possible values: ', ' (default), '; ', PHP_EOL (newline)</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>email_separator_form<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-s"><span class="pl-pds">'</span>,<span class="pl-pds">'</span></span>, <span class="pl-c">// possible values: ',' (default), ';', PHP_EOL (newline)</span></span>
<span class="pl-s1">),</span></pre></div>
<p>The input for addresses can be separated by <code>,</code>, <code>;</code>, <code>:</code>, <code>|</code>, <code>newline</code> and combinations since all of them will result in a valid list of addresses in database, magic.</p>
<h2>
<a id="update--upgrade-webmum" class="anchor" href="#update--upgrade-webmum" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Update / Upgrade WebMUM</h2>
<p>If you cloned WebMUM into your filesystem via <code>git clone https://github.com/ohartl/webmum</code>:</p>
<div class="highlight highlight-source-shell"><pre>git stash
git pull origin master
git stash pop</pre></div>
<p>... and you are ready to go. Git might complain about conflicting files - you will have to resolve the merge conflict manually then.</p>
<p>If you downloaded WebMUM as a ZIP package, you have to update WebMUM manually.</p>
<p><strong>After every update:</strong>
Please check if your config.inc.php fits the current requirements by comparing your version of the file with the config.inc.php in the repository.</p>
<h2>
<a id="faq" class="anchor" href="#faq" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>FAQ</h2>
<h3>
<a id="which-password-hash-algorithm-does-webmum-use" class="anchor" href="#which-password-hash-algorithm-does-webmum-use" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Which password hash algorithm does WebMUM use?</h3>
<p>By default WebMUM uses the <code>SHA-512</code> hash algorithm for passwords. You can also choose between the alternatives <code>SHA-256</code> or <code>BLOWFISH</code> in the config.</p>
<div class="highlight highlight-text-html-php"><pre><span class="pl-s1"><span class="pl-s"><span class="pl-pds">'</span>password<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-c1">array</span>(</span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1"> <span class="pl-s"><span class="pl-pds">'</span>hash_algorithm<span class="pl-pds">'</span></span> <span class="pl-k">=&gt;</span> <span class="pl-s"><span class="pl-pds">'</span>SHA-512<span class="pl-pds">'</span></span>, <span class="pl-c">// Supported algorithms: SHA-512, SHA-256, BLOWFISH</span></span>
<span class="pl-s1"> <span class="pl-k">...</span></span>
<span class="pl-s1">),</span></pre></div>
<h3>
<a id="login-cannot-be-found" class="anchor" href="#login-cannot-be-found" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>"login/ cannot be found"</h3>
<p>Webserver rewrites have to be enabled on your server, because WebMUM does not use real URLs for the frontend, but virtual URLs based on URL rewriting.
When rewriting fails, you receive a 404 error message.</p>
</section>
<aside id="sidebar">
<a href="https://github.com/ohartl/webmum/zipball/master" class="button">
<small>Download</small>
.zip file
</a>
<a href="https://github.com/ohartl/webmum/tarball/master" class="button">
<small>Download</small>
.tar.gz file
</a>
<p class="repo-owner"><a href="https://github.com/ohartl/webmum"></a> is maintained by <a href="https://github.com/ohartl">ohartl</a>.</p>
<p>This page was generated by <a href="https://pages.github.com">GitHub Pages</a> using the Architect theme by <a href="https://twitter.com/jasonlong">Jason Long</a>.</p>
</aside>
</div>
</div>
</body>
</html>

49
index.php Normal file
View file

@ -0,0 +1,49 @@
<?php
if (php_sapi_name() == "cli-server") {
// running under built-in server
$extensions = array("php", "jpg", "jpeg", "css", "js");
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$ext = pathinfo($path, PATHINFO_EXTENSION);
if (in_array($ext, $extensions)) {
return false;
}
}
try {
/**
* Loading system
*/
require_once 'include/php/default.inc.php';
if(defined('INSTALLER_ENABLED')){
/**
* Load installer
*/
$content = Router::loadAndBufferOutput('installer/index.php');
}
else {
/**
* Handle request
*/
$content = Router::executeCurrentRequest();
}
}
catch(DatabaseException $e){
$content = '<div class="notification notification-fail">Faulty database query: "'.$e->getQuery().'".</div>';
}
catch(Exception $e){
$content = '<div class="notification notification-fail">'.$e->getMessage().'</div>';
}
if(defined('USING_OLD_CONFIG')){
$content = '<div class="notification notification-fail"><strong>Your WebMUM installation is still using the old deprecated config style!</strong><br><br>Please update your config to the new style (an example config can be found in <cite>config.php.example</cite>)<br>and delete your old <cite>config.inc.php</cite> and <cite>config.inc.php.example</cite>.</div>'.$content;
}
echo Router::loadAndBufferOutput(
'include/php/template/layout.php',
array(
'content' => $content,
)
);

1
installer/.htaccess Normal file
View file

@ -0,0 +1 @@
Deny from all

125
installer/index.php Normal file
View file

@ -0,0 +1,125 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
define('INSTALLER_MAX_STEP', 6);
define('INSTALLER_TYPE_CREATE', 0);
define('INSTALLER_TYPE_MAP', 1);
$installerStepTitles = array(
'Requirements',
'Database connection',
'Database schema',
'Your first admin user',
'General settings',
'Optional features',
'Finish installation',
);
$installerStepMapping = array(
0 => 0,
1 => 1,
2 => 2,
3 => 2,
4 => 3,
5 => 4,
6 => 5,
7 => 6,
);
/*-----------------------------------------------------------------------------*/
function installer_reset()
{
global $_SESSION;
$_SESSION['installer'] = array(
'lastStep' => 0,
'step' => 0,
'config' => array(),
);
}
function installer_message($setMessage = null)
{
global $_SESSION;
if(!is_null($setMessage)){
$_SESSION['installer']['message'] = $setMessage;
}
elseif(isset($_SESSION['installer']['message'])){
$m = '<div class="notification notification-success">'.$_SESSION['installer']['message'].'</div>';
unset($_SESSION['installer']['message']);
return $m;
}
return $setMessage;
}
function installer_prev($thisStep, $stepSize = 1)
{
$s = ($thisStep < 0) ? 0 : ($thisStep - $stepSize);
$_SESSION['installer']['lastStep'] = $thisStep;
$_SESSION['installer']['step'] = $s;
Router::redirect('/?step='.$s);
}
function installer_next($thisStep, $stepSize = 1)
{
$s = ($thisStep > 8) ? 8 : ($thisStep + $stepSize);
$_SESSION['installer']['lastStep'] = $thisStep;
$_SESSION['installer']['step'] = $s;
Router::redirect('/?step='.$s);
}
if(!isset($_SESSION['installer'])){
installer_reset();
}
/*-----------------------------------------------------------------------------*/
$step = (isset($_GET['step']) && is_numeric($_GET['step'])) ? intval($_GET['step']) : 0;
echo '<h1>Installation of WebMUM</h1>';
if($step > 0){
?>
<ol style="font-size: 1.1em;">
<?php for($s = 1; $s <= INSTALLER_MAX_STEP; $s++): ?>
<li>
<?php if(isset($installerStepMapping[$step]) && $s < $installerStepMapping[$step]): ?>
<span style="color: #999;"><?php echo $installerStepTitles[$s]; ?></span>
<?php elseif(isset($installerStepMapping[$step]) && $s === $installerStepMapping[$step]): ?>
<strong><?php echo $installerStepTitles[$s]; ?></strong>
<?php else: ?>
<?php echo $installerStepTitles[$s]; ?>
<?php endif; ?>
</li>
<?php endfor; ?>
</ol>
<?php
}
try{
$stepFile = __DIR__.'/step'.$step.'.php';
if(file_exists($stepFile)){
include_once $stepFile;
}
else{
installer_reset();
echo 'Wizard step '.$step.' is missing.';
}
}
catch(Exception $e){
echo $e->getMessage();
}

103
installer/step0.php Normal file
View file

@ -0,0 +1,103 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
$thisStep = 0;
/*-----------------------------------------------------------------------------*/
$requirements = array();
$numberOfRequirements = 5;
if(version_compare(phpversion(), '5.4.0', '>=')){
$requirements[] = 'php_version';
}
if(function_exists('mysqli_connect')){
$requirements[] = 'php_extension_mysqli';
}
if(session_status() != PHP_SESSION_DISABLED){
$requirements[] = 'php_session_enabled';
}
if(file_exists('config') && is_dir('config')){
$requirements[] = 'config_directory';
}
if(file_exists('config/config.php.example')){
$requirements[] = 'config_example';
}
/*-----------------------------------------------------------------------------*/
if(isset($_GET['go']) && $_GET['go'] == 'next'){
if(count($requirements) === $numberOfRequirements){
installer_message('All requirements fulfilled, let\'s get started with the installation!');
installer_next($thisStep);
}
}
?>
<?php echo installer_message(); ?>
<h2>Getting started</h2>
<p>By following this wizard you will install and configure your new WebMUM installation.</p>
<hr>
<strong>System Info:</strong>
<ul>
<li>System: <strong><?php echo php_uname(); ?></strong></li>
<li>Hostname: <strong><?php echo isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'n/a'; ?></strong></li>
<li>IP: <strong><?php echo isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : (isset($_SERVER['SERVER_NAME']) ? gethostbyname($_SERVER['SERVER_NAME']) : 'n/a'); ?></strong></li>
<li>PHP version: <strong><?php echo phpversion(); ?></strong></li>
<li>Server API: <strong><?php echo php_sapi_name(); ?></strong></li>
<li>WebMUM directory: <strong><?php echo dirname($_SERVER['SCRIPT_FILENAME']); ?></strong></li>
</ul>
<strong>Server requirements</strong>
<ul>
<?php if(in_array('php_version', $requirements)): ?>
<li class="text-success">PHP version (>=5.4.0 or >=7.0.0): <strong><?php echo phpversion(); ?> &#x2713;</strong></li>
<?php else: ?>
<li class="text-fail">PHP version (>=5.4.0 or >=7.0.0): <strong><?php echo phpversion(); ?> &#x274c;</strong></li>
<?php endif; ?>
</ul>
<strong>Required PHP settings</strong>
<ul>
<?php if(in_array('php_extension_mysqli', $requirements)): ?>
<li class="text-success">Database extension (mysqli): <strong>enabled &#x2713;</strong></li>
<?php else: ?>
<li class="text-fail">Database extension (mysqli): <strong>disabled &#x274c;</strong></li>
<?php endif; ?>
<?php if(in_array('php_session_enabled', $requirements)): ?>
<li class="text-success">Session support: <strong>enabled &#x2713;</strong></li>
<?php else: ?>
<li class="text-fail">Session support: <strong>disabled &#x274c;</strong></li>
<?php endif; ?>
</ul>
<strong>Directories and files</strong>
<ul>
<?php if(in_array('config_directory', $requirements)): ?>
<li class="text-success">"config/": <strong>exists &#x2713;</strong></li>
<?php else: ?>
<li class="text-fail">"config/": <strong>is missing &#x274c;</strong></li>
<?php endif; ?>
<?php if(in_array('config_example', $requirements)): ?>
<li class="text-success">"config/config.php.example": <strong>exists &#x2713;</strong></li>
<?php else: ?>
<li class="text-fail">"config/config.php.example": <strong>is missing &#x274c;</strong></li>
<?php endif; ?>
</ul>
<hr>
<?php if(count($requirements) === $numberOfRequirements):?>
<p>Click on the Start button to continue.</p>
<a class="button button-primary" href="/?step=<?php echo $thisStep; ?>&go=next">Start</a>
<?php else:?>
<p class="notification notification-fail">Some requirements aren't fulfilled.</p>
<?php endif; ?>

131
installer/step1.php Normal file
View file

@ -0,0 +1,131 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
$thisStep = 1;
$error = null;
/*-----------------------------------------------------------------------------*/
if(isset($_GET['go'])){
if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){
try{
// testing db settings
Database::init($_POST);
// saving information
$_SESSION['installer']['config']['mysql'] = array(
'host' => $_POST['host'],
'user' => $_POST['user'],
'password' => $_POST['password'],
'database' => $_POST['database'],
);
$_SESSION['installer']['type'] = (isset($_POST['install_type']) && $_POST['install_type'] == INSTALLER_TYPE_MAP)
? INSTALLER_TYPE_MAP
: INSTALLER_TYPE_CREATE;
installer_message('Database connection was successfully established.');
installer_next($thisStep, ($_SESSION['installer']['type'] === INSTALLER_TYPE_MAP) ? 2 : 1);
}
catch(InvalidArgumentException $e){
$error = 'Some fields are missing.';
}
catch(Exception $e){
$error = $e->getMessage();
}
}
elseif($_GET['go'] == 'prev'){
// reset
unset($_SESSION['installer']['config']['mysql']);
unset($_SESSION['installer']['type']);
installer_prev($thisStep);
}
}
function getAttr($name, $default = null)
{
global $_SESSION, $_POST;
if(isset($_POST[$name])){
return strip_tags($_POST[$name]);
}
elseif(isset($_SESSION['installer']['config']['mysql'][$name])){
return $_SESSION['installer']['config']['mysql'][$name];
}
elseif($name === 'install_type' && isset($_SESSION['installer']['type'])){
return $_SESSION['installer']['type'];
}
return $default;
}
?>
<?php echo installer_message(); ?>
<h2>Step 1 of <?php echo INSTALLER_MAX_STEP; ?>: Database connection.</h2>
<?php if(!empty($error)): ?>
<div class="notification notification-fail"><?php echo $error; ?></div>
<?php endif; ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=next" method="post">
<p>Setup your MySQL database connection.</p>
<div class="input-group">
<label for="host">Database Host</label>
<div class="input">
<input type="text" name="host" value="<?php echo getAttr('host', 'localhost'); ?>" autofocus/>
</div>
</div>
<div class="input-group">
<label for="database">Database Name</label>
<div class="input">
<input type="text" name="database" value="<?php echo getAttr('database'); ?>"/>
</div>
</div>
<div class="input-group">
<label for="user">Database Username</label>
<div class="input">
<input type="text" name="user" value="<?php echo getAttr('user'); ?>"/>
</div>
</div>
<div class="input-group">
<label for="password">Database Password</label>
<div class="input">
<input type="password" name="password" value="<?php echo getAttr('password'); ?>"/>
</div>
</div>
<hr>
<div class="input-group">
<label for="install_type">Installation Type</label>
<div class="input-info">Be sure to select the correct option.</div>
<div class="input">
<input type="radio" name="install_type" id="install_type_0" value="0" <?php echo getAttr('install_type', 0) == 0 ? 'checked' : ''; ?>/>
<label for="install_type_0">Create new database schema</label>
</div>
<div class="input">
<input type="radio" name="install_type" id="install_type_1" value="1" <?php echo getAttr('install_type', 0) == 1 ? 'checked' : ''; ?>/>
<label for="install_type_1">Map existing database schema</label>
</div>
</div>
<hr class="invisible">
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<button class="button button-primary" type="submit">Continue</button>
</div>
</form>

209
installer/step2.php Normal file
View file

@ -0,0 +1,209 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
$thisStep = 2;
/*-----------------------------------------------------------------------------*/
$exampleConfigValues = require_once 'config/config.php.example';
$tablesInDatabase = array();
try{
Database::init($_SESSION['installer']['config']['mysql']);
$tablesResult = Database::getInstance()->query("SELECT table_name FROM information_schema.tables WHERE table_schema='".$_SESSION['installer']['config']['mysql']['database']."';");
foreach($tablesResult->fetch_all() as $row){
$tablesInDatabase[] = $row[0];
}
}
catch(Exception $e){
}
/*-----------------------------------------------------------------------------*/
$databaseSchema = array(
'domains' => "CREATE TABLE IF NOT EXISTS ___database___.___table___ (___id___ INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, ___domain___ VARCHAR(128) NOT NULL, PRIMARY KEY (___domain___), UNIQUE KEY ___id___ (___id___)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;",
'users' => "CREATE TABLE IF NOT EXISTS ___database___.___table___ (___id___ INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, ___username___ VARCHAR(128) NOT NULL DEFAULT '', ___domain___ VARCHAR(128) NOT NULL DEFAULT '', ___password___ VARCHAR(128) NOT NULL DEFAULT '', ___mailbox_limit___ INT(10) NOT NULL DEFAULT '128', ___max_user_redirects___ INT(10) NOT NULL DEFAULT '0', PRIMARY KEY (___username___,___domain___), UNIQUE KEY ___id___ (___id___)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;",
'aliases' => "CREATE TABLE IF NOT EXISTS ___database___.___table___ (___id___ INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, ___source___ VARCHAR(128) NOT NULL, ___destination___ TEXT NOT NULL, ___multi_source___ VARCHAR(32) DEFAULT NULL, ___is_created_by_user___ INT(1) NOT NULL DEFAULT '0', PRIMARY KEY (___source___), UNIQUE KEY ___id___ (___id___)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;",
);
/**
* @param string $stmt
* @param string $database
* @param string $table
* @param array $attributes
*
* @return string
*/
function prepareSchemaTableStmt($stmt, $database, $table, $attributes)
{
$attributes['database'] = $database;
$attributes['table'] = $table;
foreach($attributes as $search => $replace){
$stmt = str_replace('___'.$search.'___', '`'.Database::getInstance()->escape($replace).'`', $stmt);
}
return $stmt;
}
$preparedSchemaStmt = '';
$allTablesFromSchemaExist = true;
foreach($databaseSchema as $table => $stmt){
$preparedSchemaStmt .= prepareSchemaTableStmt(
$stmt,
$_SESSION['installer']['config']['mysql']['database'],
$exampleConfigValues['schema']['tables'][$table],
$exampleConfigValues['schema']['attributes'][$table]
).PHP_EOL;
// check if tables exist, should be enough for now
if(!in_array($exampleConfigValues['schema']['tables'][$table], $tablesInDatabase)){
$allTablesFromSchemaExist = false;
}
}
$commandDenied = false;
/*-----------------------------------------------------------------------------*/
if(isset($_GET['go'])){
if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){
if(isset($_POST['manual'])){
if($_POST['manual'] == 1){
// display SQL
}
elseif($_POST['manual'] == 2){
// check if schema was created
if($allTablesFromSchemaExist){
// saving information
$_SESSION['installer']['config']['schema'] = $exampleConfigValues['schema'];
installer_message('Database schema was manually created.');
installer_next($thisStep, 2);
}
else{
$_POST['manual'] = 1;
}
}
}
else{
if(!$allTablesFromSchemaExist){
try{
foreach(explode(PHP_EOL, $preparedSchemaStmt) as $stmt){
Database::getInstance()->query($stmt);
}
// saving information
$_SESSION['installer']['config']['schema'] = $exampleConfigValues['schema'];
installer_message('Database schema was automatically created.');
installer_next($thisStep, 2);
}
catch(Exception $e){
if(strpos($e->getMessage(), 'command denied') !== false){
$commandDenied = true;
}
else{
throw $e;
}
}
}
}
}
elseif($_GET['go'] == 'prev'){
// reset
unset($_SESSION['installer']['config']['schema']);
installer_prev($thisStep);
}
}
?>
<?php echo installer_message(); ?>
<h2>Step 2 of <?php echo INSTALLER_MAX_STEP; ?>: Create database schema.</h2>
<?php if($allTablesFromSchemaExist): ?>
<div class="notification notification-fail">
The schema already exists in database "<?php echo $_SESSION['installer']['config']['mysql']['database']; ?>".
</div>
<div>
Your next possible steps:
<ul>
<li>Either <strong>delete</strong> the existing schema.</li>
<li>Go Back and <strong>change</strong> the used database.</li>
<li>Go Back and <strong>start mapping</strong> the existing database schema.</li>
</ul>
</div>
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<a class="button" href="/?step=<?php echo $thisStep; ?>">Retry</a>
</div>
<?php else: ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=next" method="post">
<?php if(isset($_POST['manual']) && $_POST['manual'] == 1): ?>
<textarea readonly style="width: 100%; height: 170px"><?php echo $preparedSchemaStmt; ?></textarea>
<div class="notification notification-warning">
Copy the SQL-Code above and import it into your database "<?php echo $_SESSION['installer']['config']['mysql']['database']; ?>".
</div>
<hr class="invisible">
<p>Once you have imported the schema, you can continue by clicking on the Continue button.</p>
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>">Back</a>
<button class="button button-primary" name="manual" value="2" type="submit">Continue</button>
</div>
<?php else: ?>
<div class="notification notification-warning">
The following database schema will be created in
<strong>database "<?php echo $_SESSION['installer']['config']['mysql']['database']; ?>"</strong>.
<br><strong>Please make sure that "<?php echo $_SESSION['installer']['config']['mysql']['database']; ?>" is clean / empty database!</strong>
</div>
<?php if($commandDenied): ?>
<div class="notification notification-fail">The
<strong>user "<?php echo $_SESSION['installer']['config']['mysql']['user']; ?>" is missing the permission</strong> to execute MySQL "CREATE" commands.
</div>
<?php else: ?>
<div class="notification notification-warning">
Also <strong>make sure</strong> that the database
<strong>user "<?php echo $_SESSION['installer']['config']['mysql']['user']; ?>" has the privileges to create</strong> the schema.
</div>
<?php endif; ?>
<?php foreach($exampleConfigValues['schema']['tables'] as $table => $mappedTable): ?>
<div>
<strong>Table "<?php echo $table; ?>"</strong>
<ul>
<?php foreach($exampleConfigValues['schema']['attributes'][$table] as $attribute => $mappedAttribute): ?>
<li><?php echo $attribute; ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?>
<hr class="invisible">
<p>Click on the Continue button to try creating the schema automatically.</p>
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<button class="button" name="manual" value="1" type="submit">Import schema manually</button>
<button class="button button-primary" type="submit">Continue</button>
</div>
<?php endif; ?>
</form>
<?php endif; ?>

306
installer/step3.php Normal file
View file

@ -0,0 +1,306 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
$thisStep = 3;
if($_SESSION['installer']['lastStep'] > $thisStep){
$_SESSION['installer']['subStep'] = 1;
}
elseif($_SESSION['installer']['lastStep'] < $thisStep || !isset($_SESSION['installer']['subStep'])){
$_SESSION['installer']['subStep'] = 0;
}
$error = null;
/*-----------------------------------------------------------------------------*/
$exampleConfigValues = require_once 'config/config.php.example';
$tablesInDatabase = array();
try{
Database::init($_SESSION['installer']['config']['mysql']);
$db = Database::getInstance();
$tablesResult = $db->query(
"SELECT TABLE_NAME FROM information_schema.tables "
."WHERE TABLE_SCHEMA='".$db->escape($_SESSION['installer']['config']['mysql']['database'])."';"
);
foreach($tablesResult->fetch_all() as $row){
$tablesInDatabase[] = $row[0];
}
}
catch(Exception $e){
}
function getTableAttributes($table)
{
global $_SESSION;
$attributes = array();
if(Database::isInitialized()){
try{
$db = Database::getInstance();
$tablesResult = $db->query(
"SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA FROM information_schema.columns "
."WHERE TABLE_SCHEMA = '".$db->escape($_SESSION['installer']['config']['mysql']['database'])."' "
."AND TABLE_NAME = '".$db->escape($table)."' "
."ORDER BY TABLE_NAME,ORDINAL_POSITION;"
);
foreach($tablesResult->fetch_all() as $row){
$s = $row[0];
if(!empty($row[1])){
$s .= ' : '.$row[1];
}
if($row[2] == 'NO'){
$s .= ', NOT NULL';
}
if(!is_null($row[3])){
$s .= ', DEFAULT \''.$row[3].'\'';
}
if(!empty($row[4])){
if(strpos($row[4], 'PR') !== false){
$s .= ', PRIMARY KEY';
}
if(strpos($row[4], 'UN') !== false){
$s .= ', UNIQUE KEY';
}
}
if(!empty($row[5]) && strpos($row[5], 'auto_inc') !== false){
$s .= ', AUTO_INCREMENT';
}
$attributes[$row[0]] = $s;
}
}
catch(Exception $e){
}
}
return $attributes;
}
$optionalAttributes = array(
'users' => array('mailbox_limit', 'max_user_redirects'),
'aliases' => array('multi_source', 'is_created_by_user'),
);
define('ATTR_SEP', '---');
function getAttr($name, $default = null)
{
global $_SESSION, $_POST;
if(isset($_POST[$name])){
return strip_tags($_POST[$name]);
}
elseif(strpos($name, ATTR_SEP) !== false){
list($table, $attribute) = explode(ATTR_SEP, $name);
if(isset($_SESSION['installer']['config']['schema']['attributes'][$table][$attribute])){
return $_SESSION['installer']['config']['schema']['attributes'][$table][$attribute];
}
}
elseif(isset($_SESSION['installer']['config']['schema']['tables'][$name])){
return $_SESSION['installer']['config']['schema']['tables'][$name];
}
return $default;
}
/*-----------------------------------------------------------------------------*/
if(isset($_GET['go'])){
if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){
try{
if($_SESSION['installer']['subStep'] === 0){
$tables = array();
foreach($exampleConfigValues['schema']['tables'] as $table => $mappedTable){
if(!isset($_POST[$table])
|| !in_array($_POST[$table], $tablesInDatabase)
){
throw new InvalidArgumentException('Missing mapping for table "'.$table.'".');
}
if(in_array($_POST[$table], array_values($tables))){
throw new Exception('You cannot map table "'.$_POST[$table].'" twice.');
}
$tables[$table] = $_POST[$table];
}
// saving information
$_SESSION['installer']['config']['schema'] = array();
$_SESSION['installer']['config']['schema']['tables'] = $tables;
installer_message('Database tables were successfully mapped.');
$_SESSION['installer']['subStep'] = 1;
installer_next($thisStep, 0);
}
elseif($_SESSION['installer']['subStep'] === 1){
$attributes = array();
foreach($_SESSION['installer']['config']['schema']['tables'] as $table => $mappedTable){
$attributes[$table] = array();
$attributesInDatabase = getTableAttributes($table);
foreach($exampleConfigValues['schema']['attributes'][$table] as $attribute => $mappedAttribute){
$key = $table.'---'.$attribute;
if(isset($optionalAttributes[$table])
&& in_array($attribute, $optionalAttributes[$table])
&& !isset($attributesInDatabase[$_POST[$key]])
){
$attributes[$table][$attribute] = '';
}
else{
if(!isset($_POST[$key]) || !isset($attributesInDatabase[$_POST[$key]])){
throw new InvalidArgumentException('Missing mapping for attribute "'.$attribute.'" on table "'.$table.'".');
}
if(in_array($_POST[$key], $attributes[$table])){
throw new Exception('You cannot map attribute "'.$_POST[$key].'" twice on table "'.$table.'".');
}
$attributes[$table][$attribute] = $_POST[$key];
}
}
}
// saving information
$_SESSION['installer']['config']['schema']['attributes'] = $attributes;
installer_message('Database attributes were successfully mapped.');
unset($_SESSION['installer']['subStep']);
installer_next($thisStep);
}
}
catch(Exception $e){
$error = $e->getMessage();
}
}
elseif($_GET['go'] == 'prev'){
// reset
if(isset($_SESSION['installer']['config']['schema']['tables'])){
if($_SESSION['installer']['subStep'] === 0){
unset($_SESSION['installer']['config']['schema']);
}
elseif($_SESSION['installer']['subStep'] === 1){
unset($_SESSION['installer']['config']['schema']['attributes']);
}
}
if($_SESSION['installer']['subStep'] === 0){
unset($_SESSION['installer']['subStep']);
installer_prev($thisStep, ($_SESSION['installer']['type'] === INSTALLER_TYPE_MAP) ? 2 : 1);
}
else{
$_SESSION['installer']['subStep'] = 0;
installer_prev($thisStep, 0);
}
}
}
?>
<?php echo installer_message(); ?>
<h2>
Step 2 of <?php echo INSTALLER_MAX_STEP; ?>:
<?php if($_SESSION['installer']['subStep'] === 0): ?>
Database - table mapping.
<?php elseif($_SESSION['installer']['subStep'] === 1): ?>
Database - attribute mapping.
<?php else: ?>
Wrong turn
<?php endif; ?>
</h2>
<?php if(!empty($error)): ?>
<div class="notification notification-fail"><?php echo $error; ?></div>
<?php endif; ?>
<?php if($_SESSION['installer']['subStep'] === 0): ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=next" method="post">
<?php foreach($exampleConfigValues['schema']['tables'] as $table => $mappedTable): ?>
<div class="input-group">
<label for="<?php echo $table; ?>">Table "<?php echo $table; ?>"</label>
<div class="input">
<select name="<?php echo $table; ?>">
<option value="">-- Not mapped --</option>
<?php foreach($tablesInDatabase as $t): ?>
<option value="<?php echo $t; ?>" <?php echo getAttr($table, $mappedTable) == $t ? 'selected' : ''; ?>><?php echo $t; ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<?php endforeach; ?>
<hr class="invisible">
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<button class="button button-primary" type="submit">Continue</button>
</div>
</form>
<?php elseif($_SESSION['installer']['subStep'] === 1): ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=next" method="post">
<?php
$lastTable = array_keys($_SESSION['installer']['config']['schema']['tables']);
$lastTable = $lastTable[count($lastTable) - 1];
foreach($_SESSION['installer']['config']['schema']['tables'] as $table => $mappedTable):
$attributesInDatabase = getTableAttributes($mappedTable);
?>
<h3>
Table "<?php echo $table; ?>"
<div class="sub-header">Has been mapped to table "<?php echo $mappedTable; ?>".</div>
</h3>
<div style="margin-left: 25px;">
<?php foreach($exampleConfigValues['schema']['attributes'][$table] as $attribute => $mappedAttribute): ?>
<div class="input-group">
<label for="<?php echo $table.ATTR_SEP.$attribute; ?>">Attribute "<?php echo $attribute; ?>"</label>
<?php if(isset($optionalAttributes[$table]) && in_array($attribute, $optionalAttributes[$table])): ?>
<div class="input-info">This attribute is optional (used by optional features) and doesn't need to be mapped.</div>
<?php endif; ?>
<div class="input">
<select name="<?php echo $table.ATTR_SEP.$attribute; ?>">
<option value="">-- Not mapped --</option>
<?php foreach($attributesInDatabase as $dbAttr => $dbAttrText): ?>
<option value="<?php echo $dbAttr; ?>" <?php echo getAttr($table.ATTR_SEP.$attribute, $mappedAttribute) == $dbAttr ? 'selected' : ''; ?>><?php echo $dbAttrText; ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if($table != $lastTable): ?>
<hr>
<?php endif; ?>
<?php endforeach; ?>
<hr class="invisible">
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<button class="button button-primary" type="submit">Continue</button>
</div>
</form>
<?php else: ?>
<div class="notification notification-fail">You took the wrong turn, <a href="/">restart installation</a>.</div>
<?php endif; ?>

216
installer/step4.php Normal file
View file

@ -0,0 +1,216 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
$thisStep = 4;
$error = null;
/*-----------------------------------------------------------------------------*/
$exampleConfigValues = require_once 'config/config.php.example';
$hashAlgorithms = array(
'SHA-512',
'SHA-256',
'BLOWFISH',
);
Database::init($_SESSION['installer']['config']['mysql']);
$databaseUserCount = Database::getInstance()->count(
$_SESSION['installer']['config']['schema']['tables']['users'],
$_SESSION['installer']['config']['schema']['attributes']['users']['id']
);
function getAttr($name, $default = null)
{
global $_SESSION, $_POST;
if(isset($_POST[$name])){
return strip_tags($_POST[$name]);
}
elseif(isset($_SESSION['installer']['config']['password'][$name])){
return $_SESSION['installer']['config']['password'][$name];
}
elseif($name === 'admin_user' && isset($_SESSION['installer']['user']['user'])){
return $_SESSION['installer']['user']['user'];
}
elseif($name === 'admin_password' && isset($_SESSION['installer']['user']['password'])){
return $_SESSION['installer']['user']['password'];
}
return $default;
}
/*-----------------------------------------------------------------------------*/
if(isset($_GET['go'])){
if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){
try{
if(!isset($_POST['hash_algorithm']) || !isset($_POST['min_length']) || !isset($_POST['admin_user']) || !isset($_POST['admin_password'])){
throw new InvalidArgumentException;
}
$passwordConfig = array(
'hash_algorithm' => in_array($_POST['hash_algorithm'], $hashAlgorithms) ? $_POST['hash_algorithm'] : $exampleConfigValues['password']['hash_algorithm'],
'min_length' => intval($_POST['min_length']),
);
// init system for testing
Config::init(array('password' => $passwordConfig));
// handle user
if($databaseUserCount > 0){
// testing existing login
$validLogin = Auth::login($_POST['admin_user'], $_POST['admin_password']);
unset($_SESSION[Auth::SESSION_IDENTIFIER]);
if(!$validLogin){
throw new Exception('Invalid combination of user and password.');
}
}
else{
// create user in database
if(strpos($_POST['admin_user'], '@') === false){
throw new Exception('The field "Your user" must be an email address.');
}
else{
list($username, $domain) = explode('@', $_POST['admin_user']);
$passwordHash = Auth::generatePasswordHash($_POST['admin_password']);
$hasDomain = Database::getInstance()->count(
$_SESSION['installer']['config']['schema']['tables']['domains'],
$_SESSION['installer']['config']['schema']['attributes']['domains']['id'],
array($_SESSION['installer']['config']['schema']['attributes']['domains']['domain'], $domain)
);
if($hasDomain === 0){
Database::getInstance()->insert(
$_SESSION['installer']['config']['schema']['tables']['domains'],
array(
$_SESSION['installer']['config']['schema']['attributes']['domains']['domain'] => $domain,
)
);
}
Database::getInstance()->insert(
$_SESSION['installer']['config']['schema']['tables']['users'],
array(
$_SESSION['installer']['config']['schema']['attributes']['users']['username'] => $username,
$_SESSION['installer']['config']['schema']['attributes']['users']['domain'] => $domain,
$_SESSION['installer']['config']['schema']['attributes']['users']['password'] => $passwordHash,
)
);
}
}
// saving information
$_SESSION['installer']['config']['password'] = $passwordConfig;
$_SESSION['installer']['config']['admins'] = array($_POST['admin_user']);
$_SESSION['installer']['config']['admin_domain_limits'] = array();
$_SESSION['installer']['user'] = array(
'user' => $_POST['admin_user'],
'password' => $_POST['admin_password'],
);
installer_message('You have successfully added your first admin user.');
installer_next($thisStep);
}
catch(InvalidArgumentException $e){
$error = 'Some fields are missing.';
}
catch(Exception $e){
$error = $e->getMessage();
}
}
elseif($_GET['go'] == 'prev'){
// reset
unset($_SESSION['installer']['config']['password']);
unset($_SESSION['installer']['config']['admins']);
unset($_SESSION['installer']['config']['admin_domain_limits']);
unset($_SESSION['installer']['user']);
installer_prev($thisStep, ($_SESSION['installer']['type'] === INSTALLER_TYPE_MAP) ? 1 : 2);
}
}
?>
<?php echo installer_message(); ?>
<h2>Step 3 of <?php echo INSTALLER_MAX_STEP; ?>: Your first admin user.</h2>
<?php if(!empty($error)): ?>
<div class="notification notification-fail"><?php echo $error; ?></div>
<?php endif; ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=next" method="post">
<div class="input-group">
<label for="password">Password hash algorithm</label>
<div class="input-info">Hash algorithm that you chose in your mailserver installation process.</div>
<div class="input">
<select name="hash_algorithm">
<?php foreach($hashAlgorithms as $algo): ?>
<option value="<?php echo $algo; ?>" <?php echo getAttr('hash_algorithm', $exampleConfigValues['password']['hash_algorithm']) == $algo ? 'selected' : ''; ?>>
<?php echo $algo; ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="input-group">
<label for="min_length">Minimum password length</label>
<div class="input">
<div class="input input-labeled input-labeled-right">
<input name="min_length" type="number" value="<?php echo getAttr('min_length', $exampleConfigValues['password']['min_length']); ?>" placeholder="Mailbox limit in MB" min="0"/>
<span class="input-label">chars</span>
</div>
</div>
</div>
<hr>
<?php if($databaseUserCount === 0): ?>
<div class="notification notification-warning">
There is no user created yet, please create one now as your admin user.
<br>Please note that once the user is created you will have to remember the password.
</div>
<?php endif; ?>
<p>This user will be mark as an admin in the configuration.</p>
<div class="input-group">
<label for="admin_user">Your user</label>
<div class="input-info">
Must be an email address (user@domain).<br>
<?php if($databaseUserCount > 0): ?>
This user must have been added in mailserver installation process.<br>
<?php endif; ?>
</div>
<div class="input">
<input type="text" name="admin_user" value="<?php echo getAttr('admin_user'); ?>"/>
</div>
</div>
<div class="input-group">
<label for="admin_password">Your password</label>
<div class="input">
<input type="password" name="admin_password" value="<?php echo getAttr('admin_password'); ?>"/>
</div>
</div>
<hr class="invisible">
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<button class="button button-primary" type="submit">Continue</button>
</div>
</form>

150
installer/step5.php Normal file
View file

@ -0,0 +1,150 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
$thisStep = 5;
$error = null;
/*-----------------------------------------------------------------------------*/
$exampleConfigValues = require_once 'config/config.php.example';
$possibleEmailSeparatorsText = array(', ', '; ', "\n");
$possibleEmailSeparatorsForm = array(',', ';', "\n");
function getAttr($name, $default = null)
{
global $_SESSION, $_POST;
if(isset($_POST[$name])){
return strip_tags($_POST[$name]);
}
elseif($name === 'base_url' && isset($_SESSION['installer']['config']['base_url'])){
return $_SESSION['installer']['config']['base_url'];
}
elseif(isset($_SESSION['installer']['config']['frontend_options'][$name])){
return $_SESSION['installer']['config']['frontend_options'][$name];
}
return $default;
}
/*-----------------------------------------------------------------------------*/
if(isset($_GET['go'])){
if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){
try{
if(!isset($_POST['base_url']) || empty($_POST['base_url'])){
throw new Exception('The field URL isn\'t filled out yet.');
}
if(!isset($_POST['email_separator_text'])
|| !is_numeric($_POST['email_separator_text'])
|| !isset($possibleEmailSeparatorsText[$_POST['email_separator_text']])
|| !isset($_POST['email_separator_form'])
|| !is_numeric($_POST['email_separator_form'])
|| !isset($possibleEmailSeparatorsForm[$_POST['email_separator_form']])
){
throw new InvalidArgumentException;
}
// saving information
$_SESSION['installer']['config']['base_url'] = $_POST['base_url'];
$_SESSION['installer']['config']['frontend_options'] = array(
'email_separator_text' => $possibleEmailSeparatorsText[$_POST['email_separator_text']],
'email_separator_form' => $possibleEmailSeparatorsForm[$_POST['email_separator_form']],
);
installer_message('General settings saved.');
installer_next($thisStep);
}
catch(InvalidArgumentException $e){
$error = 'Some field is missing.';
}
catch(Exception $e){
$error = $e->getMessage();
}
}
elseif($_GET['go'] == 'prev'){
// reset
unset($_SESSION['installer']['config']['base_url']);
unset($_SESSION['installer']['config']['frontend_options']);
installer_prev($thisStep);
}
}
?>
<?php echo installer_message(); ?>
<h2>Step 4 of <?php echo INSTALLER_MAX_STEP; ?>: General settings</h2>
<?php if(!empty($error)): ?>
<div class="notification notification-fail"><?php echo $error; ?></div>
<?php endif; ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=next" method="post">
<div class="input-group">
<label for="base_url">URL to this WebMUM installation</label>
<div class="input-info">
The URL your WebMUM installation is accessible from outside including subdirectories, ports and the protocol.
<br><br>Some examples:
<ul style="margin: 2px 0">
<li>http://localhost/webmum</li>
<li>http://webmum.mydomain.tld</li>
<li>https://mydomain.tld/dir</li>
<li>http://mydomain.tld:8080</li>
</ul>
</div>
<div class="input">
<input type="text" name="base_url" value="<?php echo getAttr('base_url'); ?>"/>
</div>
</div>
<hr>
<div class="input-group">
<label>Separator for email lists</label>
<div class="input-group">
<label for="email_separator_text">&hellip; in texts.</label>
<div class="input">
<input type="radio" name="email_separator_text" id="email_separator_text_0" value="0" <?php echo (getAttr('email_separator_text', 0) == 0) ? 'checked' : ''; ?>>
<label for="email_separator_text_0">comma: <code>', '</code></label>
<input type="radio" name="email_separator_text" id="email_separator_text_1" value="1" <?php echo (getAttr('email_separator_text', 0) == 1) ? 'checked' : ''; ?>>
<label for="email_separator_text_1">semicolon: <code>'; '</code></label>
<input type="radio" name="email_separator_text" id="email_separator_text_2" value="2" <?php echo (getAttr('email_separator_text', 0) == 2) ? 'checked' : ''; ?>>
<label for="email_separator_text_2">newline: <code>'&lt;br&gt;'</code></label>
</div>
</div>
<div class="input-group">
<label for="email_separator_form">&hellip; in forms.</label>
<div class="input">
<input type="radio" name="email_separator_form" id="email_separator_form_0" value="0" <?php echo (getAttr('email_separator_form', 0) == 0) ? 'checked' : ''; ?>>
<label for="email_separator_form_0">comma: <code>','</code></label>
<input type="radio" name="email_separator_form" id="email_separator_form_1" value="1" <?php echo (getAttr('email_separator_form', 0) == 1) ? 'checked' : ''; ?>>
<label for="email_separator_form_1">semicolon: <code>';'</code></label>
<input type="radio" name="email_separator_form" id="email_separator_form_2" value="2" <?php echo (getAttr('email_separator_form', 0) == 2) ? 'checked' : ''; ?>>
<label for="email_separator_form_2">newline: <code>'\n'</code></label>
</div>
</div>
</div>
<hr class="invisible">
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<button class="button button-primary" type="submit">Continue</button>
</div>
</form>

270
installer/step6.php Normal file
View file

@ -0,0 +1,270 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
$thisStep = 6;
$error = null;
/*-----------------------------------------------------------------------------*/
$exampleConfigValues = require_once 'config/config.php.example';
function getAttr($name, $default = null)
{
global $_SESSION, $_POST;
if(isset($_POST[$name])){
return strip_tags($_POST[$name]);
}
elseif(isset($_SESSION['installer']['config']['options'][$name])){
return $_SESSION['installer']['config']['options'][$name];
}
return $default;
}
/*-----------------------------------------------------------------------------*/
if(isset($_GET['go'])){
if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){
try{
$options = array();
// Mailbox limits
if(isset($_POST['enable_mailbox_limits']) && $_POST['enable_mailbox_limits'] == 1){
if(empty($_SESSION['installer']['config']['schema']['attributes']['users']['mailbox_limit'])){
throw new Exception('Mailbox limits couldn\'t be enabled, because the attribute "mailbox_limit" in database table "users" is missing or not mapped yet');
}
else{
$options['enable_mailbox_limits'] = true;
}
}
else{
$options['enable_mailbox_limits'] = false;
}
// Validate source addresses in redirects
if(isset($_POST['enable_validate_aliases_source_domain']) && $_POST['enable_validate_aliases_source_domain'] == 1){
$options['enable_validate_aliases_source_domain'] = true;
}
else{
$options['enable_validate_aliases_source_domain'] = false;
}
// Multiple source redirect support
if(isset($_POST['enable_multi_source_redirects']) && $_POST['enable_multi_source_redirects'] == 1){
if(empty($_SESSION['installer']['config']['schema']['attributes']['aliases']['multi_source'])){
throw new Exception('Multiple source redirect support couldn\'t be enabled, because the attribute "multi_source" in database table "aliases" is missing or not mapped yet');
}
else{
$options['enable_multi_source_redirects'] = true;
}
}
else{
$options['enable_multi_source_redirects'] = false;
}
// Admin domain limits
if(isset($_POST['enable_admin_domain_limits']) && $_POST['enable_admin_domain_limits'] == 1){
$options['enable_admin_domain_limits'] = true;
}
else{
$options['enable_admin_domain_limits'] = false;
}
// Users redirects
if(isset($_POST['enable_user_redirects']) && $_POST['enable_user_redirects'] == 1){
if(empty($_SESSION['installer']['config']['schema']['attributes']['users']['max_user_redirects'])
|| empty($_SESSION['installer']['config']['schema']['attributes']['aliases']['is_created_by_user'])
){
throw new Exception('Users redirects couldn\'t be enabled, because some database attributes are missing or not mapped yet');
}
else{
$options['enable_user_redirects'] = true;
}
}
else{
$options['enable_user_redirects'] = false;
}
// Logging for failed login attempts
$logPath = '';
if(isset($_POST['enable_logging']) && $_POST['enable_logging'] == 1){
$options['enable_logging'] = true;
if(!isset($_POST['log_path']) || empty($_POST['log_path'])){
throw new Exception('You need to set the log path if you enabled logging.');
}
$logPath = $_POST['log_path'];
if(!file_exists($_POST['log_path'])){
throw new Exception('The log path you set doesn\'t exist.');
}
if(!is_writable($_POST['log_path'])){
throw new Exception('The log path you set isn\'t writable.');
}
}
else{
$options['enable_logging'] = false;
}
// saving information
$_SESSION['installer']['config']['options'] = $options;
$_SESSION['installer']['config']['log_path'] = $logPath;
installer_message('Saved settings for optional features.');
installer_next($thisStep);
}
catch(Exception $e){
$error = $e->getMessage();
}
}
elseif($_GET['go'] == 'prev'){
// reset
unset($_SESSION['installer']['config']['options']);
unset($_SESSION['installer']['config']['log_path']);
installer_prev($thisStep);
}
}
?>
<?php echo installer_message(); ?>
<h2>Step 5 of <?php echo INSTALLER_MAX_STEP; ?>: Optional features</h2>
<?php if(!empty($error)): ?>
<div class="notification notification-fail"><?php echo $error; ?></div>
<?php endif; ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=next" method="post">
<div class="input-group">
<label for="enable_mailbox_limits">Mailbox limits</label>
<div class="input-info">Limit the maximum size of mailbox for users.</div>
<?php if(empty($_SESSION['installer']['config']['schema']['attributes']['users']['mailbox_limit'])): ?>
<p class="text-warning">
<strong>This feature cannot be enabled because the attribute "mailbox_limit" in database table "users" is missing or not mapped yet.</strong>
<br><br>You could go back and create / map the missing attribute.
</p>
<?php else: ?>
<div class="input">
<input type="checkbox" name="enable_mailbox_limits" id="enable_mailbox_limits" value="1" <?php echo getAttr('enable_mailbox_limits', false) ? 'checked' : ''; ?>>
<label for="enable_mailbox_limits">Enable feature</label>
</div>
<?php endif; ?>
</div>
<hr>
<div class="input-group">
<label for="enable_validate_aliases_source_domain">Validate source addresses in redirects</label>
<div class="input-info">Only email addresses ending with a domain from domains will be allowed.</div>
<div class="input">
<input type="checkbox" name="enable_validate_aliases_source_domain" id="enable_validate_aliases_source_domain" value="1" <?php echo getAttr('enable_validate_aliases_source_domain', true) ? 'checked' : ''; ?>>
<label for="enable_validate_aliases_source_domain">Enable feature</label>
</div>
</div>
<hr>
<div class="input-group">
<label for="enable_multi_source_redirects">Multiple source redirect support</label>
<div class="input-info">Redirects can have multiple source addresses. This enables you to enter multiple redirects to a destination at once.</div>
<?php if(empty($_SESSION['installer']['config']['schema']['attributes']['aliases']['multi_source'])): ?>
<p class="text-warning">
<strong>This feature cannot be enabled because the attribute "multi_source" in database table "aliases" is missing or not mapped yet.</strong>
<br><br>You could go back and create / map the missing attribute.
</p>
<?php else: ?>
<div class="input">
<input type="checkbox" name="enable_multi_source_redirects" id="enable_multi_source_redirects" value="1" <?php echo getAttr('enable_multi_source_redirects', false) ? 'checked' : ''; ?>>
<label for="enable_multi_source_redirects">Enable feature</label>
</div>
<?php endif; ?>
</div>
<hr>
<div class="input-group">
<label for="enable_admin_domain_limits">Admin domain limits</label>
<div class="input-info">
Limit certain admins to have access to certain domains only.
<br>Note: This needs to be manually configured in the <code>'admin_domain_limits'</code> config variable.
</div>
<div class="input">
<input type="checkbox" name="enable_admin_domain_limits" id="enable_admin_domain_limits" value="1" <?php echo getAttr('enable_admin_domain_limits', false) ? 'checked' : ''; ?>>
<label for="enable_admin_domain_limits">Enable feature</label>
</div>
</div>
<hr>
<div class="input-group">
<label for="enable_user_redirects">Users redirects</label>
<div class="input-info">
Enable users to create their redirects on their own.
<br>Users can also be limited to a maximum number of redirects they can create.
</div>
<?php if(empty($_SESSION['installer']['config']['schema']['attributes']['users']['max_user_redirects']) || empty($_SESSION['installer']['config']['schema']['attributes']['aliases']['is_created_by_user'])): ?>
<p class="text-warning">
<strong>This feature cannot be enabled because,
<?php if(empty($_SESSION['installer']['config']['schema']['attributes']['users']['max_user_redirects']) && empty($_SESSION['installer']['config']['schema']['attributes']['aliases']['is_created_by_user'])): ?>
there are missing attributes in two database tables:</strong>
<ul>
<li>"max_user_redirects" in "users"</li>
<li>"is_created_by_user" in "aliases"</li>
</ul>
<br>You could go back and create / map the missing attributes.
<?php else: ?>
the attribute <?php echo empty($_SESSION['installer']['config']['schema']['attributes']['users']['max_user_redirects']) ? '"max_user_redirects" in database table "users"' : '"is_created_by_user" in database table "aliases"'; ?> is missing or not mapped yet.
<?php endif; ?>
</strong>
<br><br>You could go back and create / map the missing attributes.
</p>
<?php else: ?>
<div class="input">
<input type="checkbox" name="enable_user_redirects" id="enable_user_redirects" value="1" <?php echo getAttr('enable_user_redirects', false) ? 'checked' : ''; ?>>
<label for="enable_user_redirects">Enable feature</label>
</div>
<?php endif; ?>
</div>
<hr>
<div class="input-group">
<label for="enable_logging">Logging for failed login attempts</label>
<div class="input-info">
WebMUM will write messages into the logfile.
<br>The logfile could be used by <strong>Fail2ban</strong> to block brute-forcing attacks.
</div>
<div class="input">
<input type="checkbox" name="enable_logging" id="enable_logging" value="1" <?php echo getAttr('enable_logging', false) ? 'checked' : ''; ?>>
<label for="enable_logging">Enable feature</label>
</div>
</div>
<div class="input-group">
<label for="log_path">Log path</label>
<div class="input-info">Directory where the <code>webmum.log</code> should be written to:</div>
<div class="input">
<input type="text" name="log_path" value="<?php echo getAttr('log_path'); ?>">
</div>
</div>
<hr class="invisible">
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<button class="button button-primary" type="submit">Continue</button>
</div>
</form>

152
installer/step7.php Normal file
View file

@ -0,0 +1,152 @@
<?php
if(strpos($_SERVER['REQUEST_URI'], 'installer/') !== false){
die('You cannot directly access the installer files.');
}
/*-----------------------------------------------------------------------------*/
$thisStep = 7;
$error = '';
/*-----------------------------------------------------------------------------*/
$configPath = dirname(__DIR__).DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php';
$configString = '<?php'.PHP_EOL
.'// This config has been automatically generated by the WebMUM installer.'.PHP_EOL.PHP_EOL
.'return '.var_export($_SESSION['installer']['config'], true).';'.PHP_EOL;
/*-----------------------------------------------------------------------------*/
if(isset($_SESSION['installer']['finished'])){
if(!file_exists($configPath)){
unset($_SESSION['installer']['finished']);
}
}
if(isset($_GET['go'])){
if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){
try{
if(isset($_POST['automatic']) && $_POST['automatic'] == 1){
if(file_exists($configPath)){
throw new Exception('The file "'.$configPath.'"" already exists, if you\'ve already written the config manually then complete manually.');
}
if(!file_exists(dirname($configPath)) || !is_dir(dirname($configPath))){
throw new Exception('The directory "'.dirname($configPath).'"" is missing.');
}
// Write config
if(file_put_contents($configPath, $configString) === false){
throw new Exception('Couldn\'t automatically write config to "'.$configPath.'", please write the config on your own.');
}
$_SESSION['installer']['finished'] = true;
}
elseif(isset($_POST['manual']) && $_POST['manual'] == 1){
if(!file_exists($configPath)){
throw new Exception('You need to write the config file first before you can manually complete the installation.');
}
$configValues = require_once 'config/config.php';
if(!is_array($configValues)){
throw new Exception('The data in the config file is invalid, please try again and be sure to use the config provided below.');
}
$_SESSION['installer']['finished'] = true;
}
}
catch(Exception $e){
$error = $e->getMessage();
}
}
elseif($_GET['go'] == 'finish'){
try{
if(isset($_SESSION['installer']['finished'])){
// Load config
$configValues = include_once 'config/config.php';
if(!is_array($configValues)){
throw new Exception('Error writing the config, please manually write the config to "'.$configPath.'".');
}
// Init system
Config::init($configValues);
Database::init(Config::get('mysql'));
Auth::init();
// Login user
Auth::login($_SESSION['installer']['user']['user'], $_SESSION['installer']['user']['password']);
// Reset installer
unset($_SESSION['installer']);
Router::redirect('/');
}
}
catch(Exception $e){
$error = $e->getMessage();
}
}
elseif($_GET['go'] == 'prev'){
installer_prev($thisStep);
}
}
?>
<?php if(isset($_SESSION['installer']['finished'])): ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=finish" method="post">
<div class="notification notification-success">You finished your installation!</div>
<h2 style="text-align: center;">Welcome to WebMUM - Web Mailserver User Manager.</h2>
<div>
If you like this project, be sure to give us a Star and Follow the project on GitHub <a target="_blank" href="https://git.io/vwXhh">https://github.com/ohartl/webmum</a>
<ol>
<li>To change the configuration you have to edit to config file "<?php echo $configPath; ?>" (see <a target="_blank" href="https://git.io/vwXhh#webmum-configuration">the README</a> for further instructions.</li>
<li>If you've found a bug or got a great idea, feel free to submit an issue on GitHub <a target="_blank" href="https://git.io/vrnOM">here</a>.</li>
</ol>
</div>
<hr class="invisible">
<p>By clicking Finish you will end the installation process and get logged in automatically.</p>
<div class="buttons">
<button class="button button-primary" type="submit">Finish &amp; Start using WebMUM</button>
</div>
</form>
<?php else: ?>
<?php echo installer_message(); ?>
<h2>Step 6 of <?php echo INSTALLER_MAX_STEP; ?>: Write the config &amp; finish the installation!</h2>
<?php if(!empty($error)): ?>
<div class="notification notification-fail"><?php echo $error; ?></div>
<?php endif; ?>
<form class="form" action="/?step=<?php echo $thisStep; ?>&go=next" method="post">
<p>The following config needs to be written to <code><?php echo $configPath; ?></code>.</p>
<textarea readonly style="width: 100%; height: 500px;"><?php echo $configString; ?></textarea>
<hr class="invisible">
<div>
This is the last step, you are almost there!<br>
<ul>
<li>Click "Already manually completed" if you already wrote the config to "<?php echo $configPath; ?>".</li>
<li>Or click "Complete automatically" if you want the installer to do the work for you.</li>
</ul>
</div>
<div class="buttons">
<a class="button" href="/?step=<?php echo $thisStep; ?>&go=prev">Back</a>
<button class="button" name="manual" value="1" type="submit">Already manually completed</button>
<button class="button button-primary" name="automatic" value="1" type="submit">Complete automatically!</button>
</div>
</form>
<?php endif; ?>

View file

@ -1 +0,0 @@
console.log('This would be the main JS file.');

File diff suppressed because one or more lines are too long

13
phpunit.xml Normal file
View file

@ -0,0 +1,13 @@
<phpunit bootstrap="include/php/default.inc.php">
<testsuites>
<testsuite name="webmum">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">include/php/classes</directory>
<directory suffix=".php">include/php/models</directory>
</whitelist>
</filter>
</phpunit>

View file

@ -1,124 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2015 GitHub, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
.pl-c /* comment */ {
color: #969896;
}
.pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */,
.pl-s .pl-v /* string variable */ {
color: #0086b3;
}
.pl-e /* entity */,
.pl-en /* entity.name */ {
color: #795da3;
}
.pl-s .pl-s1 /* string source */,
.pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ {
color: #333;
}
.pl-ent /* entity.name.tag */ {
color: #63a35c;
}
.pl-k /* keyword, storage, storage.type */ {
color: #a71d5d;
}
.pl-pds /* punctuation.definition.string, string.regexp.character-class */,
.pl-s /* string */,
.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
.pl-sr /* string.regexp */,
.pl-sr .pl-cce /* string.regexp constant.character.escape */,
.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */,
.pl-sr .pl-sre /* string.regexp source.ruby.embedded */ {
color: #183691;
}
.pl-v /* variable */ {
color: #ed6a43;
}
.pl-id /* invalid.deprecated */ {
color: #b52a1d;
}
.pl-ii /* invalid.illegal */ {
background-color: #b52a1d;
color: #f8f8f8;
}
.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
color: #63a35c;
font-weight: bold;
}
.pl-ml /* markup.list */ {
color: #693a17;
}
.pl-mh /* markup.heading */,
.pl-mh .pl-en /* markup.heading entity.name */,
.pl-ms /* meta.separator */ {
color: #1d3e81;
font-weight: bold;
}
.pl-mq /* markup.quote */ {
color: #008080;
}
.pl-mi /* markup.italic */ {
color: #333;
font-style: italic;
}
.pl-mb /* markup.bold */ {
color: #333;
font-weight: bold;
}
.pl-md /* markup.deleted, meta.diff.header.from-file */ {
background-color: #ffecec;
color: #bd2c00;
}
.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ {
background-color: #eaffea;
color: #55a532;
}
.pl-mdr /* meta.diff.range */ {
color: #795da3;
font-weight: bold;
}
.pl-mo /* meta.output */ {
color: #1d3e81;
}

View file

@ -1,228 +0,0 @@
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
padding: 0;
margin: 0;
font: inherit;
font-size: 100%;
vertical-align: baseline;
border: 0;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, serif;
font-size: 13px;
line-height: 1.5;
color: #000;
}
a {
font-weight: bold;
color: #d5000d;
}
header {
padding-top: 35px;
padding-bottom: 10px;
}
header h1 {
font-size: 48px;
font-weight: bold;
line-height: 1.2;
color: #303030;
letter-spacing: -1px;
}
header h2 {
font-size: 24px;
font-weight: normal;
line-height: 1.3;
color: #aaa;
letter-spacing: -1px;
}
#downloads {
display: none;
}
#main_content {
padding-top: 20px;
}
code, pre {
margin-bottom: 30px;
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal;
font-size: 12px;
color: #222;
}
code {
padding: 0 3px;
}
pre {
padding: 20px;
overflow: auto;
border: solid 1px #ddd;
}
pre code {
padding: 0;
}
ul, ol, dl {
margin-bottom: 20px;
}
/* COMMON STYLES */
table {
width: 100%;
border: 1px solid #ebebeb;
}
th {
font-weight: 500;
}
td {
font-weight: 300;
text-align: center;
border: 1px solid #ebebeb;
}
form {
padding: 20px;
background: #f2f2f2;
}
/* GENERAL ELEMENT TYPE STYLES */
h1 {
font-size: 2.8em;
}
h2 {
margin-bottom: 8px;
font-size: 22px;
font-weight: bold;
color: #303030;
}
h3 {
margin-bottom: 8px;
font-size: 18px;
font-weight: bold;
color: #d5000d;
}
h4 {
font-size: 16px;
font-weight: bold;
color: #303030;
}
h5 {
font-size: 1em;
color: #303030;
}
h6 {
font-size: .8em;
color: #303030;
}
p {
margin-bottom: 20px;
font-weight: 300;
}
a {
text-decoration: none;
}
p a {
font-weight: 400;
}
blockquote {
padding: 0 0 0 30px;
margin-bottom: 20px;
font-size: 1.6em;
border-left: 10px solid #e9e9e9;
}
ul li {
padding-left: 20px;
list-style-position: inside;
list-style: disc;
}
ol li {
padding-left: 3px;
list-style-position: inside;
list-style: decimal;
}
dl dd {
font-style: italic;
font-weight: 100;
}
footer {
padding-top: 20px;
padding-bottom: 30px;
margin-top: 40px;
font-size: 13px;
color: #aaa;
}
footer a {
color: #666;
}
/* MISC */
.clearfix:after {
display: block;
height: 0;
clear: both;
visibility: hidden;
content: '.';
}
.clearfix {display: inline-block;}
* html .clearfix {height: 1%;}
.clearfix {display: block;}

View file

@ -1,881 +0,0 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
margin: 0.67em 0;
font-size: 2em;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
color: #000;
background: #ff0;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
height: 0;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
margin: 0; /* 3 */
font: inherit; /* 2 */
color: inherit; /* 1 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
padding: 0;
border: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-box-sizing: content-box; /* 2 */
-moz-box-sizing: content-box;
box-sizing: content-box;
-webkit-appearance: textfield; /* 1 */
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
padding: 0.35em 0.625em 0.75em;
margin: 0 2px;
border: 1px solid #c0c0c0;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
padding: 0; /* 2 */
border: 0; /* 1 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-spacing: 0;
border-collapse: collapse;
}
td,
th {
padding: 0;
}
/* LAYOUT STYLES */
body {
font-family: 'Helvetica Neue', Helvetica, Arial, serif;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
color: #666;
background: #fafafa url(../images/body-bg.jpg) 0 0 repeat;
}
p {
margin-top: 0;
}
a {
color: #2879d0;
}
a:hover {
color: #2268b2;
}
header {
padding-top: 40px;
padding-bottom: 40px;
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
background: #2e7bcf url(../images/header-bg.jpg) 0 0 repeat-x;
border-bottom: solid 1px #275da1;
}
header h1 {
width: 540px;
margin-top: 0;
margin-bottom: 0.2em;
font-size: 72px;
font-weight: normal;
line-height: 1;
color: #fff;
letter-spacing: -1px;
}
header h2 {
width: 540px;
margin-top: 0;
margin-bottom: 0;
font-size: 26px;
font-weight: normal;
line-height: 1.3;
color: #9ddcff;
letter-spacing: 0;
}
.inner {
position: relative;
width: 940px;
margin: 0 auto;
}
#content-wrapper {
padding-top: 30px;
border-top: solid 1px #fff;
}
#main-content {
float: left;
width: 690px;
}
#main-content img {
max-width: 100%;
}
aside#sidebar {
float: right;
width: 200px;
min-height: 504px;
padding-left: 20px;
font-size: 12px;
line-height: 1.3;
background: transparent url(../images/sidebar-bg.jpg) 0 0 no-repeat;
}
aside#sidebar p.repo-owner,
aside#sidebar p.repo-owner a {
font-weight: bold;
}
#downloads {
margin-bottom: 40px;
}
a.button {
width: 134px;
height: 58px;
padding-top: 22px;
padding-left: 68px;
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
font-size: 23px;
line-height: 1.2;
color: #fff;
}
a.button small {
display: block;
font-size: 11px;
}
header a.button {
position: absolute;
top: 0;
right: 0;
background: transparent url(../images/github-button.png) 0 0 no-repeat;
}
aside a.button {
display: block;
width: 138px;
padding-left: 64px;
margin-bottom: 20px;
font-size: 21px;
background: transparent url(../images/download-button.png) 0 0 no-repeat;
}
code, pre {
margin-bottom: 30px;
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
font-size: 13px;
color: #222;
}
code {
padding: 0 3px;
background-color: #f2f8fc;
border: solid 1px #dbe7f3;
}
pre {
padding: 20px;
overflow: auto;
text-shadow: none;
background: #fff;
border: solid 1px #f2f2f2;
}
pre code {
padding: 0;
color: #2879d0;
background-color: #fff;
border: none;
}
ul, ol, dl {
margin-bottom: 20px;
}
/* COMMON STYLES */
hr {
height: 0;
margin-top: 1em;
margin-bottom: 1em;
border: 0;
border-top: solid 1px #ddd;
}
table {
width: 100%;
border: 1px solid #ebebeb;
}
th {
font-weight: 500;
}
td {
font-weight: 300;
text-align: center;
border: 1px solid #ebebeb;
}
form {
padding: 20px;
background: #f2f2f2;
}
/* GENERAL ELEMENT TYPE STYLES */
#main-content h1 {
margin-top: 0;
margin-bottom: 0;
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
font-size: 2.8em;
font-weight: normal;
color: #474747;
text-indent: 6px;
letter-spacing: -1px;
}
#main-content h1:before {
padding-right: 0.3em;
margin-left: -0.9em;
color: #9ddcff;
content: "/";
}
#main-content h2 {
margin-bottom: 8px;
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
font-size: 22px;
font-weight: bold;
color: #474747;
text-indent: 4px;
}
#main-content h2:before {
padding-right: 0.3em;
margin-left: -1.5em;
content: "//";
color: #9ddcff;
}
#main-content h3 {
margin-top: 24px;
margin-bottom: 8px;
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
font-size: 18px;
font-weight: bold;
color: #474747;
text-indent: 3px;
}
#main-content h3:before {
padding-right: 0.3em;
margin-left: -2em;
content: "///";
color: #9ddcff;
}
#main-content h4 {
margin-bottom: 8px;
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
font-size: 15px;
font-weight: bold;
color: #474747;
text-indent: 3px;
}
h4:before {
padding-right: 0.3em;
margin-left: -2.8em;
content: "////";
color: #9ddcff;
}
#main-content h5 {
margin-bottom: 8px;
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
font-size: 14px;
color: #474747;
text-indent: 3px;
}
h5:before {
padding-right: 0.3em;
margin-left: -3.2em;
content: "/////";
color: #9ddcff;
}
#main-content h6 {
margin-bottom: 8px;
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
font-size: .8em;
color: #474747;
text-indent: 3px;
}
h6:before {
padding-right: 0.3em;
margin-left: -3.7em;
content: "//////";
color: #9ddcff;
}
p {
margin-bottom: 20px;
}
a {
text-decoration: none;
}
p a {
font-weight: 400;
}
blockquote {
padding: 0 0 0 30px;
margin-bottom: 20px;
font-size: 1.6em;
border-left: 10px solid #e9e9e9;
}
ul {
list-style-position: inside;
list-style: disc;
padding-left: 20px;
}
ol {
list-style-position: inside;
list-style: decimal;
padding-left: 3px;
}
dl dd {
font-style: italic;
font-weight: 100;
}
footer {
padding-top: 20px;
padding-bottom: 30px;
margin-top: 40px;
font-size: 13px;
color: #aaa;
background: transparent url('../images/hr.png') 0 0 no-repeat;
}
footer a {
color: #666;
}
footer a:hover {
color: #444;
}
/* MISC */
.clearfix:after {
display: block;
height: 0;
clear: both;
visibility: hidden;
content: '.';
}
.clearfix {display: inline-block;}
* html .clearfix {height: 1%;}
.clearfix {display: block;}
/* #Media Queries
================================================== */
/* Smaller than standard 960 (devices and browsers) */
@media only screen and (max-width: 959px) { }
/* Tablet Portrait size to standard 960 (devices and browsers) */
@media only screen and (min-width: 768px) and (max-width: 959px) {
.inner {
width: 740px;
}
header h1, header h2 {
width: 340px;
}
header h1 {
font-size: 60px;
}
header h2 {
font-size: 30px;
}
#main-content {
width: 490px;
}
#main-content h1:before,
#main-content h2:before,
#main-content h3:before,
#main-content h4:before,
#main-content h5:before,
#main-content h6:before {
padding-right: 0;
margin-left: 0;
content: none;
}
}
/* All Mobile Sizes (devices and browser) */
@media only screen and (max-width: 767px) {
.inner {
width: 93%;
}
header {
padding: 20px 0;
}
header .inner {
position: relative;
}
header h1, header h2 {
width: 100%;
}
header h1 {
font-size: 48px;
}
header h2 {
font-size: 24px;
}
header a.button {
position: relative;
display: inline-block;
width: auto;
height: auto;
padding: 5px 10px;
margin-top: 15px;
font-size: 13px;
line-height: 1;
color: #2879d0;
text-align: center;
background-color: #9ddcff;
background-image: none;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
header a.button small {
display: inline;
font-size: 13px;
}
#main-content,
aside#sidebar {
float: none;
width: 100% ! important;
}
aside#sidebar {
min-height: 0;
padding: 20px 0;
margin-top: 20px;
background-image: none;
border-top: solid 1px #ddd;
}
aside#sidebar a.button {
display: none;
}
#main-content h1:before,
#main-content h2:before,
#main-content h3:before,
#main-content h4:before,
#main-content h5:before,
#main-content h6:before {
padding-right: 0;
margin-left: 0;
content: none;
}
}
/* Mobile Landscape Size to Tablet Portrait (devices and browsers) */
@media only screen and (min-width: 480px) and (max-width: 767px) { }
/* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */
@media only screen and (max-width: 479px) { }

202
tests/AuthTest.php Normal file
View file

@ -0,0 +1,202 @@
<?php
require_once 'TestCase.php';
/**
* @covers Auth
*/
class AuthTest extends TestCase
{
public function tearDown()
{
Auth::logout();
$_SESSION = array();
}
public function testInitGuest()
{
$_SESSION = array();
Auth::init();
$this->assertFalse(Auth::isLoggedIn());
$this->assertNull(Auth::getUser());
$this->assertFalse(Auth::hasPermission(User::ROLE_USER));
$this->assertFalse(Auth::hasPermission(User::ROLE_ADMIN));
}
public function testInitUser()
{
$_SESSION = array(
Auth::SESSION_IDENTIFIER => self::USER_ROLE_USER_ID
);
Auth::init();
$this->assertTrue(Auth::isLoggedIn());
$this->assertInstanceOf('User', Auth::getUser());
$this->assertTrue(Auth::hasPermission(User::ROLE_USER));
$this->assertFalse(Auth::hasPermission(User::ROLE_ADMIN));
}
public function testInitAdmin()
{
$_SESSION = array(
Auth::SESSION_IDENTIFIER => self::USER_ROLE_ADMIN_ID
);
Auth::init();
$this->assertTrue(Auth::isLoggedIn());
$this->assertInstanceOf('User', Auth::getUser());
$this->assertTrue(Auth::hasPermission(User::ROLE_USER));
$this->assertTrue(Auth::hasPermission(User::ROLE_ADMIN));
}
public function testLogin()
{
$_SESSION = array();
Auth::init();
$this->assertFalse(Auth::isLoggedIn());
$this->assertTrue(Auth::login('user@domain.tld', 'testtest'));
$this->assertTrue(Auth::isLoggedIn());
}
public function testLoginInvalidEmail()
{
$_SESSION = array();
Auth::init();
$this->assertFalse(Auth::isLoggedIn());
$this->assertFalse(Auth::login('domain.tld', 'test'));
$this->assertFalse(Auth::isLoggedIn());
}
public function testLoginInvalidUser()
{
$_SESSION = array();
Auth::init();
$this->assertFalse(Auth::isLoggedIn());
$this->assertFalse(Auth::login('no.user@domain.tld', 'test'));
$this->assertFalse(Auth::isLoggedIn());
}
public function testLogout()
{
$_SESSION = array(
Auth::SESSION_IDENTIFIER => self::USER_ROLE_USER_ID
);
Auth::init();
$this->assertTrue(Auth::isLoggedIn());
Auth::logout();
$this->assertFalse(Auth::isLoggedIn());
$this->assertArrayNotHasKey(Auth::SESSION_IDENTIFIER, $_SESSION);
}
/**
* @param int $length
* @return string
*/
protected static function genTestPw($length)
{
return substr(str_shuffle("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-+=_,!@$#*%<>[]{}"), 0, $length);
}
/**
* @expectedException AuthException
* @expectedExceptionCode 2
*/
public function testValidateNewPasswordFirstEmpty()
{
Auth::validateNewPassword('', static::genTestPw(Config::get('password.min_length', 8)));
}
/**
* @expectedException AuthException
* @expectedExceptionCode 2
*/
public function testValidateNewPasswordLastEmpty()
{
Auth::validateNewPassword(static::genTestPw(Config::get('password.min_length', 8)), '');
}
/**
* @expectedException AuthException
* @expectedExceptionCode 3
*/
public function testValidateNewPasswordNotEqual()
{
$pw = static::genTestPw(Config::get('password.min_length', 8));
Auth::validateNewPassword($pw, $pw.'neq');
}
/**
* @expectedException AuthException
* @expectedExceptionCode 4
*/
public function testValidateNewPasswordTooShort()
{
$pw = static::genTestPw(Config::get('password.min_length', 8) - 1);
Auth::validateNewPassword($pw, $pw);
}
public function testValidateNewPasswordOk()
{
$pw = static::genTestPw(Config::get('password.min_length', 8));
Auth::validateNewPassword($pw, $pw);
}
public function testGeneratePasswordHash()
{
Auth::generatePasswordHash(static::genTestPw(Config::get('password.min_length', 8)));
}
public function testGeneratePasswordHashAlgorithmFallback()
{
Config::set('password.hash_algorithm', '--not-an-algorithm--');
Auth::generatePasswordHash(static::genTestPw(Config::get('password.min_length', 8)));
}
public function testChangeUserPassword()
{
$this->assertTrue(Auth::login('user@domain.tld', 'testtest'));
Auth::changeUserPassword(static::USER_ROLE_USER_ID, 'newpassword');
$this->assertFalse(Auth::login('user@domain.tld', 'testtest'));
$this->assertTrue(Auth::login('user@domain.tld', 'newpassword'));
}
}

69
tests/ConfigTest.php Normal file
View file

@ -0,0 +1,69 @@
<?php
/**
* @covers Config
*/
class ConfigTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
Config::init(
array(
'test-value' => 123,
'test' => array(
'deep' => array(
'deeper' => 'value',
)
)
)
);
}
public function testInit()
{
$this->assertEquals(Config::get('test-value'), 123);
$this->assertEquals(Config::get('test.deep.deeper'), 'value');
}
public function testSet()
{
$this->assertEquals(Config::get('test-value'), 123);
Config::set('test-value', false);
$this->assertEquals(Config::get('test-value'), false);
$this->assertEquals(Config::get('test.deep.deeper'), 'value');
Config::set('test.deep.deeper', 'other');
$this->assertEquals(Config::get('test.deep.deeper'), 'other');
Config::set('test.new.deep.deeper', true);
$this->assertEquals(Config::get('test.new.deep.deeper'), true);
}
public function testGet()
{
$this->assertTrue(is_array(Config::get(null)));
$this->assertEquals(Config::get('test-value'), 123);
$this->assertEquals(Config::get('test.deep.deeper'), 'value');
$this->assertEquals(Config::get('test-default', 123456), 123456);
}
public function testHas()
{
$this->assertTrue(Config::has('test-value'));
$this->assertTrue(Config::has('test.deep.deeper'));
$this->assertFalse(Config::has('test-default'));
Config::init(null);
$this->assertFalse(Config::has(null));
}
}

44
tests/MessageTest.php Normal file
View file

@ -0,0 +1,44 @@
<?php
/**
* @covers Message
*/
class MessageTest extends PHPUnit_Framework_TestCase
{
public function testAdd()
{
Message::getInstance()->add(Message::TYPE_SUCCESS, 'lorem');
$out = Message::getInstance()->render();
$this->assertContains(Message::TYPE_SUCCESS, $out);
$this->assertContains('lorem', $out);
}
/**
* @expectedException InvalidArgumentException
*/
public function testAddRestrictTypes()
{
Message::getInstance()->add('wrong-type', 'lorem');
}
public function testAddShortcuts()
{
Message::getInstance()->fail('lorem');
$this->assertContains(Message::TYPE_FAIL, Message::getInstance()->render());
Message::getInstance()->error('lorem');
$this->assertContains(Message::TYPE_ERROR, Message::getInstance()->render());
Message::getInstance()->warning('lorem');
$this->assertContains(Message::TYPE_WARNING, Message::getInstance()->render());
Message::getInstance()->success('lorem');
$this->assertContains(Message::TYPE_SUCCESS, Message::getInstance()->render());
}
}

147
tests/RouterTest.php Normal file
View file

@ -0,0 +1,147 @@
<?php
/**
* @covers Router
*/
class RouterTest extends TestCase
{
const BASE_URL = 'http://test.tld/somedir';
public function setUp()
{
Config::set('base_url', self::BASE_URL);
Router::init(array());
}
public function testUrl()
{
$this->assertEquals(self::BASE_URL, Router::url());
$this->assertEquals(self::BASE_URL, Router::url('/'));
$this->assertEquals(self::BASE_URL.'/this/sub/dir?get=123', Router::url('this/sub/dir?get=123'));
}
public function testAdd()
{
Router::addRoute(Router::METHOD_GET, 'test-get', 'test-get-file');
Router::execute('test-get', Router::METHOD_GET);
Router::addRoute(Router::METHOD_POST, 'test-post', 'test-post-file');
Router::execute('test-post', Router::METHOD_POST);
Router::addRoute(array(Router::METHOD_GET, Router::METHOD_POST), 'test-mixed', 'test-mixed-file');
Router::execute('test-mixed', Router::METHOD_GET);
Router::execute('test-mixed', Router::METHOD_POST);
}
public function testAddCallback()
{
$reachedCallback = false;
Router::addRoute(Router::METHOD_GET, 'test-callback', function() use(&$reachedCallback) {
$reachedCallback = true;
});
Router::execute('test-callback', Router::METHOD_GET);
$this->assertTrue($reachedCallback);
}
/**
* @expectedException Exception
* @expectedExceptionMessageRegExp /unsupported/i
*/
public function testAddMethodUnsupported()
{
Router::addRoute('not-a-method', 'test-fail', 'test-fail-file');
}
public function testAddShortcuts()
{
Router::addGet('test-get', 'test-get-file');
Router::execute('test-get', Router::METHOD_GET);
Router::addPost('test-post', 'test-post-file');
Router::execute('test-post', Router::METHOD_POST);
Router::addMixed('test-mixed', 'test-mixed-file');
Router::execute('test-mixed', Router::METHOD_GET);
Router::execute('test-mixed', Router::METHOD_POST);
}
/**
* @expectedException Exception
* @expectedExceptionMessageRegExp /unsupported/i
*/
public function testExecuteMethodUnsupported()
{
Router::execute('test-fail', 'not-a-method');
}
public function testExecuteCurrentRequest()
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/somedir/test-get';
Router::executeCurrentRequest();
}
public function testRouteWithPermission()
{
$this->assertFalse(Auth::isLoggedIn());
$reachedCallback = false;
Router::addRoute(Router::METHOD_GET, 'test-perm-admin', function() use(&$reachedCallback) {
$reachedCallback = true;
}, User::ROLE_ADMIN);
Router::addRoute(Router::METHOD_GET, 'test-perm-user', function() use(&$reachedCallback) {
$reachedCallback = true;
}, User::ROLE_USER);
$reachedCallback = false;
Router::execute('test-perm-admin', Router::METHOD_GET);
$this->assertFalse($reachedCallback);
$reachedCallback = false;
Router::execute('test-perm-user', Router::METHOD_GET);
$this->assertFalse($reachedCallback);
// Now auth as admin and try again
Auth::login('admin@domain.tld', 'testtest');
$reachedCallback = false;
Router::execute('test-perm-admin', Router::METHOD_GET);
$this->assertTrue($reachedCallback);
$reachedCallback = false;
Router::execute('test-perm-user', Router::METHOD_GET);
$this->assertTrue($reachedCallback);
Auth::logout();
// Now auth as user and try again
Auth::login('user@domain.tld', 'testtest');
$reachedCallback = false;
Router::execute('test-perm-admin', Router::METHOD_GET);
$this->assertFalse($reachedCallback);
$reachedCallback = false;
Router::execute('test-perm-user', Router::METHOD_GET);
$this->assertTrue($reachedCallback);
Auth::logout();
}
}

77
tests/TestCase.php Normal file
View file

@ -0,0 +1,77 @@
<?php
/**
* @covers Auth
*/
abstract class TestCase extends PHPUnit_Framework_TestCase
{
const USER_ROLE_ADMIN_ID = 100001;
const USER_ROLE_ADMIN_ID_LIMITED_NO_ACCESS = 100002;
const USER_ROLE_ADMIN_ID_LIMITED_HAS_ACCESS = 100003;
const USER_ROLE_USER_ID = 100013;
public static function setUpBeforeClass()
{
Database::getInstance()->insert(
'users',
array(
'id' => static::USER_ROLE_ADMIN_ID,
'username' => 'admin',
'domain' => 'domain.tld',
'password' => Auth::generatePasswordHash('testtest'),
'mailbox_limit' => 0,
)
);
Database::getInstance()->insert(
'users',
array(
'id' => static::USER_ROLE_ADMIN_ID_LIMITED_NO_ACCESS,
'username' => 'no-access-limited-admin',
'domain' => 'domain.tld',
'password' => Auth::generatePasswordHash('testtest'),
'mailbox_limit' => 0,
)
);
Database::getInstance()->insert(
'users',
array(
'id' => static::USER_ROLE_ADMIN_ID_LIMITED_HAS_ACCESS,
'username' => 'has-access-limited-admin',
'domain' => 'domain.tld',
'password' => Auth::generatePasswordHash('testtest'),
'mailbox_limit' => 0,
)
);
Database::getInstance()->insert(
'users',
array(
'id' => static::USER_ROLE_USER_ID,
'username' => 'user',
'domain' => 'domain.tld',
'password' => Auth::generatePasswordHash('testtest'),
'mailbox_limit' => 64,
)
);
Config::set('admins', array('admin@domain.tld', 'limited-admin@domain.tld'));
Config::set('admin_domain_limits', array(
'no-access-limited-admin@domain.tld' => array(),
'has-access-limited-admin@domain.tld' => array('his-domain.tld'),
));
}
public static function tearDownAfterClass()
{
Database::getInstance()->delete('users', 'id', static::USER_ROLE_ADMIN_ID);
Database::getInstance()->delete('users', 'id', static::USER_ROLE_ADMIN_ID_LIMITED_NO_ACCESS);
Database::getInstance()->delete('users', 'id', static::USER_ROLE_ADMIN_ID_LIMITED_HAS_ACCESS);
Database::getInstance()->delete('users', 'id', static::USER_ROLE_USER_ID);
}
}