PageRenderTime 79ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/textpattern/lib/txplib_misc.php

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