utils.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. <?php
  2. // Misc help functions and utilities.
  3. /**
  4. * Returns a string of the required length containing random characters. Note that
  5. * the maximum possible string length is 32.
  6. *
  7. * @param int $len Optional. The required length. Default 32.
  8. * @return string The string.
  9. */
  10. function rand_str( $len = 32 ) {
  11. return substr( md5( uniqid( rand() ) ), 0, $len );
  12. }
  13. /**
  14. * Returns a string of the required length containing random characters.
  15. *
  16. * @param int $len The required length.
  17. * @return string The string.
  18. */
  19. function rand_long_str( $length ) {
  20. $chars = 'abcdefghijklmnopqrstuvwxyz';
  21. $string = '';
  22. for ( $i = 0; $i < $length; $i++ ) {
  23. $rand = rand( 0, strlen( $chars ) - 1 );
  24. $string .= substr( $chars, $rand, 1 );
  25. }
  26. return $string;
  27. }
  28. /**
  29. * Strips leading and trailing whitespace from each line in the string.
  30. *
  31. * @param string $txt The text.
  32. * @return string Text with line-leading and line-trailing whitespace stripped.
  33. */
  34. function strip_ws( $txt ) {
  35. $lines = explode( "\n", $txt );
  36. $result = array();
  37. foreach ( $lines as $line ) {
  38. if ( trim( $line ) ) {
  39. $result[] = trim( $line );
  40. }
  41. }
  42. return trim( implode( "\n", $result ) );
  43. }
  44. /*
  45. * Helper class for testing code that involves actions and filters.
  46. *
  47. * Typical use:
  48. *
  49. * $ma = new MockAction();
  50. * add_action( 'foo', array( &$ma, 'action' ) );
  51. */
  52. class MockAction {
  53. public $events;
  54. public $debug;
  55. /**
  56. * PHP5 constructor.
  57. */
  58. function __construct( $debug = 0 ) {
  59. $this->reset();
  60. $this->debug = $debug;
  61. }
  62. function reset() {
  63. $this->events = array();
  64. }
  65. function current_filter() {
  66. if ( is_callable( 'current_filter' ) ) {
  67. return current_filter();
  68. }
  69. global $wp_actions;
  70. return end( $wp_actions );
  71. }
  72. function action( $arg ) {
  73. if ( $this->debug ) {
  74. dmp( __FUNCTION__, $this->current_filter() );
  75. }
  76. $args = func_get_args();
  77. $this->events[] = array(
  78. 'action' => __FUNCTION__,
  79. 'tag' => $this->current_filter(),
  80. 'args' => $args,
  81. );
  82. return $arg;
  83. }
  84. function action2( $arg ) {
  85. if ( $this->debug ) {
  86. dmp( __FUNCTION__, $this->current_filter() );
  87. }
  88. $args = func_get_args();
  89. $this->events[] = array(
  90. 'action' => __FUNCTION__,
  91. 'tag' => $this->current_filter(),
  92. 'args' => $args,
  93. );
  94. return $arg;
  95. }
  96. function filter( $arg ) {
  97. if ( $this->debug ) {
  98. dmp( __FUNCTION__, $this->current_filter() );
  99. }
  100. $args = func_get_args();
  101. $this->events[] = array(
  102. 'filter' => __FUNCTION__,
  103. 'tag' => $this->current_filter(),
  104. 'args' => $args,
  105. );
  106. return $arg;
  107. }
  108. function filter2( $arg ) {
  109. if ( $this->debug ) {
  110. dmp( __FUNCTION__, $this->current_filter() );
  111. }
  112. $args = func_get_args();
  113. $this->events[] = array(
  114. 'filter' => __FUNCTION__,
  115. 'tag' => $this->current_filter(),
  116. 'args' => $args,
  117. );
  118. return $arg;
  119. }
  120. function filter_append( $arg ) {
  121. if ( $this->debug ) {
  122. dmp( __FUNCTION__, $this->current_filter() );
  123. }
  124. $args = func_get_args();
  125. $this->events[] = array(
  126. 'filter' => __FUNCTION__,
  127. 'tag' => $this->current_filter(),
  128. 'args' => $args,
  129. );
  130. return $arg . '_append';
  131. }
  132. function filterall( $tag, ...$args ) {
  133. // This one doesn't return the result, so it's safe to use with the new 'all' filter.
  134. if ( $this->debug ) {
  135. dmp( __FUNCTION__, $this->current_filter() );
  136. }
  137. $this->events[] = array(
  138. 'filter' => __FUNCTION__,
  139. 'tag' => $tag,
  140. 'args' => $args,
  141. );
  142. }
  143. // Return a list of all the actions, tags and args.
  144. function get_events() {
  145. return $this->events;
  146. }
  147. // Return a count of the number of times the action was called since the last reset.
  148. function get_call_count( $tag = '' ) {
  149. if ( $tag ) {
  150. $count = 0;
  151. foreach ( $this->events as $e ) {
  152. if ( $e['action'] === $tag ) {
  153. ++$count;
  154. }
  155. }
  156. return $count;
  157. }
  158. return count( $this->events );
  159. }
  160. // Return an array of the tags that triggered calls to this action.
  161. function get_tags() {
  162. $out = array();
  163. foreach ( $this->events as $e ) {
  164. $out[] = $e['tag'];
  165. }
  166. return $out;
  167. }
  168. // Return an array of args passed in calls to this action.
  169. function get_args() {
  170. $out = array();
  171. foreach ( $this->events as $e ) {
  172. $out[] = $e['args'];
  173. }
  174. return $out;
  175. }
  176. }
  177. // Convert valid XML to an array tree structure.
  178. // Kinda lame, but it works with a default PHP 4 installation.
  179. class TestXMLParser {
  180. public $xml;
  181. public $data = array();
  182. /**
  183. * PHP5 constructor.
  184. */
  185. function __construct( $in ) {
  186. $this->xml = xml_parser_create();
  187. xml_set_object( $this->xml, $this );
  188. xml_parser_set_option( $this->xml, XML_OPTION_CASE_FOLDING, 0 );
  189. xml_set_element_handler( $this->xml, array( $this, 'start_handler' ), array( $this, 'end_handler' ) );
  190. xml_set_character_data_handler( $this->xml, array( $this, 'data_handler' ) );
  191. $this->parse( $in );
  192. }
  193. function parse( $in ) {
  194. $parse = xml_parse( $this->xml, $in, true );
  195. if ( ! $parse ) {
  196. trigger_error(
  197. sprintf(
  198. 'XML error: %s at line %d',
  199. xml_error_string( xml_get_error_code( $this->xml ) ),
  200. xml_get_current_line_number( $this->xml )
  201. ),
  202. E_USER_ERROR
  203. );
  204. xml_parser_free( $this->xml );
  205. }
  206. return true;
  207. }
  208. function start_handler( $parser, $name, $attributes ) {
  209. $data['name'] = $name;
  210. if ( $attributes ) {
  211. $data['attributes'] = $attributes; }
  212. $this->data[] = $data;
  213. }
  214. function data_handler( $parser, $data ) {
  215. $index = count( $this->data ) - 1;
  216. if ( ! isset( $this->data[ $index ]['content'] ) ) {
  217. $this->data[ $index ]['content'] = '';
  218. }
  219. $this->data[ $index ]['content'] .= $data;
  220. }
  221. function end_handler( $parser, $name ) {
  222. if ( count( $this->data ) > 1 ) {
  223. $data = array_pop( $this->data );
  224. $index = count( $this->data ) - 1;
  225. $this->data[ $index ]['child'][] = $data;
  226. }
  227. }
  228. }
  229. /**
  230. * Converts an XML string into an array tree structure.
  231. *
  232. * The output of this function can be passed to xml_find() to find nodes by their path.
  233. *
  234. * @param string $in The XML string.
  235. * @return array XML as an array.
  236. */
  237. function xml_to_array( $in ) {
  238. $p = new TestXMLParser( $in );
  239. return $p->data;
  240. }
  241. /**
  242. * Finds XML nodes by a given "path".
  243. *
  244. * Example usage:
  245. *
  246. * $tree = xml_to_array( $rss );
  247. * $items = xml_find( $tree, 'rss', 'channel', 'item' );
  248. *
  249. * @param array $tree An array tree structure of XML, typically from xml_to_array().
  250. * @param string ...$elements Names of XML nodes to create a "path" to find within the XML.
  251. * @return array Array of matching XML node information.
  252. */
  253. function xml_find( $tree, ...$elements ) {
  254. $n = count( $elements );
  255. $out = array();
  256. if ( $n < 1 ) {
  257. return $out;
  258. }
  259. for ( $i = 0; $i < count( $tree ); $i++ ) {
  260. # echo "checking '{$tree[$i][name]}' == '{$elements[0]}'\n";
  261. # var_dump( $tree[$i]['name'], $elements[0] );
  262. if ( $tree[ $i ]['name'] === $elements[0] ) {
  263. # echo "n == {$n}\n";
  264. if ( 1 === $n ) {
  265. $out[] = $tree[ $i ];
  266. } else {
  267. $subtree =& $tree[ $i ]['child'];
  268. $out = array_merge( $out, xml_find( $subtree, ...array_slice( $elements, 1 ) ) );
  269. }
  270. }
  271. }
  272. return $out;
  273. }
  274. function xml_join_atts( $atts ) {
  275. $a = array();
  276. foreach ( $atts as $k => $v ) {
  277. $a[] = $k . '="' . $v . '"';
  278. }
  279. return implode( ' ', $a );
  280. }
  281. function xml_array_dumbdown( &$data ) {
  282. $out = array();
  283. foreach ( array_keys( $data ) as $i ) {
  284. $name = $data[ $i ]['name'];
  285. if ( ! empty( $data[ $i ]['attributes'] ) ) {
  286. $name .= ' ' . xml_join_atts( $data[ $i ]['attributes'] );
  287. }
  288. if ( ! empty( $data[ $i ]['child'] ) ) {
  289. $out[ $name ][] = xml_array_dumbdown( $data[ $i ]['child'] );
  290. } else {
  291. $out[ $name ] = $data[ $i ]['content'];
  292. }
  293. }
  294. return $out;
  295. }
  296. function dmp( ...$args ) {
  297. foreach ( $args as $thing ) {
  298. echo ( is_scalar( $thing ) ? (string) $thing : var_export( $thing, true ) ), "\n";
  299. }
  300. }
  301. function dmp_filter( $a ) {
  302. dmp( $a );
  303. return $a;
  304. }
  305. function get_echo( $callable, $args = array() ) {
  306. ob_start();
  307. call_user_func_array( $callable, $args );
  308. return ob_get_clean();
  309. }
  310. // Recursively generate some quick assertEquals() tests based on an array.
  311. function gen_tests_array( $name, $array ) {
  312. $out = array();
  313. foreach ( $array as $k => $v ) {
  314. if ( is_numeric( $k ) ) {
  315. $index = (string) $k;
  316. } else {
  317. $index = "'" . addcslashes( $k, "\n\r\t'\\" ) . "'";
  318. }
  319. if ( is_string( $v ) ) {
  320. $out[] = '$this->assertEquals( \'' . addcslashes( $v, "\n\r\t'\\" ) . '\', $' . $name . '[' . $index . '] );';
  321. } elseif ( is_numeric( $v ) ) {
  322. $out[] = '$this->assertEquals( ' . $v . ', $' . $name . '[' . $index . '] );';
  323. } elseif ( is_array( $v ) ) {
  324. $out[] = gen_tests_array( "{$name}[{$index}]", $v );
  325. }
  326. }
  327. return implode( "\n", $out ) . "\n";
  328. }
  329. /**
  330. * Use to create objects by yourself
  331. */
  332. class MockClass {};
  333. /**
  334. * Drops all tables from the WordPress database
  335. */
  336. function drop_tables() {
  337. global $wpdb;
  338. $tables = $wpdb->get_col( 'SHOW TABLES;' );
  339. foreach ( $tables as $table ) {
  340. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
  341. $wpdb->query( "DROP TABLE IF EXISTS {$table}" );
  342. }
  343. }
  344. function print_backtrace() {
  345. $bt = debug_backtrace();
  346. echo "Backtrace:\n";
  347. $i = 0;
  348. foreach ( $bt as $stack ) {
  349. echo ++$i, ': ';
  350. if ( isset( $stack['class'] ) ) {
  351. echo $stack['class'] . '::';
  352. }
  353. if ( isset( $stack['function'] ) ) {
  354. echo $stack['function'] . '() ';
  355. }
  356. echo "line {$stack[line]} in {$stack[file]}\n";
  357. }
  358. echo "\n";
  359. }
  360. // Mask out any input fields matching the given name.
  361. function mask_input_value( $in, $name = '_wpnonce' ) {
  362. return preg_replace( '@<input([^>]*) name="' . preg_quote( $name ) . '"([^>]*) value="[^>]*" />@', '<input$1 name="' . preg_quote( $name ) . '"$2 value="***" />', $in );
  363. }
  364. /**
  365. * Removes the post type and its taxonomy associations.
  366. */
  367. function _unregister_post_type( $cpt_name ) {
  368. unregister_post_type( $cpt_name );
  369. }
  370. function _unregister_taxonomy( $taxonomy_name ) {
  371. unregister_taxonomy( $taxonomy_name );
  372. }
  373. /**
  374. * Unregister a post status.
  375. *
  376. * @since 4.2.0
  377. *
  378. * @param string $status
  379. */
  380. function _unregister_post_status( $status ) {
  381. unset( $GLOBALS['wp_post_statuses'][ $status ] );
  382. }
  383. function _cleanup_query_vars() {
  384. // Clean out globals to stop them polluting wp and wp_query.
  385. foreach ( $GLOBALS['wp']->public_query_vars as $v ) {
  386. unset( $GLOBALS[ $v ] );
  387. }
  388. foreach ( $GLOBALS['wp']->private_query_vars as $v ) {
  389. unset( $GLOBALS[ $v ] );
  390. }
  391. foreach ( get_taxonomies( array(), 'objects' ) as $t ) {
  392. if ( $t->publicly_queryable && ! empty( $t->query_var ) ) {
  393. $GLOBALS['wp']->add_query_var( $t->query_var );
  394. }
  395. }
  396. foreach ( get_post_types( array(), 'objects' ) as $t ) {
  397. if ( is_post_type_viewable( $t ) && ! empty( $t->query_var ) ) {
  398. $GLOBALS['wp']->add_query_var( $t->query_var );
  399. }
  400. }
  401. }
  402. function _clean_term_filters() {
  403. remove_filter( 'get_terms', array( 'Featured_Content', 'hide_featured_term' ), 10, 2 );
  404. remove_filter( 'get_the_terms', array( 'Featured_Content', 'hide_the_featured_term' ), 10, 3 );
  405. }
  406. /**
  407. * Special class for exposing protected wpdb methods we need to access
  408. */
  409. class WpdbExposedMethodsForTesting extends wpdb {
  410. public function __construct() {
  411. global $wpdb;
  412. $this->dbh = $wpdb->dbh;
  413. $this->use_mysqli = $wpdb->use_mysqli;
  414. $this->is_mysql = $wpdb->is_mysql;
  415. $this->ready = true;
  416. $this->field_types = $wpdb->field_types;
  417. $this->charset = $wpdb->charset;
  418. $this->dbuser = $wpdb->dbuser;
  419. $this->dbpassword = $wpdb->dbpassword;
  420. $this->dbname = $wpdb->dbname;
  421. $this->dbhost = $wpdb->dbhost;
  422. }
  423. public function __call( $name, $arguments ) {
  424. return call_user_func_array( array( $this, $name ), $arguments );
  425. }
  426. }
  427. /**
  428. * Determine approximate backtrack count when running PCRE.
  429. *
  430. * @return int The backtrack count.
  431. */
  432. function benchmark_pcre_backtracking( $pattern, $subject, $strategy ) {
  433. $saved_config = ini_get( 'pcre.backtrack_limit' );
  434. // Attempt to prevent PHP crashes. Adjust lower when needed.
  435. $limit = 1000000;
  436. // Start with small numbers, so if a crash is encountered at higher numbers we can still debug the problem.
  437. for ( $i = 4; $i <= $limit; $i *= 2 ) {
  438. ini_set( 'pcre.backtrack_limit', $i );
  439. switch ( $strategy ) {
  440. case 'split':
  441. preg_split( $pattern, $subject );
  442. break;
  443. case 'match':
  444. preg_match( $pattern, $subject );
  445. break;
  446. case 'match_all':
  447. $matches = array();
  448. preg_match_all( $pattern, $subject, $matches );
  449. break;
  450. }
  451. ini_set( 'pcre.backtrack_limit', $saved_config );
  452. switch ( preg_last_error() ) {
  453. case PREG_NO_ERROR:
  454. return $i;
  455. case PREG_BACKTRACK_LIMIT_ERROR:
  456. break;
  457. case PREG_RECURSION_LIMIT_ERROR:
  458. trigger_error( 'PCRE recursion limit encountered before backtrack limit.' );
  459. return;
  460. case PREG_BAD_UTF8_ERROR:
  461. trigger_error( 'UTF-8 error during PCRE benchmark.' );
  462. return;
  463. case PREG_INTERNAL_ERROR:
  464. trigger_error( 'Internal error during PCRE benchmark.' );
  465. return;
  466. default:
  467. trigger_error( 'Unexpected error during PCRE benchmark.' );
  468. return;
  469. }
  470. }
  471. return $i;
  472. }
  473. function test_rest_expand_compact_links( $links ) {
  474. if ( empty( $links['curies'] ) ) {
  475. return $links;
  476. }
  477. foreach ( $links as $rel => $links_array ) {
  478. if ( ! strpos( $rel, ':' ) ) {
  479. continue;
  480. }
  481. $name = explode( ':', $rel );
  482. $curie = wp_list_filter( $links['curies'], array( 'name' => $name[0] ) );
  483. $full_uri = str_replace( '{rel}', $name[1], $curie[0]['href'] );
  484. $links[ $full_uri ] = $links_array;
  485. unset( $links[ $rel ] );
  486. }
  487. return $links;
  488. }