Textpattern | PHP Cross Reference | Content Management Systems |
Description: Collection of miscellaneous tools.
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 miscellaneous tools. 26 * 27 * @package Misc 28 */ 29 30 /** 31 * Strips NULL bytes. 32 * 33 * @param string|array $in The input value 34 * @return mixed 35 */ 36 37 function deNull($in) 38 { 39 return is_array($in) ? doArray($in, 'deNull') : strtr($in, array("\0" => '')); 40 } 41 42 /** 43 * Strips carriage returns and linefeeds. 44 * 45 * @param string|array $in The input value 46 * @return mixed 47 */ 48 49 function deCRLF($in) 50 { 51 return is_array($in) ? doArray($in, 'deCRLF') : strtr($in, array( 52 "\n" => '', 53 "\r" => '', 54 )); 55 } 56 57 /** 58 * Applies a callback to a given string or an array. 59 * 60 * @param string|array $in An array or a string to run through the callback function 61 * @param callback $function The callback function 62 * @return mixed 63 * @example 64 * echo doArray(array('value1', 'value2'), 'intval'); 65 */ 66 67 function doArray($in, $function) 68 { 69 if (is_array($in)) { 70 return array_map($function, $in); 71 } 72 73 if (is_array($function)) { 74 return call_user_func($function, $in); 75 } 76 77 return $function($in); 78 } 79 80 /** 81 * Un-quotes a quoted string or an array of values. 82 * 83 * @param string|array $in The input value 84 * @return mixed 85 */ 86 87 function doStrip($in) 88 { 89 return is_array($in) ? doArray($in, 'doStrip') : doArray($in, 'stripslashes'); 90 } 91 92 /** 93 * Strips HTML and PHP tags from a string or an array. 94 * 95 * @param string|array $in The input value 96 * @return mixed 97 * @example 98 * echo doStripTags('<p>Hello world!</p>'); 99 */ 100 101 function doStripTags($in) 102 { 103 return is_array($in) ? doArray($in, 'doStripTags') : doArray($in, 'strip_tags'); 104 } 105 106 /** 107 * Converts entity escaped brackets back to characters. 108 * 109 * @param string|array $in The input value 110 * @return mixed 111 */ 112 113 function doDeEnt($in) 114 { 115 return doArray($in, 'deEntBrackets'); 116 } 117 118 /** 119 * Converts entity escaped brackets back to characters. 120 * 121 * @param string $in The input value 122 * @return string 123 */ 124 125 function deEntBrackets($in) 126 { 127 $array = array( 128 '<' => '<', 129 '<' => '<', 130 '<' => '<', 131 '>' => '>', 132 '>' => '>', 133 '>' => '>', 134 ); 135 136 foreach ($array as $k => $v) { 137 $in = preg_replace("/".preg_quote($k)."/i", $v, $in); 138 } 139 140 return $in; 141 } 142 143 /** 144 * Escapes special characters for use in an SQL statement. 145 * 146 * Always use this function when dealing with user-defined values in SQL 147 * statements. If this function is not used to escape user-defined data in a 148 * statement, the query is vulnerable to SQL injection attacks. 149 * 150 * @param string|array $in The input value 151 * @return mixed An array of escaped values or a string depending on $in 152 * @package DB 153 * @example 154 * echo safe_field('column', 'table', "color = '" . doSlash(gps('color')) . "'"); 155 */ 156 157 function doSlash($in) 158 { 159 return doArray($in, 'safe_escape'); 160 } 161 162 /** 163 * Escape SQL LIKE pattern's wildcards for use in an SQL statement. 164 * 165 * @param string|array $in The input value 166 * @return mixed An array of escaped values or a string depending on $in 167 * @since 4.6.0 168 * @package DB 169 * @example 170 * echo safe_field('column', 'table', "color LIKE '" . doLike(gps('color')) . "'"); 171 */ 172 173 function doLike($in) 174 { 175 return doArray($in, 'safe_escape_like'); 176 } 177 178 /** 179 * A shell for htmlspecialchars() with $flags defaulting to ENT_QUOTES. 180 * 181 * @param string $string The string being converted 182 * @param int $flags A bitmask of one or more flags. The default is ENT_QUOTES 183 * @param string $encoding Defines encoding used in conversion. The default is UTF-8 184 * @param bool $double_encode When double_encode is turned off PHP will not encode existing HTML entities, the default is to convert everything 185 * @return string 186 * @see https://www.php.net/manual/en/function.htmlspecialchars.php 187 * @since 4.5.0 188 * @package Filter 189 */ 190 191 function txpspecialchars($string, $flags = ENT_QUOTES, $encoding = 'UTF-8', $double_encode = true) 192 { 193 // Ignore ENT_HTML5 and ENT_XHTML for now. 194 // ENT_HTML5 and ENT_XHTML are defined in PHP 5.4+ but we consistently encode single quotes as ' in any doctype. 195 // global $prefs; 196 // static $h5 = null; 197 // 198 // if (defined(ENT_HTML5)) { 199 // if ($h5 === null) { 200 // $h5 = ($prefs['doctype'] == 'html5' && txpinterface == 'public'); 201 // } 202 // 203 // if ($h5) { 204 // $flags = ($flags | ENT_HTML5) & ~ENT_HTML401; 205 // } 206 // } 207 // 208 return htmlspecialchars($string, $flags, $encoding, $double_encode); 209 } 210 211 /** 212 * Converts special characters to HTML entities. 213 * 214 * @param array|string $in The input value 215 * @return mixed The array or string with HTML syntax characters escaped 216 * @package Filter 217 */ 218 219 function doSpecial($in) 220 { 221 return doArray($in, 'txpspecialchars'); 222 } 223 224 /** 225 * Converts the given value to NULL. 226 * 227 * @param mixed $a The input value 228 * @return null 229 * @package Filter 230 * @access private 231 */ 232 233 function _null($a) 234 { 235 return null; 236 } 237 238 /** 239 * Converts an array of values to NULL. 240 * 241 * @param array $in The array 242 * @return array 243 * @package Filter 244 */ 245 246 function array_null($in) 247 { 248 return array_map('_null', $in); 249 } 250 251 /** 252 * Escapes a page title. Converts <, >, ', " characters to HTML entities. 253 * 254 * @param string $title The input string 255 * @return string The string escaped 256 * @package Filter 257 */ 258 259 function escape_title($title) 260 { 261 return strtr($title, array( 262 '<' => '<', 263 '>' => '>', 264 "'" => ''', 265 '"' => '"', 266 )); 267 } 268 269 /** 270 * Sanitises a string for use in a JavaScript string. 271 * 272 * Escapes \, \n, \r, " and ' characters. It removes 'PARAGRAPH SEPARATOR' 273 * (U+2029) and 'LINE SEPARATOR' (U+2028). When you need to pass a string 274 * from PHP to JavaScript, use this function to sanitise the value to avoid 275 * XSS attempts. 276 * 277 * @param string $js JavaScript input 278 * @return string Escaped JavaScript 279 * @since 4.4.0 280 * @package Filter 281 */ 282 283 function escape_js($js) 284 { 285 $js = preg_replace('/[\x{2028}\x{2029}]/u', '', $js); 286 287 return addcslashes($js, "\\\'\"\n\r"); 288 } 289 290 /** 291 * Escapes CDATA section for an XML document. 292 * 293 * @param string $str The string 294 * @return string XML representation wrapped in CDATA tags 295 * @package XML 296 */ 297 298 function escape_cdata($str) 299 { 300 return '<![CDATA['.str_replace(']]>', ']]]><![CDATA[]>', $str).']]>'; 301 } 302 303 /** 304 * Returns a localisation string. 305 * 306 * @param string $var String name 307 * @param array $atts Replacement pairs 308 * @param string $escape Convert special characters to HTML entities. Either "html" or "" 309 * @return string A localisation string 310 * @package L10n 311 */ 312 313 function gTxt($var, $atts = array(), $escape = 'html') 314 { 315 global $event, $plugin, $txp_current_plugin; 316 static $txpLang = null; 317 318 if ($txpLang === null) { 319 $txpLang = Txp::get('\Textpattern\L10n\Lang'); 320 $lang = txpinterface == 'admin' ? get_pref('language_ui', TEXTPATTERN_DEFAULT_LANG) : LANG; 321 $loaded = $txpLang->load($lang, true); 322 323 if (empty($loaded) || !in_array($event, $loaded)) { 324 load_lang($lang, $event); 325 } 326 } 327 328 // Hackish 329 if (isset($txp_current_plugin) && isset($plugin['textpack'])) { 330 $txpLang->loadTextpack($plugin['textpack']); 331 unset($plugin['textpack']); 332 } 333 334 return $txpLang->txt($var, $atts, $escape); 335 } 336 337 /** 338 * Returns given timestamp in a format of 01 Jan 2001 15:19:16. 339 * 340 * @param int $timestamp The UNIX timestamp 341 * @return string A formatted date 342 * @access private 343 * @see safe_stftime() 344 * @package DateTime 345 * @example 346 * echo gTime(); 347 */ 348 349 function gTime($timestamp = 0) 350 { 351 return safe_strftime('%d %b %Y %X', $timestamp); 352 } 353 354 /** 355 * Creates a dumpfile from a backtrace and outputs given parameters. 356 * 357 * @package Debug 358 */ 359 360 function dmp() 361 { 362 static $f = false; 363 364 if (defined('txpdmpfile')) { 365 global $prefs; 366 367 if (!$f) { 368 $f = fopen($prefs['tempdir'].'/'.txpdmpfile, 'a'); 369 } 370 371 $stack = get_caller(); 372 fwrite($f, "\n[".$stack[0].t.safe_strftime('iso8601')."]\n"); 373 } 374 375 $a = func_get_args(); 376 377 if (!$f) { 378 echo "<pre dir=\"auto\">".n; 379 } 380 381 foreach ($a as $thing) { 382 $out = is_scalar($thing) ? strval($thing) : var_export($thing, true); 383 384 if ($f) { 385 fwrite($f, $out.n); 386 } else { 387 echo txpspecialchars($out).n; 388 } 389 } 390 391 if (!$f) { 392 echo "</pre>".n; 393 } 394 } 395 396 /** 397 * Gets the given language's strings from the database. 398 * 399 * Fetches the given language from the database and returns the strings 400 * as an array. 401 * 402 * If no $events is specified, only appropriate strings for the current context 403 * are returned. If 'txpinterface' constant equals 'admin' all strings are 404 * returned. Otherwise, only strings from events 'common' and 'public'. 405 * 406 * If $events is FALSE, returns all strings. 407 * 408 * @param string $lang The language code 409 * @param array|string|bool $events An array of loaded events 410 * @return array 411 * @package L10n 412 * @example 413 * print_r( 414 * load_lang('en-gb', false) 415 * ); 416 */ 417 418 function load_lang($lang, $events = null) 419 { 420 global $production_status, $event, $textarray; 421 422 isset($textarray) or $textarray = array(); 423 $textarray = array_merge($textarray, Txp::get('\Textpattern\L10n\Lang')->load($lang, $events)); 424 425 if (($production_status !== 'live' || $event === 'diag') 426 && @$debug = parse_ini_file(txpath.DS.'mode.ini') 427 ) { 428 $textarray += (array)$debug; 429 Txp::get('\Textpattern\L10n\Lang')->setPack($textarray); 430 } 431 432 return $textarray; 433 } 434 435 /** 436 * Gets a list of user groups. 437 * 438 * @return array 439 * @package User 440 * @example 441 * print_r( 442 * get_groups() 443 * ); 444 */ 445 446 function get_groups() 447 { 448 global $txp_groups; 449 450 return doArray($txp_groups, 'gTxt'); 451 } 452 453 /** 454 * Checks if a user has privileges to the given resource. 455 * 456 * @param string $res The resource 457 * @param mixed $user The user. If no user name is supplied, assume the current logged in user 458 * @return bool 459 * @package User 460 * @example 461 * add_privs('my_privilege_resource', '1,2,3'); 462 * if (has_privs('my_privilege_resource', 'username')) 463 * { 464 * echo "'username' has privileges to 'my_privilege_resource'."; 465 * } 466 */ 467 468 function has_privs($res = null, $user = '') 469 { 470 global $txp_user, $txp_permissions; 471 static $privs; 472 473 if (is_array($user)) { 474 $level = isset($user['privs']) ? $user['privs'] : null; 475 $user = isset($user['name']) ? $user['name'] : ''; 476 } 477 478 $user = (string) $user; 479 480 if ($user === '') { 481 $user = (string) $txp_user; 482 } 483 484 if ($user !== '') { 485 if (!isset($privs[$user])) { 486 $privs[$user] = isset($level) ? 487 $level : 488 safe_field("privs", 'txp_users', "name = '".doSlash($user)."'"); 489 } 490 491 if (!isset($res)) { 492 return $privs[$user]; 493 } elseif (isset($txp_permissions[$res]) && $privs[$user] && $txp_permissions[$res]) { 494 return in_list($privs[$user], $txp_permissions[$res]); 495 } 496 } 497 498 return false; 499 } 500 501 /** 502 * Adds dynamic privileges. 503 * 504 * @param array $pluggable The array, see global $txp_options 505 * @since 4.7.2 506 * @package User 507 */ 508 509 function plug_privs($pluggable = null, $user = null) 510 { 511 global $txp_options; 512 513 isset($pluggable) or $pluggable = $txp_options; 514 $level = isset($user['privs']) ? $user['privs'] : has_privs(); 515 516 foreach ((array)$pluggable as $pref => $pane) { 517 if (is_array($pane)) { 518 if (isset($pane[0])) { 519 if (!in_list($level, $pane[0])) { 520 return; 521 } 522 523 unset($pane[0]); 524 } 525 } else { 526 $pane = array('prefs.'.$pref => $pane); 527 } 528 529 array_walk($pane, function (&$item) use ($level) { 530 if ($item === true) { 531 $item = $level; 532 } 533 }); 534 535 if (get_pref($pref)) { 536 add_privs($pane); 537 } else { 538 add_privs(array_fill_keys(array_keys($pane), null)); 539 } 540 } 541 } 542 543 /** 544 * Grants privileges to user-groups. 545 * 546 * Will not let you override existing privs. 547 * 548 * @param mixed $res The resource 549 * @param string $perm List of user-groups, e.g. '1,2,3' 550 * @package User 551 * @example 552 * add_privs('my_admin_side_panel_event', '1,2,3,4,5'); 553 */ 554 555 function add_privs($res, $perm = '1') 556 { 557 global $txp_permissions; 558 559 if (!is_array($res)) { 560 $res = array($res => $perm); 561 } 562 563 foreach ($res as $priv => $group) { 564 if ($group === null) { 565 unset($txp_permissions[$priv]); 566 } else { 567 $group .= (empty($txp_permissions[$priv]) ? '' : ','.$txp_permissions[$priv]); 568 $group = join(',', do_list_unique($group)); 569 $txp_permissions[$priv] = $group; 570 } 571 } 572 } 573 574 /** 575 * Require privileges from a user to the given resource. 576 * 577 * Terminates the script if user doesn't have required privileges. 578 * 579 * @param string|null $res The resource, or NULL 580 * @param string $user The user. If no user name is supplied, assume the current logged in user 581 * @package User 582 * @example 583 * require_privs('article.edit'); 584 */ 585 586 function require_privs($res = null, $user = '') 587 { 588 if ($res === null || !has_privs($res, $user)) { 589 pagetop(gTxt('restricted_area')); 590 echo graf(gTxt('restricted_area'), array('class' => 'restricted-area')); 591 end_page(); 592 exit; 593 } 594 } 595 596 /** 597 * Gets a list of users having access to a resource. 598 * 599 * @param string $res The resource, e.g. 'article.edit.published' 600 * @return array A list of usernames 601 * @since 4.5.0 602 * @package User 603 */ 604 605 function the_privileged($res, $real = false) 606 { 607 global $txp_permissions; 608 609 $out = array(); 610 611 if (isset($txp_permissions[$res])) { 612 foreach (safe_rows("name, RealName", 'txp_users', "FIND_IN_SET(privs, '".$txp_permissions[$res]."') ORDER BY ".($real ? "RealName" : "name")." ASC") as $user) { 613 extract($user); 614 $out[$name] = $real ? $RealName : $name; 615 } 616 } 617 618 return $out; 619 } 620 621 /** 622 * Gets the dimensions of an image for a HTML <img> tag. 623 * 624 * @param string $name The filename 625 * @return string|bool height="100" width="40", or FALSE on failure 626 * @package Image 627 * @example 628 * if ($size = sizeImage('/path/to/image.png')) 629 * { 630 * echo "<img src='image.png' {$size} />"; 631 * } 632 */ 633 634 function sizeImage($name) 635 { 636 $size = @getimagesize($name); 637 638 return is_array($size) ? $size[3] : false; 639 } 640 641 /** 642 * Gets an image as an array. 643 * 644 * @param int $id image ID 645 * @param string $name image name 646 * @return array|bool An image data array, or FALSE on failure 647 * @package Image 648 * @example 649 * if ($image = imageFetchInfo($id)) 650 * { 651 * print_r($image); 652 * } 653 */ 654 655 function imageFetchInfo($id = "", $name = "") 656 { 657 global $thisimage, $p; 658 static $cache = array(); 659 660 if ($id) { 661 if (isset($cache['i'][$id])) { 662 return $cache['i'][$id]; 663 } else { 664 $where = 'id = '.intval($id).' LIMIT 1'; 665 } 666 } elseif ($name) { 667 if (isset($cache['n'][$name])) { 668 return $cache['n'][$name]; 669 } else { 670 $where = "name = '".doSlash($name)."' LIMIT 1"; 671 } 672 } elseif ($thisimage) { 673 $id = (int) $thisimage['id']; 674 return $cache['i'][$id] = $thisimage; 675 } elseif ($p) { 676 if (isset($cache['i'][$p])) { 677 return $cache['i'][$p]; 678 } else { 679 $where = 'id = '.intval($p).' LIMIT 1'; 680 } 681 } else { 682 assert_image(); 683 return false; 684 } 685 686 $rs = safe_row("*", 'txp_image', $where); 687 688 if ($rs) { 689 $id = (int) $rs['id']; 690 return $cache['i'][$id] = image_format_info($rs); 691 } else { 692 trigger_error(gTxt('unknown_image')); 693 } 694 695 return false; 696 } 697 698 /** 699 * Formats image info. 700 * 701 * Takes an image data array generated by imageFetchInfo() and formats the contents. 702 * 703 * @param array $image The image 704 * @return array 705 * @see imageFetchInfo() 706 * @access private 707 * @package Image 708 */ 709 710 function image_format_info($image) 711 { 712 if (($unix_ts = @strtotime($image['date'])) > 0) { 713 $image['date'] = $unix_ts; 714 } 715 716 return $image; 717 } 718 719 /** 720 * Formats link info. 721 * 722 * @param array $link The link to format 723 * @return array Formatted link data 724 * @access private 725 * @package Link 726 */ 727 728 function link_format_info($link) 729 { 730 if (($unix_ts = @strtotime($link['date'])) > 0) { 731 $link['date'] = $unix_ts; 732 } 733 734 return $link; 735 } 736 737 /** 738 * Gets a HTTP GET or POST parameter. 739 * 740 * Internally strips CRLF from GET parameters and removes NULL bytes. 741 * 742 * @param string $thing The parameter to get 743 * @return string|array The value of $thing, or an empty string 744 * @package Network 745 * @example 746 * if (gps('sky') == 'blue' && gps('roses') == 'red') 747 * { 748 * echo 'Roses are red, sky is blue.'; 749 * } 750 */ 751 752 function gps($thing, $default = '') 753 { 754 global $pretext; 755 756 if (isset($_GET[$thing])) { 757 $out = $_GET[$thing]; 758 $out = doArray($out, 'deCRLF'); 759 } elseif (isset($_POST[$thing])) { 760 $out = $_POST[$thing]; 761 } elseif (is_numeric($thing) && isset($pretext[abs($thing)])) { 762 $thing >= 0 or $thing += $pretext[0] + 1; 763 $out = $pretext[$thing]; 764 } else { 765 return $default; 766 } 767 768 $out = doArray($out, 'deNull'); 769 770 return $out; 771 } 772 773 /** 774 * Gets an array of HTTP GET or POST parameters. 775 * 776 * @param array $array The parameters to extract 777 * @return array 778 * @package Network 779 * @example 780 * extract(gpsa(array('sky', 'roses')); 781 * if ($sky == 'blue' && $roses == 'red') 782 * { 783 * echo 'Roses are red, sky is blue.'; 784 * } 785 */ 786 787 function gpsa($array) 788 { 789 if (is_array($array)) { 790 $out = array(); 791 792 foreach ($array as $a) { 793 $out[$a] = gps($a); 794 } 795 796 return $out; 797 } 798 799 return false; 800 } 801 802 /** 803 * Gets a HTTP POST parameter. 804 * 805 * Internally removes NULL bytes. 806 * 807 * @param string $thing The parameter to get 808 * @return string|array The value of $thing, or an empty string 809 * @package Network 810 * @example 811 * if (ps('sky') == 'blue' && ps('roses') == 'red') 812 * { 813 * echo 'Roses are red, sky is blue.'; 814 * } 815 */ 816 817 function ps($thing) 818 { 819 $out = ''; 820 821 if (isset($_POST[$thing])) { 822 $out = $_POST[$thing]; 823 } 824 825 $out = doArray($out, 'deNull'); 826 827 return $out; 828 } 829 830 /** 831 * Gets an array of HTTP POST parameters. 832 * 833 * @param array $array The parameters to extract 834 * @return array 835 * @package Network 836 * @example 837 * extract(psa(array('sky', 'roses')); 838 * if ($sky == 'blue' && $roses == 'red') 839 * { 840 * echo 'Roses are red, sky is blue.'; 841 * } 842 */ 843 844 function psa($array) 845 { 846 foreach ($array as $a) { 847 $out[$a] = ps($a); 848 } 849 850 return $out; 851 } 852 853 /** 854 * Gets an array of HTTP POST parameters and strips HTML and PHP tags 855 * from values. 856 * 857 * @param array $array The parameters to extract 858 * @return array 859 * @package Network 860 */ 861 862 function psas($array) 863 { 864 foreach ($array as $a) { 865 $out[$a] = doStripTags(ps($a)); 866 } 867 868 return $out; 869 } 870 871 /** 872 * Gets all received HTTP POST parameters. 873 * 874 * @return array 875 * @package Network 876 */ 877 878 function stripPost() 879 { 880 if (isset($_POST)) { 881 return $_POST; 882 } 883 884 return ''; 885 } 886 887 /** 888 * Gets a variable from $_SERVER global array. 889 * 890 * @param mixed $thing The variable 891 * @return mixed The variable, or an empty string on error 892 * @package System 893 * @example 894 * echo serverSet('HTTP_USER_AGENT'); 895 */ 896 897 function serverSet($thing) 898 { 899 return (isset($_SERVER[$thing])) ? $_SERVER[$thing] : ''; 900 } 901 902 /** 903 * Gets the client's IP address. 904 * 905 * Supports proxies and uses 'X_FORWARDED_FOR' HTTP header if deemed necessary. 906 * 907 * @return string 908 * @package Network 909 * @example 910 * if ($ip = remote_addr()) 911 * { 912 * echo "Your IP address is: {$ip}."; 913 * } 914 */ 915 916 function remote_addr() 917 { 918 $ip = serverSet('REMOTE_ADDR'); 919 920 if (($ip == '127.0.0.1' || $ip == '::1' || $ip == '::ffff:127.0.0.1' || $ip == serverSet('SERVER_ADDR')) && serverSet('HTTP_X_FORWARDED_FOR')) { 921 $ips = explode(', ', serverSet('HTTP_X_FORWARDED_FOR')); 922 $ip = $ips[0]; 923 } 924 925 return $ip; 926 } 927 928 /** 929 * Gets a variable from HTTP POST or a prefixed cookie. 930 * 931 * Fetches either a HTTP cookie of the given name prefixed with 932 * 'txp_', or a HTTP POST parameter without a prefix. 933 * 934 * @param string $thing The variable 935 * @return array|string The variable or an empty string 936 * @package Network 937 * @example 938 * if ($cs = psc('myVariable')) 939 * { 940 * echo "'txp_myVariable' cookie or 'myVariable' POST parameter contained: '{$cs}'."; 941 * } 942 */ 943 944 function pcs($thing) 945 { 946 if (isset($_COOKIE["txp_".$thing])) { 947 return $_COOKIE["txp_".$thing]; 948 } elseif (isset($_POST[$thing])) { 949 return $_POST[$thing]; 950 } 951 952 return ''; 953 } 954 955 /** 956 * Gets a HTTP cookie. 957 * 958 * @param string $thing The cookie 959 * @return string The cookie or an empty string 960 * @package Network 961 * @example 962 * if ($cs = cs('myVariable')) 963 * { 964 * echo "'myVariable' cookie contained: '{$cs}'."; 965 * } 966 */ 967 968 function cs($thing) 969 { 970 if (isset($_COOKIE[$thing])) { 971 return $_COOKIE[$thing]; 972 } 973 974 return ''; 975 } 976 977 /** 978 * Sets a HTTP cookie (polyfill). 979 * 980 * @param string $name The cookie name 981 * @param string $value The cookie value 982 * @param array $options The cookie options 983 * @package Network 984 */ 985 986 function set_cookie($name, $value = '', $options = array()) 987 { 988 $options += array ( 989 'expires' => time() - 3600, 990 'path' => '', 991 'domain' => '', 992 'secure' => false, 993 'httponly' => false, 994 'samesite' => 'Lax' // None || Lax || Strict 995 ); 996 997 if (version_compare(phpversion(), '7.3.0') >= 0) { 998 return setcookie($name, $value, $options); 999 } 1000 1001 extract($options); 1002 1003 return setcookie($name, $value, $expires, $path.'; samesite='.$samesite, $domain, $secure, $httponly); 1004 } 1005 1006 /** 1007 * Converts a boolean to a localised "Yes" or "No" string. 1008 * 1009 * @param bool $status The boolean. Ignores type and as such can also take a string or an integer 1010 * @return string No if FALSE, Yes otherwise 1011 * @package L10n 1012 * @example 1013 * echo yes_no(3 * 3 === 2); 1014 */ 1015 1016 function yes_no($status) 1017 { 1018 return ($status) ? gTxt('yes') : gTxt('no'); 1019 } 1020 1021 /** 1022 * Gets UNIX timestamp with microseconds. 1023 * 1024 * @return float 1025 * @package DateTime 1026 * @example 1027 * echo getmicrotime(); 1028 */ 1029 1030 function getmicrotime() 1031 { 1032 list($usec, $sec) = explode(" ", microtime()); 1033 1034 return ((float) $usec + (float) $sec); 1035 } 1036 1037 /** 1038 * Loads the given plugin or checks if it was loaded. 1039 * 1040 * @param string $name The plugin 1041 * @param bool $force If TRUE loads the plugin even if it's disabled 1042 * @return bool TRUE if the plugin is loaded 1043 * @example 1044 * if (load_plugin('abc_plugin')) 1045 * { 1046 * echo "'abc_plugin' is active."; 1047 * } 1048 */ 1049 1050 function load_plugin($name, $force = false) 1051 { 1052 global $plugin, $plugins, $plugins_ver, $prefs, $txp_current_plugin, $textarray; 1053 1054 if (is_array($plugins) && in_array($name, $plugins)) { 1055 return true; 1056 } 1057 1058 if (!empty($prefs['plugin_cache_dir'])) { 1059 $dir = rtrim($prefs['plugin_cache_dir'], '/').'/'; 1060 1061 // In case it's a relative path. 1062 if (!is_dir($dir)) { 1063 $dir = rtrim(realpath(txpath.'/'.$dir), '/').'/'; 1064 } 1065 1066 if (is_file($dir.$name.'.php')) { 1067 $plugins[] = $name; 1068 $old_plugin = isset($plugin) ? $plugin : null; 1069 set_error_handler("pluginErrorHandler"); 1070 1071 if (isset($txp_current_plugin)) { 1072 $txp_parent_plugin = $txp_current_plugin; 1073 } 1074 1075 $txp_current_plugin = $name; 1076 include $dir.$name.'.php'; 1077 $txp_current_plugin = isset($txp_parent_plugin) ? $txp_parent_plugin : null; 1078 $plugins_ver[$name] = isset($plugin['version']) ? $plugin['version'] : 0; 1079 1080 if (isset($plugin['textpack'])) { 1081 Txp::get('\Textpattern\L10n\Lang')->loadTextpack($plugin['textpack']); 1082 } 1083 1084 restore_error_handler(); 1085 $plugin = $old_plugin; 1086 1087 return true; 1088 } 1089 } 1090 1091 $version = safe_field("version", 'txp_plugin', ($force ? '' : "status = 1 AND ")."name = '".doSlash($name)."'"); 1092 1093 if ($version !== false) { 1094 $plugins[] = $name; 1095 $plugins_ver[$name] = $version; 1096 set_error_handler("pluginErrorHandler"); 1097 1098 if (isset($txp_current_plugin)) { 1099 $txp_parent_plugin = $txp_current_plugin; 1100 } 1101 1102 $txp_current_plugin = $name; 1103 $dir = sanitizeForFile($name); 1104 $filename = PLUGINPATH.DS.$dir.DS.$dir.'.php'; 1105 1106 if (!is_file($filename)) { 1107 $code = safe_field("code", 'txp_plugin', "name = '".doSlash($name)."'"); 1108 \Txp::get('\Textpattern\Plugin\Plugin')->updateFile($txp_current_plugin, $code); 1109 } 1110 1111 $ok = @include_once($filename); 1112 $txp_current_plugin = isset($txp_parent_plugin) ? $txp_parent_plugin : null; 1113 restore_error_handler(); 1114 1115 return $ok; 1116 } 1117 1118 return false; 1119 } 1120 1121 /** 1122 * Loads a plugin. 1123 * 1124 * Identical to load_plugin() except upon failure it issues an E_USER_ERROR. 1125 * 1126 * @param string $name The plugin 1127 * @return bool 1128 * @see load_plugin() 1129 */ 1130 1131 function require_plugin($name) 1132 { 1133 if (!load_plugin($name)) { 1134 trigger_error(gTxt('plugin_include_error', array('{name}' => $name)), E_USER_ERROR); 1135 1136 return false; 1137 } 1138 1139 return true; 1140 } 1141 1142 /** 1143 * Loads a plugin. 1144 * 1145 * Identical to load_plugin() except upon failure it issues an E_USER_WARNING. 1146 * 1147 * @param string $name The plugin 1148 * @return bool 1149 * @see load_plugin() 1150 */ 1151 1152 function include_plugin($name) 1153 { 1154 if (!load_plugin($name)) { 1155 trigger_error(gTxt('plugin_include_error', array('{name}' => $name)), E_USER_WARNING); 1156 1157 return false; 1158 } 1159 1160 return true; 1161 } 1162 1163 /** 1164 * Error handler for plugins. 1165 * 1166 * @param int $errno 1167 * @param string $errstr 1168 * @param string $errfile 1169 * @param int $errline 1170 * @access private 1171 * @package Debug 1172 */ 1173 1174 function pluginErrorHandler($errno, $errstr, $errfile, $errline) 1175 { 1176 global $production_status, $txp_current_plugin; 1177 1178 $error = array(); 1179 1180 if ($production_status == 'testing') { 1181 $error = array( 1182 E_WARNING => 'Warning', 1183 E_RECOVERABLE_ERROR => 'Catchable fatal error', 1184 E_USER_ERROR => 'User_Error', 1185 E_USER_WARNING => 'User_Warning', 1186 ); 1187 } elseif ($production_status == 'debug') { 1188 $error = array( 1189 E_WARNING => 'Warning', 1190 E_NOTICE => 'Notice', 1191 E_RECOVERABLE_ERROR => 'Catchable fatal error', 1192 E_USER_ERROR => 'User_Error', 1193 E_USER_WARNING => 'User_Warning', 1194 E_USER_NOTICE => 'User_Notice', 1195 ); 1196 1197 if (!isset($error[$errno])) { 1198 $error[$errno] = $errno; 1199 } 1200 } 1201 1202 if (!isset($error[$errno]) || !error_reporting()) { 1203 return; 1204 } 1205 1206 printf( 1207 '<pre dir="auto">'.gTxt('plugin_load_error').' <b>%s</b> -> <b>%s: %s on line %s</b></pre>', 1208 $txp_current_plugin, 1209 $error[$errno], 1210 $errstr, 1211 $errline 1212 ); 1213 1214 if ($production_status == 'debug') { 1215 print "\n<pre class=\"backtrace\" dir=\"ltr\"><code>".txpspecialchars(join("\n", get_caller(10)))."</code></pre>"; 1216 } 1217 } 1218 1219 /** 1220 * Error handler for page templates. 1221 * 1222 * @param int $errno 1223 * @param string $errstr 1224 * @param string $errfile 1225 * @param int $errline 1226 * @access private 1227 * @package Debug 1228 */ 1229 1230 function tagErrorHandler($errno, $errstr, $errfile, $errline) 1231 { 1232 global $production_status, $txp_current_tag, $txp_current_form, $pretext, $trace; 1233 1234 $error = array(); 1235 1236 if ($production_status == 'testing') { 1237 $error = array( 1238 E_WARNING => 'Warning', 1239 E_RECOVERABLE_ERROR => 'Textpattern Catchable fatal error', 1240 E_USER_ERROR => 'Textpattern Error', 1241 E_USER_WARNING => 'Textpattern Warning', 1242 ); 1243 } elseif ($production_status == 'debug') { 1244 $error = array( 1245 E_WARNING => 'Warning', 1246 E_NOTICE => 'Notice', 1247 E_RECOVERABLE_ERROR => 'Textpattern Catchable fatal error', 1248 E_USER_ERROR => 'Textpattern Error', 1249 E_USER_WARNING => 'Textpattern Warning', 1250 E_USER_NOTICE => 'Textpattern Notice', 1251 ); 1252 1253 if (!isset($error[$errno])) { 1254 $error[$errno] = $errno; 1255 } 1256 } 1257 1258 if (!isset($error[$errno]) || !error_reporting()) { 1259 return; 1260 } 1261 1262 if (empty($pretext['page'])) { 1263 $page = gTxt('none'); 1264 } else { 1265 $page = $pretext['page']; 1266 } 1267 1268 if (!isset($txp_current_form)) { 1269 $txp_current_form = gTxt('none'); 1270 } 1271 1272 $locus = gTxt('while_parsing_page_form', array( 1273 '{page}' => $page, 1274 '{form}' => $txp_current_form, 1275 )); 1276 1277 printf( 1278 "<pre dir=\"auto\">".gTxt('tag_error').' <b>%s</b> -> <b> %s: %s %s</b></pre>', 1279 txpspecialchars($txp_current_tag), 1280 $error[$errno], 1281 $errstr, 1282 $locus 1283 ); 1284 1285 if ($production_status == 'debug') { 1286 print "\n<pre class=\"backtrace\" dir=\"ltr\"><code>".txpspecialchars(join("\n", get_caller(10)))."</code></pre>"; 1287 1288 $trace->log(gTxt('tag_error').' '.$txp_current_tag.' -> '.$error[$errno].': '.$errstr.' '.$locus); 1289 } 1290 } 1291 1292 /** 1293 * Error handler for XML feeds. 1294 * 1295 * @param int $errno 1296 * @param string $errstr 1297 * @param string $errfile 1298 * @param int $errline 1299 * @access private 1300 * @package Debug 1301 */ 1302 1303 function feedErrorHandler($errno, $errstr, $errfile, $errline) 1304 { 1305 global $production_status; 1306 1307 if ($production_status != 'debug') { 1308 return; 1309 } 1310 1311 return tagErrorHandler($errno, $errstr, $errfile, $errline); 1312 } 1313 1314 /** 1315 * Error handler for public-side. 1316 * 1317 * @param int $errno 1318 * @param string $errstr 1319 * @param string $errfile 1320 * @param int $errline 1321 * @access private 1322 * @package Debug 1323 */ 1324 1325 function publicErrorHandler($errno, $errstr, $errfile, $errline) 1326 { 1327 global $production_status; 1328 1329 $error = array(); 1330 1331 if ($production_status == 'testing') { 1332 $error = array( 1333 E_WARNING => 'Warning', 1334 E_USER_ERROR => 'Textpattern Error', 1335 E_USER_WARNING => 'Textpattern Warning', 1336 ); 1337 } elseif ($production_status == 'debug') { 1338 $error = array( 1339 E_WARNING => 'Warning', 1340 E_NOTICE => 'Notice', 1341 E_USER_ERROR => 'Textpattern Error', 1342 E_USER_WARNING => 'Textpattern Warning', 1343 E_USER_NOTICE => 'Textpattern Notice', 1344 ); 1345 1346 if (!isset($error[$errno])) { 1347 $error[$errno] = $errno; 1348 } 1349 } 1350 1351 if (!isset($error[$errno]) || !error_reporting()) { 1352 return; 1353 } 1354 1355 printf( 1356 "<pre dir=\"auto\">".gTxt('general_error').' <b>%s: %s on line %s</b></pre>', 1357 $error[$errno], 1358 $errstr, 1359 $errline 1360 ); 1361 1362 if ($production_status == 'debug') { 1363 print "\n<pre class=\"backtrace\" dir=\"ltr\"><code>".txpspecialchars(join("\n", get_caller(10)))."</code></pre>"; 1364 } 1365 } 1366 1367 /** 1368 * Loads plugins. 1369 * 1370 * @param bool $type If TRUE loads admin-side plugins, otherwise public 1371 */ 1372 1373 function load_plugins($type = false, $pre = null) 1374 { 1375 global $prefs, $plugins, $plugins_ver, $app_mode, $trace; 1376 static $rs = null; 1377 1378 if (!is_array($plugins)) { 1379 $plugins = array(); 1380 } 1381 1382 $trace->start('[Loading plugins]'); 1383 1384 if (!empty($prefs['plugin_cache_dir'])) { 1385 $dir = rtrim($prefs['plugin_cache_dir'], '/').'/'; 1386 1387 // In case it's a relative path. 1388 if (!is_dir($dir)) { 1389 $dir = rtrim(realpath(txpath.'/'.$dir), '/').'/'; 1390 } 1391 1392 $files = glob($dir.'*.php'); 1393 1394 if ($files) { 1395 natsort($files); 1396 1397 foreach ($files as $f) { 1398 $trace->start("[Loading plugin from cache dir: '$f']"); 1399 load_plugin(basename($f, '.php')); 1400 $trace->stop(); 1401 } 1402 } 1403 } 1404 1405 if (!isset($rs)) { 1406 $admin = ($app_mode == 'async' ? '4,5' : '1,3,4,5'); 1407 $where = 'status = 1 AND type IN ('.($type ? $admin : '0,1,5').')'. 1408 ($plugins ? ' AND name NOT IN ('.join(',', quote_list($plugins)).')' : ''); 1409 1410 $rs = safe_rows("name, version, load_order", 'txp_plugin', $where." ORDER BY load_order ASC, name ASC"); 1411 } 1412 1413 if ($rs) { 1414 $old_error_handler = set_error_handler("pluginErrorHandler"); 1415 $pre = intval($pre); 1416 1417 $writable = is_dir(PLUGINPATH) && is_writable(PLUGINPATH); 1418 1419 foreach ($rs as $a) { 1420 if (!isset($plugins_ver[$a['name']]) && (!$pre || $a['load_order'] < $pre)) { 1421 $plugins[] = $a['name']; 1422 $plugins_ver[$a['name']] = $a['version']; 1423 $GLOBALS['txp_current_plugin'] = $a['name']; 1424 $trace->start("[Loading plugin: '{$a['name']}' version '{$a['version']}']"); 1425 1426 $dir = $a['name']; 1427 $filename = PLUGINPATH.DS.$dir.DS.$dir.'.php'; 1428 1429 if ($writable && !is_file($filename)) { 1430 $code = safe_field('code', 'txp_plugin', "name='".doSlash($a['name'])."'"); 1431 \Txp::get('\Textpattern\Plugin\Plugin')->updateFile($a['name'], $code); 1432 } 1433 1434 $eval_ok = @include($filename); 1435 $trace->stop(); 1436 1437 if ($eval_ok === false) { 1438 trigger_error(gTxt('plugin_include_error', array('{name}' => $a['name'])), E_USER_WARNING); 1439 } 1440 1441 unset($GLOBALS['txp_current_plugin']); 1442 } 1443 } 1444 1445 restore_error_handler(); 1446 } 1447 1448 $trace->stop(); 1449 } 1450 1451 /** 1452 * Attaches a handler to a callback event. 1453 * 1454 * @param callback $func The callback function 1455 * @param string $event The callback event 1456 * @param string $step The callback step 1457 * @param bool $pre Before or after. Works only with selected callback events 1458 * @package Callback 1459 * @example 1460 * register_callback('my_callback_function', 'article.updated'); 1461 * function my_callback_function($event) 1462 * { 1463 * return "'$event' fired."; 1464 * } 1465 */ 1466 1467 function register_callback($func, $event, $step = '', $pre = 0) 1468 { 1469 global $plugin_callback; 1470 1471 $plugin_callback[] = array( 1472 'function' => $func, 1473 'event' => $event, 1474 'step' => $step, 1475 'pre' => $pre, 1476 ); 1477 } 1478 1479 /** 1480 * Call an event's callback. 1481 * 1482 * Executes all callback handlers attached to the matched event and step. 1483 * 1484 * When called, any event handlers attached with register_callback() to the 1485 * matching event, step and pre will be called. The handlers, callback 1486 * functions, will be executed in the same order they were registered. 1487 * 1488 * Any extra arguments will be passed to the callback handlers in the same 1489 * argument position. This allows passing any type of data to the attached 1490 * handlers. Callback handlers will also receive the event and the step. 1491 * 1492 * Returns a combined value of all values returned by the callback handlers. 1493 * 1494 * @param string $event The callback event 1495 * @param string $step Additional callback step 1496 * @param bool|int|array $pre Allows two callbacks, a prepending and an appending, with same event and step. Array allows return values chaining 1497 * @return mixed The value returned by the attached callback functions, or an empty string 1498 * @package Callback 1499 * @see register_callback() 1500 * @example 1501 * register_callback('my_callback_function', 'my_custom_event'); 1502 * function my_callback_function($event, $step, $extra) 1503 * { 1504 * return "Passed '$extra' on '$event'."; 1505 * } 1506 * echo callback_event('my_custom_event', '', 0, 'myExtraValue'); 1507 */ 1508 1509 function callback_event($event, $step = '', $pre = 0) 1510 { 1511 global $plugin_callback, $production_status, $trace; 1512 1513 if (!is_array($plugin_callback)) { 1514 return ''; 1515 } 1516 1517 list($pre, $renew) = (array)$pre + array(0, null); 1518 $trace->start("[Callback_event: '$event', step='$step', pre='$pre']"); 1519 1520 // Any payload parameters? 1521 $argv = func_get_args(); 1522 $argv = (count($argv) > 3) ? array_slice($argv, 3) : array(); 1523 1524 foreach ($plugin_callback as $c) { 1525 if ($c['event'] == $event && (empty($c['step']) || $c['step'] == $step) && $c['pre'] == $pre) { 1526 if (is_callable($c['function'])) { 1527 if ($production_status !== 'live') { 1528 $trace->start("\t[Call function: '".Txp::get('\Textpattern\Type\TypeCallable', $c['function'])->toString()."'". 1529 (empty($argv) ? '' : ", argv='".serialize($argv)."'")."]"); 1530 } 1531 1532 $return_value = call_user_func_array($c['function'], array_merge(array( 1533 $event, 1534 $step 1535 ), $argv)); 1536 1537 if (isset($renew)) { 1538 $argv[$renew] = $return_value; 1539 } 1540 1541 if (isset($out) && !isset($renew)) { 1542 if (is_array($return_value) && is_array($out)) { 1543 $out = array_merge($out, $return_value); 1544 } elseif (is_bool($return_value) && is_bool($out)) { 1545 $out = $return_value && $out; 1546 } else { 1547 $out .= $return_value; 1548 } 1549 } else { 1550 $out = $return_value; 1551 } 1552 1553 if ($production_status !== 'live') { 1554 $trace->stop(); 1555 } 1556 } elseif ($production_status === 'debug') { 1557 trigger_error(gTxt('unknown_callback_function', array('{function}' => Txp::get('\Textpattern\Type\TypeCallable', $c['function'])->toString())), E_USER_WARNING); 1558 } 1559 } 1560 } 1561 1562 $trace->stop(); 1563 1564 if (isset($out)) { 1565 return $out; 1566 } 1567 1568 return ''; 1569 } 1570 1571 /** 1572 * Call an event's callback with two optional byref parameters. 1573 * 1574 * @param string $event The callback event 1575 * @param string $step Optional callback step 1576 * @param bool $pre Allows two callbacks, a prepending and an appending, with same event and step 1577 * @param mixed $data Optional arguments for event handlers 1578 * @param mixed $options Optional arguments for event handlers 1579 * @return array Collection of return values from event handlers 1580 * @since 4.5.0 1581 * @package Callback 1582 */ 1583 1584 function callback_event_ref($event, $step = '', $pre = 0, &$data = null, &$options = null) 1585 { 1586 global $plugin_callback, $production_status; 1587 1588 if (!is_array($plugin_callback)) { 1589 return array(); 1590 } 1591 1592 $return_value = array(); 1593 1594 foreach ($plugin_callback as $c) { 1595 if ($c['event'] == $event and (empty($c['step']) or $c['step'] == $step) and $c['pre'] == $pre) { 1596 if (is_callable($c['function'])) { 1597 // Cannot call event handler via call_user_func() as this would 1598 // dereference all arguments. Side effect: callback handler 1599 // *must* be ordinary function, *must not* be class method in 1600 // PHP <5.4. See https://bugs.php.net/bug.php?id=47160. 1601 $return_value[] = $c['function']($event, $step, $data, $options); 1602 } elseif ($production_status == 'debug') { 1603 trigger_error(gTxt('unknown_callback_function', array('{function}' => Txp::get('\Textpattern\Type\TypeCallable', $c['function'])->toString())), E_USER_WARNING); 1604 } 1605 } 1606 } 1607 1608 return $return_value; 1609 } 1610 1611 /** 1612 * Checks if a callback event has active handlers. 1613 * 1614 * @param string $event The callback event 1615 * @param string $step The callback step 1616 * @param bool $pre The position 1617 * @return bool TRUE if the event is active, FALSE otherwise 1618 * @since 4.6.0 1619 * @package Callback 1620 * @example 1621 * if (has_handler('article_saved')) 1622 * { 1623 * echo "There are active handlers for 'article_saved' event."; 1624 * } 1625 */ 1626 1627 function has_handler($event, $step = '', $pre = 0) 1628 { 1629 return (bool) callback_handlers($event, $step, $pre, false); 1630 } 1631 1632 /** 1633 * Lists handlers attached to an event. 1634 * 1635 * @param string $event The callback event 1636 * @param string $step The callback step 1637 * @param bool $pre The position 1638 * @param bool $as_string Return callables in string representation 1639 * @return array|bool An array of handlers, or FALSE 1640 * @since 4.6.0 1641 * @package Callback 1642 * @example 1643 * if ($handlers = callback_handlers('article_saved')) 1644 * { 1645 * print_r($handlers); 1646 * } 1647 */ 1648 1649 function callback_handlers($event, $step = '', $pre = 0, $as_string = true) 1650 { 1651 global $plugin_callback; 1652 1653 $out = array(); 1654 1655 foreach ((array) $plugin_callback as $c) { 1656 if ($c['event'] == $event && (!$c['step'] || $c['step'] == $step) && $c['pre'] == $pre) { 1657 if ($as_string) { 1658 $out[] = Txp::get('\Textpattern\Type\TypeCallable', $c['function'])->toString(); 1659 } else { 1660 $out[] = $c['function']; 1661 } 1662 } 1663 } 1664 1665 if ($out) { 1666 return $out; 1667 } 1668 1669 return false; 1670 } 1671 1672 /** 1673 * Merge the second array into the first array. 1674 * 1675 * @param array $pairs The first array 1676 * @param array $atts The second array 1677 * @param bool $warn If TRUE triggers errors if second array contains values that are not in the first 1678 * @return array The two arrays merged 1679 * @package TagParser 1680 */ 1681 1682 function lAtts($pairs, $atts, $warn = true) 1683 { 1684 global $pretext, $production_status, $txp_atts; 1685 static $globals = null, $global_atts, $partial; 1686 1687 if ($globals === null) { 1688 $global_atts = Txp::get('\Textpattern\Tag\Registry')->getRegistered(true); 1689 $globals = array_filter($global_atts); 1690 } 1691 1692 if (isset($atts['yield']) && !isset($pairs['yield'])) { 1693 isset($partial) or $partial = Txp::get('\Textpattern\Tag\Registry')->getTag('yield'); 1694 1695 foreach (parse_qs($atts['yield']) as $name => $alias) { 1696 $value = call_user_func($partial, array('name' => $alias === false ? $name : $alias)); 1697 1698 if (isset($value)) { 1699 $atts[$name] = $value; 1700 } 1701 } 1702 1703 unset($atts['yield']); 1704 } 1705 1706 if (empty($pretext['_txp_atts'])) { 1707 foreach ($atts as $name => $value) { 1708 if (array_key_exists($name, $pairs)) { 1709 if ($pairs[$name] !== null) { 1710 unset($txp_atts[$name]); 1711 } 1712 1713 $pairs[$name] = $value; 1714 } elseif ($warn && $production_status !== 'live' && !array_key_exists($name, $global_atts)) { 1715 trigger_error(gTxt('unknown_attribute', array('{att}' => $name))); 1716 } 1717 } 1718 } else { // don't import unset globals 1719 foreach ($atts as $name => $value) { 1720 if (array_key_exists($name, $pairs) && (!isset($globals[$name]) || isset($txp_atts[$name]))) { 1721 $pairs[$name] = $value; 1722 unset($txp_atts[$name]); 1723 } 1724 } 1725 } 1726 1727 return $pairs ? $pairs : false; 1728 } 1729 1730 /** 1731 * Sanitises a string for use in an article's URL title. 1732 * 1733 * @param string $text The title or an URL 1734 * @param bool $force Force sanitisation 1735 * @return string|null 1736 * @package URL 1737 */ 1738 1739 function stripSpace($text, $force = false) 1740 { 1741 if ($force || get_pref('attach_titles_to_permalinks')) { 1742 $text = trim(sanitizeForUrl($text, '/[^\p{L}\p{N}\-_\s\/\\\\\x{1F300}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{2600}-\x{27BF}]/u'), '-'); 1743 1744 if (get_pref('permlink_format')) { 1745 return (function_exists('mb_strtolower') ? mb_strtolower($text, 'UTF-8') : strtolower($text)); 1746 } else { 1747 return str_replace('-', '', $text); 1748 } 1749 } 1750 } 1751 1752 /** 1753 * Sanitises a string for use in a URL. 1754 * 1755 * Be aware that you still have to urlencode the string when appropriate. 1756 * This function just makes the string look prettier and excludes some 1757 * unwanted characters, but leaves UTF-8 letters and digits intact. 1758 * 1759 * @param string $text The string 1760 * @param string $strip The regex of the characters to strip 1761 * @return string 1762 * @package URL 1763 */ 1764 1765 function sanitizeForUrl($text, $strip = '/[^\p{L}\p{N}\-_\s\/\\\\]/u') 1766 { 1767 $out = callback_event('sanitize_for_url', '', 0, $text); 1768 1769 if ($out !== '') { 1770 return $out; 1771 } 1772 1773 // Remove named entities and tags. 1774 $text = preg_replace("/(^|&\S+;)|(<[^>]*>)/U", "", dumbDown($text)); 1775 // Remove all characters except letter, number, dash, space and backslash 1776 $text = preg_replace($strip, '', $text); 1777 // Collapse spaces, minuses, (back-)slashes. 1778 $text = trim(preg_replace('/[\s\-\/\\\\]+/', '-', $text), '-'); 1779 1780 return $text; 1781 } 1782 1783 /** 1784 * Sanitises a string for use in a filename. 1785 * 1786 * @param string $text The string 1787 * @return string 1788 * @package File 1789 */ 1790 1791 function sanitizeForFile($text) 1792 { 1793 $out = callback_event('sanitize_for_file', '', 0, $text); 1794 1795 if ($out !== '') { 1796 return $out; 1797 } 1798 1799 // Remove control characters and " * \ : < > ? / | 1800 $text = preg_replace('/[\x00-\x1f\x22\x2a\x2f\x3a\x3c\x3e\x3f\x5c\x7c\x7f]+/', '', $text); 1801 // Remove duplicate dots and any leading or trailing dots/spaces. 1802 $text = preg_replace('/[.]{2,}/', '.', trim($text, '. ')); 1803 1804 return $text; 1805 } 1806 1807 /** 1808 * Sanitises a string for use in a page template's name. 1809 * 1810 * @param string $text The string 1811 * @return string 1812 * @package Filter 1813 * @access private 1814 */ 1815 1816 function sanitizeForPage($text) 1817 { 1818 $out = callback_event('sanitize_for_page', '', 0, $text); 1819 1820 if ($out !== '') { 1821 return $out; 1822 } 1823 1824 return trim(preg_replace('/[<>&"\']/', '', $text)); 1825 } 1826 1827 /** 1828 * Sanitizes a string for use in a ORDER BY clause. 1829 * 1830 * @param string $text The string 1831 * @return string 1832 * @package Filter 1833 * @access private 1834 */ 1835 1836 function sanitizeForSort($text) 1837 { 1838 return trim(strtr($text, array('#' => ' ', '--' => ' '))); 1839 } 1840 1841 /** 1842 * Transliterates a string to ASCII. 1843 * 1844 * Used to generate RFC 3986 compliant and pretty ASCII-only URLs. 1845 * 1846 * @param string $str The string to convert 1847 * @param string $lang The language which translation table is used 1848 * @see sanitizeForUrl() 1849 * @package L10n 1850 */ 1851 1852 function dumbDown($str, $lang = null) 1853 { 1854 static $array; 1855 1856 if ($lang === null) { 1857 $lang = get_pref('language_ui', LANG); 1858 } 1859 1860 if (empty($array[$lang])) { 1861 $array[$lang] = array( // Nasty, huh? 1862 'À' => 'A', 'À' => 'A', 'Á' => 'A', 'Á' => 'A', 'Â' => 'A', 'Â' => 'A', 1863 'Ã' => 'A', 'Ã' => 'A', 'Ä' => 'Ae', 'Ä' => 'A', 'Å' => 'A', 'Å' => 'A', 1864 'Æ' => 'Ae', 'Æ' => 'AE', 1865 'Ā' => 'A', 'Ą' => 'A', 'Ă' => 'A', 1866 'Ç' => 'C', 'Ç' => 'C', 'Ć' => 'C', 'Č' => 'C', 'Ĉ' => 'C', 'Ċ' => 'C', 1867 'Ď' => 'D', 'Đ' => 'D', 'Ð' => 'D', 'Ð' => 'D', 1868 'È' => 'E', 'È' => 'E', 'É' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ë' => 'E', 1869 'Ē' => 'E', 'Ę' => 'E', 'Ě' => 'E', 'Ĕ' => 'E', 'Ė' => 'E', 1870 'Ĝ' => 'G', 'Ğ' => 'G', 'Ġ' => 'G', 'Ģ' => 'G', 1871 'Ĥ' => 'H', 'Ħ' => 'H', 1872 'Ì' => 'I', 'Ì' => 'I', 'Í' => 'I', 'Í' => 'I', 'Î' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ï' => 'I', 1873 'Ī' => 'I', 'Ĩ' => 'I', 'Ĭ' => 'I', 'Į' => 'I', 'İ' => 'I', 1874 'IJ' => 'IJ', 1875 'Ĵ' => 'J', 1876 'Ķ' => 'K', 1877 'Ł' => 'K', 'Ľ' => 'K', 'Ĺ' => 'K', 'Ļ' => 'K', 'Ŀ' => 'K', 1878 'Ñ' => 'N', 'Ñ' => 'N', 'Ń' => 'N', 'Ň' => 'N', 'Ņ' => 'N', 'Ŋ' => 'N', 1879 'Ò' => 'O', 'Ò' => 'O', 'Ó' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Õ' => 'O', 1880 'Ö' => 'Oe', 'Ö' => 'Oe', 1881 'Ø' => 'O', 'Ø' => 'O', 'Ō' => 'O', 'Ő' => 'O', 'Ŏ' => 'O', 1882 'Œ' => 'OE', 1883 'Ŕ' => 'R', 'Ř' => 'R', 'Ŗ' => 'R', 1884 'Ś' => 'S', 'Š' => 'S', 'Ş' => 'S', 'Ŝ' => 'S', 'Ș' => 'S', 1885 'Ť' => 'T', 'Ţ' => 'T', 'Ŧ' => 'T', 'Ț' => 'T', 1886 'Ù' => 'U', 'Ù' => 'U', 'Ú' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Û' => 'U', 1887 'Ü' => 'Ue', 'Ū' => 'U', 'Ü' => 'Ue', 1888 'Ů' => 'U', 'Ű' => 'U', 'Ŭ' => 'U', 'Ũ' => 'U', 'Ų' => 'U', 1889 'Ŵ' => 'W', 1890 'Ý' => 'Y', 'Ý' => 'Y', 'Ŷ' => 'Y', 'Ÿ' => 'Y', 1891 'Ź' => 'Z', 'Ž' => 'Z', 'Ż' => 'Z', 1892 'Þ' => 'T', 'Þ' => 'T', 1893 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'ae', 1894 'ä' => 'ae', 1895 'å' => 'a', 'ā' => 'a', 'ą' => 'a', 'ă' => 'a', 'å' => 'a', 1896 'æ' => 'ae', 1897 'ç' => 'c', 'ć' => 'c', 'č' => 'c', 'ĉ' => 'c', 'ċ' => 'c', 1898 'ď' => 'd', 'đ' => 'd', 'ð' => 'd', 1899 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ē' => 'e', 1900 'ę' => 'e', 'ě' => 'e', 'ĕ' => 'e', 'ė' => 'e', 1901 'ƒ' => 'f', 1902 'ĝ' => 'g', 'ğ' => 'g', 'ġ' => 'g', 'ģ' => 'g', 1903 'ĥ' => 'h', 'ħ' => 'h', 1904 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ī' => 'i', 1905 'ĩ' => 'i', 'ĭ' => 'i', 'į' => 'i', 'ı' => 'i', 1906 'ij' => 'ij', 1907 'ĵ' => 'j', 1908 'ķ' => 'k', 'ĸ' => 'k', 1909 'ł' => 'l', 'ľ' => 'l', 'ĺ' => 'l', 'ļ' => 'l', 'ŀ' => 'l', 1910 'ñ' => 'n', 'ń' => 'n', 'ň' => 'n', 'ņ' => 'n', 'ʼn' => 'n', 1911 'ŋ' => 'n', 1912 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'oe', 1913 'ö' => 'oe', 1914 'ø' => 'o', 'ō' => 'o', 'ő' => 'o', 'ŏ' => 'o', 1915 'œ' => 'oe', 1916 'ŕ' => 'r', 'ř' => 'r', 'ŗ' => 'r', 1917 'š' => 's', 1918 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'ue', 'ū' => 'u', 1919 'ü' => 'ue', 1920 'ů' => 'u', 'ű' => 'u', 'ŭ' => 'u', 'ũ' => 'u', 'ų' => 'u', 1921 'ŵ' => 'w', 1922 'ý' => 'y', 'ÿ' => 'y', 'ŷ' => 'y', 1923 'ž' => 'z', 'ż' => 'z', 'ź' => 'z', 1924 'þ' => 't', 1925 'ß' => 'ss', 1926 'ſ' => 'ss', 1927 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'ae', 1928 'å' => 'a', 'æ' => 'ae', 'ç' => 'c', 'ð' => 'd', 1929 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 1930 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 1931 'ñ' => 'n', 1932 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'oe', 1933 'ø' => 'o', 1934 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'ue', 1935 'ý' => 'y', 'ÿ' => 'y', 1936 'þ' => 't', 1937 'ß' => 'ss', 1938 ); 1939 1940 if (is_file(txpath.'/lib/i18n-ascii.txt')) { 1941 $i18n = parse_ini_file(txpath.'/lib/i18n-ascii.txt', true); 1942 1943 // Load the global map. 1944 if (isset($i18n['default']) && is_array($i18n['default'])) { 1945 $array[$lang] = array_merge($array[$lang], $i18n['default']); 1946 1947 // Base language overrides: 'de-AT' applies the 'de' section. 1948 if (preg_match('/([a-zA-Z]+)-.+/', $lang, $m)) { 1949 if (isset($i18n[$m[1]]) && is_array($i18n[$m[1]])) { 1950 $array[$lang] = array_merge($array[$lang], $i18n[$m[1]]); 1951 } 1952 } 1953 1954 // Regional language overrides: 'de-AT' applies the 'de-AT' section. 1955 if (isset($i18n[$lang]) && is_array($i18n[$lang])) { 1956 $array[$lang] = array_merge($array[$lang], $i18n[$lang]); 1957 } 1958 } 1959 // Load an old file (no sections) just in case. 1960 else { 1961 $array[$lang] = array_merge($array[$lang], $i18n); 1962 } 1963 } 1964 } 1965 1966 return strtr($str, $array[$lang]); 1967 } 1968 1969 /** 1970 * Cleans a URL. 1971 * 1972 * @param string $url The URL 1973 * @return string 1974 * @access private 1975 * @package URL 1976 */ 1977 1978 function clean_url($url) 1979 { 1980 return preg_replace("/\"|'|(?:\s.*$)/", '', $url); 1981 } 1982 1983 /** 1984 * Replace the last space with a   non-breaking space. 1985 * 1986 * @param string $str The string 1987 * @return string 1988 */ 1989 1990 function noWidow($str) 1991 { 1992 if (REGEXP_UTF8 == 1) { 1993 return preg_replace('@[ ]+([[:punct:]]?[\p{L}\p{N}\p{Pc}]+[[:punct:]]?)$@u', ' $1', rtrim($str)); 1994 } 1995 1996 return preg_replace('@[ ]+([[:punct:]]?\w+[[:punct:]]?)$@', ' $1', rtrim($str)); 1997 } 1998 1999 /** 2000 * Checks if an IP is on a spam blocklist. 2001 * 2002 * @param string $ip The IP address 2003 * @param string|array $checks The checked lists. Defaults to 'spam_blacklists' preferences string 2004 * @return string|bool The lists the IP is on or FALSE 2005 * @package Comment 2006 * @example 2007 * if (is_blacklisted('192.0.2.1')) 2008 * { 2009 * echo "'192.0.2.1' is on the blocklist."; 2010 * } 2011 */ 2012 2013 function is_blacklisted($ip, $checks = '') 2014 { 2015 if (!$checks) { 2016 $checks = do_list_unique(get_pref('spam_blacklists')); 2017 } 2018 2019 $rip = join('.', array_reverse(explode('.', $ip))); 2020 2021 foreach ((array) $checks as $a) { 2022 $parts = explode(':', $a, 2); 2023 $rbl = $parts[0]; 2024 2025 if (isset($parts[1])) { 2026 foreach (explode(':', $parts[1]) as $code) { 2027 $codes[] = strpos($code, '.') ? $code : '127.0.0.'.$code; 2028 } 2029 } 2030 2031 $hosts = $rbl ? @gethostbynamel($rip.'.'.trim($rbl, '. ').'.') : false; 2032 2033 if ($hosts and (!isset($codes) or array_intersect($hosts, $codes))) { 2034 $listed[] = $rbl; 2035 } 2036 } 2037 2038 return (!empty($listed)) ? join(', ', $listed) : false; 2039 } 2040 2041 /** 2042 * Checks if the user is authenticated on the public-side. 2043 * 2044 * @param string $user The checked username. If not provided, any user is accepted 2045 * @return array|bool An array containing details about the user; name, RealName, email, privs. FALSE when the user hasn't authenticated. 2046 * @package User 2047 * @example 2048 * if ($user = is_logged_in()) 2049 * { 2050 * echo "Logged in as {$user['RealName']}"; 2051 * } 2052 */ 2053 2054 function is_logged_in($user = '') 2055 { 2056 static $users = array(); 2057 2058 $name = substr(cs('txp_login_public'), 10); 2059 2060 if (!strlen($name) || strlen($user) && $user !== $name) { 2061 return false; 2062 } 2063 2064 if (!isset($users[$name])) { 2065 $users[$name] = safe_row("nonce, name, RealName, email, privs", 'txp_users', "name = '".doSlash($name)."'"); 2066 } 2067 2068 $rs = $users[$name]; 2069 2070 if ($rs && substr(md5($rs['nonce']), -10) === substr(cs('txp_login_public'), 0, 10)) { 2071 unset($rs['nonce']); 2072 2073 return $rs; 2074 } else { 2075 return false; 2076 } 2077 } 2078 2079 /** 2080 * Updates the path to the site. 2081 * 2082 * @param string $here The path 2083 * @access private 2084 * @package Pref 2085 */ 2086 2087 function updateSitePath($here) 2088 { 2089 set_pref('path_to_site', $here, 'publish', PREF_HIDDEN); 2090 } 2091 2092 /** 2093 * Converts Textpattern tag's attribute list to an array. 2094 * 2095 * @param string $text The attribute list, e.g. foobar="1" barfoo="0" 2096 * @return array Array of attributes 2097 * @access private 2098 * @package TagParser 2099 */ 2100 2101 function splat($text) 2102 { 2103 static $stack = array(), $parse = array(), $global_atts = array(), $globals = null; 2104 global $production_status, $trace, $txp_atts; 2105 2106 if ($globals === null) { 2107 $globals = array_filter(Txp::get('\Textpattern\Tag\Registry')->getRegistered(true)); 2108 } 2109 2110 $sha = sha1($text); 2111 2112 if (!isset($stack[$sha])) { 2113 $stack[$sha] = $parse[$sha] = array(); 2114 2115 if (preg_match_all('@([\w\-]+)(?:\s*=\s*(?:"((?:[^"]|"")*)"|\'((?:[^\']|\'\')*)\'|([^\s\'"/>]+)))?@s', $text, $match, PREG_SET_ORDER)) { 2116 foreach ($match as $m) { 2117 $name = strtolower($m[1]); 2118 2119 switch (count($m)) { 2120 case 2: 2121 $val = true; 2122 break; 2123 case 3: 2124 $val = str_replace('""', '"', $m[2]); 2125 break; 2126 case 4: 2127 $val = str_replace("''", "'", $m[3]); 2128 2129 if (strpos($m[3], ':') !== false) { 2130 $parse[$sha][] = $name; 2131 } 2132 2133 break; 2134 case 5: 2135 $val = $m[4]; 2136 trigger_error(gTxt('attribute_values_must_be_quoted'), E_USER_WARNING); 2137 break; 2138 } 2139 2140 $stack[$sha][$name] = $val; 2141 } 2142 } 2143 2144 $global_atts[$sha] = array_intersect_key($stack[$sha], $globals) or $global_atts[$sha] = null; 2145 } 2146 2147 $txp_atts = $global_atts[$sha]; 2148 2149 if (empty($parse[$sha])) { 2150 return $stack[$sha]; 2151 } 2152 2153 $atts = $stack[$sha]; 2154 2155 if ($production_status !== 'live') { 2156 foreach ($parse[$sha] as $p) { 2157 $trace->start("[attribute '".$p."']"); 2158 $atts[$p] = parse($atts[$p], true, false); 2159 isset($txp_atts[$p]) and $txp_atts[$p] = $atts[$p]; 2160 $trace->stop('[/attribute]'); 2161 } 2162 } else { 2163 foreach ($parse[$sha] as $p) { 2164 $atts[$p] = parse($atts[$p], true, false); 2165 isset($txp_atts[$p]) and $txp_atts[$p] = $atts[$p]; 2166 } 2167 } 2168 2169 return $atts; 2170 } 2171 2172 /** 2173 * Replaces CR and LF with spaces, and drops NULL bytes. 2174 * 2175 * Used for sanitising email headers. 2176 * 2177 * @param string $str The string 2178 * @return string 2179 * @package Mail 2180 * @deprecated in 4.6.0 2181 * @see \Textpattern\Mail\Encode::escapeHeader() 2182 */ 2183 2184 function strip_rn($str) 2185 { 2186 return Txp::get('\Textpattern\Mail\Encode')->escapeHeader($str); 2187 } 2188 2189 /** 2190 * Validates a string as an email address. 2191 * 2192 * <code> 2193 * if (is_valid_email('john.doe@example.com')) 2194 * { 2195 * echo "'john.doe@example.com' validates."; 2196 * } 2197 * </code> 2198 * 2199 * @param string $address The email address 2200 * @return bool 2201 * @package Mail 2202 * @deprecated in 4.6.0 2203 * @see filter_var() 2204 */ 2205 2206 function is_valid_email($address) 2207 { 2208 return (bool) filter_var($address, FILTER_VALIDATE_EMAIL); 2209 } 2210 2211 /** 2212 * Sends an email message as the currently logged in user. 2213 * 2214 * <code> 2215 * if (txpMail('john.doe@example.com', 'Subject', 'Some message')) 2216 * { 2217 * echo "Email sent to 'john.doe@example.com'."; 2218 * } 2219 * </code> 2220 * 2221 * @param string $to_address The receiver 2222 * @param string $subject The subject 2223 * @param string $body The message 2224 * @param string $reply_to The reply to address 2225 * @return bool Returns FALSE when sending failed 2226 * @see \Textpattern\Mail\Compose 2227 * @package Mail 2228 */ 2229 2230 function txpMail($to_address, $subject, $body, $reply_to = null) 2231 { 2232 global $txp_user; 2233 2234 // Send the email as the currently logged in user. 2235 if ($txp_user) { 2236 $sender = safe_row( 2237 "RealName, email", 2238 'txp_users', 2239 "name = '".doSlash($txp_user)."'" 2240 ); 2241 2242 if ($sender && is_valid_email(get_pref('publisher_email'))) { 2243 $sender['email'] = get_pref('publisher_email'); 2244 } 2245 } 2246 // If not logged in, the receiver is the sender. 2247 else { 2248 $sender = safe_row( 2249 "RealName, email", 2250 'txp_users', 2251 "email = '".doSlash($to_address)."'" 2252 ); 2253 } 2254 2255 if ($sender) { 2256 extract($sender); 2257 2258 try { 2259 $message = Txp::get('\Textpattern\Mail\Compose') 2260 ->from($email, $RealName) 2261 ->to($to_address) 2262 ->subject($subject) 2263 ->body($body); 2264 2265 if ($reply_to) { 2266 $message->replyTo($reply_to); 2267 } 2268 2269 $message->send(); 2270 } catch (\Textpattern\Mail\Exception $e) { 2271 return false; 2272 } 2273 2274 return true; 2275 } 2276 2277 return false; 2278 } 2279 2280 /** 2281 * Encodes a string for use in an email header. 2282 * 2283 * @param string $string The string 2284 * @param string $type The type of header, either "text" or "phrase" 2285 * @return string 2286 * @package Mail 2287 * @deprecated in 4.6.0 2288 * @see \Textpattern\Mail\Encode::header() 2289 */ 2290 2291 function encode_mailheader($string, $type) 2292 { 2293 try { 2294 return Txp::get('\Textpattern\Mail\Encode')->header($string, $type); 2295 } catch (\Textpattern\Mail\Exception $e) { 2296 trigger_error($e->getMessage(), E_USER_WARNING); 2297 } 2298 } 2299 2300 /** 2301 * Converts an email address into unicode entities. 2302 * 2303 * @param string $txt The email address 2304 * @return string Encoded email address 2305 * @package Mail 2306 * @deprecated in 4.6.0 2307 * @see \Textpattern\Mail\Encode::entityObfuscateAddress() 2308 */ 2309 2310 function eE($txt) 2311 { 2312 return Txp::get('\Textpattern\Mail\Encode')->entityObfuscateAddress($txt); 2313 } 2314 2315 /** 2316 * Strips PHP tags from a string. 2317 * 2318 * @param string $in The input 2319 * @return string 2320 */ 2321 2322 function stripPHP($in) 2323 { 2324 return preg_replace("/".chr(60)."\?(?:php)?|\?".chr(62)."/i", '', $in); 2325 } 2326 2327 /** 2328 * Creates a form template. 2329 * 2330 * On a successful run, will trigger a 'form.create > done' callback event. 2331 * 2332 * @param string $name The name 2333 * @param string $type The type 2334 * @param string $Form The template 2335 * @return bool FALSE on error 2336 * @since 4.6.0 2337 * @package Template 2338 */ 2339 2340 function create_form($name, $type, $Form) 2341 { 2342 $types = get_form_types(); 2343 2344 if (form_exists($name) || !is_valid_form($name) || !in_array($type, array_keys($types))) { 2345 return false; 2346 } 2347 2348 if ( 2349 safe_insert( 2350 'txp_form', 2351 "name = '".doSlash($name)."', 2352 type = '".doSlash($type)."', 2353 Form = '".doSlash($Form)."'" 2354 ) === false 2355 ) { 2356 return false; 2357 } 2358 2359 callback_event('form.create', 'done', 0, compact('name', 'type', 'Form')); 2360 2361 return true; 2362 } 2363 2364 /** 2365 * Checks if a form template exists. 2366 * 2367 * @param string $name The form 2368 * @return bool TRUE if the form exists 2369 * @since 4.6.0 2370 * @package Template 2371 */ 2372 2373 function form_exists($name) 2374 { 2375 return (bool) safe_row("name", 'txp_form', "name = '".doSlash($name)."'"); 2376 } 2377 2378 /** 2379 * Validates a string as a form template name. 2380 * 2381 * @param string $name The form name 2382 * @return bool TRUE if the string validates 2383 * @since 4.6.0 2384 * @package Template 2385 */ 2386 2387 function is_valid_form($name) 2388 { 2389 if (function_exists('mb_strlen')) { 2390 $length = mb_strlen($name, '8bit'); 2391 } else { 2392 $length = strlen($name); 2393 } 2394 2395 return $name && !preg_match('/^\s|[<>&"\']|\s$/u', $name) && $length <= 64; 2396 } 2397 2398 /** 2399 * Gets a "since days ago" date format from a given UNIX timestamp. 2400 * 2401 * @param int $stamp UNIX timestamp 2402 * @return string "n days ago" 2403 * @package DateTime 2404 */ 2405 2406 function since($stamp) 2407 { 2408 $diff = (time() - $stamp); 2409 2410 if ($diff <= 3600) { 2411 $mins = round($diff / 60); 2412 $since = ($mins <= 1) ? ($mins == 1) ? '1 '.gTxt('minute') : gTxt('a_few_seconds') : "$mins ".gTxt('minutes'); 2413 } elseif (($diff <= 86400) && ($diff > 3600)) { 2414 $hours = round($diff / 3600); 2415 $since = ($hours <= 1) ? '1 '.gTxt('hour') : "$hours ".gTxt('hours'); 2416 } elseif ($diff >= 86400) { 2417 $days = round($diff / 86400); 2418 $since = ($days <= 1) ? "1 ".gTxt('day') : "$days ".gTxt('days'); 2419 } 2420 2421 return gTxt('ago', array('{since}' => $since)); 2422 } 2423 2424 /** 2425 * Calculates a timezone offset. 2426 * 2427 * Calculates the offset between the server local time and the 2428 * user's selected timezone at a given point in time. 2429 * 2430 * @param int $timestamp The timestamp. Defaults to time() 2431 * @return int The offset in seconds 2432 * @package DateTime 2433 */ 2434 2435 function tz_offset($timestamp = null) 2436 { 2437 global $gmtoffset, $timezone_key; 2438 static $dtz = array(), $timezone_server = null; 2439 2440 if ($timezone_server === null) { 2441 $timezone_server = date_default_timezone_get(); 2442 } 2443 2444 if ($timezone_server === $timezone_key) { 2445 return 0; 2446 } 2447 2448 if ($timestamp === null) { 2449 $timestamp = time(); 2450 } 2451 2452 try { 2453 if (!isset($dtz[$timezone_server])) { 2454 $dtz[$timezone_server] = new \DateTimeZone($timezone_server); 2455 } 2456 2457 $transition = $dtz[$timezone_server]->getTransitions($timestamp, $timestamp); 2458 $serveroffset = $transition[0]['offset']; 2459 } catch (\Exception $e) { 2460 extract(getdate($timestamp)); 2461 $serveroffset = gmmktime($hours, $minutes, 0, $mon, $mday, $year) - mktime($hours, $minutes, 0, $mon, $mday, $year); 2462 } 2463 2464 try { 2465 if (!isset($dtz[$timezone_key])) { 2466 $dtz[$timezone_key] = new \DateTimeZone($timezone_key); 2467 } 2468 2469 $transition = $dtz[$timezone_key]->getTransitions($timestamp, $timestamp); 2470 $siteoffset = $transition[0]['offset']; 2471 } catch (\Exception $e) { 2472 $siteoffset = $gmtoffset; 2473 } 2474 2475 return $siteoffset - $serveroffset; 2476 } 2477 2478 /** 2479 * Formats a time. 2480 * 2481 * Respects the locale and local timezone, and makes sure the 2482 * output string is encoded in UTF-8. 2483 * 2484 * @param string $format The date format 2485 * @param int $time UNIX timestamp. Defaults to time() 2486 * @param bool $gmt Return GMT time 2487 * @param string $override_locale Override the locale 2488 * @return string Formatted date 2489 * @package DateTime 2490 * @example 2491 * echo safe_strftime('w3cdtf'); 2492 */ 2493 2494 function safe_strftime($format, $time = '', $gmt = false, $override_locale = '') 2495 { 2496 static $charsets = array(), $txpLocale = null; 2497 2498 if (!$time) { 2499 $time = time(); 2500 } 2501 2502 if ($txpLocale === null) { 2503 $txpLocale = Txp::get('\Textpattern\L10n\Locale'); 2504 } 2505 2506 // We could add some other formats here. 2507 if ($format == 'iso8601' || $format == 'w3cdtf') { 2508 $format = '%Y-%m-%dT%H:%M:%SZ'; 2509 $gmt = true; 2510 } elseif ($format == 'rfc822') { 2511 $format = '%a, %d %b %Y %H:%M:%S GMT'; 2512 $gmt = true; 2513 $override_locale = 'C'; 2514 } 2515 2516 if ($override_locale) { 2517 $oldLocale = $txpLocale->getLocale(LC_TIME); 2518 2519 if ($oldLocale != $override_locale) { 2520 $txpLocale->setLocale(LC_TIME, $override_locale); 2521 } else { 2522 $oldLocale = null; 2523 } 2524 } 2525 2526 if ($format == 'since') { 2527 $str = since($time); 2528 } elseif ($gmt) { 2529 $str = gmstrftime($format, $time); 2530 } else { 2531 $tztime = $time + tz_offset($time); 2532 $format = str_replace('%s', $tztime, $format); 2533 $str = strftime($format, $tztime); 2534 } 2535 2536 if (!isset($charsets[$override_locale])) { 2537 $charsets[$override_locale] = $txpLocale->getCharset(LC_TIME, IS_WIN ? 'Windows-1252' : 'ISO-8859-1'); 2538 } 2539 2540 $charset = $charsets[$override_locale]; 2541 2542 if ($charset != 'UTF-8' && $format != 'since') { 2543 $new = ''; 2544 if (is_callable('iconv')) { 2545 $new = @iconv($charset, 'UTF-8', $str); 2546 } 2547 2548 if ($new) { 2549 $str = $new; 2550 } elseif (is_callable('utf8_encode')) { 2551 $str = utf8_encode($str); 2552 } 2553 } 2554 2555 // Revert to the old locale. 2556 if (isset($oldLocale)) { 2557 $txpLocale->setLocale(LC_TIME, $oldLocale); 2558 } 2559 2560 return $str; 2561 } 2562 2563 /** 2564 * Converts a time string from the Textpattern timezone to GMT. 2565 * 2566 * @param string $time_str The time string 2567 * @return int UNIX timestamp 2568 * @package DateTime 2569 */ 2570 2571 function safe_strtotime($time_str) 2572 { 2573 $ts = strtotime($time_str); 2574 2575 // tz_offset calculations are expensive 2576 $tz_offset = tz_offset($ts); 2577 2578 return strtotime($time_str, time() + $tz_offset) - $tz_offset; 2579 } 2580 2581 /** 2582 * Generic error handler. 2583 * 2584 * @param int $errno 2585 * @param string $errstr 2586 * @param string $errfile 2587 * @param int $errline 2588 * @access private 2589 * @package Debug 2590 */ 2591 2592 function myErrorHandler($errno, $errstr, $errfile, $errline) 2593 { 2594 if (!error_reporting()) { 2595 return; 2596 } 2597 2598 echo '<pre dir="auto">'.n.n."$errno: $errstr in $errfile at line $errline\n"; 2599 2600 if (is_callable('debug_backtrace')) { 2601 echo "Backtrace:\n"; 2602 $trace = debug_backtrace(); 2603 2604 foreach ($trace as $ent) { 2605 if (isset($ent['file'])) { 2606 echo $ent['file'].':'; 2607 } 2608 2609 if (isset($ent['function'])) { 2610 echo $ent['function'].'('; 2611 2612 if (isset($ent['args'])) { 2613 $args = ''; 2614 2615 foreach ($ent['args'] as $arg) { 2616 $args .= $arg.','; 2617 } 2618 2619 echo rtrim($args, ','); 2620 } 2621 2622 echo ') '; 2623 } 2624 2625 if (isset($ent['line'])) { 2626 echo 'at line '.$ent['line'].' '; 2627 } 2628 2629 if (isset($ent['file'])) { 2630 echo 'in '.$ent['file']; 2631 } 2632 2633 echo "\n"; 2634 } 2635 } 2636 2637 echo "</pre>"; 2638 } 2639 2640 /** 2641 * Renders a download link. 2642 * 2643 * @param int $id The file ID 2644 * @param string $label The label 2645 * @param string $filename The filename 2646 * @return string HTML 2647 * @package File 2648 */ 2649 2650 function make_download_link($id, $label = '', $filename = '') 2651 { 2652 if ((string) $label === '') { 2653 $label = gTxt('download'); 2654 } 2655 2656 $url = filedownloadurl($id, $filename); 2657 2658 // Do not use the array() form of passing $atts to href(). 2659 // Doing so breaks download links on the admin side due to 2660 // double-encoding of the ampersands. 2661 return href($label, $url, ' title = "'.gTxt('download').'"'); 2662 } 2663 2664 /** 2665 * Sets error reporting level. 2666 * 2667 * @param string $level The level. Either "debug", "live" or "testing" 2668 * @package Debug 2669 */ 2670 2671 function set_error_level($level) 2672 { 2673 if ($level == 'debug') { 2674 error_reporting(E_ALL | E_STRICT); 2675 } elseif ($level == 'live') { 2676 // Don't show errors on screen. 2677 $suppress = E_NOTICE | E_USER_NOTICE | E_WARNING | E_STRICT | (defined('E_DEPRECATED') ? E_DEPRECATED : 0); 2678 error_reporting(E_ALL ^ $suppress); 2679 @ini_set("display_errors", "1"); 2680 } else { 2681 // Default is 'testing': display everything except notices. 2682 error_reporting((E_ALL | E_STRICT) ^ (E_NOTICE | E_USER_NOTICE)); 2683 } 2684 } 2685 2686 /** 2687 * Translates upload error code to a localised error message. 2688 * 2689 * @param int $err_code The error code 2690 * @return string The $err_code as a message 2691 * @package File 2692 */ 2693 2694 function upload_get_errormsg($err_code) 2695 { 2696 $msg = ''; 2697 2698 switch ($err_code) { 2699 // Value: 0; There is no error, the file uploaded with success. 2700 case UPLOAD_ERR_OK: 2701 $msg = ''; 2702 break; 2703 // Value: 1; The uploaded file exceeds the upload_max_filesize directive in php.ini. 2704 case UPLOAD_ERR_INI_SIZE: 2705 $msg = gTxt('upload_err_ini_size'); 2706 break; 2707 // Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form. 2708 case UPLOAD_ERR_FORM_SIZE: 2709 $msg = gTxt('upload_err_form_size'); 2710 break; 2711 // Value: 3; The uploaded file was only partially uploaded. 2712 case UPLOAD_ERR_PARTIAL: 2713 $msg = gTxt('upload_err_partial'); 2714 break; 2715 // Value: 4; No file was uploaded. 2716 case UPLOAD_ERR_NO_FILE: 2717 $msg = gTxt('upload_err_no_file'); 2718 break; 2719 // Value: 6; Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3. 2720 case UPLOAD_ERR_NO_TMP_DIR: 2721 $msg = gTxt('upload_err_tmp_dir'); 2722 break; 2723 // Value: 7; Failed to write file to disk. Introduced in PHP 5.1.0. 2724 case UPLOAD_ERR_CANT_WRITE: 2725 $msg = gTxt('upload_err_cant_write'); 2726 break; 2727 // Value: 8; File upload stopped by extension. Introduced in PHP 5.2.0. 2728 case UPLOAD_ERR_EXTENSION: 2729 $msg = gTxt('upload_err_extension'); 2730 break; 2731 } 2732 2733 return $msg; 2734 } 2735 2736 /** 2737 * Formats a file size. 2738 * 2739 * @param int $bytes Size in bytes 2740 * @param int $decimals Number of decimals 2741 * @param string $format The format the size is represented 2742 * @return string Formatted file size 2743 * @package File 2744 * @example 2745 * echo format_filesize(168642); 2746 */ 2747 2748 function format_filesize($bytes, $decimals = 2, $format = '') 2749 { 2750 $units = array('b', 'k', 'm', 'g', 't', 'p', 'e', 'z', 'y'); 2751 2752 if (in_array($format, $units)) { 2753 $pow = array_search($format, $units); 2754 } else { 2755 $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); 2756 $pow = min($pow, count($units) - 1); 2757 } 2758 2759 $bytes /= pow(1024, $pow); 2760 2761 $separators = localeconv(); 2762 $sep_dec = isset($separators['decimal_point']) ? $separators['decimal_point'] : '.'; 2763 $sep_thous = isset($separators['thousands_sep']) ? $separators['thousands_sep'] : ','; 2764 2765 return number_format($bytes, $decimals, $sep_dec, $sep_thous).sp.gTxt('units_'.$units[$pow]); 2766 } 2767 2768 /** 2769 * Gets a file download as an array. 2770 * 2771 * @param string $where SQL where clause 2772 * @return array|bool An array of files, or FALSE on failure 2773 * @package File 2774 * @example 2775 * if ($file = fileDownloadFetchInfo('id = 1')) 2776 * { 2777 * print_r($file); 2778 * } 2779 */ 2780 2781 function fileDownloadFetchInfo($where) 2782 { 2783 $rs = safe_row("*", 'txp_file', $where); 2784 2785 if ($rs) { 2786 return file_download_format_info($rs); 2787 } 2788 2789 return false; 2790 } 2791 2792 /** 2793 * Formats file download info. 2794 * 2795 * Takes a data array generated by fileDownloadFetchInfo() 2796 * and formats the contents. 2797 * 2798 * @param array $file The file info to format 2799 * @return array Formatted file info 2800 * @access private 2801 * @package File 2802 */ 2803 2804 function file_download_format_info($file) 2805 { 2806 if (($unix_ts = @strtotime($file['created'])) > 0) { 2807 $file['created'] = $unix_ts; 2808 } 2809 2810 if (($unix_ts = @strtotime($file['modified'])) > 0) { 2811 $file['modified'] = $unix_ts; 2812 } 2813 2814 return $file; 2815 } 2816 2817 /** 2818 * Formats file download's modification and creation timestamps. 2819 * 2820 * Used by file_download tags. 2821 * 2822 * @param array $params 2823 * @return string 2824 * @access private 2825 * @package File 2826 */ 2827 2828 function fileDownloadFormatTime($params) 2829 { 2830 extract(lAtts(array( 2831 'ftime' => '', 2832 'format' => '', 2833 ), $params)); 2834 2835 if (!empty($ftime)) { 2836 if ($format) { 2837 return safe_strftime($format, $ftime); 2838 } 2839 2840 return safe_strftime(get_pref('archive_dateformat'), $ftime); 2841 } 2842 2843 return ''; 2844 } 2845 2846 /** 2847 * file_get_contents wrapper. 2848 * 2849 */ 2850 2851 function txp_get_contents($file) 2852 { 2853 return is_readable($file) ? file_get_contents($file) : null; 2854 } 2855 2856 /** 2857 * Returns the contents of the found files as an array. 2858 * 2859 */ 2860 2861 function get_files_content($dir, $ext) 2862 { 2863 $result = array(); 2864 foreach ((array)@scandir($dir) as $file) { 2865 if (preg_match('/^(.+)\.'.$ext.'$/', $file, $match)) { 2866 $result[$match[1]] = file_get_contents("$dir/$file"); 2867 } 2868 } 2869 2870 return $result; 2871 } 2872 2873 /** 2874 * Checks if a function is disabled. 2875 * 2876 * @param string $function The function name 2877 * @return bool TRUE if the function is disabled 2878 * @package System 2879 * @example 2880 * if (is_disabled('mail')) 2881 * { 2882 * echo "'mail' function is disabled."; 2883 * } 2884 */ 2885 2886 function is_disabled($function) 2887 { 2888 static $disabled; 2889 2890 if (!isset($disabled)) { 2891 $disabled = do_list(ini_get('disable_functions')); 2892 } 2893 2894 return in_array($function, $disabled); 2895 } 2896 2897 /** 2898 * Joins two strings to form a single filesystem path. 2899 * 2900 * @param string $base The base directory 2901 * @param string $path The second path, a relative filename 2902 * @return string A path to a file 2903 * @package File 2904 */ 2905 2906 function build_file_path($base, $path) 2907 { 2908 $base = rtrim($base, '/\\'); 2909 $path = ltrim($path, '/\\'); 2910 2911 return $base.DS.$path; 2912 } 2913 2914 /** 2915 * Gets a user's real name. 2916 * 2917 * @param string $name The username 2918 * @return string A real name, or username if empty 2919 * @package User 2920 */ 2921 2922 function get_author_name($name) 2923 { 2924 static $authors = array(); 2925 2926 if (isset($authors[$name])) { 2927 return $authors[$name]; 2928 } 2929 2930 $realname = fetch('RealName', 'txp_users', 'name', $name); 2931 $authors[$name] = $realname; 2932 2933 return ($realname) ? $realname : $name; 2934 } 2935 2936 /** 2937 * Gets a user's email address. 2938 * 2939 * @param string $name The username 2940 * @return string 2941 * @package User 2942 */ 2943 2944 function get_author_email($name) 2945 { 2946 static $authors = array(); 2947 2948 if (isset($authors[$name])) { 2949 return $authors[$name]; 2950 } 2951 2952 $email = fetch('email', 'txp_users', 'name', $name); 2953 $authors[$name] = $email; 2954 2955 return $email; 2956 } 2957 2958 /** 2959 * Checks if a database table contains items just from one user. 2960 * 2961 * @param string $table The database table 2962 * @param string $col The column 2963 * @return bool 2964 * @package User 2965 * @example 2966 * if (has_single_author('textpattern', 'AuthorID')) 2967 * { 2968 * echo "'textpattern' table has only content from one author."; 2969 * } 2970 */ 2971 2972 function has_single_author($table, $col = 'author') 2973 { 2974 static $cache = array(); 2975 2976 if (!isset($cache[$table][$col])) { 2977 $cache[$table][$col] = (safe_field("COUNT(name)", 'txp_users', "1 = 1") <= 1) && 2978 (safe_field("COUNT(DISTINCT(".doSlash($col)."))", doSlash($table), "1 = 1") <= 1); 2979 } 2980 2981 return $cache[$table][$col]; 2982 } 2983 2984 /** 2985 * Parse a string and store the result. 2986 * 2987 * @param string $thing The raw string 2988 * @param null|string $hash The string SHA1 hash 2989 * @param bool|callable $transform The function applied to txp tags 2990 * @package TagParser 2991 */ 2992 2993 function txp_tokenize($thing, $hash = null, $transform = null) 2994 { 2995 global $txp_parsed, $txp_else; 2996 static $short_tags = null; 2997 2998 isset($short_tags) or $short_tags = get_pref('enable_short_tags', false); 2999 3000 $f = '@(</?(?:'.TXP_PATTERN.'):\w+(?:\[-?\d+\])?(?:\s+[\w\-]+(?:\s*=\s*(?:"(?:[^"]|"")*"|\'(?:[^\']|\'\')*\'|[^\s\'"/>]+))?)*\s*/?\>)@s'; 3001 $t = '@^</?('.TXP_PATTERN.'):(\w+)(?:\[(-?\d+)\])?(.*)\>$@s'; 3002 3003 $parsed = preg_split($f, $thing, -1, PREG_SPLIT_DELIM_CAPTURE); 3004 $last = count($parsed); 3005 3006 if (isset($transform) && (is_bool($transform) || is_callable($transform))) { 3007 $transform !== true or $transform = 'txpspecialchars'; 3008 3009 for ($i = 1; $i < $last; $i+=2) { 3010 $parsed[$i] = $transform === false ? null : call_user_func($transform, $parsed[$i]); 3011 } 3012 } 3013 3014 if ($hash === false) { 3015 return $parsed; 3016 } elseif (!is_string($hash)) { 3017 $hash = sha1($thing); 3018 } 3019 3020 $inside = array($parsed[0]); 3021 $tags = array($inside); 3022 $tag = array(); 3023 $outside = array(); 3024 $order = array(array()); 3025 $else = array(-1); 3026 $count = array(-1); 3027 $level = 0; 3028 3029 for ($i = 1; $i < $last || $level > 0; $i++) { 3030 $chunk = $i < $last ? $parsed[$i] : '</txp:'.$tag[$level-1][2].'>'; 3031 preg_match($t, $chunk, $tag[$level]); 3032 $count[$level] += 2; 3033 3034 if ($tag[$level][2] === 'else') { 3035 $else[$level] = $count[$level]; 3036 } elseif ($tag[$level][1] === 'txp:') { 3037 // Handle <txp::shortcode />. 3038 $tag[$level][4] .= ' form="'.$tag[$level][2].'"'; 3039 $tag[$level][2] = 'output_form'; 3040 } elseif ($short_tags && $tag[$level][1] !== 'txp') { 3041 // Handle <short::tags />. 3042 $tag[$level][2] = rtrim($tag[$level][1], ':').'_'.$tag[$level][2]; 3043 } 3044 3045 if ($chunk[strlen($chunk) - 2] === '/') { 3046 // Self closed tag. 3047 if ($chunk[1] === '/') { 3048 trigger_error(gTxt('ambiguous_tag_format', array('{chunk}' => $chunk)), E_USER_WARNING); 3049 } 3050 3051 $tags[$level][] = array($chunk, $tag[$level][2], trim(rtrim($tag[$level][4], '/')), null, null); 3052 $inside[$level] .= $chunk; 3053 empty($tag[$level][3]) or $order[$level][count($tags[$level])/2] = $tag[$level][3]; 3054 } elseif ($chunk[1] !== '/') { 3055 // Opening tag. 3056 $inside[$level] .= $chunk; 3057 empty($tag[$level][3]) or $order[$level][(count($tags[$level])+1)/2] = $tag[$level][3]; 3058 $level++; 3059 $outside[$level] = $chunk; 3060 $inside[$level] = ''; 3061 $else[$level] = $count[$level] = -1; 3062 $tags[$level] = array(); 3063 $order[$level] = array(); 3064 } else { 3065 // Closing tag. 3066 if ($level < 1) { 3067 trigger_error(gTxt('missing_open_tag', array('{chunk}' => $chunk)), E_USER_WARNING); 3068 $tags[$level][] = array($chunk, null, '', null, null); 3069 $inside[$level] .= $chunk; 3070 } else { 3071 if ($i >= $last) { 3072 trigger_error(gTxt('missing_close_tag', array('{chunk}' => $outside[$level])), E_USER_WARNING); 3073 } elseif ($tag[$level-1][2] != $tag[$level][2]) { 3074 trigger_error(gTxt('mismatch_open_close_tag', array( 3075 '{from}' => $outside[$level], 3076 '{to}' => $chunk, 3077 )), E_USER_WARNING); 3078 } 3079 3080 $sha = sha1($inside[$level]); 3081 txp_fill_parsed($sha, $tags[$level], $order[$level], $count[$level], $else[$level]); 3082 3083 $level--; 3084 $tags[$level][] = array($outside[$level+1], $tag[$level][2], trim($tag[$level][4]), $inside[$level+1], $chunk); 3085 $inside[$level] .= $inside[$level+1].$chunk; 3086 } 3087 } 3088 3089 $chunk = ++$i < $last ? $parsed[$i] : ''; 3090 $tags[$level][] = $chunk; 3091 $inside[$level] .= $chunk; 3092 } 3093 3094 txp_fill_parsed($hash, $tags[0], $order[0], $count[0] + 2, $else[0]); 3095 } 3096 3097 /** Auxiliary **/ 3098 3099 function txp_fill_parsed($sha, $tags, $order, $count, $else) { 3100 global $txp_parsed, $txp_else; 3101 3102 $txp_parsed[$sha] = $count > 2 ? $tags : false; 3103 $txp_else[$sha] = array($else > 0 ? $else : $count, $count - 2); 3104 3105 if (!empty($order)) { 3106 $pre = array_filter($order, function ($v) {return $v > 0;}); 3107 $post = array_filter($order, function ($v) {return $v < 0;}); 3108 3109 if ($pre) { 3110 asort($pre); 3111 } 3112 3113 if ($post) { 3114 asort($post); 3115 } 3116 3117 $txp_else[$sha]['test'] = $post ? array_merge(array_keys($pre), array(0), array_keys($post)) : ($pre ? array_keys($pre) : null); 3118 //rtrim(trim(implode(',', array_keys($pre)).',0,'.implode(',', array_keys($post)), ','), '0'); 3119 } 3120 } 3121 3122 3123 /** 3124 * Extracts a statement from a if/else condition. 3125 * 3126 * @param string $thing Statement in Textpattern tag markup presentation 3127 * @param bool $condition TRUE to return if statement, FALSE to else 3128 * @return string Either if or else statement 3129 * @since 4.8.2 3130 * @see parse 3131 * @package TagParser 3132 * @example 3133 * echo getIfElse('true <txp:else /> false', 1 === 1); 3134 */ 3135 3136 function getIfElse($thing, $condition = true) 3137 { 3138 global $txp_parsed, $txp_else; 3139 3140 if (!$thing || strpos($thing, ':else') === false) { 3141 return $condition ? $thing : null; 3142 } 3143 3144 $hash = sha1($thing); 3145 3146 if (!isset($txp_parsed[$hash])) { 3147 txp_tokenize($thing, $hash); 3148 } 3149 3150 $tag = $txp_parsed[$hash]; 3151 list($first, $last) = $txp_else[$hash]; 3152 3153 if ($condition) { 3154 $last = $first - 2; 3155 $first = 1; 3156 } elseif ($first <= $last) { 3157 $first += 2; 3158 } else { 3159 return null; 3160 } 3161 3162 for ($out = $tag[$first - 1]; $first <= $last; $first++) { 3163 $out .= $tag[$first][0].$tag[$first][3].$tag[$first][4].$tag[++$first]; 3164 } 3165 3166 return $out; 3167 } 3168 3169 /** 3170 * Extracts a statement from a if/else condition to parse. 3171 * 3172 * @param string $thing Statement in Textpattern tag markup presentation 3173 * @param bool $condition TRUE to return if statement, FALSE to else 3174 * @return string Either if or else statement 3175 * @deprecated in 4.6.0 3176 * @see parse 3177 * @package TagParser 3178 * @example 3179 * echo parse(EvalElse('true <txp:else /> false', 1 === 1)); 3180 */ 3181 3182 function EvalElse($thing, $condition) 3183 { 3184 global $txp_atts; 3185 3186 if (!empty($txp_atts['not'])) { 3187 $condition = empty($condition); 3188 unset($txp_atts['not']); 3189 } 3190 3191 if (empty($condition)) { 3192 $txp_atts = null; 3193 } 3194 3195 return (string)getIfElse($thing, $condition); 3196 } 3197 3198 /** 3199 * Gets a form template's contents. 3200 * 3201 * The form template's reading method can be modified by registering a handler 3202 * to a 'form.fetch' callback event. Any value returned by the callback function 3203 * will be used as the form template markup. 3204 * 3205 * @param array|string $name The form 3206 * @return string 3207 * @package TagParser 3208 */ 3209 3210 function fetch_form($name, $theme = null) 3211 { 3212 global $skin; 3213 static $forms = array(); 3214 3215 isset($theme) or $theme = $skin; 3216 isset($forms[$theme]) or $forms[$theme] = array(); 3217 $fetch = is_array($name); 3218 3219 if ($fetch || !isset($forms[$theme][$name])) { 3220 $names = $fetch ? array_diff($name, array_keys($forms[$theme])) : array($name); 3221 3222 if (has_handler('form.fetch')) { 3223 foreach ($names as $name) { 3224 $forms[$theme][$name] = callback_event('form.fetch', '', false, compact('name', 'skin', 'theme')); 3225 } 3226 } elseif ($fetch) { 3227 $nameset = implode(',', quote_list($names)); 3228 3229 if ($nameset and $rs = safe_rows_start('name, Form', 'txp_form', "name IN (".$nameset.") AND skin = '".doSlash($theme)."'")) { 3230 while ($row = nextRow($rs)) { 3231 $forms[$theme][$row['name']] = $row['Form']; 3232 } 3233 } 3234 } else { 3235 $forms[$theme][$name] = safe_field('Form', 'txp_form', "name ='".doSlash($name)."' AND skin = '".doSlash($theme)."'"); 3236 } 3237 3238 foreach ($names as $form) { 3239 if (empty($forms[$theme][$form])) { 3240 trigger_error(gTxt('form_not_found').' '.$theme.'.'.$form); 3241 $forms[$theme][$form] = false; 3242 } 3243 } 3244 } 3245 3246 if (!$fetch) { 3247 return $forms[$theme][$name]; 3248 } 3249 } 3250 3251 /** 3252 * Parses a form template. 3253 * 3254 * @param string $name The form 3255 * @return string The parsed contents 3256 * @package TagParser 3257 */ 3258 3259 function parse_form($name, $theme = null) 3260 { 3261 global $production_status, $skin, $txp_current_form, $trace; 3262 static $stack = array(), $depth = null; 3263 3264 if ($depth === null) { 3265 $depth = get_pref('form_circular_depth', 15); 3266 } 3267 3268 isset($theme) or $theme = $skin; 3269 $name = (string) $name; 3270 $f = fetch_form($name, $theme); 3271 3272 if ($f === false) { 3273 return false; 3274 } 3275 3276 if (!isset($stack[$name])) { 3277 $stack[$name] = 1; 3278 } elseif ($stack[$name] >= $depth) { 3279 trigger_error(gTxt('form_circular_reference', array('{name}' => $name))); 3280 3281 return ''; 3282 } else { 3283 $stack[$name]++; 3284 } 3285 3286 $old_form = $txp_current_form; 3287 $txp_current_form = $name; 3288 3289 if ($production_status === 'debug') { 3290 $trace->log("[Form: '$theme.$name']"); 3291 $trace->log("[Nesting forms: '".join("' / '", array_keys(array_filter($stack)))."'".($stack[$name] > 1 ? '('.$stack[$name].')' : '')."]"); 3292 } 3293 3294 $out = parse($f); 3295 3296 $txp_current_form = $old_form; 3297 $stack[$name]--; 3298 3299 return $out; 3300 } 3301 3302 /** 3303 * Gets a page template's contents. 3304 * 3305 * The page template's reading method can be modified by registering a handler 3306 * to a 'page.fetch' callback event. Any value returned by the callback function 3307 * will be used as the template markup. 3308 * 3309 * @param string $name The template 3310 * @param string $theme The public theme 3311 * @return string|bool The page template, or FALSE on error 3312 * @package TagParser 3313 * @since 4.6.0 3314 * @example 3315 * echo fetch_page('default'); 3316 */ 3317 3318 function fetch_page($name, $theme) 3319 { 3320 global $pretext, $trace; 3321 3322 if (empty($theme)) { 3323 if (empty($pretext['skin'])) { 3324 $pretext = safe_row("skin, page, css", "txp_section", "name='default'") + $pretext; 3325 } 3326 3327 $theme = $pretext['skin']; 3328 } 3329 3330 if (has_handler('page.fetch')) { 3331 $page = callback_event('page.fetch', '', false, compact('name', 'theme')); 3332 } else { 3333 $page = safe_field('user_html', 'txp_page', "name = '".doSlash($name)."' AND skin = '".doSlash($theme)."'"); 3334 } 3335 3336 if ($page === false) { 3337 return false; 3338 } 3339 3340 $trace->log("[Page: '$theme.$name']"); 3341 3342 return $page; 3343 } 3344 3345 /** 3346 * Parses a page template. 3347 * 3348 * @param string $name The template to parse 3349 * @param string $theme The public theme 3350 * @param string $page Default content to parse 3351 * @return string|bool The parsed page template, or FALSE on error 3352 * @since 4.6.0 3353 * @package TagParser 3354 * @example 3355 * echo parse_page('default'); 3356 */ 3357 3358 function parse_page($name, $theme, $page = '') 3359 { 3360 global $pretext, $trace; 3361 3362 if (!$page) { 3363 $page = fetch_page($name, $theme); 3364 } 3365 3366 if ($page !== false) { 3367 while ($pretext['secondpass'] <= get_pref('secondpass', 1) && preg_match('@<(?:'.TXP_PATTERN.'):@', $page)) { 3368 $page = parse($page); 3369 // the function so nice, he ran it twice 3370 $pretext['secondpass']++; 3371 $trace->log('[ ~~~ secondpass ('.$pretext['secondpass'].') ~~~ ]'); 3372 } 3373 } 3374 3375 return $page; 3376 } 3377 3378 /** 3379 * Gets a HTML select field containing all categories, or sub-categories. 3380 * 3381 * @param string $name Return specified parent category's sub-categories 3382 * @param string $cat The selected category option 3383 * @param string $id The HTML ID 3384 * @return string|bool HTML select field or FALSE on error 3385 * @package Form 3386 */ 3387 3388 function event_category_popup($name, $cat = '', $id = '', $atts = array()) 3389 { 3390 $rs = getTree('root', $name); 3391 3392 if ($rs) { 3393 return treeSelectInput('category', $rs, $cat, $id, 0, $atts); 3394 } 3395 3396 return false; 3397 } 3398 3399 /** 3400 * Gets a category's title. 3401 * 3402 * @param string $name The category 3403 * @param string $type Category's type. Either "article", "file", "image" or "link" 3404 * @return string|bool The title or FALSE on error 3405 */ 3406 3407 function fetch_category_title($name, $type = 'article') 3408 { 3409 static $cattitles = array(); 3410 global $thiscategory; 3411 3412 if (isset($cattitles[$type][$name])) { 3413 return $cattitles[$type][$name]; 3414 } 3415 3416 if (!empty($thiscategory['title']) && $thiscategory['name'] == $name && $thiscategory['type'] == $type) { 3417 $cattitles[$type][$name] = $thiscategory['title']; 3418 3419 return $thiscategory['title']; 3420 } 3421 3422 $f = safe_field("title", 'txp_category', "name = '".doSlash($name)."' AND type = '".doSlash($type)."'"); 3423 $cattitles[$type][$name] = $f; 3424 3425 return $f; 3426 } 3427 3428 /** 3429 * Gets a section's title. 3430 * 3431 * @param string $name The section 3432 * @return string|bool The title or FALSE on error 3433 */ 3434 3435 function fetch_section_title($name) 3436 { 3437 static $sectitles = array(); 3438 global $thissection, $txp_sections; 3439 3440 // Try cache. 3441 if (isset($sectitles[$name])) { 3442 return $sectitles[$name]; 3443 } 3444 3445 if (!empty($thissection) && $thissection['name'] == $name) { 3446 return $thissection['title']; 3447 } elseif ($name == 'default' or empty($name)) { 3448 return ''; 3449 } elseif (isset($txp_sections[$name])) { 3450 return $sectitles[$name] = $txp_sections[$name]['title']; 3451 } 3452 3453 $f = safe_field("title", 'txp_section', "name = '".doSlash($name)."'"); 3454 3455 return $sectitles[$name] = $f; 3456 } 3457 3458 /** 3459 * Updates an article's comment count. 3460 * 3461 * @param int $id The article 3462 * @return bool 3463 * @package Comment 3464 */ 3465 3466 function update_comments_count($id) 3467 { 3468 $id = assert_int($id); 3469 $thecount = safe_field("COUNT(*)", 'txp_discuss', "parentid = '".$id."' AND visible = ".VISIBLE); 3470 $thecount = assert_int($thecount); 3471 $updated = safe_update('textpattern', "comments_count = ".$thecount, "ID = '".$id."'"); 3472 3473 return ($updated) ? true : false; 3474 } 3475 3476 /** 3477 * Recalculates and updates comment counts. 3478 * 3479 * @param array $parentids List of articles to update 3480 * @package Comment 3481 */ 3482 3483 function clean_comment_counts($parentids) 3484 { 3485 $parentids = array_map('assert_int', $parentids); 3486 $parentids = array_filter($parentids); 3487 3488 if ($parentids) { 3489 $rs = safe_rows_start("parentid, COUNT(*) AS thecount", 'txp_discuss', "parentid IN (".implode(',', $parentids).") AND visible = ".VISIBLE." GROUP BY parentid"); 3490 3491 if (!$rs) { 3492 return; 3493 } 3494 3495 $updated = array(); 3496 3497 while ($a = nextRow($rs)) { 3498 safe_update('textpattern', "comments_count = ".$a['thecount'], "ID = ".$a['parentid']); 3499 $updated[] = $a['parentid']; 3500 } 3501 3502 // We still need to update all those, that have zero comments left. 3503 $leftover = array_diff($parentids, $updated); 3504 3505 if ($leftover) { 3506 safe_update('textpattern', "comments_count = 0", "ID IN (".implode(',', $leftover).")"); 3507 } 3508 } 3509 } 3510 3511 /** 3512 * Parses and formats comment message using Textile. 3513 * 3514 * @param string $msg The comment message 3515 * @return string HTML markup 3516 * @package Comment 3517 */ 3518 3519 function markup_comment($msg) 3520 { 3521 $textile = new \Textpattern\Textile\RestrictedParser(); 3522 3523 return $textile->parse($msg); 3524 } 3525 3526 /** 3527 * Updates site's last modification date. 3528 * 3529 * When this action is performed, it will trigger a 3530 * 'site.update > {event}' callback event and pass 3531 * any record set that triggered the update, along 3532 * with the exact time the update was triggered. 3533 * 3534 * @param $trigger Textpattern event or step that triggered the update 3535 * @param $rs Record set data at the time of update 3536 * @package Pref 3537 * @example 3538 * update_lastmod(); 3539 */ 3540 3541 function update_lastmod($trigger = '', $rs = array()) 3542 { 3543 $whenStamp = time(); 3544 $whenDate = strftime('%Y-%m-%d %H:%M:%S', $whenStamp); 3545 3546 safe_upsert('txp_prefs', "val = '$whenDate'", "name = 'lastmod'"); 3547 callback_event('site.update', $trigger, 0, $rs, compact('whenStamp', 'whenDate')); 3548 } 3549 3550 /** 3551 * Gets the site's last modification date. 3552 * 3553 * @param int $unix_ts UNIX timestamp 3554 * @return int UNIX timestamp 3555 * @package Pref 3556 */ 3557 3558 function get_lastmod($unix_ts = null) 3559 { 3560 if ($unix_ts === null) { 3561 $unix_ts = @strtotime(get_pref('lastmod')); 3562 } 3563 3564 // Check for future articles that are now visible. 3565 if (txpinterface === 'public' && $max_article = safe_field("UNIX_TIMESTAMP(Posted)", 'textpattern', "Posted <= ".now('posted')." AND Status >= 4 ORDER BY Posted DESC LIMIT 1")) { 3566 $unix_ts = max($unix_ts, $max_article); 3567 } 3568 3569 return $unix_ts; 3570 } 3571 3572 /** 3573 * Sets headers. 3574 * 3575 * @param array $headers 'name' => 'value' 3576 * @param bool $rewrite If TRUE, rewrites existing headers 3577 */ 3578 3579 function set_headers($headers = array('Content-Type' => 'text/html; charset=utf-8'), $rewrite = false) 3580 { 3581 if (headers_sent()) { 3582 return; 3583 } 3584 3585 $rewrite = (int)$rewrite; 3586 $out = $headers_low = array(); 3587 3588 if (($rewrite != 1 || in_array(true, $headers, true)) && $headers_list = headers_list()) { 3589 foreach ($headers_list as $header) { 3590 list($name, $value) = explode(':', $header, 2) + array(null, null); 3591 $headers_low[strtolower(trim($name))] = $value; 3592 } 3593 } 3594 3595 foreach ($headers as $name => $header) { 3596 $name_low = strtolower(trim($name)); 3597 3598 if ((string)$header === '') { 3599 !$rewrite or header_remove($name && $name != 1 ? $name : null); 3600 } elseif ($header === true) { 3601 if ($name == 1) { 3602 $out = array_merge($out, $headers_low); 3603 } elseif (isset($headers_low[$name_low])) { 3604 $out[$name_low] = $headers_low[$name_low]; 3605 } 3606 } elseif ($name == 1) { 3607 txp_status_header($header); 3608 } elseif ($rewrite == 1 || !isset($headers_low[$name_low])) { 3609 header($name ? $name.': '.$header : $header); 3610 } elseif ($rewrite) { 3611 $header = implode(', ', do_list_unique($headers_low[$name_low].','.$header)); 3612 header($name ? $name.': '.$header : $header); 3613 } 3614 } 3615 3616 return $out ? $out : null; 3617 } 3618 3619 /** 3620 * Sends and handles a lastmod header. 3621 * 3622 * @param int|null $unix_ts The last modification date as a UNIX timestamp 3623 * @param bool $exit If TRUE, terminates the script 3624 * @return array|null Array of sent HTTP status and the lastmod header, or NULL 3625 * @package Pref 3626 */ 3627 3628 function handle_lastmod($unix_ts = null, $exit = true) 3629 { 3630 // Disable caching when not in production 3631 if (get_pref('production_status') != 'live') { 3632 header('Cache-Control: no-cache, no-store, max-age=0'); 3633 } elseif (get_pref('send_lastmod')) { 3634 $unix_ts = get_lastmod($unix_ts); 3635 3636 // Make sure lastmod isn't in the future. 3637 $unix_ts = min($unix_ts, time()); 3638 3639 $last = safe_strftime('rfc822', $unix_ts, 1); 3640 header("Last-Modified: $last"); 3641 3642 $etag = base_convert($unix_ts, 10, 32); 3643 header('ETag: "' . $etag . '"'); 3644 3645 // Get timestamp from request caching headers 3646 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { 3647 $hims = $_SERVER['HTTP_IF_MODIFIED_SINCE']; 3648 $imsd = ($hims) ? strtotime($hims) : 0; 3649 } elseif (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { 3650 $hinm = trim(trim($_SERVER['HTTP_IF_NONE_MATCH']), '"'); 3651 $hinm_apache_gzip_workaround = explode('-gzip', $hinm); 3652 $hinm_apache_gzip_workaround = $hinm_apache_gzip_workaround[0]; 3653 $inmd = ($hinm) ? base_convert($hinm_apache_gzip_workaround, 32, 10) : 0; 3654 } 3655 3656 // Check request timestamps against the current timestamp 3657 if ((isset($imsd) && $imsd >= $unix_ts) || 3658 (isset($inmd) && $inmd >= $unix_ts)) { 3659 log_hit('304'); 3660 3661 header('Content-Length: 0'); 3662 3663 txp_status_header('304 Not Modified'); 3664 3665 if ($exit) { 3666 exit(); 3667 } 3668 3669 return array('304', $last); 3670 } 3671 3672 return array('200', $last); 3673 } 3674 } 3675 3676 /** 3677 * Gets preferences as an array. 3678 * 3679 * Returns preference values from the database as an array. Shouldn't be used to 3680 * retrieve selected preferences, see get_pref() instead. 3681 * 3682 * By default only the global preferences are returned. 3683 * If the optional user name parameter is supplied, the private preferences 3684 * for that user are returned. 3685 * 3686 * @param string $user User name. 3687 * @return array 3688 * @package Pref 3689 * @access private 3690 * @see get_pref() 3691 */ 3692 3693 function get_prefs($user = '') 3694 { 3695 $out = array(); 3696 $user = implode(',', (array) quote_list($user)); 3697 3698 $r = safe_rows_start("name, val", 'txp_prefs', "user_name IN (".$user.") ORDER BY FIELD(user_name, ".$user.")"); 3699 3700 if ($r) { 3701 while ($a = nextRow($r)) { 3702 $out[$a['name']] = $a['val']; 3703 } 3704 } 3705 3706 return $out; 3707 } 3708 3709 /** 3710 * Creates or updates a preference. 3711 * 3712 * @param string $name The name 3713 * @param string $val The value 3714 * @param string $event The section the preference appears in 3715 * @param int $type Either PREF_CORE, PREF_PLUGIN, PREF_HIDDEN 3716 * @param string $html The HTML control type the field uses. Can take a custom function name 3717 * @param int $position Used to sort the field on the Preferences panel 3718 * @param bool $is_private If PREF_PRIVATE, is created as a user pref 3719 * @return bool FALSE on error 3720 * @package Pref 3721 * @example 3722 * if (set_pref('myPref', 'value')) 3723 * { 3724 * echo "'myPref' created or updated."; 3725 * } 3726 */ 3727 3728 function set_pref($name, $val, $event = 'publish', $type = PREF_CORE, $html = 'text_input', $position = 0, $is_private = PREF_GLOBAL) 3729 { 3730 global $prefs; 3731 $prefs[$name] = $val; 3732 $user_name = null; 3733 3734 if ($is_private == PREF_PRIVATE) { 3735 $user_name = PREF_PRIVATE; 3736 } 3737 3738 if (pref_exists($name, $user_name)) { 3739 return update_pref($name, $val, null, null, null, null, $user_name); 3740 } 3741 3742 return create_pref($name, $val, $event, $type, $html, $position, $user_name); 3743 } 3744 3745 /** 3746 * Gets a preference string. 3747 * 3748 * Prefers global system-wide preferences over a user's private preferences. 3749 * 3750 * @param string $thing The named variable 3751 * @param mixed $default Used as a replacement if named pref isn't found 3752 * @param bool $from_db If TRUE checks database opposed $prefs variable in memory 3753 * @return string Preference value or $default 3754 * @package Pref 3755 * @example 3756 * if (get_pref('enable_xmlrpc_server')) 3757 * { 3758 * echo "XML-RPC server is enabled."; 3759 * } 3760 */ 3761 3762 function get_pref($thing, $default = '', $from_db = false) 3763 { 3764 global $prefs, $txp_user; 3765 3766 if ($from_db) { 3767 $name = doSlash($thing); 3768 $user_name = doSlash($txp_user); 3769 3770 $field = safe_field( 3771 "val", 3772 'txp_prefs', 3773 "name = '$name' AND (user_name = '' OR user_name = '$user_name') ORDER BY user_name LIMIT 1" 3774 ); 3775 3776 if ($field !== false) { 3777 $prefs[$thing] = $field; 3778 } 3779 } 3780 3781 if (isset($prefs[$thing])) { 3782 return $prefs[$thing]; 3783 } 3784 3785 return $default; 3786 } 3787 3788 /** 3789 * Removes a preference string. 3790 * 3791 * Removes preference strings based on the given arguments. Use NULL to omit an argument. 3792 * 3793 * @param string|null $name The preference string name 3794 * @param string|null $event The preference event 3795 * @param string|null|bool $user_name The owner. If PREF_PRIVATE, the current user 3796 * @return bool TRUE on success 3797 * @since 4.6.0 3798 * @package Pref 3799 * @example 3800 * if (remove_pref(null, 'myEvent')) 3801 * { 3802 * echo "Removed all preferences from 'myEvent'."; 3803 * } 3804 */ 3805 3806 function remove_pref($name = null, $event = null, $user_name = null) 3807 { 3808 global $txp_user; 3809 3810 $sql = array(); 3811 3812 if ($user_name === PREF_PRIVATE) { 3813 if (!$txp_user) { 3814 return false; 3815 } 3816 3817 $user_name = $txp_user; 3818 } 3819 3820 if ($user_name !== null) { 3821 $sql[] = "user_name = '".doSlash((string) $user_name)."'"; 3822 } 3823 3824 if ($event !== null) { 3825 $sql[] = "event = '".doSlash($event)."'"; 3826 } 3827 3828 if ($name !== null) { 3829 $sql[] = "name = '".doSlash($name)."'"; 3830 } 3831 3832 if ($sql) { 3833 return safe_delete('txp_prefs', join(" AND ", $sql)); 3834 } 3835 3836 return false; 3837 } 3838 3839 /** 3840 * Checks if a preference string exists. 3841 * 3842 * Searches for matching preference strings based on the given arguments. 3843 * 3844 * The $user_name argument can be used to limit the search to a specific user, 3845 * or to global and private strings. If NULL, matches are searched from both 3846 * private and global strings. 3847 * 3848 * @param string $name The preference string name 3849 * @param string|null|bool $user_name Either the username, NULL, PREF_PRIVATE or PREF_GLOBAL 3850 * @return bool TRUE if the string exists, or FALSE on error 3851 * @since 4.6.0 3852 * @package Pref 3853 * @example 3854 * if (pref_exists('myPref')) 3855 * { 3856 * echo "'myPref' exists."; 3857 * } 3858 */ 3859 3860 function pref_exists($name, $user_name = null) 3861 { 3862 global $txp_user; 3863 3864 $sql = array(); 3865 $sql[] = "name = '".doSlash($name)."'"; 3866 3867 if ($user_name === PREF_PRIVATE) { 3868 if (!$txp_user) { 3869 return false; 3870 } 3871 3872 $user_name = $txp_user; 3873 } 3874 3875 if ($user_name !== null) { 3876 $sql[] = "user_name = '".doSlash((string) $user_name)."'"; 3877 } 3878 3879 if (safe_row("name", 'txp_prefs', join(" AND ", $sql))) { 3880 return true; 3881 } 3882 3883 return false; 3884 } 3885 3886 /** 3887 * Creates a preference string. 3888 * 3889 * When a string is created, will trigger a 'preference.create > done' callback event. 3890 * 3891 * @param string $name The name 3892 * @param string $val The value 3893 * @param string $event The section the preference appears in 3894 * @param int $type Either PREF_CORE, PREF_PLUGIN, PREF_HIDDEN 3895 * @param string $html The HTML control type the field uses. Can take a custom function name 3896 * @param int $position Used to sort the field on the Preferences panel 3897 * @param string|bool $user_name The user name, PREF_GLOBAL or PREF_PRIVATE 3898 * @return bool TRUE if the string exists, FALSE on error 3899 * @since 4.6.0 3900 * @package Pref 3901 * @example 3902 * if (create_pref('myPref', 'value', 'site', PREF_PLUGIN, 'text_input', 25)) 3903 * { 3904 * echo "'myPref' created."; 3905 * } 3906 */ 3907 3908 function create_pref($name, $val, $event = 'publish', $type = PREF_CORE, $html = 'text_input', $position = 0, $user_name = PREF_GLOBAL) 3909 { 3910 global $txp_user; 3911 3912 if ($user_name === PREF_PRIVATE) { 3913 if (!$txp_user) { 3914 return false; 3915 } 3916 3917 $user_name = $txp_user; 3918 } 3919 3920 if (pref_exists($name, $user_name)) { 3921 return true; 3922 } 3923 3924 $val = is_scalar($val) ? (string)$val : json_encode($val, TEXTPATTERN_JSON); 3925 3926 if ( 3927 safe_insert( 3928 'txp_prefs', 3929 "name = '".doSlash($name)."', 3930 val = '".doSlash($val)."', 3931 event = '".doSlash($event)."', 3932 html = '".doSlash($html)."', 3933 type = ".intval($type).", 3934 position = ".intval($position).", 3935 user_name = '".doSlash((string) $user_name)."'" 3936 ) === false 3937 ) { 3938 return false; 3939 } 3940 3941 callback_event('preference.create', 'done', 0, compact('name', 'val', 'event', 'type', 'html', 'position', 'user_name')); 3942 3943 return true; 3944 } 3945 3946 /** 3947 * Updates a preference string. 3948 * 3949 * Updates a preference string's properties. The $name and $user_name 3950 * arguments are used for selecting the updated string, and rest of the 3951 * arguments take the new values. Use NULL to omit an argument. 3952 * 3953 * When a string is updated, will trigger a 'preference.update > done' callback event. 3954 * 3955 * @param string $name The update preference string's name 3956 * @param string|null $val The value 3957 * @param string|null $event The section the preference appears in 3958 * @param int|null $type Either PREF_CORE, PREF_PLUGIN, PREF_HIDDEN 3959 * @param string|null $html The HTML control type the field uses. Can take a custom function name 3960 * @param int|null $position Used to sort the field on the Preferences panel 3961 * @param string|bool|null $user_name The updated string's owner, PREF_GLOBAL or PREF_PRIVATE 3962 * @return bool FALSE on error 3963 * @since 4.6.0 3964 * @package Pref 3965 * @example 3966 * if (update_pref('myPref', 'New value.')) 3967 * { 3968 * echo "Updated 'myPref' value."; 3969 * } 3970 */ 3971 3972 function update_pref($name, $val = null, $event = null, $type = null, $html = null, $position = null, $user_name = PREF_GLOBAL) 3973 { 3974 global $txp_user; 3975 3976 $where = $set = array(); 3977 $where[] = "name = '".doSlash($name)."'"; 3978 3979 if ($user_name === PREF_PRIVATE) { 3980 if (!$txp_user) { 3981 return false; 3982 } 3983 3984 $user_name = $txp_user; 3985 } 3986 3987 if ($user_name !== null) { 3988 $where[] = "user_name = '".doSlash((string) $user_name)."'"; 3989 } 3990 3991 if (isset($val)) { 3992 $val = is_scalar($val) ? (string)$val : json_encode($val, TEXTPATTERN_JSON); 3993 } 3994 3995 foreach (array('val', 'event', 'type', 'html', 'position') as $field) { 3996 if ($$field !== null) { 3997 $set[] = $field." = '".doSlash($$field)."'"; 3998 } 3999 } 4000 4001 if ($set && safe_update('txp_prefs', join(', ', $set), join(" AND ", $where))) { 4002 callback_event('preference.update', 'done', 0, compact('name', 'val', 'event', 'type', 'html', 'position', 'user_name')); 4003 4004 return true; 4005 } 4006 4007 return false; 4008 } 4009 4010 /** 4011 * Renames a preference string. 4012 * 4013 * When a string is renamed, will trigger a 'preference.rename > done' callback event. 4014 * 4015 * @param string $newname The new name 4016 * @param string $name The current name 4017 * @param string $user_name Either the username, PREF_GLOBAL or PREF_PRIVATE 4018 * @return bool FALSE on error 4019 * @since 4.6.0 4020 * @package Pref 4021 * @example 4022 * if (rename_pref('mynewPref', 'myPref')) 4023 * { 4024 * echo "Renamed 'myPref' to 'mynewPref'."; 4025 * } 4026 */ 4027 4028 function rename_pref($newname, $name, $user_name = null) 4029 { 4030 global $txp_user; 4031 4032 $where = array(); 4033 $where[] = "name = '".doSlash($name)."'"; 4034 4035 if ($user_name === PREF_PRIVATE) { 4036 if (!$txp_user) { 4037 return false; 4038 } 4039 4040 $user_name = $txp_user; 4041 } 4042 4043 if ($user_name !== null) { 4044 $where[] = "user_name = '".doSlash((string) $user_name)."'"; 4045 } 4046 4047 if (safe_update('txp_prefs', "name = '".doSlash($newname)."'", join(" AND ", $where))) { 4048 callback_event('preference.rename', 'done', 0, compact('newname', 'name', 'user_name')); 4049 4050 return true; 4051 } 4052 4053 return false; 4054 } 4055 4056 /** 4057 * Gets a list of custom fields. 4058 * 4059 * @return array 4060 * @package CustomField 4061 */ 4062 4063 function getCustomFields() 4064 { 4065 global $prefs; 4066 static $out = null; 4067 4068 // Have cache? 4069 if (!is_array($out)) { 4070 $cfs = preg_grep('/^custom_\d+_set/', array_keys($prefs)); 4071 $out = array(); 4072 4073 foreach ($cfs as $name) { 4074 preg_match('/(\d+)/', $name, $match); 4075 4076 if ($prefs[$name] !== '') { 4077 $out[$match[1]] = strtolower($prefs[$name]); 4078 } 4079 } 4080 } 4081 4082 return $out; 4083 } 4084 4085 /** 4086 * Build a query qualifier to filter non-matching custom fields from the 4087 * result set. 4088 * 4089 * @param array $custom An array of 'custom_field_name' => field_number tuples 4090 * @param array $pairs Filter criteria: An array of 'name' => value tuples 4091 * @return bool|string An SQL qualifier for a query's 'WHERE' part 4092 * @package CustomField 4093 */ 4094 4095 function buildCustomSql($custom, $pairs, $exclude = array()) 4096 { 4097 if ($pairs) { 4098 foreach ($pairs as $k => $val) { 4099 $no = array_search($k, $custom); 4100 4101 if ($no !== false) { 4102 $not = ($exclude === true || isset($exclude[$k])) ? 'NOT ' : ''; 4103 4104 if ($val === true) { 4105 $out[] = "({$not}custom_{$no} != '')"; 4106 } else { 4107 $val = doSlash($val); 4108 $parts = array(); 4109 4110 foreach ((array)$val as $v) { 4111 list($from, $to) = explode('%%', $v, 2) + array(null, null); 4112 4113 if (!isset($to)) { 4114 $parts[] = "{$not}custom_{$no} LIKE '$from'"; 4115 } elseif ($from !== '') { 4116 $parts[] = $to === '' ? "{$not}custom_{$no} >= '$from'" : "{$not}custom_{$no} BETWEEN '$from' AND '$to'"; 4117 } elseif ($to !== '') { 4118 $parts[] = "{$not}custom_{$no} <= '$to'"; 4119 } 4120 } 4121 4122 if ($parts) { 4123 $out[] = '('.join(' OR ', $parts).')'; 4124 } 4125 } 4126 } 4127 } 4128 } 4129 4130 return !empty($out) ? ' AND '.join(' AND ', $out).' ' : false; 4131 } 4132 4133 /** 4134 * Build a query qualifier to filter time fields from the 4135 * result set. 4136 * 4137 * @param string $month A starting time point 4138 * @param string $time A time offset 4139 * @param string $field The field to filter 4140 * @return string An SQL qualifier for a query's 'WHERE' part 4141 */ 4142 4143 function buildTimeSql($month, $time, $field = 'Posted') 4144 { 4145 $safe_field = '`'.doSlash($field).'`'; 4146 $timeq = '1'; 4147 4148 if ($month === 'past' || $month === 'any' || $month === 'future') { 4149 if ($month === 'past') { 4150 $timeq = "$safe_field <= ".now($field); 4151 } elseif ($month === 'future') { 4152 $timeq = "$safe_field > ".now($field); 4153 } 4154 } elseif ($time === 'past' || $time === 'any' || $time === 'future') { 4155 if ($time === 'past') { 4156 $timeq = "$safe_field <= ".now($field); 4157 } elseif ($time === 'future') { 4158 $timeq = "$safe_field > ".now($field); 4159 } 4160 4161 $timeq .= ($month ? " AND $safe_field LIKE '".doSlash($month)."%'" : ''); 4162 } elseif (strpos($time, '%') !== false) { 4163 $start = $month ? strtotime($month) : time() or $start = time(); 4164 $timeq = "$safe_field LIKE '".doSlash(strftime($time, $start))."%'"; 4165 } else { 4166 $start = $month ? safe_strtotime($month) : false; 4167 4168 if ($start === false) { 4169 $from = $month ? "'".doSlash($month)."'" : now($field); 4170 $start = time(); 4171 } else { 4172 $from = "FROM_UNIXTIME($start)"; 4173 } 4174 4175 if ($time === 'since') { 4176 $timeq = "$safe_field > $from"; 4177 } elseif ($time === 'until') { 4178 $timeq = "$safe_field <= $from"; 4179 } else { 4180 $stop = strtotime($time, $start) or $stop = time(); 4181 4182 if ($start > $stop) { 4183 list($start, $stop) = array($stop, $start); 4184 } 4185 4186 $timeq = ($start == $stop ? 4187 "$safe_field = FROM_UNIXTIME($start)" : 4188 "$safe_field BETWEEN FROM_UNIXTIME($start) AND FROM_UNIXTIME($stop)" 4189 ); 4190 } 4191 } 4192 4193 return $timeq; 4194 } 4195 4196 /** 4197 * Sends a HTTP status header. 4198 * 4199 * @param string $status The HTTP status code 4200 * @package Network 4201 * @example 4202 * txp_status_header('403 Forbidden'); 4203 */ 4204 4205 function txp_status_header($status = '200 OK') 4206 { 4207 if (IS_FASTCGI) { 4208 header("Status: $status"); 4209 } elseif (serverSet('SERVER_PROTOCOL') == 'HTTP/1.0') { 4210 header("HTTP/1.0 $status"); 4211 } else { 4212 header("HTTP/1.1 $status"); 4213 } 4214 } 4215 4216 /** 4217 * Terminates normal page rendition and outputs an error page. 4218 * 4219 * @param string|array $msg The error message 4220 * @param string $status HTTP status code 4221 * @param string $url Redirects to the specified URL. Can be used with $status of 301, 302 and 307 4222 * @package Tag 4223 */ 4224 4225 function txp_die($msg, $status = '503', $url = '') 4226 { 4227 global $connected, $txp_error_message, $txp_error_status, $txp_error_code, $pretext, $production_status, $trace; 4228 4229 // Make it possible to call this function as a tag, e.g. in an article 4230 // <txp:txp_die status="410" />. 4231 if (is_array($msg)) { 4232 extract(lAtts(array( 4233 'msg' => '', 4234 'status' => '503', 4235 'url' => '', 4236 ), $msg)); 4237 } 4238 4239 // Intentionally incomplete - just the ones we're likely to use. 4240 $codes = array( 4241 '200' => 'OK', 4242 '301' => 'Moved Permanently', 4243 '302' => 'Found', 4244 '303' => 'See Other', 4245 '304' => 'Not Modified', 4246 '307' => 'Temporary Redirect', 4247 '308' => 'Permanent Redirect', 4248 '401' => 'Unauthorized', 4249 '403' => 'Forbidden', 4250 '404' => 'Not Found', 4251 '410' => 'Gone', 4252 '414' => 'Request-URI Too Long', 4253 '451' => 'Unavailable For Legal Reasons', 4254 '500' => 'Internal Server Error', 4255 '501' => 'Not Implemented', 4256 '503' => 'Service Unavailable' 4257 ); 4258 4259 if ($status) { 4260 if (isset($codes[strval($status)])) { 4261 $status = strval($status).' '.$codes[$status]; 4262 } 4263 4264 txp_status_header($status); 4265 } 4266 4267 $code = (int) $status; 4268 4269 callback_event('txp_die', $code, 0, $url); 4270 4271 // Redirect with status. 4272 if ($url && in_array($code, array(301, 302, 303, 307, 308))) { 4273 ob_end_clean(); 4274 header("Location: $url", true, $code); 4275 die('<html><head><meta http-equiv="refresh" content="0;URL='.txpspecialchars($url).'"></head><body><p>Document has <a href="'.txpspecialchars($url).'">moved here</a>.</p></body></html>'); 4276 } 4277 4278 $out = false; 4279 $skin = empty($pretext['skin']) ? null : $pretext['skin']; 4280 4281 if ($connected && @txpinterface == 'public') { 4282 $out = fetch_page("error_{$code}", $skin) or $out = fetch_page('error_default', $skin); 4283 } 4284 4285 if ($out === false) { 4286 $out = <<<eod 4287 <!DOCTYPE html> 4288 <html lang="en"> 4289 <head> 4290 <meta charset="utf-8"> 4291 <meta name="robots" content="noindex"> 4292 <title>Textpattern Error: <txp:error_status /></title> 4293 </head> 4294 <body> 4295 <p><txp:error_message /></p> 4296 </body> 4297 </html> 4298 eod; 4299 } 4300 4301 header("Content-Type: text/html; charset=utf-8"); 4302 $debug = $production_status === 'live' ? 4303 '' : 4304 $trace->summary().($production_status === 'debug' ? $trace->result() : ''); 4305 4306 if (is_callable('parse')) { 4307 $txp_error_message = $msg; 4308 $txp_error_status = $status; 4309 $txp_error_code = $code; 4310 set_error_handler("tagErrorHandler"); 4311 die(parse($out).$debug); 4312 } else { 4313 $out = preg_replace( 4314 array('@<txp:error_status[^>]*/>@', '@<txp:error_message[^>]*/>@'), 4315 array($status, $msg), 4316 $out 4317 ); 4318 4319 die($out.$debug); 4320 } 4321 } 4322 4323 /** 4324 * Get field => alias array. 4325 * 4326 * @param string $match 4327 * @return array() 4328 * @since 4.8.0 4329 * @package TagParser 4330 */ 4331 4332 function parse_qs($match, $sep='=') 4333 { 4334 $pairs = array(); 4335 4336 foreach (do_list_unique($match) as $chunk) { 4337 $name = strtok($chunk, $sep); 4338 $alias = strtok($sep); 4339 $pairs[strtolower($name)] = $alias; 4340 }; 4341 4342 return $pairs; 4343 } 4344 4345 /** 4346 * Gets a URL-encoded and HTML entity-escaped query string for a URL. 4347 * 4348 * Builds a HTTP query string from an associative array. 4349 * 4350 * @param array $q The parameters for the query 4351 * @return string The query, including starting "?". 4352 * @package URL 4353 * @example 4354 * echo join_qs(array('param1' => 'value1', 'param2' => 'value2')); 4355 */ 4356 4357 function join_qs($q, $sep = '&') 4358 { 4359 $qs = array(); 4360 $sql = $sep !== '&'; 4361 4362 foreach ($q as $k => $v) { 4363 if (is_array($v)) { 4364 $v = join(',', $v); 4365 } 4366 4367 if ($k && (string) $v !== '') { 4368 $qs[$k] = $sql ? "$k = $v" : urlencode($k).'='.urlencode($v); 4369 } 4370 } 4371 4372 if (!isset($sep)) { 4373 return $qs; 4374 } 4375 4376 $str = join($sep, $qs); 4377 4378 return $str ? ($sql ? '' : '?').$str : ''; 4379 } 4380 4381 /** 4382 * Builds a HTML attribute list from an array. 4383 * 4384 * Takes an array of raw HTML attributes, and returns a properly 4385 * sanitised HTML attribute string for use in a HTML tag. 4386 * 4387 * Internally handles HTML boolean attributes, array lists and query strings. 4388 * If an attributes value is set as a boolean, the attribute is considered 4389 * as one too. If a value is NULL, it's omitted and the attribute is added 4390 * without a value. An array value is converted to a space-separated list, 4391 * or for 'href' and 'src' to a URL encoded query string. 4392 * 4393 * @param array|string $atts HTML attributes 4394 * @param int $flags TEXTPATTERN_STRIP_EMPTY_STRING 4395 * @return string HTML attribute list 4396 * @since 4.6.0 4397 * @package HTML 4398 * @example 4399 * echo join_atts(array('class' => 'myClass', 'disabled' => true)); 4400 */ 4401 4402 function join_atts($atts, $flags = TEXTPATTERN_STRIP_EMPTY_STRING, $glue = ' ') 4403 { 4404 if (!is_array($atts)) { 4405 return $atts ? ' '.trim($atts) : ''; 4406 } 4407 4408 $list = ''; 4409 $txp = $flags & TEXTPATTERN_STRIP_TXP; 4410 4411 foreach ($atts as $name => $value) { 4412 if (($flags & TEXTPATTERN_STRIP_EMPTY && !$value) || ($value === false) || ($txp && $value === null)) { 4413 continue; 4414 } elseif ($value === null || $txp && $value === true) { 4415 $list .= ' '.$name; 4416 continue; 4417 } elseif (is_array($value)) { 4418 if ($name == 'href' || $name == 'src') { 4419 $value = join_qs($value); 4420 } else { 4421 $value = txpspecialchars(join($glue, $value)); 4422 } 4423 } elseif ($name != 'href' && $name != 'src') { 4424 $value = txpspecialchars($value === true ? $name : $value); 4425 } else { 4426 $value = txpspecialchars(str_replace('&', '&', $value)); 4427 } 4428 4429 if (!($flags & TEXTPATTERN_STRIP_EMPTY_STRING && $value === '')) { 4430 $list .= ' '.$name.'="'.$value.'"'; 4431 } 4432 } 4433 4434 return $list; 4435 } 4436 4437 /** 4438 * Builds a page URL from an array of parameters. 4439 * 4440 * The $inherit can be used to add parameters to an existing url, e.g: 4441 * pagelinkurl(array('pg' => 2), $pretext). 4442 * 4443 * Cannot be used to link to an article. See permlinkurl() and permlinkurl_id() instead. 4444 * 4445 * @param array $parts The parts used to construct the URL 4446 * @param array $inherit Can be used to add parameters to an existing url 4447 * @return string 4448 * @see permlinkurl() 4449 * @see permlinkurl_id() 4450 * @package URL 4451 */ 4452 4453 function pagelinkurl($parts, $inherit = array(), $url_mode = null) 4454 { 4455 global $permlink_mode, $prefs, $txp_context, $txp_sections; 4456 4457 // Link to an article. 4458 if (!empty($parts['id'])) { 4459 return permlinkurl_id($parts['id']); 4460 } 4461 4462 $keys = $parts; 4463 empty($inherit) or $keys += $inherit; 4464 empty($txp_context) or $keys += $txp_context; 4465 unset($keys['id']); 4466 4467 if (isset($prefs['custom_url_func']) 4468 && is_callable($prefs['custom_url_func']) 4469 && ($url = call_user_func($prefs['custom_url_func'], $keys, PAGELINKURL)) !== false) { 4470 return $url; 4471 } 4472 4473 if (isset($keys['s'])) { 4474 if (!isset($url_mode) && isset($txp_sections[$keys['s']])) { 4475 $url_mode = $txp_sections[$keys['s']]['permlink_mode']; 4476 } 4477 4478 if ($keys['s'] == 'default') { 4479 unset($keys['s']); 4480 } 4481 } 4482 4483 if (empty($url_mode)) { 4484 $url_mode = $permlink_mode; 4485 } 4486 4487 // 'article' context is implicit, no need to add it to the page URL. 4488 if (isset($keys['context']) && $keys['context'] == 'article') { 4489 unset($keys['context']); 4490 } 4491 4492 $numkeys = array(); 4493 4494 foreach ($keys as $key => $v) { 4495 if (is_numeric($key)) { 4496 $numkeys[$key] = urlencode($v).'/'; 4497 unset($keys[$key]); 4498 } 4499 } 4500 4501 if ($url_mode == 'messy') { 4502 if (!empty($keys['context'])) { 4503 $keys['context'] = gTxt($keys['context'].'_context'); 4504 } 4505 4506 return hu.'index.php'.join_qs($keys); 4507 } else { 4508 // All clean URL modes use the same schemes for list pages. 4509 $url = hu; 4510 4511 if (!empty($keys['rss'])) { 4512 $url = hu.'rss/'; 4513 unset($keys['rss']); 4514 } elseif (!empty($keys['atom'])) { 4515 $url = hu.'atom/'; 4516 unset($keys['atom']); 4517 } elseif (!empty($keys['s'])) { 4518 if (!empty($keys['context'])) { 4519 $keys['context'] = gTxt($keys['context'].'_context'); 4520 } 4521 $url = hu.urlencode($keys['s']).'/'; 4522 unset($keys['s']); 4523 if (!empty($keys['c']) && ($url_mode == 'section_category_title' || $url_mode == 'breadcrumb_title')) { 4524 $catpath = $url_mode == 'breadcrumb_title' ? 4525 array_column(getRootPath($keys['c'], empty($keys['context']) ? 'article' : $keys['context']), 'name') : 4526 array($keys['c']); 4527 $url .= implode('/', array_map('urlencode', array_reverse($catpath))).'/'; 4528 unset($keys['c']); 4529 } 4530 } elseif (!empty($keys['month']) && $url_mode == 'year_month_day_title') { 4531 if (!empty($keys['context'])) { 4532 $keys['context'] = gTxt($keys['context'].'_context'); 4533 } 4534 $url = hu.implode('/', explode('-', urlencode($keys['month']))).'/'; 4535 unset($keys['month']); 4536 } elseif (!empty($keys['author'])) { 4537 $ct = empty($keys['context']) ? '' : strtolower(urlencode(gTxt($keys['context'].'_context'))).'/'; 4538 $url = hu.strtolower(urlencode(gTxt('author'))).'/'.$ct.urlencode($keys['author']).'/'; 4539 unset($keys['author'], $keys['context']); 4540 } elseif (!empty($keys['c'])) { 4541 $ct = empty($keys['context']) ? '' : strtolower(urlencode(gTxt($keys['context'].'_context'))).'/'; 4542 $url = hu.strtolower(urlencode(gTxt('category'))).'/'.$ct; 4543 $catpath = $url_mode == 'breadcrumb_title' ? 4544 array_column(getRootPath($keys['c'], empty($keys['context']) ? 'article' : $keys['context']), 'name') : 4545 array($keys['c']); 4546 $url .= implode('/', array_map('urlencode', array_reverse($catpath))).'/'; 4547 unset($keys['c'], $keys['context']); 4548 } 4549 4550 return (empty($prefs['no_trailing_slash']) ? $url : rtrim($url, '/')).join_qs($keys); 4551 } 4552 } 4553 4554 /** 4555 * Gets a URL for the given article. 4556 * 4557 * If you need to generate a list of article URLs from already fetched table 4558 * rows, consider using permlinkurl() over this due to performance benefits. 4559 * 4560 * @param int $id The article ID 4561 * @return string The URL 4562 * @see permlinkurl() 4563 * @package URL 4564 * @example 4565 * echo permlinkurl_id(12); 4566 */ 4567 4568 function permlinkurl_id($id) 4569 { 4570 global $permlinks, $thisarticle; 4571 4572 $id = (int) $id; 4573 4574 if (isset($permlinks[$id])) { 4575 return permlinkurl(array('id' => $id)); 4576 } 4577 4578 if (isset($thisarticle['thisid']) && $thisarticle['thisid'] == $id) { 4579 return permlinkurl($thisarticle); 4580 } 4581 4582 $rs = safe_row( 4583 "ID AS thisid, Section, Title, url_title, Category1, Category2, UNIX_TIMESTAMP(Posted) AS posted, UNIX_TIMESTAMP(Expires) AS expires", 4584 'textpattern', 4585 "ID = $id" 4586 ); 4587 4588 return permlinkurl($rs); 4589 } 4590 4591 /** 4592 * Generates an article URL from the given data array. 4593 * 4594 * @param array $article_array An array consisting of keys 'thisid', 'section', 'title', 'url_title', 'posted', 'expires' 4595 * @return string The URL 4596 * @package URL 4597 * @see permlinkurl_id() 4598 * @example 4599 * echo permlinkurl_id(array( 4600 * 'thisid' => 12, 4601 * 'section' => 'blog', 4602 * 'url_title' => 'my-title', 4603 * 'posted' => 1345414041, 4604 * 'expires' => 1345444077 4605 * )); 4606 */ 4607 4608 function permlinkurl($article_array, $hu = hu) 4609 { 4610 global $permlink_mode, $prefs, $permlinks, $production_status, $txp_sections; 4611 static $internals = array('id', 's', 'context', 'pg', 'p'), $now = null; 4612 4613 if (isset($prefs['custom_url_func']) 4614 and is_callable($prefs['custom_url_func']) 4615 and ($url = call_user_func($prefs['custom_url_func'], $article_array, PERMLINKURL)) !== false) { 4616 return $url; 4617 } 4618 4619 extract(lAtts(array( 4620 'thisid' => null, 4621 'id' => null, 4622 'title' => null, 4623 'url_title' => null, 4624 'section' => null, 4625 'category1' => null, 4626 'category2' => null, 4627 'posted' => null, 4628 'uposted' => null, 4629 'expires' => null, 4630 'uexpires' => null, 4631 ), array_change_key_case($article_array, CASE_LOWER), false)); 4632 4633 if (empty($thisid)) { 4634 $thisid = $id; 4635 } 4636 4637 $thisid = (int) $thisid; 4638 $keys = get_context(null); 4639 4640 foreach ($internals as $key) { 4641 unset($keys[$key]); 4642 } 4643 4644 if (isset($permlinks[$thisid])) { 4645 return $hu.($permlinks[$thisid] === true ? 4646 'index.php'.join_qs(array('id' => $thisid) + $keys) : 4647 $permlinks[$thisid].join_qs($keys) 4648 ); 4649 } 4650 4651 if (!isset($now)) { 4652 $now = strftime('%F %T'); 4653 } 4654 4655 if (empty($prefs['publish_expired_articles']) && 4656 !empty($expires) && 4657 $production_status != 'live' && 4658 txpinterface == 'public' && 4659 (is_numeric($expires) ? $expires < time() 4660 : (isset($uexpires) ? $uexpires < time() 4661 : $expires < $now) 4662 ) 4663 ) { 4664 trigger_error(gTxt('permlink_to_expired_article', array('{id}' => $thisid)), E_USER_NOTICE); 4665 } 4666 4667 if (empty($section)) { 4668 $url_mode = 'messy'; 4669 } elseif (isset($txp_sections[$section])) { 4670 $url_mode = empty($txp_sections[$section]['permlink_mode']) ? $permlink_mode : $txp_sections[$section]['permlink_mode']; 4671 } else { 4672 $url_mode = $permlink_mode; 4673 } 4674 4675 if (empty($url_title) && !in_array($url_mode, array('section_id_title', 'id_title'))) { 4676 $url_mode = 'messy'; 4677 } 4678 4679 $section = urlencode($section); 4680 $url_title = urlencode($url_title); 4681 4682 switch ($url_mode) { 4683 case 'section_id_title': 4684 if ($url_title && $prefs['attach_titles_to_permalinks']) { 4685 $out = "$section/$thisid/$url_title"; 4686 } else { 4687 $out = "$section/$thisid"; 4688 } 4689 break; 4690 case 'year_month_day_title': 4691 list($y, $m, $d) = explode("-", date("Y-m-d", isset($uposted) ? $uposted : $posted)); 4692 $out = "$y/$m/$d/$url_title"; 4693 break; 4694 case 'id_title': 4695 if ($url_title && $prefs['attach_titles_to_permalinks']) { 4696 $out = "$thisid/$url_title"; 4697 } else { 4698 $out = "$thisid"; 4699 } 4700 break; 4701 case 'section_title': 4702 $out = "$section/$url_title"; 4703 break; 4704 case 'section_category_title': 4705 $out = $section.'/'. 4706 (empty($category1) ? '' : urlencode($category1).'/'). 4707 (empty($category2) ? '' : urlencode($category2).'/').$url_title; 4708 break; 4709 case 'breadcrumb_title': 4710 $out = $section.'/'; 4711 if (empty($category1)) { 4712 if (!empty($category2)) { 4713 $path = array_reverse(array_column(getRootPath($category2), 'name')); 4714 $out .= implode('/', array_map('urlencode', $path)).'/'; 4715 } 4716 } elseif (empty($category2)) { 4717 $path = array_reverse(array_column(getRootPath($category1), 'name')); 4718 $out .= implode('/', array_map('urlencode', $path)).'/'; 4719 } else { 4720 $c2_path = array_reverse(array_column(getRootPath($category2), 'name')); 4721 if (in_array($category1, $c2_path)) { 4722 $out .= implode('/', array_map('urlencode', $c2_path)).'/'; 4723 } else { 4724 $c1_path = array_reverse(array_column(getRootPath($category1), 'name')); 4725 if (in_array($category2, $c1_path)) { 4726 $out .= implode('/', array_map('urlencode', $c1_path)).'/'; 4727 } else { 4728 $c0_path = array_intersect($c1_path, $c2_path); 4729 $out .= ($c0_path ? implode('/', array_map('urlencode', $c0_path)).'/' : ''). 4730 urlencode($category1).'/'.urlencode($category2).'/'; 4731 } 4732 } 4733 } 4734 $out .= $url_title; 4735 break; 4736 case 'title_only': 4737 $out = $url_title; 4738 break; 4739 case 'messy': 4740 $out = "index.php"; 4741 $keys['id'] = $thisid; 4742 break; 4743 } 4744 4745 $permlinks[$thisid] = $url_mode == 'messy' ? true : $out; 4746 4747 return $hu.$out.join_qs($keys); 4748 } 4749 4750 /** 4751 * Gets a file download URL. 4752 * 4753 * @param int $id The ID 4754 * @param string $filename The filename 4755 * @return string 4756 * @package File 4757 */ 4758 4759 function filedownloadurl($id, $filename = '') 4760 { 4761 global $permlink_mode; 4762 4763 if ($permlink_mode == 'messy') { 4764 return hu.'index.php'.join_qs(array( 4765 's' => 'file_download', 4766 'id' => (int) $id, 4767 )); 4768 } 4769 4770 if ($filename) { 4771 $filename = '/'.urlencode($filename); 4772 4773 // FIXME: work around yet another mod_deflate problem (double compression) 4774 // https://blogs.msdn.microsoft.com/wndp/2006/08/21/content-encoding-content-type/ 4775 if (preg_match('/gz$/i', $filename)) { 4776 $filename .= a; 4777 } 4778 } 4779 4780 return hu.'file_download/'.intval($id).$filename; 4781 } 4782 4783 /** 4784 * Gets an image's absolute URL. 4785 * 4786 * @param int $id The image 4787 * @param string $ext The file extension 4788 * @param bool $thumbnail If TRUE returns a URL to the thumbnail 4789 * @return string 4790 * @package Image 4791 */ 4792 4793 function imagesrcurl($id, $ext, $thumbnail = false) 4794 { 4795 global $img_dir; 4796 $thumbnail = $thumbnail ? 't' : ''; 4797 4798 return ihu.$img_dir.'/'.$id.$thumbnail.$ext; 4799 } 4800 4801 /** 4802 * Checks if a value exists in a list. 4803 * 4804 * @param string $val The searched value 4805 * @param string $list The value list 4806 * @param string $delim The list boundary 4807 * @return bool Returns TRUE if $val is found, FALSE otherwise 4808 * @example 4809 * if (in_list('red', 'blue, green, red, yellow')) 4810 * { 4811 * echo "'red' found from the list."; 4812 * } 4813 */ 4814 4815 function in_list($val, $list, $delim = ',') 4816 { 4817 return in_array((string) $val, do_list($list, $delim), true); 4818 } 4819 4820 /** 4821 * Split a string by string. 4822 * 4823 * Trims the created values of whitespace. 4824 * 4825 * @param string $list The string 4826 * @param string $delim The boundary 4827 * @return array 4828 * @example 4829 * print_r( 4830 * do_list('value1, value2, value3') 4831 * ); 4832 */ 4833 4834 function do_list($list, $delim = ',') 4835 { 4836 if (is_array($delim)) { 4837 list($delim, $range) = $delim + array(null, null); 4838 } 4839 4840 $array = explode($delim, $list); 4841 4842 if (isset($range) && strpos($list, $range) !== false) { 4843 $pattern = '/^\s*(\w|[-+]?\d+)\s*'.preg_quote($range, '/').'\s*(\w|[-+]?\d+)\s*$/'; 4844 $out = array(); 4845 4846 foreach ($array as $item) { 4847 if (!preg_match($pattern, $item, $match)) { 4848 $out[] = trim($item); 4849 } else { 4850 list($m, $start, $end) = $match; 4851 foreach(range($start, $end) as $v) { 4852 $out[] = $v; 4853 } 4854 } 4855 } 4856 } 4857 4858 return isset($out) ? $out : array_map('trim', $array); 4859 } 4860 4861 /** 4862 * Split a string by string, returning only unique results. 4863 * 4864 * Trims unique values of whitespace. Flags permit exclusion of empty strings. 4865 * 4866 * @param string $list The string 4867 * @param string $delim The boundary 4868 * @param int $flags TEXTPATTERN_STRIP_NONE | TEXTPATTERN_STRIP_EMPTY | TEXTPATTERN_STRIP_EMPTY_STRING 4869 * @return array 4870 * @example 4871 * print_r( 4872 * do_list_unique('value1, value2, value3') 4873 * ); 4874 */ 4875 4876 function do_list_unique($list, $delim = ',', $flags = TEXTPATTERN_STRIP_EMPTY_STRING) 4877 { 4878 $out = array_unique(do_list($list, $delim)); 4879 4880 if ($flags & TEXTPATTERN_STRIP_EMPTY) { 4881 $out = array_filter($out); 4882 } 4883 4884 if ($flags & TEXTPATTERN_STRIP_EMPTY_STRING) { 4885 $out = array_filter($out, function ($v) { 4886 return ($v=='') ? false : true; 4887 }); 4888 } 4889 4890 return $out; 4891 } 4892 4893 /** 4894 * Wraps a string in single quotes. 4895 * 4896 * @param string $val The input string 4897 * @return string 4898 */ 4899 4900 function doQuote($val) 4901 { 4902 return "'$val'"; 4903 } 4904 4905 /** 4906 * Escapes special characters for use in an SQL statement and wraps the value 4907 * in quote. 4908 * 4909 * Useful for creating an array/string of values for use in an SQL statement. 4910 * 4911 * @param string|array $in The input value 4912 * @param string|null $separator The separator 4913 * @return mixed 4914 * @package DB 4915 * @example 4916 * if ($r = safe_row('name', 'myTable', 'type in(' . quote_list(array('value1', 'value2'), ',') . ')') 4917 * { 4918 * echo "Found '{$r['name']}'."; 4919 * } 4920 */ 4921 4922 function quote_list($in, $separator = null) 4923 { 4924 $out = doArray(doSlash($in), 'doQuote'); 4925 4926 return isset($separator) ? implode($separator, $out) : $out; 4927 } 4928 4929 /** 4930 * Adds a line to the tag trace. 4931 * 4932 * @param string $msg The message 4933 * @param int $tracelevel_diff Change trace level 4934 * @deprecated in 4.6.0 4935 * @package Debug 4936 */ 4937 4938 function trace_add($msg, $level = 0, $dummy = null) 4939 { 4940 global $trace; 4941 4942 if ((int) $level > 0) { 4943 $trace->start($msg); 4944 } elseif ((int) $level < 0) { 4945 $trace->stop(); 4946 } else { 4947 $trace->log($msg); 4948 } 4949 4950 // TODO: Uncomment this to trigger deprecated warning in a version (or two). 4951 // Due to the radical changes under the hood, plugin authors will probably 4952 // support dual 4.5/4.6 plugins for the short term. Deprecating this 4953 // immediately causes unnecessary pain for developers. 4954 // trigger_error(gTxt('deprecated_function_with', array('{name}' => __FUNCTION__, '{with}' => 'class Trace')), E_USER_NOTICE); 4955 } 4956 4957 /** 4958 * Push current article on the end of data stack. 4959 * 4960 * Populates $stack_article global with the current $thisarticle. 4961 */ 4962 4963 function article_push() 4964 { 4965 global $thisarticle, $stack_article; 4966 $stack_article[] = $thisarticle; 4967 } 4968 4969 /** 4970 * Advance to the next article in the current data stack. 4971 * 4972 * Populates $thisarticle global with the last article from the 4973 * stack stored in $stack_article. 4974 */ 4975 4976 function article_pop() 4977 { 4978 global $thisarticle, $stack_article; 4979 $thisarticle = array_pop($stack_article); 4980 } 4981 4982 /** 4983 * Gets a path relative to the site's root directory. 4984 * 4985 * @param string $path The filename to parse 4986 * @param string $pfx The root directory 4987 * @return string The absolute $path converted to relative 4988 * @package File 4989 */ 4990 4991 function relative_path($path, $pfx = null) 4992 { 4993 if ($pfx === null) { 4994 $pfx = dirname(txpath); 4995 } 4996 4997 return preg_replace('@^/'.preg_quote(ltrim($pfx, '/'), '@').'/?@', '', $path); 4998 } 4999 5000 /** 5001 * Gets a backtrace. 5002 * 5003 * @param int $num The limit 5004 * @param int $start The offset 5005 * @return array A backtrace 5006 * @package Debug 5007 */ 5008 5009 function get_caller($num = 1, $start = 2) 5010 { 5011 $out = array(); 5012 5013 if (!is_callable('debug_backtrace')) { 5014 return $out; 5015 } 5016 5017 $bt = debug_backtrace(); 5018 5019 for ($i = $start; $i < $num+$start; $i++) { 5020 if (!empty($bt[$i])) { 5021 $t = ''; 5022 5023 if (!empty($bt[$i]['file'])) { 5024 $t .= relative_path($bt[$i]['file']); 5025 } 5026 5027 if (!empty($bt[$i]['line'])) { 5028 $t .= ':'.$bt[$i]['line']; 5029 } 5030 5031 if ($t) { 5032 $t .= ' '; 5033 } 5034 5035 if (!empty($bt[$i]['class'])) { 5036 $t .= $bt[$i]['class']; 5037 } 5038 5039 if (!empty($bt[$i]['type'])) { 5040 $t .= $bt[$i]['type']; 5041 } 5042 5043 if (!empty($bt[$i]['function'])) { 5044 $t .= $bt[$i]['function']; 5045 $t .= '()'; 5046 } 5047 5048 $out[] = $t; 5049 } 5050 } 5051 5052 return $out; 5053 } 5054 5055 /** 5056 * Sets a locale. 5057 * 5058 * The function name is misleading but remains for legacy reasons. 5059 * 5060 * @param string $lang 5061 * @return string Current locale 5062 * @package L10n 5063 * @deprecated in 4.6.0 5064 * @see \Textpattern\L10n\Locale::setLocale() 5065 */ 5066 5067 function getlocale($lang) 5068 { 5069 global $locale; 5070 5071 Txp::get('\Textpattern\L10n\Locale')->setLocale(LC_TIME, array($lang, $locale)); 5072 5073 return Txp::get('\Textpattern\L10n\Locale')->getLocale(LC_TIME); 5074 } 5075 5076 /** 5077 * Fetch meta description from the given (or automatic) context. 5078 * 5079 * Category context may be refined by specifying the content type as well 5080 * after a dot. e.g. category.image to check image context category. 5081 * 5082 * @param string $type Flavour of meta content to fetch (section, category, article) 5083 */ 5084 5085 function getMetaDescription($type = null) 5086 { 5087 global $thisarticle, $thiscategory, $thissection, $c, $s, $context, $txp_sections; 5088 5089 $content = ''; 5090 5091 if ($type === null) { 5092 if ($thiscategory) { 5093 $content = $thiscategory['description']; 5094 } elseif ($thissection) { 5095 $content = $thissection['description']; 5096 } elseif ($thisarticle) { 5097 $content = $thisarticle['description']; 5098 } elseif ($c) { 5099 $content = safe_field("description", 'txp_category', "name = '".doSlash($c)."' AND type = '".doSlash($context)."'"); 5100 } elseif ($s) { 5101 $content = isset($txp_sections[$s]) ? $txp_sections[$s]['description'] : ''; 5102 } 5103 } else { 5104 if (strpos($type, 'category') === 0) { 5105 // Category context. 5106 if ($thiscategory) { 5107 $content = $thiscategory['description']; 5108 } else { 5109 $thisContext = $context; 5110 $catParts = do_list($type, '.'); 5111 5112 if (isset($catParts[1])) { 5113 $thisContext = $catParts[1]; 5114 } 5115 5116 $clause = " AND type = '".$thisContext."'"; 5117 $content = safe_field("description", 'txp_category', "name = '".doSlash($c)."'".$clause); 5118 } 5119 } elseif ($type === 'section') { 5120 $theSection = ($thissection) ? $thissection['name'] : $s; 5121 $content = isset($txp_sections[$theSection]) ? $txp_sections[$theSection]['description'] : ''; 5122 } elseif ($type === 'article') { 5123 assert_article(); 5124 $content = ($thisarticle? $thisarticle['description'] : ''); 5125 } 5126 } 5127 5128 return $content; 5129 } 5130 5131 /** 5132 * Get some URL data. 5133 * @param mixed $context The data to retrieve 5134 * @param array $internals Data restrictions 5135 * @return array The retrieved data 5136 */ 5137 5138 function get_context($context = true, $internals = array('id', 's', 'c', 'context', 'q', 'm', 'pg', 'p', 'month', 'author', 'f')) 5139 { 5140 global $pretext, $txp_context; 5141 5142 if (!isset($context)) { 5143 return empty($txp_context) ? array() : $txp_context; 5144 } elseif (empty($context)) { 5145 return array(); 5146 } elseif (!is_array($context)) { 5147 $context = array_fill_keys($context === true ? $internals : do_list_unique($context), null); 5148 } 5149 5150 $out = array(); 5151 5152 foreach ($context as $q => $v) { 5153 if (isset($pretext[$q]) && in_array($q, $internals)) { 5154 $out[$q] = $q === 'author' ? $pretext['realname'] : $pretext[$q]; 5155 } elseif (isset($v)) { 5156 $out[$q] = $v; 5157 } else { 5158 $out[$q] = gps($q, null); 5159 } 5160 } 5161 5162 return $out; 5163 } 5164 5165 /** 5166 * Assert article context error. 5167 */ 5168 5169 function assert_article() 5170 { 5171 global $thisarticle; 5172 5173 if (empty($thisarticle)) { 5174 trigger_error(gTxt('error_article_context')); 5175 5176 return false; 5177 } 5178 5179 return true; 5180 } 5181 5182 /** 5183 * Assert comment context error. 5184 */ 5185 5186 function assert_comment() 5187 { 5188 global $thiscomment; 5189 5190 if (empty($thiscomment)) { 5191 trigger_error(gTxt('error_comment_context')); 5192 } 5193 } 5194 5195 /** 5196 * Assert file context error. 5197 */ 5198 5199 function assert_file() 5200 { 5201 global $thisfile; 5202 5203 if (empty($thisfile)) { 5204 trigger_error(gTxt('error_file_context')); 5205 } 5206 } 5207 5208 /** 5209 * Assert image context error. 5210 */ 5211 5212 function assert_image() 5213 { 5214 global $thisimage; 5215 5216 if (empty($thisimage)) { 5217 trigger_error(gTxt('error_image_context')); 5218 } 5219 } 5220 5221 /** 5222 * Assert link context error. 5223 */ 5224 5225 function assert_link() 5226 { 5227 global $thislink; 5228 5229 if (empty($thislink)) { 5230 trigger_error(gTxt('error_link_context')); 5231 } 5232 } 5233 5234 /** 5235 * Assert section context error. 5236 */ 5237 5238 function assert_section() 5239 { 5240 global $thissection; 5241 5242 if (empty($thissection)) { 5243 trigger_error(gTxt('error_section_context')); 5244 } 5245 } 5246 5247 /** 5248 * Assert category context error. 5249 */ 5250 5251 function assert_category() 5252 { 5253 global $thiscategory; 5254 5255 if (empty($thiscategory)) { 5256 trigger_error(gTxt('error_category_context')); 5257 } 5258 } 5259 5260 /** 5261 * Validate a variable as an integer. 5262 * 5263 * @param mixed $myvar The variable 5264 * @return int|bool The variable or FALSE on error 5265 */ 5266 5267 function assert_int($myvar) 5268 { 5269 if (is_numeric($myvar) && $myvar == intval($myvar)) { 5270 return (int) $myvar; 5271 } 5272 5273 trigger_error(gTxt('assert_int_value', array('{name}' => (string) $myvar)), E_USER_ERROR); 5274 5275 return false; 5276 } 5277 5278 /** 5279 * Validate a variable as a string. 5280 * 5281 * @param mixed $myvar The variable 5282 * @return string|bool The variable or FALSE on error 5283 */ 5284 5285 function assert_string($myvar) 5286 { 5287 if (is_string($myvar)) { 5288 return $myvar; 5289 } 5290 5291 trigger_error(gTxt('assert_string_value', array('{name}' => (string) $myvar)), E_USER_ERROR); 5292 5293 return false; 5294 } 5295 5296 /** 5297 * Validate a variable as an array. 5298 * 5299 * @param mixed $myvar The variable 5300 * @return array|bool The variable or FALSE on error 5301 */ 5302 5303 function assert_array($myvar) 5304 { 5305 if (is_array($myvar)) { 5306 return $myvar; 5307 } 5308 5309 trigger_error(gTxt('assert_array_value', array('{name}' => (string) $myvar)), E_USER_ERROR); 5310 5311 return false; 5312 } 5313 5314 /** 5315 * Converts relative links in HTML markup to absolute. 5316 * 5317 * @param string $html The HTML to check 5318 * @param string $permalink Optional URL part appended to the links 5319 * @return string HTML 5320 * @package URL 5321 */ 5322 5323 function replace_relative_urls($html, $permalink = '') 5324 { 5325 global $siteurl; 5326 5327 // URLs like "/foo/bar" - relative to the domain. 5328 if (serverSet('HTTP_HOST')) { 5329 $html = preg_replace('@(<a[^>]+href=")/(?!/)@', '$1'.PROTOCOL.serverSet('HTTP_HOST').'/', $html); 5330 $html = preg_replace('@(<img[^>]+src=")/(?!/)@', '$1'.PROTOCOL.serverSet('HTTP_HOST').'/', $html); 5331 } 5332 5333 // "foo/bar" - relative to the textpattern root, 5334 // leave "http:", "mailto:" et al. as absolute URLs. 5335 $html = preg_replace('@(<a[^>]+href=")(?!\w+:|//)@', '$1'.PROTOCOL.$siteurl.'/$2', $html); 5336 $html = preg_replace('@(<img[^>]+src=")(?!\w+:|//)@', '$1'.PROTOCOL.$siteurl.'/$2', $html); 5337 5338 if ($permalink) { 5339 $html = preg_replace("/href=\\\"#(.*)\"/", "href=\"".$permalink."#\\1\"", $html); 5340 } 5341 5342 return ($html); 5343 } 5344 5345 /** 5346 * Used for clean URL test. 5347 * 5348 * @param array $pretext 5349 * @access private 5350 */ 5351 5352 function show_clean_test($pretext) 5353 { 5354 ob_clean(); 5355 if (is_array($pretext) && isset($pretext['req'])) { 5356 echo md5($pretext['req']).n; 5357 } 5358 5359 if (serverSet('SERVER_ADDR') === serverSet('REMOTE_ADDR')) { 5360 var_export($pretext); 5361 } 5362 } 5363 5364 /** 5365 * Calculates paging. 5366 * 5367 * Takes a total number of items, a per page limit and the current page number, 5368 * and in return returns the page number, an offset and a number of pages. 5369 * 5370 * @param int $total The number of items in total 5371 * @param int $limit The number of items per page 5372 * @param int $page The page number 5373 * @return array Array of page, offset and number of pages. 5374 * @example 5375 * list($page, $offset, $num_pages) = pager(150, 10, 1); 5376 * echo "Page {$page} of {$num_pages}. Offset is {$offset}."; 5377 */ 5378 5379 function pager($total, $limit, $page) 5380 { 5381 $total = (int) $total; 5382 $limit = (int) $limit; 5383 $page = (int) $page; 5384 5385 $num_pages = ceil($total / $limit); 5386 5387 $page = min(max($page, 1), $num_pages); 5388 5389 $offset = max(($page - 1) * $limit, 0); 5390 5391 return array($page, $offset, $num_pages); 5392 } 5393 5394 /** 5395 * Word-wrap a string using a zero width space. 5396 * 5397 * @param string $text The input string 5398 * @param int $width Target line length 5399 * @param string $break Is not used 5400 * @return string 5401 */ 5402 5403 function soft_wrap($text, $width, $break = '​') 5404 { 5405 $wbr = chr(226).chr(128).chr(139); 5406 $words = explode(' ', $text); 5407 5408 foreach ($words as $wordnr => $word) { 5409 $word = preg_replace('|([,./\\>?!:;@-]+)(?=.)|', '$1 ', $word); 5410 $parts = explode(' ', $word); 5411 5412 foreach ($parts as $partnr => $part) { 5413 $len = strlen(utf8_decode($part)); 5414 5415 if (!$len) { 5416 continue; 5417 } 5418 5419 $parts[$partnr] = preg_replace('/(.{'.ceil($len/ceil($len/$width)).'})(?=.)/u', '$1'.$wbr, $part); 5420 } 5421 5422 $words[$wordnr] = join($wbr, $parts); 5423 } 5424 5425 return join(' ', $words); 5426 } 5427 5428 /** 5429 * Removes prefix from a string. 5430 * 5431 * @param string $str The string 5432 * @param string $pfx The prefix 5433 * @return string 5434 */ 5435 5436 function strip_prefix($str, $pfx) 5437 { 5438 return preg_replace('/^'.preg_quote($pfx, '/').'/', '', $str); 5439 } 5440 5441 /** 5442 * Sends an XML envelope. 5443 * 5444 * Wraps an array of name => value tuples into an XML envelope, supports one 5445 * level of nested arrays at most. 5446 * 5447 * @param array $response 5448 * @return string XML envelope 5449 * @package XML 5450 */ 5451 5452 function send_xml_response($response = array()) 5453 { 5454 static $headers_sent = false; 5455 5456 if (!$headers_sent) { 5457 ob_clean(); 5458 header('Content-Type: text/xml; charset=utf-8'); 5459 $out[] = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>'; 5460 $headers_sent = true; 5461 } 5462 5463 $default_response = array('http-status' => '200 OK'); 5464 5465 // Backfill default response properties. 5466 $response = $response + $default_response; 5467 5468 txp_status_header($response['http-status']); 5469 $out[] = '<textpattern>'; 5470 5471 foreach ($response as $element => $value) { 5472 if (is_array($value)) { 5473 $out[] = t."<$element>".n; 5474 5475 foreach ($value as $e => $v) { 5476 // Character escaping in values; 5477 // @see https://www.w3.org/TR/REC-xml/#sec-references. 5478 $v = str_replace(array("\t", "\n", "\r"), array("	", "
", "
"), htmlentities($v, ENT_QUOTES, 'UTF-8')); 5479 $out[] = t.t."<$e value='$v' />".n; 5480 } 5481 5482 $out[] = t."</$element>".n; 5483 } else { 5484 $value = str_replace(array("\t", "\n", "\r"), array("	", "
", "
"), htmlentities($value, ENT_QUOTES, 'UTF-8')); 5485 $out[] = t."<$element value='$value' />".n; 5486 } 5487 } 5488 5489 $out[] = '</textpattern>'; 5490 echo join(n, $out); 5491 } 5492 5493 /** 5494 * Sends a text/javascript response. 5495 * 5496 * @param string $out The JavaScript 5497 * @since 4.4.0 5498 * @package Ajax 5499 */ 5500 5501 function send_script_response($out = '') 5502 { 5503 static $headers_sent = false; 5504 5505 if (!$headers_sent) { 5506 ob_clean(); 5507 header('Content-Type: text/javascript; charset=utf-8'); 5508 txp_status_header('200 OK'); 5509 $headers_sent = true; 5510 } 5511 5512 echo ";\n".$out.";\n"; 5513 } 5514 5515 /** 5516 * Sends an application/json response. 5517 * 5518 * If the provided $out is not a string, its encoded as JSON. Any string is 5519 * treated as it were valid JSON. 5520 * 5521 * @param mixed $out The JSON 5522 * @since 4.6.0 5523 * @package Ajax 5524 */ 5525 5526 function send_json_response($out = '') 5527 { 5528 static $headers_sent = false; 5529 5530 if (!$headers_sent) { 5531 ob_clean(); 5532 header('Content-Type: application/json; charset=utf-8'); 5533 txp_status_header('200 OK'); 5534 $headers_sent = true; 5535 } 5536 5537 if (!is_string($out)) { 5538 $out = json_encode($out, TEXTPATTERN_JSON); 5539 } 5540 5541 echo $out; 5542 } 5543 5544 /** 5545 * Performs regular housekeeping. 5546 * 5547 * @access private 5548 */ 5549 5550 function janitor() 5551 { 5552 global $prefs, $auto_dst, $timezone_key, $is_dst; 5553 5554 // Update DST setting. 5555 if ($auto_dst && $timezone_key) { 5556 $is_dst = Txp::get('\Textpattern\Date\Timezone')->isDst(null, $timezone_key); 5557 5558 if ($is_dst != $prefs['is_dst']) { 5559 $prefs['is_dst'] = $is_dst; 5560 set_pref('is_dst', $is_dst, 'publish', PREF_HIDDEN); 5561 } 5562 } 5563 } 5564 5565 /** 5566 * Protection from those who'd bomb the site by GET. 5567 * 5568 * Origin of the infamous 'Nice try' message and an even more useful '503' 5569 * HTTP status. 5570 */ 5571 5572 function bombShelter() 5573 { 5574 global $prefs; 5575 $in = serverSet('REQUEST_URI'); 5576 5577 if (!empty($prefs['max_url_len']) and strlen($in) > $prefs['max_url_len']) { 5578 txp_status_header('503 Service Unavailable'); 5579 exit('Nice try.'); 5580 } 5581 } 5582 5583 /** 5584 * Test whether the client accepts a certain response format. 5585 * 5586 * Discards formats with a quality factor below 0.1 5587 * 5588 * @param string $format One of 'html', 'txt', 'js', 'css', 'json', 'xml', 'rdf', 'atom', 'rss' 5589 * @return boolean $format TRUE if accepted 5590 * @since 4.5.0 5591 * @package Network 5592 */ 5593 5594 function http_accept_format($format) 5595 { 5596 static $formats = array( 5597 'html' => array('text/html', 'application/xhtml+xml', '*/*'), 5598 'txt' => array('text/plain', '*/*'), 5599 'js' => array('application/javascript', 'application/x-javascript', 'text/javascript', 'application/ecmascript', 'application/x-ecmascript', '*/*'), 5600 'css' => array('text/css', '*/*'), 5601 'json' => array('application/json', 'application/x-json', '*/*'), 5602 'xml' => array('text/xml', 'application/xml', 'application/x-xml', '*/*'), 5603 'rdf' => array('application/rdf+xml', '*/*'), 5604 'atom' => array('application/atom+xml', '*/*'), 5605 'rss' => array('application/rss+xml', '*/*'), 5606 ); 5607 static $accepts = array(); 5608 static $q = array(); 5609 5610 if (empty($accepts)) { 5611 // Build cache of accepted formats. 5612 $accepts = preg_split('/\s*,\s*/', serverSet('HTTP_ACCEPT'), null, PREG_SPLIT_NO_EMPTY); 5613 5614 foreach ($accepts as $i => &$a) { 5615 // Sniff out quality factors if present. 5616 if (preg_match('/(.*)\s*;\s*q=([.0-9]*)/', $a, $m)) { 5617 $a = $m[1]; 5618 $q[$a] = floatval($m[2]); 5619 } else { 5620 $q[$a] = 1.0; 5621 } 5622 5623 // Discard formats with quality factors below an arbitrary threshold 5624 // as jQuery adds a wildcard '*/*; q=0.01' to the 'Accepts' header 5625 // for XHR requests. 5626 if ($q[$a] < 0.1) { 5627 unset($q[$a]); 5628 unset($accepts[$i]); 5629 } 5630 } 5631 } 5632 5633 return isset($formats[$format]) && count(array_intersect($formats[$format], $accepts)) > 0; 5634 } 5635 5636 /** 5637 * Return a list of status codes and their associated names. 5638 * 5639 * The list can be extended with a 'status.types > types' callback event. 5640 * Callback functions get passed three arguments: '$event', '$step' and 5641 * '$status_list'. The third parameter contains a reference to an array of 5642 * 'status_code => label' pairs. 5643 * 5644 * @param bool Return the list with L10n labels (for UI purposes) or raw values (for comparisons) 5645 * @param array List of status keys (numbers) to exclude 5646 * @return array A status array 5647 * @since 4.6.0 5648 */ 5649 5650 function status_list($labels = true, $exclude = array()) 5651 { 5652 $status_list = array( 5653 STATUS_DRAFT => 'draft', 5654 STATUS_HIDDEN => 'hidden', 5655 STATUS_PENDING => 'pending', 5656 STATUS_LIVE => 'live', 5657 STATUS_STICKY => 'sticky', 5658 ); 5659 5660 if (!is_array($exclude)) { 5661 $exclude = array(); 5662 } 5663 5664 foreach ($exclude as $remove) { 5665 unset($status_list[(int) $remove]); 5666 } 5667 5668 callback_event_ref('status.types', 'types', 0, $status_list); 5669 5670 if ($labels) { 5671 $status_list = array_map('gTxt', $status_list); 5672 } 5673 5674 return $status_list; 5675 } 5676 5677 /** 5678 * Translates article status names into numerical status codes. 5679 * 5680 * @param string $name Status name 5681 * @param int $default Status code to return if $name is not a defined status name 5682 * @return int Matching numerical status 5683 */ 5684 5685 function getStatusNum($name, $default = STATUS_LIVE) 5686 { 5687 $statuses = status_list(false); 5688 $status = strtolower($name); 5689 $num = array_search($status, $statuses); 5690 5691 if ($num === false) { 5692 $num = $default; 5693 } 5694 5695 return (int) $num; 5696 } 5697 5698 /** 5699 * Gets the maximum allowed file upload size. 5700 * 5701 * Computes the maximum acceptable file size to the application if the 5702 * user-selected value is larger than the maximum allowed by the current PHP 5703 * configuration. 5704 * 5705 * @param int $user_max Desired upload size supplied by the administrator 5706 * @return int Actual value; the lower of user-supplied value or system-defined value 5707 */ 5708 5709 function real_max_upload_size($user_max, $php = true) 5710 { 5711 // The minimum of the candidates, is the real max. possible size 5712 $candidates = $php ? array($user_max, 5713 ini_get('post_max_size'), 5714 ini_get('upload_max_filesize') 5715 ) : array($user_max); 5716 $real_max = null; 5717 5718 foreach ($candidates as $item) { 5719 $val = floatval($item); 5720 $modifier = strtolower(substr(trim($item), -1)); 5721 5722 switch ($modifier) { 5723 // The 'G' modifier is available since PHP 5.1.0 5724 case 'g': 5725 $val *= 1024; 5726 // no break 5727 case 'm': 5728 $val *= 1024; 5729 // no break 5730 case 'k': 5731 $val *= 1024; 5732 } 5733 5734 if ($val >= 1) { 5735 if (is_null($real_max) || $val < $real_max) { 5736 $real_max = floor($val); 5737 } 5738 } 5739 } 5740 5741 // 2^53 - 1 is max safe JavaScript integer, let 8192Tb 5742 return number_format(min($real_max, pow(2, 53) - 1), 0, '.', ''); 5743 } 5744 5745 // ------------------------------------------------------------- 5746 5747 function txp_match($atts, $what) 5748 { 5749 static $dlmPool = array('/', '@', '#', '~', '`', '|', '!', '%'); 5750 5751 extract($atts + array( 5752 'value' => null, 5753 'match' => 'exact', 5754 'separator' => '', 5755 )); 5756 5757 5758 if ($value !== null) { 5759 switch ($match) { 5760 case '': 5761 case 'exact': 5762 $cond = (is_array($what) ? implode('', $what) == $value : $what == $value); 5763 break; 5764 case 'any': 5765 $values = do_list_unique($value); 5766 $cond = false; 5767 $cf_contents = $separator && !is_array($what) ? do_list_unique($what, $separator) : $what; 5768 5769 foreach ($values as $term) { 5770 if (is_array($cf_contents) ? in_array($term, $cf_contents) : strpos($cf_contents, $term) !== false) { 5771 $cond = true; 5772 break; 5773 } 5774 } 5775 break; 5776 case 'all': 5777 $values = do_list_unique($value); 5778 $cond = true; 5779 $cf_contents = $separator && !is_array($what) ? do_list_unique($what, $separator) : $what; 5780 5781 foreach ($values as $term) { 5782 if (is_array($cf_contents) ? !in_array($term, $cf_contents) : strpos($cf_contents, $term) === false) { 5783 $cond = false; 5784 break; 5785 } 5786 } 5787 break; 5788 case 'pattern': 5789 // Cannot guarantee that a fixed delimiter won't break preg_match 5790 // (and preg_quote doesn't help) so dynamically assign the delimiter 5791 // based on the first entry in $dlmPool that is NOT in the value 5792 // attribute. This minimises (does not eliminate) the possibility 5793 // of a TXP-initiated preg_match error, while still preserving 5794 // errors outside TXP's control (e.g. mangled user-submitted 5795 // PCRE pattern). 5796 if ($separator === true) { 5797 $dlm = $value; 5798 } elseif ($separator && in_array($separator, $dlmPool)) { 5799 $dlm = strpos($value, $separator) === 0 ? $value : $separator.$value.$separator; 5800 } else { 5801 $dlm = array_diff($dlmPool, preg_split('//', $value)); 5802 $dlm = reset($dlm); 5803 $dlm = $dlm.$value.$dlm; 5804 } 5805 5806 $cond = preg_match($dlm, is_array($what) ? implode('', $what) : $what); 5807 break; 5808 default: 5809 trigger_error(gTxt('invalid_attribute_value', array('{name}' => 'match')), E_USER_NOTICE); 5810 $cond = false; 5811 } 5812 } else { 5813 $cond = ($what !== null); 5814 } 5815 5816 return !empty($cond); 5817 } 5818 5819 /*** Polyfills ***/ 5820 5821 if (!function_exists('array_column')) { 5822 include txpath.'/lib/array_column.php'; 5823 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title