├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── .htaccess └── config.php.example ├── include ├── css │ └── style.css └── php │ ├── .htaccess │ ├── classes │ ├── Auth.php │ ├── AuthException.php │ ├── Config.php │ ├── Database.php │ ├── DatabaseException.php │ ├── Message.php │ └── Router.php │ ├── default.inc.php │ ├── global.inc.php │ ├── models │ ├── AbstractModel.php │ ├── AbstractMultiRedirect.php │ ├── AbstractRedirect.php │ ├── Alias.php │ ├── Domain.php │ ├── DomainLimitTrait.php │ ├── ModelCollection.php │ ├── MultiAlias.php │ ├── MultiRedirect.php │ ├── Redirect.php │ └── User.php │ ├── pages │ ├── admin │ │ ├── createdomain.php │ │ ├── deletedomain.php │ │ ├── deleteredirect.php │ │ ├── deleteuser.php │ │ ├── editredirect.php │ │ ├── edituser.php │ │ ├── listdomains.php │ │ ├── listredirects.php │ │ ├── listusers.php │ │ └── start.php │ ├── login.php │ ├── private │ │ ├── changepass.php │ │ ├── createredirect.php │ │ ├── deleteredirect.php │ │ ├── start.php │ │ └── yourredirects.php │ └── start.php │ ├── routes.inc.php │ └── template │ ├── error │ ├── not-allowed.php │ └── not-found.php │ └── layout.php ├── index.php ├── installer ├── .htaccess ├── index.php ├── step0.php ├── step1.php ├── step2.php ├── step3.php ├── step4.php ├── step5.php ├── step6.php └── step7.php ├── phpunit.xml └── tests ├── AuthTest.php ├── ConfigTest.php ├── MessageTest.php ├── RouterTest.php └── TestCase.php /.gitignore: -------------------------------------------------------------------------------- 1 | config/config.php -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.4' 4 | - '5.5' 5 | - '5.6' 6 | - '7.0' 7 | - hhvm 8 | - nightly 9 | matrix: 10 | allow_failures: 11 | - php: hhvm 12 | - php: nightly 13 | services: 14 | - mysql 15 | before_install: 16 | # Create example schema 17 | - mysql -u root -e "CREATE USER 'vmail'@'localhost' IDENTIFIED BY 'vmail';" 18 | - mysql -u root -e "CREATE DATABASE IF NOT EXISTS vmail;" 19 | - mysql -u root -e "GRANT ALL PRIVILEGES ON vmail.* TO 'vmail'@'localhost'" 20 | - 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;" 21 | - 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;" 22 | - 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;" 23 | # Copy the example config 24 | - cp config/config.php.example config/config.php 25 | notifications: 26 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Thomas Leister 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/.htaccess: -------------------------------------------------------------------------------- 1 | Deny from all -------------------------------------------------------------------------------- /config/config.php.example: -------------------------------------------------------------------------------- 1 | 'http://localhost/webmum', 19 | 20 | 21 | /****************************************************** 22 | * MySQL database connection settings 23 | */ 24 | 25 | 'mysql' => array( 26 | 'host' => 'localhost', 27 | 'user' => 'vmail', 28 | 'password' => 'vmail', 29 | 'database' => 'vmail', 30 | ), 31 | 32 | 33 | /****************************************************** 34 | * Database schema mapping 35 | */ 36 | 37 | 'schema' => array( 38 | // Table names 39 | 'tables' => array( 40 | // Example: 41 | // 'table-keyword' => 'actual-table-name', 42 | 43 | 'users' => 'users', 44 | 'domains' => 'domains', 45 | 'aliases' => 'aliases', 46 | ), 47 | 48 | 'attributes' => array( 49 | // Example: 50 | // 'table-keyword' => array( 51 | // 'attribute-keyword' => 'actual-attribute-name', 52 | // ... 53 | // ), 54 | 55 | // Users table columns 56 | 'users' => array( 57 | 'id' => 'id', 58 | 'username' => 'username', 59 | 'domain' => 'domain', 60 | 'password' => 'password', 61 | 'mailbox_limit' => 'mailbox_limit', // (Optional see 'options.enable_mailbox_limits') 62 | 'max_user_redirects' => 'max_user_redirects', // (Optional see 'options.enable_user_redirects') 63 | ), 64 | 65 | // Domains table columns 66 | 'domains' => array( 67 | 'id' => 'id', 68 | 'domain' => 'domain', 69 | ), 70 | 71 | // Aliases table columns 72 | 'aliases' => array( 73 | 'id' => 'id', 74 | 'source' => 'source', 75 | 'destination' => 'destination', 76 | 'multi_source' => 'multi_source', // (Optional see 'options.enable_multi_source_redirects') 77 | 'is_created_by_user' => 'is_created_by_user', // (Optional see 'options.enable_user_redirects') 78 | ), 79 | ), 80 | ), 81 | 82 | 83 | /****************************************************** 84 | * General options 85 | */ 86 | 87 | 'options' => array( 88 | 89 | /** 90 | * Enable mailbox limits. (Default false == off) 91 | * 92 | * Needs a new db attribute in users table with INT(10). 93 | * (see 'schema.attributes.users.mailbox_limit') 94 | */ 95 | 96 | 'enable_mailbox_limits' => false, 97 | 98 | 99 | /** 100 | * Enable validating that the source addresses are ending with domain from domains. (Default true == on) 101 | */ 102 | 103 | 'enable_validate_aliases_source_domain' => true, 104 | 105 | 106 | /** 107 | * Enable multi source redirects. (Default false == off) 108 | * 109 | * Needs a new db attribute in aliases table with VARCHAR(32). 110 | * (see 'schema.attributes.aliases.multi_source') 111 | */ 112 | 113 | 'enable_multi_source_redirects' => false, 114 | 115 | 116 | /** 117 | * Enable limited admin domain access. (Default false == off) 118 | * 119 | * Limitations can be configured under 'admin_domain_limits'. 120 | */ 121 | 122 | 'enable_admin_domain_limits' => false, 123 | 124 | 125 | /** 126 | * Enable users can create own redirects. (Default false == off) 127 | * 128 | * Needs two new db attributes in users table with INT(10) and aliases table with INT(1) + DEFAULT 0 129 | * (see 'schema.attributes.users.max_user_redirects' and 'schema.attributes.aliases.is_created_by_user') 130 | * 131 | * A maximum number of redirects per user can be configured. 132 | */ 133 | 134 | 'enable_user_redirects' => false, 135 | 136 | 137 | /** 138 | * Enable logging for failed login attempts. (Default false == off) 139 | * 140 | * You can monitor the logfile with fail2ban and ban attackers' IP-addresses. 141 | * Path to logfile can be configured under 'log_path'. 142 | */ 143 | 144 | 'enable_logging' => false, 145 | 146 | ), 147 | 148 | 149 | /****************************************************** 150 | * Admin e-mail addresses 151 | * 152 | * Users with these e-mail addresses will have admin access, 153 | * you can limit their access with the 'options.enable_admin_domain_limits' feature 154 | */ 155 | 156 | 'admins' => array( 157 | 'admin@domain.tld', 158 | ), 159 | 160 | 161 | /****************************************************** 162 | * Limited admin domain access (only used if 'options.enable_admin_domain_limits' is true) 163 | * 164 | * Unlisted admins have access to every domain, the admin is limited to listed domains only! 165 | * Unlisted domains are not accessible by that admin. 166 | * Note that listed admins cannot create new domains! 167 | */ 168 | 169 | 'admin_domain_limits' => array( 170 | // Example: 171 | // 'low_rank_admin@domain.tld' => array('his-domain.tld', 'shared-domain.tld'), 172 | ), 173 | 174 | 175 | /****************************************************** 176 | * Password 177 | */ 178 | 179 | 'password' => array( 180 | 181 | // Algorithm used for password encryption 182 | 'hash_algorithm' => 'SHA-512', // Supported algorithms: SHA-512, SHA-256, BLOWFISH 183 | 184 | // Minimum length for passwords 185 | 'min_length' => 8, 186 | 187 | ), 188 | 189 | 190 | /****************************************************** 191 | * Log file path (only used if 'options.enable_logging' is true) 192 | * 193 | * Make sure that PHP has permission to create the log directory and webmum.log (write permissions for php user) 194 | */ 195 | 196 | 'log_path' => '/var/www/webmum/log/', 197 | 198 | 199 | /****************************************************** 200 | * Frontend options 201 | */ 202 | 203 | 'frontend_options' => array( 204 | 205 | // Separator for email lists 206 | 'email_separator_text' => ', ', // possible values: ', ' (default), '; ', PHP_EOL (newline) 207 | 'email_separator_form' => ',', // possible values: ',' (default), ';', PHP_EOL (newline) 208 | 209 | ), 210 | 211 | ); 212 | -------------------------------------------------------------------------------- /include/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: arial, serif; 3 | font-size: 12px; 4 | margin: 0; 5 | background-color: white; 6 | } 7 | 8 | hr { 9 | border: none; 10 | border-bottom: 1px solid #ccc; 11 | margin: 20px 0; 12 | } 13 | 14 | hr.invisible { 15 | border-color: transparent; 16 | } 17 | 18 | .text-fail { 19 | color: #d90000; 20 | } 21 | 22 | .text-warning { 23 | color: #ADA900; 24 | } 25 | 26 | .text-success { 27 | color: #39AD00; 28 | } 29 | 30 | 31 | #header { 32 | position: relative; 33 | height: 50px; 34 | width: 100%; 35 | background: rgba(15, 15, 15, 1) linear-gradient(rgba(63, 63, 63, 1), rgba(15, 15, 15, 1)); 36 | color: white; 37 | line-height: 50px; 38 | box-sizing: border-box; 39 | padding-left: 20px; 40 | padding-right: 20px; 41 | } 42 | 43 | #header div.title { 44 | float: left; 45 | height: 50px; 46 | width: auto; 47 | } 48 | 49 | #header div.title a { 50 | font-size: 15px; 51 | color: white; 52 | text-decoration: none; 53 | } 54 | 55 | #header div.title a:hover { 56 | text-decoration: underline; 57 | } 58 | 59 | #header div.header-menu { 60 | float: left; 61 | padding-left: 100px; 62 | } 63 | 64 | #header div.header-button { 65 | float: left; 66 | height: 50px; 67 | margin-right: 30px; 68 | color: white; 69 | } 70 | 71 | #header div.header-button a { 72 | color: white; 73 | text-decoration: none; 74 | } 75 | 76 | #header div.header-button a:hover { 77 | text-decoration: underline; 78 | } 79 | 80 | #content { 81 | height: auto; 82 | min-height: calc(100vh - 150px); 83 | padding: 20px; 84 | background-color: white; 85 | } 86 | 87 | #content h1 { 88 | color: rgba(62, 59, 59, 1); 89 | } 90 | 91 | #content .sub-header { 92 | font-weight: normal; 93 | font-size: .9em; 94 | color: #999; 95 | padding: 5px 0 0 5px; 96 | } 97 | 98 | #content a { 99 | color: blue; 100 | text-decoration: none; 101 | } 102 | 103 | #content a:hover { 104 | text-decoration: underline; 105 | } 106 | 107 | 108 | #content .form { 109 | margin: 25px 0; 110 | } 111 | 112 | #content .form hr { 113 | margin: 5px 0 15px; 114 | } 115 | 116 | #content .form .input-group, #content .form .buttons { 117 | padding-bottom: 10px; 118 | } 119 | 120 | #content .form .input-group > label { 121 | font-weight: bold; 122 | line-height: 22px; 123 | font-size: 13px; 124 | } 125 | 126 | #content .form .input-group > .input-info { 127 | padding-bottom: 5px; 128 | color: #999; 129 | } 130 | 131 | #content .form .input-group > .input-group { 132 | padding-left: 25px; 133 | } 134 | 135 | #content .form .input-group > .input-group:first-of-type { 136 | padding-top: 10px; 137 | } 138 | 139 | #content .form .input-group > .input-group > label { 140 | font-size: 12px; 141 | } 142 | 143 | #content .form .input { 144 | } 145 | 146 | #content .form .input input, #content .form .input textarea, #content .form .input select { 147 | background: #fefefe; 148 | border: 1px solid rgba(200, 200, 200, 1); 149 | border-radius: 3px; 150 | margin-bottom: 5px; 151 | padding: 0 10px; 152 | box-shadow: inset 1px 1px 3px rgba(230, 230, 230, 1); 153 | } 154 | 155 | #content .form .input input:focus, #content .form .input input:focus, #content .form .input select:focus { 156 | border: 1px solid rgba(137, 137, 137, 1); 157 | } 158 | 159 | #content .form .input input { 160 | min-width: 180px; 161 | } 162 | 163 | #content .form .input input[type="number"] { 164 | padding-left: 15px; 165 | padding-right: 0; 166 | min-width: 70px; 167 | width: 70px; 168 | } 169 | 170 | #content .form .input input[type="checkbox"], 171 | #content .form .input input[type="radio"] { 172 | border: none; 173 | box-shadow: none; 174 | min-width: inherit; 175 | vertical-align: middle; 176 | margin: 6px 0 8px 5px; 177 | cursor: pointer; 178 | } 179 | 180 | #content .form .input input[type="checkbox"]+label, 181 | #content .form .input input[type="radio"]+label { 182 | padding-left: 3px; 183 | margin-right: 15px; 184 | cursor: pointer; 185 | -webkit-user-select: none; 186 | -moz-user-select: none; 187 | -ms-user-select: none; 188 | user-select: none; 189 | } 190 | 191 | #content .form .input textarea { 192 | min-height: 150px; 193 | min-width: 400px; 194 | line-height: 18px !important; 195 | padding-top: 8px; 196 | padding-bottom: 8px; 197 | } 198 | 199 | #content .form .input input, 200 | #content .form .input textarea, 201 | #content .form .input.input-labeled.input-labeled-left > *:first-child, 202 | #content .form .input.input-labeled.input-labeled-right > *:last-child { 203 | line-height: 33px; 204 | } 205 | 206 | #content .form .input select { 207 | padding: 8px 10px 9px; 208 | min-width: 200px; 209 | } 210 | 211 | #content .form .input select option[value=""] { 212 | color: #ccc; 213 | } 214 | 215 | #content .form .input.input-labeled.input-labeled-left > *:first-child, 216 | #content .form .input.input-labeled.input-labeled-right > *:last-child { 217 | background: #eee; 218 | border: 1px solid rgba(200, 200, 200, 1); 219 | border-radius: 3px; 220 | display: inline-block; 221 | margin: 0 0 0 3px; 222 | padding: 0 10px; 223 | position: absolute; 224 | } 225 | 226 | #content .form .input.input-action > *:first-child, 227 | #content .form .input.input-labeled > *:first-child { 228 | border-top-right-radius: 0 !important; 229 | border-bottom-right-radius: 0 !important; 230 | margin-right: -4px; 231 | } 232 | #content .form .input.input-action > *:last-child, 233 | #content .form .input.input-labeled > *:last-child { 234 | border-top-left-radius: 0 !important; 235 | border-bottom-left-radius: 0 !important; 236 | } 237 | 238 | 239 | #content .buttons { 240 | margin: 15px 0; 241 | } 242 | 243 | #content .form .buttons { 244 | margin: 0; 245 | } 246 | 247 | #content .buttons.buttons-horizontal .button { 248 | display: block; 249 | margin-top: 10px; 250 | } 251 | 252 | #content .buttons.buttons-horizontal .button:first-child { 253 | margin-top: 0; 254 | } 255 | 256 | #content .button { 257 | background: #dddddd; 258 | background: linear-gradient(#ffffff, #eaeaea); 259 | border: 1px solid rgba(200, 200, 200, 1); 260 | border-radius: 3px; 261 | font-family: arial, serif; 262 | transition: all 0.2s; 263 | 264 | height: auto; 265 | min-width: 200px; 266 | width: 200px; 267 | line-height: 31px; 268 | font-size: 13px; 269 | text-align: center; 270 | color: rgba(57, 57, 57, 1); 271 | text-decoration: none; 272 | 273 | -webkit-user-select: none; 274 | -moz-user-select: none; 275 | -ms-user-select: none; 276 | user-select: none; 277 | } 278 | 279 | #content a.button { 280 | display: inline-block; 281 | } 282 | 283 | #content .button:hover { 284 | box-shadow: 1px 1px 4px #dbdbdb; 285 | text-decoration: none; 286 | cursor: pointer; 287 | } 288 | 289 | #content .button.button-large, 290 | #content .buttons.button-large .button { 291 | min-width: 300px; 292 | width: 300px; 293 | font-size: 15px; 294 | line-height: 45px; 295 | color: rgba(57, 57, 57, 1); 296 | } 297 | 298 | #content .button.button-primary, 299 | #content .buttons.button-primary .button { 300 | background: #666; 301 | background: linear-gradient(#999, #666); 302 | border-color: #444; 303 | color: #fff; 304 | } 305 | 306 | #content .button.button-primary:hover, 307 | #content .button.button-primary:active, 308 | #content .buttons.button-primary .button:hover, 309 | #content .buttons.button-primary .button:active { 310 | background: #777; 311 | background: linear-gradient(#777777, #444); 312 | border-color: #333; 313 | color: #fff; 314 | } 315 | 316 | #content .button.button-disabled { 317 | background: #f1f1f1; 318 | border-color: #f1f1f1; 319 | box-shadow: none; 320 | cursor: not-allowed; 321 | color: #bbb; 322 | } 323 | 324 | 325 | #content .table { 326 | margin: 25px 0; 327 | border-collapse: collapse; 328 | border: none; 329 | } 330 | 331 | #content .table-compact { 332 | margin: 5px 0; 333 | } 334 | 335 | #content .table thead th { 336 | line-height: 38px; 337 | padding: 2px 15px 0; 338 | border: 1px solid rgba(179, 176, 176, 1); 339 | background: #eeeeee; 340 | background: linear-gradient(#ffffff, #eaeaea); 341 | font-size: 13px; 342 | } 343 | 344 | #content .table tfoot th { 345 | line-height: 33px; 346 | text-align: left; 347 | padding: 0 10px; 348 | font-weight: normal; 349 | color: #999; 350 | } 351 | 352 | #content .table tbody td { 353 | line-height: 21px; 354 | padding: 9px 10px; 355 | border: 1px solid rgba(179, 176, 176, 1); 356 | } 357 | 358 | #content .table tbody > tr:hover { 359 | background-color: rgba(234, 234, 234, 1); 360 | } 361 | 362 | #content .table tbody > tr.warning { 363 | background-color: #fcf897; 364 | } 365 | 366 | #content .table a { 367 | color: rgb(148, 148, 255); 368 | } 369 | #content .table tbody > tr:hover a { 370 | color: blue; 371 | } 372 | 373 | 374 | #content .notifications { 375 | } 376 | 377 | #content .notification { 378 | height: auto; 379 | width: 100%; 380 | margin: 15px 0; 381 | text-align: center; 382 | border: 1px solid; 383 | border-radius: 3px; 384 | padding: 15px 10px; 385 | box-sizing: border-box; 386 | } 387 | 388 | #content .notification.notification-fail { 389 | background-color: #fcacac; 390 | border-color: red; 391 | } 392 | 393 | #content .notification.notification-warning { 394 | background-color: #fcf897; 395 | border-color: #ffe600; 396 | } 397 | 398 | #content .notification.notification-success { 399 | background-color: rgba(182, 255, 183, 1); 400 | border-color: green; 401 | } 402 | 403 | 404 | #footer { 405 | position: relative; 406 | width: 100%; 407 | background-color: white; 408 | padding: 15px 20px; 409 | box-sizing: border-box; 410 | color: grey; 411 | } 412 | 413 | #footer ul { 414 | list-style: none; 415 | } 416 | 417 | #footer li { 418 | display: inline-block; 419 | } 420 | 421 | #footer li:not(:last-child):after { 422 | content: '|'; 423 | color: #444444; 424 | padding: 0 6px; 425 | } 426 | 427 | #footer a { 428 | color: #888888; 429 | } 430 | 431 | #footer a:hover { 432 | color: #666666; 433 | } -------------------------------------------------------------------------------- /include/php/.htaccess: -------------------------------------------------------------------------------- 1 | Deny from all -------------------------------------------------------------------------------- /include/php/classes/Auth.php: -------------------------------------------------------------------------------- 1 | getPasswordHash())){ 106 | 107 | static::loginUserByModel($user); 108 | 109 | $_SESSION[static::SESSION_IDENTIFIER] = $user->getId(); 110 | 111 | return true; 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | 118 | 119 | /** 120 | * @return void 121 | */ 122 | public static function logout() 123 | { 124 | unset($_SESSION[static::SESSION_IDENTIFIER]); 125 | 126 | static::$loggedInUser = null; 127 | 128 | if(session_status() === PHP_SESSION_ACTIVE){ 129 | session_destroy(); 130 | } 131 | } 132 | 133 | 134 | /** 135 | * Check if current user has a certain role, but User::ROLE_ADMIN will have access to all 136 | * 137 | * @param string $requiredRole 138 | * 139 | * @return bool 140 | */ 141 | public static function hasPermission($requiredRole) 142 | { 143 | if(static::isLoggedIn()){ 144 | $user = static::getUser(); 145 | 146 | return $user->getRole() === $requiredRole 147 | || $user->getRole() === User::ROLE_ADMIN; 148 | } 149 | 150 | return false; 151 | } 152 | 153 | 154 | /** 155 | * Checks the new password entered by user on certain criteria, and throws an exception if its invalid. 156 | * 157 | * @param string $password 158 | * @param string $passwordRepeated 159 | * 160 | * @throws AuthException Codes explained below 161 | * 2: One password field is empty 162 | * 3: Passwords aren't equal 163 | * 4: Passwort is too snort 164 | */ 165 | public static function validateNewPassword($password, $passwordRepeated) 166 | { 167 | // Check if one passwort input is empty 168 | if(empty($password)){ 169 | throw new AuthException("First password field was'nt filled out.", 2); 170 | } 171 | if(empty($passwordRepeated)){ 172 | throw new AuthException("Repeat password field was'nt filled out.", 2); 173 | } 174 | 175 | // Check if password are equal 176 | if($password !== $passwordRepeated){ 177 | throw new AuthException("The repeated password must be equal to the first one.", 3); 178 | } 179 | 180 | // Check if password length is okay 181 | if(Config::has('password.min_length') 182 | && strlen($password) < Config::get('password.min_length') 183 | ){ 184 | throw new AuthException("Passwords must be at least ".Config::get('password.min_length')." characters long.", 4); 185 | } 186 | } 187 | 188 | 189 | /** 190 | * @param string $password 191 | * @param string $hash 192 | * 193 | * @return bool 194 | */ 195 | public static function checkPasswordByHash($password, $hash) 196 | { 197 | return crypt($password, $hash) === $hash; 198 | } 199 | 200 | 201 | /** 202 | * @return string 203 | */ 204 | private static function getPasswordSchemaPrefix() 205 | { 206 | $map = array( 207 | 'SHA-256' => '$5$rounds=5000$', 208 | 'BLOWFISH' => '$2a$09$', 209 | 'SHA-512' => '$6$rounds=5000$', 210 | ); 211 | 212 | $key = Config::get('password.hash_algorithm', 'SHA-512'); 213 | 214 | if(!isset($map[$key])){ 215 | $key = 'SHA-512'; 216 | } 217 | 218 | return $map[$key]; 219 | } 220 | 221 | 222 | /** 223 | * @param string $password 224 | * 225 | * @return string 226 | */ 227 | public static function generatePasswordHash($password) 228 | { 229 | if(function_exists('mt_rand')){ 230 | mt_srand(time()); 231 | $num = mt_rand(1, 100000); 232 | } 233 | else{ 234 | srand(time()); 235 | $num = rand(1, 100000); 236 | } 237 | 238 | $salt = base64_encode($num); 239 | $schemaPrefix = static::getPasswordSchemaPrefix(); 240 | 241 | $hash = crypt($password, $schemaPrefix.$salt.'$'); 242 | 243 | return $hash; 244 | } 245 | 246 | 247 | /** 248 | * @param string $userId 249 | * @param $password 250 | */ 251 | public static function changeUserPassword($userId, $password) 252 | { 253 | $passwordHash = static::generatePasswordHash($password); 254 | 255 | /** @var User $user */ 256 | $user = User::find($userId); 257 | 258 | if(!is_null($user)){ 259 | $user->setPasswordHash($passwordHash); 260 | $user->save(); 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /include/php/classes/AuthException.php: -------------------------------------------------------------------------------- 1 | 1){ 52 | $key = array_shift($keys); 53 | 54 | if(!isset($array[$key]) || !is_array($array[$key])){ 55 | $array[$key] = array(); 56 | } 57 | 58 | $array =& $array[$key]; 59 | } 60 | 61 | $array[array_shift($keys)] = $value; 62 | 63 | return $array; 64 | } 65 | 66 | 67 | /** 68 | * Get a config value using "dot" notation. 69 | * 70 | * @param string $key 71 | * @param mixed $default 72 | * @return mixed 73 | */ 74 | public static function get($key, $default = null) 75 | { 76 | if(is_null($key)) return static::$config; 77 | 78 | if(isset(static::$config[$key])) return static::$config[$key]; 79 | 80 | $pointer = static::$config; 81 | foreach(explode('.', $key) as $segment){ 82 | if(!is_array($pointer) || !array_key_exists($segment, $pointer)){ 83 | return $default; 84 | } 85 | 86 | $pointer = $pointer[$segment]; 87 | } 88 | 89 | return $pointer; 90 | } 91 | 92 | 93 | /** 94 | * Check if a config value exists using "dot" notation. 95 | * 96 | * @param string $key 97 | * @return bool 98 | */ 99 | public static function has($key) 100 | { 101 | if(empty(static::$config) || is_null($key)) return false; 102 | 103 | if(array_key_exists($key, static::$config)) return true; 104 | 105 | $pointer = static::$config; 106 | foreach(explode('.', $key) as $segment){ 107 | if(!is_array($pointer) || !array_key_exists($segment, $pointer)){ 108 | return false; 109 | } 110 | 111 | $pointer = $pointer[$segment]; 112 | } 113 | 114 | return true; 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /include/php/classes/DatabaseException.php: -------------------------------------------------------------------------------- 1 | query = $query; 18 | 19 | return $this; 20 | } 21 | 22 | /** 23 | * Get the executed SQL query 24 | * 25 | * @return string 26 | */ 27 | public function getQuery() 28 | { 29 | return $this->query; 30 | } 31 | } -------------------------------------------------------------------------------- /include/php/classes/Message.php: -------------------------------------------------------------------------------- 1 | messages[] = array( 70 | 'type' => $type, 71 | 'message' => $text, 72 | ); 73 | } 74 | 75 | 76 | /** 77 | * Add a new success message 78 | * 79 | * @param string $text 80 | */ 81 | public function fail($text) 82 | { 83 | $this->add(static::TYPE_FAIL, $text); 84 | } 85 | 86 | 87 | /** 88 | * Add a new success message 89 | * 90 | * @param string $text 91 | */ 92 | public function error($text) 93 | { 94 | $this->add(static::TYPE_ERROR, $text); 95 | } 96 | 97 | 98 | /** 99 | * Add a new success message 100 | * 101 | * @param string $text 102 | */ 103 | public function warning($text) 104 | { 105 | $this->add(static::TYPE_WARNING, $text); 106 | } 107 | 108 | 109 | /** 110 | * Add a new success message 111 | * 112 | * @param string $text 113 | */ 114 | public function success($text) 115 | { 116 | $this->add(static::TYPE_SUCCESS, $text); 117 | } 118 | 119 | 120 | /** 121 | * Render all messages 122 | * 123 | * @param null|string $type null = render all 124 | * 125 | * @return string 126 | */ 127 | public function render($type = null) 128 | { 129 | $out = ''; 130 | 131 | if(count($this->messages) > 0){ 132 | $out .= '
'; 133 | 134 | foreach($this->messages as $message){ 135 | if(is_null($type) || $type == $message['type']){ 136 | $out .= '
'.$message['message'].'
'; 137 | } 138 | } 139 | 140 | $out .= '
'; 141 | } 142 | 143 | return $out; 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /include/php/classes/Router.php: -------------------------------------------------------------------------------- 1 | 'include/php/template/error/not-found.php', 20 | 403 => 'include/php/template/error/not-allowed.php' 21 | ); 22 | 23 | 24 | /** 25 | * @codeCoverageIgnore 26 | */ 27 | private function __construct() 28 | { 29 | } 30 | 31 | 32 | /** 33 | * @codeCoverageIgnore 34 | */ 35 | private function __clone() 36 | { 37 | } 38 | 39 | 40 | /** 41 | * @param array $routes 42 | */ 43 | public static function init($routes) 44 | { 45 | static::$routes = $routes; 46 | } 47 | 48 | 49 | /** 50 | * @param string $method 51 | * 52 | * @return bool 53 | */ 54 | protected static function isValidMethod($method) 55 | { 56 | return in_array( 57 | $method, 58 | array( 59 | static::METHOD_GET, 60 | static::METHOD_POST 61 | ) 62 | ); 63 | } 64 | 65 | 66 | /** 67 | * @param string|array $methods 68 | * @param string $pattern 69 | * @param callable|array|string $routeConfig 70 | * @param array $permission 71 | * 72 | * @throws Exception 73 | */ 74 | public static function addRoute($methods, $pattern, $routeConfig, $permission = null) 75 | { 76 | if(!is_array($methods)){ 77 | $methods = array($methods); 78 | } 79 | 80 | $config = array( 81 | 'pattern' => $pattern, 82 | 'config' => $routeConfig, 83 | 'permission' => $permission, 84 | ); 85 | 86 | foreach($methods as $method){ 87 | $method = strtoupper($method); 88 | 89 | if(!static::isValidMethod($method)){ 90 | throw new Exception('Unsupported HTTP method "'.$method.'".'); 91 | } 92 | 93 | if(!isset(static::$routes[$method])){ 94 | static::$routes[$method] = array(); 95 | } 96 | 97 | static::$routes[$method][] = $config; 98 | } 99 | } 100 | 101 | 102 | /** 103 | * @param string $pattern 104 | * @param callable|string $routeConfig 105 | * @param array $permission 106 | */ 107 | public static function addGet($pattern, $routeConfig, $permission = null) 108 | { 109 | static::addRoute(static::METHOD_GET, $pattern, $routeConfig, $permission); 110 | } 111 | 112 | 113 | /** 114 | * @param string $pattern 115 | * @param callable|string $routeConfig 116 | * @param array $permission 117 | */ 118 | public static function addPost($pattern, $routeConfig, $permission = null) 119 | { 120 | static::addRoute(static::METHOD_POST, $pattern, $routeConfig, $permission); 121 | } 122 | 123 | 124 | /** 125 | * @param string $pattern 126 | * @param callable|string $routeConfig 127 | * @param array $permission 128 | */ 129 | public static function addMixed($pattern, $routeConfig, $permission = null) 130 | { 131 | static::addRoute(array(static::METHOD_GET, static::METHOD_POST), $pattern, $routeConfig, $permission); 132 | } 133 | 134 | 135 | /** 136 | * @param string $url 137 | * @param string $method 138 | * 139 | * @return string 140 | * 141 | * @throws Exception 142 | */ 143 | public static function execute($url, $method = self::METHOD_GET) 144 | { 145 | $method = strtoupper($method); 146 | 147 | if(!static::isValidMethod($method) && !isset(self::$routes[$method])){ 148 | throw new Exception('Unsupported HTTP method "'.$method.'".'); 149 | } 150 | 151 | if(isset(self::$routes[$method])){ 152 | foreach(self::$routes[$method] as $route){ 153 | if(rtrim($route['pattern'], '/') === rtrim($url, '/')){ 154 | if(!is_null($route['permission'])){ 155 | if(!Auth::isLoggedIn() || !Auth::hasPermission($route['permission'])){ 156 | return static::loadAndBufferOutput(static::$errorPages[403]); 157 | } 158 | } 159 | 160 | return static::resolveRouteConfig($route['config']); 161 | } 162 | } 163 | } 164 | 165 | return static::loadAndBufferOutput(static::$errorPages[404]); 166 | } 167 | 168 | /** 169 | * @return string 170 | */ 171 | public static function executeCurrentRequest() 172 | { 173 | return static::execute( 174 | static::getCurrentUrlPath(), 175 | isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : static::METHOD_GET 176 | ); 177 | } 178 | 179 | 180 | /** 181 | * @param int $errorNumber 182 | * 183 | * @return string|null 184 | * 185 | * @codeCoverageIgnore 186 | */ 187 | public static function displayError($errorNumber) 188 | { 189 | $errorPage = isset(static::$errorPages[$errorNumber]) 190 | ? static::loadAndBufferOutput(static::$errorPages[$errorNumber]) 191 | : ''; 192 | 193 | echo Router::loadAndBufferOutput( 194 | 'include/php/template/layout.php', 195 | array( 196 | 'content' => $errorPage, 197 | ) 198 | ); 199 | exit; 200 | } 201 | 202 | 203 | /** 204 | * @param bool $removeGetParameters 205 | * 206 | * @return string 207 | */ 208 | public static function getCurrentUrlPath($removeGetParameters = true) 209 | { 210 | $baseUrl = parse_url(Config::get('base_url')); 211 | $basePath = isset($baseUrl['path']) ? rtrim($baseUrl['path'], '/') : ''; 212 | 213 | $url = $_SERVER['REQUEST_URI']; 214 | 215 | if($removeGetParameters){ 216 | $url = preg_replace('/\?.*/', '', $url); // Trim GET Parameters 217 | } 218 | 219 | // Trim all leading slashes 220 | $url = rtrim($url, '/'); 221 | 222 | if(!empty($basePath) && ($basePathPos = strpos($url, $basePath)) === 0){ 223 | $url = substr($url, strlen($basePath)); 224 | } 225 | 226 | return $url; 227 | } 228 | 229 | 230 | /** 231 | * @param array $config 232 | * 233 | * @return string 234 | */ 235 | public static function resolveRouteConfig($config) 236 | { 237 | if(is_string($config)){ 238 | if(file_exists($config)){ 239 | return static::loadAndBufferOutput($config); 240 | } 241 | } 242 | elseif(is_callable($config) && $config instanceof Closure){ 243 | return $config(); 244 | } 245 | 246 | return static::loadAndBufferOutput(static::$errorPages[404]); 247 | } 248 | 249 | /** 250 | * @param string $file 251 | * @param array $variables 252 | * 253 | * @return string 254 | */ 255 | public static function loadAndBufferOutput($file, $variables = array()) 256 | { 257 | ob_start(); 258 | 259 | extract($variables); 260 | 261 | require $file; 262 | 263 | return ob_get_clean(); 264 | } 265 | 266 | 267 | /** 268 | * Generate full url 269 | * 270 | * @param string $url 271 | * 272 | * @return string 273 | */ 274 | public static function url($url = '') 275 | { 276 | return rtrim( 277 | sprintf( 278 | '%s/%s', 279 | rtrim(Config::get('base_url'), '/'), 280 | trim($url, '/') 281 | ), 282 | '/' 283 | ); 284 | } 285 | 286 | 287 | /** 288 | * Redirect user to an url 289 | * 290 | * @param string $url 291 | * 292 | * @codeCoverageIgnore 293 | */ 294 | public static function redirect($url) 295 | { 296 | header("Location: ".static::url($url)); 297 | exit; 298 | } 299 | } -------------------------------------------------------------------------------- /include/php/default.inc.php: -------------------------------------------------------------------------------- 1 | &$email){ 52 | if(empty($email)){ 53 | unset($list[$i]); 54 | } 55 | } 56 | 57 | $emails = array_values( 58 | array_unique( 59 | array_map( 60 | 'formatEmail', 61 | $list 62 | ) 63 | ) 64 | ); 65 | 66 | asort($emails); 67 | 68 | return $emails; 69 | } 70 | 71 | /** 72 | * List of emails to comma or $glue separated list string 73 | * 74 | * @param array $list 75 | * @param string $glue 76 | * 77 | * @return string 78 | */ 79 | function emailsToString($list, $glue = ',') 80 | { 81 | if(is_string($list)){ 82 | return $list; 83 | } 84 | 85 | return implode($glue, $list); 86 | } 87 | 88 | /** 89 | * Format single email address 90 | * 91 | * @param string $input 92 | * 93 | * @return string 94 | */ 95 | function formatEmail($input) 96 | { 97 | return strtolower(trim($input)); 98 | } 99 | 100 | /** 101 | * Format email addresses (single, multiple in separated list, or array of email addresses) 102 | * 103 | * @param string|array $input 104 | * @param string $glue 105 | * 106 | * @return string 107 | */ 108 | function formatEmails($input, $glue) 109 | { 110 | if(!is_array($input)){ 111 | $input = stringToEmails($input); 112 | } 113 | 114 | return emailsToString($input, $glue); 115 | } 116 | 117 | /** 118 | * Format email addresses for text output (not in an input field) 119 | * 120 | * @param string|array $input 121 | * @return string 122 | */ 123 | function formatEmailsText($input) 124 | { 125 | return formatEmails( 126 | $input, 127 | str_replace(PHP_EOL, '
', Config::get('frontend_options.email_separator_text', ', ')) 128 | ); 129 | } 130 | 131 | 132 | /** 133 | * Format email addresses for form output (in an input field) 134 | * 135 | * @param string|array $input 136 | * @return string 137 | */ 138 | function formatEmailsForm($input) 139 | { 140 | return strip_tags( 141 | formatEmails( 142 | $input, 143 | Config::get('frontend_options.email_separator_form', ',') 144 | ) 145 | ); 146 | } 147 | 148 | /** 149 | * @param string $textPattern 150 | * @param int|mixed $value 151 | * @return string 152 | */ 153 | function textValue($textPattern, $value) 154 | { 155 | $text = str_replace( 156 | array('_', ':val:', ':value:'), 157 | $value, 158 | $textPattern 159 | ); 160 | 161 | if(is_numeric($value) && $value > 1){ 162 | $text .= 's'; 163 | } 164 | 165 | return $text; 166 | } -------------------------------------------------------------------------------- /include/php/models/AbstractModel.php: -------------------------------------------------------------------------------- 1 | setId($id); 93 | } 94 | } 95 | 96 | 97 | /** 98 | * Create a model from data 99 | * 100 | * @param array $data 101 | * 102 | * @return static|null The Model 103 | */ 104 | public static function create($data) 105 | { 106 | if(count($data) > 0){ 107 | return new static($data); 108 | } 109 | 110 | return null; 111 | } 112 | 113 | 114 | /** 115 | * Create a model collection from data 116 | * 117 | * @param array $multiData 118 | * 119 | * @return ModelCollection|static[] 120 | */ 121 | public static function createMultiple($multiData = array()) 122 | { 123 | $collection = new ModelCollection(); 124 | 125 | foreach($multiData as $data){ 126 | $model = static::create($data); 127 | 128 | if(!is_null($model)){ 129 | if(is_null($model->getId())){ 130 | $collection->add($model); 131 | } 132 | else{ 133 | $collection->add($model, $model->getId()); 134 | } 135 | } 136 | } 137 | 138 | return $collection; 139 | } 140 | 141 | 142 | /** 143 | * @see create 144 | * 145 | * @param array $data 146 | * 147 | * @return AbstractModel|null 148 | */ 149 | public static function createAndSave($data) 150 | { 151 | $model = static::create($data); 152 | 153 | if(!is_null($model)){ 154 | $model->save(); 155 | 156 | return $model; 157 | } 158 | 159 | return null; 160 | } 161 | 162 | 163 | /** 164 | * @see createMultiple 165 | * 166 | * @param array $multiData 167 | * 168 | * @return ModelCollection|static[] 169 | */ 170 | public static function createMultipleAndSave($multiData = array()) 171 | { 172 | $collection = new ModelCollection(); 173 | 174 | foreach($multiData as $data){ 175 | $model = static::createAndSave($data); 176 | 177 | if(!is_null($model)){ 178 | $collection->add($model); 179 | } 180 | } 181 | 182 | return $collection; 183 | } 184 | 185 | 186 | /** 187 | * Create a model from mysqli result 188 | * 189 | * @param mysqli_result $result 190 | * 191 | * @return static|null 192 | */ 193 | public static function createFromDbResult($result) 194 | { 195 | if($result->num_rows === 0){ 196 | return null; 197 | } 198 | 199 | return static::create($result->fetch_assoc()); 200 | } 201 | 202 | 203 | /** 204 | * Create a model collection from mysqli result 205 | * 206 | * @param mysqli_result $result 207 | * 208 | * @return ModelCollection|static[] 209 | */ 210 | public static function createMultipleFromDbResult($result) 211 | { 212 | $rows = array(); 213 | 214 | while($row = $result->fetch_assoc()){ 215 | $rows[] = $row; 216 | } 217 | 218 | return static::createMultiple($rows); 219 | } 220 | 221 | 222 | /** 223 | * @param string $attribute 224 | * @param mixed $value 225 | */ 226 | public function setAttribute($attribute, $value) 227 | { 228 | $this->data[$attribute] = $value; 229 | } 230 | 231 | 232 | /** 233 | * @param string $attribute 234 | * 235 | * @return mixed|null 236 | */ 237 | public function getAttribute($attribute) 238 | { 239 | if(isset($this->data[$attribute])){ 240 | $value = $this->data[$attribute]; 241 | 242 | if(is_array($value)){ 243 | return array_map('strip_tags', $value); 244 | } 245 | elseif(is_string($value)){ 246 | return strip_tags($value); 247 | } 248 | 249 | return $value; 250 | } 251 | 252 | return null; 253 | } 254 | 255 | 256 | /** 257 | * @return mixed 258 | */ 259 | public function getId() 260 | { 261 | return $this->getAttribute('id'); 262 | } 263 | 264 | 265 | /** 266 | * @param mixed $value 267 | */ 268 | protected function setId($value) 269 | { 270 | $this->setAttribute('id', $value); 271 | } 272 | 273 | 274 | /** 275 | * Find all models by raw sql 276 | * 277 | * @param $sql 278 | * @param null|string $useSpecificModel 279 | * 280 | * @return ModelCollection|static[] 281 | */ 282 | public static function findAllRaw($sql, $useSpecificModel = null) 283 | { 284 | $result = Database::getInstance()->query($sql); 285 | 286 | if(is_null($useSpecificModel)){ 287 | return static::createMultipleFromDbResult($result); 288 | } 289 | elseif(class_exists($useSpecificModel)){ 290 | return call_user_func_array(array($useSpecificModel, 'createMultipleFromDbResult'), array($result)); 291 | } 292 | 293 | return new ModelCollection(); 294 | } 295 | 296 | 297 | /** 298 | * Find a model by raw sql 299 | * 300 | * @param $sql 301 | * @param null|string $useSpecificModel 302 | * 303 | * @return AbstractModel 304 | */ 305 | public static function findRaw($sql, $useSpecificModel = null) 306 | { 307 | $result = Database::getInstance()->query($sql); 308 | 309 | if(is_null($useSpecificModel)){ 310 | return static::createFromDbResult($result); 311 | } 312 | elseif(class_exists($useSpecificModel)){ 313 | return call_user_func_array(array($useSpecificModel, 'createFromDbResult'), array($result)); 314 | } 315 | 316 | return null; 317 | } 318 | 319 | 320 | /** 321 | * Find models by a condition 322 | * 323 | * @param array $conditions see helperConditionArray 324 | * @param string $conditionConnector see helperConditionArray 325 | * @param array|null $orderBy 326 | * @param int $limit see helperLimit 327 | * 328 | * @return ModelCollection|static[]|AbstractModel|null 329 | */ 330 | public static function findWhere($conditions = array(), $conditionConnector = 'AND', $orderBy = null, $limit = 0) 331 | { 332 | static::initModel(); 333 | 334 | $result = Database::getInstance()->select(static::$table, $conditions, $conditionConnector, $orderBy, $limit); 335 | 336 | if($limit === 1){ 337 | return static::createFromDbResult($result); 338 | } 339 | 340 | return static::createMultipleFromDbResult($result); 341 | } 342 | 343 | 344 | /** 345 | * Find all models 346 | * 347 | * @param array|null $orderBy see helperOrderBy 348 | * 349 | * @return ModelCollection|static[] 350 | */ 351 | public static function findAll($orderBy = null) 352 | { 353 | return static::findWhere(array(), 'AND', $orderBy); 354 | } 355 | 356 | 357 | /** 358 | * Find first model matching a condition 359 | * 360 | * @param array $conditions see helperConditionArray 361 | * @param string $conditionConnector see helperConditionArray 362 | * @param array|null $orderBy 363 | * 364 | * @return AbstractModel|null 365 | */ 366 | public static function findWhereFirst($conditions = array(), $conditionConnector = 'AND', $orderBy = null) 367 | { 368 | return static::findWhere($conditions, $conditionConnector, $orderBy, 1); 369 | } 370 | 371 | 372 | /** 373 | * Find a model by id 374 | * 375 | * @param mixed $id 376 | * 377 | * @return AbstractModel|null 378 | */ 379 | public static function find($id) 380 | { 381 | static::initModel(); 382 | 383 | return static::findWhereFirst(array(static::$idAttribute, $id)); 384 | } 385 | 386 | 387 | /** 388 | * Save model data to database 389 | */ 390 | public function save() 391 | { 392 | $data = $this->preSave($this->data); 393 | 394 | $values = array(); 395 | foreach(static::$attributeDbAttributeMapping as $attribute => $sqlAttribute){ 396 | if($sqlAttribute === static::$idAttribute){ 397 | continue; 398 | } 399 | 400 | $values[$sqlAttribute] = $data[$attribute]; 401 | } 402 | 403 | if(is_null($this->getId())){ 404 | $insertId = Database::getInstance()->insert(static::$table, $values); 405 | 406 | $this->setId(intval($insertId)); 407 | } 408 | else{ 409 | Database::getInstance()->update(static::$table, $values, array(static::$idAttribute, $this->getId())); 410 | } 411 | } 412 | 413 | /** 414 | * Delete model from database 415 | * 416 | * @return bool 417 | */ 418 | public function delete() 419 | { 420 | if(!is_null($this->getId())){ 421 | 422 | Database::getInstance()->delete(static::$table, static::$idAttribute, $this->getId()); 423 | 424 | return true; 425 | } 426 | 427 | return false; 428 | } 429 | 430 | 431 | /** 432 | * Count models by a condition 433 | * 434 | * @param array $conditions see helperConditionArray 435 | * @param string $conditionConnector see helperConditionArray 436 | * 437 | * @return int 438 | */ 439 | public static function countWhere($conditions = array(), $conditionConnector = 'AND') 440 | { 441 | static::initModel(); 442 | 443 | return Database::getInstance()->count(static::$table, static::$idAttribute, $conditions, $conditionConnector); 444 | } 445 | 446 | 447 | /** 448 | * Count all models 449 | * 450 | * @return int 451 | */ 452 | public static function count() 453 | { 454 | return static::countWhere(); 455 | } 456 | 457 | } 458 | -------------------------------------------------------------------------------- /include/php/models/AbstractMultiRedirect.php: -------------------------------------------------------------------------------- 1 | Config::get('schema.attributes.aliases.id', 'id'), 49 | 'source' => Config::get('schema.attributes.aliases.source', 'source'), 50 | 'destination' => Config::get('schema.attributes.aliases.destination', 'destination'), 51 | 'multi_hash' => Config::get('schema.attributes.aliases.multi_source', 'multi_source'), 52 | ); 53 | 54 | if(Config::get('options.enable_user_redirects', false)){ 55 | static::$attributeDbAttributeMapping['is_created_by_user'] = Config::get('schema.attributes.aliases.is_created_by_user', 'is_created_by_user'); 56 | } 57 | } 58 | } 59 | 60 | 61 | /** 62 | * @inheritdoc 63 | */ 64 | protected function preSave($data) 65 | { 66 | $data = parent::preSave($data); 67 | 68 | $data['source'] = emailsToString($data['source']); 69 | $data['destination'] = emailsToString($data['destination']); 70 | 71 | if(Config::get('options.enable_user_redirects', false)){ 72 | $data['is_created_by_user'] = $data['is_created_by_user'] ? 1 : 0; 73 | } 74 | 75 | return $data; 76 | } 77 | 78 | 79 | /** 80 | * @inheritdoc 81 | */ 82 | protected function __construct($data) 83 | { 84 | parent::__construct($data); 85 | 86 | $source = stringToEmails($data[static::attr('source')]); 87 | $destination = stringToEmails($data[static::attr('destination')]); 88 | 89 | if(get_called_class() === 'Alias' || get_called_class() === 'Redirect'){ 90 | $source = $source[0]; 91 | } 92 | 93 | if(get_called_class() === 'Alias' || get_called_class() === 'MultiAlias'){ 94 | $destination = $destination[0]; 95 | } 96 | 97 | $this->setSource($source); 98 | $this->setDestination($destination); 99 | 100 | if(Config::get('options.enable_multi_source_redirects', false)){ 101 | $this->setMultiHash($data[static::attr('multi_hash')]); 102 | } 103 | 104 | if(Config::get('options.enable_user_redirects', false)){ 105 | $this->setIsCreatedByUser($data[static::attr('is_created_by_user')]); 106 | } 107 | } 108 | 109 | 110 | /** 111 | * @inheritdoc 112 | */ 113 | public static function create($data) 114 | { 115 | if(get_called_class() !== 'AbstractRedirect'){ 116 | return parent::create($data); 117 | } 118 | 119 | $hasMultipleSources = array_key_exists(static::attr('source'), $data) 120 | && strpos($data[static::attr('source')], ',') !== false; 121 | 122 | $hasMultipleDestinations = array_key_exists(static::attr('destination'), $data) 123 | && strpos($data[static::attr('destination')], ',') !== false; 124 | 125 | if(Config::get('options.enable_multi_source_redirects', false) && $hasMultipleSources 126 | ){ 127 | if($hasMultipleDestinations){ 128 | return MultiRedirect::create($data); 129 | } 130 | else{ 131 | return MultiAlias::create($data); 132 | } 133 | } 134 | else{ 135 | if($hasMultipleDestinations){ 136 | return Redirect::create($data); 137 | } 138 | else{ 139 | return Alias::create($data); 140 | } 141 | } 142 | } 143 | 144 | 145 | /** 146 | * @return array|string 147 | */ 148 | public function getSource() 149 | { 150 | return $this->getAttribute('source'); 151 | } 152 | 153 | 154 | /** 155 | * @param string|array $value 156 | */ 157 | public function setSource($value) 158 | { 159 | if(is_array($value)){ 160 | $this->setAttribute('source', array_map('strtolower', $value)); 161 | } 162 | else{ 163 | $this->setAttribute('source', strtolower($value)); 164 | } 165 | } 166 | 167 | 168 | /** 169 | * @return array|string 170 | */ 171 | public function getDestination() 172 | { 173 | return $this->getAttribute('destination'); 174 | } 175 | 176 | 177 | /** 178 | * @param string|array $value 179 | */ 180 | public function setDestination($value) 181 | { 182 | if(is_array($value)){ 183 | $this->setAttribute('destination', array_map('strtolower', $value)); 184 | } 185 | else{ 186 | $this->setAttribute('destination', strtolower($value)); 187 | } 188 | } 189 | 190 | 191 | /** 192 | * @return string 193 | */ 194 | public function getMultiHash() 195 | { 196 | return $this->getAttribute('multi_hash'); 197 | } 198 | 199 | 200 | /** 201 | * @param string $value 202 | */ 203 | public function setMultiHash($value) 204 | { 205 | $this->setAttribute('multi_hash', $value); 206 | } 207 | 208 | 209 | /** 210 | * @return bool 211 | */ 212 | public function isCreatedByUser() 213 | { 214 | return $this->getAttribute('is_created_by_user'); 215 | } 216 | 217 | 218 | /**n 219 | * @param bool $value 220 | */ 221 | public function setIsCreatedByUser($value) 222 | { 223 | $this->setAttribute('is_created_by_user', $value ? true : false); 224 | } 225 | 226 | 227 | /** 228 | * @return array 229 | */ 230 | protected function getDomain() 231 | { 232 | $sources = $this->getSource(); 233 | if(is_string($sources)){ 234 | $sources = array($sources); 235 | } 236 | 237 | $domains = array(); 238 | foreach($sources as $source){ 239 | $emailParts = explode('@', $source); 240 | if(count($emailParts) === 2){ 241 | $domains[] = $emailParts[1]; 242 | } 243 | } 244 | 245 | return array_unique($domains); 246 | } 247 | 248 | 249 | /** 250 | * @return ModelCollection 251 | */ 252 | public function getConflictingUsers() 253 | { 254 | if(is_null($this->conflictingUsers)){ 255 | $sources = $this->getSource(); 256 | 257 | if(is_string($sources)){ 258 | $sources = array($sources); 259 | } 260 | 261 | $this->conflictingUsers = new ModelCollection(); 262 | foreach($sources as $source){ 263 | $user = User::findByEmail($source); 264 | if(!is_null($user)){ 265 | $this->conflictingUsers->add($user); 266 | } 267 | } 268 | } 269 | 270 | return $this->conflictingUsers; 271 | } 272 | 273 | 274 | /** 275 | * @param string $template 276 | * 277 | * @return array|string 278 | */ 279 | public function getConflictingMarkedSource($template = "%email%") 280 | { 281 | $conflictingUsers = $this->getConflictingUsers(); 282 | 283 | $sources = $this->getSource(); 284 | 285 | if(is_string($sources)){ 286 | $sources = array($sources); 287 | } 288 | 289 | foreach($conflictingUsers as $user){ 290 | if(($key = array_search($user->getEmail(), $sources)) !== false){ 291 | $sources[$key] = str_replace('%email%', $sources[$key], $template); 292 | } 293 | } 294 | 295 | return $sources; 296 | } 297 | 298 | 299 | /** 300 | * @inheritdoc 301 | */ 302 | public static function findAll($orderBy = null) 303 | { 304 | if(is_null($orderBy)){ 305 | $orderBy = array(static::attr('source')); 306 | } 307 | 308 | return parent::findAll($orderBy); 309 | } 310 | 311 | 312 | /** 313 | * @return string 314 | */ 315 | private static function generateRedirectBaseQuery() 316 | { 317 | if(Config::get('options.enable_multi_source_redirects', false)){ 318 | return "SELECT r.* FROM ( 319 | SELECT 320 | GROUP_CONCAT(g.`".static::$idAttribute."` ORDER BY g.`".static::$idAttribute."` SEPARATOR ',') AS `".static::$idAttribute."`, 321 | GROUP_CONCAT(g.`".static::attr('source')."` SEPARATOR ',') AS `".static::attr('source')."`, 322 | g.`".static::attr('destination')."`, 323 | g.`".static::attr('multi_hash')."` 324 | ".(Config::get('options.enable_user_redirects', false) ? ", g.`".static::attr('is_created_by_user')."`" : "")." 325 | FROM `".static::$table."` AS g 326 | WHERE g.`".static::attr('multi_hash')."` IS NOT NULL 327 | GROUP BY g.`".static::attr('multi_hash')."` 328 | UNION 329 | SELECT 330 | s.`".static::$idAttribute."`, 331 | s.`".static::attr('source')."`, 332 | s.`".static::attr('destination')."`, 333 | s.`".static::attr('multi_hash')."` 334 | ".(Config::get('options.enable_user_redirects', false) ? ", s.`".static::attr('is_created_by_user')."`" : "")." 335 | FROM `".static::$table."` AS s 336 | WHERE s.`".static::attr('multi_hash')."` IS NULL 337 | ) AS r"; 338 | } 339 | else{ 340 | return "SELECT * FROM `".static::$table."`"; 341 | } 342 | } 343 | 344 | 345 | public static function findMultiAll($orderBy = null) 346 | { 347 | static::initModel(); 348 | 349 | if(is_null($orderBy)){ 350 | $orderBy = array(static::attr('source')); 351 | } 352 | 353 | $sql = static::generateRedirectBaseQuery() 354 | .Database::helperOrderBy($orderBy); 355 | 356 | return static::findAllRaw($sql); 357 | } 358 | 359 | 360 | public static function findMultiWhere($conditions = array(), $conditionConnector = 'AND', $orderBy = null, $limit = 0) 361 | { 362 | $sql = static::generateRedirectBaseQuery() 363 | .Database::helperWhere($conditions, $conditionConnector) 364 | .Database::helperOrderBy($orderBy) 365 | .Database::helperLimit($limit); 366 | 367 | if($limit === 1){ 368 | return static::findRaw($sql); 369 | } 370 | 371 | return static::findAllRaw($sql); 372 | } 373 | 374 | 375 | public static function findMultiWhereFirst($conditions = array(), $conditionConnector = 'AND', $orderBy = null) 376 | { 377 | return static::findMultiWhere($conditions, $conditionConnector, $orderBy, 1); 378 | } 379 | 380 | 381 | public static function findMulti($id) 382 | { 383 | static::initModel(); 384 | 385 | return static::findMultiWhereFirst(array(static::$idAttribute, $id)); 386 | } 387 | 388 | 389 | /** 390 | * @param array|User|null $limitedBy 391 | * 392 | * @return ModelCollection|static[] 393 | */ 394 | public static function getMultiByLimitedDomains($limitedBy = null) 395 | { 396 | return static::filterModelCollectionByLimitedDomains(static::findMultiAll(), $limitedBy); 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /include/php/models/Alias.php: -------------------------------------------------------------------------------- 1 | Config::get('schema.attributes.domains.id', 'id'), 43 | 'domain' => Config::get('schema.attributes.domains.domain', 'domain'), 44 | ); 45 | } 46 | } 47 | 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | protected function __construct($data) 53 | { 54 | parent::__construct($data); 55 | 56 | $this->setDomain($data[static::attr('domain')]); 57 | } 58 | 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getDomain() 64 | { 65 | return $this->getAttribute('domain'); 66 | } 67 | 68 | 69 | /** 70 | * @param string $value 71 | */ 72 | public function setDomain($value) 73 | { 74 | $this->setAttribute('domain', strtolower($value)); 75 | } 76 | 77 | 78 | /** 79 | * @return int 80 | */ 81 | public function countUsers() 82 | { 83 | return User::countWhere( 84 | array(User::attr('domain'), $this->getDomain()) 85 | ); 86 | } 87 | 88 | 89 | /** 90 | * @return int 91 | */ 92 | public function countRedirects() 93 | { 94 | return AbstractRedirect::countWhere( 95 | array( 96 | array(AbstractRedirect::attr('source'), 'LIKE', "%@{$this->getDomain()}"), 97 | array(AbstractRedirect::attr('destination'), 'LIKE', "%@{$this->getDomain()}") 98 | ), 99 | 'OR' 100 | ); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /include/php/models/DomainLimitTrait.php: -------------------------------------------------------------------------------- 1 | isDomainLimited() || static::isInLimitedDomains($limitedBy->getDomainLimits()); 21 | } 22 | 23 | if(!is_array($limitedBy)){ 24 | throw new InvalidArgumentException; 25 | } 26 | 27 | /** @var string|array|string[] $domain */ 28 | $domain = $this->getDomain(); 29 | 30 | if(is_string($domain)) { 31 | return in_array($domain, $limitedBy); 32 | } 33 | 34 | foreach($domain as $d){ 35 | if(!in_array($d, $limitedBy)) { 36 | return false; 37 | } 38 | } 39 | 40 | return true; 41 | } 42 | 43 | 44 | /** 45 | * @param ModelCollection|static[] $collection 46 | * @param array|User|null $limitedBy 47 | * 48 | * @return ModelCollection|static[] 49 | */ 50 | protected static function filterModelCollectionByLimitedDomains($collection, $limitedBy = null) 51 | { 52 | return $collection->searchAll(function($model) use ($limitedBy){ 53 | /** @var static $model */ 54 | //var_dump($model->isInLimitedDomains($limitedBy), $model->getDomain()); 55 | return $model->isInLimitedDomains($limitedBy); 56 | }); 57 | } 58 | 59 | 60 | /** 61 | * @param array|User|null $limitedBy 62 | * 63 | * @return ModelCollection|static[] 64 | */ 65 | public static function getByLimitedDomains($limitedBy = null) 66 | { 67 | return static::filterModelCollectionByLimitedDomains(static::findAll(), $limitedBy); 68 | } 69 | } -------------------------------------------------------------------------------- /include/php/models/ModelCollection.php: -------------------------------------------------------------------------------- 1 | isNumericArray($array)){ 20 | foreach($array as $model){ 21 | $this->add($model); 22 | } 23 | } 24 | else{ 25 | foreach($array as $key => $model){ 26 | $this->add($model, $key); 27 | } 28 | } 29 | } 30 | 31 | 32 | /** 33 | * @param array $array 34 | * 35 | * @return bool 36 | */ 37 | protected function isNumericArray($array) 38 | { 39 | return array_keys($array) === range(0, count($array) - 1) 40 | && count(array_filter($array, 'is_string')) === 0; 41 | } 42 | 43 | 44 | /** 45 | * Adds a model to the collection, 46 | * but won't replace if it exists with that key 47 | * 48 | * @param AbstractModel $model 49 | * @param mixed|null $key 50 | */ 51 | public function add($model, $key = null) 52 | { 53 | if(is_null($model) || !($model instanceof AbstractModel)){ 54 | return; 55 | } 56 | 57 | if(is_null($key)){ 58 | $this->models[] = $model; 59 | } 60 | elseif(!$this->has($key)){ 61 | $this->models[$key] = $model; 62 | } 63 | } 64 | 65 | 66 | /** 67 | * Replace a model with given key 68 | * 69 | * @param AbstractModel $model 70 | * @param mixed $key 71 | */ 72 | public function replace($model, $key) 73 | { 74 | if(is_null($model) || !($model instanceof AbstractModel)){ 75 | return; 76 | } 77 | 78 | $model[$key] = $model; 79 | } 80 | 81 | 82 | /** 83 | * Delete a model by key 84 | * 85 | * @param mixed $key 86 | */ 87 | public function delete($key) 88 | { 89 | if($this->has($key)){ 90 | unset($this->models[$key]); 91 | } 92 | } 93 | 94 | 95 | /** 96 | * Check if collection has a model by key 97 | * 98 | * @param mixed $key 99 | * 100 | * @return bool 101 | */ 102 | public function has($key) 103 | { 104 | return isset($this->models[$key]); 105 | } 106 | 107 | 108 | /** 109 | * Get a model from the collection by key 110 | * 111 | * @param mixed $key 112 | * 113 | * @return AbstractModel|null 114 | */ 115 | public function get($key) 116 | { 117 | if($this->has($key)){ 118 | return $this->models[$key]; 119 | } 120 | 121 | return null; 122 | } 123 | 124 | 125 | /** 126 | * Search a model in collection with a condition 127 | * 128 | * @param callable $callable Gives back if the search matches 129 | * 130 | * @return AbstractModel|null 131 | */ 132 | public function search($callable) 133 | { 134 | if(is_callable($callable)){ 135 | foreach($this->models as $model){ 136 | if($callable($model)){ 137 | return $model; 138 | } 139 | } 140 | } 141 | 142 | return null; 143 | } 144 | 145 | 146 | /** 147 | * Search all models in collection with a condition 148 | * 149 | * @param callable $callable Gives back if the search matches 150 | * 151 | * @return static 152 | */ 153 | public function searchAll($callable) 154 | { 155 | $collection = new static; 156 | 157 | if(is_callable($callable)){ 158 | foreach($this->models as $model){ 159 | if($callable($model)){ 160 | $collection->add($model); 161 | } 162 | } 163 | } 164 | 165 | return $collection; 166 | } 167 | 168 | 169 | /** 170 | * Convert models to an array of strings 171 | * 172 | * @param callable $callable Gives back a string for a model 173 | * 174 | * @return array|string[] 175 | */ 176 | public function toStringArray($callable) 177 | { 178 | $strings = array(); 179 | 180 | if(is_callable($callable)){ 181 | foreach($this->models as $model){ 182 | $strings[] = $callable($model); 183 | } 184 | } 185 | 186 | return $strings; 187 | } 188 | 189 | 190 | /** 191 | * @inheritdoc 192 | */ 193 | public function current() 194 | { 195 | return current($this->models); 196 | } 197 | 198 | 199 | /** 200 | * @inheritdoc 201 | */ 202 | public function next() 203 | { 204 | return next($this->models); 205 | } 206 | 207 | 208 | /** 209 | * @inheritdoc 210 | */ 211 | public function key() 212 | { 213 | return key($this->models); 214 | } 215 | 216 | 217 | /** 218 | * @inheritdoc 219 | */ 220 | public function valid() 221 | { 222 | return $this->current() !== false; 223 | } 224 | 225 | 226 | /** 227 | * @inheritdoc 228 | */ 229 | public function rewind() 230 | { 231 | reset($this->models); 232 | } 233 | 234 | 235 | /** 236 | * @inheritdoc 237 | */ 238 | public function offsetExists($offset) 239 | { 240 | return $this->has($offset); 241 | } 242 | 243 | 244 | /** 245 | * @inheritdoc 246 | */ 247 | public function offsetGet($offset) 248 | { 249 | return $this->get($offset); 250 | } 251 | 252 | 253 | /** 254 | * @inheritdoc 255 | */ 256 | public function offsetSet($offset, $value) 257 | { 258 | $this->add($value, $offset); 259 | } 260 | 261 | 262 | /** 263 | * @inheritdoc 264 | */ 265 | public function offsetUnset($offset) 266 | { 267 | $this->delete($offset); 268 | } 269 | 270 | 271 | /** 272 | * @inheritdoc 273 | */ 274 | public function count() 275 | { 276 | return count($this->models); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /include/php/models/MultiAlias.php: -------------------------------------------------------------------------------- 1 | Config::get('schema.attributes.users.id', 'id'), 59 | 'username' => Config::get('schema.attributes.users.username', 'username'), 60 | 'domain' => Config::get('schema.attributes.users.domain', 'domain'), 61 | 'password_hash' => Config::get('schema.attributes.users.password', 'password'), 62 | ); 63 | 64 | if(Config::get('options.enable_mailbox_limits', false)){ 65 | static::$attributeDbAttributeMapping['mailbox_limit'] = Config::get('schema.attributes.users.mailbox_limit'); 66 | } 67 | 68 | if(Config::get('options.enable_user_redirects', false)){ 69 | static::$attributeDbAttributeMapping['max_user_redirects'] = Config::get('schema.attributes.users.max_user_redirects'); 70 | } 71 | } 72 | } 73 | 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | protected function __construct($data) 79 | { 80 | parent::__construct($data); 81 | 82 | $this->setUsername($data[static::attr('username')]); 83 | $this->setDomain($data[static::attr('domain')]); 84 | $this->setPasswordHash($data[static::attr('password_hash')]); 85 | 86 | if(Config::get('options.enable_mailbox_limits', false)){ 87 | $this->setMailboxLimit($data[static::attr('mailbox_limit')]); 88 | } 89 | 90 | if(Config::get('options.enable_user_redirects', false)){ 91 | $this->setMaxUserRedirects($data[static::attr('max_user_redirects')]); 92 | } 93 | 94 | $this->setAttribute('role', static::getRoleByEmail($this->getEmail())); 95 | } 96 | 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function getUsername() 102 | { 103 | return $this->getAttribute('username'); 104 | } 105 | 106 | 107 | /** 108 | * @param string $value 109 | */ 110 | public function setUsername($value) 111 | { 112 | $this->setAttribute('username', strtolower($value)); 113 | } 114 | 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function getDomain() 120 | { 121 | return $this->getAttribute('domain'); 122 | } 123 | 124 | 125 | /** 126 | * @param string $value 127 | */ 128 | public function setDomain($value) 129 | { 130 | $this->setAttribute('domain', strtolower($value)); 131 | } 132 | 133 | 134 | /** 135 | * @return string 136 | */ 137 | public function getEmail() 138 | { 139 | return $this->getUsername().'@'.$this->getDomain(); 140 | } 141 | 142 | 143 | /** 144 | * @return string 145 | */ 146 | public function getPasswordHash() 147 | { 148 | return $this->getAttribute('password_hash'); 149 | } 150 | 151 | 152 | /** 153 | * @param string $value 154 | */ 155 | public function setPasswordHash($value) 156 | { 157 | $this->setAttribute('password_hash', $value); 158 | } 159 | 160 | 161 | /** 162 | * @return int 163 | */ 164 | public function getMailboxLimit() 165 | { 166 | return $this->getAttribute('mailbox_limit'); 167 | } 168 | 169 | 170 | /** 171 | * @param int $value 172 | */ 173 | public function setMailboxLimit($value) 174 | { 175 | $this->setAttribute('mailbox_limit', intval($value)); 176 | } 177 | 178 | 179 | /** 180 | * @return int 181 | */ 182 | public function getMaxUserRedirects() 183 | { 184 | return $this->getAttribute('max_user_redirects'); 185 | } 186 | 187 | 188 | /** 189 | * @param int $value 190 | */ 191 | public function setMaxUserRedirects($value) 192 | { 193 | $this->setAttribute('max_user_redirects', intval($value)); 194 | } 195 | 196 | 197 | /** 198 | * @param string $attr 199 | * @param mixed $default 200 | * 201 | * @return mixed 202 | * 203 | * @throws Exception 204 | */ 205 | protected static function getAttributeDefaultValue($attr, $default) 206 | { 207 | static::initModel(); 208 | 209 | $sql = "SELECT DEFAULT(".static::attr($attr).") FROM `".static::$table."` LIMIT 1"; 210 | 211 | try { 212 | $result = Database::getInstance()->query($sql); 213 | 214 | if($result->num_rows === 1){ 215 | $row = $result->fetch_array(); 216 | 217 | return $row[0]; 218 | } 219 | } 220 | catch(Exception $e) { 221 | if (strpos($e->getMessage(), 'doesn\'t have a default') !== false) { 222 | throw new Exception('Database table "'.static::$table.'" is missing a default value for attribute "'.static::attr($attr).'".'); 223 | } 224 | 225 | return $default; 226 | } 227 | 228 | return $default; 229 | } 230 | 231 | 232 | /** 233 | * Get mailbox limit default via database default value 234 | * 235 | * @return int 236 | */ 237 | public static function getMailboxLimitDefault() 238 | { 239 | if(Config::get('options.enable_mailbox_limits', false)){ 240 | return intval(static::getAttributeDefaultValue('mailbox_limit', 0)); 241 | } 242 | 243 | return 0; 244 | } 245 | 246 | 247 | /** 248 | * Get max user redirects default via database default value 249 | * 250 | * @return int 251 | */ 252 | public static function getMaxUserRedirectsDefault() 253 | { 254 | if(Config::get('options.enable_user_redirects', false)){ 255 | return intval(static::getAttributeDefaultValue('max_user_redirects', 0)); 256 | } 257 | 258 | return 0; 259 | } 260 | 261 | 262 | /** 263 | * @return string 264 | */ 265 | public function getRole() 266 | { 267 | return $this->getAttribute('role'); 268 | } 269 | 270 | 271 | /** 272 | * @param string $email 273 | * 274 | * @return string 275 | */ 276 | private static function getRoleByEmail($email) 277 | { 278 | if(in_array($email, Config::get('admins', array()))){ 279 | return static::ROLE_ADMIN; 280 | } 281 | 282 | return static::ROLE_USER; 283 | } 284 | 285 | 286 | /** 287 | * Is user limited by domain limits? 288 | * 289 | * @return bool 290 | */ 291 | public function isDomainLimited() 292 | { 293 | $adminDomainLimits = Config::get('admin_domain_limits', array()); 294 | 295 | return Config::get('options.enable_admin_domain_limits', false) 296 | && is_array($adminDomainLimits) && isset($adminDomainLimits[$this->getEmail()]); 297 | } 298 | 299 | 300 | /** 301 | * Get domain limits, returns an empty array if user has no limits or ADMIN_DOMAIN_LIMITS_ENABLED is disabled 302 | * 303 | * @return array 304 | */ 305 | public function getDomainLimits() 306 | { 307 | if($this->isDomainLimited()){ 308 | $adminDomainLimits = Config::get('admin_domain_limits', array()); 309 | 310 | if(!is_array($adminDomainLimits[$this->getEmail()])){ 311 | throw new InvalidArgumentException('Config value of admin domain limits for email "'.$this->getEmail().'" needs to be of type array.'); 312 | } 313 | 314 | return $adminDomainLimits[$this->getEmail()]; 315 | } 316 | 317 | return array(); 318 | } 319 | 320 | 321 | /** 322 | * @return bool 323 | */ 324 | public function isAllowedToCreateUserRedirects() 325 | { 326 | return $this->getMaxUserRedirects() >= 0; 327 | } 328 | 329 | 330 | /** 331 | * @return bool 332 | */ 333 | public function canCreateUserRedirects() 334 | { 335 | if(!$this->isAllowedToCreateUserRedirects() 336 | || ( 337 | $this->getMaxUserRedirects() > 0 338 | && $this->getSelfCreatedRedirects()->count() >= $this->getMaxUserRedirects() 339 | ) 340 | ){ 341 | return false; 342 | } 343 | 344 | return true; 345 | } 346 | 347 | 348 | /** 349 | * @return AbstractRedirect 350 | */ 351 | public function getConflictingRedirect() 352 | { 353 | if(is_null($this->conflictingRedirect)){ 354 | $this->conflictingRedirect = AbstractRedirect::findWhereFirst( 355 | array(AbstractRedirect::attr('source'), $this->getEmail()) 356 | ); 357 | } 358 | 359 | return $this->conflictingRedirect; 360 | } 361 | 362 | 363 | /** 364 | * @return ModelCollection|AbstractRedirect[] 365 | */ 366 | public function getRedirects() 367 | { 368 | if(is_null($this->redirects)){ 369 | $this->redirects = AbstractRedirect::findMultiWhere( 370 | array(AbstractRedirect::attr('destination'), 'LIKE', '%'.$this->getEmail().'%') 371 | ); 372 | } 373 | 374 | return $this->redirects; 375 | } 376 | 377 | 378 | /** 379 | * @return ModelCollection|AbstractRedirect[] 380 | */ 381 | public function getAnonymizedRedirects() 382 | { 383 | $redirects = $this->getRedirects(); 384 | 385 | foreach($redirects as $redirect){ 386 | $emails = $redirect->getDestination(); 387 | 388 | if(is_array($emails) && count($emails) > 1){ 389 | $redirect->setDestination(array($this->getEmail(), '…')); 390 | } 391 | } 392 | 393 | return $redirects; 394 | } 395 | 396 | 397 | /** 398 | * @return ModelCollection|AbstractRedirect[] 399 | */ 400 | public function getSelfCreatedRedirects() 401 | { 402 | $redirects = $this->getRedirects(); 403 | 404 | return $redirects->searchAll( 405 | function($redirect) { 406 | /** @var AbstractRedirect $redirect */ 407 | return $redirect->isCreatedByUser(); 408 | } 409 | ); 410 | } 411 | 412 | 413 | /** 414 | * Change this users password, throws Exception if password is invalid. 415 | * 416 | * @param string $password 417 | * @param string $passwordRepeated 418 | * 419 | * @throws AuthException 420 | */ 421 | public function changePassword($password, $passwordRepeated) 422 | { 423 | Auth::validateNewPassword($password, $passwordRepeated); 424 | 425 | $passwordHash = Auth::generatePasswordHash($password); 426 | 427 | $this->setPasswordHash($passwordHash); 428 | $this->save(); 429 | } 430 | 431 | 432 | /** 433 | * @inheritdoc 434 | */ 435 | public static function findAll($orderBy = null) 436 | { 437 | if(is_null($orderBy)){ 438 | $orderBy = array(static::attr('domain'), static::attr('username')); 439 | } 440 | 441 | return parent::findAll($orderBy); 442 | } 443 | 444 | 445 | /** 446 | * @param string $email 447 | * 448 | * @return User|null 449 | */ 450 | public static function findByEmail($email) 451 | { 452 | $emailInParts = explode("@", $email); 453 | if(count($emailInParts) !== 2){ 454 | return null; 455 | } 456 | $username = $emailInParts[0]; 457 | $domain = $emailInParts[1]; 458 | 459 | return static::findWhereFirst( 460 | array( 461 | array(static::attr('username'), $username), 462 | array(static::attr('domain'), $domain) 463 | ) 464 | ); 465 | } 466 | 467 | } 468 | -------------------------------------------------------------------------------- /include/php/pages/admin/createdomain.php: -------------------------------------------------------------------------------- 1 | isDomainLimited()){ 4 | Router::displayError(403); 5 | } 6 | 7 | if(isset($_POST['domain'])){ 8 | $inputDomain = $_POST['domain']; 9 | 10 | if(!empty($inputDomain)){ 11 | 12 | $existingDomain = Domain::findWhere(array(Domain::attr('domain'), $inputDomain)); 13 | 14 | if(!is_null($existingDomain)){ 15 | 16 | Domain::createAndSave( 17 | array( 18 | Domain::attr('domain') => $inputDomain, 19 | ) 20 | ); 21 | 22 | // Created domain successfull, redirect to overview 23 | Router::redirect("admin/listdomains/?created=1"); 24 | } 25 | else{ 26 | Message::getInstance()->fail("Domain already exists in database."); 27 | } 28 | } 29 | else{ 30 | Message::getInstance()->fail("Empty domain couldn't be created."); 31 | } 32 | } 33 | 34 | ?> 35 | 36 |

Create new domain

37 | 38 | render(); ?> 39 | 40 |
41 | ❬ Back to domain list 42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 | 54 |
55 |
-------------------------------------------------------------------------------- /include/php/pages/admin/deletedomain.php: -------------------------------------------------------------------------------- 1 | isDomainLimited()){ 4 | Router::displayError(403); 5 | } 6 | 7 | if(!isset($_GET['id'])){ 8 | // Domain id not set, redirect to overview 9 | Router::redirect("admin/listdomains"); 10 | } 11 | 12 | $id = $_GET['id']; 13 | 14 | /** @var Domain $domain */ 15 | $domain = Domain::find($id); 16 | 17 | if(is_null($domain)){ 18 | // Domain doesn't exist, redirect to overview 19 | Router::redirect("admin/listdomains"); 20 | } 21 | 22 | if(!$domain->isInLimitedDomains()){ 23 | Router::redirect("admin/listdomains/?missing-permission=1"); 24 | } 25 | 26 | // Delete domain 27 | if(isset($_POST['confirm'])){ 28 | $confirm = $_POST['confirm']; 29 | 30 | if($confirm === "yes"){ 31 | 32 | // Check if admin domain is affected 33 | $isAdminDomain = false; 34 | foreach(Config::get('admins', array()) as $admin){ 35 | $parts = explode("@", $admin); 36 | if(count($parts) === 2 && $parts[2] === $domain->getDomain()){ 37 | $isAdminDomain = true; 38 | break; 39 | } 40 | } 41 | 42 | if(!$isAdminDomain){ 43 | 44 | $users = User::findWhere(array(User::attr('domain'), $domain->getDomain())); 45 | 46 | /** @var User $user */ 47 | foreach($users as $user){ 48 | $user->delete(); 49 | } 50 | 51 | $domain->delete(); 52 | 53 | // Delete domain successfull, redirect to overview 54 | Router::redirect("admin/listdomains/?deleted=1"); 55 | } 56 | else{ 57 | // Cannot delete domain with admin emails, redirect to overview 58 | Router::redirect("admin/listdomains/?adm_del=1"); 59 | } 60 | } 61 | 62 | else{ 63 | // Choose to not delete domain, redirect to overview 64 | Router::redirect("admin/listdomains"); 65 | } 66 | } 67 | ?> 68 | 69 |

Delete domain "getDomain() ?>"?

70 | 71 |
72 | ❬ Back to domain list 73 |
74 | 75 |
76 |
77 | 78 |
Mailbox directories in the filesystem won't be affected.
79 |
80 | 81 |
82 | 83 |
84 | 88 |
89 |
90 | 91 |
92 | 93 |
94 |
-------------------------------------------------------------------------------- /include/php/pages/admin/deleteredirect.php: -------------------------------------------------------------------------------- 1 | isInLimitedDomains()){ 19 | Router::redirect("admin/listredirects/?missing-permission=1"); 20 | } 21 | 22 | if(isset($_POST['confirm'])){ 23 | $confirm = $_POST['confirm']; 24 | 25 | if($confirm === "yes"){ 26 | 27 | if ($redirect instanceof AbstractMultiRedirect){ 28 | 29 | // Get single source rows of multi source redirect/alias instead 30 | $hash = $redirect->getMultiHash(); 31 | $singleRedirects = AbstractRedirect::findWhere(array(AbstractRedirect::attr('multi_hash'), $hash)); 32 | 33 | /** @var AbstractRedirect $redirectToDelete */ 34 | foreach($singleRedirects as $redirectToDelete){ 35 | $redirectToDelete->delete(); 36 | } 37 | } 38 | else { 39 | $redirect->delete(); 40 | } 41 | 42 | // Delete redirect successfull, redirect to overview 43 | Router::redirect("admin/listredirects/?deleted=1"); 44 | } 45 | else{ 46 | // Choose to not delete redirect, redirect to overview 47 | Router::redirect("admin/listredirects"); 48 | } 49 | } 50 | 51 | else{ 52 | ?> 53 | 54 |

Delete redirection?

55 | 56 |
57 | ❬ Back to redirect list 58 |
59 | 60 |
61 |
62 | 63 |
getSource()); ?>
64 |
65 | 66 |
67 | 68 |
getDestination()); ?>
69 |
70 | 71 |
72 | 73 |
74 | 78 |
79 |
80 | 81 |
82 | 83 |
84 |
85 | -------------------------------------------------------------------------------- /include/php/pages/admin/deleteuser.php: -------------------------------------------------------------------------------- 1 | isInLimitedDomains()){ 19 | Router::redirect('admin/listusers/?missing-permission=1'); 20 | } 21 | 22 | // Delete user 23 | if(isset($_POST['confirm'])){ 24 | $confirm = $_POST['confirm']; 25 | 26 | if($confirm === 'yes'){ 27 | // Check if admin is affected 28 | if(!in_array($user->getEmail(), Config::get('admins', array()))){ 29 | 30 | // Delete redirects of this user 31 | if(isset($_POST['delete_redirects']) && $_POST['delete_redirects'] === 'yes' 32 | && isset($_POST['selected_redirects']) && is_array($_POST['selected_redirects']) 33 | ){ 34 | $redirectMultiIds = $_POST['selected_redirects']; 35 | 36 | foreach($redirectMultiIds as $redirectMultiId){ 37 | $redirectIds = explode(',', $redirectMultiId); 38 | 39 | foreach($redirectIds as $redirectId){ 40 | 41 | // Note: No Multi* selected, so there is only Alias & Redirect 42 | $redirects = AbstractRedirect::findWhere( 43 | array( 44 | array(AbstractRedirect::attr('id'), $redirectId), 45 | array(AbstractRedirect::attr('destination'), 'LIKE', '%'.$user->getEmail().'%') 46 | ) 47 | ); 48 | 49 | /** @var AbstractRedirect $redirect */ 50 | foreach($redirects as $redirect){ 51 | if($redirect instanceof Alias) { 52 | $redirect->delete(); 53 | } 54 | elseif($redirect instanceof Redirect) { 55 | $redirect->setDestination( 56 | array_diff( 57 | $redirect->getDestination(), 58 | array($user->getEmail()) 59 | ) 60 | ); 61 | $redirect->save(); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | $user->delete(); 69 | 70 | // Delete user successful, redirect to overview 71 | Router::redirect('admin/listusers/?deleted=1'); 72 | } 73 | else{ 74 | // Admin tried to delete himself, redirect to overview 75 | Router::redirect('admin/listusers/?adm_del=1'); 76 | } 77 | } 78 | else{ 79 | // Choose to not delete user, redirect to overview 80 | Router::redirect('admin/listusers'); 81 | } 82 | } 83 | 84 | $redirects = $user->getAnonymizedRedirects(); 85 | 86 | ?> 87 | 88 |

Delete user "getEmail() ?>"?

89 | 90 |
91 | ❬ Back to user list 92 |
93 | 94 |
95 |
96 | 97 |
The mailbox in the filesystem won't be affected.
98 |
99 | 100 |
101 | 102 | count() > 0): ?> 103 |
Do you also want to delete the following redirects to this user?
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
SourceDestination
getSource()); ?>getDestination()); ?>
122 |
123 | 129 |
130 | 131 |
There are currently no redirects to this user.
132 | 133 |
134 | 135 |
136 | 137 |
138 | 144 |
145 |
146 | 147 |
148 | 149 |
150 |
-------------------------------------------------------------------------------- /include/php/pages/admin/editredirect.php: -------------------------------------------------------------------------------- 1 | isInLimitedDomains()){ 18 | Router::redirect("admin/listredirects/?missing-permission=1"); 19 | } 20 | } 21 | 22 | if(isset($_POST['savemode'])){ 23 | $savemode = $_POST['savemode']; 24 | 25 | $inputSources = stringToEmails($_POST['source']); 26 | $inputDestinations = stringToEmails($_POST['destination']); 27 | 28 | // validate emails 29 | $emailErrors = array(); 30 | 31 | // basic email validation isn't working 100% correct though 32 | foreach(array_merge($inputSources, $inputDestinations) as $email){ 33 | if(strpos($email, '@') === false){ 34 | $emailErrors[$email] = "Address \"{$email}\" isn't a valid email address."; 35 | } 36 | } 37 | 38 | // validate source emails are on domains 39 | if(Config::get('options.enable_validate_aliases_source_domain', true)){ 40 | $domains = Domain::getByLimitedDomains(); 41 | 42 | foreach($inputSources as $email){ 43 | if(isset($emailErrors[$email])){ 44 | continue; 45 | } 46 | 47 | $emailParts = explode('@', $email); 48 | $searchResult = $domains->search( 49 | function($domain) use ($emailParts){ 50 | /** @var Domain $domain */ 51 | return $domain->getDomain() === $emailParts[1]; 52 | } 53 | ); 54 | 55 | if(is_null($searchResult)){ 56 | $emailErrors[$email] = "Domain of source address \"{$email}\" not in your domains."; 57 | } 58 | } 59 | } 60 | 61 | // validate no redirect loops 62 | foreach(array_intersect($inputSources, $inputDestinations) as $email){ 63 | $emailErrors[$email] = "Address \"{$email}\" cannot be in source and destination in same redirect."; 64 | } 65 | 66 | 67 | if(count($emailErrors) > 0){ 68 | Message::getInstance()->fail(implode("
", $emailErrors)); 69 | } 70 | else{ 71 | if(count($emailErrors) === 0 && $savemode === "edit" && !is_null($redirect)){ 72 | 73 | if(count($inputSources) > 0 && count($inputDestinations) > 0){ 74 | 75 | if(Config::get('options.enable_multi_source_redirects', false) && $redirect instanceof AbstractMultiRedirect){ 76 | $existingRedirectsToEdit = AbstractRedirect::findWhere( 77 | array(AbstractRedirect::attr('multi_hash'), $redirect->getMultiHash()) 78 | ); 79 | } 80 | else{ 81 | $existingRedirectsToEdit = AbstractRedirect::findWhere( 82 | array(AbstractRedirect::attr('id'), $redirect->getId()) 83 | ); 84 | } 85 | 86 | $emailsToCheck = $inputSources; 87 | foreach($existingRedirectsToEdit as $r){ 88 | $key = array_search($r->getSource(), $emailsToCheck); 89 | if($key !== false){ 90 | unset($emailsToCheck[$key]); 91 | } 92 | } 93 | 94 | if(count($emailsToCheck) > 0){ 95 | $existingRedirectsOther = AbstractRedirect::findWhere( 96 | array( 97 | array(AbstractRedirect::attr('source'), 'IN', $emailsToCheck) 98 | ) 99 | ); 100 | } 101 | else{ 102 | $existingRedirectsOther = null; 103 | } 104 | 105 | if(!is_null($existingRedirectsOther) && $existingRedirectsOther->count() > 0){ 106 | $errorMessages = array(); 107 | /** @var AbstractRedirect $existingRedirect */ 108 | foreach($existingRedirectsOther as $id => $existingRedirect){ 109 | if(!$existingRedirectsToEdit->has($id)){ 110 | $errorMessages[] = "Source address \"{$existingRedirect->getSource()}\" is already redirected to some destination."; 111 | } 112 | } 113 | 114 | Message::getInstance()->fail(implode("
", $errorMessages)); 115 | } 116 | else{ 117 | // multi source handling 118 | $hash = (count($inputSources) === 1) ? null : md5(emailsToString($inputSources)); 119 | 120 | foreach($inputSources as $sourceAddress){ 121 | $sourceAddress = formatEmail($sourceAddress); 122 | 123 | /** @var AbstractRedirect $thisRedirect */ 124 | $thisRedirect = $existingRedirectsToEdit->search( 125 | function($model) use ($sourceAddress){ 126 | /** @var AbstractRedirect $model */ 127 | return $model->getSource() === $sourceAddress; 128 | } 129 | ); 130 | 131 | if(!is_null($thisRedirect)){ 132 | // edit existing source 133 | 134 | $thisRedirect->setSource($sourceAddress); 135 | $thisRedirect->setDestination($inputDestinations); 136 | $thisRedirect->setMultiHash($hash); 137 | // Don't set 'isCreatedByUser' here, it will overwrite redirects created by user 138 | $thisRedirect->save(); 139 | 140 | $existingRedirectsToEdit->delete($thisRedirect->getId()); // mark updated 141 | } 142 | else{ 143 | $data = array( 144 | AbstractRedirect::attr('source') => $sourceAddress, 145 | AbstractRedirect::attr('destination') => emailsToString($inputDestinations), 146 | AbstractRedirect::attr('multi_hash') => $hash, 147 | ); 148 | 149 | if(Config::get('options.enable_user_redirects', false)){ 150 | $data[AbstractRedirect::attr('is_created_by_user')] = false; 151 | } 152 | 153 | AbstractRedirect::createAndSave($data); 154 | } 155 | } 156 | 157 | // Delete none updated redirect 158 | foreach($existingRedirectsToEdit as $redirect){ 159 | $redirect->delete(); 160 | } 161 | 162 | // Edit successfull, redirect to overview 163 | Router::redirect("admin/listredirects/?edited=1"); 164 | } 165 | } 166 | else{ 167 | Message::getInstance()->fail("Redirect couldn't be edited. Fill out all fields."); 168 | } 169 | } 170 | 171 | else if(count($emailErrors) === 0 && $savemode === "create"){ 172 | if(count($inputSources) > 0 && count($inputDestinations) > 0){ 173 | 174 | $existingRedirects = AbstractRedirect::findWhere( 175 | array(AbstractRedirect::attr('source'), 'IN', $inputSources) 176 | ); 177 | 178 | if($existingRedirects->count() > 0){ 179 | $errorMessages = array(); 180 | /** @var AbstractRedirect $existingRedirect */ 181 | foreach($existingRedirects as $existingRedirect){ 182 | $errorMessages[] = "Source address \"{$existingRedirect->getSource()}\" is already redirected to some destination."; 183 | } 184 | 185 | Message::getInstance()->fail(implode("
", $errorMessages)); 186 | } 187 | else{ 188 | $inputDestination = emailsToString($inputDestinations); 189 | $hash = (count($inputSources) === 1) ? null : md5(emailsToString($inputSources)); 190 | 191 | foreach($inputSources as $inputSource){ 192 | $data = array( 193 | AbstractRedirect::attr('source') => $inputSource, 194 | AbstractRedirect::attr('destination') => $inputDestination, 195 | AbstractRedirect::attr('multi_hash') => $hash, 196 | ); 197 | 198 | if(Config::get('options.enable_user_redirects', false)){ 199 | $data[AbstractRedirect::attr('is_created_by_user')] = false; 200 | } 201 | 202 | $a = AbstractRedirect::createAndSave($data); 203 | } 204 | 205 | // Redirect created, redirect to overview 206 | Router::redirect("admin/listredirects/?created=1"); 207 | } 208 | } 209 | else{ 210 | Message::getInstance()->fail("Redirect couldn't be created. Fill out all fields."); 211 | } 212 | } 213 | } 214 | } 215 | 216 | 217 | // Select mode 218 | $mode = "create"; 219 | if(isset($_GET['id'])){ 220 | $mode = "edit"; 221 | } 222 | 223 | $domains = Domain::getByLimitedDomains(); 224 | ?> 225 | 226 |

Redirect

227 | 228 |
229 | ❬ Back to redirects list 230 |
231 | 232 |
233 | Please note that mailservers will prefer to deliver mails to redirects over mailboxes.
234 | So make sure you don't accidentally override a mailbox with a redirect. 235 |
236 | 237 | render(); ?> 238 | 239 | isDomainLimited() && $domains->count() === 0): ?> 240 |
241 | You are listed for limited access to domains, but it seems there are no domains listed you can access. 242 |
243 | 244 |
245 | 246 | 247 |
248 |
Enter single or multiple addresses separated by comma, semicolon or newline.
249 |
250 | 251 |
252 | 253 |
254 | count() > 0): ?> 255 | isDomainLimited()): ?> 256 | You can create redirects for source addresses from these domains only: 257 | 258 | You can create redirects for every domain you want,
259 | but here's a list of domains managed by WebMUM: 260 | 261 |
    262 | 263 |
  • getDomain(); ?>
  • 264 | 265 |
266 | 267 | There are no domains managed by WebMUM yet. 268 | 269 |
270 |
271 | 272 | 273 | 274 | 275 | 276 |
277 |
278 | 279 |
280 | 281 |
282 | 283 |
284 |
285 | 286 |
287 | 288 |
289 |
290 | -------------------------------------------------------------------------------- /include/php/pages/admin/listdomains.php: -------------------------------------------------------------------------------- 1 | isDomainLimited()){ 4 | Router::displayError(403); 5 | } 6 | 7 | if(isset($_GET['deleted']) && $_GET['deleted'] == "1"){ 8 | Message::getInstance()->success("Domain deleted successfully."); 9 | } 10 | else if(isset($_GET['created']) && $_GET['created'] == "1"){ 11 | Message::getInstance()->success("Domain created successfully."); 12 | } 13 | else if(isset($_GET['adm_del']) && $_GET['adm_del'] == "1"){ 14 | Message::getInstance()->fail("Domain couldn't be deleted because admin account would be affected."); 15 | } 16 | else if(isset($_GET['missing-permission']) && $_GET['missing-permission'] == "1"){ 17 | Message::getInstance()->fail("You don't have the permission to delete that domain."); 18 | } 19 | 20 | $domains = Domain::findAll(); 21 | 22 | ?> 23 | 24 |

Domains

25 | 26 | isDomainLimited()): ?> 27 |
28 | Create new domain 29 |
30 | 31 | 32 | render(); ?> 33 | 34 | count() > 0): ?> 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
DomainUser countRedirect count
getDomain(); ?>countUsers(); ?>countRedirects(); ?> 51 | [Delete] 52 |
count()); ?>
62 | 63 |
64 | There are currently no domains created you can manage. 65 |
66 | -------------------------------------------------------------------------------- /include/php/pages/admin/listredirects.php: -------------------------------------------------------------------------------- 1 | success("Redirect deleted successfully."); 5 | } 6 | else if(isset($_GET['created']) && $_GET['created'] == "1"){ 7 | Message::getInstance()->success("Redirect created successfully."); 8 | } 9 | else if(isset($_GET['edited']) && $_GET['edited'] == "1"){ 10 | Message::getInstance()->success("Redirect edited successfully."); 11 | } 12 | else if(isset($_GET['missing-permission']) && $_GET['missing-permission'] == "1"){ 13 | Message::getInstance()->fail("You don't have the permission to edit/delete redirects of that domain."); 14 | } 15 | 16 | $redirects = AbstractRedirect::getMultiByLimitedDomains(); 17 | ?> 18 | 19 |

Redirects

20 | 21 | isDomainLimited() && count(Domain::getByLimitedDomains()) === 0)): ?> 22 |
23 | Create new redirect 24 |
25 | 26 |
27 | You are listed for limited access to domains, but it seems there are no domains listed you can access. 28 |
29 | 30 | 31 | render(); ?> 32 | 33 | count() > 0): ?> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | getConflictingUsers()->count() > 0 ? ' class="warning"' : ''; ?>> 49 | 55 | 56 | 57 | 58 | 59 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 80 | 81 |
SourceDestinationCreated by user
50 | getConflictingUsers()->count() > 0): ?> 51 | getConflictingUsers()->count() === 1 ? 'The marked redirect overrides a mailbox.' : 'The marked redirects override mailboxes.'; ?>
52 | 53 | getConflictingMarkedSource()); ?> 54 |
getDestination()); ?>isCreatedByUser() ? 'Yes' : 'No'; ?> 60 | [Edit] 61 | 63 | [Delete] 64 |
count()); ?>
82 | isDomainLimited() && count(Domain::getByLimitedDomains()) === 0)): ?> 83 |
84 | There are currently no redirects created you can manage. 85 |
86 | -------------------------------------------------------------------------------- /include/php/pages/admin/listusers.php: -------------------------------------------------------------------------------- 1 | success("User deleted successfully."); 5 | } 6 | else if(isset($_GET['created']) && $_GET['created'] == "1"){ 7 | Message::getInstance()->success("User created successfully."); 8 | } 9 | else if(isset($_GET['edited']) && $_GET['edited'] == "1"){ 10 | Message::getInstance()->success("User edited successfully."); 11 | } 12 | else if(isset($_GET['adm_del']) && $_GET['adm_del'] == "1"){ 13 | Message::getInstance()->fail("Admin user cannot be deleted."); 14 | } 15 | else if(isset($_GET['missing-permission']) && $_GET['missing-permission'] == "1"){ 16 | Message::getInstance()->fail("You don't have the permission to edit/delete users of that domain."); 17 | } 18 | 19 | $users = User::getByLimitedDomains(); 20 | 21 | ?> 22 | 23 |

List of all mailbox accounts

24 | 25 | isDomainLimited() && count(Domain::getByLimitedDomains()) === 0)): ?> 26 |
27 | Create new user 28 |
29 | 30 |
31 | You are listed for limited access to domains, but it seems there are no domains listed you can access. 32 |
33 | 34 | 35 | render(); ?> 36 | 37 | count() > 0): ?> 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | getConflictingRedirect()) ? ' class="warning"' : ''; ?>> 58 | 64 | 65 | 66 | 67 | 68 | 71 | 72 | 81 | 82 | 83 | 86 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
UsernameDomainMailbox LimitRedirect countUser RedirectsRole
59 | getConflictingRedirect())): ?> 60 | This mailbox is overridden by a redirect.
61 | 62 | getUsername(); ?> 63 |
getDomain(); ?>getMailboxLimit() > 0) ? $user->getMailboxLimit().' MB' : 'No limit'; ?> 69 | getRedirects()->count(); ?> 70 | 73 | getMaxUserRedirects() < 0): ?> 74 | Not Allowed 75 | getMaxUserRedirects() > 0): ?> 76 | Limited (getMaxUserRedirects(); ?>) 77 | 78 | Unlimited 79 | 80 | getRole() === User::ROLE_ADMIN) ? 'Admin' : 'User'; ?> 84 | [Edit] 85 | 87 | [Delete] 88 |
count()); ?>
98 | isDomainLimited() && count(Domain::getByLimitedDomains()) === 0)): ?> 99 |
100 | There are currently no users created you can manage. 101 |
102 | 103 | -------------------------------------------------------------------------------- /include/php/pages/admin/start.php: -------------------------------------------------------------------------------- 1 |

Admin Dashboard

2 | 3 |
4 | Manage users 5 | 6 | isDomainLimited()): ?> 7 | Manage domains 8 | 9 | 10 | Manage redirects 11 |
-------------------------------------------------------------------------------- /include/php/pages/login.php: -------------------------------------------------------------------------------- 1 | fail('Please fill out both email and password fields.'); 11 | } 12 | else { 13 | // Start login 14 | if(Auth::login($_POST['email'], $_POST['password'])){ 15 | Router::redirect("private"); 16 | } 17 | // If login isn't successful 18 | else{ 19 | //Log error message 20 | writeLog("WebMUM login failed for IP ".$_SERVER['REMOTE_ADDR']); 21 | Message::getInstance()->fail("Sorry, but we cannot log you in with this combination of email and password, there might be a typo."); 22 | } 23 | } 24 | } 25 | 26 | ?> 27 | 28 |

Login

29 | 30 | render(); ?> 31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 | -------------------------------------------------------------------------------- /include/php/pages/private/changepass.php: -------------------------------------------------------------------------------- 1 | changePassword($_POST['password'], $_POST['password_repeat']); 6 | 7 | Message::getInstance()->success("Password changed successfully!"); 8 | } 9 | catch(AuthException $passwordInvalidException){ 10 | Message::getInstance()->fail($passwordInvalidException->getMessage()); 11 | } 12 | } 13 | 14 | ?> 15 | 16 |

Change password

17 | 18 |
19 | ❬ Back to personal dashboard 20 |
21 | 22 | render(); ?> 23 | 24 |
25 |
26 | 27 | 28 |
Your new password must be at least characters long.
29 | 30 |
31 | 32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 | 41 |
42 |
-------------------------------------------------------------------------------- /include/php/pages/private/createredirect.php: -------------------------------------------------------------------------------- 1 | canCreateUserRedirects() 5 | ){ 6 | Router::redirect('private/redirects'); 7 | } 8 | 9 | if(isset($_POST['source'])){ 10 | 11 | $destination = Auth::getUser()->getEmail(); 12 | $domain = Auth::getUser()->getDomain(); 13 | 14 | $inputSources = stringToEmails($_POST['source']); 15 | 16 | // validate emails 17 | $emailErrors = array(); 18 | 19 | // basic email validation isn't working 100% correct though 20 | foreach($inputSources as $email){ 21 | if(strpos($email, '@') === false){ 22 | $emailErrors[$email] = "Address \"{$email}\" isn't a valid email address."; 23 | } 24 | } 25 | 26 | // validate source emails are on domains 27 | if(Config::get('options.enable_validate_aliases_source_domain', true)){ 28 | $domains = Domain::getByLimitedDomains(); 29 | 30 | foreach($inputSources as $email){ 31 | if(isset($emailErrors[$email])){ 32 | continue; 33 | } 34 | 35 | $emailParts = explode('@', $email); 36 | if($emailParts[1] != $domain){ 37 | $emailErrors[$email] = "Domain of source address \"{$email}\" must be \"{$domain}\"."; 38 | } 39 | } 40 | } 41 | 42 | // validate no redirect loops 43 | if(in_array($destination, $inputSources)){ 44 | $emailErrors[$destination] = "Address \"{$destination}\" cannot be in source and destination in same redirect."; 45 | } 46 | 47 | 48 | if(count($emailErrors) > 0){ 49 | Message::getInstance()->fail(implode("
", $emailErrors)); 50 | } 51 | elseif(count($inputSources) !== 1){ 52 | Message::getInstance()->fail("Only one email address as source."); 53 | } 54 | else{ 55 | if(count($inputSources) > 0){ 56 | 57 | $existingRedirects = AbstractRedirect::findWhere( 58 | array(AbstractRedirect::attr('source'), 'IN', $inputSources) 59 | ); 60 | 61 | if($existingRedirects->count() > 0){ 62 | $errorMessages = array(); 63 | /** @var AbstractRedirect $existingRedirect */ 64 | foreach($existingRedirects as $existingRedirect){ 65 | $errorMessages[] = "Source address \"{$existingRedirect->getSource()}\" is already redirected to some destination."; 66 | } 67 | 68 | Message::getInstance()->fail(implode("
", $errorMessages)); 69 | } 70 | else{ 71 | foreach($inputSources as $inputSource){ 72 | $data = array( 73 | AbstractRedirect::attr('source') => $inputSource, 74 | AbstractRedirect::attr('destination') => $destination, 75 | AbstractRedirect::attr('multi_hash') => null, 76 | AbstractRedirect::attr('is_created_by_user') => true, 77 | ); 78 | 79 | $a = Alias::createAndSave($data); 80 | } 81 | 82 | // Redirect created, redirect to overview 83 | Router::redirect('private/redirects'); 84 | } 85 | } 86 | else{ 87 | Message::getInstance()->fail("Redirect couldn't be created. Fill out all fields."); 88 | } 89 | } 90 | } 91 | 92 | 93 | $domains = Domain::getByLimitedDomains(); 94 | ?> 95 | 96 |

Create Redirect

97 | 98 |
99 | ❬ Back to your redirects 100 |
101 | 102 | render(); ?> 103 | 104 |
105 | 106 |
107 | 108 |
109 | count() > 0): ?> 110 | You can only create redirects with this domain: 111 |
    112 |
  • getDomain(); ?>
  • 113 |
114 | 115 | There are no domains managed by WebMUM yet. 116 | 117 |
118 |
119 | 120 |
121 |
122 | 123 |
124 | 125 |
126 | getEmail()); ?> 127 |
128 |
129 | 130 |
131 | 132 |
133 |
-------------------------------------------------------------------------------- /include/php/pages/private/deleteredirect.php: -------------------------------------------------------------------------------- 1 | isAllowedToCreateUserRedirects() 5 | ){ 6 | Router::redirect('private/redirects'); 7 | } 8 | 9 | if(!isset($_GET['id'])){ 10 | // Redirect id not set, redirect to overview 11 | Router::redirect('private/redirects'); 12 | } 13 | 14 | $id = $_GET['id']; 15 | 16 | /** @var AbstractRedirect $redirect */ 17 | $redirect = AbstractRedirect::findMultiWhereFirst( 18 | array( 19 | array(AbstractRedirect::attr('id'), $id), 20 | array(AbstractRedirect::attr('is_created_by_user'), true), 21 | array(AbstractRedirect::attr('destination'), Auth::getUser()->getEmail()), 22 | ) 23 | ); 24 | 25 | if(is_null($redirect)){ 26 | // Redirect doesn't exist, redirect to overview 27 | Router::redirect('private/redirects'); 28 | } 29 | 30 | if(isset($_POST['confirm'])){ 31 | $confirm = $_POST['confirm']; 32 | 33 | if($confirm === "yes"){ 34 | 35 | $redirect->delete(); 36 | 37 | // Delete redirect successfull, redirect to overview 38 | Router::redirect('private/redirects/?deleted=1'); 39 | } 40 | else{ 41 | // Choose to not delete redirect, redirect to overview 42 | Router::redirect('private/redirects'); 43 | } 44 | } 45 | 46 | else{ 47 | ?> 48 | 49 |

Delete redirection?

50 | 51 |
52 | ❬ Back to your redirects 53 |
54 | 55 |
56 |
57 | 58 |
getSource()); ?>
59 |
60 | 61 |
62 | 63 |
getDestination()); ?>
64 |
65 | 66 |
67 | 68 |
69 | 73 |
74 |
75 | 76 |
77 | 78 |
79 |
80 | -------------------------------------------------------------------------------- /include/php/pages/private/start.php: -------------------------------------------------------------------------------- 1 |

Welcome to your dashboard!

2 | 3 |

4 | Please choose an action. 5 |

6 | 7 |
8 | Change your password 9 |
10 | 11 |
12 | Redirects to your mailbox 13 |
-------------------------------------------------------------------------------- /include/php/pages/private/yourredirects.php: -------------------------------------------------------------------------------- 1 | isAllowedToCreateUserRedirects(); 5 | 6 | $redirects = $user->getAnonymizedRedirects(); 7 | 8 | $userRedirectsCount = $user->getSelfCreatedRedirects()->count(); 9 | ?> 10 | 11 |

Redirects to your mailbox

12 | 13 |
14 | ❬ Back to personal dashboard 15 | 16 | canCreateUserRedirects()): ?> 17 | Create new redirect 18 | 19 | Create new redirect 20 | 21 | 22 |
23 | 24 | render(); ?> 25 | 26 | 27 |
28 | You are allowed to create getMaxUserRedirects() === 0 ? 'unlimited user redirects' : textValue('up to _ user redirect', $user->getMaxUserRedirects()); ?> on your own. 29 | getMaxUserRedirects() > 0): ?> 30 | canCreateUserRedirects()): ?> 31 |

You can still create getMaxUserRedirects() - $userRedirectsCount); ?>. 32 | 33 |

You cannot create anymore redirects as your limit is reached. 34 |
Consider deleting unused redirects or ask an admin to extend your limit. 35 | 36 | 37 |
38 | 39 | 40 | count() > 0): ?> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 80 | 81 | 82 | 83 |
SourceDestinationCreated by you
getSource()); ?>getDestination()); ?>isCreatedByUser() ? 'Yes' : 'No'; ?> 60 | isCreatedByUser()): ?> 61 | [Delete] 62 | 63 |
count()); ?> 74 | getMaxUserRedirects() === 0): ?> 75 | 76 | 77 | getMaxUserRedirects()); ?> 78 | 79 |
84 | 85 |
86 | There are currently no redirects to your mailbox. 87 |
88 | -------------------------------------------------------------------------------- /include/php/pages/start.php: -------------------------------------------------------------------------------- 1 | 6 | 7 |

WebMUM

8 | 9 |

10 | WebMUM is an easy to use web interface for managing user accounts on your e-mail server with a MySQL user backend.
11 | Users of your server can log in here to change their passwords. 12 |

13 | 14 |
15 | Log in 16 |
17 | 18 | -------------------------------------------------------------------------------- /include/php/routes.inc.php: -------------------------------------------------------------------------------- 1 | Not allowed! 2 | 3 |

4 | Sorry, you aren't allowed to access this page. 5 |

-------------------------------------------------------------------------------- /include/php/template/error/not-found.php: -------------------------------------------------------------------------------- 1 |

This page does not exist.

2 | 3 |

4 | Sorry, the page you requested couldn't be found. 5 |

-------------------------------------------------------------------------------- /include/php/template/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebMUM 5 | 6 | 17 | 18 | 19 | 20 | 41 | 42 |
43 | 44 |
45 | 46 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | Faulty database query: "'.$e->getQuery().'".'; 35 | } 36 | catch(Exception $e){ 37 | $content = '
'.$e->getMessage().'
'; 38 | } 39 | 40 | if(defined('USING_OLD_CONFIG')){ 41 | $content = '
Your WebMUM installation is still using the old deprecated config style!

Please update your config to the new style (an example config can be found in config.php.example)
and delete your old config.inc.php and config.inc.php.example.
'.$content; 42 | } 43 | 44 | echo Router::loadAndBufferOutput( 45 | 'include/php/template/layout.php', 46 | array( 47 | 'content' => $content, 48 | ) 49 | ); -------------------------------------------------------------------------------- /installer/.htaccess: -------------------------------------------------------------------------------- 1 | Deny from all -------------------------------------------------------------------------------- /installer/index.php: -------------------------------------------------------------------------------- 1 | 0, 26 | 1 => 1, 27 | 2 => 2, 28 | 3 => 2, 29 | 4 => 3, 30 | 5 => 4, 31 | 6 => 5, 32 | 7 => 6, 33 | ); 34 | 35 | /*-----------------------------------------------------------------------------*/ 36 | 37 | function installer_reset() 38 | { 39 | global $_SESSION; 40 | 41 | $_SESSION['installer'] = array( 42 | 'lastStep' => 0, 43 | 'step' => 0, 44 | 'config' => array(), 45 | ); 46 | } 47 | 48 | function installer_message($setMessage = null) 49 | { 50 | global $_SESSION; 51 | 52 | if(!is_null($setMessage)){ 53 | $_SESSION['installer']['message'] = $setMessage; 54 | } 55 | elseif(isset($_SESSION['installer']['message'])){ 56 | $m = '
'.$_SESSION['installer']['message'].'
'; 57 | unset($_SESSION['installer']['message']); 58 | 59 | return $m; 60 | } 61 | 62 | return $setMessage; 63 | } 64 | 65 | function installer_prev($thisStep, $stepSize = 1) 66 | { 67 | $s = ($thisStep < 0) ? 0 : ($thisStep - $stepSize); 68 | 69 | $_SESSION['installer']['lastStep'] = $thisStep; 70 | $_SESSION['installer']['step'] = $s; 71 | 72 | Router::redirect('/?step='.$s); 73 | } 74 | 75 | function installer_next($thisStep, $stepSize = 1) 76 | { 77 | $s = ($thisStep > 8) ? 8 : ($thisStep + $stepSize); 78 | 79 | $_SESSION['installer']['lastStep'] = $thisStep; 80 | $_SESSION['installer']['step'] = $s; 81 | 82 | Router::redirect('/?step='.$s); 83 | } 84 | 85 | if(!isset($_SESSION['installer'])){ 86 | installer_reset(); 87 | } 88 | 89 | /*-----------------------------------------------------------------------------*/ 90 | 91 | $step = (isset($_GET['step']) && is_numeric($_GET['step'])) ? intval($_GET['step']) : 0; 92 | 93 | echo '

Installation of WebMUM

'; 94 | 95 | if($step > 0){ 96 | ?> 97 |
    98 | 99 |
  1. 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
  2. 108 | 109 |
110 | getMessage(); 125 | } 126 | -------------------------------------------------------------------------------- /installer/step0.php: -------------------------------------------------------------------------------- 1 | =')){ 16 | $requirements[] = 'php_version'; 17 | } 18 | if(function_exists('mysqli_connect')){ 19 | $requirements[] = 'php_extension_mysqli'; 20 | } 21 | if(session_status() != PHP_SESSION_DISABLED){ 22 | $requirements[] = 'php_session_enabled'; 23 | } 24 | if(file_exists('config') && is_dir('config')){ 25 | $requirements[] = 'config_directory'; 26 | } 27 | if(file_exists('config/config.php.example')){ 28 | $requirements[] = 'config_example'; 29 | } 30 | 31 | /*-----------------------------------------------------------------------------*/ 32 | 33 | if(isset($_GET['go']) && $_GET['go'] == 'next'){ 34 | if(count($requirements) === $numberOfRequirements){ 35 | installer_message('All requirements fulfilled, let\'s get started with the installation!'); 36 | 37 | installer_next($thisStep); 38 | } 39 | } 40 | ?> 41 | 42 | 43 |

Getting started

44 | 45 |

By following this wizard you will install and configure your new WebMUM installation.

46 | 47 |
48 | 49 | System Info: 50 | 58 | 59 | Server requirements 60 | 67 | 68 | Required PHP settings 69 | 81 | 82 | Directories and files 83 | 95 | 96 |
97 | 98 | 99 |

Click on the Start button to continue.

100 | Start 101 | 102 |

Some requirements aren't fulfilled.

103 | -------------------------------------------------------------------------------- /installer/step1.php: -------------------------------------------------------------------------------- 1 | $_POST['host'], 24 | 'user' => $_POST['user'], 25 | 'password' => $_POST['password'], 26 | 'database' => $_POST['database'], 27 | ); 28 | $_SESSION['installer']['type'] = (isset($_POST['install_type']) && $_POST['install_type'] == INSTALLER_TYPE_MAP) 29 | ? INSTALLER_TYPE_MAP 30 | : INSTALLER_TYPE_CREATE; 31 | 32 | installer_message('Database connection was successfully established.'); 33 | 34 | installer_next($thisStep, ($_SESSION['installer']['type'] === INSTALLER_TYPE_MAP) ? 2 : 1); 35 | } 36 | catch(InvalidArgumentException $e){ 37 | $error = 'Some fields are missing.'; 38 | } 39 | catch(Exception $e){ 40 | $error = $e->getMessage(); 41 | } 42 | } 43 | elseif($_GET['go'] == 'prev'){ 44 | // reset 45 | unset($_SESSION['installer']['config']['mysql']); 46 | unset($_SESSION['installer']['type']); 47 | 48 | installer_prev($thisStep); 49 | } 50 | } 51 | 52 | function getAttr($name, $default = null) 53 | { 54 | global $_SESSION, $_POST; 55 | 56 | if(isset($_POST[$name])){ 57 | return strip_tags($_POST[$name]); 58 | } 59 | elseif(isset($_SESSION['installer']['config']['mysql'][$name])){ 60 | return $_SESSION['installer']['config']['mysql'][$name]; 61 | } 62 | elseif($name === 'install_type' && isset($_SESSION['installer']['type'])){ 63 | return $_SESSION['installer']['type']; 64 | } 65 | 66 | return $default; 67 | } 68 | 69 | ?> 70 | 71 | 72 |

Step 1 of : Database connection.

73 | 74 | 75 |
76 | 77 | 78 |
79 | 80 |

Setup your MySQL database connection.

81 | 82 |
83 | 84 |
85 | 86 |
87 |
88 | 89 |
90 | 91 |
92 | 93 |
94 |
95 | 96 |
97 | 98 |
99 | 100 |
101 |
102 | 103 |
104 | 105 |
106 | 107 |
108 |
109 | 110 |
111 | 112 |
113 | 114 |
Be sure to select the correct option.
115 |
116 | /> 117 | 118 |
119 |
120 | /> 121 | 122 |
123 |
124 | 125 | 126 | 127 |
128 | Back 129 | 130 |
131 |
-------------------------------------------------------------------------------- /installer/step2.php: -------------------------------------------------------------------------------- 1 | query("SELECT table_name FROM information_schema.tables WHERE table_schema='".$_SESSION['installer']['config']['mysql']['database']."';"); 20 | foreach($tablesResult->fetch_all() as $row){ 21 | $tablesInDatabase[] = $row[0]; 22 | } 23 | } 24 | catch(Exception $e){ 25 | } 26 | 27 | /*-----------------------------------------------------------------------------*/ 28 | 29 | $databaseSchema = array( 30 | '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;", 31 | '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;", 32 | '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;", 33 | ); 34 | 35 | /** 36 | * @param string $stmt 37 | * @param string $database 38 | * @param string $table 39 | * @param array $attributes 40 | * 41 | * @return string 42 | */ 43 | function prepareSchemaTableStmt($stmt, $database, $table, $attributes) 44 | { 45 | $attributes['database'] = $database; 46 | $attributes['table'] = $table; 47 | 48 | foreach($attributes as $search => $replace){ 49 | $stmt = str_replace('___'.$search.'___', '`'.Database::getInstance()->escape($replace).'`', $stmt); 50 | } 51 | 52 | return $stmt; 53 | } 54 | 55 | $preparedSchemaStmt = ''; 56 | $allTablesFromSchemaExist = true; 57 | 58 | foreach($databaseSchema as $table => $stmt){ 59 | $preparedSchemaStmt .= prepareSchemaTableStmt( 60 | $stmt, 61 | $_SESSION['installer']['config']['mysql']['database'], 62 | $exampleConfigValues['schema']['tables'][$table], 63 | $exampleConfigValues['schema']['attributes'][$table] 64 | ).PHP_EOL; 65 | 66 | // check if tables exist, should be enough for now 67 | if(!in_array($exampleConfigValues['schema']['tables'][$table], $tablesInDatabase)){ 68 | $allTablesFromSchemaExist = false; 69 | } 70 | } 71 | 72 | $commandDenied = false; 73 | 74 | /*-----------------------------------------------------------------------------*/ 75 | 76 | if(isset($_GET['go'])){ 77 | if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){ 78 | if(isset($_POST['manual'])){ 79 | if($_POST['manual'] == 1){ 80 | // display SQL 81 | } 82 | elseif($_POST['manual'] == 2){ 83 | // check if schema was created 84 | if($allTablesFromSchemaExist){ 85 | // saving information 86 | $_SESSION['installer']['config']['schema'] = $exampleConfigValues['schema']; 87 | 88 | installer_message('Database schema was manually created.'); 89 | 90 | installer_next($thisStep, 2); 91 | } 92 | else{ 93 | $_POST['manual'] = 1; 94 | } 95 | } 96 | } 97 | else{ 98 | if(!$allTablesFromSchemaExist){ 99 | try{ 100 | foreach(explode(PHP_EOL, $preparedSchemaStmt) as $stmt){ 101 | Database::getInstance()->query($stmt); 102 | } 103 | 104 | // saving information 105 | $_SESSION['installer']['config']['schema'] = $exampleConfigValues['schema']; 106 | 107 | installer_message('Database schema was automatically created.'); 108 | 109 | installer_next($thisStep, 2); 110 | } 111 | catch(Exception $e){ 112 | if(strpos($e->getMessage(), 'command denied') !== false){ 113 | $commandDenied = true; 114 | } 115 | else{ 116 | throw $e; 117 | } 118 | } 119 | } 120 | } 121 | } 122 | elseif($_GET['go'] == 'prev'){ 123 | // reset 124 | unset($_SESSION['installer']['config']['schema']); 125 | 126 | installer_prev($thisStep); 127 | } 128 | } 129 | ?> 130 | 131 | 132 |

Step 2 of : Create database schema.

133 | 134 | 135 |
136 | The schema already exists in database "". 137 |
138 | 139 |
140 | Your next possible steps: 141 | 146 |
147 | 148 |
149 | Back 150 | Retry 151 |
152 | 153 |
154 | 155 | 156 | 157 |
158 | Copy the SQL-Code above and import it into your database "". 159 |
160 | 161 | 162 | 163 |

Once you have imported the schema, you can continue by clicking on the Continue button.

164 | 165 |
166 | Back 167 | 168 |
169 | 170 |
171 | The following database schema will be created in 172 | database "". 173 |
Please make sure that "" is clean / empty database! 174 |
175 | 176 | 177 |
The 178 | user "" is missing the permission to execute MySQL "CREATE" commands. 179 |
180 | 181 |
182 | Also make sure that the database 183 | user "" has the privileges to create the schema. 184 |
185 | 186 | 187 | $mappedTable): ?> 188 |
189 | Table "" 190 | 195 |
196 | 197 | 198 | 199 | 200 |

Click on the Continue button to try creating the schema automatically.

201 | 202 |
203 | Back 204 | 205 | 206 |
207 | 208 |
209 | -------------------------------------------------------------------------------- /installer/step3.php: -------------------------------------------------------------------------------- 1 | $thisStep){ 12 | $_SESSION['installer']['subStep'] = 1; 13 | } 14 | elseif($_SESSION['installer']['lastStep'] < $thisStep || !isset($_SESSION['installer']['subStep'])){ 15 | $_SESSION['installer']['subStep'] = 0; 16 | } 17 | 18 | $error = null; 19 | 20 | /*-----------------------------------------------------------------------------*/ 21 | 22 | $exampleConfigValues = require_once 'config/config.php.example'; 23 | 24 | $tablesInDatabase = array(); 25 | try{ 26 | Database::init($_SESSION['installer']['config']['mysql']); 27 | 28 | $db = Database::getInstance(); 29 | $tablesResult = $db->query( 30 | "SELECT TABLE_NAME FROM information_schema.tables " 31 | ."WHERE TABLE_SCHEMA='".$db->escape($_SESSION['installer']['config']['mysql']['database'])."';" 32 | ); 33 | 34 | foreach($tablesResult->fetch_all() as $row){ 35 | $tablesInDatabase[] = $row[0]; 36 | } 37 | } 38 | catch(Exception $e){ 39 | } 40 | 41 | function getTableAttributes($table) 42 | { 43 | global $_SESSION; 44 | $attributes = array(); 45 | 46 | if(Database::isInitialized()){ 47 | try{ 48 | $db = Database::getInstance(); 49 | $tablesResult = $db->query( 50 | "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA FROM information_schema.columns " 51 | ."WHERE TABLE_SCHEMA = '".$db->escape($_SESSION['installer']['config']['mysql']['database'])."' " 52 | ."AND TABLE_NAME = '".$db->escape($table)."' " 53 | ."ORDER BY TABLE_NAME,ORDINAL_POSITION;" 54 | ); 55 | 56 | foreach($tablesResult->fetch_all() as $row){ 57 | $s = $row[0]; 58 | 59 | if(!empty($row[1])){ 60 | $s .= ' : '.$row[1]; 61 | } 62 | 63 | if($row[2] == 'NO'){ 64 | $s .= ', NOT NULL'; 65 | } 66 | 67 | if(!is_null($row[3])){ 68 | $s .= ', DEFAULT \''.$row[3].'\''; 69 | } 70 | 71 | if(!empty($row[4])){ 72 | if(strpos($row[4], 'PR') !== false){ 73 | $s .= ', PRIMARY KEY'; 74 | } 75 | if(strpos($row[4], 'UN') !== false){ 76 | $s .= ', UNIQUE KEY'; 77 | } 78 | } 79 | 80 | if(!empty($row[5]) && strpos($row[5], 'auto_inc') !== false){ 81 | $s .= ', AUTO_INCREMENT'; 82 | } 83 | 84 | $attributes[$row[0]] = $s; 85 | } 86 | } 87 | catch(Exception $e){ 88 | } 89 | } 90 | 91 | return $attributes; 92 | } 93 | 94 | $optionalAttributes = array( 95 | 'users' => array('mailbox_limit', 'max_user_redirects'), 96 | 'aliases' => array('multi_source', 'is_created_by_user'), 97 | ); 98 | 99 | define('ATTR_SEP', '---'); 100 | 101 | function getAttr($name, $default = null) 102 | { 103 | global $_SESSION, $_POST; 104 | 105 | if(isset($_POST[$name])){ 106 | return strip_tags($_POST[$name]); 107 | } 108 | elseif(strpos($name, ATTR_SEP) !== false){ 109 | list($table, $attribute) = explode(ATTR_SEP, $name); 110 | 111 | if(isset($_SESSION['installer']['config']['schema']['attributes'][$table][$attribute])){ 112 | return $_SESSION['installer']['config']['schema']['attributes'][$table][$attribute]; 113 | } 114 | } 115 | elseif(isset($_SESSION['installer']['config']['schema']['tables'][$name])){ 116 | return $_SESSION['installer']['config']['schema']['tables'][$name]; 117 | } 118 | 119 | return $default; 120 | } 121 | 122 | /*-----------------------------------------------------------------------------*/ 123 | 124 | if(isset($_GET['go'])){ 125 | if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){ 126 | try{ 127 | if($_SESSION['installer']['subStep'] === 0){ 128 | 129 | $tables = array(); 130 | foreach($exampleConfigValues['schema']['tables'] as $table => $mappedTable){ 131 | if(!isset($_POST[$table]) 132 | || !in_array($_POST[$table], $tablesInDatabase) 133 | ){ 134 | throw new InvalidArgumentException('Missing mapping for table "'.$table.'".'); 135 | } 136 | 137 | if(in_array($_POST[$table], array_values($tables))){ 138 | throw new Exception('You cannot map table "'.$_POST[$table].'" twice.'); 139 | } 140 | 141 | $tables[$table] = $_POST[$table]; 142 | } 143 | 144 | // saving information 145 | $_SESSION['installer']['config']['schema'] = array(); 146 | $_SESSION['installer']['config']['schema']['tables'] = $tables; 147 | 148 | installer_message('Database tables were successfully mapped.'); 149 | 150 | $_SESSION['installer']['subStep'] = 1; 151 | installer_next($thisStep, 0); 152 | } 153 | elseif($_SESSION['installer']['subStep'] === 1){ 154 | 155 | $attributes = array(); 156 | foreach($_SESSION['installer']['config']['schema']['tables'] as $table => $mappedTable){ 157 | 158 | $attributes[$table] = array(); 159 | 160 | $attributesInDatabase = getTableAttributes($table); 161 | 162 | foreach($exampleConfigValues['schema']['attributes'][$table] as $attribute => $mappedAttribute){ 163 | $key = $table.'---'.$attribute; 164 | 165 | if(isset($optionalAttributes[$table]) 166 | && in_array($attribute, $optionalAttributes[$table]) 167 | && !isset($attributesInDatabase[$_POST[$key]]) 168 | ){ 169 | $attributes[$table][$attribute] = ''; 170 | } 171 | else{ 172 | if(!isset($_POST[$key]) || !isset($attributesInDatabase[$_POST[$key]])){ 173 | throw new InvalidArgumentException('Missing mapping for attribute "'.$attribute.'" on table "'.$table.'".'); 174 | } 175 | 176 | if(in_array($_POST[$key], $attributes[$table])){ 177 | throw new Exception('You cannot map attribute "'.$_POST[$key].'" twice on table "'.$table.'".'); 178 | } 179 | 180 | $attributes[$table][$attribute] = $_POST[$key]; 181 | } 182 | } 183 | } 184 | 185 | // saving information 186 | $_SESSION['installer']['config']['schema']['attributes'] = $attributes; 187 | 188 | installer_message('Database attributes were successfully mapped.'); 189 | 190 | unset($_SESSION['installer']['subStep']); 191 | installer_next($thisStep); 192 | } 193 | } 194 | catch(Exception $e){ 195 | $error = $e->getMessage(); 196 | } 197 | } 198 | elseif($_GET['go'] == 'prev'){ 199 | 200 | // reset 201 | if(isset($_SESSION['installer']['config']['schema']['tables'])){ 202 | if($_SESSION['installer']['subStep'] === 0){ 203 | unset($_SESSION['installer']['config']['schema']); 204 | } 205 | elseif($_SESSION['installer']['subStep'] === 1){ 206 | unset($_SESSION['installer']['config']['schema']['attributes']); 207 | } 208 | } 209 | 210 | if($_SESSION['installer']['subStep'] === 0){ 211 | unset($_SESSION['installer']['subStep']); 212 | installer_prev($thisStep, ($_SESSION['installer']['type'] === INSTALLER_TYPE_MAP) ? 2 : 1); 213 | } 214 | else{ 215 | $_SESSION['installer']['subStep'] = 0; 216 | installer_prev($thisStep, 0); 217 | } 218 | } 219 | } 220 | ?> 221 | 222 | 223 | 224 |

225 | Step 2 of : 226 | 227 | Database - table mapping. 228 | 229 | Database - attribute mapping. 230 | 231 | Wrong turn 232 | 233 |

234 | 235 | 236 |
237 | 238 | 239 | 240 |
241 | $mappedTable): ?> 242 |
243 | 244 |
245 | 251 |
252 |
253 | 254 | 255 | 256 | 257 |
258 | Back 259 | 260 |
261 |
262 | 263 |
264 | $mappedTable): 268 | $attributesInDatabase = getTableAttributes($mappedTable); 269 | ?> 270 |

271 | Table "" 272 |
Has been mapped to table "".
273 |

274 |
275 | $mappedAttribute): ?> 276 |
277 | 278 | 279 |
This attribute is optional (used by optional features) and doesn't need to be mapped.
280 | 281 |
282 | 288 |
289 |
290 | 291 |
292 | 293 |
294 | 295 | 296 | 297 | 298 | 299 |
300 | Back 301 | 302 |
303 |
304 | 305 |
You took the wrong turn, restart installation.
306 | 307 | -------------------------------------------------------------------------------- /installer/step4.php: -------------------------------------------------------------------------------- 1 | count( 26 | $_SESSION['installer']['config']['schema']['tables']['users'], 27 | $_SESSION['installer']['config']['schema']['attributes']['users']['id'] 28 | ); 29 | 30 | function getAttr($name, $default = null) 31 | { 32 | global $_SESSION, $_POST; 33 | 34 | if(isset($_POST[$name])){ 35 | return strip_tags($_POST[$name]); 36 | } 37 | elseif(isset($_SESSION['installer']['config']['password'][$name])){ 38 | return $_SESSION['installer']['config']['password'][$name]; 39 | } 40 | elseif($name === 'admin_user' && isset($_SESSION['installer']['user']['user'])){ 41 | return $_SESSION['installer']['user']['user']; 42 | } 43 | elseif($name === 'admin_password' && isset($_SESSION['installer']['user']['password'])){ 44 | return $_SESSION['installer']['user']['password']; 45 | } 46 | 47 | return $default; 48 | } 49 | 50 | /*-----------------------------------------------------------------------------*/ 51 | 52 | if(isset($_GET['go'])){ 53 | 54 | if($_GET['go'] == 'next' && $_SERVER['REQUEST_METHOD'] == 'POST'){ 55 | try{ 56 | if(!isset($_POST['hash_algorithm']) || !isset($_POST['min_length']) || !isset($_POST['admin_user']) || !isset($_POST['admin_password'])){ 57 | throw new InvalidArgumentException; 58 | } 59 | 60 | $passwordConfig = array( 61 | 'hash_algorithm' => in_array($_POST['hash_algorithm'], $hashAlgorithms) ? $_POST['hash_algorithm'] : $exampleConfigValues['password']['hash_algorithm'], 62 | 'min_length' => intval($_POST['min_length']), 63 | ); 64 | 65 | // init system for testing 66 | Config::init(array('password' => $passwordConfig)); 67 | 68 | // handle user 69 | if($databaseUserCount > 0){ 70 | // testing existing login 71 | 72 | $validLogin = Auth::login($_POST['admin_user'], $_POST['admin_password']); 73 | unset($_SESSION[Auth::SESSION_IDENTIFIER]); 74 | 75 | if(!$validLogin){ 76 | throw new Exception('Invalid combination of user and password.'); 77 | } 78 | } 79 | else{ 80 | // create user in database 81 | 82 | if(strpos($_POST['admin_user'], '@') === false){ 83 | throw new Exception('The field "Your user" must be an email address.'); 84 | } 85 | else{ 86 | list($username, $domain) = explode('@', $_POST['admin_user']); 87 | $passwordHash = Auth::generatePasswordHash($_POST['admin_password']); 88 | 89 | $hasDomain = Database::getInstance()->count( 90 | $_SESSION['installer']['config']['schema']['tables']['domains'], 91 | $_SESSION['installer']['config']['schema']['attributes']['domains']['id'], 92 | array($_SESSION['installer']['config']['schema']['attributes']['domains']['domain'], $domain) 93 | ); 94 | if($hasDomain === 0){ 95 | Database::getInstance()->insert( 96 | $_SESSION['installer']['config']['schema']['tables']['domains'], 97 | array( 98 | $_SESSION['installer']['config']['schema']['attributes']['domains']['domain'] => $domain, 99 | ) 100 | ); 101 | } 102 | 103 | Database::getInstance()->insert( 104 | $_SESSION['installer']['config']['schema']['tables']['users'], 105 | array( 106 | $_SESSION['installer']['config']['schema']['attributes']['users']['username'] => $username, 107 | $_SESSION['installer']['config']['schema']['attributes']['users']['domain'] => $domain, 108 | $_SESSION['installer']['config']['schema']['attributes']['users']['password'] => $passwordHash, 109 | ) 110 | ); 111 | } 112 | } 113 | 114 | // saving information 115 | $_SESSION['installer']['config']['password'] = $passwordConfig; 116 | $_SESSION['installer']['config']['admins'] = array($_POST['admin_user']); 117 | $_SESSION['installer']['config']['admin_domain_limits'] = array(); 118 | $_SESSION['installer']['user'] = array( 119 | 'user' => $_POST['admin_user'], 120 | 'password' => $_POST['admin_password'], 121 | ); 122 | 123 | installer_message('You have successfully added your first admin user.'); 124 | 125 | installer_next($thisStep); 126 | } 127 | catch(InvalidArgumentException $e){ 128 | $error = 'Some fields are missing.'; 129 | } 130 | catch(Exception $e){ 131 | $error = $e->getMessage(); 132 | } 133 | } 134 | elseif($_GET['go'] == 'prev'){ 135 | // reset 136 | unset($_SESSION['installer']['config']['password']); 137 | unset($_SESSION['installer']['config']['admins']); 138 | unset($_SESSION['installer']['config']['admin_domain_limits']); 139 | unset($_SESSION['installer']['user']); 140 | 141 | installer_prev($thisStep, ($_SESSION['installer']['type'] === INSTALLER_TYPE_MAP) ? 1 : 2); 142 | } 143 | } 144 | ?> 145 | 146 | 147 | 148 |

Step 3 of : Your first admin user.

149 | 150 | 151 |
152 | 153 | 154 |
155 |
156 | 157 |
Hash algorithm that you chose in your mailserver installation process.
158 |
159 | 166 |
167 |
168 | 169 |
170 | 171 |
172 |
173 | 174 | chars 175 |
176 |
177 |
178 | 179 |
180 | 181 | 182 |
183 | There is no user created yet, please create one now as your admin user. 184 |
Please note that once the user is created you will have to remember the password. 185 |
186 | 187 | 188 |

This user will be mark as an admin in the configuration.

189 | 190 |
191 | 192 |
193 | Must be an email address (user@domain).
194 | 0): ?> 195 | This user must have been added in mailserver installation process.
196 | 197 |
198 |
199 | 200 |
201 |
202 | 203 |
204 | 205 |
206 | 207 |
208 |
209 | 210 | 211 | 212 |
213 | Back 214 | 215 |
216 |
-------------------------------------------------------------------------------- /installer/step5.php: -------------------------------------------------------------------------------- 1 | $possibleEmailSeparatorsText[$_POST['email_separator_text']], 59 | 'email_separator_form' => $possibleEmailSeparatorsForm[$_POST['email_separator_form']], 60 | ); 61 | 62 | installer_message('General settings saved.'); 63 | 64 | installer_next($thisStep); 65 | } 66 | catch(InvalidArgumentException $e){ 67 | $error = 'Some field is missing.'; 68 | } 69 | catch(Exception $e){ 70 | $error = $e->getMessage(); 71 | } 72 | } 73 | elseif($_GET['go'] == 'prev'){ 74 | // reset 75 | unset($_SESSION['installer']['config']['base_url']); 76 | unset($_SESSION['installer']['config']['frontend_options']); 77 | 78 | installer_prev($thisStep); 79 | } 80 | } 81 | ?> 82 | 83 | 84 | 85 |

Step 4 of : General settings

86 | 87 | 88 |
89 | 90 | 91 |
92 | 93 |
94 | 95 |
96 | The URL your WebMUM installation is accessible from outside including subdirectories, ports and the protocol. 97 |

Some examples: 98 |
    99 |
  • http://localhost/webmum
  • 100 |
  • http://webmum.mydomain.tld
  • 101 |
  • https://mydomain.tld/dir
  • 102 |
  • http://mydomain.tld:8080
  • 103 |
104 |
105 |
106 | 107 |
108 |
109 | 110 |
111 | 112 |
113 | 114 | 115 |
116 | 117 |
118 | > 119 | 120 | 121 | > 122 | 123 | 124 | > 125 | 126 |
127 |
128 | 129 |
130 | 131 |
132 | > 133 | 134 | 135 | > 136 | 137 | 138 | > 139 | 140 |
141 |
142 |
143 | 144 | 145 | 146 |
147 | Back 148 | 149 |
150 |
-------------------------------------------------------------------------------- /installer/step6.php: -------------------------------------------------------------------------------- 1 | getMessage(); 129 | } 130 | } 131 | elseif($_GET['go'] == 'prev'){ 132 | // reset 133 | unset($_SESSION['installer']['config']['options']); 134 | unset($_SESSION['installer']['config']['log_path']); 135 | 136 | installer_prev($thisStep); 137 | } 138 | } 139 | ?> 140 | 141 | 142 | 143 |

Step 5 of : Optional features

144 | 145 | 146 |
147 | 148 | 149 |
150 | 151 |
152 | 153 |
Limit the maximum size of mailbox for users.
154 | 155 |

156 | This feature cannot be enabled because the attribute "mailbox_limit" in database table "users" is missing or not mapped yet. 157 |

You could go back and create / map the missing attribute. 158 |

159 | 160 |
161 | > 162 | 163 |
164 | 165 |
166 | 167 |
168 | 169 |
170 | 171 |
Only email addresses ending with a domain from domains will be allowed.
172 |
173 | > 174 | 175 |
176 |
177 | 178 |
179 | 180 |
181 | 182 |
Redirects can have multiple source addresses. This enables you to enter multiple redirects to a destination at once.
183 | 184 |

185 | This feature cannot be enabled because the attribute "multi_source" in database table "aliases" is missing or not mapped yet. 186 |

You could go back and create / map the missing attribute. 187 |

188 | 189 |
190 | > 191 | 192 |
193 | 194 |
195 | 196 |
197 | 198 |
199 | 200 |
201 | Limit certain admins to have access to certain domains only. 202 |
Note: This needs to be manually configured in the 'admin_domain_limits' config variable. 203 |
204 |
205 | > 206 | 207 |
208 |
209 | 210 |
211 | 212 |
213 | 214 |
215 | Enable users to create their redirects on their own. 216 |
Users can also be limited to a maximum number of redirects they can create. 217 |
218 | 219 |

220 | This feature cannot be enabled because, 221 | 222 | there are missing attributes in two database tables: 223 |

227 |
You could go back and create / map the missing attributes. 228 | 229 | the attribute is missing or not mapped yet. 230 | 231 | 232 |

You could go back and create / map the missing attributes. 233 |

234 | 235 |
236 | > 237 | 238 |
239 | 240 |
241 | 242 |
243 | 244 |
245 | 246 |
247 | WebMUM will write messages into the logfile. 248 |
The logfile could be used by Fail2ban to block brute-forcing attacks. 249 |
250 |
251 | > 252 | 253 |
254 |
255 | 256 |
257 | 258 |
Directory where the webmum.log should be written to:
259 |
260 | 261 |
262 |
263 | 264 | 265 | 266 |
267 | Back 268 | 269 |
270 |
-------------------------------------------------------------------------------- /installer/step7.php: -------------------------------------------------------------------------------- 1 | getMessage(); 65 | } 66 | } 67 | elseif($_GET['go'] == 'finish'){ 68 | try{ 69 | if(isset($_SESSION['installer']['finished'])){ 70 | // Load config 71 | $configValues = include_once 'config/config.php'; 72 | if(!is_array($configValues)){ 73 | throw new Exception('Error writing the config, please manually write the config to "'.$configPath.'".'); 74 | } 75 | 76 | // Init system 77 | Config::init($configValues); 78 | Database::init(Config::get('mysql')); 79 | Auth::init(); 80 | 81 | // Login user 82 | Auth::login($_SESSION['installer']['user']['user'], $_SESSION['installer']['user']['password']); 83 | 84 | // Reset installer 85 | unset($_SESSION['installer']); 86 | 87 | Router::redirect('/'); 88 | } 89 | } 90 | catch(Exception $e){ 91 | $error = $e->getMessage(); 92 | } 93 | } 94 | elseif($_GET['go'] == 'prev'){ 95 | installer_prev($thisStep); 96 | } 97 | } 98 | ?> 99 | 100 | 101 |
102 |
You finished your installation!
103 | 104 |

Welcome to WebMUM - Web Mailserver User Manager.

105 | 106 |
107 | If you like this project, be sure to give us a Star and Follow the project on GitHub https://github.com/ohartl/webmum 108 | 109 |
    110 |
  1. To change the configuration you have to edit to config file "" (see the README for further instructions.
  2. 111 |
  3. If you've found a bug or got a great idea, feel free to submit an issue on GitHub here.
  4. 112 |
113 |
114 | 115 | 116 | 117 |

By clicking Finish you will end the installation process and get logged in automatically.

118 | 119 |
120 | 121 |
122 |
123 | 124 | 125 | 126 |

Step 6 of : Write the config & finish the installation!

127 | 128 | 129 |
130 | 131 |
132 |

The following config needs to be written to .

133 | 134 | 135 | 136 | 137 | 138 |
139 | This is the last step, you are almost there!
140 | 144 |
145 | 146 |
147 | Back 148 | 149 | 150 |
151 |
152 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | 9 | include/php/classes 10 | include/php/models 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/AuthTest.php: -------------------------------------------------------------------------------- 1 | assertFalse(Auth::isLoggedIn()); 25 | $this->assertNull(Auth::getUser()); 26 | $this->assertFalse(Auth::hasPermission(User::ROLE_USER)); 27 | $this->assertFalse(Auth::hasPermission(User::ROLE_ADMIN)); 28 | } 29 | 30 | public function testInitUser() 31 | { 32 | $_SESSION = array( 33 | Auth::SESSION_IDENTIFIER => self::USER_ROLE_USER_ID 34 | ); 35 | 36 | Auth::init(); 37 | 38 | $this->assertTrue(Auth::isLoggedIn()); 39 | $this->assertInstanceOf('User', Auth::getUser()); 40 | $this->assertTrue(Auth::hasPermission(User::ROLE_USER)); 41 | $this->assertFalse(Auth::hasPermission(User::ROLE_ADMIN)); 42 | } 43 | 44 | 45 | public function testInitAdmin() 46 | { 47 | $_SESSION = array( 48 | Auth::SESSION_IDENTIFIER => self::USER_ROLE_ADMIN_ID 49 | ); 50 | 51 | Auth::init(); 52 | 53 | $this->assertTrue(Auth::isLoggedIn()); 54 | $this->assertInstanceOf('User', Auth::getUser()); 55 | $this->assertTrue(Auth::hasPermission(User::ROLE_USER)); 56 | $this->assertTrue(Auth::hasPermission(User::ROLE_ADMIN)); 57 | } 58 | 59 | 60 | public function testLogin() 61 | { 62 | $_SESSION = array(); 63 | 64 | Auth::init(); 65 | 66 | $this->assertFalse(Auth::isLoggedIn()); 67 | 68 | $this->assertTrue(Auth::login('user@domain.tld', 'testtest')); 69 | 70 | $this->assertTrue(Auth::isLoggedIn()); 71 | } 72 | 73 | 74 | public function testLoginInvalidEmail() 75 | { 76 | $_SESSION = array(); 77 | 78 | Auth::init(); 79 | 80 | $this->assertFalse(Auth::isLoggedIn()); 81 | 82 | $this->assertFalse(Auth::login('domain.tld', 'test')); 83 | 84 | $this->assertFalse(Auth::isLoggedIn()); 85 | } 86 | 87 | 88 | public function testLoginInvalidUser() 89 | { 90 | $_SESSION = array(); 91 | 92 | Auth::init(); 93 | 94 | $this->assertFalse(Auth::isLoggedIn()); 95 | 96 | $this->assertFalse(Auth::login('no.user@domain.tld', 'test')); 97 | 98 | $this->assertFalse(Auth::isLoggedIn()); 99 | } 100 | 101 | 102 | public function testLogout() 103 | { 104 | $_SESSION = array( 105 | Auth::SESSION_IDENTIFIER => self::USER_ROLE_USER_ID 106 | ); 107 | 108 | Auth::init(); 109 | 110 | $this->assertTrue(Auth::isLoggedIn()); 111 | 112 | Auth::logout(); 113 | 114 | $this->assertFalse(Auth::isLoggedIn()); 115 | $this->assertArrayNotHasKey(Auth::SESSION_IDENTIFIER, $_SESSION); 116 | } 117 | 118 | 119 | /** 120 | * @param int $length 121 | * @return string 122 | */ 123 | protected static function genTestPw($length) 124 | { 125 | return substr(str_shuffle("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-+=_,!@$#*%<>[]{}"), 0, $length); 126 | } 127 | 128 | 129 | /** 130 | * @expectedException AuthException 131 | * @expectedExceptionCode 2 132 | */ 133 | public function testValidateNewPasswordFirstEmpty() 134 | { 135 | Auth::validateNewPassword('', static::genTestPw(Config::get('password.min_length', 8))); 136 | } 137 | 138 | 139 | /** 140 | * @expectedException AuthException 141 | * @expectedExceptionCode 2 142 | */ 143 | public function testValidateNewPasswordLastEmpty() 144 | { 145 | Auth::validateNewPassword(static::genTestPw(Config::get('password.min_length', 8)), ''); 146 | } 147 | 148 | 149 | /** 150 | * @expectedException AuthException 151 | * @expectedExceptionCode 3 152 | */ 153 | public function testValidateNewPasswordNotEqual() 154 | { 155 | $pw = static::genTestPw(Config::get('password.min_length', 8)); 156 | Auth::validateNewPassword($pw, $pw.'neq'); 157 | } 158 | 159 | 160 | /** 161 | * @expectedException AuthException 162 | * @expectedExceptionCode 4 163 | */ 164 | public function testValidateNewPasswordTooShort() 165 | { 166 | $pw = static::genTestPw(Config::get('password.min_length', 8) - 1); 167 | Auth::validateNewPassword($pw, $pw); 168 | } 169 | 170 | 171 | public function testValidateNewPasswordOk() 172 | { 173 | $pw = static::genTestPw(Config::get('password.min_length', 8)); 174 | Auth::validateNewPassword($pw, $pw); 175 | } 176 | 177 | 178 | public function testGeneratePasswordHash() 179 | { 180 | Auth::generatePasswordHash(static::genTestPw(Config::get('password.min_length', 8))); 181 | } 182 | 183 | 184 | public function testGeneratePasswordHashAlgorithmFallback() 185 | { 186 | Config::set('password.hash_algorithm', '--not-an-algorithm--'); 187 | Auth::generatePasswordHash(static::genTestPw(Config::get('password.min_length', 8))); 188 | } 189 | 190 | 191 | public function testChangeUserPassword() 192 | { 193 | $this->assertTrue(Auth::login('user@domain.tld', 'testtest')); 194 | 195 | Auth::changeUserPassword(static::USER_ROLE_USER_ID, 'newpassword'); 196 | 197 | $this->assertFalse(Auth::login('user@domain.tld', 'testtest')); 198 | 199 | $this->assertTrue(Auth::login('user@domain.tld', 'newpassword')); 200 | } 201 | 202 | } -------------------------------------------------------------------------------- /tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | 123, 14 | 'test' => array( 15 | 'deep' => array( 16 | 'deeper' => 'value', 17 | ) 18 | ) 19 | ) 20 | ); 21 | } 22 | 23 | 24 | public function testInit() 25 | { 26 | $this->assertEquals(Config::get('test-value'), 123); 27 | $this->assertEquals(Config::get('test.deep.deeper'), 'value'); 28 | } 29 | 30 | 31 | public function testSet() 32 | { 33 | $this->assertEquals(Config::get('test-value'), 123); 34 | Config::set('test-value', false); 35 | $this->assertEquals(Config::get('test-value'), false); 36 | 37 | $this->assertEquals(Config::get('test.deep.deeper'), 'value'); 38 | Config::set('test.deep.deeper', 'other'); 39 | $this->assertEquals(Config::get('test.deep.deeper'), 'other'); 40 | 41 | Config::set('test.new.deep.deeper', true); 42 | $this->assertEquals(Config::get('test.new.deep.deeper'), true); 43 | } 44 | 45 | 46 | public function testGet() 47 | { 48 | $this->assertTrue(is_array(Config::get(null))); 49 | 50 | $this->assertEquals(Config::get('test-value'), 123); 51 | 52 | $this->assertEquals(Config::get('test.deep.deeper'), 'value'); 53 | 54 | $this->assertEquals(Config::get('test-default', 123456), 123456); 55 | } 56 | 57 | 58 | public function testHas() 59 | { 60 | $this->assertTrue(Config::has('test-value')); 61 | 62 | $this->assertTrue(Config::has('test.deep.deeper')); 63 | 64 | $this->assertFalse(Config::has('test-default')); 65 | 66 | Config::init(null); 67 | $this->assertFalse(Config::has(null)); 68 | } 69 | } -------------------------------------------------------------------------------- /tests/MessageTest.php: -------------------------------------------------------------------------------- 1 | add(Message::TYPE_SUCCESS, 'lorem'); 12 | 13 | $out = Message::getInstance()->render(); 14 | 15 | $this->assertContains(Message::TYPE_SUCCESS, $out); 16 | $this->assertContains('lorem', $out); 17 | } 18 | 19 | 20 | /** 21 | * @expectedException InvalidArgumentException 22 | */ 23 | public function testAddRestrictTypes() 24 | { 25 | Message::getInstance()->add('wrong-type', 'lorem'); 26 | } 27 | 28 | 29 | public function testAddShortcuts() 30 | { 31 | Message::getInstance()->fail('lorem'); 32 | $this->assertContains(Message::TYPE_FAIL, Message::getInstance()->render()); 33 | 34 | Message::getInstance()->error('lorem'); 35 | $this->assertContains(Message::TYPE_ERROR, Message::getInstance()->render()); 36 | 37 | Message::getInstance()->warning('lorem'); 38 | $this->assertContains(Message::TYPE_WARNING, Message::getInstance()->render()); 39 | 40 | Message::getInstance()->success('lorem'); 41 | $this->assertContains(Message::TYPE_SUCCESS, Message::getInstance()->render()); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /tests/RouterTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(self::BASE_URL, Router::url()); 22 | $this->assertEquals(self::BASE_URL, Router::url('/')); 23 | 24 | $this->assertEquals(self::BASE_URL.'/this/sub/dir?get=123', Router::url('this/sub/dir?get=123')); 25 | } 26 | 27 | 28 | public function testAdd() 29 | { 30 | Router::addRoute(Router::METHOD_GET, 'test-get', 'test-get-file'); 31 | Router::execute('test-get', Router::METHOD_GET); 32 | 33 | 34 | Router::addRoute(Router::METHOD_POST, 'test-post', 'test-post-file'); 35 | Router::execute('test-post', Router::METHOD_POST); 36 | 37 | 38 | Router::addRoute(array(Router::METHOD_GET, Router::METHOD_POST), 'test-mixed', 'test-mixed-file'); 39 | Router::execute('test-mixed', Router::METHOD_GET); 40 | Router::execute('test-mixed', Router::METHOD_POST); 41 | } 42 | 43 | 44 | public function testAddCallback() 45 | { 46 | $reachedCallback = false; 47 | Router::addRoute(Router::METHOD_GET, 'test-callback', function() use(&$reachedCallback) { 48 | $reachedCallback = true; 49 | }); 50 | Router::execute('test-callback', Router::METHOD_GET); 51 | 52 | $this->assertTrue($reachedCallback); 53 | } 54 | 55 | 56 | /** 57 | * @expectedException Exception 58 | * @expectedExceptionMessageRegExp /unsupported/i 59 | */ 60 | public function testAddMethodUnsupported() 61 | { 62 | Router::addRoute('not-a-method', 'test-fail', 'test-fail-file'); 63 | } 64 | 65 | 66 | public function testAddShortcuts() 67 | { 68 | Router::addGet('test-get', 'test-get-file'); 69 | Router::execute('test-get', Router::METHOD_GET); 70 | 71 | Router::addPost('test-post', 'test-post-file'); 72 | Router::execute('test-post', Router::METHOD_POST); 73 | 74 | Router::addMixed('test-mixed', 'test-mixed-file'); 75 | Router::execute('test-mixed', Router::METHOD_GET); 76 | Router::execute('test-mixed', Router::METHOD_POST); 77 | } 78 | 79 | 80 | /** 81 | * @expectedException Exception 82 | * @expectedExceptionMessageRegExp /unsupported/i 83 | */ 84 | public function testExecuteMethodUnsupported() 85 | { 86 | Router::execute('test-fail', 'not-a-method'); 87 | } 88 | 89 | 90 | public function testExecuteCurrentRequest() 91 | { 92 | $_SERVER['REQUEST_METHOD'] = 'GET'; 93 | $_SERVER['REQUEST_URI'] = '/somedir/test-get'; 94 | 95 | Router::executeCurrentRequest(); 96 | } 97 | 98 | 99 | public function testRouteWithPermission() 100 | { 101 | $this->assertFalse(Auth::isLoggedIn()); 102 | 103 | $reachedCallback = false; 104 | Router::addRoute(Router::METHOD_GET, 'test-perm-admin', function() use(&$reachedCallback) { 105 | $reachedCallback = true; 106 | }, User::ROLE_ADMIN); 107 | 108 | Router::addRoute(Router::METHOD_GET, 'test-perm-user', function() use(&$reachedCallback) { 109 | $reachedCallback = true; 110 | }, User::ROLE_USER); 111 | 112 | 113 | $reachedCallback = false; 114 | Router::execute('test-perm-admin', Router::METHOD_GET); 115 | $this->assertFalse($reachedCallback); 116 | 117 | $reachedCallback = false; 118 | Router::execute('test-perm-user', Router::METHOD_GET); 119 | $this->assertFalse($reachedCallback); 120 | 121 | // Now auth as admin and try again 122 | Auth::login('admin@domain.tld', 'testtest'); 123 | 124 | $reachedCallback = false; 125 | Router::execute('test-perm-admin', Router::METHOD_GET); 126 | $this->assertTrue($reachedCallback); 127 | 128 | $reachedCallback = false; 129 | Router::execute('test-perm-user', Router::METHOD_GET); 130 | $this->assertTrue($reachedCallback); 131 | 132 | Auth::logout(); 133 | 134 | // Now auth as user and try again 135 | Auth::login('user@domain.tld', 'testtest'); 136 | 137 | $reachedCallback = false; 138 | Router::execute('test-perm-admin', Router::METHOD_GET); 139 | $this->assertFalse($reachedCallback); 140 | 141 | $reachedCallback = false; 142 | Router::execute('test-perm-user', Router::METHOD_GET); 143 | $this->assertTrue($reachedCallback); 144 | 145 | Auth::logout(); 146 | } 147 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | insert( 18 | 'users', 19 | array( 20 | 'id' => static::USER_ROLE_ADMIN_ID, 21 | 'username' => 'admin', 22 | 'domain' => 'domain.tld', 23 | 'password' => Auth::generatePasswordHash('testtest'), 24 | 'mailbox_limit' => 0, 25 | ) 26 | ); 27 | 28 | Database::getInstance()->insert( 29 | 'users', 30 | array( 31 | 'id' => static::USER_ROLE_ADMIN_ID_LIMITED_NO_ACCESS, 32 | 'username' => 'no-access-limited-admin', 33 | 'domain' => 'domain.tld', 34 | 'password' => Auth::generatePasswordHash('testtest'), 35 | 'mailbox_limit' => 0, 36 | ) 37 | ); 38 | 39 | Database::getInstance()->insert( 40 | 'users', 41 | array( 42 | 'id' => static::USER_ROLE_ADMIN_ID_LIMITED_HAS_ACCESS, 43 | 'username' => 'has-access-limited-admin', 44 | 'domain' => 'domain.tld', 45 | 'password' => Auth::generatePasswordHash('testtest'), 46 | 'mailbox_limit' => 0, 47 | ) 48 | ); 49 | 50 | Database::getInstance()->insert( 51 | 'users', 52 | array( 53 | 'id' => static::USER_ROLE_USER_ID, 54 | 'username' => 'user', 55 | 'domain' => 'domain.tld', 56 | 'password' => Auth::generatePasswordHash('testtest'), 57 | 'mailbox_limit' => 64, 58 | ) 59 | ); 60 | 61 | Config::set('admins', array('admin@domain.tld', 'limited-admin@domain.tld')); 62 | Config::set('admin_domain_limits', array( 63 | 'no-access-limited-admin@domain.tld' => array(), 64 | 'has-access-limited-admin@domain.tld' => array('his-domain.tld'), 65 | )); 66 | } 67 | 68 | 69 | public static function tearDownAfterClass() 70 | { 71 | Database::getInstance()->delete('users', 'id', static::USER_ROLE_ADMIN_ID); 72 | Database::getInstance()->delete('users', 'id', static::USER_ROLE_ADMIN_ID_LIMITED_NO_ACCESS); 73 | Database::getInstance()->delete('users', 'id', static::USER_ROLE_ADMIN_ID_LIMITED_HAS_ACCESS); 74 | Database::getInstance()->delete('users', 'id', static::USER_ROLE_USER_ID); 75 | } 76 | 77 | } --------------------------------------------------------------------------------