1<?php 2/** 3 * Postfix Admin 4 * 5 * LICENSE 6 * This source file is subject to the GPL license that is bundled with 7 * this package in the file LICENSE.TXT. 8 * 9 * Further details on the project are available at http://postfixadmin.sf.net 10 * 11 * @license GNU GPL v2 or later. 12 * 13 * File: functions.inc.php 14 * Contains re-usable code. 15 */ 16 17 18$min_db_version = 1844; # update (at least) before a release with the latest function numbrer in upgrade.php 19 20/** 21 * check_session 22 * Action: Check if a session already exists, if not redirect to login.php 23 * Call: check_session () 24 * @return String username (e.g. foo@example.com) 25 */ 26function authentication_get_username() { 27 if (defined('POSTFIXADMIN_CLI')) { 28 return 'CLI'; 29 } 30 31 if (defined('POSTFIXADMIN_SETUP')) { 32 return 'SETUP.PHP'; 33 } 34 35 if (!isset($_SESSION['sessid'])) { 36 header("Location: login.php"); 37 exit(0); 38 } 39 $SESSID_USERNAME = $_SESSION['sessid']['username']; 40 return $SESSID_USERNAME; 41} 42 43/** 44 * Returns the type of user - either 'user' or 'admin' 45 * Returns false if neither (E.g. if not logged in) 46 * @return string|bool admin or user or (boolean) false. 47 */ 48function authentication_get_usertype() { 49 if (isset($_SESSION['sessid'])) { 50 if (isset($_SESSION['sessid']['type'])) { 51 return $_SESSION['sessid']['type']; 52 } 53 } 54 return false; 55} 56/** 57 * 58 * Used to determine whether a user has a particular role. 59 * @param string $role role-name. (E.g. admin, global-admin or user) 60 * @return boolean True if they have the requested role in their session. 61 * Note, user < admin < global-admin 62 */ 63function authentication_has_role($role) { 64 if (isset($_SESSION['sessid'])) { 65 if (isset($_SESSION['sessid']['roles'])) { 66 if (in_array($role, $_SESSION['sessid']['roles'])) { 67 return true; 68 } 69 } 70 } 71 return false; 72} 73 74/** 75 * Used to enforce that $user has a particular role when 76 * viewing a page. 77 * If they are lacking a role, redirect them to login.php 78 * 79 * Note, user < admin < global-admin 80 * @param string $role 81 * @return bool 82 */ 83function authentication_require_role($role) { 84 // redirect to appropriate page? 85 if (authentication_has_role($role)) { 86 return true; 87 } 88 89 header("Location: login.php"); 90 exit(0); 91} 92 93/** 94 * Initialize a user or admin session 95 * 96 * @param String $username the user or admin name 97 * @param boolean $is_admin true if the user is an admin, false otherwise 98 * @return boolean true on success 99 */ 100function init_session($username, $is_admin = false) { 101 $status = session_regenerate_id(true); 102 $_SESSION['sessid'] = array(); 103 $_SESSION['sessid']['roles'] = array(); 104 $_SESSION['sessid']['roles'][] = $is_admin ? 'admin' : 'user'; 105 $_SESSION['sessid']['username'] = $username; 106 107 $_SESSION['PFA_token'] = md5(random_bytes(8) . uniqid('pfa', true)); 108 109 return $status; 110} 111 112/** 113 * Add an error message for display on the next page that is rendered. 114 * @param string|array $string message(s) to show. 115 * 116 * Stores string in session. Flushed through header template. 117 * @see _flash_string() 118 * @return void 119 */ 120function flash_error($string) { 121 _flash_string('error', $string); 122} 123 124/** 125 * Used to display an info message on successful update. 126 * @param string|array $string message(s) to show. 127 * Stores data in session. 128 * @see _flash_string() 129 * @return void 130 */ 131function flash_info($string) { 132 _flash_string('info', $string); 133} 134/** 135 * 'Private' method used for flash_info() and flash_error(). 136 * @param string $type 137 * @param array|string $string 138 * @retrn void 139 */ 140function _flash_string($type, $string) { 141 if (is_array($string)) { 142 foreach ($string as $singlestring) { 143 _flash_string($type, $singlestring); 144 } 145 return; 146 } 147 148 if (!isset($_SESSION['flash'])) { 149 $_SESSION['flash'] = array(); 150 } 151 if (!isset($_SESSION['flash'][$type])) { 152 $_SESSION['flash'][$type] = array(); 153 } 154 $_SESSION['flash'][$type][] = $string; 155} 156 157/** 158 * @param bool $use_post - set to 0 if $_POST should NOT be read 159 * @return string e.g en 160 * Try to figure out what language the user wants based on browser / cookie 161 */ 162function check_language($use_post = true) { 163 global $supported_languages; # from languages/languages.php 164 165 // prefer a $_POST['lang'] if present 166 if ($use_post && safepost('lang')) { 167 $lang = safepost('lang'); 168 if (is_string($lang) && array_key_exists($lang, $supported_languages)) { 169 return $lang; 170 } 171 } 172 173 // Failing that, is there a $_COOKIE['lang'] ? 174 if (safecookie('lang')) { 175 $lang = safecookie('lang'); 176 if (is_string($lang) && array_key_exists($lang, $supported_languages)) { 177 return $lang; 178 } 179 } 180 181 $lang = Config::read_string('default_language'); 182 183 // If not, did the browser give us any hint(s)? 184 if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 185 $lang_array = preg_split('/(\s*,\s*)/', $_SERVER['HTTP_ACCEPT_LANGUAGE']); 186 foreach ($lang_array as $value) { 187 $lang_next = strtolower(trim($value)); 188 $lang_next = preg_replace('/;.*$/', '', $lang_next); # remove things like ";q=0.8" 189 if (array_key_exists($lang_next, $supported_languages)) { 190 return $lang_next; 191 } 192 } 193 } 194 return $lang; 195} 196 197/** 198 * Action: returns a language selector dropdown with the browser (or cookie) language preselected 199 * @return string 200 * 201 * 202 */ 203function language_selector() { 204 global $supported_languages; # from languages/languages.php 205 206 $current_lang = check_language(); 207 208 $selector = '<select name="lang" xml:lang="en" dir="ltr">'; 209 210 foreach ($supported_languages as $lang => $lang_name) { 211 if ($lang == $current_lang) { 212 $selected = ' selected="selected"'; 213 } else { 214 $selected = ''; 215 } 216 $selector .= "<option value='$lang'$selected>$lang_name</option>"; 217 } 218 $selector .= "</select>"; 219 return $selector; 220} 221 222 223 224 225/** 226 * Checks if a domain is valid 227 * @param string $domain 228 * @return string empty if the domain is valid, otherwise string with the errormessage 229 * 230 * @todo make check_domain able to handle as example .local domains 231 * @todo skip DNS check if the domain exists in PostfixAdmin? 232 */ 233function check_domain($domain) { 234 if (!preg_match('/^([-0-9A-Z]+\.)+' . '([-0-9A-Z]){1,13}$/i', ($domain))) { 235 return sprintf(Config::lang('pInvalidDomainRegex'), htmlentities($domain)); 236 } 237 238 if (Config::bool('emailcheck_resolve_domain') && 'WINDOWS'!=(strtoupper(substr(php_uname('s'), 0, 7)))) { 239 240 // Look for an AAAA, A, or MX record for the domain 241 242 if (function_exists('checkdnsrr')) { 243 $start = microtime(true); # check for slow nameservers, part 1 244 245 // AAAA (IPv6) is only available in PHP v. >= 5 246 if (version_compare(phpversion(), "5.0.0", ">=") && checkdnsrr($domain, 'AAAA')) { 247 $retval = ''; 248 } elseif (checkdnsrr($domain, 'A')) { 249 $retval = ''; 250 } elseif (checkdnsrr($domain, 'MX')) { 251 $retval = ''; 252 } elseif (checkdnsrr($domain, 'NS')) { 253 error_log("DNS is not correctly configured for $domain to send or receive email"); 254 $retval = ''; 255 } else { 256 $retval = sprintf(Config::lang('pInvalidDomainDNS'), htmlentities($domain)); 257 } 258 259 $end = microtime(true); # check for slow nameservers, part 2 260 $time_needed = $end - $start; 261 if ($time_needed > 2) { 262 error_log("Warning: slow nameserver - lookup for $domain took $time_needed seconds"); 263 } 264 265 return $retval; 266 } else { 267 return 'emailcheck_resolve_domain is enabled, but function (checkdnsrr) missing!'; 268 } 269 } 270 271 return ''; 272} 273 274/** 275 * Get password expiration value for a domain 276 * @param string $domain - a string that may be a domain 277 * @return int password expiration value for this domain (DAYS, or zero if not enabled) 278 */ 279function get_password_expiration_value($domain) { 280 $table_domain = table_by_key('domain'); 281 $query = "SELECT password_expiry FROM $table_domain WHERE domain= :domain"; 282 283 $result = db_query_one($query, array('domain' => $domain)); 284 if (is_array($result) && isset($result['password_expiry'])) { 285 return $result['password_expiry']; 286 } 287 return 0; 288} 289 290/** 291 * check_email 292 * Checks if an email is valid - if it is, return true, else false. 293 * @todo make check_email able to handle already added domains 294 * @param string $email - a string that may be an email address. 295 * @return string empty if it's a valid email address, otherwise string with the errormessage 296 */ 297function check_email($email) { 298 $ce_email=$email; 299 300 //strip the vacation domain out if we are using it 301 //and change from blah#foo.com@autoreply.foo.com to blah@foo.com 302 if (Config::bool('vacation')) { 303 $vacation_domain = Config::read_string('vacation_domain'); 304 $ce_email = preg_replace("/@$vacation_domain\$/", '', $ce_email); 305 $ce_email = preg_replace("/#/", '@', $ce_email); 306 } 307 308 // Perform non-domain-part sanity checks 309 if (!preg_match('/^[-!#$%&\'*+\\.\/0-9=?A-Z^_{|}~]+' . '@' . '[^@]+$/i', $ce_email)) { 310 return "" . Config::lang_f('pInvalidMailRegex', $email); 311 } 312 313 if (function_exists('filter_var')) { 314 $check = filter_var($email, FILTER_VALIDATE_EMAIL); 315 if (!$check) { 316 return "" . Config::lang_f('pInvalidMailRegex', $email); 317 } 318 } 319 // Determine domain name 320 $matches = array(); 321 if (preg_match('|@(.+)$|', $ce_email, $matches)) { 322 $domain=$matches[1]; 323 # check domain name 324 return "" . check_domain($domain); 325 } 326 327 return "" . Config::lang_f('pInvalidMailRegex', $email); 328} 329 330 331 332/** 333 * Clean a string, escaping any meta characters that could be 334 * used to disrupt an SQL string. The method of the escaping is dependent on the underlying DB 335 * and MAY NOT be just \' ing. (e.g. sqlite and PgSQL change "it's" to "it''s". 336 * 337 * The PDO quote function surrounds what you pass in with quote marks; for legacy reasons we remove these, 338 * but assume the caller will actually add them back in (!). 339 * 340 * e.g. caller code looks like : 341 * 342 * <code> 343 * $sql = "SELECT * FROM foo WHERE x = '" . escape_string('fish') . "'"; 344 * </code> 345 * 346 * @param int|string $string_or_int parameters to escape 347 * @return string cleaned data, suitable for use within an SQL statement. 348 */ 349function escape_string($string_or_int) { 350 $link = db_connect(); 351 $string_or_int = (string) $string_or_int; 352 $quoted = $link->quote($string_or_int); 353 return trim($quoted, "'"); 354} 355 356 357/** 358 * safeget 359 * Action: get value from $_GET[$param], or $default if $_GET[$param] is not set 360 * Call: $param = safeget('param') # replaces $param = $_GET['param'] 361 * - or - 362 * $param = safeget('param', 'default') 363 * 364 * @param string $param parameter name. 365 * @param string $default (optional) - default value if key is not set. 366 * @return string 367 */ 368function safeget($param, $default = "") { 369 $retval = $default; 370 if (isset($_GET[$param]) && is_string($_GET[$param])) { 371 $retval = $_GET[$param]; 372 } 373 return $retval; 374} 375 376/** 377 * safepost - similar to safeget() but for $_POST 378 * @see safeget() 379 * @param string $param parameter name 380 * @param string $default (optional) default value (defaults to "") 381 * @return string - value in $_POST[$param] or $default 382 */ 383function safepost($param, $default = "") { 384 $retval = $default; 385 if (isset($_POST[$param]) && is_string($_POST[$param])) { 386 $retval = $_POST[$param]; 387 } 388 return $retval; 389} 390 391/** 392 * safeserver 393 * @see safeget() 394 * @param string $param 395 * @param string $default (optional) 396 * @return string value from $_SERVER[$param] or $default 397 */ 398function safeserver($param, $default = "") { 399 $retval = $default; 400 if (isset($_SERVER[$param])) { 401 $retval = $_SERVER[$param]; 402 } 403 return $retval; 404} 405 406/** 407 * safecookie 408 * @see safeget() 409 * @param string $param 410 * @param string $default (optional) 411 * @return string value from $_COOKIE[$param] or $default 412 */ 413function safecookie($param, $default = "") { 414 $retval = $default; 415 if (isset($_COOKIE[$param]) && is_string($_COOKIE[$param])) { 416 $retval = $_COOKIE[$param]; 417 } 418 return $retval; 419} 420 421/** 422 * safesession 423 * @see safeget() 424 * @param string $param 425 * @param string $default (optional) 426 * @return string value from $_SESSION[$param] or $default 427 */ 428function safesession($param, $default = "") { 429 $retval = $default; 430 if (isset($_SESSION[$param]) && is_string($_SESSION[$param])) { 431 $retval = $_SESSION[$param]; 432 } 433 return $retval; 434} 435 436 437/** 438 * pacol 439 * @param int $allow_editing 440 * @param int $display_in_form 441 * @param int display_in_list 442 * @param string $type 443 * @param string PALANG_label 444 * @param string PALANG_desc 445 * @param any optional $default 446 * @param array $options optional options 447 * @param int or $not_in_db - if array, can contain the remaining parameters as associated array. Otherwise counts as $not_in_db 448 * @return array for $struct 449 */ 450function pacol($allow_editing, $display_in_form, $display_in_list, $type, $PALANG_label, $PALANG_desc, $default = "", $options = array(), $multiopt=0, $dont_write_to_db=0, $select="", $extrafrom="", $linkto="") { 451 if ($PALANG_label != '') { 452 $PALANG_label = Config::lang($PALANG_label); 453 } 454 if ($PALANG_desc != '') { 455 $PALANG_desc = Config::lang($PALANG_desc); 456 } 457 458 if (is_array($multiopt)) { # remaining parameters provided in named array 459 $not_in_db = 0; # keep default value 460 foreach ($multiopt as $key => $value) { 461 $$key = $value; # extract everything to the matching variable 462 } 463 } else { 464 $not_in_db = $multiopt; 465 } 466 467 return array( 468 'editable' => $allow_editing, 469 'display_in_form' => $display_in_form, 470 'display_in_list' => $display_in_list, 471 'type' => $type, 472 'label' => $PALANG_label, # $PALANG field label 473 'desc' => $PALANG_desc, # $PALANG field description 474 'default' => $default, 475 'options' => $options, 476 'not_in_db' => $not_in_db, 477 'dont_write_to_db' => $dont_write_to_db, 478 'select' => $select, # replaces the field name after SELECT 479 'extrafrom' => $extrafrom, # added after FROM xy - useful for JOINs etc. 480 'linkto' => $linkto, # make the value a link - %s will be replaced with the ID 481 ); 482} 483 484/** 485 * Action: Get all the properties of a domain. 486 * @param string $domain 487 * @return array 488 */ 489function get_domain_properties($domain) { 490 $handler = new DomainHandler(); 491 if (!$handler->init($domain)) { 492 throw new Exception("Error: " . join("\n", $handler->errormsg)); 493 } 494 495 if (!$handler->view()) { 496 throw new Exception("Error: " . join("\n", $handler->errormsg)); 497 } 498 499 $result = $handler->result(); 500 return $result; 501} 502 503 504/** 505 * create_page_browser 506 * Action: Get page browser for a long list of mailboxes, aliases etc. 507 * 508 * @param string $idxfield - database field name to use as title e.g. alias.address 509 * @param string $querypart - core part of the query (starting at "FROM") e.g. FROM alias WHERE address like ... 510 * @return array 511 */ 512function create_page_browser($idxfield, $querypart, $sql_params = []) { 513 global $CONF; 514 $page_size = (int) $CONF['page_size']; 515 $label_len = 2; 516 $pagebrowser = array(); 517 518 $count_results = 0; 519 520 if ($page_size < 2) { # will break the page browser 521 throw new Exception('$CONF[\'page_size\'] must be 2 or more!'); 522 } 523 524 # get number of rows 525 $query = "SELECT count(*) as counter FROM (SELECT $idxfield $querypart) AS tmp"; 526 $result = db_query_one($query, $sql_params); 527 if ($result && isset($result['counter'])) { 528 $count_results = $result['counter'] -1; # we start counting at 0, not 1 529 } 530 531 if ($count_results < $page_size) { 532 return array(); # only one page - no pagebrowser required 533 } 534 535 # init row counter 536 $initcount = "SET @r=-1"; 537 if (db_pgsql()) { 538 $initcount = "CREATE TEMPORARY SEQUENCE rowcount MINVALUE 0"; 539 } 540 if (!db_sqlite()) { 541 db_execute($initcount); 542 } 543 544 # get labels for relevant rows (first and last of each page) 545 $page_size_zerobase = $page_size - 1; 546 $query = " 547 SELECT * FROM ( 548 SELECT $idxfield AS label, @r := @r + 1 AS 'r' $querypart 549 ) idx WHERE MOD(idx.r, $page_size) IN (0,$page_size_zerobase) OR idx.r = $count_results 550 "; 551 552 if (db_pgsql()) { 553 $query = " 554 SELECT * FROM ( 555 SELECT $idxfield AS label, nextval('rowcount') AS r $querypart 556 ) idx WHERE MOD(idx.r, $page_size) IN (0,$page_size_zerobase) OR idx.r = $count_results 557 "; 558 } 559 560 if (db_sqlite()) { 561 $end = $idxfield; 562 if (strpos($idxfield, '.') !== false) { 563 $bits = explode('.', $idxfield); 564 $end = $bits[1]; 565 } 566 $query = " 567 WITH idx AS (SELECT * $querypart) 568 SELECT $end AS label, (SELECT (COUNT(*) - 1) FROM idx t1 WHERE t1.$end <= t2.$end ) AS r 569 FROM idx t2 570 WHERE (r % $page_size) IN (0,$page_size_zerobase) OR r = $count_results"; 571 } 572 573 # PostgreSQL: 574 # http://www.postgresql.org/docs/8.1/static/sql-createsequence.html 575 # http://www.postgresonline.com/journal/archives/79-Simulating-Row-Number-in-PostgreSQL-Pre-8.4.html 576 # http://www.pg-forum.de/sql/1518-nummerierung-der-abfrageergebnisse.html 577 # CREATE TEMPORARY SEQUENCE foo MINVALUE 0 MAXVALUE $page_size_zerobase CYCLE 578 # afterwards: DROP SEQUENCE foo 579 580 $result = db_query_all($query, $sql_params); 581 for ($k = 0; $k < count($result); $k+=2) { 582 if (isset($result[$k + 1])) { 583 $label = substr($result[$k]['label'], 0, $label_len) . '-' . substr($result[$k+1]['label'], 0, $label_len); 584 } else { 585 $label = substr($result[$k]['label'], 0, $label_len); 586 } 587 $pagebrowser[] = $label; 588 } 589 590 if (db_pgsql()) { 591 db_execute("DROP SEQUENCE rowcount"); 592 } 593 594 return $pagebrowser; 595} 596 597 598/** 599 * Recalculates the quota from MBs to bytes (divide, /) 600 * @param int $quota 601 * @return float 602 */ 603function divide_quota($quota) { 604 if ($quota == -1) { 605 return $quota; 606 } 607 $value = round($quota / (int) Config::read_string('quota_multiplier'), 2); 608 return $value; 609} 610 611 612/** 613 * Checks if the admin is the owner of the domain (or global-admin) 614 * @param string $username 615 * @param string $domain 616 * @return bool 617 */ 618function check_owner($username, $domain) { 619 $table_domain_admins = table_by_key('domain_admins'); 620 621 $result = db_query_all( 622 "SELECT 1 FROM $table_domain_admins WHERE username= ? AND (domain = ? OR domain = 'ALL') AND active = ?" , 623 array($username, $domain, db_get_boolean(true)) 624 ); 625 626 if (sizeof($result) == 1 || sizeof($result) == 2) { # "ALL" + specific domain permissions is possible 627 # TODO: if superadmin, check if given domain exists in the database 628 return true; 629 } else { 630 if (sizeof($result) > 2) { # more than 2 results means something really strange happened... 631 flash_error("Permission check returned multiple results. Please go to 'edit admin' for your username and press the save " 632 . "button once to fix the database. If this doesn't help, open a bugreport."); 633 } 634 return false; 635 } 636} 637 638 639 640/** 641 * List domains for an admin user. 642 * @param string $username 643 * @return array of domain names. 644 */ 645function list_domains_for_admin($username) { 646 $table_domain = table_by_key('domain'); 647 $table_domain_admins = table_by_key('domain_admins'); 648 649 $condition = array(); 650 651 $E_username = escape_string($username); 652 653 $query = "SELECT $table_domain.domain FROM $table_domain "; 654 $condition[] = "$table_domain.domain != 'ALL'"; 655 656 $pvalues = array(); 657 658 $result = db_query_one("SELECT username FROM $table_domain_admins WHERE username= :username AND domain='ALL'", array('username' => $username)); 659 if (empty($result)) { # not a superadmin 660 $pvalues['username'] = $username; 661 $pvalues['active'] = db_get_boolean(true); 662 $pvalues['backupmx'] = db_get_boolean(false); 663 664 $query .= " LEFT JOIN $table_domain_admins ON $table_domain.domain=$table_domain_admins.domain "; 665 $condition[] = "$table_domain_admins.username = :username "; 666 $condition[] = "$table_domain.active = :active "; # TODO: does it really make sense to exclude inactive... 667 $condition[] = "$table_domain.backupmx = :backupmx" ; # TODO: ... and backupmx domains for non-superadmins? 668 } 669 670 $query .= " WHERE " . join(' AND ', $condition); 671 $query .= " ORDER BY $table_domain.domain"; 672 673 $result = db_query_all($query, $pvalues); 674 675 return array_column($result, 'domain'); 676} 677 678/** 679 * List all available domains. 680 * 681 * @return array 682 */ 683function list_domains() { 684 $list = array(); 685 686 $table_domain = table_by_key('domain'); 687 $result = db_query_all("SELECT domain FROM $table_domain WHERE domain!='ALL' ORDER BY domain"); 688 $i = 0; 689 foreach ($result as $row) { 690 $list[$i] = $row['domain']; 691 $i++; 692 } 693 return $list; 694} 695 696 697 698 699// 700// list_admins 701// Action: Lists all the admins 702// Call: list_admins () 703// 704// was admin_list_admins 705// 706function list_admins() { 707 $handler = new AdminHandler(); 708 709 $handler->getList(''); 710 711 return $handler->result(); 712} 713 714 715 716// 717// encode_header 718// Action: Encode a string according to RFC 1522 for use in headers if it contains 8-bit characters. 719// Call: encode_header (string header, string charset) 720// 721function encode_header($string, $default_charset = "utf-8") { 722 if (strtolower($default_charset) == 'iso-8859-1') { 723 $string = str_replace("\240", ' ', $string); 724 } 725 726 $j = strlen($string); 727 $max_l = 75 - strlen($default_charset) - 7; 728 $aRet = array(); 729 $ret = ''; 730 $iEncStart = $enc_init = false; 731 $cur_l = $iOffset = 0; 732 733 for ($i = 0; $i < $j; ++$i) { 734 switch ($string[$i]) { 735 case '=': 736 case '<': 737 case '>': 738 case ',': 739 case '?': 740 case '_': 741 if ($iEncStart === false) { 742 $iEncStart = $i; 743 } 744 $cur_l+=3; 745 if ($cur_l > ($max_l-2)) { 746 $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); 747 $aRet[] = "=?$default_charset?Q?$ret?="; 748 $iOffset = $i; 749 $cur_l = 0; 750 $ret = ''; 751 $iEncStart = false; 752 } else { 753 $ret .= sprintf("=%02X", ord($string[$i])); 754 } 755 break; 756 case '(': 757 case ')': 758 if ($iEncStart !== false) { 759 $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); 760 $aRet[] = "=?$default_charset?Q?$ret?="; 761 $iOffset = $i; 762 $cur_l = 0; 763 $ret = ''; 764 $iEncStart = false; 765 } 766 break; 767 case ' ': 768 if ($iEncStart !== false) { 769 $cur_l++; 770 if ($cur_l > $max_l) { 771 $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); 772 $aRet[] = "=?$default_charset?Q?$ret?="; 773 $iOffset = $i; 774 $cur_l = 0; 775 $ret = ''; 776 $iEncStart = false; 777 } else { 778 $ret .= '_'; 779 } 780 } 781 break; 782 default: 783 $k = ord($string[$i]); 784 if ($k > 126) { 785 if ($iEncStart === false) { 786 // do not start encoding in the middle of a string, also take the rest of the word. 787 $sLeadString = substr($string, 0, $i); 788 $aLeadString = explode(' ', $sLeadString); 789 $sToBeEncoded = array_pop($aLeadString); 790 $iEncStart = $i - strlen($sToBeEncoded); 791 $ret .= $sToBeEncoded; 792 $cur_l += strlen($sToBeEncoded); 793 } 794 $cur_l += 3; 795 // first we add the encoded string that reached it's max size 796 if ($cur_l > ($max_l-2)) { 797 $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); 798 $aRet[] = "=?$default_charset?Q?$ret?= "; 799 $cur_l = 3; 800 $ret = ''; 801 $iOffset = $i; 802 $iEncStart = $i; 803 } 804 $enc_init = true; 805 $ret .= sprintf("=%02X", $k); 806 } else { 807 if ($iEncStart !== false) { 808 $cur_l++; 809 if ($cur_l > $max_l) { 810 $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); 811 $aRet[] = "=?$default_charset?Q?$ret?="; 812 $iEncStart = false; 813 $iOffset = $i; 814 $cur_l = 0; 815 $ret = ''; 816 } else { 817 $ret .= $string[$i]; 818 } 819 } 820 } 821 break; 822 # end switch 823 } 824 } 825 if ($enc_init) { 826 if ($iEncStart !== false) { 827 $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); 828 $aRet[] = "=?$default_charset?Q?$ret?="; 829 } else { 830 $aRet[] = substr($string, $iOffset); 831 } 832 $string = implode('', $aRet); 833 } 834 return $string; 835} 836 837 838 839/** 840 * Generate a random password of $length characters. 841 * @param int $length (optional, default: 12) 842 * @return string 843 * 844 */ 845function generate_password($length = 12) { 846 847 // define possible characters 848 $possible = "2345678923456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"; # skip 0 and 1 to avoid confusion with O and l 849 850 // add random characters to $password until $length is reached 851 $password = ""; 852 while (strlen($password) < $length) { 853 $random = random_int(0, strlen($possible) -1); 854 $char = substr($possible, $random, 1); 855 856 // we don't want this character if it's already in the password 857 if (!strstr($password, $char)) { 858 $password .= $char; 859 } 860 } 861 862 return $password; 863} 864 865 866 867/** 868 * Check if a password is strong enough based on the conditions in $CONF['password_validation'] 869 * @param string $password 870 * @return array of error messages, or empty array if the password is ok 871 */ 872function validate_password($password) { 873 $result = array(); 874 $val_conf = Config::read_array('password_validation'); 875 876 if (Config::has('min_password_length')) { 877 $minlen = (int)Config::read_string('min_password_length'); # used up to 2.3.x - check it for backward compatibility 878 if ($minlen > 0) { 879 $val_conf['/.{' . $minlen . '}/'] = "password_too_short $minlen"; 880 } 881 } 882 883 foreach ($val_conf as $regex => $message) { 884 if (is_callable($message)) { 885 $ret = $message($password); 886 if (!empty($ret)) { 887 $result[] = $ret; 888 } 889 continue; 890 } 891 892 if (!preg_match($regex, $password)) { 893 $msgparts = preg_split("/ /", $message, 2); 894 if (count($msgparts) == 1) { 895 $result[] = Config::lang($msgparts[0]); 896 } else { 897 $result[] = sprintf(Config::lang($msgparts[0]), $msgparts[1]); 898 } 899 } 900 } 901 902 return $result; 903} 904 905/** 906 * @param string $pw 907 * @param string $pw_db - encrypted hash 908 * @return string crypt'ed password, should equal $pw_db if $pw matches the original 909 */ 910function _pacrypt_md5crypt($pw, $pw_db = '') { 911 if ($pw_db) { 912 $split_salt = preg_split('/\$/', $pw_db); 913 if (isset($split_salt[2])) { 914 $salt = $split_salt[2]; 915 return md5crypt($pw, $salt); 916 } 917 } 918 919 return md5crypt($pw); 920} 921 922/** 923 * @todo fix this to not throw an E_NOTICE or deprecate/remove. 924 */ 925function _pacrypt_crypt($pw, $pw_db = '') { 926 if ($pw_db) { 927 return crypt($pw, $pw_db); 928 } 929 // Throws E_NOTICE as salt is not specified. 930 return crypt($pw); 931} 932 933/** 934 * Crypt with MySQL's ENCRYPT function 935 * 936 * @param string $pw 937 * @param string $pw_db (hashed password) 938 * @return string if $pw_db and the return value match then $pw matches the original password. 939 */ 940function _pacrypt_mysql_encrypt($pw, $pw_db = '') { 941 // See https://sourceforge.net/tracker/?func=detail&atid=937966&aid=1793352&group_id=191583 942 // this is apparently useful for pam_mysql etc. 943 944 if ( $pw_db ) { 945 $res = db_query_one("SELECT ENCRYPT(:pw,:pw_db) as result", ['pw' => $pw, 'pw_db' => $pw_db]); 946 } else { 947 // see https://security.stackexchange.com/questions/150687/is-it-safe-to-use-the-encrypt-function-in-mysql-to-hash-passwords 948 // if no existing password, use a random SHA512 salt. 949 $salt = _php_crypt_generate_crypt_salt(); 950 $res= db_query_one("SELECT ENCRYPT(:pw, CONCAT('$6$', '$salt')) as result", ['pw' => $pw]); 951 } 952 953 return $res['result']; 954} 955 956/** 957 * Create/Validate courier authlib style crypt'ed passwords. (md5, md5raw, crypt, sha1) 958 * 959 * @param string $pw 960 * @param string $pw_db (optional) 961 * @return string crypted password - contains {xxx} prefix to identify mechanism. 962 */ 963function _pacrypt_authlib($pw, $pw_db) { 964 global $CONF; 965 $flavor = $CONF['authlib_default_flavor']; 966 $salt = substr(create_salt(), 0, 2); # courier-authlib supports only two-character salts 967 if (preg_match('/^{.*}/', $pw_db)) { 968 // we have a flavor in the db -> use it instead of default flavor 969 $result = preg_split('/[{}]/', $pw_db, 3); # split at { and/or } 970 $flavor = $result[1]; 971 $salt = substr($result[2], 0, 2); 972 } 973 974 if (stripos($flavor, 'md5raw') === 0) { 975 $password = '{' . $flavor . '}' . md5($pw); 976 } elseif (stripos($flavor, 'md5') === 0) { 977 $password = '{' . $flavor . '}' . base64_encode(md5($pw, true)); 978 } elseif (stripos($flavor, 'crypt') === 0) { 979 $password = '{' . $flavor . '}' . crypt($pw, $salt); 980 } elseif (stripos($flavor, 'SHA') === 0) { 981 $password = '{' . $flavor . '}' . base64_encode(sha1($pw, true)); 982 } else { 983 throw new Exception("authlib_default_flavor '" . $flavor . "' unknown. Valid flavors are 'md5raw', 'md5', 'SHA' and 'crypt'"); 984 } 985 return $password; 986} 987 988/** 989 * Uses the doveadm pw command, crypted passwords have a {...} prefix to identify type. 990 * 991 * @param string $pw - plain text password 992 * @param string $pw_db - encrypted password, or '' for generation. 993 * @return string crypted password 994 */ 995function _pacrypt_dovecot($pw, $pw_db = '') { 996 global $CONF; 997 998 $split_method = preg_split('/:/', $CONF['encrypt']); 999 $method = strtoupper($split_method[1]); 1000 # If $pw_db starts with {method}, change $method accordingly 1001 if (!empty($pw_db) && preg_match('/^\{([A-Z0-9.-]+)\}.+/', $pw_db, $method_matches)) { 1002 $method = $method_matches[1]; 1003 } 1004 if (! preg_match("/^[A-Z0-9.-]+$/", $method)) { 1005 throw new Exception("invalid dovecot encryption method"); 1006 } 1007 1008 # digest-md5 hashes include the username - until someone implements it, let's declare it as unsupported 1009 if (strtolower($method) == 'digest-md5') { 1010 throw new Exception("Sorry, \$CONF['encrypt'] = 'dovecot:digest-md5' is not supported by PostfixAdmin."); 1011 } 1012 # TODO: add -u option for those hashes, or for everything that is salted (-u was available before dovecot 2.1 -> no problem with backward compatibility ) 1013 1014 $dovecotpw = "doveadm pw"; 1015 if (!empty($CONF['dovecotpw'])) { 1016 $dovecotpw = $CONF['dovecotpw']; 1017 } 1018 1019 # Use proc_open call to avoid safe_mode problems and to prevent showing plain password in process table 1020 $spec = array( 1021 0 => array("pipe", "r"), // stdin 1022 1 => array("pipe", "w"), // stdout 1023 2 => array("pipe", "w"), // stderr 1024 ); 1025 1026 $nonsaltedtypes = "SHA|SHA1|SHA256|SHA512|CLEAR|CLEARTEXT|PLAIN|PLAIN-TRUNC|CRAM-MD5|HMAC-MD5|PLAIN-MD4|PLAIN-MD5|LDAP-MD5|LANMAN|NTLM|RPA"; 1027 $salted = ! preg_match("/^($nonsaltedtypes)(\.B64|\.BASE64|\.HEX)?$/", strtoupper($method)); 1028 1029 $dovepasstest = ''; 1030 if ($salted && (!empty($pw_db))) { 1031 # only use -t for salted passwords to be backward compatible with dovecot < 2.1 1032 $dovepasstest = " -t " . escapeshellarg($pw_db); 1033 } 1034 1035 $pipes = []; 1036 1037 $pipe = proc_open("$dovecotpw '-s' $method$dovepasstest", $spec, $pipes); 1038 1039 if (!$pipe) { 1040 throw new Exception("can't proc_open $dovecotpw"); 1041 } 1042 1043 // use dovecot's stdin, it uses getpass() twice (except when using -t) 1044 // Write pass in pipe stdin 1045 if (empty($dovepasstest)) { 1046 fwrite($pipes[0], $pw . "\n", 1+strlen($pw)); 1047 usleep(1000); 1048 } 1049 1050 fwrite($pipes[0], $pw . "\n", 1+strlen($pw)); 1051 fclose($pipes[0]); 1052 1053 $stderr_output = stream_get_contents($pipes[2]); 1054 1055 // Read hash from pipe stdout 1056 $password = fread($pipes[1], 200); 1057 1058 if (!empty($stderr_output) || empty($password)) { 1059 error_log("Failed to read password from $dovecotpw ... stderr: $stderr_output, password: $password "); 1060 throw new Exception("$dovecotpw failed, see error log for details"); 1061 } 1062 1063 if (empty($dovepasstest)) { 1064 if (!preg_match('/^\{' . $method . '\}/', $password)) { 1065 error_log("dovecotpw password encryption failed (method: $method) . stderr: $stderr_output"); 1066 throw new Exception("can't encrypt password with dovecotpw, see error log for details"); 1067 } 1068 } else { 1069 if (!preg_match('(verified)', $password)) { 1070 $password="Thepasswordcannotbeverified"; 1071 } else { 1072 $password = rtrim(str_replace('(verified)', '', $password)); 1073 } 1074 } 1075 1076 fclose($pipes[1]); 1077 fclose($pipes[2]); 1078 proc_close($pipe); 1079 1080 if ((!empty($pw_db)) && (substr($pw_db, 0, 1) != '{')) { 1081 # for backward compability with "old" dovecot passwords that don't have the {method} prefix 1082 $password = str_replace('{' . $method . '}', '', $password); 1083 } 1084 1085 return rtrim($password); 1086} 1087 1088/** 1089 * Supports DES, MD5, BLOWFISH, SHA256, SHA512 methods. 1090 * 1091 * Via config we support an optional prefix (e.g. if you need hashes to start with {SHA256-CRYPT} and optional rounds (hardness) setting. 1092 * 1093 * @param string $pw 1094 * @param string $pw_db (can be empty if setting a new password) 1095 * @return string crypt'ed password; if it matches $pw_db then $pw is the original password. 1096 */ 1097function _pacrypt_php_crypt($pw, $pw_db) { 1098 $configEncrypt = Config::read_string('encrypt'); 1099 1100 // use PHPs crypt(), which uses the system's crypt() 1101 // same algorithms as used in /etc/shadow 1102 // you can have mixed hash types in the database for authentication, changed passwords get specified hash type 1103 // the algorithm for a new hash is chosen by feeding a salt with correct magic to crypt() 1104 // set $CONF['encrypt'] to 'php_crypt' to use the default SHA512 crypt method 1105 // set $CONF['encrypt'] to 'php_crypt:METHOD' to use another method; methods supported: DES, MD5, BLOWFISH, SHA256, SHA512 1106 // set $CONF['encrypt'] to 'php_crypt:METHOD:difficulty' where difficulty is between 1000-999999999 1107 // set $CONF['encrypt'] to 'php_crypt:METHOD:difficulty:PREFIX' to prefix the hash with the {PREFIX} etc. 1108 // tested on linux 1109 1110 $prefix = ''; 1111 1112 if (strlen($pw_db) > 0) { 1113 // existing pw provided. send entire password hash as salt for crypt() to figure out 1114 $salt = $pw_db; 1115 1116 // if there was a prefix in the password, use this (override anything given in the config). 1117 1118 if (preg_match('/^\{([-A-Z0-9]+)\}(.+)$/', $pw_db, $method_matches)) { 1119 $salt = $method_matches[2]; 1120 $prefix = "{" . $method_matches[1] . "}"; 1121 } 1122 } else { 1123 $salt_method = 'SHA512'; // hopefully a reasonable default (better than MD5) 1124 $hash_difficulty = ''; 1125 // no pw provided. create new password hash 1126 if (strpos($configEncrypt, ':') !== false) { 1127 // use specified hash method 1128 $spec = explode(':', $configEncrypt); 1129 $salt_method = $spec[1]; 1130 if (isset($spec[2])) { 1131 $hash_difficulty = $spec[2]; 1132 } 1133 if (isset($spec[3])) { 1134 $prefix = $spec[3]; // hopefully something like {SHA256-CRYPT} 1135 } 1136 } 1137 // create appropriate salt for selected hash method 1138 $salt = _php_crypt_generate_crypt_salt($salt_method, $hash_difficulty); 1139 } 1140 1141 $password = crypt($pw, $salt); 1142 1143 return "{$prefix}{$password}"; 1144} 1145 1146 1147/** 1148 * @param string $hash_type must be one of: MD5, DES, BLOWFISH, SHA256 or SHA512 (default) 1149 * @param int hash difficulty 1150 * @return string 1151 */ 1152function _php_crypt_generate_crypt_salt($hash_type='SHA512', $hash_difficulty=null) { 1153 // generate a salt (with magic matching chosen hash algorithm) for the PHP crypt() function 1154 1155 // most commonly used alphabet 1156 $alphabet = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 1157 1158 switch ($hash_type) { 1159 case 'DES': 1160 $alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 1161 $length = 2; 1162 $salt = _php_crypt_random_string($alphabet, $length); 1163 return $salt; 1164 1165 case 'MD5': 1166 $length = 12; 1167 $algorithm = '1'; 1168 $salt = _php_crypt_random_string($alphabet, $length); 1169 return sprintf('$%s$%s', $algorithm, $salt); 1170 1171 case 'BLOWFISH': 1172 $length = 22; 1173 if (empty($hash_difficulty)) { 1174 $cost = 10; 1175 } else { 1176 $cost = (int)$hash_difficulty; 1177 if ($cost < 4 || $cost > 31) { 1178 throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 4-31'); 1179 } 1180 } 1181 if (version_compare(PHP_VERSION, '5.3.7') >= 0) { 1182 $algorithm = '2y'; // bcrypt, with fixed unicode problem 1183 } else { 1184 $algorithm = '2a'; // bcrypt 1185 } 1186 $salt = _php_crypt_random_string($alphabet, $length); 1187 return sprintf('$%s$%02d$%s', $algorithm, $cost, $salt); 1188 1189 case 'SHA256': 1190 $length = 16; 1191 $algorithm = '5'; 1192 if (empty($hash_difficulty)) { 1193 $rounds = ''; 1194 } else { 1195 $rounds = (int)$hash_difficulty; 1196 if ($rounds < 1000 || $rounds > 999999999) { 1197 throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 1000-999999999'); 1198 } 1199 } 1200 $salt = _php_crypt_random_string($alphabet, $length); 1201 if (!empty($rounds)) { 1202 $rounds = sprintf('rounds=%d$', $rounds); 1203 } 1204 return sprintf('$%s$%s%s', $algorithm, $rounds, $salt); 1205 1206 case 'SHA512': 1207 $length = 16; 1208 $algorithm = '6'; 1209 if (empty($hash_difficulty)) { 1210 $rounds = ''; 1211 } else { 1212 $rounds = (int)$hash_difficulty; 1213 if ($rounds < 1000 || $rounds > 999999999) { 1214 throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 1000-999999999'); 1215 } 1216 } 1217 $salt = _php_crypt_random_string($alphabet, $length); 1218 if (!empty($rounds)) { 1219 $rounds = sprintf('rounds=%d$', $rounds); 1220 } 1221 return sprintf('$%s$%s%s', $algorithm, $rounds, $salt); 1222 1223 default: 1224 throw new Exception("unknown hash type: '$hash_type'"); 1225 } 1226} 1227 1228/** 1229 * Generates a random string of specified $length from $characters. 1230 * @param string $characters 1231 * @param int $length 1232 * @return string of given $length 1233 */ 1234function _php_crypt_random_string($characters, $length) { 1235 $string = ''; 1236 for ($p = 0; $p < $length; $p++) { 1237 $string .= $characters[random_int(0, strlen($characters) -1)]; 1238 } 1239 return $string; 1240} 1241 1242 1243/** 1244 * Encrypt a password, using the apparopriate hashing mechanism as defined in 1245 * config.inc.php ($CONF['encrypt']). 1246 * 1247 * When wanting to compare one pw to another, it's necessary to provide the salt used - hence 1248 * the second parameter ($pw_db), which is the existing hash from the DB. 1249 * 1250 * @param string $pw 1251 * @param string $pw_db optional encrypted password 1252 * @return string encrypted password - if this matches $pw_db then the original password is $pw. 1253 */ 1254function pacrypt($pw, $pw_db="") { 1255 global $CONF; 1256 1257 switch ($CONF['encrypt']) { 1258 case 'md5crypt': 1259 return _pacrypt_md5crypt($pw, $pw_db); 1260 case 'md5': 1261 return md5($pw); 1262 case 'system': 1263 return _pacrypt_crypt($pw, $pw_db); 1264 case 'cleartext': 1265 return $pw; 1266 case 'mysql_encrypt': 1267 return _pacrypt_mysql_encrypt($pw, $pw_db); 1268 case 'authlib': 1269 return _pacrypt_authlib($pw, $pw_db); 1270 case 'sha512.b64': 1271 return _pacrypt_sha512_b64($pw, $pw_db); 1272 } 1273 1274 if (preg_match("/^dovecot:/", $CONF['encrypt'])) { 1275 return _pacrypt_dovecot($pw, $pw_db); 1276 } 1277 1278 if (substr($CONF['encrypt'], 0, 9) === 'php_crypt') { 1279 return _pacrypt_php_crypt($pw, $pw_db); 1280 } 1281 1282 throw new Exception('unknown/invalid $CONF["encrypt"] setting: ' . $CONF['encrypt']); 1283} 1284 1285/** 1286 * @see https://github.com/postfixadmin/postfixadmin/issues/58 1287 */ 1288function _pacrypt_sha512_b64($pw, $pw_db="") { 1289 if (!function_exists('random_bytes') || !function_exists('crypt') || !defined('CRYPT_SHA512') || !function_exists('mb_substr')) { 1290 throw new Exception("sha512.b64 not supported!"); 1291 } 1292 if (!$pw_db) { 1293 $salt = mb_substr(rtrim(base64_encode(random_bytes(16)),'='),0,16,'8bit'); 1294 return '{SHA512-CRYPT.B64}'.base64_encode(crypt($pw,'$6$'.$salt)); 1295 } 1296 1297 1298 $password="#Thepasswordcannotbeverified"; 1299 if (strncmp($pw_db,'{SHA512-CRYPT.B64}',18)==0) { 1300 $dcpwd = base64_decode(mb_substr($pw_db,18,null,'8bit'),true); 1301 if ($dcpwd !== false && !empty($dcpwd) && strncmp($dcpwd,'$6$',3)==0) { 1302 $password = '{SHA512-CRYPT.B64}'.base64_encode(crypt($pw,$dcpwd)); 1303 } 1304 } elseif (strncmp($pw_db,'{MD5-CRYPT}',11)==0) { 1305 $dcpwd = mb_substr($pw_db,11,null,'8bit'); 1306 if (!empty($dcpwd) && strncmp($dcpwd,'$1$',3)==0) { 1307 $password = '{MD5-CRYPT}'.crypt($pw,$dcpwd); 1308 } 1309 } 1310 return $password; 1311} 1312 1313/** 1314 * Creates MD5 based crypt formatted password. 1315 * If salt is not provided we generate one. 1316 * 1317 * @param string $pw plain text password 1318 * @param string $salt (optional) 1319 * @param string $magic (optional) 1320 * @return string hashed password in crypt format. 1321 */ 1322function md5crypt($pw, $salt="", $magic="") { 1323 $MAGIC = "$1$"; 1324 1325 if ($magic == "") { 1326 $magic = $MAGIC; 1327 } 1328 if ($salt == "") { 1329 $salt = create_salt(); 1330 } 1331 $slist = explode("$", $salt); 1332 if ($slist[0] == "1") { 1333 $salt = $slist[1]; 1334 } 1335 1336 $salt = substr($salt, 0, 8); 1337 $ctx = $pw . $magic . $salt; 1338 $final = hex2bin(md5($pw . $salt . $pw)); 1339 1340 for ($i=strlen($pw); $i>0; $i-=16) { 1341 if ($i > 16) { 1342 $ctx .= substr($final, 0, 16); 1343 } else { 1344 $ctx .= substr($final, 0, $i); 1345 } 1346 } 1347 $i = strlen($pw); 1348 1349 while ($i > 0) { 1350 if ($i & 1) { 1351 $ctx .= chr(0); 1352 } else { 1353 $ctx .= $pw[0]; 1354 } 1355 $i = $i >> 1; 1356 } 1357 $final = hex2bin(md5($ctx)); 1358 1359 for ($i=0;$i<1000;$i++) { 1360 $ctx1 = ""; 1361 if ($i & 1) { 1362 $ctx1 .= $pw; 1363 } else { 1364 $ctx1 .= substr($final, 0, 16); 1365 } 1366 if ($i % 3) { 1367 $ctx1 .= $salt; 1368 } 1369 if ($i % 7) { 1370 $ctx1 .= $pw; 1371 } 1372 if ($i & 1) { 1373 $ctx1 .= substr($final, 0, 16); 1374 } else { 1375 $ctx1 .= $pw; 1376 } 1377 $final = hex2bin(md5($ctx1)); 1378 } 1379 $passwd = ""; 1380 $passwd .= to64(((ord($final[0]) << 16) | (ord($final[6]) << 8) | (ord($final[12]))), 4); 1381 $passwd .= to64(((ord($final[1]) << 16) | (ord($final[7]) << 8) | (ord($final[13]))), 4); 1382 $passwd .= to64(((ord($final[2]) << 16) | (ord($final[8]) << 8) | (ord($final[14]))), 4); 1383 $passwd .= to64(((ord($final[3]) << 16) | (ord($final[9]) << 8) | (ord($final[15]))), 4); 1384 $passwd .= to64(((ord($final[4]) << 16) | (ord($final[10]) << 8) | (ord($final[5]))), 4); 1385 $passwd .= to64(ord($final[11]), 2); 1386 return "$magic$salt\$$passwd"; 1387} 1388 1389/** 1390 * @return string - should be random, 8 chars long 1391 */ 1392function create_salt() { 1393 srand((int) microtime()*1000000); 1394 $salt = substr(md5("" . rand(0, 9999999)), 0, 8); 1395 return $salt; 1396} 1397 1398/* 1399 * remove item $item from array $array 1400 */ 1401function remove_from_array($array, $item) { 1402 # array_diff might be faster, but doesn't provide an easy way to know if the value was found or not 1403 # return array_diff($array, array($item)); 1404 $ret = array_search($item, $array); 1405 if ($ret === false) { 1406 $found = 0; 1407 } else { 1408 $found = 1; 1409 unset($array[$ret]); 1410 } 1411 return array($found, $array); 1412} 1413 1414function to64($v, $n) { 1415 $ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 1416 $ret = ""; 1417 while (($n - 1) >= 0) { 1418 $n--; 1419 $ret .= $ITOA64[$v & 0x3f]; 1420 $v = $v >> 6; 1421 } 1422 return $ret; 1423} 1424 1425 1426 1427/** 1428 * smtp_mail 1429 * Action: Send email 1430 * Call: smtp_mail (string to, string from, string subject, string body]) - or - 1431 * Call: smtp_mail (string to, string from, string data) - DEPRECATED 1432 * @param string $to 1433 * @param string $from 1434 * @param string $subject (if called with 4 parameters) or full mail body (if called with 3 parameters) 1435 * @param string $password (optional) - Password 1436 * @param string $body (optional, but recommended) - mail body 1437 * @return bool - true on success, otherwise false 1438 * TODO: Replace this with something decent like PEAR::Mail or Zend_Mail. 1439 */ 1440function smtp_mail($to, $from, $data, $password = "", $body = "") { 1441 global $CONF; 1442 1443 $smtpd_server = $CONF['smtp_server']; 1444 $smtpd_port = $CONF['smtp_port']; 1445 1446 $smtp_server = php_uname('n'); 1447 if (!empty($CONF['smtp_client'])) { 1448 $smtp_server = $CONF['smtp_client']; 1449 } 1450 $errno = 0; 1451 $errstr = "0"; 1452 $timeout = 30; 1453 1454 if ($body != "") { 1455 $maildata = 1456 "To: " . $to . "\n" 1457 . "From: " . $from . "\n" 1458 . "Subject: " . encode_header($data) . "\n" 1459 . "MIME-Version: 1.0\n" 1460 . "Date: " . date('r') . "\n" 1461 . "Content-Type: text/plain; charset=utf-8\n" 1462 . "Content-Transfer-Encoding: 8bit\n" 1463 . "\n" 1464 . $body 1465 ; 1466 } else { 1467 $maildata = $data; 1468 } 1469 1470 $fh = @fsockopen($smtpd_server, $smtpd_port, $errno, $errstr, $timeout); 1471 1472 if (!$fh) { 1473 error_log("fsockopen failed - errno: $errno - errstr: $errstr"); 1474 return false; 1475 } else { 1476 smtp_get_response($fh); 1477 1478 if (Config::bool('smtp_sendmail_tls')) { 1479 fputs($fh, "STARTTLS\r\n"); 1480 smtp_get_response($fh); 1481 1482 stream_set_blocking($fh, true); 1483 stream_socket_enable_crypto($fh, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); 1484 stream_set_blocking($fh, true); 1485 } 1486 1487 fputs($fh, "EHLO $smtp_server\r\n"); 1488 smtp_get_response($fh); 1489 1490 if (!empty($password)) { 1491 fputs($fh,"AUTH LOGIN\r\n"); 1492 smtp_get_response($fh); 1493 fputs($fh, base64_encode($from) . "\r\n"); 1494 smtp_get_response($fh); 1495 fputs($fh, base64_encode($password) . "\r\n"); 1496 smtp_get_response($fh); 1497 } 1498 1499 fputs($fh, "MAIL FROM:<$from>\r\n"); 1500 smtp_get_response($fh); 1501 fputs($fh, "RCPT TO:<$to>\r\n"); 1502 smtp_get_response($fh); 1503 fputs($fh, "DATA\r\n"); 1504 smtp_get_response($fh); 1505 fputs($fh, "$maildata\r\n.\r\n"); 1506 smtp_get_response($fh); 1507 fputs($fh, "QUIT\r\n"); 1508 smtp_get_response($fh); 1509 fclose($fh); 1510 } 1511 return true; 1512} 1513 1514/** 1515 * smtp_get_admin_email 1516 * Action: Get configured email address or current user if nothing configured 1517 * Call: smtp_get_admin_email 1518 * @return string - username/mail address 1519 */ 1520function smtp_get_admin_email() { 1521 $admin_email = Config::read_string('admin_email'); 1522 if (!empty($admin_email)) { 1523 return $admin_email; 1524 } else { 1525 return authentication_get_username(); 1526 } 1527} 1528 1529/** 1530 * smtp_get_admin_password 1531 * Action: Get smtp password for admin email 1532 * Call: smtp_get_admin_password 1533 * @return string - admin smtp password 1534 */ 1535function smtp_get_admin_password() { 1536 return Config::read_string('admin_smtp_password'); 1537} 1538 1539 1540// 1541// smtp_get_response 1542// Action: Get response from mail server 1543// Call: smtp_get_response (string FileHandle) 1544// 1545function smtp_get_response($fh) { 1546 $res =''; 1547 do { 1548 $line = fgets($fh, 256); 1549 $res .= $line; 1550 } while (preg_match("/^\d\d\d\-/", $line)); 1551 return $res; 1552} 1553 1554 1555 1556$DEBUG_TEXT = <<<EOF 1557 <p>Please check the documentation and website for more information.</p> 1558 <ul> 1559 <li><a href="http://postfixadmin.sf.net">PostfixAdmin - Project website</a></li> 1560 <li><a href='https://sourceforge.net/p/postfixadmin/discussion/676076'>Forums</a></li> 1561 </ul> 1562EOF; 1563 1564 1565/** 1566 * @return string - PDO DSN for PHP. 1567 * @throws Exception 1568 */ 1569function db_connection_string() { 1570 global $CONF; 1571 $dsn = null; 1572 if (db_mysql()) { 1573 $socket = false; 1574 if (Config::has('database_socket')) { 1575 $socket = Config::read_string('database_socket'); 1576 } 1577 1578 $database_name = Config::read_string('database_name'); 1579 1580 if ($socket) { 1581 $dsn = "mysql:unix_socket={$socket};dbname={$database_name};charset=UTF8"; 1582 } else { 1583 $dsn = "mysql:host={$CONF['database_host']};dbname={$database_name};charset=UTF8"; 1584 } 1585 } elseif (db_sqlite()) { 1586 $db = $CONF['database_name']; 1587 1588 $dsn = "sqlite:{$db}"; 1589 } elseif (db_pgsql()) { 1590 $dsn = "pgsql:dbname={$CONF['database_name']}"; 1591 if (isset($CONF['database_host'])) { 1592 $dsn .= ";host={$CONF['database_host']}"; 1593 } 1594 if (isset($CONF['database_port'])) { 1595 $dsn .= ";port={$CONF['database_port']}"; 1596 } 1597 $dsn .= ";options='-c client_encoding=utf8'"; 1598 } else { 1599 throw new Exception("<p style='color: red'>FATAL Error:<br />Invalid \$CONF['database_type'] <br/>'pgsql', 'mysql' or 'sqlite' supported. <br/> Please fix your config.inc.php!</p>"); 1600 } 1601 1602 return $dsn; 1603} 1604 1605/** 1606 * db_connect 1607 * Action: Makes a connection to the database if it doesn't exist 1608 * Call: db_connect () 1609 * 1610 * Return value: 1611 * 1612 * @return \PDO 1613 */ 1614function db_connect() { 1615 global $CONF; 1616 1617 /* some attempt at not reopening an existing connection */ 1618 static $link; 1619 if (isset($link) && $link) { 1620 return $link; 1621 } 1622 1623 $link = false; 1624 1625 // throws. 1626 $dsn = db_connection_string(); 1627 1628 $options = array( 1629 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 1630 PDO::ATTR_TIMEOUT => 5, 1631 ); 1632 $username_password = true; 1633 1634 $queries = array(); 1635 1636 1637 if (db_mysql()) { 1638 if (Config::bool('database_use_ssl')) { 1639 $options[PDO::MYSQL_ATTR_SSL_KEY] = Config::read_string('database_ssl_key'); 1640 $options[PDO::MYSQL_ATTR_SSL_CA] = Config::read_string('database_ssl_ca'); 1641 $options[PDO::MYSQL_ATTR_SSL_CAPATH] = Config::read_string('database_ssl_ca_path'); 1642 $options[PDO::MYSQL_ATTR_SSL_CERT] = Config::read_string('database_ssl_cert'); 1643 $options[PDO::MYSQL_ATTR_SSL_CIPHER] = Config::read_string('database_ssl_cipher'); 1644 $options = array_filter($options); // remove empty settings. 1645 1646 $verify = Config::read('database_ssl_verify_server_cert'); 1647 if ($verify === null) { // undefined 1648 $verify = true; 1649 } 1650 1651 $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$verify; 1652 } 1653 $queries[] = 'SET CHARACTER SET utf8'; 1654 $queries[] = "SET COLLATION_CONNECTION='utf8_general_ci'"; 1655 } elseif (db_sqlite()) { 1656 $db = $CONF['database_name']; 1657 1658 if (!file_exists($db)) { 1659 $error_text = 'SQLite database missing: ' . $db; 1660 throw new Exception($error_text); 1661 } 1662 1663 if (!is_writeable($db)) { 1664 $error_text = 'SQLite database not writeable: ' . $db; 1665 throw new Exception($error_text); 1666 } 1667 1668 if (!is_writeable(dirname($db))) { 1669 $error_text = 'The directory the SQLite database is in is not writeable: ' . dirname($db); 1670 throw new Exception($error_text); 1671 } 1672 1673 $username_password = false; 1674 } elseif (db_pgsql()) { 1675 // nothing to do. 1676 } else { 1677 throw new Exception("<p style='color: red'>FATAL Error:<br />Invalid \$CONF['database_type']! Please fix your config.inc.php!</p>"); 1678 } 1679 1680 if ($username_password) { 1681 $link = new PDO($dsn, Config::read_string('database_user'), Config::read_string('database_password'), $options); 1682 } else { 1683 $link = new PDO($dsn, null, null, $options); 1684 } 1685 1686 if (!empty($queries)) { 1687 foreach ($queries as $q) { 1688 $link->exec($q); 1689 } 1690 } 1691 1692 return $link; 1693} 1694 1695/** 1696 * Returns the appropriate boolean value for the database. 1697 * 1698 * @param bool|string $bool 1699 * @return string|int as appropriate for underlying db platform 1700 */ 1701function db_get_boolean($bool) { 1702 if (! (is_bool($bool) || $bool == '0' || $bool == '1')) { 1703 error_log("Invalid usage of 'db_get_boolean($bool)'"); 1704 throw new Exception("Invalid usage of 'db_get_boolean($bool)'"); 1705 } 1706 1707 if (db_pgsql()) { 1708 // return either true or false (unquoted strings) 1709 if ($bool) { 1710 return 't'; 1711 } 1712 return 'f'; 1713 } elseif (db_mysql() || db_sqlite()) { 1714 if ($bool) { 1715 return 1; 1716 } 1717 return 0; 1718 } else { 1719 throw new Exception('Unknown value in $CONF[database_type]'); 1720 } 1721} 1722 1723/** 1724 * Returns a query that reports the used quota ("x / y") 1725 * @param string column containing used quota 1726 * @param string column containing allowed quota 1727 * @param string column that will contain "x / y" 1728 * @return string 1729 */ 1730function db_quota_text($count, $quota, $fieldname) { 1731 if (db_pgsql() || db_sqlite()) { 1732 // SQLite and PostgreSQL use || to concatenate strings 1733 return " CASE $quota 1734 WHEN '-1' THEN (coalesce($count,0) || ' / -') 1735 WHEN '0' THEN (coalesce($count,0) || ' / " . escape_string(html_entity_decode('∞')) . "') 1736 ELSE (coalesce($count,0) || ' / ' || $quota) 1737 END AS $fieldname"; 1738 } else { 1739 return " CASE $quota 1740 WHEN '-1' THEN CONCAT(coalesce($count,0), ' / -') 1741 WHEN '0' THEN CONCAT(coalesce($count,0), ' / ', '" . escape_string(html_entity_decode('∞')) . "') 1742 ELSE CONCAT(coalesce($count,0), ' / ', $quota) 1743 END AS $fieldname"; 1744 } 1745} 1746 1747/** 1748 * Returns a query that reports the used quota ("x / y") 1749 * @param string column containing used quota 1750 * @param string column containing allowed quota 1751 * @param string column that will contain "x / y" 1752 * @return string 1753 */ 1754function db_quota_percent($count, $quota, $fieldname) { 1755 return " CASE $quota 1756 WHEN '-1' THEN -1 1757 WHEN '0' THEN -1 1758 ELSE round(100 * coalesce($count,0) / $quota) 1759 END AS $fieldname"; 1760} 1761 1762/** 1763 * @return boolean true if it's a MySQL database variant. 1764 */ 1765function db_mysql() { 1766 $type = Config::Read('database_type'); 1767 1768 if ($type == 'mysql' || $type == 'mysqli') { 1769 return true; 1770 } 1771 return false; 1772} 1773 1774/** 1775 * @return bool true if PostgreSQL is used, false otherwise 1776 */ 1777function db_pgsql() { 1778 return Config::read_string('database_type') == 'pgsql'; 1779} 1780 1781/** 1782 * returns true if SQLite is used, false otherwise 1783 */ 1784function db_sqlite() { 1785 if (Config::Read('database_type')=='sqlite') { 1786 return true; 1787 } else { 1788 return false; 1789 } 1790} 1791 1792/** 1793 * @param string $sql 1794 * @param array $values 1795 * @return array 1796 */ 1797function db_query_all($sql, array $values = []) { 1798 $r = db_query($sql, $values); 1799 return $r['result']->fetchAll(PDO::FETCH_ASSOC); 1800} 1801 1802/** 1803 * @param string $sql 1804 * @param array $values 1805 * @return array 1806 */ 1807function db_query_one($sql, array $values = []) { 1808 $r = db_query($sql, $values); 1809 return $r['result']->fetch(PDO::FETCH_ASSOC); 1810} 1811 1812 1813/** 1814 * @param string $sql e.g. UPDATE foo SET bar = :baz 1815 * @param array $values - parameters for the prepared statement e.g. ['baz' => 1234] 1816 * @param bool $throw_exceptions 1817 * @return int number of rows affected by the query 1818 */ 1819function db_execute($sql, array $values = [], $throw_exceptions = false) { 1820 $link = db_connect(); 1821 1822 try { 1823 $stmt = $link->prepare($sql); 1824 $stmt->execute($values); 1825 } catch (PDOException $e) { 1826 $error_text = "Invalid query: " . $e->getMessage() . " caused by " . $sql ; 1827 error_log($error_text); 1828 if ($throw_exceptions) { 1829 throw $e; 1830 } 1831 1832 return 0; 1833 } 1834 1835 return $stmt->rowCount(); 1836} 1837 1838/** 1839 * @param string $sql 1840 * @param array $values 1841 * @param bool $ignore_errors - set to true to ignore errors. 1842 * @return array e.g. ['result' => PDOStatement, 'error' => string ] 1843 */ 1844function db_query($sql, array $values = array(), $ignore_errors = false) { 1845 $link = db_connect(); 1846 $error_text = ''; 1847 1848 $stmt = null; 1849 try { 1850 $stmt = $link->prepare($sql); 1851 $stmt->execute($values); 1852 } catch (PDOException $e) { 1853 $error_text = "Invalid query: " . $e->getMessage() . " caused by " . $sql ; 1854 error_log($error_text); 1855 if (defined('PHPUNIT_TEST')) { 1856 throw new Exception("SQL query failed: {{{$sql}}} with " . json_encode($values) . ". Error message: " . $e->getMessage()); 1857 } 1858 if (!$ignore_errors) { 1859 throw new Exception("DEBUG INFORMATION: " . $e->getMessage() . "<br/> Check your error_log for the failed query"); 1860 } 1861 } 1862 1863 return array( 1864 "result" => $stmt, 1865 "error" => $error_text, 1866 ); 1867} 1868 1869 1870 1871 1872 1873/** 1874 * Delete a row from the specified table. 1875 * 1876 * DELETE FROM $table WHERE $where = $delete $aditionalWhere 1877 * 1878 * @param string $table 1879 * @param string $where - should never be a user supplied value 1880 * @param string $delete 1881 * @param string $additionalwhere (default ''). 1882 * @return int|mixed rows deleted. 1883 */ 1884function db_delete($table, $where, $delete, $additionalwhere='') { 1885 $table = table_by_key($table); 1886 1887 $query = "DELETE FROM $table WHERE $where = ? $additionalwhere"; 1888 1889 return db_execute($query, array($delete)); 1890} 1891 1892 1893 1894/** 1895 * db_insert 1896 * Action: Inserts a row from a specified table 1897 * Call: db_insert (string table, array values [, array timestamp]) 1898 * 1899 * @param string - table name 1900 * @param array $values - key/value map of data to insert into the table. 1901 * @param array $timestamp (optional) - array of fields to set to now() - default: array('created', 'modified') 1902 * @param boolean $throw_exceptions 1903 * @return int - number of inserted rows 1904 */ 1905function db_insert($table, array $values, $timestamp = array('created', 'modified'), $throw_exceptions = false) { 1906 $table = table_by_key($table); 1907 1908 foreach ($timestamp as $key) { 1909 if (db_sqlite()) { 1910 $values[$key] = "datetime('now')"; 1911 } else { 1912 $values[$key] = "now()"; 1913 } 1914 } 1915 1916 $value_string = ''; 1917 $comma = ''; 1918 $prepared_statment_values = $values; 1919 1920 foreach ($values as $field => $value) { 1921 if (in_array($field, $timestamp)) { 1922 $value_string .= $comma . $value; // see above. 1923 unset($prepared_statment_values[$field]); 1924 } else { 1925 $value_string .= $comma . ":{$field}"; 1926 } 1927 $comma = ','; 1928 } 1929 1930 1931 return db_execute( 1932 "INSERT INTO $table (" . implode(",", array_keys($values)) .") VALUES ($value_string)", 1933 $prepared_statment_values, 1934 $throw_exceptions); 1935} 1936 1937 1938/** 1939 * db_update 1940 * Action: Updates a specified table 1941 * Call: db_update (string table, string where_col, string where_value, array values [, array timestamp]) 1942 * @param string $table - table name 1943 * @param string $where_col - column of WHERE condition 1944 * @param string $where_value - value of WHERE condition 1945 * @param array $values - key/value map of data to insert into the table. 1946 * @param array $timestamp (optional) - array of fields to set to now() - default: array('modified') 1947 * @return int - number of updated rows 1948 */ 1949function db_update(string $table, string $where_col, string $where_value, array $values, array $timestamp = array('modified'), bool $throw_exceptions = false):int { 1950 $table_key = table_by_key($table); 1951 1952 $pvalues = array(); 1953 1954 $set = array(); 1955 1956 foreach ($timestamp as $k) { 1957 if (!isset($values[$k])) { 1958 $values[$k] = 'x'; // timestamp field not in the values list, add it in so we set it to now() see #469 1959 } 1960 } 1961 1962 foreach ($values as $key => $value) { 1963 if (in_array($key, $timestamp)) { 1964 if (db_sqlite()) { 1965 $set[] = " $key = datetime('now') "; 1966 } else { 1967 $set[] = " $key = now() "; 1968 } 1969 continue; 1970 } 1971 1972 $set[] = " $key = :$key "; 1973 $pvalues[$key] = $value; 1974 } 1975 1976 $pvalues['where'] = $where_value; 1977 1978 1979 $sql="UPDATE $table_key SET " . implode(",", $set) . " WHERE $where_col = :where"; 1980 1981 return db_execute($sql, $pvalues, $throw_exceptions); 1982} 1983 1984 1985/** 1986 * db_log 1987 * Action: Logs actions from admin 1988 * Call: db_log (string domain, string action, string data) 1989 * Possible actions are defined in $LANG["pViewlog_action_$action"] 1990 */ 1991function db_log($domain, $action, $data) { 1992 if (!Config::bool('logging')) { 1993 return true; 1994 } 1995 1996 $REMOTE_ADDR = getRemoteAddr(); 1997 1998 $username = authentication_get_username(); 1999 2000 if (Config::Lang("pViewlog_action_$action") == '') { 2001 throw new Exception("Invalid log action : $action"); // could do with something better? 2002 } 2003 2004 2005 $logdata = array( 2006 'username' => "$username ($REMOTE_ADDR)", 2007 'domain' => $domain, 2008 'action' => $action, 2009 'data' => $data, 2010 ); 2011 $result = db_insert('log', $logdata, array('timestamp')); 2012 if ($result != 1) { 2013 return false; 2014 } else { 2015 return true; 2016 } 2017} 2018 2019/** 2020 * db_in_clause 2021 * Action: builds and returns the "field in(x, y)" clause for database queries 2022 * Call: db_in_clause (string field, array values) 2023 * @param string $field 2024 * @param array $values 2025 * @return string 2026 */ 2027function db_in_clause($field, array $values) { 2028 $v = array_map('escape_string', array_values($values)); 2029 return " $field IN ('" . implode("','", $v) . "') "; 2030} 2031 2032/** 2033 * db_where_clause 2034 * Action: builds and returns a WHERE clause for database queries. All given conditions will be AND'ed. 2035 * Call: db_where_clause (array $conditions, array $struct) 2036 * @param array $condition - array('field' => 'value', 'field2' => 'value2, ...) 2037 * @param array $struct - field structure, used for automatic bool conversion 2038 * @param string $additional_raw_where - raw sniplet to include in the WHERE part - typically needs to start with AND 2039 * @param array $searchmode - operators to use (=, <, > etc.) - defaults to = if not specified for a field (see 2040 * $allowed_operators for available operators) 2041 * Note: the $searchmode operator will only be used if a $condition for that field is set. 2042 * This also means you'll need to set a (dummy) condition for NULL and NOTNULL. 2043 */ 2044function db_where_clause(array $condition, array $struct, $additional_raw_where = '', array $searchmode = array()) { 2045 if (count($condition) == 0 && trim($additional_raw_where) == '') { 2046 throw new Exception("db_where_cond: parameter is an empty array!"); 2047 } 2048 2049 $allowed_operators = array('<', '>', '>=', '<=', '=', '!=', '<>', 'CONT', 'LIKE', 'NULL', 'NOTNULL'); 2050 $where_parts = array(); 2051 $having_parts = array(); 2052 2053 foreach ($condition as $field => $value) { 2054 if (isset($struct[$field]) && $struct[$field]['type'] == 'bool') { 2055 $value = db_get_boolean($value); 2056 } 2057 $operator = '='; 2058 if (isset($searchmode[$field])) { 2059 if (in_array($searchmode[$field], $allowed_operators)) { 2060 $operator = $searchmode[$field]; 2061 2062 if ($operator == 'CONT') { # CONT - as in "contains" 2063 $operator = ' LIKE '; # add spaces 2064 $value = '%' . $value . '%'; 2065 } elseif ($operator == 'LIKE') { # LIKE -without adding % wildcards (the search value can contain %) 2066 $operator = ' LIKE '; # add spaces 2067 } 2068 } else { 2069 throw new Exception('db_where_clause: Invalid searchmode for ' . $field); 2070 } 2071 } 2072 2073 if ($operator == "NULL") { 2074 $querypart = $field . ' IS NULL'; 2075 } elseif ($operator == "NOTNULL") { 2076 $querypart = $field . ' IS NOT NULL'; 2077 } else { 2078 $querypart = $field . $operator . "'" . escape_string($value) . "'"; 2079 2080 // might need other types adding here. 2081 if (db_pgsql() && isset($struct[$field]) && in_array($struct[$field]['type'], array('ts', 'num')) && $value === '') { 2082 $querypart = $field . $operator . " NULL"; 2083 } 2084 } 2085 2086 if (!empty($struct[$field]['select'])) { 2087 $having_parts[$field] = $querypart; 2088 } else { 2089 $where_parts[$field] = $querypart; 2090 } 2091 } 2092 $query = ' WHERE 1=1 '; 2093 $query .= " $additional_raw_where "; 2094 if (count($where_parts) > 0) { 2095 $query .= " AND ( " . join(" AND ", $where_parts) . " ) "; 2096 } 2097 if (count($having_parts) > 0) { 2098 $query .= " HAVING ( " . join(" AND ", $having_parts) . " ) "; 2099 } 2100 2101 return $query; 2102} 2103 2104/** 2105 * Convert a programmatic db table name into what may be the actual name. 2106 * 2107 * Takes into consideration any CONF database_prefix or database_tables map 2108 * 2109 * If it's a MySQL database, then we return the name with backticks around it (`). 2110 * 2111 * @param string database table name. 2112 * @return string - database table name with appropriate prefix (and quoting if MySQL) 2113 */ 2114function table_by_key($table_key) { 2115 global $CONF; 2116 2117 $table = $table_key; 2118 2119 if (!empty($CONF['database_tables'][$table_key])) { 2120 $table = $CONF['database_tables'][$table_key]; 2121 } 2122 2123 $table = $CONF['database_prefix'] . $table; 2124 2125 if (db_mysql()) { 2126 return "`" . $table . "`"; 2127 } 2128 2129 return $table; 2130} 2131 2132 2133/** 2134 * check if the database layout is up to date 2135 * returns the current 'version' value from the config table 2136 * if $error_out is True (default), exit(1) with a message that recommends to run setup.php. 2137 * @param bool $error_out 2138 * @return int 2139 */ 2140function check_db_version($error_out = true) { 2141 global $min_db_version; 2142 2143 $table = table_by_key('config'); 2144 2145 $sql = "SELECT value FROM $table WHERE name = 'version'"; 2146 $row = db_query_one($sql); 2147 if (isset($row['value'])) { 2148 $dbversion = (int) $row['value']; 2149 } else { 2150 db_execute("INSERT INTO $table (name, value) VALUES ('version', '0')"); 2151 $dbversion = 0; 2152 } 2153 2154 if (($dbversion < $min_db_version) && $error_out == true) { 2155 echo "ERROR: The PostfixAdmin database layout is outdated (you have r$dbversion, but r$min_db_version is expected).\nPlease run setup.php to upgrade the database.\n"; 2156 exit(1); 2157 } 2158 2159 return $dbversion; 2160} 2161 2162 2163/** 2164 * 2165 * Action: Return a string of colored 's that indicate 2166 * the if an alias goto has an error or is sent to 2167 * addresses list in show_custom_domains 2168 * 2169 * @param string $show_alias 2170 * @return string 2171 */ 2172function gen_show_status($show_alias) { 2173 global $CONF; 2174 $table_alias = table_by_key('alias'); 2175 $stat_string = ""; 2176 2177 $stat_goto = ""; 2178 $stat_result = db_query_one("SELECT goto FROM $table_alias WHERE address=?", array($show_alias)); 2179 2180 if ($stat_result) { 2181 $stat_goto = $stat_result['goto']; 2182 } 2183 2184 $delimiter_regex = null; 2185 2186 if (!empty($CONF['recipient_delimiter'])) { 2187 $delimiter = preg_quote($CONF['recipient_delimiter'], "/"); 2188 $delimiter_regex = '/' .$delimiter. '[^' .$delimiter. '@]*@/'; 2189 } 2190 2191 // UNDELIVERABLE CHECK 2192 if ($CONF['show_undeliverable'] == 'YES') { 2193 $gotos=array(); 2194 $gotos=explode(',', $stat_goto); 2195 $undel_string=""; 2196 2197 //make sure this alias goes somewhere known 2198 $stat_ok = 1; 2199 foreach ($gotos as $g) { 2200 if (!$stat_ok) { 2201 break; 2202 } 2203 if (strpos($g, '@') === false) { 2204 continue; 2205 } 2206 2207 list($local_part, $stat_domain) = explode('@', $g); 2208 2209 $v = array(); 2210 2211 $stat_delimiter = ""; 2212 2213 $sql = "SELECT address FROM $table_alias WHERE address = ? OR address = ?"; 2214 $v[] = $g; 2215 $v[] = '@' . $stat_domain; 2216 2217 if (!empty($CONF['recipient_delimiter']) && isset($delimiter_regex)) { 2218 $v[] = preg_replace($delimiter_regex, "@", $g); 2219 $sql .= " OR address = ? "; 2220 } 2221 2222 $stat_result = db_query_one($sql, $v); 2223 2224 if (empty($stat_result)) { 2225 $stat_ok = 0; 2226 } 2227 2228 if ($stat_ok == 0) { 2229 if ($stat_domain == $CONF['vacation_domain'] || in_array($stat_domain, $CONF['show_undeliverable_exceptions'])) { 2230 $stat_ok = 1; 2231 } 2232 } 2233 } // while 2234 if ($stat_ok == 0) { 2235 $stat_string .= "<span style='background-color:" . $CONF['show_undeliverable_color'] . "'>" . $CONF['show_status_text'] . "</span> "; 2236 } else { 2237 $stat_string .= $CONF['show_status_text'] . " "; 2238 } 2239 } 2240 2241 // Vacation CHECK 2242 if ( array_key_exists('show_vacation', $CONF) && $CONF['show_vacation'] == 'YES' ) { 2243 $stat_result = db_query_one("SELECT * FROM ". table_by_key('vacation') ." WHERE email = ? AND active = ? ", array($show_alias, db_get_boolean(true) )) ; 2244 if (!empty($stat_result)) { 2245 $stat_string .= "<span style='background-color:" . $CONF['show_vacation_color'] . "'>" . $CONF['show_status_text'] . "</span> "; 2246 } else { 2247 $stat_string .= $CONF['show_status_text'] . " "; 2248 } 2249 } 2250 2251 // Disabled CHECK 2252 if ( array_key_exists('show_disabled', $CONF) && $CONF['show_disabled'] == 'YES' ) { 2253 $stat_result = db_query_one( 2254 "SELECT * FROM ". table_by_key('mailbox') ." WHERE username = ? AND active = ?", 2255 array($show_alias, db_get_boolean(false)) 2256 ); 2257 if (!empty($stat_result)) { 2258 $stat_string .= "<span style='background-color:" . $CONF['show_disabled_color'] . "'>" . $CONF['show_status_text'] . "</span> "; 2259 } else { 2260 $stat_string .= $CONF['show_status_text'] . " "; 2261 } 2262 } 2263 2264 // Expired CHECK 2265 if (Config::has('password_expiration') && Config::bool('password_expiration') && Config::bool('show_expired')) { 2266 $now = 'now()'; 2267 if (db_sqlite()) { 2268 $now = "datetime('now')"; 2269 } 2270 2271 $stat_result = db_query_one("SELECT * FROM " . table_by_key('mailbox') . " WHERE username = ? AND password_expiry <= $now AND active = ?", array($show_alias, db_get_boolean(true))); 2272 2273 if (!empty($stat_result)) { 2274 $stat_string .= "<span style='background-color:" . $CONF['show_expired_color'] . "'>" . $CONF['show_status_text'] . "</span> "; 2275 } else { 2276 $stat_string .= $CONF['show_status_text'] . " "; 2277 } 2278 } 2279 2280 // POP/IMAP CHECK 2281 if ($CONF['show_popimap'] == 'YES') { 2282 $stat_delimiter = ""; 2283 if (!empty($CONF['recipient_delimiter']) && isset($delimiter_regex)) { 2284 $stat_delimiter = ',' . preg_replace($delimiter_regex, "@", $stat_goto); 2285 } 2286 2287 //if the address passed in appears in its own goto field, its POP/IMAP 2288 # TODO: or not (might also be an alias loop) -> check mailbox table! 2289 if (preg_match('/,' . $show_alias . ',/', ',' . $stat_goto . $stat_delimiter . ',')) { 2290 $stat_string .= "<span style='background-color:" . $CONF['show_popimap_color'] . 2291 "'>" . $CONF['show_status_text'] . "</span> "; 2292 } else { 2293 $stat_string .= $CONF['show_status_text'] . " "; 2294 } 2295 } 2296 2297 // CUSTOM DESTINATION CHECK 2298 if (count($CONF['show_custom_domains']) > 0) { 2299 for ($i = 0; $i < sizeof($CONF['show_custom_domains']); $i++) { 2300 if (preg_match('/^.*' . $CONF['show_custom_domains'][$i] . '.*$/', $stat_goto)) { 2301 $stat_string .= "<span style='background-color:" . $CONF['show_custom_colors'][$i] . 2302 "'>" . $CONF['show_status_text'] . "</span> "; 2303 } else { 2304 $stat_string .= $CONF['show_status_text'] . " "; 2305 } 2306 } 2307 } else { 2308 $stat_string .= "; "; 2309 } 2310 2311 // $stat_string .= "<span style='background-color:green'> </span> " . 2312 // "<span style='background-color:blue'> </span> "; 2313 return $stat_string; 2314} 2315 2316/** 2317 * @return string 2318 */ 2319function getRemoteAddr() { 2320 $REMOTE_ADDR = 'localhost'; 2321 if (isset($_SERVER['REMOTE_ADDR'])) { 2322 $REMOTE_ADDR = $_SERVER['REMOTE_ADDR']; 2323 } 2324 2325 return $REMOTE_ADDR; 2326} 2327 2328/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ 2329