.
*/
/**
* Tools for page routing and handling article data.
*
* @since 4.5.0
* @package Routing
*/
/**
* Build a query qualifier to remove non-frontpage articles from the result set.
*
* @return string An SQL qualifier for a query's 'WHERE' part
*/
function filterFrontPage()
{
static $filterFrontPage;
if (isset($filterFrontPage)) {
return $filterFrontPage;
}
$filterFrontPage = false;
$rs = safe_column("name", 'txp_section', "on_frontpage != '1'");
if ($rs) {
$filters = array();
foreach ($rs as $name) {
$filters[] = " and Section != '".doSlash($name)."'";
}
$filterFrontPage = join('', $filters);
}
return $filterFrontPage;
}
/**
* Populates the current article data.
*
* Fills members of $thisarticle global from a database row.
*
* Keeps all article tag-related values in one place, in order to do easy
* bugfixing and ease the addition of new article tags.
*
* @param array $rs An article as an assocative array
* @example
* if ($rs = safe_rows_start("*,
* UNIX_TIMESTAMP(Posted) AS uPosted,
* UNIX_TIMESTAMP(Expires) AS uExpires,
* UNIX_TIMESTAMP(LastMod) AS uLastMod",
* 'textpattern',
* "1 = 1"
* ))
* {
* global $thisarticle;
* while ($row = nextRow($rs))
* {
* populateArticleData($row);
* echo $thisarticle['title'];
* }
* }
*/
function populateArticleData($rs)
{
global $thisarticle, $trace;
$trace->log("[Article: '{$rs['ID']}']");
foreach (article_column_map() as $key => $column) {
$thisarticle[$key] = $rs[$column];
}
}
/**
* Formats article info and populates the current article data.
*
* Fills members of $thisarticle global from a database row.
*
* Basically just converts an article's date values to UNIX timestamps.
* Convenience for those who prefer doing conversion in application end instead
* of in the SQL statement.
*
* @param array $rs An article as an assocative array
* @example
* article_format_info(
* safe_row('*', 'textpattern', 'Status = 4 LIMIT 1')
* )
*/
function article_format_info($rs)
{
$rs['uPosted'] = (($unix_ts = @strtotime($rs['Posted'])) !== false) ? $unix_ts : null;
$rs['uLastMod'] = (($unix_ts = @strtotime($rs['LastMod'])) !== false) ? $unix_ts : null;
$rs['uExpires'] = (($unix_ts = @strtotime($rs['Expires'])) !== false) ? $unix_ts : null;
populateArticleData($rs);
}
/**
* Maps 'textpattern' table's columns to article data values.
*
* This function returns an array of 'data-value' => 'column' pairs.
*
* @return array
*/
function article_column_map()
{
$custom = getCustomFields();
$custom_map = array();
if ($custom) {
foreach ($custom as $i => $name) {
$custom_map[$name] = 'custom_'.$i;
}
}
return array(
'thisid' => 'ID',
'posted' => 'uPosted', // Calculated value!
'expires' => 'uExpires', // Calculated value!
'modified' => 'uLastMod', // Calculated value!
'annotate' => 'Annotate',
'comments_invite' => 'AnnotateInvite',
'authorid' => 'AuthorID',
'title' => 'Title',
'url_title' => 'url_title',
'description' => 'description',
'category1' => 'Category1',
'category2' => 'Category2',
'section' => 'Section',
'keywords' => 'Keywords',
'article_image' => 'Image',
'comments_count' => 'comments_count',
'body' => 'Body_html',
'excerpt' => 'Excerpt_html',
'override_form' => 'override_form',
'status' => 'Status',
) + $custom_map;
}
/**
* Find an adjacent article relative to a provided threshold level.
*
* @param scalar $threshold The value to compare against
* @param string $s Optional section restriction
* @param string $type Lesser or greater neighbour? Either '<' (previous) or '>' (next)
* @param array $atts Attribute of article at threshold
* @param string $threshold_type 'cooked': Use $threshold as SQL clause; 'raw': Use $threshold as an escapable scalar
* @return array|bool An array populated with article data, or 'false' in case of no matches
*/
function getNeighbour($threshold, $s, $type, $atts = array(), $threshold_type = 'raw')
{
global $prefs;
static $cache = array();
$key = md5($threshold.$s.$type.join(n, $atts));
if (isset($cache[$key])) {
return $cache[$key];
}
extract($atts);
$expired = ($expired && ($prefs['publish_expired_articles']));
$customFields = getCustomFields();
$thisid = isset($thisid) ? intval($thisid) : 0;
// Building query parts; lifted from publish.php.
$ids = array_map('intval', do_list($id));
$id = (!$id) ? '' : " AND ID IN (".join(',', $ids).")";
switch ($time) {
case 'any':
$time = "";
break;
case 'future':
$time = " AND Posted > ".now('posted');
break;
default:
$time = " AND Posted <= ".now('posted');
}
if (!$expired) {
$time .= " AND (".now('expires')." <= Expires OR Expires IS NULL)";
}
$custom = '';
if ($customFields) {
foreach ($customFields as $cField) {
if (isset($atts[$cField])) {
$customPairs[$cField] = $atts[$cField];
}
}
if (!empty($customPairs)) {
$custom = buildCustomSql($customFields, $customPairs);
}
}
if ($keywords) {
$keys = doSlash(do_list($keywords));
foreach ($keys as $key) {
$keyparts[] = "FIND_IN_SET('".$key."', Keywords)";
}
$keywords = " AND (".join(" OR ", $keyparts).")";
}
$sortdir = strtolower($sortdir);
// Invert $type for ascending sortdir.
$types = array(
'>' => array('desc' => '>', 'asc' => '<'),
'<' => array('desc' => '<', 'asc' => '>'),
);
$type = ($type == '>') ? $types['>'][$sortdir] : $types['<'][$sortdir];
// Escape threshold and treat it as a string unless explicitly told otherwise.
if ($threshold_type != 'cooked') {
$threshold = "'".doSlash($threshold)."'";
}
$safe_name = safe_pfx('textpattern');
$q = array(
"SELECT ID AS thisid, Section AS section, Title AS title, url_title, UNIX_TIMESTAMP(Posted) AS posted FROM $safe_name
WHERE ($sortby $type $threshold OR ".($thisid ? "$sortby = $threshold AND ID $type $thisid" : "0").")",
($s != '' && $s != 'default') ? "AND Section = '".doSlash($s)."'" : filterFrontPage(),
$id,
$time,
$custom,
$keywords,
"AND Status = 4",
"ORDER BY $sortby",
($type == '<') ? "DESC" : "ASC",
', ID '.($type == '<' ? 'DESC' : 'ASC'),
"LIMIT 1",
);
$cache[$key] = getRow(join(n.' ', $q));
return (is_array($cache[$key])) ? $cache[$key] : false;
}
/**
* Find next and previous articles relative to a provided threshold level.
*
* @param int $id The "pivot" article's id; use zero (0) to indicate $thisarticle
* @param scalar $threshold The value to compare against if $id != 0
* @param string $s Optional section restriction if $id != 0
* @return array An array populated with article data
*/
function getNextPrev($id = 0, $threshold = null, $s = '')
{
$threshold_type = 'raw';
if ($id !== 0) {
// Pivot is specific article by ID: In lack of further information,
// revert to default sort order 'Posted desc'.
$atts = filterAtts() + array('sortby' => 'Posted', 'sortdir' => 'DESC', 'thisid' => $id);
} else {
// Pivot is $thisarticle: Use article attributes to find its neighbours.
assert_article();
global $thisarticle;
if (!is_array($thisarticle)) {
return array();
}
$s = $thisarticle['section'];
$atts = filterAtts() + array('thisid' => $thisarticle['thisid'], 'sort' => 'Posted DESC');
$m = preg_split('/\s+/', $atts['sort']);
// If in doubt, fall back to chronologically descending order.
if (empty($m[0]) // No explicit sort attribute
|| count($m) > 2 // Complex clause, e.g. 'foo asc, bar desc'
|| !preg_match('/^(?:[0-9a-zA-Z$_\x{0080}-\x{FFFF}]+|`[\x{0001}-\x{FFFF}]+`)$/u', $m[0]) // The clause's first verb is not a MySQL column identifier.
) {
$atts['sortby'] = "Posted";
$atts['sortdir'] = "DESC";
} else {
// Sort is like 'foo asc'.
$atts['sortby'] = $m[0];
$atts['sortdir'] = (isset($m[1]) && strtolower($m[1]) == 'desc' ? "DESC" : "ASC");
}
// Attributes with special treatment.
switch ($atts['sortby']) {
case 'Posted':
$threshold = "FROM_UNIXTIME(".doSlash($thisarticle['posted']).")";
$threshold_type = 'cooked';
break;
case 'Expires':
$threshold = "FROM_UNIXTIME(".doSlash($thisarticle['expires']).")";
$threshold_type = 'cooked';
break;
case 'LastMod':
$threshold = "FROM_UNIXTIME(".doSlash($thisarticle['modified']).")";
$threshold_type = 'cooked';
break;
default:
// Retrieve current threshold value per sort column from $thisarticle.
$acm = array_flip(article_column_map());
$key = $acm[$atts['sortby']];
$threshold = $thisarticle[$key];
break;
}
}
$out['next'] = getNeighbour($threshold, $s, '>', $atts, $threshold_type);
$out['prev'] = getNeighbour($threshold, $s, '<', $atts, $threshold_type);
return $out;
}
/**
* Gets the site last modification date.
*
* @return string
* @package Pref
*/
function lastMod()
{
$last = safe_field("UNIX_TIMESTAMP(val)", 'txp_prefs', "name = 'lastmod' AND prefs_id = 1");
return gmdate("D, d M Y H:i:s \G\M\T", $last);
}
/**
* Parse a string and replace any Textpattern tags with their actual value.
*
* @param string $thing The raw string
* @param null|bool $condition Process true/false part
* @return string The parsed string
* @package TagParser
*/
function parse($thing, $condition = null)
{
global $production_status, $trace, $txp_parsed, $txp_else, $txp_current_tag;
if (isset($condition)) {
if ($production_status === 'debug') {
$trace->log("[$txp_current_tag: ".($condition ? 'true' : 'false') .']');
}
} else {
$condition = true;
}
if (false === strpos($thing, ']+))*\s*/?'.chr(62).')@s';
$t = '@^./?(txp|[a-z]{3}:):(\w+)(.*?)/?.$@s';
$parsed = preg_split($f, $thing, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parsed as $i => $chunk) {
if ($i&1) {
preg_match($t, $chunk, $tag[$level]);
$count[$level] += 2;
if ($tag[$level][2] === 'else') {
$else[$level] = $count[$level];
}
// Handle short tags.
if (strlen($tag[$level][1]) !== 3 and $tag[$level][1] !== 'txp:' and $tag[$level][2] !== 'else') {
$tag[$level][2] = $tag[$level][1] . $tag[$level][2];
$tag[$level][2][3] = '_';
}
if ($chunk[strlen($chunk) - 2] === '/') {
// Self closed tag.
$tags[$level][] = array($chunk, $tag[$level][2], $tag[$level][3], null, null);
$inside[$level] .= $chunk;
} elseif ($chunk[1] !== '/') {
// Opening tag.
$inside[$level] .= $chunk;
$level++;
$outside[$level] = $chunk;
$inside[$level] = '';
$else[$level] = $count[$level] = -1;
$tags[$level] = array();
} else {
// Closing tag.
$sha = sha1($inside[$level]);
$txp_parsed[$sha] = $count[$level] > 2 ? $tags[$level] : false;
$txp_else[$sha] = array($else[$level] > 0 ? $else[$level] : $count[$level], $count[$level] - 2);
$level--;
$tags[$level][] = array($outside[$level+1], $tag[$level][2], $tag[$level][3], $inside[$level+1], $chunk);
$inside[$level] .= $inside[$level+1] . $chunk;
}
} else {
$tags[$level][] = $chunk;
$inside[$level] .= $chunk;
}
}
$txp_parsed[$hash] = $count[0] > 0 ? $tags[0] : false;
$txp_else[$hash] = array($else[0] > 0 ? $else[0] : $count[0] + 2, $count[0]);
}
$tag = $txp_parsed[$hash];
if (empty($tag)) {
return $condition ? $thing : '';
}
list($first, $last) = $txp_else[$hash];
if ($condition) {
$last = $first - 2;
$first = 1;
} elseif ($first <= $last) {
$first += 2;
} else {
return '';
}
for ($out = $tag[$first - 1]; $first <= $last; $first++) {
$t = $tag[$first];
$out .= processTags($t[1], $t[2], $t[3]) . $tag[++$first];
}
return $out;
}
/**
* Guesstimate whether a given function name may be a valid tag handler.
*
* @param string $tag function name
* @return bool FALSE if the function name is not a valid tag handler
* @package TagParser
*/
function maybe_tag($tag)
{
static $tags = null;
if ($tags === null) {
$tags = get_defined_functions();
$tags = array_flip($tags['user']);
}
return isset($tags[$tag]);
}
/**
* Parse a tag for attributes and hand over to the tag handler function.
*
* @param string $tag The tag name
* @param string $atts The attribute string
* @param string|null $thing The tag's content in case of container tags
* @return string Parsed tag result
* @package TagParser
*/
function processTags($tag, $atts, $thing = null)
{
global $production_status, $txp_current_tag, $txp_current_form, $trace;
static $registry = null;
if ($production_status !== 'live') {
$old_tag = $txp_current_tag;
$txp_current_tag = '' : '/>');
$trace->start($txp_current_tag);
}
if ($registry === null) {
$registry = Txp::get('\Textpattern\Tag\Registry');
}
$out = $registry->process($tag, splat($atts), $thing);
if ($out === false) {
if (maybe_tag($tag)) { // Deprecated in 4.6.0.
trigger_error(gTxt('unregistered_tag'), E_USER_NOTICE);
$out = $registry->register($tag)->process($tag, splat($atts), $thing);
} else {
trigger_error(gTxt('unknown_tag'), E_USER_WARNING);
$out = '';
}
}
if ($production_status !== 'live') {
$trace->stop(isset($thing) ? "" : null);
$txp_current_tag = $old_tag;
}
return $out;
}
/**
* Protection from those who'd bomb the site by GET.
*
* Origin of the infamous 'Nice try' message and an even more useful '503'
* HTTP status.
*/
function bombShelter()
{
global $prefs;
$in = serverset('REQUEST_URI');
if (!empty($prefs['max_url_len']) and strlen($in) > $prefs['max_url_len']) {
txp_status_header('503 Service Unavailable');
exit('Nice try.');
}
}
/**
* Checks a named item's existence in a database table.
*
* The given database table is prefixed with 'txp_'. As such this function can
* only be used with core database tables.
*
* @param string $table The database table name
* @param string $val The name to look for
* @param bool $debug Dump the query
* @return bool|string The item's name, or FALSE when it doesn't exist
* @package Filter
* @example
* if ($r = ckEx('section', 'about'))
* {
* echo "Section '{$r}' exists.";
* }
*/
function ckEx($table, $val, $debug = false)
{
return safe_field("name", 'txp_'.$table, "name = '".doSlash($val)."' LIMIT 1", $debug);
}
/**
* Checks if the given category exists.
*
* @param string $type The category type, either 'article', 'file', 'link', 'image'
* @param string $val The category name to look for
* @param bool $debug Dump the query
* @return bool|string The category's name, or FALSE when it doesn't exist
* @package Filter
* @see ckEx()
* @example
* if ($r = ckCat('article', 'development'))
* {
* echo "Category '{$r}' exists.";
* }
*/
function ckCat($type, $val, $debug = false)
{
return safe_field("name", 'txp_category', "name = '".doSlash($val)."' AND type = '".doSlash($type)."' LIMIT 1", $debug);
}
/**
* Lookup an article by ID.
*
* This function takes an article's ID, and checks if it's been published. If it
* has, returns the section and the ID as an array. FALSE otherwise.
*
* @param int $val The article ID
* @param bool $debug Dump the query
* @return array|bool Array of ID and section on success, FALSE otherwise
* @package Filter
* @example
* if ($r = ckExID(36))
* {
* echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}.";
* }
*/
function ckExID($val, $debug = false)
{
return safe_row("ID, Section", 'textpattern', "ID = ".intval($val)." AND Status >= 4 LIMIT 1", $debug);
}
/**
* Lookup an article by URL title.
*
* This function takes an article's URL title, and checks if the article has
* been published. If it has, returns the section and the ID as an array.
* FALSE otherwise.
*
* @param string $val The URL title
* @param bool $debug Dump the query
* @return array|bool Array of ID and section on success, FALSE otherwise
* @package Filter
* @example
* if ($r = ckExID('my-article-title'))
* {
* echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}.";
* }
*/
function lookupByTitle($val, $debug = false)
{
return safe_row("ID, Section", 'textpattern', "url_title = '".doSlash($val)."' AND Status >= 4 LIMIT 1", $debug);
}
/**
* Lookup a published article by URL title and section.
*
* This function takes an article's URL title, and checks if the article has
* been published. If it has, returns the section and the ID as an array.
* FALSE otherwise.
*
* @param string $val The URL title
* @param string $section The section name
* @param bool $debug Dump the query
* @return array|bool Array of ID and section on success, FALSE otherwise
* @package Filter
* @example
* if ($r = ckExID('my-article-title', 'my-section'))
* {
* echo "Article #{$r['id']} is published, and belongs to the section {$r['section']}.";
* }
*/
function lookupByTitleSection($val, $section, $debug = false)
{
return safe_row("ID, Section", 'textpattern', "url_title = '".doSlash($val)."' AND Section = '".doSlash($section)."' AND Status >= 4 LIMIT 1", $debug);
}
/**
* Lookup live article by ID and section.
*
* @param int $id Article ID
* @param string $section Section name
* @param bool $debug
* @return array|bool
* @package Filter
*/
function lookupByIDSection($id, $section, $debug = false)
{
return safe_row("ID, Section", 'textpattern', "ID = ".intval($id)." AND Section = '".doSlash($section)."' AND Status >= 4 LIMIT 1", $debug);
}
/**
* Lookup live article by ID.
*
* @param int $id Article ID
* @param bool $debug
* @return array|bool
* @package Filter
*/
function lookupByID($id, $debug = false)
{
return safe_row("ID, Section", 'textpattern', "ID = ".intval($id)." AND Status >= 4 LIMIT 1", $debug);
}
/**
* Lookup live article by date and URL title.
*
* @param string $when date wildcard
* @param string $title URL title
* @param bool $debug
* @return array|bool
* @package Filter
*/
function lookupByDateTitle($when, $title, $debug = false)
{
return safe_row("ID, Section", 'textpattern', "posted LIKE '".doSlash($when)."%' AND url_title LIKE '".doSlash($title)."' AND Status >= 4 LIMIT 1");
}
/**
* Chops a request string into URL-decoded path parts.
*
* @param string $req Request string
* @return array
* @package URL
*/
function chopUrl($req)
{
$req = strtolower($req);
// Strip off query_string, if present.
$qs = strpos($req, '?');
if ($qs) {
$req = substr($req, 0, $qs);
}
$req = preg_replace('/index\.php$/', '', $req);
$r = array_map('urldecode', explode('/', $req));
$o['u0'] = (isset($r[0])) ? $r[0] : '';
$o['u1'] = (isset($r[1])) ? $r[1] : '';
$o['u2'] = (isset($r[2])) ? $r[2] : '';
$o['u3'] = (isset($r[3])) ? $r[3] : '';
$o['u4'] = (isset($r[4])) ? $r[4] : '';
return $o;
}
/**
* Save and retrieve the individual article's attributes plus article list
* attributes for next/prev tags.
*
* @param array $atts
* @return array
* @since 4.5.0
* @package TagParser
*/
function filterAtts($atts = null)
{
global $prefs, $trace;
static $out = array();
if (is_array($atts)) {
if (empty($out)) {
$out = lAtts(array(
'sort' => 'Posted desc',
'sortby' => '',
'sortdir' => '',
'keywords' => '',
'expired' => $prefs['publish_expired_articles'],
'id' => '',
'time' => 'past',
), $atts, 0);
$trace->log('[filterAtts accepted]');
} else {
// TODO: deal w/ nested txp:article[_custom] tags.
$trace->log('[filterAtts ignored]');
}
}
if (empty($out)) {
$trace->log('[filterAtts not set]');
}
return $out;
}