Bez popisu

search.php 35KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058
  1. <?php
  2. /**
  3. * Jetpack Search: Jetpack_Search_Widget class
  4. *
  5. * @package Jetpack
  6. * @subpackage Jetpack Search
  7. * @since 5.0.0
  8. */
  9. use Automattic\Jetpack\Redirect;
  10. use Automattic\Jetpack\Status;
  11. use Automattic\Jetpack\Tracking;
  12. add_action( 'widgets_init', 'jetpack_search_widget_init' );
  13. function jetpack_search_widget_init() {
  14. if (
  15. ! Jetpack::is_connection_ready()
  16. || ( method_exists( 'Jetpack_Plan', 'supports' ) && ! Jetpack_Plan::supports( 'search' ) )
  17. ) {
  18. return;
  19. }
  20. require_once JETPACK__PLUGIN_DIR . 'modules/search/class.jetpack-search-helpers.php';
  21. require_once JETPACK__PLUGIN_DIR . 'modules/search/class-jetpack-search-options.php';
  22. register_widget( 'Jetpack_Search_Widget' );
  23. }
  24. /**
  25. * Provides a widget to show available/selected filters on searches.
  26. *
  27. * @since 5.0.0
  28. *
  29. * @see WP_Widget
  30. */
  31. class Jetpack_Search_Widget extends WP_Widget {
  32. /**
  33. * The Jetpack_Search instance.
  34. *
  35. * @since 5.7.0
  36. * @var Jetpack_Search
  37. */
  38. protected $jetpack_search;
  39. /**
  40. * Number of aggregations (filters) to show by default.
  41. *
  42. * @since 5.8.0
  43. * @var int
  44. */
  45. const DEFAULT_FILTER_COUNT = 5;
  46. /**
  47. * Default sort order for search results.
  48. *
  49. * @since 5.8.0
  50. * @var string
  51. */
  52. const DEFAULT_SORT = 'relevance_desc';
  53. /**
  54. * Jetpack_Search_Widget constructor.
  55. *
  56. * @since 5.0.0
  57. */
  58. public function __construct( $name = null ) {
  59. if ( empty( $name ) ) {
  60. $name = esc_html__( 'Search', 'jetpack' );
  61. }
  62. parent::__construct(
  63. Jetpack_Search_Helpers::FILTER_WIDGET_BASE,
  64. /** This filter is documented in modules/widgets/facebook-likebox.php */
  65. apply_filters( 'jetpack_widget_name', $name ),
  66. array(
  67. 'classname' => 'jetpack-filters widget_search',
  68. 'description' => __( 'Instant search and filtering to help visitors quickly find relevant answers and explore your site.', 'jetpack' ),
  69. )
  70. );
  71. if (
  72. Jetpack_Search_Helpers::is_active_widget( $this->id ) &&
  73. ! $this->is_search_active()
  74. ) {
  75. $this->activate_search();
  76. }
  77. if ( is_admin() ) {
  78. add_action( 'sidebar_admin_setup', array( $this, 'widget_admin_setup' ) );
  79. } else {
  80. add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
  81. }
  82. add_action( 'jetpack_search_render_filters_widget_title', array( 'Jetpack_Search_Template_Tags', 'render_widget_title' ), 10, 3 );
  83. if ( Jetpack_Search_Options::is_instant_enabled() ) {
  84. add_action( 'jetpack_search_render_filters', array( 'Jetpack_Search_Template_Tags', 'render_instant_filters' ), 10, 2 );
  85. } else {
  86. add_action( 'jetpack_search_render_filters', array( 'Jetpack_Search_Template_Tags', 'render_available_filters' ), 10, 2 );
  87. }
  88. }
  89. /**
  90. * Check whether search is currently active
  91. *
  92. * @since 6.3
  93. */
  94. public function is_search_active() {
  95. return Jetpack::is_module_active( 'search' );
  96. }
  97. /**
  98. * Activate search
  99. *
  100. * @since 6.3
  101. */
  102. public function activate_search() {
  103. Jetpack::activate_module( 'search', false, false );
  104. }
  105. /**
  106. * Enqueues the scripts and styles needed for the customizer.
  107. *
  108. * @since 5.7.0
  109. */
  110. public function widget_admin_setup() {
  111. wp_enqueue_style( 'widget-jetpack-search-filters', plugins_url( 'search/css/search-widget-admin-ui.css', __FILE__ ) );
  112. // Register jp-tracks and jp-tracks-functions.
  113. Tracking::register_tracks_functions_scripts();
  114. wp_register_script(
  115. 'jetpack-search-widget-admin',
  116. plugins_url( 'search/js/search-widget-admin.js', __FILE__ ),
  117. array( 'jquery', 'jquery-ui-sortable', 'jp-tracks-functions' ),
  118. JETPACK__VERSION
  119. );
  120. wp_localize_script(
  121. 'jetpack-search-widget-admin',
  122. 'jetpack_search_filter_admin',
  123. array(
  124. 'defaultFilterCount' => self::DEFAULT_FILTER_COUNT,
  125. 'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(),
  126. 'tracksEventData' => array(
  127. 'is_customizer' => (int) is_customize_preview(),
  128. ),
  129. 'i18n' => array(
  130. 'month' => Jetpack_Search_Helpers::get_date_filter_type_name( 'month', false ),
  131. 'year' => Jetpack_Search_Helpers::get_date_filter_type_name( 'year', false ),
  132. 'monthUpdated' => Jetpack_Search_Helpers::get_date_filter_type_name( 'month', true ),
  133. 'yearUpdated' => Jetpack_Search_Helpers::get_date_filter_type_name( 'year', true ),
  134. ),
  135. )
  136. );
  137. wp_enqueue_script( 'jetpack-search-widget-admin' );
  138. }
  139. /**
  140. * Enqueue scripts and styles for the frontend.
  141. *
  142. * @since 5.8.0
  143. */
  144. public function enqueue_frontend_scripts() {
  145. if ( ! is_active_widget( false, false, $this->id_base, true ) || Jetpack_Search_Options::is_instant_enabled() ) {
  146. return;
  147. }
  148. wp_enqueue_script(
  149. 'jetpack-search-widget',
  150. plugins_url( 'search/js/search-widget.js', __FILE__ ),
  151. array(),
  152. JETPACK__VERSION,
  153. true
  154. );
  155. wp_enqueue_style( 'jetpack-search-widget', plugins_url( 'search/css/search-widget-frontend.css', __FILE__ ) );
  156. }
  157. /**
  158. * Get the list of valid sort types/orders.
  159. *
  160. * @since 5.8.0
  161. *
  162. * @return array The sort orders.
  163. */
  164. private function get_sort_types() {
  165. return array(
  166. 'relevance|DESC' => is_admin() ? esc_html__( 'Relevance (recommended)', 'jetpack' ) : esc_html__( 'Relevance', 'jetpack' ),
  167. 'date|DESC' => esc_html__( 'Newest first', 'jetpack' ),
  168. 'date|ASC' => esc_html__( 'Oldest first', 'jetpack' ),
  169. );
  170. }
  171. /**
  172. * Callback for an array_filter() call in order to only get filters for the current widget.
  173. *
  174. * @see Jetpack_Search_Widget::widget()
  175. *
  176. * @since 5.7.0
  177. *
  178. * @param array $item Filter item.
  179. *
  180. * @return bool Whether the current filter item is for the current widget.
  181. */
  182. function is_for_current_widget( $item ) {
  183. return isset( $item['widget_id'] ) && $this->id == $item['widget_id'];
  184. }
  185. /**
  186. * This method returns a boolean for whether the widget should show site-wide filters for the site.
  187. *
  188. * This is meant to provide backwards-compatibility for VIP, and other professional plan users, that manually
  189. * configured filters via `Jetpack_Search::set_filters()`.
  190. *
  191. * @since 5.7.0
  192. *
  193. * @return bool Whether the widget should display site-wide filters or not.
  194. */
  195. public function should_display_sitewide_filters() {
  196. $filter_widgets = get_option( 'widget_jetpack-search-filters' );
  197. // This shouldn't be empty, but just for sanity
  198. if ( empty( $filter_widgets ) ) {
  199. return false;
  200. }
  201. // If any widget has any filters, return false
  202. foreach ( $filter_widgets as $number => $widget ) {
  203. $widget_id = sprintf( '%s-%d', $this->id_base, $number );
  204. if ( ! empty( $widget['filters'] ) && is_active_widget( false, $widget_id, $this->id_base ) ) {
  205. return false;
  206. }
  207. }
  208. return true;
  209. }
  210. public function jetpack_search_populate_defaults( $instance ) {
  211. $instance = wp_parse_args(
  212. (array) $instance,
  213. array(
  214. 'title' => '',
  215. 'search_box_enabled' => true,
  216. 'user_sort_enabled' => true,
  217. 'sort' => self::DEFAULT_SORT,
  218. 'filters' => array( array() ),
  219. 'post_types' => array(),
  220. )
  221. );
  222. return $instance;
  223. }
  224. /**
  225. * Populates the instance array with appropriate default values.
  226. *
  227. * @since 8.6.0
  228. * @param array $instance Previously saved values from database.
  229. * @return array Instance array with default values approprate for instant search
  230. */
  231. public function populate_defaults_for_instant_search( $instance ) {
  232. return wp_parse_args(
  233. (array) $instance,
  234. array(
  235. 'title' => '',
  236. 'filters' => array(),
  237. )
  238. );
  239. }
  240. /**
  241. * Responsible for rendering the widget on the frontend.
  242. *
  243. * @since 5.0.0
  244. *
  245. * @param array $args Widgets args supplied by the theme.
  246. * @param array $instance The current widget instance.
  247. */
  248. public function widget( $args, $instance ) {
  249. $instance = $this->jetpack_search_populate_defaults( $instance );
  250. if ( ( new Status() )->is_offline_mode() ) {
  251. echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  252. ?><div id="<?php echo esc_attr( $this->id ); ?>-wrapper">
  253. <div class="jetpack-search-sort-wrapper">
  254. <label>
  255. <?php esc_html_e( 'Jetpack Search not supported in Offline Mode', 'jetpack' ); ?>
  256. </label>
  257. </div>
  258. </div>
  259. <?php
  260. echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  261. return;
  262. }
  263. if ( Jetpack_Search_Options::is_instant_enabled() ) {
  264. if ( array_key_exists( 'id', $args ) && 'jetpack-instant-search-sidebar' === $args['id'] ) {
  265. $this->widget_empty_instant( $args, $instance );
  266. } else {
  267. $this->widget_instant( $args, $instance );
  268. }
  269. } else {
  270. $this->widget_non_instant( $args, $instance );
  271. }
  272. }
  273. /**
  274. * Render the non-instant frontend widget.
  275. *
  276. * @since 8.3.0
  277. *
  278. * @param array $args Widgets args supplied by the theme.
  279. * @param array $instance The current widget instance.
  280. */
  281. public function widget_non_instant( $args, $instance ) {
  282. $display_filters = false;
  283. if ( is_search() ) {
  284. if ( Jetpack_Search_Helpers::should_rerun_search_in_customizer_preview() ) {
  285. Jetpack_Search::instance()->update_search_results_aggregations();
  286. }
  287. $filters = Jetpack_Search::instance()->get_filters();
  288. if ( ! Jetpack_Search_Helpers::are_filters_by_widget_disabled() && ! $this->should_display_sitewide_filters() ) {
  289. $filters = array_filter( $filters, array( $this, 'is_for_current_widget' ) );
  290. }
  291. if ( ! empty( $filters ) ) {
  292. $display_filters = true;
  293. }
  294. }
  295. if ( ! $display_filters && empty( $instance['search_box_enabled'] ) && empty( $instance['user_sort_enabled'] ) ) {
  296. return;
  297. }
  298. $title = ! empty( $instance['title'] ) ? $instance['title'] : '';
  299. /** This filter is documented in core/src/wp-includes/default-widgets.php */
  300. $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
  301. echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  302. ?>
  303. <div id="<?php echo esc_attr( $this->id ); ?>-wrapper" >
  304. <?php
  305. if ( ! empty( $title ) ) {
  306. /**
  307. * Responsible for displaying the title of the Jetpack Search filters widget.
  308. *
  309. * @module search
  310. *
  311. * @since 5.7.0
  312. *
  313. * @param string $title The widget's title
  314. * @param string $args['before_title'] The HTML tag to display before the title
  315. * @param string $args['after_title'] The HTML tag to display after the title
  316. */
  317. do_action( 'jetpack_search_render_filters_widget_title', $title, $args['before_title'], $args['after_title'] );
  318. }
  319. $default_sort = isset( $instance['sort'] ) ? $instance['sort'] : self::DEFAULT_SORT;
  320. list( $orderby, $order ) = $this->sorting_to_wp_query_param( $default_sort );
  321. $current_sort = "{$orderby}|{$order}";
  322. // we need to dynamically inject the sort field into the search box when the search box is enabled, and display
  323. // it separately when it's not.
  324. if ( ! empty( $instance['search_box_enabled'] ) ) {
  325. Jetpack_Search_Template_Tags::render_widget_search_form( $instance['post_types'], $orderby, $order );
  326. }
  327. if ( ! empty( $instance['search_box_enabled'] ) && ! empty( $instance['user_sort_enabled'] ) ) :
  328. ?>
  329. <div class="jetpack-search-sort-wrapper">
  330. <label>
  331. <?php esc_html_e( 'Sort by', 'jetpack' ); ?>
  332. <select class="jetpack-search-sort">
  333. <?php foreach ( $this->get_sort_types() as $sort => $label ) { ?>
  334. <option value="<?php echo esc_attr( $sort ); ?>" <?php selected( $current_sort, $sort ); ?>>
  335. <?php echo esc_html( $label ); ?>
  336. </option>
  337. <?php } ?>
  338. </select>
  339. </label>
  340. </div>
  341. <?php
  342. endif;
  343. if ( $display_filters ) {
  344. /**
  345. * Responsible for rendering filters to narrow down search results.
  346. *
  347. * @module search
  348. *
  349. * @since 5.8.0
  350. *
  351. * @param array $filters The possible filters for the current query.
  352. * @param array $post_types An array of post types to limit filtering to.
  353. */
  354. do_action(
  355. 'jetpack_search_render_filters',
  356. $filters,
  357. isset( $instance['post_types'] ) ? $instance['post_types'] : null
  358. );
  359. }
  360. $this->maybe_render_sort_javascript( $instance, $order, $orderby );
  361. echo '</div>';
  362. echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  363. }
  364. /**
  365. * Render the instant frontend widget.
  366. *
  367. * @since 8.3.0
  368. *
  369. * @param array $args Widgets args supplied by the theme.
  370. * @param array $instance The current widget instance.
  371. */
  372. public function widget_instant( $args, $instance ) {
  373. if ( Jetpack_Search_Helpers::should_rerun_search_in_customizer_preview() ) {
  374. Jetpack_Search::instance()->update_search_results_aggregations();
  375. }
  376. $filters = Jetpack_Search::instance()->get_filters();
  377. if ( ! Jetpack_Search_Helpers::are_filters_by_widget_disabled() && ! $this->should_display_sitewide_filters() ) {
  378. $filters = array_filter( $filters, array( $this, 'is_for_current_widget' ) );
  379. }
  380. $display_filters = ! empty( $filters );
  381. $title = ! empty( $instance['title'] ) ? $instance['title'] : '';
  382. /** This filter is documented in core/src/wp-includes/default-widgets.php */
  383. $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
  384. echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  385. ?>
  386. <div id="<?php echo esc_attr( $this->id ); ?>-wrapper" class="jetpack-instant-search-wrapper">
  387. <?php
  388. if ( ! empty( $title ) ) {
  389. /**
  390. * Responsible for displaying the title of the Jetpack Search filters widget.
  391. *
  392. * @module search
  393. *
  394. * @since 5.7.0
  395. *
  396. * @param string $title The widget's title
  397. * @param string $args['before_title'] The HTML tag to display before the title
  398. * @param string $args['after_title'] The HTML tag to display after the title
  399. */
  400. do_action( 'jetpack_search_render_filters_widget_title', $title, $args['before_title'], $args['after_title'] );
  401. }
  402. Jetpack_Search_Template_Tags::render_widget_search_form( array(), '', '' );
  403. if ( $display_filters ) {
  404. /**
  405. * Responsible for rendering filters to narrow down search results.
  406. *
  407. * @module search
  408. *
  409. * @since 5.8.0
  410. *
  411. * @param array $filters The possible filters for the current query.
  412. * @param array $post_types An array of post types to limit filtering to.
  413. */
  414. do_action(
  415. 'jetpack_search_render_filters',
  416. $filters,
  417. null
  418. );
  419. }
  420. echo '</div>';
  421. echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  422. }
  423. /**
  424. * Render the instant widget for the overlay.
  425. *
  426. * @since 8.3.0
  427. *
  428. * @param array $args Widgets args supplied by the theme.
  429. * @param array $instance The current widget instance.
  430. */
  431. public function widget_empty_instant( $args, $instance ) {
  432. $title = isset( $instance['title'] ) ? $instance['title'] : '';
  433. if ( empty( $title ) ) {
  434. $title = '';
  435. }
  436. /** This filter is documented in core/src/wp-includes/default-widgets.php */
  437. $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
  438. echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  439. ?>
  440. <div id="<?php echo esc_attr( $this->id ); ?>-wrapper" class="jetpack-instant-search-wrapper">
  441. <?php
  442. if ( ! empty( $title ) ) {
  443. /**
  444. * Responsible for displaying the title of the Jetpack Search filters widget.
  445. *
  446. * @module search
  447. *
  448. * @since 5.7.0
  449. *
  450. * @param string $title The widget's title
  451. * @param string $args['before_title'] The HTML tag to display before the title
  452. * @param string $args['after_title'] The HTML tag to display after the title
  453. */
  454. do_action( 'jetpack_search_render_filters_widget_title', $title, $args['before_title'], $args['after_title'] );
  455. }
  456. echo '</div>';
  457. echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  458. }
  459. /**
  460. * Renders JavaScript for the sorting controls on the frontend.
  461. *
  462. * This JS is a bit complicated, but here's what it's trying to do:
  463. * - find the search form
  464. * - find the orderby/order fields and set default values
  465. * - detect changes to the sort field, if it exists, and use it to set the order field values
  466. *
  467. * @since 5.8.0
  468. *
  469. * @param array $instance The current widget instance.
  470. * @param string $order The order to initialize the select with.
  471. * @param string $orderby The orderby to initialize the select with.
  472. */
  473. private function maybe_render_sort_javascript( $instance, $order, $orderby ) {
  474. if ( Jetpack_Search_Options::is_instant_enabled() ) {
  475. return;
  476. }
  477. if ( ! empty( $instance['user_sort_enabled'] ) ) :
  478. ?>
  479. <script type="text/javascript">
  480. var jetpackSearchModuleSorting = function() {
  481. var orderByDefault = '<?php echo 'date' === $orderby ? 'date' : 'relevance'; ?>',
  482. orderDefault = '<?php echo 'ASC' === $order ? 'ASC' : 'DESC'; ?>',
  483. widgetId = decodeURIComponent( '<?php echo rawurlencode( $this->id ); ?>' ),
  484. searchQuery = decodeURIComponent( '<?php echo rawurlencode( get_query_var( 's', '' ) ); ?>' ),
  485. isSearch = <?php echo (int) is_search(); ?>;
  486. var container = document.getElementById( widgetId + '-wrapper' ),
  487. form = container.querySelector( '.jetpack-search-form form' ),
  488. orderBy = form.querySelector( 'input[name=orderby]' ),
  489. order = form.querySelector( 'input[name=order]' ),
  490. searchInput = form.querySelector( 'input[name="s"]' ),
  491. sortSelectInput = container.querySelector( '.jetpack-search-sort' );
  492. orderBy.value = orderByDefault;
  493. order.value = orderDefault;
  494. // Some themes don't set the search query, which results in the query being lost
  495. // when doing a sort selection. So, if the query isn't set, let's set it now. This approach
  496. // is chosen over running a regex over HTML for every search query performed.
  497. if ( isSearch && ! searchInput.value ) {
  498. searchInput.value = searchQuery;
  499. }
  500. searchInput.classList.add( 'show-placeholder' );
  501. sortSelectInput.addEventListener( 'change', function( event ) {
  502. var values = event.target.value.split( '|' );
  503. orderBy.value = values[0];
  504. order.value = values[1];
  505. form.submit();
  506. } );
  507. }
  508. if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
  509. jetpackSearchModuleSorting();
  510. } else {
  511. document.addEventListener( 'DOMContentLoaded', jetpackSearchModuleSorting );
  512. }
  513. </script>
  514. <?php
  515. endif;
  516. }
  517. /**
  518. * Convert a sort string into the separate order by and order parts.
  519. *
  520. * @since 5.8.0
  521. *
  522. * @param string $sort A sort string.
  523. *
  524. * @return array Order by and order.
  525. */
  526. private function sorting_to_wp_query_param( $sort ) {
  527. $parts = explode( '|', $sort );
  528. $orderby = isset( $_GET['orderby'] )
  529. ? $_GET['orderby']
  530. : $parts[0];
  531. $order = isset( $_GET['order'] )
  532. ? strtoupper( $_GET['order'] )
  533. : ( ( isset( $parts[1] ) && 'ASC' === strtoupper( $parts[1] ) ) ? 'ASC' : 'DESC' );
  534. return array( $orderby, $order );
  535. }
  536. /**
  537. * Updates a particular instance of the widget. Validates and sanitizes the options.
  538. *
  539. * @since 5.0.0
  540. *
  541. * @param array $new_instance New settings for this instance as input by the user via Jetpack_Search_Widget::form().
  542. * @param array $old_instance Old settings for this instance.
  543. *
  544. * @return array Settings to save.
  545. */
  546. public function update( $new_instance, $old_instance ) {
  547. $new_instance = $this->maybe_reformat_widget( $new_instance );
  548. $instance = array();
  549. $instance['title'] = sanitize_text_field( $new_instance['title'] );
  550. $instance['search_box_enabled'] = empty( $new_instance['search_box_enabled'] ) ? '0' : '1';
  551. $instance['user_sort_enabled'] = empty( $new_instance['user_sort_enabled'] ) ? '0' : '1';
  552. $instance['sort'] = $new_instance['sort'];
  553. $instance['post_types'] = empty( $new_instance['post_types'] ) || empty( $instance['search_box_enabled'] )
  554. ? array()
  555. : array_map( 'sanitize_key', $new_instance['post_types'] );
  556. $filters = array();
  557. if ( isset( $new_instance['filter_type'] ) ) {
  558. foreach ( (array) $new_instance['filter_type'] as $index => $type ) {
  559. $count = (int) $new_instance['num_filters'][ $index ];
  560. $count = min( 50, $count ); // Set max boundary at 50.
  561. $count = max( 1, $count ); // Set min boundary at 1.
  562. switch ( $type ) {
  563. case 'taxonomy':
  564. $filters[] = array(
  565. 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ),
  566. 'type' => 'taxonomy',
  567. 'taxonomy' => sanitize_key( $new_instance['taxonomy_type'][ $index ] ),
  568. 'count' => $count,
  569. );
  570. break;
  571. case 'post_type':
  572. $filters[] = array(
  573. 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ),
  574. 'type' => 'post_type',
  575. 'count' => $count,
  576. );
  577. break;
  578. case 'date_histogram':
  579. $filters[] = array(
  580. 'name' => sanitize_text_field( $new_instance['filter_name'][ $index ] ),
  581. 'type' => 'date_histogram',
  582. 'count' => $count,
  583. 'field' => sanitize_key( $new_instance['date_histogram_field'][ $index ] ),
  584. 'interval' => sanitize_key( $new_instance['date_histogram_interval'][ $index ] ),
  585. );
  586. break;
  587. }
  588. }
  589. }
  590. if ( ! empty( $filters ) ) {
  591. $instance['filters'] = $filters;
  592. }
  593. return $instance;
  594. }
  595. /**
  596. * Reformats the widget instance array to one that is recognized by the `update` function.
  597. * This is only necessary when handling changes from the block-based widget editor.
  598. *
  599. * @param array $widget_instance - Jetpack Search widget instance.
  600. *
  601. * @return array - Potentially reformatted instance compatible with the save function.
  602. */
  603. protected function maybe_reformat_widget( $widget_instance ) {
  604. if ( isset( $widget_instance['filter_type'] ) || ! isset( $widget_instance['filters'] ) || ! is_array( $widget_instance['filters'] ) ) {
  605. return $widget_instance;
  606. }
  607. $instance = $widget_instance;
  608. foreach ( $widget_instance['filters'] as $filter ) {
  609. $instance['filter_type'][] = isset( $filter['type'] ) ? $filter['type'] : '';
  610. $instance['taxonomy_type'][] = isset( $filter['taxonomy'] ) ? $filter['taxonomy'] : '';
  611. $instance['filter_name'][] = isset( $filter['name'] ) ? $filter['name'] : '';
  612. $instance['num_filters'][] = isset( $filter['count'] ) ? $filter['count'] : 5;
  613. $instance['date_histogram_field'][] = isset( $filter['field'] ) ? $filter['field'] : '';
  614. $instance['date_histogram_interval'][] = isset( $filter['interval'] ) ? $filter['interval'] : '';
  615. }
  616. unset( $instance['filters'] );
  617. return $instance;
  618. }
  619. /**
  620. * Outputs the settings update form.
  621. *
  622. * @since 5.0.0
  623. *
  624. * @param array $instance Previously saved values from database.
  625. */
  626. public function form( $instance ) {
  627. if ( Jetpack_Search_Options::is_instant_enabled() ) {
  628. return $this->form_for_instant_search( $instance );
  629. }
  630. $instance = $this->jetpack_search_populate_defaults( $instance );
  631. $title = strip_tags( $instance['title'] );
  632. $hide_filters = Jetpack_Search_Helpers::are_filters_by_widget_disabled();
  633. $classes = sprintf(
  634. 'jetpack-search-filters-widget %s %s %s',
  635. $hide_filters ? 'hide-filters' : '',
  636. $instance['search_box_enabled'] ? '' : 'hide-post-types',
  637. $this->id
  638. );
  639. ?>
  640. <div class="<?php echo esc_attr( $classes ); ?>">
  641. <p>
  642. <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
  643. <?php esc_html_e( 'Title (optional):', 'jetpack' ); ?>
  644. </label>
  645. <input
  646. class="widefat"
  647. id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
  648. name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
  649. type="text"
  650. value="<?php echo esc_attr( $title ); ?>"
  651. />
  652. </p>
  653. <p>
  654. <label>
  655. <input
  656. type="checkbox"
  657. class="jetpack-search-filters-widget__search-box-enabled"
  658. name="<?php echo esc_attr( $this->get_field_name( 'search_box_enabled' ) ); ?>"
  659. <?php checked( $instance['search_box_enabled'] ); ?>
  660. />
  661. <?php esc_html_e( 'Show search box', 'jetpack' ); ?>
  662. </label>
  663. </p>
  664. <p>
  665. <label>
  666. <input
  667. type="checkbox"
  668. class="jetpack-search-filters-widget__sort-controls-enabled"
  669. name="<?php echo esc_attr( $this->get_field_name( 'user_sort_enabled' ) ); ?>"
  670. <?php checked( $instance['user_sort_enabled'] ); ?>
  671. <?php disabled( ! $instance['search_box_enabled'] ); ?>
  672. />
  673. <?php esc_html_e( 'Show sort selection dropdown', 'jetpack' ); ?>
  674. </label>
  675. </p>
  676. <p class="jetpack-search-filters-widget__post-types-select">
  677. <label><?php esc_html_e( 'Post types to search (minimum of 1):', 'jetpack' ); ?></label>
  678. <?php foreach ( get_post_types( array( 'exclude_from_search' => false ), 'objects' ) as $post_type ) : ?>
  679. <label>
  680. <input
  681. type="checkbox"
  682. value="<?php echo esc_attr( $post_type->name ); ?>"
  683. name="<?php echo esc_attr( $this->get_field_name( 'post_types' ) ); ?>[]"
  684. <?php checked( empty( $instance['post_types'] ) || in_array( $post_type->name, $instance['post_types'] ) ); ?>
  685. />&nbsp;
  686. <?php echo esc_html( $post_type->label ); ?>
  687. </label>
  688. <?php endforeach; ?>
  689. </p>
  690. <p>
  691. <label>
  692. <?php esc_html_e( 'Default sort order:', 'jetpack' ); ?>
  693. <select
  694. name="<?php echo esc_attr( $this->get_field_name( 'sort' ) ); ?>"
  695. class="widefat jetpack-search-filters-widget__sort-order">
  696. <?php foreach ( $this->get_sort_types() as $sort_type => $label ) { ?>
  697. <option value="<?php echo esc_attr( $sort_type ); ?>" <?php selected( $instance['sort'], $sort_type ); ?>>
  698. <?php echo esc_html( $label ); ?>
  699. </option>
  700. <?php } ?>
  701. </select>
  702. </label>
  703. </p>
  704. <?php if ( ! $hide_filters ) : ?>
  705. <script class="jetpack-search-filters-widget__filter-template" type="text/template">
  706. <?php echo $this->render_widget_edit_filter( array(), true ); ?>
  707. </script>
  708. <div class="jetpack-search-filters-widget__filters">
  709. <?php foreach ( (array) $instance['filters'] as $filter ) : ?>
  710. <?php $this->render_widget_edit_filter( $filter ); ?>
  711. <?php endforeach; ?>
  712. </div>
  713. <p class="jetpack-search-filters-widget__add-filter-wrapper">
  714. <a class="button jetpack-search-filters-widget__add-filter" href="#">
  715. <?php esc_html_e( 'Add a filter', 'jetpack' ); ?>
  716. </a>
  717. </p>
  718. <noscript>
  719. <p class="jetpack-search-filters-help">
  720. <?php echo esc_html_e( 'Adding filters requires JavaScript!', 'jetpack' ); ?>
  721. </p>
  722. </noscript>
  723. <?php if ( is_customize_preview() ) : ?>
  724. <p class="jetpack-search-filters-help">
  725. <a href="<?php echo esc_url( Redirect::get_url( 'jetpack-support-search', array( 'anchor' => 'filters-not-showing-up' ) ) ); ?>" target="_blank">
  726. <?php esc_html_e( "Why aren't my filters appearing?", 'jetpack' ); ?>
  727. </a>
  728. </p>
  729. <?php endif; ?>
  730. <?php endif; ?>
  731. </div>
  732. <?php
  733. }
  734. /**
  735. * Outputs the widget update form to be used in the Customizer for Instant Search.
  736. *
  737. * @since 8.6.0
  738. *
  739. * @param array $instance Previously saved values from database.
  740. */
  741. private function form_for_instant_search( $instance ) {
  742. $instance = $this->populate_defaults_for_instant_search( $instance );
  743. $classes = sprintf( 'jetpack-search-filters-widget %s', $this->id );
  744. ?>
  745. <div class="<?php echo esc_attr( $classes ); ?>">
  746. <!-- Title control -->
  747. <p>
  748. <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
  749. <?php esc_html_e( 'Title (optional):', 'jetpack' ); ?>
  750. </label>
  751. <input
  752. class="widefat"
  753. id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
  754. name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
  755. type="text"
  756. value="<?php echo esc_attr( wp_strip_all_tags( $instance['title'] ) ); ?>"
  757. />
  758. </p>
  759. <!-- Filters control -->
  760. <?php if ( ! Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) : ?>
  761. <div class="jetpack-search-filters-widget__filters">
  762. <?php foreach ( (array) $instance['filters'] as $filter ) : ?>
  763. <?php $this->render_widget_edit_filter( $filter ); ?>
  764. <?php endforeach; ?>
  765. </div>
  766. <p class="jetpack-search-filters-widget__add-filter-wrapper">
  767. <a class="button jetpack-search-filters-widget__add-filter" href="#">
  768. <?php esc_html_e( 'Add a filter', 'jetpack' ); ?>
  769. </a>
  770. </p>
  771. <script class="jetpack-search-filters-widget__filter-template" type="text/template">
  772. <?php $this->render_widget_edit_filter( array(), true ); ?>
  773. </script>
  774. <noscript>
  775. <p class="jetpack-search-filters-help">
  776. <?php echo esc_html_e( 'Adding filters requires JavaScript!', 'jetpack' ); ?>
  777. </p>
  778. </noscript>
  779. <?php endif; ?>
  780. </div>
  781. <?php
  782. }
  783. /**
  784. * We need to render HTML in two formats: an Underscore template (client-side)
  785. * and native PHP (server-side). This helper function allows for easy rendering
  786. * of attributes in both formats.
  787. *
  788. * @since 5.8.0
  789. *
  790. * @param string $name Attribute name.
  791. * @param string $value Attribute value.
  792. * @param bool $is_template Whether this is for an Underscore template or not.
  793. */
  794. private function render_widget_attr( $name, $value, $is_template ) {
  795. echo $is_template ? "<%= $name %>" : esc_attr( $value );
  796. }
  797. /**
  798. * We need to render HTML in two formats: an Underscore template (client-size)
  799. * and native PHP (server-side). This helper function allows for easy rendering
  800. * of the "selected" attribute in both formats.
  801. *
  802. * @since 5.8.0
  803. *
  804. * @param string $name Attribute name.
  805. * @param string $value Attribute value.
  806. * @param string $compare Value to compare to the attribute value to decide if it should be selected.
  807. * @param bool $is_template Whether this is for an Underscore template or not.
  808. */
  809. private function render_widget_option_selected( $name, $value, $compare, $is_template ) {
  810. $compare_js = rawurlencode( $compare );
  811. echo $is_template ? "<%= decodeURIComponent( '$compare_js' ) === $name ? 'selected=\"selected\"' : '' %>" : selected( $value, $compare );
  812. }
  813. /**
  814. * Responsible for rendering a single filter in the customizer or the widget administration screen in wp-admin.
  815. *
  816. * We use this method for two purposes - rendering the fields server-side, and also rendering a script template for Underscore.
  817. *
  818. * @since 5.7.0
  819. *
  820. * @param array $filter The filter to render.
  821. * @param bool $is_template Whether this is for an Underscore template or not.
  822. */
  823. public function render_widget_edit_filter( $filter, $is_template = false ) {
  824. $args = wp_parse_args(
  825. $filter,
  826. array(
  827. 'name' => '',
  828. 'type' => 'taxonomy',
  829. 'taxonomy' => '',
  830. 'post_type' => '',
  831. 'field' => '',
  832. 'interval' => '',
  833. 'count' => self::DEFAULT_FILTER_COUNT,
  834. )
  835. );
  836. $args['name_placeholder'] = Jetpack_Search_Helpers::generate_widget_filter_name( $args );
  837. ?>
  838. <div class="jetpack-search-filters-widget__filter is-<?php $this->render_widget_attr( 'type', $args['type'], $is_template ); ?>">
  839. <p class="jetpack-search-filters-widget__type-select">
  840. <label>
  841. <?php esc_html_e( 'Filter Type:', 'jetpack' ); ?>
  842. <select name="<?php echo esc_attr( $this->get_field_name( 'filter_type' ) ); ?>[]" class="widefat filter-select">
  843. <option value="taxonomy" <?php $this->render_widget_option_selected( 'type', $args['type'], 'taxonomy', $is_template ); ?>>
  844. <?php esc_html_e( 'Taxonomy', 'jetpack' ); ?>
  845. </option>
  846. <option value="post_type" <?php $this->render_widget_option_selected( 'type', $args['type'], 'post_type', $is_template ); ?>>
  847. <?php esc_html_e( 'Post Type', 'jetpack' ); ?>
  848. </option>
  849. <option value="date_histogram" <?php $this->render_widget_option_selected( 'type', $args['type'], 'date_histogram', $is_template ); ?>>
  850. <?php esc_html_e( 'Date', 'jetpack' ); ?>
  851. </option>
  852. </select>
  853. </label>
  854. </p>
  855. <p class="jetpack-search-filters-widget__taxonomy-select">
  856. <label>
  857. <?php
  858. esc_html_e( 'Choose a taxonomy:', 'jetpack' );
  859. $seen_taxonomy_labels = array();
  860. ?>
  861. <select name="<?php echo esc_attr( $this->get_field_name( 'taxonomy_type' ) ); ?>[]" class="widefat taxonomy-select">
  862. <?php foreach ( get_taxonomies( array( 'public' => true ), 'objects' ) as $taxonomy ) : ?>
  863. <option value="<?php echo esc_attr( $taxonomy->name ); ?>" <?php $this->render_widget_option_selected( 'taxonomy', $args['taxonomy'], $taxonomy->name, $is_template ); ?>>
  864. <?php
  865. $label = in_array( $taxonomy->label, $seen_taxonomy_labels )
  866. ? sprintf(
  867. /* translators: %1$s is the taxonomy name, %2s is the name of its type to help distinguish between several taxonomies with the same name, e.g. category and tag. */
  868. _x( '%1$s (%2$s)', 'A label for a taxonomy selector option', 'jetpack' ),
  869. $taxonomy->label,
  870. $taxonomy->name
  871. )
  872. : $taxonomy->label;
  873. echo esc_html( $label );
  874. $seen_taxonomy_labels[] = $taxonomy->label;
  875. ?>
  876. </option>
  877. <?php endforeach; ?>
  878. </select>
  879. </label>
  880. </p>
  881. <p class="jetpack-search-filters-widget__date-histogram-select">
  882. <label>
  883. <?php esc_html_e( 'Choose a field:', 'jetpack' ); ?>
  884. <select name="<?php echo esc_attr( $this->get_field_name( 'date_histogram_field' ) ); ?>[]" class="widefat date-field-select">
  885. <option value="post_date" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_date', $is_template ); ?>>
  886. <?php esc_html_e( 'Date', 'jetpack' ); ?>
  887. </option>
  888. <option value="post_date_gmt" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_date_gmt', $is_template ); ?>>
  889. <?php esc_html_e( 'Date GMT', 'jetpack' ); ?>
  890. </option>
  891. <option value="post_modified" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_modified', $is_template ); ?>>
  892. <?php esc_html_e( 'Modified', 'jetpack' ); ?>
  893. </option>
  894. <option value="post_modified_gmt" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_modified_gmt', $is_template ); ?>>
  895. <?php esc_html_e( 'Modified GMT', 'jetpack' ); ?>
  896. </option>
  897. </select>
  898. </label>
  899. </p>
  900. <p class="jetpack-search-filters-widget__date-histogram-select">
  901. <label>
  902. <?php esc_html_e( 'Choose an interval:', 'jetpack' ); ?>
  903. <select name="<?php echo esc_attr( $this->get_field_name( 'date_histogram_interval' ) ); ?>[]" class="widefat date-interval-select">
  904. <option value="month" <?php $this->render_widget_option_selected( 'interval', $args['interval'], 'month', $is_template ); ?>>
  905. <?php esc_html_e( 'Month', 'jetpack' ); ?>
  906. </option>
  907. <option value="year" <?php $this->render_widget_option_selected( 'interval', $args['interval'], 'year', $is_template ); ?>>
  908. <?php esc_html_e( 'Year', 'jetpack' ); ?>
  909. </option>
  910. </select>
  911. </label>
  912. </p>
  913. <p class="jetpack-search-filters-widget__title">
  914. <label>
  915. <?php esc_html_e( 'Title:', 'jetpack' ); ?>
  916. <input
  917. class="widefat"
  918. type="text"
  919. name="<?php echo esc_attr( $this->get_field_name( 'filter_name' ) ); ?>[]"
  920. value="<?php $this->render_widget_attr( 'name', $args['name'], $is_template ); ?>"
  921. placeholder="<?php $this->render_widget_attr( 'name_placeholder', $args['name_placeholder'], $is_template ); ?>"
  922. />
  923. </label>
  924. </p>
  925. <p>
  926. <label>
  927. <?php esc_html_e( 'Maximum number of filters (1-50):', 'jetpack' ); ?>
  928. <input
  929. class="widefat filter-count"
  930. name="<?php echo esc_attr( $this->get_field_name( 'num_filters' ) ); ?>[]"
  931. type="number"
  932. value="<?php $this->render_widget_attr( 'count', $args['count'], $is_template ); ?>"
  933. min="1"
  934. max="50"
  935. step="1"
  936. required
  937. />
  938. </label>
  939. </p>
  940. <p class="jetpack-search-filters-widget__controls">
  941. <a href="#" class="delete"><?php esc_html_e( 'Remove', 'jetpack' ); ?></a>
  942. </p>
  943. </div>
  944. <?php
  945. }
  946. }