Textpattern | PHP Cross Reference | Content Management Systems |
Description: Collection of password handling functions.
1 <?php 2 3 /* 4 * Textpattern Content Management System 5 * https://textpattern.com/ 6 * 7 * Copyright (C) 2020 The Textpattern Development Team 8 * 9 * This file is part of Textpattern. 10 * 11 * Textpattern is free software; you can redistribute it and/or 12 * modify it under the terms of the GNU General Public License 13 * as published by the Free Software Foundation, version 2. 14 * 15 * Textpattern is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU General Public License for more details. 19 * 20 * You should have received a copy of the GNU General Public License 21 * along with Textpattern. If not, see <https://www.gnu.org/licenses/>. 22 */ 23 24 /** 25 * Collection of password handling functions. 26 * 27 * @package User 28 */ 29 30 /** 31 * Emails a new user with account details and requests they set a password. 32 * 33 * @param string $name The login name 34 * @return bool FALSE on error. 35 */ 36 37 function send_account_activation($name) 38 { 39 global $sitename; 40 41 require_privs('admin.edit'); 42 43 $rs = safe_row("user_id, email, nonce, RealName, pass", 'txp_users', "name = '".doSlash($name)."'"); 44 45 if ($rs) { 46 extract($rs); 47 48 $expiryTimestamp = time() + (60 * 60 * ACTIVATION_EXPIRY_HOURS); 49 50 $activation_code = generate_user_token($user_id, 'account_activation', $expiryTimestamp, $pass, $nonce); 51 52 $expiryYear = safe_strftime('%Y', $expiryTimestamp); 53 $expiryMonth = safe_strftime('%B', $expiryTimestamp); 54 $expiryDay = safe_strftime('%Oe', $expiryTimestamp); 55 $expiryTime = safe_strftime('%H:%M %Z', $expiryTimestamp); 56 57 $authorLang = safe_field('val', 'txp_prefs', "name='language_ui' AND user_name = '".doSlash($name)."'"); 58 $authorLang = ($authorLang) ? $authorLang : TEXTPATTERN_DEFAULT_LANG; 59 60 $txpLang = Txp::get('\Textpattern\L10n\Lang'); 61 $txpLang->swapStrings($authorLang, 'admin, common'); 62 63 $message = gTxt('salutation', array('{name}' => $RealName)). 64 n.n.gTxt('you_have_been_registered').' '.$sitename. 65 66 n.n.gTxt('your_login_is').' '.$name. 67 n.n.gTxt('account_activation_confirmation'). 68 n.ahu.'index.php?lang='.$authorLang.'&activate='.$activation_code. 69 n.n.gTxt('link_expires', array( 70 '{year}' => $expiryYear, 71 '{month}' => $expiryMonth, 72 '{day}' => $expiryDay, 73 '{time}' => $expiryTime, 74 )); 75 76 $subject = gTxt('account_activation'); 77 78 $txpLang->swapStrings(null); 79 80 if (txpMail($email, "[$sitename] ".$subject, $message)) { 81 return gTxt('login_sent_to', array('{email}' => $email)); 82 } else { 83 return array(gTxt('could_not_mail'), E_ERROR); 84 } 85 } 86 } 87 88 /** 89 * Sends a password reset link to a user's email address. 90 * 91 * This function will return a success message even when the specified user 92 * doesn't exist. Though an error message could be thrown when a user isn't 93 * found, security best practice prevents leaking existing account names. 94 * 95 * @param string $name The login name 96 * @return string A localized message string 97 * @see send_new_password() 98 * @see reset_author_pass() 99 * @example 100 * echo send_reset_confirmation_request('username'); 101 */ 102 103 function send_reset_confirmation_request($name) 104 { 105 global $sitename; 106 107 $expiryTimestamp = time() + (60 * RESET_EXPIRY_MINUTES); 108 $safeName = doSlash($name); 109 110 $rs = safe_query( 111 "SELECT 112 txp_users.user_id, txp_users.email, 113 txp_users.nonce, txp_users.pass, 114 txp_token.expires 115 FROM ".safe_pfx('txp_users')." txp_users 116 LEFT JOIN ".safe_pfx('txp_token')." txp_token 117 ON txp_users.user_id = txp_token.reference_id 118 AND txp_token.type = 'password_reset' 119 WHERE txp_users.name = '$safeName'" 120 ); 121 122 $row = nextRow($rs); 123 124 if ($row) { 125 extract($row); 126 127 // Rate limit the reset requests. 128 if ($expires) { 129 $originalExpiry = strtotime($expires); 130 131 if (($expiryTimestamp - $originalExpiry) < (60 * RESET_RATE_LIMIT_MINUTES)) { 132 return gTxt('password_reset_confirmation_request_sent'); 133 } 134 } 135 136 $confirm = generate_user_token($user_id, 'password_reset', $expiryTimestamp, $pass, $nonce); 137 138 $expiryYear = safe_strftime('%Y', $expiryTimestamp); 139 $expiryMonth = safe_strftime('%B', $expiryTimestamp); 140 $expiryDay = safe_strftime('%Oe', $expiryTimestamp); 141 $expiryTime = safe_strftime('%H:%M %Z', $expiryTimestamp); 142 143 $authorLang = safe_field('val', 'txp_prefs', "name='language_ui' AND user_name = '$safeName'"); 144 $authorLang = ($authorLang) ? $authorLang : TEXTPATTERN_DEFAULT_LANG; 145 146 $txpLang = Txp::get('\Textpattern\L10n\Lang'); 147 $txpLang->swapStrings($authorLang, 'admin, common'); 148 149 $message = gTxt('salutation', array('{name}' => $name)). 150 n.n.gTxt('password_reset_confirmation'). 151 n.ahu.'index.php?lang='.$authorLang.'&confirm='.$confirm. 152 n.n.gTxt('link_expires', array( 153 '{year}' => $expiryYear, 154 '{month}' => $expiryMonth, 155 '{day}' => $expiryDay, 156 '{time}' => $expiryTime, 157 )); 158 159 $subject = gTxt('password_reset_confirmation_request'); 160 $txpLang->swapStrings(null); 161 162 if (txpMail($email, "[$sitename] ".$subject, $message)) { 163 return gTxt('password_reset_confirmation_request_sent'); 164 } else { 165 return array(gTxt('could_not_mail'), E_ERROR); 166 } 167 } else { 168 // Send generic 'request_sent' message so that (non-)existence of 169 // account names are not leaked. Since this is a short circuit, there's 170 // a possibility of a timing attack revealing the existence of an 171 // account, which we could defend against to some degree. 172 return gTxt('password_reset_confirmation_request_sent'); 173 } 174 } 175 176 /** 177 * Emails a new user with login details. 178 * 179 * This function can be only executed when the currently authenticated user 180 * trying to send the email was granted 'admin.edit' privileges. 181 * 182 * Should NEVER be used as sending plaintext passwords is wrong. 183 * Will be removed in future, in lieu of sending reset request tokens. 184 * 185 * @param string $RealName The real name 186 * @param string $name The login name 187 * @param string $email The email address 188 * @param string $password The password 189 * @return bool FALSE on error. 190 * @deprecated in 4.6.0 191 * @see send_new_password(), send_reset_confirmation_request 192 * @example 193 * if (send_password('John Doe', 'login', 'example@example.tld', 'password')) 194 * { 195 * echo "Login details sent."; 196 * } 197 */ 198 199 function send_password($RealName, $name, $email, $password) 200 { 201 global $sitename; 202 203 require_privs('admin.edit'); 204 205 $message = gTxt('salutation', array('{name}' => $RealName)). 206 207 n.n.gTxt('you_have_been_registered').' '.$sitename. 208 209 n.n.gTxt('your_login_is').' '.$name. 210 n.gTxt('your_password_is').' '.$password. 211 212 n.n.gTxt('log_in_at').' '.ahu.'index.php'; 213 214 return txpMail($email, "[$sitename] ".gTxt('your_login_info'), $message); 215 } 216 217 /** 218 * Sends a new password to an existing user. 219 * 220 * If the $name is FALSE, the password is sent to the currently 221 * authenticated user. 222 * 223 * Should NEVER be used as sending plaintext passwords is wrong. 224 * Will be removed in future, in lieu of sending reset request tokens. 225 * 226 * @param string $password The new password 227 * @param string $email The email address 228 * @param string $name The login name 229 * @return bool FALSE on error. 230 * @deprecated in 4.6.0 231 * @see send_reset_confirmation_request 232 * @see reset_author_pass() 233 * @example 234 * $pass = generate_password(); 235 * if (send_new_password($pass, 'example@example.tld', 'user')) 236 * { 237 * echo "Password was sent to 'user'."; 238 * } 239 */ 240 241 function send_new_password($password, $email, $name) 242 { 243 global $txp_user, $sitename; 244 245 if (empty($name)) { 246 $name = $txp_user; 247 } 248 249 $message = gTxt('salutation', array('{name}' => $name)). 250 251 n.n.gTxt('your_password_is').' '.$password. 252 253 n.n.gTxt('log_in_at').' '.ahu.'index.php'; 254 255 return txpMail($email, "[$sitename] ".gTxt('your_new_password'), $message); 256 } 257 258 /** 259 * Generates a password. 260 * 261 * Generates a random password of given length using the symbols set in 262 * PASSWORD_SYMBOLS constant. 263 * 264 * Should NEVER be used as it is not cryptographically secure. 265 * Will be removed in future, in lieu of sending reset request tokens. 266 * 267 * @param int $length The length of the password 268 * @return string Random plain-text password 269 * @deprecated in 4.6.0 270 * @see \Textpattern\Password\Generate 271 * @see \Textpattern\Password\Random 272 * @example 273 * echo generate_password(128); 274 */ 275 276 function generate_password($length = 10) 277 { 278 static $chars; 279 280 if (!$chars) { 281 $chars = str_split(PASSWORD_SYMBOLS); 282 } 283 284 $pool = false; 285 $pass = ''; 286 287 for ($i = 0; $i < $length; $i++) { 288 if (!$pool) { 289 $pool = $chars; 290 } 291 292 $index = mt_rand(0, count($pool) - 1); 293 $pass .= $pool[$index]; 294 unset($pool[$index]); 295 $pool = array_values($pool); 296 } 297 298 return $pass; 299 } 300 301 /** 302 * Resets the given user's password and emails it. 303 * 304 * The old password is replaced with a new random-generated one. 305 * 306 * Should NEVER be used as sending plaintext passwords is wrong. 307 * Will be removed in future, in lieu of sending reset request tokens. 308 * 309 * @param string $name The login name 310 * @return string A localized message string 311 * @deprecated in 4.6.0 312 * @see PASSWORD_LENGTH 313 * @see generate_password() 314 * @example 315 * echo reset_author_pass('username'); 316 */ 317 318 function reset_author_pass($name) 319 { 320 $email = safe_field("email", 'txp_users', "name = '".doSlash($name)."'"); 321 322 $new_pass = Txp::get('\Textpattern\Password\Random')->generate(PASSWORD_LENGTH); 323 324 $rs = change_user_password($name, $new_pass); 325 326 if ($rs) { 327 if (send_new_password($new_pass, $email, $name)) { 328 return gTxt('password_sent_to').' '.$email; 329 } else { 330 return gTxt('could_not_mail').' '.$email; 331 } 332 } else { 333 return gTxt('could_not_update_author').' '.txpspecialchars($name); 334 } 335 } 336 337 /** 338 * Loads client-side localisation scripts. 339 * 340 * Passes localisation strings from the database to JavaScript. 341 * 342 * Only works on the admin-side pages. 343 * 344 * @param string|array $var Scalar or array of string keys 345 * @param array $atts Array or array of arrays of variable substitution pairs 346 * @param array $route Optional events/steps upon which to add the strings 347 * @since 4.5.0 348 * @package L10n 349 * @example 350 * gTxtScript(array('string1', 'string2', 'string3')); 351 */ 352 353 function gTxtScript($var, $atts = array(), $route = array()) 354 { 355 global $textarray_script, $event, $step; 356 357 $targetEvent = empty($route[0]) ? null : (array)$route[0]; 358 $targetStep = empty($route[1]) ? null : (array)$route[1]; 359 360 if (($targetEvent === null || in_array($event, $targetEvent)) && ($targetStep === null || in_array($step, $targetStep))) { 361 if (!is_array($textarray_script)) { 362 $textarray_script = array(); 363 } 364 365 $data = is_array($var) ? array_map('gTxt', $var, $atts) : (array) gTxt($var, $atts); 366 $textarray_script = $textarray_script + array_combine((array) $var, $data); 367 } 368 } 369 370 /** 371 * Handle refreshing the passed AJAX content to the UI. 372 * 373 * @param array $partials Partials array 374 * @param array $rs Record set of the edited content 375 */ 376 377 function updatePartials($partials, $rs, $types) 378 { 379 if (!is_array($types)) { 380 $types = array($types); 381 } 382 383 foreach ($partials as $k => $p) { 384 if (in_array($p['mode'], $types)) { 385 $cb = $p['cb']; 386 $partials[$k]['html'] = (is_array($cb) ? call_user_func($cb, $rs, $k) : $cb($rs, $k)); 387 } 388 } 389 390 return $partials; 391 } 392 393 /** 394 * Handle refreshing the passed AJAX content to the UI. 395 * 396 * @param array $partials Partials array 397 * @return array Response to send back to the browser 398 */ 399 400 function updateVolatilePartials($partials) 401 { 402 $response = array(); 403 404 // Update the volatile partials. 405 foreach ($partials as $k => $p) { 406 // Volatile partials need a target DOM selector. 407 if (empty($p['selector']) && $p['mode'] != PARTIAL_STATIC) { 408 trigger_error(gTxt('empty_partial_selector', array('{name}' => $k)), E_USER_ERROR); 409 } else { 410 // Build response script. 411 list($selector, $fragment) = (array)$p['selector'] + array(null, null); 412 413 if ($p['mode'] == PARTIAL_VOLATILE) { 414 // Volatile partials replace *all* of the existing HTML 415 // fragment for their selector with the new one. 416 $selector = do_list($selector); 417 $fragment = isset($fragment) ? do_list($fragment) + $selector : $selector; 418 $response[] = 'var $html = $("<div>'.escape_js($p['html']).'</div>")'; 419 420 foreach ($selector as $i => $sel) { 421 $response[] = '$("'.$sel.'").replaceWith($html.find("'.$fragment[$i].'"))'; 422 } 423 } elseif ($p['mode'] == PARTIAL_VOLATILE_VALUE) { 424 // Volatile partial values replace the *value* of elements 425 // matching their selector. 426 $response[] = '$("'.$selector.'").val("'.escape_js($p['html']).'")'; 427 } 428 } 429 } 430 431 return $response; 432 } 433 434 /** 435 * Lists image types that can be safely uploaded. 436 * 437 * Returns different results based on the logged in user's privileges. 438 * 439 * @param int $type If set, validates the given value 440 * @return mixed 441 * @package Image 442 * @since 4.6.0 443 * @example 444 * list($width, $height, $extension) = getimagesize('image'); 445 * if ($type = get_safe_image_types($extension)) 446 * { 447 * echo "Valid image of {$type}."; 448 * } 449 */ 450 451 function get_safe_image_types($type = null) 452 { 453 if (!has_privs('image.create.trusted')) { 454 $extensions = array(0, '.gif', '.jpg', '.png'); 455 } else { 456 $extensions = array(0, '.gif', '.jpg', '.png', '.swf', 0, 0, 0, 0, 0, 0, 0, 0, '.swf'); 457 } 458 459 if (func_num_args() > 0) { 460 return !empty($extensions[$type]) ? $extensions[$type] : false; 461 } 462 463 return $extensions; 464 } 465 466 /** 467 * Checks if GD supports the given image type. 468 * 469 * @param string $image_type Either '.gif', '.jpg', '.png' 470 * @return bool TRUE if the type is supported 471 * @package Image 472 */ 473 474 function check_gd($image_type) 475 { 476 if (!function_exists('gd_info')) { 477 return false; 478 } 479 480 $gd_info = gd_info(); 481 482 switch ($image_type) { 483 case '.gif': 484 return ($gd_info['GIF Create Support'] == true); 485 break; 486 case '.jpg': 487 return ($gd_info['JPEG Support'] == true); 488 break; 489 case '.png': 490 return ($gd_info['PNG Support'] == true); 491 break; 492 } 493 494 return false; 495 } 496 497 /** 498 * Uploads an image. 499 * 500 * Can be used to upload a new image or replace an existing one. 501 * If $id is specified, the image will be replaced. If $uploaded is set FALSE, 502 * $file can take a local file instead of HTTP file upload variable. 503 * 504 * All uploaded files will included on the Images panel. 505 * 506 * @param array $file HTTP file upload variables 507 * @param array $meta Image meta data, allowed keys 'caption', 'alt', 'category' 508 * @param int $id Existing image's ID 509 * @param bool $uploaded If FALSE, $file takes a filename instead of upload vars 510 * @return array|string An array of array(message, id) on success, localized error string on error 511 * @package Image 512 * @example 513 * print_r(image_data( 514 * $_FILES['myfile'], 515 * array( 516 * 'caption' => '', 517 * 'alt' => '', 518 * 'category' => '', 519 * ) 520 * )); 521 */ 522 523 function image_data($file, $meta = array(), $id = 0, $uploaded = true) 524 { 525 global $txp_user, $event; 526 527 $name = $file['name']; 528 $error = $file['error']; 529 $file = $file['tmp_name']; 530 531 if ($uploaded) { 532 if ($error !== UPLOAD_ERR_OK) { 533 return upload_get_errormsg($error); 534 } 535 536 $file = get_uploaded_file($file); 537 } 538 539 if (empty($file)) { 540 return upload_get_errormsg(UPLOAD_ERR_NO_FILE); 541 } 542 543 if (get_pref('file_max_upload_size') < filesize($file)) { 544 unlink($file); 545 546 return upload_get_errormsg(UPLOAD_ERR_FORM_SIZE); 547 } 548 549 list($w, $h, $extension) = getimagesize($file); 550 $ext = get_safe_image_types($extension); 551 552 if (!$ext) { 553 return gTxt('only_graphic_files_allowed'); 554 } 555 556 $name = substr($name, 0, strrpos($name, '.')).$ext; 557 $safename = doSlash($name); 558 $meta = lAtts(array( 559 'category' => '', 560 'caption' => '', 561 'alt' => '', 562 ), (array) $meta, false); 563 564 extract(doSlash($meta)); 565 566 $q = " 567 name = '$safename', 568 ext = '$ext', 569 w = $w, 570 h = $h, 571 alt = '$alt', 572 caption = '$caption', 573 category = '$category', 574 date = NOW(), 575 author = '".doSlash($txp_user)."' 576 "; 577 578 if (empty($id)) { 579 $rs = safe_insert('txp_image', $q); 580 581 if ($rs) { 582 $id = $GLOBALS['ID'] = $rs; 583 } else { 584 return gTxt('image_save_error'); 585 } 586 } else { 587 $id = assert_int($id); 588 } 589 590 $newpath = IMPATH.$id.$ext; 591 592 if (shift_uploaded_file($file, $newpath) == false) { 593 if (!empty($rs)) { 594 safe_delete('txp_image', "id = '$id'"); 595 unset($GLOBALS['ID']); 596 } 597 598 return gTxt('directory_permissions', array('{path}' => $newpath)); 599 } elseif (empty($rs)) { 600 $rs = safe_update('txp_image', $q, "id = $id"); 601 602 if (!$rs) { 603 return gTxt('image_save_error'); 604 } 605 } 606 607 @chmod($newpath, 0644); 608 609 // GD is supported 610 if (check_gd($ext)) { 611 // Auto-generate a thumbnail using the last settings 612 if (get_pref('thumb_w') > 0 || get_pref('thumb_h') > 0) { 613 $t = new txp_thumb($id); 614 $t->crop = (bool) get_pref('thumb_crop'); 615 $t->hint = '0'; 616 $t->width = (int) get_pref('thumb_w'); 617 $t->height = (int) get_pref('thumb_h'); 618 $t->write(); 619 } 620 } 621 622 $message = gTxt('image_uploaded', array('{name}' => $name)); 623 update_lastmod('image_uploaded', compact('id', 'name', 'ext', 'w', 'h', 'alt', 'caption', 'category', 'txp_user')); 624 625 // call post-upload plugins with new image's $id 626 callback_event('image_uploaded', $event, false, $id); 627 628 return array($message, $id); 629 } 630 631 /** 632 * Error handler for admin-side pages. 633 * 634 * @param int $errno 635 * @param string $errstr 636 * @param string $errfile 637 * @param int $errline 638 * @access private 639 * @package Debug 640 */ 641 642 function adminErrorHandler($errno, $errstr, $errfile, $errline) 643 { 644 global $production_status, $theme, $event, $step; 645 646 $error = array(); 647 648 if ($production_status == 'testing') { 649 $error = array( 650 E_WARNING => 'Warning', 651 E_RECOVERABLE_ERROR => 'Catchable fatal error', 652 E_USER_ERROR => 'User_Error', 653 E_USER_WARNING => 'User_Warning', 654 ); 655 } elseif ($production_status == 'debug') { 656 $error = array( 657 E_WARNING => 'Warning', 658 E_NOTICE => 'Notice', 659 E_RECOVERABLE_ERROR => 'Catchable fatal error', 660 E_USER_ERROR => 'User_Error', 661 E_USER_WARNING => 'User_Warning', 662 E_USER_NOTICE => 'User_Notice', 663 ); 664 665 if (!isset($error[$errno])) { 666 $error[$errno] = $errno; 667 } 668 } 669 670 if (!isset($error[$errno]) || !error_reporting()) { 671 return; 672 } 673 674 // When even a minimum environment is missing. 675 if (!isset($production_status)) { 676 echo '<pre dir="auto">'.gTxt('internal_error').' "'.$errstr.'"'.n."in $errfile at line $errline".'</pre>'; 677 678 return; 679 } 680 681 $backtrace = ''; 682 683 if (has_privs('debug.verbose')) { 684 $msg = $error[$errno].' "'.$errstr.'"'; 685 } else { 686 $msg = gTxt('internal_error'); 687 } 688 689 if ($production_status == 'debug' && has_privs('debug.backtrace')) { 690 $msg .= n."in $errfile at line $errline"; 691 $backtrace = join(n, get_caller(10, 1)); 692 } 693 694 if ($errno == E_ERROR || $errno == E_USER_ERROR) { 695 $httpstatus = 500; 696 } else { 697 $httpstatus = 200; 698 } 699 700 $out = "$msg.\n$backtrace"; 701 702 if (http_accept_format('html')) { 703 if ($backtrace) { 704 echo "<pre dir=\"auto\">$msg.</pre>". 705 n.'<pre class="backtrace" dir="ltr"><code>'. 706 txpspecialchars($backtrace).'</code></pre>'; 707 } elseif (is_object($theme)) { 708 echo $theme->announce(array($out, E_ERROR), true); 709 } else { 710 echo "<pre dir=\"auto\">$out</pre>"; 711 } 712 } elseif (http_accept_format('js')) { 713 if (is_object($theme)) { 714 send_script_response($theme->announce_async(array($out, E_ERROR), true)); 715 } else { 716 send_script_response('/* '.$out.'*/'); 717 } 718 } elseif (http_accept_format('xml')) { 719 send_xml_response(array( 720 'http-status' => $httpstatus, 721 'internal_error' => "$out", 722 )); 723 } else { 724 txp_die($msg, 500); 725 } 726 } 727 728 /** 729 * Error handler for update scripts. 730 * 731 * @param int $errno 732 * @param string $errstr 733 * @param string $errfile 734 * @param int $errline 735 * @access private 736 * @package Debug 737 */ 738 739 function updateErrorHandler($errno, $errstr, $errfile, $errline) 740 { 741 global $production_status; 742 743 $old = $production_status; 744 $production_status = 'debug'; 745 746 adminErrorHandler($errno, $errstr, $errfile, $errline); 747 748 $production_status = $old; 749 750 throw new Exception('update failed'); 751 } 752 753 /** 754 * Registers an admin-side extension page. 755 * 756 * For now this just does the same as register_callback(). 757 * 758 * @param callback $func The callback function 759 * @param string $event The callback event 760 * @param string $step The callback step 761 * @param bool $top The top or the bottom of the page 762 * @access private 763 * @see register_callback() 764 * @package Callback 765 */ 766 767 function register_page_extension($func, $event, $step = '', $top = 0) 768 { 769 register_callback($func, $event, $step, $top); 770 } 771 772 /** 773 * Registers a new admin-side panel and adds a navigation link to the menu. 774 * 775 * @param string $area The menu the panel appears in, e.g. "home", "content", "presentation", "admin", "extensions" 776 * @param string $panel The panel's event 777 * @param string $title The menu item's label 778 * @package Callback 779 * @example 780 * add_privs('abc_admin_event', '1,2'); 781 * register_tab('extensions', 'abc_admin_event', 'My Panel'); 782 * register_callback('abc_admin_function', 'abc_admin_event'); 783 */ 784 785 function register_tab($area, $panel, $title) 786 { 787 global $plugin_areas, $event; 788 789 if ($event !== 'plugin') { 790 $plugin_areas[$area][$title] = $panel; 791 } 792 } 793 794 /** 795 * Call an event's pluggable UI function. 796 * 797 * @param string $event The event 798 * @param string $element The element selector 799 * @param string $default The default interface markup 800 * @return mixed Returned value from a callback handler, or $default if no custom UI was provided 801 * @package Callback 802 */ 803 804 function pluggable_ui($event, $element, $default = '') 805 { 806 $argv = func_get_args(); 807 $argv = array_merge(array( 808 $event, 809 $element, 810 (string) $default === '' ? 0 : array(0, 0) 811 ), array_slice($argv, 2)); 812 // Custom user interface, anyone? 813 // Signature for called functions: 814 // string my_called_func(string $event, string $step, string $default_markup[, mixed $context_data...]) 815 $ui = call_user_func_array('callback_event', $argv); 816 817 // Either plugins provided a user interface, or we render our own. 818 return ($ui === '') ? $default : $ui; 819 } 820 821 /** 822 * Gets a list of form types. 823 * 824 * The list form types can be extended with a 'form.types > types' 825 * callback event. Callback functions get passed three arguments: '$event', 826 * '$step' and '$types'. The third parameter contains a reference to an 827 * array of 'type => label' pairs. 828 * 829 * @return array An array of form types 830 * @since 4.6.0 831 * @package Template 832 */ 833 834 function get_form_types() 835 { 836 static $types = null; 837 838 if ($types === null) { 839 foreach (Txp::get('Textpattern\Skin\Form')->getTypes() as $type) { 840 $types[$type] = gTxt($type); 841 } 842 843 callback_event_ref('form.types', 'types', 0, $types); 844 } 845 846 return $types; 847 } 848 849 /** 850 * Gets a list of essential form templates. 851 * 852 * These forms can not be deleted or renamed. The array keys hold 853 * the form names, the array values their group. 854 * 855 * The list forms can be extended with a 'form.essential > forms' 856 * callback event. Callback functions get passed three arguments: '$event', 857 * '$step' and '$essential'. The third parameter contains a reference to an 858 * array of forms. 859 * 860 * @return array An array of form names 861 * @since 4.6.0 862 * @package Template 863 */ 864 865 function get_essential_forms() 866 { 867 static $essential = null; 868 869 if ($essential === null) { 870 $essential = array( 871 'comments' => 'comment', 872 'comments_display' => 'comment', 873 'comment_form' => 'comment', 874 'default' => 'article', 875 'plainlinks' => 'link', 876 'files' => 'file', 877 ); 878 879 callback_event_ref('form.essential', 'forms', 0, $essential); 880 } 881 882 return $essential; 883 } 884 885 /** 886 * Renders a HTML <select> list of supported permanent link URL formats. 887 * 888 * @param string $name HTML name and id of the list 889 * @param string $val Initial (or current) selected item 890 * @return string HTML 891 */ 892 893 function permlinkmodes($name, $val, $blank = false) 894 { 895 $vals = array( 896 'messy' => gTxt('messy'), 897 'id_title' => gTxt('id_title'), 898 'section_id_title' => gTxt('section_id_title'), 899 'section_category_title' => gTxt('section_category_title'), 900 'year_month_day_title' => gTxt('year_month_day_title'), 901 'breadcrumb_title' => gTxt('breadcrumb_title'), 902 'section_title' => gTxt('section_title'), 903 'title_only' => gTxt('title_only') 904 ); 905 906 return selectInput($name, $vals, $val, $blank, '', $name); 907 } 908 909 /** 910 * Gets the name of the default publishing section. 911 * 912 * @return string The section 913 */ 914 915 function getDefaultSection() 916 { 917 global $txp_sections; 918 919 $name = get_pref('default_section'); 920 921 if (!isset($txp_sections[$name])) { 922 foreach ($txp_sections as $name => $section) { 923 if ($name != 'default') { 924 break; 925 } 926 } 927 928 set_pref('default_section', $name, 'section', PREF_HIDDEN); 929 } 930 931 return $name; 932 } 933 934 /** 935 * Updates a list's per page number. 936 * 937 * Gets the per page number from a "qty" HTTP POST/GET parameter and 938 * creates a user-specific preference value "$name_list_pageby". 939 * 940 * @param string|null $name The name of the list 941 * @deprecated in 4.7.0 942 */ 943 944 function event_change_pageby($name = null) 945 { 946 global $event; 947 948 Txp::get('\Textpattern\Admin\Paginator', $event, $name)->change(); 949 } 950 951 /** 952 * Generic multi-edit form's edit handler shared across panels. 953 * 954 * Receives an action from a multi-edit form and runs it in the given 955 * database table. 956 * 957 * @param string $table The database table 958 * @param string $id_key The database column selected items match to. Column should be integer type 959 * @return string Comma-separated list of affected items 960 * @see multi_edit() 961 */ 962 963 function event_multi_edit($table, $id_key) 964 { 965 $method = ps('edit_method'); 966 $selected = ps('selected'); 967 968 if ($selected) { 969 if ($method == 'delete') { 970 foreach ($selected as $id) { 971 $id = assert_int($id); 972 973 if (safe_delete($table, "$id_key = '$id'")) { 974 $ids[] = $id; 975 } 976 } 977 978 return join(', ', $ids); 979 } 980 } 981 982 return ''; 983 } 984 985 /** 986 * Verifies temporary directory. 987 * 988 * Verifies that the temporary directory is writeable. 989 * 990 * @param string $dir The directory to check 991 * @return bool|null NULL on error, TRUE on success 992 * @package Debug 993 */ 994 995 function find_temp_dir() 996 { 997 global $path_to_site, $img_dir; 998 999 if (IS_WIN) { 1000 $guess = array( 1001 txpath.DS.'tmp', 1002 getenv('TMP'), 1003 getenv('TEMP'), 1004 getenv('SystemRoot').DS.'Temp', 1005 'C:'.DS.'Temp', 1006 $path_to_site.DS.$img_dir, 1007 ); 1008 1009 foreach ($guess as $k => $v) { 1010 if (empty($v)) { 1011 unset($guess[$k]); 1012 } 1013 } 1014 } else { 1015 $guess = array( 1016 txpath.DS.'tmp', 1017 '', 1018 DS.'tmp', 1019 $path_to_site.DS.$img_dir, 1020 ); 1021 } 1022 1023 foreach ($guess as $dir) { 1024 $tf = @tempnam($dir, 'txp_'); 1025 1026 if ($tf) { 1027 $tf = realpath($tf); 1028 } 1029 1030 if ($tf and file_exists($tf)) { 1031 unlink($tf); 1032 1033 return dirname($tf); 1034 } 1035 } 1036 1037 return false; 1038 } 1039 1040 /** 1041 * Moves an uploaded file and returns its new location. 1042 * 1043 * @param string $f The filename of the uploaded file 1044 * @param string $dest The destination of the moved file. If omitted, the file is moved to the temp directory 1045 * @return string|bool The new path or FALSE on error 1046 * @package File 1047 */ 1048 1049 function get_uploaded_file($f, $dest = '') 1050 { 1051 global $tempdir; 1052 1053 if (!is_uploaded_file($f)) { 1054 return false; 1055 } 1056 1057 if ($dest) { 1058 $newfile = $dest; 1059 } else { 1060 $newfile = tempnam($tempdir, 'txp_'); 1061 if (!$newfile) { 1062 return false; 1063 } 1064 } 1065 1066 // $newfile is created by tempnam(), but move_uploaded_file will overwrite it. 1067 if (move_uploaded_file($f, $newfile)) { 1068 return $newfile; 1069 } 1070 } 1071 1072 /** 1073 * Gets an array of files in the Files directory that weren't uploaded 1074 * from Textpattern. 1075 * 1076 * Used for importing existing files on the server to Textpattern's files panel. 1077 * 1078 * @param string $path The directory to scan 1079 * @param int $options glob() options 1080 * @return array An array of file paths 1081 * @package File 1082 */ 1083 1084 function get_filenames($path = null, $options = GLOB_NOSORT) 1085 { 1086 global $file_base_path; 1087 1088 $files = array(); 1089 $file_path = isset($path) ? $path : $file_base_path; 1090 $is_file = ($options & GLOB_ONLYDIR) ? 'is_dir' : 'is_file'; 1091 1092 if (!is_dir($file_path) || !is_readable($file_path)) { 1093 return array(); 1094 } 1095 1096 $cwd = getcwd(); 1097 1098 if (chdir($file_path)) { 1099 $directory = glob('*', $options); 1100 1101 if ($directory) { 1102 foreach ($directory as $filename) { 1103 if ($is_file($filename) && is_readable($filename)) { 1104 $files[$filename] = $filename; 1105 } 1106 } 1107 1108 unset($directory); 1109 } 1110 1111 if ($cwd) { 1112 chdir($cwd); 1113 } 1114 } 1115 1116 if (!$files || isset($path)) { 1117 return $files; 1118 } 1119 1120 $rs = safe_rows_start("filename", 'txp_file', "1 = 1"); 1121 1122 if ($rs && numRows($rs)) { 1123 while ($a = nextRow($rs)) { 1124 unset($files[$a['filename']]); 1125 } 1126 } 1127 1128 return $files; 1129 } 1130 1131 /** 1132 * Moves a file. 1133 * 1134 * @param string $f The file to move 1135 * @param string $dest The destination 1136 * @return bool TRUE on success, or FALSE on error 1137 * @package File 1138 */ 1139 1140 function shift_uploaded_file($f, $dest) 1141 { 1142 if (@rename($f, $dest)) { 1143 return true; 1144 } 1145 1146 if (@copy($f, $dest)) { 1147 unlink($f); 1148 1149 return true; 1150 } 1151 1152 return false; 1153 } 1154 1155 /** 1156 * Assigns assets to a different user. 1157 * 1158 * Changes the owner of user's assets. It will move articles, files, images 1159 * and links from one user to another. 1160 * 1161 * Should be run when a user's permissions are taken away, a username is 1162 * renamed or the user is removed from the site. 1163 * 1164 * Affected database tables can be extended with a 'user.assign_assets > columns' 1165 * callback event. Callback functions get passed three arguments: '$event', 1166 * '$step' and '$columns'. The third parameter contains a reference to an 1167 * array of 'table => column' pairs. 1168 * 1169 * On a successful run, will trigger a 'user.assign_assets > done' callback event. 1170 * 1171 * @param string|array $owner List of current owners 1172 * @param string $new_owner The new owner 1173 * @return bool FALSE on error 1174 * @since 4.6.0 1175 * @package User 1176 * @example 1177 * if (assign_user_assets(array('user1', 'user2'), 'new_owner')) 1178 * { 1179 * echo "Assigned assets by 'user1' and 'user2' to 'new_owner'."; 1180 * } 1181 */ 1182 1183 function assign_user_assets($owner, $new_owner) 1184 { 1185 static $columns = null; 1186 1187 if (!$owner || !user_exists($new_owner)) { 1188 return false; 1189 } 1190 1191 if ($columns === null) { 1192 $columns = array( 1193 'textpattern' => 'AuthorID', 1194 'txp_file' => 'author', 1195 'txp_image' => 'author', 1196 'txp_link' => 'author', 1197 ); 1198 1199 callback_event_ref('user.assign_assets', 'columns', 0, $columns); 1200 } 1201 1202 $names = join(',', quote_list((array) $owner)); 1203 $assign = doSlash($new_owner); 1204 1205 foreach ($columns as $table => $column) { 1206 if (safe_update($table, "$column = '$assign'", "$column IN ($names)") === false) { 1207 return false; 1208 } 1209 } 1210 1211 callback_event('user.assign_assets', 'done', 0, compact('owner', 'new_owner', 'columns')); 1212 1213 return true; 1214 } 1215 1216 /** 1217 * Validates a string as a username. 1218 * 1219 * @param string $name The username 1220 * @return bool TRUE if the string valid 1221 * @since 4.6.0 1222 * @package User 1223 * @example 1224 * if (is_valid_username('john')) 1225 * { 1226 * echo "'john' is a valid username."; 1227 * } 1228 */ 1229 1230 function is_valid_username($name) 1231 { 1232 if (function_exists('mb_strlen')) { 1233 $length = mb_strlen($name, '8bit'); 1234 } else { 1235 $length = strlen($name); 1236 } 1237 1238 return $name && !preg_match('/^\s|[,\'"<>]|\s$/u', $name) && $length <= 64; 1239 } 1240 1241 /** 1242 * Creates a user account. 1243 * 1244 * On a successful run, will trigger a 'user.create > done' callback event. 1245 * 1246 * @param string $name The login name 1247 * @param string $email The email address 1248 * @param string $password The password 1249 * @param string $realname The real name 1250 * @param int $group The user group 1251 * @return bool FALSE on error 1252 * @since 4.6.0 1253 * @package User 1254 * @example 1255 * if (create_user('john', 'john.doe@example.com', 'DancingWalrus', 'John Doe', 1)) 1256 * { 1257 * echo "User 'john' created."; 1258 * } 1259 */ 1260 1261 function create_user($name, $email, $password, $realname = '', $group = 0) 1262 { 1263 $levels = get_groups(); 1264 1265 if (!$password || !is_valid_username($name) || !is_valid_email($email) || user_exists($name) || !isset($levels[$group])) { 1266 return false; 1267 } 1268 1269 $nonce = md5(uniqid(mt_rand(), true)); 1270 $hash = Txp::get('\Textpattern\Password\Hash')->hash($password); 1271 1272 if ( 1273 safe_insert( 1274 'txp_users', 1275 "name = '".doSlash($name)."', 1276 email = '".doSlash($email)."', 1277 pass = '".doSlash($hash)."', 1278 nonce = '".doSlash($nonce)."', 1279 privs = ".intval($group).", 1280 RealName = '".doSlash($realname)."'" 1281 ) === false 1282 ) { 1283 return false; 1284 } 1285 1286 callback_event('user.create', 'done', 0, compact('name', 'email', 'password', 'realname', 'group', 'nonce', 'hash')); 1287 1288 return true; 1289 } 1290 1291 /** 1292 * Updates a user. 1293 * 1294 * Updates a user account's properties. The $user argument is used for 1295 * selecting the updated user, and rest of the arguments new values. 1296 * Use NULL to omit an argument. 1297 * 1298 * On a successful run, will trigger a 'user.update > done' callback event. 1299 * 1300 * @param string $user The updated user 1301 * @param string|null $email The email address 1302 * @param string|null $realname The real name 1303 * @param array|null $meta Additional meta fields 1304 * @return bool FALSE on error 1305 * @since 4.6.0 1306 * @package User 1307 * @example 1308 * if (update_user('login', null, 'John Doe')) 1309 * { 1310 * echo "Updated user's real name."; 1311 * } 1312 */ 1313 1314 function update_user($user, $email = null, $realname = null, $meta = array()) 1315 { 1316 if (($email !== null && !is_valid_email($email)) || !user_exists($user)) { 1317 return false; 1318 } 1319 1320 $meta = (array) $meta; 1321 $meta['RealName'] = $realname; 1322 $meta['email'] = $email; 1323 $set = array(); 1324 1325 foreach ($meta as $name => $value) { 1326 if ($value !== null) { 1327 $set[] = $name." = '".doSlash($value)."'"; 1328 } 1329 } 1330 1331 if ( 1332 safe_update( 1333 'txp_users', 1334 join(',', $set), 1335 "name = '".doSlash($user)."'" 1336 ) === false 1337 ) { 1338 return false; 1339 } 1340 1341 callback_event('user.update', 'done', 0, compact('user', 'email', 'realname', 'meta')); 1342 1343 return true; 1344 } 1345 1346 /** 1347 * Changes a user's password. 1348 * 1349 * On a successful run, will trigger a 'user.password_change > done' callback event. 1350 * 1351 * @param string $user The updated user 1352 * @param string $password The new password 1353 * @return bool FALSE on error 1354 * @since 4.6.0 1355 * @package User 1356 * @example 1357 * if (change_user_password('login', 'WalrusWasDancing')) 1358 * { 1359 * echo "Password changed."; 1360 * } 1361 */ 1362 1363 function change_user_password($user, $password) 1364 { 1365 if (!$user || !$password) { 1366 return false; 1367 } 1368 1369 $hash = Txp::get('\Textpattern\Password\Hash')->hash($password); 1370 1371 if ( 1372 safe_update( 1373 'txp_users', 1374 "pass = '".doSlash($hash)."'", 1375 "name = '".doSlash($user)."'" 1376 ) === false 1377 ) { 1378 return false; 1379 } 1380 1381 callback_event('user.password_change', 'done', 0, compact('user', 'password', 'hash')); 1382 1383 return true; 1384 } 1385 1386 /** 1387 * Removes a user. 1388 * 1389 * The user's assets are assigned to the given new owner. 1390 * 1391 * On a successful run, will trigger a 'user.remove > done' callback event. 1392 * 1393 * @param string|array $user List of removed users 1394 * @param string $new_owner Assign assets to 1395 * @return bool FALSE on error 1396 * @since 4.6.0 1397 * @package User 1398 * @example 1399 * if (remove_user('user', 'new_owner')) 1400 * { 1401 * echo "Removed 'user' and assigned assets to 'new_owner'."; 1402 * } 1403 */ 1404 1405 function remove_user($user, $new_owner) 1406 { 1407 if (!$user || !$new_owner) { 1408 return false; 1409 } 1410 1411 $names = join(',', quote_list((array) $user)); 1412 1413 if (assign_user_assets($user, $new_owner) === false) { 1414 return false; 1415 } 1416 1417 if (safe_delete('txp_prefs', "user_name IN ($names)") === false) { 1418 return false; 1419 } 1420 1421 if (safe_delete('txp_users', "name IN ($names)") === false) { 1422 return false; 1423 } 1424 1425 callback_event('user.remove', 'done', 0, compact('user', 'new_owner')); 1426 1427 return true; 1428 } 1429 1430 /** 1431 * Renames a user. 1432 * 1433 * On a successful run, will trigger a 'user.rename > done' callback event. 1434 * 1435 * @param string $user Updated user 1436 * @param string $newname The new name 1437 * @return bool FALSE on error 1438 * @since 4.6.0 1439 * @package User 1440 * @example 1441 * if (rename_user('login', 'newname')) 1442 * { 1443 * echo "'login' renamed to 'newname'."; 1444 * } 1445 */ 1446 1447 function rename_user($user, $newname) 1448 { 1449 if (!is_scalar($user) || !is_valid_username($newname)) { 1450 return false; 1451 } 1452 1453 if (assign_user_assets($user, $newname) === false) { 1454 return false; 1455 } 1456 1457 if ( 1458 safe_update( 1459 'txp_users', 1460 "name = '".doSlash($newname)."'", 1461 "name = '".doSlash($user)."'" 1462 ) === false 1463 ) { 1464 return false; 1465 } 1466 1467 callback_event('user.rename', 'done', 0, compact('user', 'newname')); 1468 1469 return true; 1470 } 1471 1472 /** 1473 * Checks if a user exists. 1474 * 1475 * @param string $user The user 1476 * @return bool TRUE if the user exists 1477 * @since 4.6.0 1478 * @package User 1479 * @example 1480 * if (user_exists('john')) 1481 * { 1482 * echo "'john' exists."; 1483 * } 1484 */ 1485 1486 function user_exists($user) 1487 { 1488 return (bool) safe_row("name", 'txp_users', "name = '".doSlash($user)."'"); 1489 } 1490 1491 /** 1492 * Changes a user's group. 1493 * 1494 * On a successful run, will trigger a 'user.change_group > done' callback event. 1495 * 1496 * @param string|array $user Updated users 1497 * @param int $group The new group 1498 * @return bool FALSE on error 1499 * @since 4.6.0 1500 * @package User 1501 * @example 1502 * if (change_user_group('john', 1)) 1503 * { 1504 * echo "'john' is now publisher."; 1505 * } 1506 */ 1507 1508 function change_user_group($user, $group) 1509 { 1510 $levels = get_groups(); 1511 1512 if (!$user || !isset($levels[$group])) { 1513 return false; 1514 } 1515 1516 $names = join(',', quote_list((array) $user)); 1517 1518 if ( 1519 safe_update( 1520 'txp_users', 1521 "privs = ".intval($group), 1522 "name IN ($names)" 1523 ) === false 1524 ) { 1525 return false; 1526 } 1527 1528 callback_event('user.change_group', 'done', 0, compact('user', 'group')); 1529 1530 return true; 1531 } 1532 1533 /** 1534 * Validates the given user credentials. 1535 * 1536 * Validates a given login and a password combination. If the combination is 1537 * correct, the user's login name is returned, FALSE otherwise. 1538 * 1539 * If $log is TRUE, also checks that the user has permissions to access the 1540 * admin side interface. On success, updates the user's last access timestamp. 1541 * 1542 * @param string $user The login 1543 * @param string $password The password 1544 * @param bool $log If TRUE, requires privilege level greater than 'none' 1545 * @return string|bool The user's login name or FALSE on error 1546 * @package User 1547 */ 1548 1549 function txp_validate($user, $password, $log = true) 1550 { 1551 $safe_user = doSlash($user); 1552 $name = false; 1553 1554 $r = safe_row("name, pass, privs", 'txp_users', "name = '$safe_user'"); 1555 1556 if (!$r) { 1557 return false; 1558 } 1559 1560 // Check post-4.3-style passwords. 1561 if ($pass = Txp::get('\Textpattern\Password\Hash')->verify($password, $r['pass'])) { 1562 if (!$log || $r['privs'] > 0) { 1563 $name = $r['name']; 1564 } 1565 1566 if ($pass === true) { 1567 safe_update('txp_users', "pass = '".doSlash(Txp::get('\Textpattern\Password\Hash')->hash($password))."'", "name = '$safe_user'"); 1568 } 1569 } else { 1570 // No good password: check 4.3-style passwords. 1571 $pass = '*'.sha1(sha1($password, true)); 1572 1573 $name = safe_field("name", 'txp_users', 1574 "name = '$safe_user' AND privs > 0 AND (pass = UPPER('$pass') OR pass = LOWER('$pass'))"); 1575 1576 // Old password is good: migrate password to phpass. 1577 if ($name !== false) { 1578 safe_update('txp_users', "pass = '".doSlash(Txp::get('\Textpattern\Password\Hash')->hash($password))."'", "name = '$safe_user'"); 1579 } 1580 } 1581 1582 if ($name !== false && $log) { 1583 // Update the last access time. 1584 safe_update('txp_users', "last_access = NOW()", "name = '$safe_user'"); 1585 } 1586 1587 return $name; 1588 } 1589 1590 /** 1591 * Calculates a password hash. 1592 * 1593 * @param string $password The password 1594 * @return string A hash 1595 * @see PASSWORD_COMPLEXITY 1596 * @see PASSWORD_PORTABILITY 1597 * @package User 1598 */ 1599 1600 function txp_hash_password($password) 1601 { 1602 static $phpass = null; 1603 1604 if (!$phpass) { 1605 include_once txpath.'/lib/PasswordHash.php'; 1606 $phpass = new PasswordHash(PASSWORD_COMPLEXITY, PASSWORD_PORTABILITY); 1607 } 1608 1609 return $phpass->HashPassword($password); 1610 } 1611 1612 /** 1613 * Create a secure token hash in the database from the passed information. 1614 * 1615 * @param int $ref Reference to the user's account (user_id) 1616 * @param string $type Flavour of token to create 1617 * @param int $expiryTimestamp UNIX timestamp of when the token will expire 1618 * @param string $pass Password, used as part of the token generation 1619 * @param string $nonce Random nonce associated with the user's account 1620 * @return string Secure token suitable for emailing as part of a link 1621 * @since 4.6.1 1622 */ 1623 1624 function generate_user_token($ref, $type, $expiryTimestamp, $pass, $nonce) 1625 { 1626 $ref = assert_int($ref); 1627 $expiry = strftime('%Y-%m-%d %H:%M:%S', $expiryTimestamp); 1628 1629 // The selector becomes an indirect reference to the user row id, 1630 // and thus does not leak information when publicly displayed. 1631 $selector = Txp::get('\Textpattern\Password\Random')->generate(12); 1632 1633 // Use a hash of the nonce, selector and password. 1634 // This ensures that requests expire automatically when: 1635 // a) The person logs in, or 1636 // b) They successfully set/change their password 1637 // Using the selector in the hash just injects randomness, otherwise two requests 1638 // back-to-back would generate the same code. 1639 // Old requests for the same user id are purged when password is set. 1640 $token = bin2hex(pack('H*', substr(hash(HASHING_ALGORITHM, $nonce.$selector.$pass), 0, SALT_LENGTH))); 1641 $user_token = $token.$selector; 1642 1643 // Remove any previous activation tokens and insert the new one. 1644 $safe_type = doSlash($type); 1645 safe_delete("txp_token", "reference_id = '$ref' AND type = '$safe_type'"); 1646 safe_insert("txp_token", 1647 "reference_id = '$ref', 1648 type = '$safe_type', 1649 selector = '".doSlash($selector)."', 1650 token = '".doSlash($token)."', 1651 expires = '".doSlash($expiry)."' 1652 "); 1653 1654 return $user_token; 1655 } 1656 1657 /** 1658 * Display a modal client message in response to an AJAX request and 1659 * halt execution. 1660 * 1661 * @param string|array $thing The $thing[0] is the message's text; $thing[1] is the message's type (one of E_ERROR or E_WARNING, anything else meaning "success"; not used) 1662 * @since 4.5.0 1663 * @package Ajax 1664 */ 1665 1666 function modal_halt($thing) 1667 { 1668 global $app_mode, $theme; 1669 1670 if ($app_mode == 'async') { 1671 send_script_response($theme->announce_async($thing, true)); 1672 die(); 1673 } 1674 } 1675 1676 /** 1677 * Sends an activity message to the client. 1678 * 1679 * @param string|array $message The message 1680 * @param int $type The type, either 0, E_ERROR, E_WARNING 1681 * @param int $flags Flags, consisting of TEXTPATTERN_ANNOUNCE_ADAPTIVE | TEXTPATTERN_ANNOUNCE_ASYNC | TEXTPATTERN_ANNOUNCE_MODAL | TEXTPATTERN_ANNOUNCE_REGULAR 1682 * @package Announce 1683 * @since 4.6.0 1684 * @example 1685 * echo announce('My message', E_WARNING); 1686 */ 1687 1688 function announce($message, $type = 0, $flags = TEXTPATTERN_ANNOUNCE_ADAPTIVE) 1689 { 1690 global $app_mode, $theme; 1691 1692 if (!is_array($message)) { 1693 $message = array($message, $type); 1694 } 1695 1696 if ($flags & TEXTPATTERN_ANNOUNCE_ASYNC || ($flags & TEXTPATTERN_ANNOUNCE_ADAPTIVE && $app_mode === 'async')) { 1697 return $theme->announce_async($message); 1698 } 1699 1700 if ($flags & TEXTPATTERN_ANNOUNCE_MODAL) { 1701 return $theme->announce_async($message, true); 1702 } 1703 1704 return $theme->announce($message); 1705 } 1706 1707 /** 1708 * Loads date definitions from a localisation file. 1709 * 1710 * @param string $lang The language 1711 * @package L10n 1712 * @deprecated in 4.6.0 1713 */ 1714 1715 function load_lang_dates($lang) 1716 { 1717 $filename = is_file(txpath.'/lang/'.$lang.'_dates.txt') ? 1718 txpath.'/lang/'.$lang.'_dates.txt' : 1719 txpath.'/lang/en-gb_dates.txt'; 1720 $file = @file(txpath.'/lang/'.$lang.'_dates.txt', 'r'); 1721 1722 if (is_array($file)) { 1723 foreach ($file as $line) { 1724 if ($line[0] == '#' || strlen($line) < 2) { 1725 continue; 1726 } 1727 1728 list($name, $val) = explode('=>', $line, 2); 1729 $out[trim($name)] = trim($val); 1730 } 1731 1732 return $out; 1733 } 1734 1735 return false; 1736 } 1737 1738 /** 1739 * Gets language strings for the given event. 1740 * 1741 * If no $lang is specified, the strings are loaded from the currently 1742 * active language. 1743 * 1744 * @param string $event The event to get, e.g. "common", "admin", "public" 1745 * @param string $lang The language code 1746 * @return array|string Array of string on success, or an empty string when no strings were found 1747 * @package L10n 1748 * @see load_lang() 1749 * @example 1750 * print_r( 1751 * load_lang_event('common') 1752 * ); 1753 */ 1754 1755 function load_lang_event($event, $lang = LANG) 1756 { 1757 $installed = (false !== safe_field("name", 'txp_lang', "lang = '".doSlash($lang)."' LIMIT 1")); 1758 1759 $lang_code = ($installed) ? $lang : TEXTPATTERN_DEFAULT_LANG; 1760 1761 $rs = safe_rows_start("name, data", 'txp_lang', "lang = '".doSlash($lang_code)."' AND event = '".doSlash($event)."'"); 1762 1763 $out = array(); 1764 1765 if ($rs && !empty($rs)) { 1766 while ($a = nextRow($rs)) { 1767 $out[$a['name']] = $a['data']; 1768 } 1769 } 1770 1771 return ($out) ? $out : ''; 1772 } 1773 1774 /** 1775 * Installs localisation strings from a Textpack. 1776 * 1777 * @param string $textpack The Textpack to install 1778 * @param bool $add_new_langs If TRUE, installs strings for any included language 1779 * @return int Number of installed strings 1780 * @package L10n 1781 * @deprecated in 4.7.0 1782 */ 1783 1784 function install_textpack($textpack, $add_new_langs = false) 1785 { 1786 return Txp::get('\Textpattern\L10n\Lang')->installTextpack($textpack, $add_new_langs); 1787 } 1788 1789 /** 1790 * Generate a ciphered token. 1791 * 1792 * The token is reproducible, unique among sites and users, expires later. 1793 * 1794 * @return string The token 1795 * @see bouncer() 1796 * @package CSRF 1797 */ 1798 1799 function form_token() 1800 { 1801 static $token = null; 1802 global $txp_user; 1803 1804 // Generate a ciphered token from the current user's nonce (thus valid for 1805 // login time plus 30 days) and a pinch of salt from the blog UID. 1806 if ($token === null && $txp_user) { 1807 $nonce = safe_field("nonce", 'txp_users', "name = '".doSlash($txp_user)."'"); 1808 $token = md5($nonce.get_pref('blog_uid')); 1809 } 1810 1811 return $token; 1812 } 1813 1814 /** 1815 * Validates admin steps and protects against CSRF attempts using tokens. 1816 * 1817 * Takes an admin step and validates it against an array of valid steps. 1818 * The valid steps array indicates the step's token based session riding 1819 * protection needs. 1820 * 1821 * If the step requires CSRF token protection, and the request doesn't come with 1822 * a valid token, the request is terminated, defeating any CSRF attempts. 1823 * 1824 * If the $step isn't in valid steps, it returns FALSE, but the request 1825 * isn't terminated. If the $step is valid and passes CSRF validation, 1826 * returns TRUE. 1827 * 1828 * @param string $step Requested admin step 1829 * @param array $steps An array of valid steps with flag indicating CSRF needs, e.g. array('savething' => true, 'listthings' => false) 1830 * @return bool If the $step is valid, proceeds and returns TRUE. Dies on CSRF attempt. 1831 * @see form_token() 1832 * @package CSRF 1833 * @example 1834 * global $step; 1835 * if (bouncer($step, array( 1836 * 'browse' => false, 1837 * 'edit' => false, 1838 * 'save' => true, 1839 * 'multi_edit' => true, 1840 * ))) 1841 * { 1842 * echo "The '{$step}' is valid."; 1843 * } 1844 */ 1845 1846 function bouncer($step, $steps) 1847 { 1848 global $event; 1849 1850 if (empty($step)) { 1851 return true; 1852 } 1853 1854 // Validate step. 1855 if (!array_key_exists($step, $steps)) { 1856 return false; 1857 } 1858 1859 // Does this step require a token? 1860 if (!$steps[$step]) { 1861 return true; 1862 } 1863 1864 // Validate token. 1865 if (gps('_txp_token') === form_token()) { 1866 return true; 1867 } 1868 1869 die(gTxt('get_off_my_lawn', array( 1870 '{event}' => $event, 1871 '{step}' => $step, 1872 ))); 1873 } 1874 1875 /** 1876 * Checks install's file integrity and returns results. 1877 * 1878 * Depending on the given $flags this function will either return an array of 1879 * file statuses, checksums or the digest of the install. It can also return the 1880 * parsed contents of the checksum file. 1881 * 1882 * @param int $flags Options are INTEGRITY_MD5 | INTEGRITY_STATUS | INTEGRITY_REALPATH | INTEGRITY_DIGEST 1883 * @return array|bool Array of files and status, or FALSE on error 1884 * @since 4.6.0 1885 * @package Debug 1886 * @example 1887 * print_r( 1888 * check_file_integrity(INTEGRITY_MD5 | INTEGRITY_REALPATH) 1889 * ); 1890 */ 1891 1892 function check_file_integrity($flags = INTEGRITY_STATUS) 1893 { 1894 static $files = null, $files_md5 = array(), $checksum_table = array(); 1895 1896 if ($files === null) { 1897 if ($cs = @file(txpath.'/checksums.txt')) { 1898 $files = array(); 1899 1900 foreach ($cs as $c) { 1901 if (preg_match('@^(\S+):(?: r?(\S+) | )\(?(.{32})\)?$@', trim($c), $m)) { 1902 list(, $relative, $r, $md5) = $m; 1903 $file = realpath(txpath.$relative); 1904 $checksum_table[$relative] = $md5; 1905 1906 if ($file === false) { 1907 $files[$relative] = INTEGRITY_MISSING; 1908 $files_md5[$relative] = false; 1909 continue; 1910 } 1911 1912 if (!is_readable($file)) { 1913 $files[$relative] = INTEGRITY_NOT_READABLE; 1914 $files_md5[$relative] = false; 1915 continue; 1916 } 1917 1918 if (!is_file($file)) { 1919 $files[$relative] = INTEGRITY_NOT_FILE; 1920 $files_md5[$relative] = false; 1921 continue; 1922 } 1923 1924 $files_md5[$relative] = md5_file($file); 1925 1926 if ($files_md5[$relative] !== $md5) { 1927 $files[$relative] = INTEGRITY_MODIFIED; 1928 } else { 1929 $files[$relative] = INTEGRITY_GOOD; 1930 } 1931 } 1932 } 1933 1934 if (!get_pref('enable_xmlrpc_server', true)) { 1935 unset( 1936 $files_md5['/../rpc/index.php'], 1937 $files_md5['/../rpc/TXP_RPCServer.php'], 1938 $files['/../rpc/index.php'], 1939 $files['/../rpc/TXP_RPCServer.php'] 1940 ); 1941 } 1942 } else { 1943 $files_md5 = $files = false; 1944 } 1945 } 1946 1947 if ($flags & INTEGRITY_DIGEST) { 1948 return $files_md5 ? md5(implode(n, $files_md5)) : false; 1949 } 1950 1951 if ($flags & INTEGRITY_TABLE) { 1952 return $checksum_table ? $checksum_table : false; 1953 } 1954 1955 $return = $files; 1956 1957 if ($flags & INTEGRITY_MD5) { 1958 $return = $files_md5; 1959 } 1960 1961 if ($return && $flags & INTEGRITY_REALPATH) { 1962 $relative = array(); 1963 1964 foreach ($return as $path => $status) { 1965 $realpath = realpath(txpath.$path); 1966 $relative[!$realpath ? $path : $realpath] = $status; 1967 } 1968 1969 return $relative; 1970 } 1971 1972 return $return; 1973 } 1974 1975 /** 1976 * Assert system requirements. 1977 * 1978 * @access private 1979 */ 1980 1981 function assert_system_requirements() 1982 { 1983 if (version_compare(REQUIRED_PHP_VERSION, PHP_VERSION) > 0) { 1984 txp_die('This server runs PHP version '.PHP_VERSION.'. Textpattern needs PHP version '.REQUIRED_PHP_VERSION.' or better.'); 1985 } 1986 1987 if (!extension_loaded('simplexml')) { 1988 txp_die('This server does not have the required SimpleXML library installed (php-xml). Please install it.'); 1989 } 1990 } 1991 1992 /** 1993 * Get Theme prefs 1994 * Now Textpattern does not support themes. If the setup folder is deleted, it will return an empty array. 1995 */ 1996 1997 function get_prefs_theme() 1998 { 1999 $out = json_decode(txp_get_contents(txpath.'/setup/data/theme.prefs'), true); 2000 if (empty($out)) { 2001 return array(); 2002 } 2003 2004 return $out; 2005 } 2006 2007 2008 /** 2009 * Renders an array of available ways to display the date. 2010 * @return array 2011 */ 2012 2013 function txp_dateformats() 2014 { 2015 $dayname = '%A'; 2016 $dayshort = '%a'; 2017 $daynum = is_numeric(@strftime('%e')) ? '%e' : '%d'; 2018 $daynumlead = '%d'; 2019 $daynumord = is_numeric(substr(trim(@strftime('%Oe')), 0, 1)) ? '%Oe' : $daynum; 2020 $monthname = '%B'; 2021 $monthshort = '%b'; 2022 $monthnum = '%m'; 2023 $year = '%Y'; 2024 $yearshort = '%y'; 2025 $time24 = '%H:%M'; 2026 $time12 = @strftime('%p') ? '%I:%M %p' : $time24; 2027 $date = @strftime('%x') ? '%x' : '%Y-%m-%d'; 2028 2029 return array( 2030 "$monthshort $daynumord, $time12", 2031 "$daynum.$monthnum.$yearshort", 2032 "$daynumord $monthname, $time12", 2033 "$yearshort.$monthnum.$daynumlead, $time12", 2034 "$dayshort $monthshort $daynumord, $time12", 2035 "$dayname $monthname $daynumord, $year", 2036 "$monthshort $daynumord", 2037 "$daynumord $monthname $yearshort", 2038 "$daynumord $monthnum $year - $time24", 2039 "$daynumord $monthname $year", 2040 "$daynumord $monthname $year, $time24", 2041 "$daynumord. $monthname $year", 2042 "$daynumord. $monthname $year, $time24", 2043 "$year-$monthnum-$daynumlead", 2044 "$year-$daynumlead-$monthnum", 2045 "$date $time12", 2046 "$date", 2047 "$time24", 2048 "$time12", 2049 "$year-$monthnum-$daynumlead $time24", 2050 "since" 2051 ); 2052 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title