Textpattern | PHP Cross Reference | Content Management Systems |
Description: Tools for page routing and handling article data.
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 * Tools for page routing and handling article data. 26 * 27 * @since 4.5.0 28 * @package Routing 29 */ 30 31 /** 32 * Build a query qualifier to remove non-frontpage articles from the result set. 33 * 34 * @return string An SQL qualifier for a query's 'WHERE' part 35 */ 36 37 function filterFrontPage($field = 'Section', $column = 'on_frontpage') 38 { 39 static $filterFrontPage = array(); 40 global $txp_sections; 41 42 is_array($column) or $column = do_list_unique($column); 43 $key = $field.'.'.implode('.', $column); 44 45 if (isset($filterFrontPage[$key])) { 46 return $filterFrontPage[$key]; 47 } 48 49 $filterFrontPage[$key] = false; 50 $field = doSlash($field); 51 $rs = array(); 52 53 foreach ($column as $col) { 54 $rs += array_filter(array_column($txp_sections, $col, 'name')); 55 } 56 57 if ($rs) { 58 $filterFrontPage[$key] = " AND $field IN(".join(',', quote_list(array_keys($rs))).")"; 59 } 60 61 return $filterFrontPage[$key]; 62 } 63 64 /** 65 * Populates the current article data. 66 * 67 * Fills members of $thisarticle global from a database row. 68 * 69 * Keeps all article tag-related values in one place, in order to do easy 70 * bugfixing and ease the addition of new article tags. 71 * 72 * @param array $rs An article as an associative array 73 * @example 74 * if ($rs = safe_rows_start("*, 75 * UNIX_TIMESTAMP(Posted) AS uPosted, 76 * UNIX_TIMESTAMP(Expires) AS uExpires, 77 * UNIX_TIMESTAMP(LastMod) AS uLastMod", 78 * 'textpattern', 79 * "1 = 1" 80 * )) 81 * { 82 * global $thisarticle; 83 * while ($row = nextRow($rs)) 84 * { 85 * populateArticleData($row); 86 * echo $thisarticle['title']; 87 * } 88 * } 89 */ 90 91 function populateArticleData($rs) 92 { 93 global $production_status, $thisarticle, $trace; 94 95 foreach (article_column_map() as $key => $column) { 96 $thisarticle[$key] = isset($rs[$column]) ? $rs[$column] : null; 97 } 98 99 if ($production_status === 'debug') { 100 $trace->log("[Article: '{$thisarticle['thisid']}']"); 101 } 102 } 103 104 /** 105 * Formats article info and populates the current article data. 106 * 107 * Fills members of $thisarticle global from a database row. 108 * 109 * Basically just converts an article's date values to UNIX timestamps. 110 * Convenience for those who prefer doing conversion in application end instead 111 * of in the SQL statement. 112 * 113 * @param array $rs An article as an associative array 114 * @example 115 * article_format_info( 116 * safe_row('*', 'textpattern', 'Status = 4 LIMIT 1') 117 * ) 118 */ 119 120 function article_format_info($rs) 121 { 122 $rs['uPosted'] = isset($rs['Posted']) && ($unix_ts = strtotime($rs['Posted'])) !== false ? $unix_ts : null; 123 $rs['uLastMod'] = isset($rs['LastMod']) && ($unix_ts = strtotime($rs['LastMod'])) !== false ? $unix_ts : null; 124 $rs['uExpires'] = isset($rs['Expires']) && ($unix_ts = strtotime($rs['Expires'])) !== false ? $unix_ts : null; 125 populateArticleData($rs); 126 } 127 128 /** 129 * Maps 'textpattern' table's columns to article data values. 130 * 131 * This function returns an array of 'data-value' => 'column' pairs. 132 * 133 * @return array 134 */ 135 136 function article_column_map() 137 { 138 static $column_map = array(); 139 140 if (empty($column_map)) { 141 $column_map = array( 142 'thisid' => 'ID', 143 'posted' => 'uPosted', // Calculated value! 144 'expires' => 'uExpires', // Calculated value! 145 'modified' => 'uLastMod', // Calculated value! 146 'annotate' => 'Annotate', 147 'comments_invite' => 'AnnotateInvite', 148 'authorid' => 'AuthorID', 149 'title' => 'Title', 150 'url_title' => 'url_title', 151 'description' => 'description', 152 'category1' => 'Category1', 153 'category2' => 'Category2', 154 'section' => 'Section', 155 'keywords' => 'Keywords', 156 'article_image' => 'Image', 157 'comments_count' => 'comments_count', 158 'body' => 'Body_html', 159 'excerpt' => 'Excerpt_html', 160 'override_form' => 'override_form', 161 'status' => 'Status', 162 ); 163 164 foreach (getCustomFields() as $i => $name) { 165 isset($column_map[$name]) or $column_map[$name] = 'custom_'.$i; 166 } 167 } 168 169 return $column_map; 170 } 171 172 /** 173 * Find an adjacent article relative to a provided threshold level. 174 * 175 * @param scalar $threshold The value to compare against 176 * @param string $s Optional section restriction 177 * @param string $type Lesser or greater neighbour? Either '<' (previous) or '>' (next) 178 * @param array $atts Attribute of article at threshold 179 * @param string $threshold_type 'cooked': Use $threshold as SQL clause; 'raw': Use $threshold as an escapable scalar 180 * @return array|bool An array populated with article data, or 'false' in case of no matches 181 */ 182 183 function getNeighbour($threshold, $s, $type, $atts = array(), $threshold_type = 'raw') 184 { 185 static $cache = array(); 186 static $types = array( 187 '>' => array( 188 'desc' => '>', 189 'asc' => '<', 190 ), 191 '<' => array( 192 'desc' => '<', 193 'asc' => '>', 194 ), 195 ); 196 197 $key = md5($threshold.$s.$type.join(n, $atts)); 198 199 if (isset($cache[$key])) { 200 return $cache[$key]; 201 } 202 203 $thisid = isset($atts['thisid']) ? intval($atts['thisid']) : 0; 204 $sortdir = isset($atts['sortdir']) ? strtolower($atts['sortdir']) : 'asc'; 205 $sortby = isset($atts['sortby']) ? $atts['sortby'] : 'Posted'; 206 207 // Invert $type for ascending sortdir. 208 $type = ($type == '>') ? $types['>'][$sortdir] : $types['<'][$sortdir]; 209 210 // Escape threshold and treat it as a string unless explicitly told otherwise. 211 if ($threshold_type != 'cooked') { 212 $threshold = "'".doSlash($threshold)."'"; 213 } 214 215 $where = isset($atts['*']) ? $atts['*'] : '1'; 216 $q = array( 217 "SELECT *, UNIX_TIMESTAMP(Posted) AS uPosted, UNIX_TIMESTAMP(Expires) AS uExpires, UNIX_TIMESTAMP(LastMod) AS uLastMod FROM ".safe_pfx('textpattern')." 218 WHERE ($sortby $type $threshold OR ".($thisid ? "$sortby = $threshold AND ID $type $thisid" : "0").")", 219 "AND $where", 220 "ORDER BY $sortby", 221 ($type == '<') ? 'DESC' : 'ASC', 222 ', ID '.($type == '<' ? 'DESC' : 'ASC'), 223 "LIMIT 1", 224 ); 225 226 $cache[$key] = getRow(join(' ', $q)); 227 228 return (is_array($cache[$key])) ? $cache[$key] : false; 229 } 230 231 /** 232 * Find next and previous articles relative to a provided threshold level. 233 * 234 * @param int $id The "pivot" article's id; use zero (0) to indicate $thisarticle 235 * @param scalar $threshold The value to compare against if $id != 0 236 * @param string $s Optional section restriction if $id != 0 237 * @return array An array populated with article data 238 */ 239 240 function getNextPrev($id = 0, $threshold = null, $s = '') 241 { 242 $threshold_type = 'cooked'; 243 $atts = filterAtts() or $atts = filterAtts(array()); 244 245 if ($id !== 0) { 246 // Pivot is specific article by ID: In lack of further information, 247 // revert to default sort order 'Posted desc'. 248 $atts += array( 249 'sortby' => 'Posted', 250 'sortdir' => 'DESC', 251 'thisid' => $id, 252 ); 253 $threshold_type = 'raw'; 254 } else { 255 // Pivot is $thisarticle: Use article attributes to find its neighbours. 256 assert_article(); 257 global $thisarticle; 258 if (!is_array($thisarticle)) { 259 return array(); 260 } 261 262 $s = $thisarticle['section']; 263 $atts += array( 264 'thisid' => $thisarticle['thisid'], 265 'sort' => 'Posted DESC', 266 ); 267 $atts['sort'] = trim($atts['sort']); 268 269 if (empty($atts['sort'])) { 270 $atts['sortby'] = !empty($atts['id']) ? "FIELD(ID, ".$atts['id'].")" : 'Posted'; 271 $atts['sortdir'] = !empty($atts['id']) ? 'ASC' : 'DESC'; 272 } elseif (preg_match('/^([$\w\x{0080}-\x{FFFF}]+|`[\x{0001}-\x{FFFF}]+`)(?i)(\s+asc|\s+desc)?$/u', $atts['sort'], $m)) { 273 // The clause's first verb is a MySQL column identifier. 274 $atts['sortby'] = trim($m[1], ' `'); 275 $atts['sortdir'] = (isset($m[2]) ? trim($m[2]) : 'ASC'); 276 } elseif (preg_match('/^((?>[^(),]|(\((?:[^()]|(?2))*\)))+)(\basc|\bdesc)?$/Ui', $atts['sort'], $m)) { 277 // More complex unique clause. 278 $atts['sortby'] = trim($m[1]); 279 $atts['sortdir'] = (isset($m[3]) ? $m[3] : 'ASC'); 280 } else { 281 $atts['sortby'] = 'Posted'; 282 $atts['sortdir'] = 'DESC'; 283 } 284 285 // Attributes with special treatment. 286 switch ($atts['sortby']) { 287 case 'Posted': 288 $threshold = "FROM_UNIXTIME(".doSlash($thisarticle['posted']).")"; 289 break; 290 case 'Expires': 291 $threshold = "FROM_UNIXTIME(".doSlash($thisarticle['expires']).")"; 292 break; 293 case 'LastMod': 294 $threshold = "FROM_UNIXTIME(".doSlash($thisarticle['modified']).")"; 295 break; 296 default: 297 // Retrieve current threshold value per sort column from $thisarticle. 298 $threshold_type = 'raw'; 299 $acm = array_flip(article_column_map()); 300 301 if (isset($acm[$atts['sortby']])) { 302 $key = $acm[$atts['sortby']]; 303 $threshold = $thisarticle[$key]; 304 } else { 305 $threshold = safe_field($atts['sortby'], 'textpattern', 'ID='.$atts['thisid']); 306 } 307 } 308 } 309 310 $out['next'] = getNeighbour($threshold, $s, '>', $atts, $threshold_type); 311 $out['prev'] = getNeighbour($threshold, $s, '<', $atts, $threshold_type); 312 313 return $out; 314 } 315 316 /** 317 * Gets the site last modification date. 318 * 319 * @return string 320 * @package Pref 321 */ 322 323 function lastMod() 324 { 325 $last = safe_field("UNIX_TIMESTAMP(val)", 'txp_prefs', "name = 'lastmod'"); 326 327 return gmdate("D, d M Y H:i:s \G\M\T", $last); 328 } 329 330 /** 331 * Parse a string and replace any Textpattern tags with their actual value. 332 * 333 * @param string $thing The raw string 334 * @param null|bool $condition Process true/false part 335 * @return string The parsed string 336 * @package TagParser 337 */ 338 339 function parse($thing, $condition = true, $in_tag = true) 340 { 341 global $pretext, $production_status, $trace, $txp_parsed, $txp_else, $txp_atts, $txp_tag; 342 static $short_tags = null; 343 344 if ($in_tag) { 345 empty($txp_atts['not']) or $condition = empty($condition); 346 } 347 348 $txp_tag = !empty($condition); 349 350 if ($production_status === 'debug') { 351 $trace->log('['.($condition ? 'true' : 'false').']'); 352 } 353 354 if (!isset($short_tags)) { 355 $short_tags = get_pref('enable_short_tags', false); 356 } 357 358 if (!$short_tags && false === strpos($thing, '<txp:') || 359 $short_tags && !preg_match('@<(?:'.TXP_PATTERN.'):@', $thing)) { 360 return $condition ? ($thing === null ? '1' : $thing) : ''; 361 } 362 363 $hash = sha1($thing); 364 365 if (!isset($txp_parsed[$hash])) { 366 txp_tokenize($thing, $hash); 367 } 368 369 $tag = $txp_parsed[$hash]; 370 371 if (empty($tag)) { 372 return $condition ? $thing : ''; 373 } 374 375 list($first, $last) = $txp_else[$hash]; 376 377 if ($condition) { 378 $last = $first - 2; 379 $first = 1; 380 } elseif ($first <= $last) { 381 $first += 2; 382 } else { 383 return ''; 384 } 385 386 $isempty = false; 387 $dotest = !empty($txp_atts['evaluate']) && $in_tag; 388 $evaluate = !$dotest ? null : 389 ($txp_atts['evaluate'] === true ? true : do_list($txp_atts['evaluate'])); 390 391 if (isset($txp_else[$hash]['test']) && (!$evaluate || $evaluate === true)) { 392 $evaluate = $txp_else[$hash]['test']; 393 } 394 395 if ($evaluate) { 396 $test = is_array($evaluate) ? array_fill_keys($evaluate, array()) : false; 397 $isempty = $last >= $first || $test !== false; 398 } 399 400 if (empty($test)) { 401 for ($out = $tag[$first - 1]; $first <= $last; $first++) { 402 $txp_tag = $tag[$first]; 403 $nextag = processTags($txp_tag[1], $txp_tag[2], $txp_tag[3]); 404 $out .= $nextag.$tag[++$first]; 405 $isempty = $isempty && trim($nextag) === ''; 406 } 407 } else { 408 if ($pre = !isset($test[0])) { 409 $test[0] = array(); 410 } 411 412 $out = array($first-1 => $tag[$first-1]); 413 414 for ($n = $first; $n <= $last; $n++) { 415 $txp_tag = $tag[$n]; 416 $out[$n] = null; 417 418 if (isset($test[($n+1)/2])) { 419 $test[($n+1)/2][] = $n; 420 } elseif (isset($test[$txp_tag[1]])) { 421 $test[$txp_tag[1]][] = $n; 422 } else { 423 $test[0][] = $n; 424 } 425 426 $out[$n] = $tag[++$n]; 427 } 428 429 foreach ($test as $k => $t) { 430 if (!$k && $pre && $dotest && $isempty == empty($txp_atts['not'])) { 431 $out = false; 432 break; 433 } 434 435 foreach ($t as $n) { 436 $txp_tag = $tag[$n]; 437 $nextag = processTags($txp_tag[1], $txp_tag[2], $txp_tag[3]); 438 $out[$n] = $nextag; 439 $k and ($isempty = $isempty && trim($nextag) === ''); 440 } 441 } 442 443 if (is_array($out)) { 444 $out = implode('', $out); 445 } 446 } 447 448 if ($dotest && $isempty == empty($txp_atts['not'])) { 449 $out = false; 450 $condition = false; 451 } 452 453 $txp_tag = !empty($condition); 454 455 return $out; 456 } 457 458 /** 459 * Guesstimate whether a given function name may be a valid tag handler. 460 * 461 * @param string $tag function name 462 * @return bool FALSE if the function name is not a valid tag handler 463 * @package TagParser 464 */ 465 466 function maybe_tag($tag) 467 { 468 static $tags = null; 469 470 if ($tags === null) { 471 global $plugins; 472 473 if (empty($plugins)) { 474 $tags = false; 475 } else { 476 $match = array(); 477 478 foreach ($plugins as $p) { 479 $pfx = strpos($p, '_') === false ? $p : strtok($p, '_').'_'; 480 $match[$pfx] = preg_quote($pfx, '/'); 481 } 482 483 $match = '/^('.implode('|', $match).')/i'; 484 $tags = get_defined_functions(); 485 $tags = array_filter($tags['user'], function ($f) use ($match) { 486 return preg_match($match, $f); 487 }); 488 $tags = array_flip($tags); 489 } 490 } 491 492 return isset($tags[$tag]); 493 } 494 495 /** 496 * Parse a tag for attributes and hand over to the tag handler function. 497 * 498 * @param string $tag The tag name 499 * @param string $atts The attribute string 500 * @param string|null $thing The tag's content in case of container tags 501 * @return string Parsed tag result 502 * @package TagParser 503 */ 504 505 function processTags($tag, $atts = '', $thing = null) 506 { 507 global $pretext, $production_status, $txp_current_tag, $txp_atts, $txp_tag, $trace; 508 static $registry = null, $maxpass, $globals; 509 510 if (empty($tag)) { 511 return; 512 } 513 514 if ($registry === null) { 515 $maxpass = get_pref('secondpass', 1); 516 $registry = Txp::get('\Textpattern\Tag\Registry'); 517 $globals = array_filter( 518 $registry->getRegistered(true), 519 function ($v) { 520 return !is_bool($v); 521 } 522 ); 523 } 524 525 $old_tag = $txp_current_tag; 526 $old_atts = $txp_atts; 527 $dotrace = $production_status !== 'live' && is_array($txp_tag); 528 529 if ($dotrace) { 530 $txp_current_tag = $txp_tag[0].$txp_tag[3].$txp_tag[4]; 531 $tag_stop = $txp_tag[4]; 532 $trace->start($txp_tag[0]); 533 } 534 535 if ($atts) { 536 $split = splat($atts); 537 } else { 538 $txp_atts = null; 539 $split = array(); 540 } 541 542 if (!isset($txp_atts['txp-process'])) { 543 $out = $registry->process($tag, $split, $thing); 544 } else { 545 $process = empty($txp_atts['txp-process']) || is_numeric($txp_atts['txp-process']) ? (int) $txp_atts['txp-process'] : 1; 546 547 if ($process <= $pretext['secondpass'] + 1) { 548 unset($txp_atts['txp-process']); 549 $out = $process > 0 ? $registry->process($tag, $split, $thing) : ''; 550 } else { 551 $txp_atts['txp-process'] = $process; 552 $out = ''; 553 } 554 } 555 556 if ($out === false) { 557 if (maybe_tag($tag)) { // Deprecated in 4.6.0. 558 trigger_error($tag.' '.gTxt('unregistered_tag'), E_USER_NOTICE); 559 $out = $registry->register($tag)->process($tag, $split, $thing); 560 } else { 561 trigger_error($tag.' '.gTxt('unknown_tag'), E_USER_WARNING); 562 $out = ''; 563 } 564 } 565 566 if (isset($txp_atts['txp-process']) && (int) $txp_atts['txp-process'] > $pretext['secondpass'] + 1) { 567 $out = $pretext['secondpass'] < $maxpass ? $txp_current_tag : ''; 568 } else { 569 if ($thing === null && !empty($txp_atts['not'])) { 570 $out = $out ? '' : '1'; 571 } 572 573 unset($txp_atts['txp-process'], $txp_atts['not'], $txp_atts['evaluate']); 574 575 if ($txp_atts && $txp_tag !== false) { 576 $pretext['_txp_atts'] = true; 577 578 foreach ($txp_atts as $attr => &$val) { 579 if (isset($val) && isset($globals[$attr])) { 580 $out = $registry->processAttr($attr, $split, $out); 581 } 582 } 583 584 $pretext['_txp_atts'] = false; 585 } 586 } 587 588 $txp_atts = $old_atts; 589 $txp_current_tag = $old_tag; 590 591 if ($dotrace) { 592 $trace->stop($tag_stop); 593 } 594 595 return $out; 596 } 597 598 /** 599 * Checks a named item's existence in a database table. 600 * 601 * The given database table is prefixed with 'txp_'. As such this function can 602 * only be used with core database tables. 603 * 604 * @param string $table The database table name 605 * @param string $val The name to look for 606 * @param bool $debug Dump the query 607 * @return bool|string The item's name, or FALSE when it doesn't exist 608 * @package Filter 609 * @example 610 * if ($r = ckEx('section', 'about')) 611 * { 612 * echo "Section '{$r}' exists."; 613 * } 614 */ 615 616 function ckEx($table, $val, $debug = false) 617 { 618 $table === 'textpattern' or $table = 'txp_'.$table; 619 620 if (is_array($val)) { 621 $fields = implode(',', array_keys($val)); 622 $where = join_qs(quote_list(array_filter($val)), ' AND '); 623 624 return safe_row($fields, $table, $where." LIMIT 1", $debug); 625 } else { 626 $fields = 'name'; 627 $where = "name = '".doSlash($val)."'"; 628 629 return safe_field($fields, $table, $where." LIMIT 1", $debug); 630 } 631 } 632 633 /** 634 * Checks if the given category exists. 635 * 636 * @param string $type The category type, either 'article', 'file', 'link', 'image' 637 * @param string $val The category name to look for 638 * @param bool $debug Dump the query 639 * @return bool|array The category's data, or FALSE when it doesn't exist 640 * @package Filter 641 * @see ckEx() 642 * @example 643 * if ($r = ckCat('article', 'development')) 644 * { 645 * echo "Category {$r['name']} exists."; 646 * } 647 */ 648 649 function ckCat($type, $val, $debug = false) 650 { 651 return safe_row("name, title, description, type", 'txp_category', "name = '".doSlash($val)."' AND type = '".doSlash($type)."' LIMIT 1", $debug); 652 } 653 654 /** 655 * Lookup an article by ID. 656 * 657 * This function takes an article's ID, and checks if it's been published. If it 658 * has, returns the section and the ID as an array. FALSE otherwise. 659 * 660 * @param int $val The article ID 661 * @param bool $debug Dump the query 662 * @return array|bool Array of ID and section on success, FALSE otherwise 663 * @package Filter 664 * @example 665 * if ($r = ckExID(36)) 666 * { 667 * echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}."; 668 * } 669 */ 670 671 function ckExID($val, $debug = false) 672 { 673 return safe_row("ID, Section", 'textpattern', "ID = ".intval($val)." AND Status >= 4 LIMIT 1", $debug); 674 } 675 676 /** 677 * Lookup an article by URL title. 678 * 679 * This function takes an article's URL title, and checks if the article has 680 * been published. If it has, returns the section and the ID as an array. 681 * FALSE otherwise. 682 * 683 * @param string $val The URL title 684 * @param bool $debug Dump the query 685 * @return array|bool Array of ID and section on success, FALSE otherwise 686 * @package Filter 687 * @example 688 * if ($r = ckExID('my-article-title')) 689 * { 690 * echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}."; 691 * } 692 */ 693 694 function lookupByTitle($val, $debug = false) 695 { 696 return safe_row("ID, Section", 'textpattern', "url_title = '".doSlash($val)."' AND Status >= 4 LIMIT 1", $debug); 697 } 698 699 /** 700 * Lookup a published article by URL title and section. 701 * 702 * This function takes an article's URL title, and checks if the article has 703 * been published. If it has, returns the section and the ID as an array. 704 * FALSE otherwise. 705 * 706 * @param string $val The URL title 707 * @param string $section The section name 708 * @param bool $debug Dump the query 709 * @return array|bool Array of ID and section on success, FALSE otherwise 710 * @package Filter 711 * @example 712 * if ($r = ckExID('my-article-title', 'my-section')) 713 * { 714 * echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}."; 715 * } 716 */ 717 718 function lookupByTitleSection($val, $section, $debug = false) 719 { 720 return safe_row("ID, Section", 'textpattern', "url_title = '".doSlash($val)."' AND Section = '".doSlash($section)."' AND Status >= 4 LIMIT 1", $debug); 721 } 722 723 /** 724 * Lookup live article by ID and section. 725 * 726 * @param int $id Article ID 727 * @param string $section Section name 728 * @param bool $debug 729 * @return array|bool 730 * @package Filter 731 */ 732 733 function lookupByIDSection($id, $section, $debug = false) 734 { 735 return safe_row("ID, Section", 'textpattern', "ID = ".intval($id)." AND Section = '".doSlash($section)."' AND Status >= 4 LIMIT 1", $debug); 736 } 737 738 /** 739 * Lookup live article by ID. 740 * 741 * @param int $id Article ID 742 * @param bool $debug 743 * @return array|bool 744 * @package Filter 745 */ 746 747 function lookupByID($id, $debug = false) 748 { 749 return safe_row("ID, Section", 'textpattern', "ID = ".intval($id)." AND Status >= 4 LIMIT 1", $debug); 750 } 751 752 /** 753 * Lookup live article by date and URL title. 754 * 755 * @param string $when date wildcard 756 * @param string $title URL title 757 * @param bool $debug 758 * @return array|bool 759 * @package Filter 760 */ 761 762 function lookupByDateTitle($when, $title, $debug = false) 763 { 764 return safe_row("ID, Section", 'textpattern', "posted LIKE '".doSlash($when)."%' AND url_title LIKE '".doSlash($title)."' AND Status >= 4 LIMIT 1"); 765 } 766 767 /** 768 * Save and retrieve the individual article's attributes plus article list 769 * attributes for next/prev tags. 770 * 771 * @param array $atts 772 * @param bool $iscustom 773 * @return array/string 774 * @since 4.5.0 775 * @package TagParser 776 */ 777 778 function filterAtts($atts = null, $iscustom = null) 779 { 780 global $pretext, $trace, $thisarticle; 781 static $out = array(); 782 783 if ($atts === false) { 784 return $out = array(); 785 } elseif (!is_array($atts)) { 786 // TODO: deal w/ nested txp:article[_custom] tags. See https://github.com/textpattern/textpattern/issues/1009 787 $trace->log('[filterAtts ignored]'); 788 789 return $out; 790 } elseif (isset($atts['*'])) { 791 return $out = $atts; 792 } 793 794 $exclude = isset($atts['exclude']) ? $atts['exclude'] : ''; 795 unset($atts['exclude']); 796 797 if ($exclude && $exclude !== true) { 798 $exclude = array_map('strtolower', do_list_unique($exclude)); 799 $excluded = array_filter($exclude, 'is_numeric'); 800 empty($excluded) or $exclude = array_diff($exclude, $excluded); 801 } else { 802 $exclude or $exclude = array(); 803 $excluded = array(); 804 } 805 806 $exclude === true or $exclude = array_fill_keys($exclude, true); 807 808 $customFields = getCustomFields(); 809 $customlAtts = array_null(array_flip($customFields)); 810 811 $extralAtts = array( 812 'form' => 'default', 813 'allowoverride' => !$iscustom, 814 'limit' => 10, 815 'offset' => 0, 816 'pageby' => null, 817 'pgonly' => 0, 818 'wraptag' => '', 819 'break' => '', 820 'breakby' => '', 821 'breakclass' => '', 822 'breakform' => '', 823 'label' => '', 824 'labeltag' => '', 825 'class' => '', 826 'searchall' => !$iscustom && !empty($pretext['q']), 827 ); 828 829 if ($iscustom) { 830 $customlAtts = array( 831 'category' => '', 832 'section' => '', 833 'author' => '', 834 'month' => '', 835 'expired' => get_pref('publish_expired_articles'), 836 ) + $customlAtts; 837 } else { 838 $extralAtts += array( 839 'listform' => '', 840 'searchform' => '', 841 'searchsticky' => 0, 842 ); 843 } 844 845 if ($exclude && is_array($exclude)) { 846 foreach ($exclude as $cField => $val) { 847 if (array_key_exists($cField, $customlAtts) && !isset($atts[$cField])) { 848 $atts[$cField] = $val; 849 } 850 } 851 } 852 853 // Getting attributes. 854 $theAtts = lAtts(array( 855 'fields' => null, 856 'sort' => '', 857 'keywords' => '', 858 'time' => null, 859 'status' => empty($atts['id']) ? STATUS_LIVE : true, 860 'frontpage' => !$iscustom, 861 'match' => 'Category', 862 'depth' => 0, 863 'id' => '', 864 'excerpted' => '' 865 ) + $extralAtts + $customlAtts, $atts); 866 867 // For the txp:article tag, some attributes are taken from globals; 868 // override them, then stash all filter attributes. 869 extract($pretext); 870 871 if (!$iscustom) { 872 $theAtts['category'] = ($c) ? $c : ''; 873 $theAtts['section'] = ($s && $s != 'default') ? $s : ''; 874 $theAtts['author'] = (!empty($author) ? $author : ''); 875 $theAtts['month'] = (!empty($month) ? $month : ''); 876 $theAtts['expired'] = get_pref('publish_expired_articles'); 877 $theAtts['frontpage'] = ($theAtts['frontpage'] && !$theAtts['section']); 878 } else { 879 $q = ''; 880 } 881 882 extract($theAtts); 883 884 // Treat sticky articles differently wrt search filtering, etc. 885 $issticky = in_array(strtolower($status), array('sticky', STATUS_STICKY)); 886 887 if ($status === true) { 888 $status = array(STATUS_LIVE, STATUS_STICKY); 889 } else { 890 $status = array($issticky ? STATUS_STICKY : STATUS_LIVE); 891 } 892 893 // Categories 894 $operator = 'AND'; 895 $match = parse_qs($match); 896 897 if (isset($match['category'])) { 898 isset($match['category1']) or $match['category1'] = $match['category']; 899 isset($match['category2']) or $match['category2'] = $match['category']; 900 $operator = 'OR'; 901 } 902 903 $categories = $category === true ? false : do_list_unique($category); 904 $catquery = array(); 905 906 if ($categories && (!$depth || $categories = getTree($categories, 'article', '1', 'txp_category', $depth))) { 907 $categories = join("','", doSlash($categories)); 908 } 909 910 for ($i = 1; $i <= 2; $i++) { 911 $not = isset($exclude["category{$i}"]) ? '!' : ''; 912 913 if (isset($match['category'.$i])) { 914 if ($match['category'.$i] === false) { 915 if ($categories) { 916 $catquery[] = "$not(Category{$i} IN ('$categories'))"; 917 } elseif ($category === true || $not) { 918 $catquery[] = "$not(Category{$i} != '')"; 919 } 920 } elseif (($val = gps($match['category'.$i], false)) !== false) { 921 $catquery[] = "$not(Category{$i} IN (".implode(',', quote_list(is_array($val) ? $val : do_list($val)))."))"; 922 } 923 } elseif ($not) { 924 $catquery[] = "(Category{$i} = '')"; 925 } 926 } 927 928 $not = $iscustom && ($exclude === true || isset($exclude['category'])) ? '!' : ''; 929 $catquery = join(" $operator ", $catquery); 930 $category = !$catquery ? '' : " AND $not($catquery)"; 931 932 // ID 933 $not = $exclude === true || isset($exclude['id']) ? 'NOT' : ''; 934 $ids = $id ? ($id === true ? array(article_id()) : array_map('intval', do_list_unique($id, array(',', '-')))) : array(); 935 $id = ((!$ids) ? '' : " AND ID $not IN (".join(',', $ids).")") 936 .(!$excluded ? '' : " AND ID NOT IN (".join(',', $excluded).")"); 937 $getid = $ids && !$not; 938 939 // Section 940 // searchall=0 can be used to show search results for the current 941 // section only. 942 if ($q && $searchall && !$issticky) { 943 $section = ''; 944 } 945 946 $not = $iscustom && ($exclude === true || isset($exclude['section'])) ? 'NOT' : ''; 947 $section !== true or $section = processTags('section'); 948 $getid = $getid || $section && !$not; 949 $section = (!$section ? '' : " AND Section $not IN ('".join("','", doSlash(do_list_unique($section)))."')"). 950 ($getid || $searchall? '' : filterFrontPage('Section', 'page')); 951 952 953 // Author 954 $not = $iscustom && ($exclude === true || isset($exclude['author'])) ? 'NOT' : ''; 955 $author !== true or $author = processTags('author', 'escape="" title=""'); 956 $author = (!$author) ? '' : " AND AuthorID $not IN ('".join("','", doSlash(do_list_unique($author)))."')"; 957 958 $frontpage = ($frontpage && (!$q || $issticky)) ? filterFrontPage() : ''; 959 $excerpted = (!$excerpted) ? '' : " AND Excerpt !=''"; 960 961 if ($time === null || $month || !$expired || $expired == '1') { 962 $not = $iscustom && ($month || $time !== null) && ($exclude === true || isset($exclude['month'])); 963 $timeq = buildTimeSql($month, $time === null ? 'past' : $time); 964 $timeq = ' AND '.($not ? "!($timeq)" : $timeq); 965 } else { 966 $timeq = ''; 967 } 968 969 if ($expired && $expired != '1') { 970 $timeq .= ' AND '.buildTimeSql($expired, $time === null && !strtotime($expired) ? 'any' : $time, 'Expires'); 971 } elseif (!$expired) { 972 $timeq .= ' AND (Expires IS NULL OR '.now('expires').' <= Expires)'; 973 } 974 975 if ($q && $searchsticky) { 976 $statusq = " AND Status >= ".STATUS_LIVE; 977 } else { 978 $statusq = " AND Status IN (".implode(',', $status).")"; 979 } 980 981 $custom = ''; 982 983 if ($customFields) { 984 foreach ($customFields as $cField) { 985 if (isset($atts[$cField])) { 986 $customPairs[$cField] = $atts[$cField]; 987 } 988 989 if (isset($match[$cField])) { 990 if ($match[$cField] === false && isset($thisarticle[$cField])) { 991 $customPairs[$cField] = $thisarticle[$cField]; 992 } elseif (($val = gps($match[$cField] === false ? $cField : $match[$cField], false)) !== false) { 993 $customPairs[$cField] = $val; 994 } 995 } 996 } 997 998 if (!empty($customPairs)) { 999 $custom = buildCustomSql($customFields, $customPairs, $exclude); 1000 } 1001 } 1002 1003 // Allow keywords for no-custom articles. That tagging mode, you know. 1004 $keywords !== true or $keywords = processTags('keywords'); 1005 1006 if ($keywords) { 1007 $keyparts = array(); 1008 $not = $exclude === true || in_array('keywords', $exclude) ? '!' : ''; 1009 $keys = doSlash(do_list_unique($keywords)); 1010 1011 foreach ($keys as $key) { 1012 $keyparts[] = "FIND_IN_SET('".$key."', Keywords)"; 1013 } 1014 1015 !$keyparts or $keywords = " AND $not(".join(' or ', $keyparts).")"; 1016 } 1017 1018 $theAtts['status'] = implode(',', $status); 1019 $theAtts['id'] = implode(',', $ids); 1020 $theAtts['sort'] = sanitizeForSort($sort); 1021 $theAtts['*'] = '1'.$timeq.$id.$category.$section.$excerpted.$author.$statusq.$frontpage.$keywords.$custom; 1022 1023 if (!$iscustom) { 1024 $out = array_diff_key($theAtts, $extralAtts); 1025 $trace->log('[filterAtts accepted]'); 1026 } 1027 1028 return $theAtts; 1029 } 1030 1031 /** 1032 * Set a flag to postpone tag processing. 1033 * 1034 * @param int $pass 1035 * @return null 1036 * @since 4.7.0 1037 * @package TagParser 1038 */ 1039 1040 function postpone_process($pass = null) 1041 { 1042 global $pretext, $txp_atts; 1043 1044 $txp_atts['txp-process'] = intval($pass === null ? $pretext['secondpass'] + 2 : $pass); 1045 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title