Keine Beschreibung

class.jetpack-search-helpers.php 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. <?php
  2. /**
  3. * Jetpack Search: Jetpack_Search_Helpers class
  4. *
  5. * @package Jetpack
  6. * @subpackage Jetpack Search
  7. * @since 5.8.0
  8. */
  9. use Automattic\Jetpack\Constants;
  10. require_once dirname( __FILE__ ) . '/class-jetpack-search-options.php';
  11. /**
  12. * Various helper functions for reuse throughout the Jetpack Search code.
  13. *
  14. * @since 5.8.0
  15. */
  16. class Jetpack_Search_Helpers {
  17. /**
  18. * The search widget's base ID.
  19. *
  20. * @since 5.8.0
  21. * @var string
  22. */
  23. const FILTER_WIDGET_BASE = 'jetpack-search-filters';
  24. /**
  25. * Create a URL for the current search that doesn't include the "paged" parameter.
  26. *
  27. * @since 5.8.0
  28. *
  29. * @return string The search URL.
  30. */
  31. static function get_search_url() {
  32. $query_args = stripslashes_deep( $_GET );
  33. // Handle the case where a permastruct is being used, such as /search/{$query}
  34. if ( ! isset( $query_args['s'] ) ) {
  35. $query_args['s'] = get_search_query();
  36. }
  37. if ( isset( $query_args['paged'] ) ) {
  38. unset( $query_args['paged'] );
  39. }
  40. $query = http_build_query( $query_args );
  41. return home_url( "?{$query}" );
  42. }
  43. /**
  44. * Wraps add_query_arg() with the URL defaulting to the current search URL.
  45. *
  46. * @see add_query_arg()
  47. *
  48. * @since 5.8.0
  49. *
  50. * @param string|array $key Either a query variable key, or an associative array of query variables.
  51. * @param string $value Optional. A query variable value.
  52. * @param bool|string $url Optional. A URL to act upon. Defaults to the current search URL.
  53. *
  54. * @return string New URL query string (unescaped).
  55. */
  56. static function add_query_arg( $key, $value = false, $url = false ) {
  57. $url = empty( $url ) ? self::get_search_url() : $url;
  58. if ( is_array( $key ) ) {
  59. return add_query_arg( $key, $url );
  60. }
  61. return add_query_arg( $key, $value, $url );
  62. }
  63. /**
  64. * Wraps remove_query_arg() with the URL defaulting to the current search URL.
  65. *
  66. * @see remove_query_arg()
  67. *
  68. * @since 5.8.0
  69. *
  70. * @param string|array $key Query key or keys to remove.
  71. * @param bool|string $query Optional. A URL to act upon. Defaults to the current search URL.
  72. *
  73. * @return string New URL query string (unescaped).
  74. */
  75. static function remove_query_arg( $key, $url = false ) {
  76. $url = empty( $url ) ? self::get_search_url() : $url;
  77. return remove_query_arg( $key, $url );
  78. }
  79. /**
  80. * Returns the name of the search widget's option.
  81. *
  82. * @since 5.8.0
  83. *
  84. * @return string The search widget option name.
  85. */
  86. static function get_widget_option_name() {
  87. return sprintf( 'widget_%s', self::FILTER_WIDGET_BASE );
  88. }
  89. /**
  90. * Returns the search widget instances from the widget's option.
  91. *
  92. * @since 5.8.0
  93. *
  94. * @return array The widget options.
  95. */
  96. static function get_widgets_from_option() {
  97. $widget_options = get_option( self::get_widget_option_name(), array() );
  98. // We don't need this
  99. if ( ! empty( $widget_options ) && isset( $widget_options['_multiwidget'] ) ) {
  100. unset( $widget_options['_multiwidget'] );
  101. }
  102. return $widget_options;
  103. }
  104. /**
  105. * Returns the widget ID (widget base plus the numeric ID).
  106. *
  107. * @param int $number The widget's numeric ID.
  108. *
  109. * @return string The widget's numeric ID prefixed with the search widget base.
  110. */
  111. static function build_widget_id( $number ) {
  112. return sprintf( '%s-%d', self::FILTER_WIDGET_BASE, $number );
  113. }
  114. /**
  115. * Wrapper for is_active_widget() with the other parameters automatically supplied.
  116. *
  117. * @see is_active_widget()
  118. *
  119. * @since 5.8.0
  120. *
  121. * @param int $widget_id Widget ID.
  122. *
  123. * @return bool Whether the widget is active or not.
  124. */
  125. static function is_active_widget( $widget_id ) {
  126. return (bool) is_active_widget( false, $widget_id, self::FILTER_WIDGET_BASE, true );
  127. }
  128. /**
  129. * Returns an array of the filters from all active search widgets.
  130. *
  131. * @since 5.8.0
  132. *
  133. * @param array|null $allowed_widget_ids array of allowed widget IDs.
  134. *
  135. * @return array Active filters.
  136. */
  137. public static function get_filters_from_widgets( $allowed_widget_ids = null ) {
  138. $filters = array();
  139. $widget_options = self::get_widgets_from_option();
  140. if ( empty( $widget_options ) ) {
  141. return $filters;
  142. }
  143. foreach ( (array) $widget_options as $number => $settings ) {
  144. $widget_id = self::build_widget_id( $number );
  145. if ( ! self::is_active_widget( $widget_id ) || empty( $settings['filters'] ) ) {
  146. continue;
  147. }
  148. if ( isset( $allowed_widget_ids ) && ! in_array( $widget_id, $allowed_widget_ids, true ) ) {
  149. continue;
  150. }
  151. foreach ( (array) $settings['filters'] as $widget_filter ) {
  152. $widget_filter['widget_id'] = $widget_id;
  153. if ( empty( $widget_filter['name'] ) ) {
  154. $widget_filter['name'] = self::generate_widget_filter_name( $widget_filter );
  155. }
  156. $type = ( isset( $widget_filter['type'] ) ) ? $widget_filter['type'] : '';
  157. $key = sprintf( '%s_%d', $type, count( $filters ) );
  158. $filters[ $key ] = $widget_filter;
  159. }
  160. }
  161. return $filters;
  162. }
  163. /**
  164. * Get the localized default label for a date filter.
  165. *
  166. * @since 5.8.0
  167. *
  168. * @param string $type Date type, either year or month.
  169. * @param bool $is_updated Whether the filter was updated or not (adds "Updated" to the end).
  170. *
  171. * @return string The filter label.
  172. */
  173. static function get_date_filter_type_name( $type, $is_updated = false ) {
  174. switch ( $type ) {
  175. case 'year':
  176. $string = ( $is_updated )
  177. ? esc_html_x( 'Year Updated', 'label for filtering posts', 'jetpack' )
  178. : esc_html_x( 'Year', 'label for filtering posts', 'jetpack' );
  179. break;
  180. case 'month':
  181. default:
  182. $string = ( $is_updated )
  183. ? esc_html_x( 'Month Updated', 'label for filtering posts', 'jetpack' )
  184. : esc_html_x( 'Month', 'label for filtering posts', 'jetpack' );
  185. break;
  186. }
  187. return $string;
  188. }
  189. /**
  190. * Creates a default name for a filter. Used when the filter label is blank.
  191. *
  192. * @since 5.8.0
  193. *
  194. * @param array $widget_filter The filter to generate the title for.
  195. *
  196. * @return string The suggested filter name.
  197. */
  198. static function generate_widget_filter_name( $widget_filter ) {
  199. $name = '';
  200. if ( ! isset( $widget_filter['type'] ) ) {
  201. return $name;
  202. }
  203. switch ( $widget_filter['type'] ) {
  204. case 'post_type':
  205. $name = _x( 'Post Types', 'label for filtering posts', 'jetpack' );
  206. break;
  207. case 'date_histogram':
  208. $modified_fields = array(
  209. 'post_modified',
  210. 'post_modified_gmt',
  211. );
  212. switch ( $widget_filter['interval'] ) {
  213. case 'year':
  214. $name = self::get_date_filter_type_name(
  215. 'year',
  216. in_array( $widget_filter['field'], $modified_fields )
  217. );
  218. break;
  219. case 'month':
  220. default:
  221. $name = self::get_date_filter_type_name(
  222. 'month',
  223. in_array( $widget_filter['field'], $modified_fields )
  224. );
  225. break;
  226. }
  227. break;
  228. case 'taxonomy':
  229. $tax = get_taxonomy( $widget_filter['taxonomy'] );
  230. if ( ! $tax ) {
  231. break;
  232. }
  233. if ( isset( $tax->label ) ) {
  234. $name = $tax->label;
  235. } elseif ( isset( $tax->labels ) && isset( $tax->labels->name ) ) {
  236. $name = $tax->labels->name;
  237. }
  238. break;
  239. }
  240. return $name;
  241. }
  242. /**
  243. * Whether we should rerun a search in the customizer preview or not.
  244. *
  245. * @since 5.8.0
  246. *
  247. * @return bool
  248. */
  249. static function should_rerun_search_in_customizer_preview() {
  250. // Only update when in a customizer preview and data is being posted.
  251. // Check for $_POST removes an extra update when the customizer loads.
  252. //
  253. // Note: We use $GLOBALS['wp_customize'] here instead of is_customize_preview() to support unit tests.
  254. if ( ! isset( $GLOBALS['wp_customize'] ) || ! $GLOBALS['wp_customize']->is_preview() || empty( $_POST ) ) {
  255. return false;
  256. }
  257. return true;
  258. }
  259. /**
  260. * Since PHP's built-in array_diff() works by comparing the values that are in array 1 to the other arrays,
  261. * if there are less values in array 1, it's possible to get an empty diff where one might be expected.
  262. *
  263. * @since 5.8.0
  264. *
  265. * @param array $array_1
  266. * @param array $array_2
  267. *
  268. * @return array
  269. */
  270. static function array_diff( $array_1, $array_2 ) {
  271. // If the array counts are the same, then the order doesn't matter. If the count of
  272. // $array_1 is higher than $array_2, that's also fine. If the count of $array_2 is higher,
  273. // we need to swap the array order though.
  274. if ( count( $array_1 ) !== count( $array_2 ) && count( $array_2 ) > count( $array_1 ) ) {
  275. $temp = $array_1;
  276. $array_1 = $array_2;
  277. $array_2 = $temp;
  278. }
  279. // Disregard keys
  280. return array_values( array_diff( $array_1, $array_2 ) );
  281. }
  282. /**
  283. * Given the widget instance, will return true when selected post types differ from searchable post types.
  284. *
  285. * @since 5.8.0
  286. *
  287. * @param array $post_types An array of post types.
  288. *
  289. * @return bool
  290. */
  291. static function post_types_differ_searchable( $post_types ) {
  292. if ( empty( $post_types ) ) {
  293. return false;
  294. }
  295. $searchable_post_types = get_post_types( array( 'exclude_from_search' => false ) );
  296. $diff_of_searchable = self::array_diff( $searchable_post_types, (array) $post_types );
  297. return ! empty( $diff_of_searchable );
  298. }
  299. /**
  300. * Given the array of post types, will return true when these differ from the current search query.
  301. *
  302. * @since 5.8.0
  303. *
  304. * @param array $post_types An array of post types.
  305. *
  306. * @return bool
  307. */
  308. static function post_types_differ_query( $post_types ) {
  309. if ( empty( $post_types ) ) {
  310. return false;
  311. }
  312. if ( empty( $_GET['post_type'] ) ) {
  313. $post_types_from_query = array();
  314. } elseif ( is_array( $_GET['post_type'] ) ) {
  315. $post_types_from_query = $_GET['post_type'];
  316. } else {
  317. $post_types_from_query = (array) explode( ',', $_GET['post_type'] );
  318. }
  319. $post_types_from_query = array_map( 'trim', $post_types_from_query );
  320. $diff_query = self::array_diff( (array) $post_types, $post_types_from_query );
  321. return ! empty( $diff_query );
  322. }
  323. /**
  324. * Determine what Tracks value should be used when updating a widget.
  325. *
  326. * @since 5.8.0
  327. *
  328. * @param mixed $old_value The old option value.
  329. * @param mixed $new_value The new option value.
  330. *
  331. * @return array|false False if the widget wasn't updated, otherwise an array of the Tracks action and widget properties.
  332. */
  333. static function get_widget_tracks_value( $old_value, $new_value ) {
  334. $old_value = (array) $old_value;
  335. if ( isset( $old_value['_multiwidget'] ) ) {
  336. unset( $old_value['_multiwidget'] );
  337. }
  338. $new_value = (array) $new_value;
  339. if ( isset( $new_value['_multiwidget'] ) ) {
  340. unset( $new_value['_multiwidget'] );
  341. }
  342. $old_keys = array_keys( $old_value );
  343. $new_keys = array_keys( $new_value );
  344. if ( count( $new_keys ) > count( $old_keys ) ) { // This is the case for a widget being added
  345. $diff = self::array_diff( $new_keys, $old_keys );
  346. $action = 'widget_added';
  347. $widget = empty( $diff ) || ! isset( $new_value[ $diff[0] ] )
  348. ? false
  349. : $new_value[ $diff[0] ];
  350. } elseif ( count( $old_keys ) > count( $new_keys ) ) { // This is the case for a widget being deleted
  351. $diff = self::array_diff( $old_keys, $new_keys );
  352. $action = 'widget_deleted';
  353. $widget = empty( $diff ) || ! isset( $old_value[ $diff[0] ] )
  354. ? false
  355. : $old_value[ $diff[0] ];
  356. } else {
  357. $action = 'widget_updated';
  358. $widget = false;
  359. // This is a bit crazy. Since there can be multiple widgets stored in a single option,
  360. // we need to diff the old and new values to figure out which widget was updated.
  361. foreach ( $new_value as $key => $new_instance ) {
  362. if ( ! isset( $old_value[ $key ] ) ) {
  363. continue;
  364. }
  365. $old_instance = $old_value[ $key ];
  366. // First, let's test the keys of each instance
  367. $diff = self::array_diff( array_keys( $new_instance ), array_keys( $old_instance ) );
  368. if ( ! empty( $diff ) ) {
  369. $widget = $new_instance;
  370. break;
  371. }
  372. // Next, lets's loop over each value and compare it
  373. foreach ( $new_instance as $k => $v ) {
  374. if ( is_scalar( $v ) && (string) $v !== (string) $old_instance[ $k ] ) {
  375. $widget = $new_instance;
  376. break;
  377. }
  378. if ( 'filters' == $k ) {
  379. if ( count( $new_instance['filters'] ) != count( $old_instance['filters'] ) ) {
  380. $widget = $new_instance;
  381. break;
  382. }
  383. foreach ( $v as $filter_key => $new_filter_value ) {
  384. $diff = self::array_diff( $new_filter_value, $old_instance['filters'][ $filter_key ] );
  385. if ( ! empty( $diff ) ) {
  386. $widget = $new_instance;
  387. break;
  388. }
  389. }
  390. }
  391. }
  392. }
  393. }
  394. if ( empty( $action ) || empty( $widget ) ) {
  395. return false;
  396. }
  397. return array(
  398. 'action' => $action,
  399. 'widget' => self::get_widget_properties_for_tracks( $widget ),
  400. );
  401. }
  402. /**
  403. * Creates the widget properties for sending to Tracks.
  404. *
  405. * @since 5.8.0
  406. *
  407. * @param array $widget The widget instance.
  408. *
  409. * @return array The widget properties.
  410. */
  411. static function get_widget_properties_for_tracks( $widget ) {
  412. $sanitized = array();
  413. foreach ( (array) $widget as $key => $value ) {
  414. if ( '_multiwidget' == $key ) {
  415. continue;
  416. }
  417. if ( is_scalar( $value ) ) {
  418. $key = str_replace( '-', '_', sanitize_key( $key ) );
  419. $key = "widget_{$key}";
  420. $sanitized[ $key ] = $value;
  421. }
  422. }
  423. $filters_properties = ! empty( $widget['filters'] )
  424. ? self::get_filter_properties_for_tracks( $widget['filters'] )
  425. : array();
  426. return array_merge( $sanitized, $filters_properties );
  427. }
  428. /**
  429. * Creates the filter properties for sending to Tracks.
  430. *
  431. * @since 5.8.0
  432. *
  433. * @param array $filters An array of filters.
  434. *
  435. * @return array The filter properties.
  436. */
  437. static function get_filter_properties_for_tracks( $filters ) {
  438. if ( empty( $filters ) ) {
  439. return $filters;
  440. }
  441. $filters_properties = array(
  442. 'widget_filter_count' => count( $filters ),
  443. );
  444. foreach ( $filters as $filter ) {
  445. if ( empty( $filter['type'] ) ) {
  446. continue;
  447. }
  448. $key = sprintf( 'widget_filter_type_%s', $filter['type'] );
  449. if ( isset( $filters_properties[ $key ] ) ) {
  450. $filters_properties[ $key ] ++;
  451. } else {
  452. $filters_properties[ $key ] = 1;
  453. }
  454. }
  455. return $filters_properties;
  456. }
  457. /**
  458. * Gets the active post types given a set of filters.
  459. *
  460. * @since 5.8.0
  461. *
  462. * @param array $filters The active filters for the current query.
  463. *
  464. * @return array The active post types.
  465. */
  466. public static function get_active_post_types( $filters ) {
  467. $active_post_types = array();
  468. foreach ( $filters as $item ) {
  469. if ( ( 'post_type' == $item['type'] ) && isset( $item['query_vars']['post_type'] ) ) {
  470. $active_post_types[] = $item['query_vars']['post_type'];
  471. }
  472. }
  473. return $active_post_types;
  474. }
  475. /**
  476. * Sets active to false on all post type buckets.
  477. *
  478. * @since 5.8.0
  479. *
  480. * @param array $filters The available filters for the current query.
  481. *
  482. * @return array The filters for the current query with modified active field.
  483. */
  484. public static function remove_active_from_post_type_buckets( $filters ) {
  485. $modified = $filters;
  486. foreach ( $filters as $key => $filter ) {
  487. if ( 'post_type' === $filter['type'] && ! empty( $filter['buckets'] ) ) {
  488. foreach ( $filter['buckets'] as $k => $bucket ) {
  489. $bucket['active'] = false;
  490. $modified[ $key ]['buckets'][ $k ] = $bucket;
  491. }
  492. }
  493. }
  494. return $modified;
  495. }
  496. /**
  497. * Given a url and an array of post types, will ensure that the post types are properly applied to the URL as args.
  498. *
  499. * @since 5.8.0
  500. *
  501. * @param string $url The URL to add post types to.
  502. * @param array $post_types An array of post types that should be added to the URL.
  503. *
  504. * @return string The URL with added post types.
  505. */
  506. public static function add_post_types_to_url( $url, $post_types ) {
  507. $url = Jetpack_Search_Helpers::remove_query_arg( 'post_type', $url );
  508. if ( empty( $post_types ) ) {
  509. return $url;
  510. }
  511. $url = Jetpack_Search_Helpers::add_query_arg(
  512. 'post_type',
  513. implode( ',', $post_types ),
  514. $url
  515. );
  516. return $url;
  517. }
  518. /**
  519. * Since we provide support for the widget restricting post types by adding the selected post types as
  520. * active filters, if removing a post type filter would result in there no longer be post_type args in the URL,
  521. * we need to be sure to add them back.
  522. *
  523. * @since 5.8.0
  524. *
  525. * @param array $filters An array of possible filters for the current query.
  526. * @param array $post_types The post types to ensure are on the link.
  527. *
  528. * @return array The updated array of filters with post typed added to the remove URLs.
  529. */
  530. public static function ensure_post_types_on_remove_url( $filters, $post_types ) {
  531. $modified = $filters;
  532. foreach ( (array) $filters as $filter_key => $filter ) {
  533. if ( 'post_type' !== $filter['type'] || empty( $filter['buckets'] ) ) {
  534. $modified[ $filter_key ] = $filter;
  535. continue;
  536. }
  537. foreach ( (array) $filter['buckets'] as $bucket_key => $bucket ) {
  538. if ( empty( $bucket['remove_url'] ) ) {
  539. continue;
  540. }
  541. $parsed = wp_parse_url( $bucket['remove_url'] );
  542. if ( ! $parsed ) {
  543. continue;
  544. }
  545. $query = array();
  546. if ( ! empty( $parsed['query'] ) ) {
  547. wp_parse_str( $parsed['query'], $query );
  548. }
  549. if ( empty( $query['post_type'] ) ) {
  550. $modified[ $filter_key ]['buckets'][ $bucket_key ]['remove_url'] = self::add_post_types_to_url(
  551. $bucket['remove_url'],
  552. $post_types
  553. );
  554. }
  555. }
  556. }
  557. return $modified;
  558. }
  559. /**
  560. * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
  561. * developers to disable filters supplied by the search widget. Useful if filters are
  562. * being defined at the code level.
  563. *
  564. * @since 5.8.0
  565. *
  566. * @return bool
  567. */
  568. public static function are_filters_by_widget_disabled() {
  569. /**
  570. * Allows developers to disable filters being set by widget, in favor of manually
  571. * setting filters via `Jetpack_Search::set_filters()`.
  572. *
  573. * @module search
  574. *
  575. * @since 5.7.0
  576. *
  577. * @param bool false
  578. */
  579. return apply_filters( 'jetpack_search_disable_widget_filters', false );
  580. }
  581. /**
  582. * Returns the maximum posts per page for a search query.
  583. *
  584. * @since 5.8.0
  585. *
  586. * @return int
  587. */
  588. public static function get_max_posts_per_page() {
  589. return Jetpack_Search_Options::site_has_vip_index() ? 1000 : 100;
  590. }
  591. /**
  592. * Returns the maximum offset for a search query.
  593. *
  594. * @since 5.8.0
  595. *
  596. * @return int
  597. */
  598. public static function get_max_offset() {
  599. return Jetpack_Search_Options::site_has_vip_index() ? 9000 : 1000;
  600. }
  601. /**
  602. * Returns the maximum offset for a search query.
  603. *
  604. * @since 8.4.0
  605. * @param string $locale A potentially valid locale string.
  606. *
  607. * @return bool
  608. */
  609. public static function is_valid_locale( $locale ) {
  610. if ( ! class_exists( 'GP_Locales' ) ) {
  611. if ( defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) && file_exists( JETPACK__GLOTPRESS_LOCALES_PATH ) ) {
  612. require JETPACK__GLOTPRESS_LOCALES_PATH;
  613. } else {
  614. // Assume locale to be valid if we can't check with GlotPress.
  615. return true;
  616. }
  617. }
  618. return false !== GP_Locales::by_field( 'wp_locale', $locale );
  619. }
  620. /**
  621. * Get the version number to use when loading the file. Allows us to bypass cache when developing.
  622. *
  623. * @since 8.6.0
  624. * @param string $file Path of the file we are looking for.
  625. * @return string $script_version Version number.
  626. */
  627. public static function get_asset_version( $file ) {
  628. return Jetpack::is_development_version() && file_exists( JETPACK__PLUGIN_DIR . $file )
  629. ? filemtime( JETPACK__PLUGIN_DIR . $file )
  630. : JETPACK__VERSION;
  631. }
  632. /**
  633. * Generates a customizer settings ID for a given post type.
  634. *
  635. * @since 8.8.0
  636. * @param object $post_type Post type object returned from get_post_types.
  637. * @return string $customizer_id Customizer setting ID.
  638. */
  639. public static function generate_post_type_customizer_id( $post_type ) {
  640. return Jetpack_Search_Options::OPTION_PREFIX . 'disable_post_type_' . $post_type->name;
  641. }
  642. /**
  643. * Generates an array of post types associated with their customizer IDs.
  644. *
  645. * @since 8.8.0
  646. * @return array $ids Post type => post type customizer ID object.
  647. */
  648. public static function generate_post_type_customizer_ids() {
  649. return array_map(
  650. array( 'self', 'generate_post_type_customizer_id' ),
  651. get_post_types( array( 'exclude_from_search' => false ), 'objects' )
  652. );
  653. }
  654. /**
  655. * Sanitizes a checkbox value for writing to the database.
  656. *
  657. * @since 8.9.0
  658. *
  659. * @param any $value from the customizer form.
  660. * @return string either '0' or '1'.
  661. */
  662. public static function sanitize_checkbox_value( $value ) {
  663. return true === $value ? '1' : '0';
  664. }
  665. /**
  666. * Sanitizes a checkbox value for rendering the Customizer.
  667. *
  668. * @since 8.9.0
  669. *
  670. * @param any $value from the database.
  671. * @return boolean
  672. */
  673. public static function sanitize_checkbox_value_for_js( $value ) {
  674. return '1' === $value;
  675. }
  676. /**
  677. * Passes all options to the JS app.
  678. */
  679. public static function generate_initial_javascript_state() {
  680. $widget_options = self::get_widgets_from_option();
  681. if ( is_array( $widget_options ) ) {
  682. $widget_options = end( $widget_options );
  683. }
  684. $overlay_widget_ids = is_active_sidebar( 'jetpack-instant-search-sidebar' ) ?
  685. wp_get_sidebars_widgets()['jetpack-instant-search-sidebar'] : array();
  686. $filters = self::get_filters_from_widgets();
  687. $widgets = array();
  688. $widgets_outside_overlay = array();
  689. foreach ( $filters as $key => &$filter ) {
  690. $filter['filter_id'] = $key;
  691. if ( in_array( $filter['widget_id'], $overlay_widget_ids, true ) ) {
  692. if ( ! isset( $widgets[ $filter['widget_id'] ] ) ) {
  693. $widgets[ $filter['widget_id'] ]['filters'] = array();
  694. $widgets[ $filter['widget_id'] ]['widget_id'] = $filter['widget_id'];
  695. }
  696. $widgets[ $filter['widget_id'] ]['filters'][] = $filter;
  697. } else {
  698. if ( ! isset( $widgets_outside_overlay[ $filter['widget_id'] ] ) ) {
  699. $widgets_outside_overlay[ $filter['widget_id'] ]['filters'] = array();
  700. $widgets_outside_overlay[ $filter['widget_id'] ]['widget_id'] = $filter['widget_id'];
  701. }
  702. $widgets_outside_overlay[ $filter['widget_id'] ]['filters'][] = $filter;
  703. }
  704. }
  705. unset( $filter );
  706. $has_non_search_widgets = false;
  707. foreach ( $overlay_widget_ids as $overlay_widget_id ) {
  708. if ( strpos( $overlay_widget_id, self::FILTER_WIDGET_BASE ) === false ) {
  709. $has_non_search_widgets = true;
  710. break;
  711. }
  712. }
  713. $post_type_objs = get_post_types( array( 'exclude_from_search' => false ), 'objects' );
  714. $post_type_labels = array();
  715. foreach ( $post_type_objs as $key => $obj ) {
  716. $post_type_labels[ $key ] = array(
  717. 'singular_name' => $obj->labels->singular_name,
  718. 'name' => $obj->labels->name,
  719. );
  720. }
  721. $prefix = Jetpack_Search_Options::OPTION_PREFIX;
  722. $posts_per_page = (int) get_option( 'posts_per_page' );
  723. if ( ( $posts_per_page > 20 ) || ( $posts_per_page <= 0 ) ) {
  724. $posts_per_page = 20;
  725. }
  726. $excluded_post_types = get_option( $prefix . 'excluded_post_types' ) ? explode( ',', get_option( $prefix . 'excluded_post_types', '' ) ) : array();
  727. $post_types = array_values(
  728. get_post_types(
  729. array(
  730. 'exclude_from_search' => false,
  731. 'public' => true,
  732. )
  733. )
  734. );
  735. $unexcluded_post_types = array_diff( $post_types, $excluded_post_types );
  736. // NOTE: If all post types are being excluded, ignore the option value.
  737. if ( count( $unexcluded_post_types ) === 0 ) {
  738. $excluded_post_types = array();
  739. }
  740. $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
  741. $is_private_site = '-1' === get_option( 'blog_public' );
  742. $is_jetpack_photon_enabled = method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'photon' );
  743. $options = array(
  744. 'overlayOptions' => array(
  745. 'colorTheme' => get_option( $prefix . 'color_theme', 'light' ),
  746. 'enableInfScroll' => get_option( $prefix . 'inf_scroll', '1' ) === '1',
  747. 'enableSort' => get_option( $prefix . 'enable_sort', '1' ) === '1',
  748. 'highlightColor' => get_option( $prefix . 'highlight_color', '#FFC' ),
  749. 'overlayTrigger' => get_option( $prefix . 'overlay_trigger', 'immediate' ),
  750. 'resultFormat' => get_option( $prefix . 'result_format', Jetpack_Search_Options::RESULT_FORMAT_MINIMAL ),
  751. 'showPoweredBy' => get_option( $prefix . 'show_powered_by', '1' ) === '1',
  752. // These options require kicking off a new search.
  753. 'defaultSort' => get_option( $prefix . 'default_sort', 'relevance' ),
  754. 'excludedPostTypes' => $excluded_post_types,
  755. ),
  756. // core config.
  757. 'homeUrl' => home_url(),
  758. 'locale' => str_replace( '_', '-', self::is_valid_locale( get_locale() ) ? get_locale() : 'en_US' ),
  759. 'postsPerPage' => $posts_per_page,
  760. 'siteId' => class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'get_option' ) ? Jetpack::get_option( 'id' ) : get_current_blog_id(),
  761. 'postTypes' => $post_type_labels,
  762. 'webpackPublicPath' => plugins_url( '_inc/build/instant-search/', JETPACK__PLUGIN_FILE ),
  763. 'isPhotonEnabled' => ( $is_wpcom || $is_jetpack_photon_enabled ) && ! $is_private_site,
  764. // config values related to private site support.
  765. 'apiRoot' => esc_url_raw( rest_url() ),
  766. 'apiNonce' => wp_create_nonce( 'wp_rest' ),
  767. 'isPrivateSite' => $is_private_site,
  768. 'isWpcom' => $is_wpcom,
  769. // widget info.
  770. 'hasOverlayWidgets' => count( $overlay_widget_ids ) > 0,
  771. 'widgets' => array_values( $widgets ),
  772. 'widgetsOutsideOverlay' => array_values( $widgets_outside_overlay ),
  773. 'hasNonSearchWidgets' => $has_non_search_widgets,
  774. );
  775. /**
  776. * Customize Instant Search Options.
  777. *
  778. * @module search
  779. *
  780. * @since 7.7.0
  781. *
  782. * @param array $options Array of parameters used in Instant Search queries.
  783. */
  784. return apply_filters( 'jetpack_instant_search_options', $options );
  785. }
  786. /**
  787. * Prints the Instant Search sidebar.
  788. */
  789. public static function print_instant_search_sidebar() {
  790. ?>
  791. <div class="jetpack-instant-search__widget-area" style="display: none">
  792. <?php if ( is_active_sidebar( 'jetpack-instant-search-sidebar' ) ) { ?>
  793. <?php dynamic_sidebar( 'jetpack-instant-search-sidebar' ); ?>
  794. <?php } ?>
  795. </div>
  796. <?php
  797. }
  798. }