PluginProbe ʕ •ᴥ•ʔ
Search Regex / trunk
Search Regex vtrunk
trunk 1.4.12 1.4.13 1.4.14 1.4.15 1.4.16 2.0 2.0.1 2.1 2.2 2.2.1 2.3 2.3.1 2.3.2 2.3.3 2.4 2.4.1 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1 3.1.1 3.1.2 3.2 3.3 3.3.0 3.3.1 3.4 3.4.1 3.4.2
search-regex / includes / admin / class-admin.php
search-regex / includes / admin Last commit date
class-admin.php 5 months ago
class-admin.php
544 lines
1 <?php
2
3 namespace SearchRegex\Admin;
4
5 use SearchRegex\Source;
6 use SearchRegex\Search;
7 use SearchRegex\Plugin;
8
9 class Admin {
10 /**
11 * @var null|Admin
12 */
13 private static ?Admin $instance = null;
14
15 /**
16 * Initialize the object
17 *
18 * @return Admin
19 */
20 public static function init() {
21 if ( is_null( self::$instance ) ) {
22 self::$instance = new self();
23 }
24
25 return self::$instance;
26 }
27
28 /**
29 * Create the admin class
30 */
31 public function __construct() {
32 add_action( 'admin_menu', [ $this, 'admin_menu' ] );
33 add_filter( 'plugin_action_links_' . basename( dirname( SEARCHREGEX_FILE ) ) . '/' . basename( SEARCHREGEX_FILE ), [ $this, 'plugin_settings' ], 10 );
34 add_filter( 'searchregex_result_actions', [ $this, 'extra_actions' ], 10, 3 );
35
36 register_uninstall_hook( SEARCHREGEX_FILE, [ self::class, 'plugin_uninstall' ] );
37 }
38
39 /**
40 * Tidy the plugin up after being uninstalled
41 *
42 * @return void
43 */
44 public static function plugin_uninstall() {
45 Plugin\Settings::init()->delete();
46 delete_option( Search\Preset::OPTION_NAME );
47 }
48
49 /**
50 * Plugin is activated. Load the presets file
51 *
52 * @return void
53 */
54 public static function plugin_activated() {
55 }
56
57 /**
58 * Add plugin settings to plugin page
59 *
60 * @param array<string> $links Links.
61 * @return array<string>
62 */
63 public function plugin_settings( $links ) {
64 array_unshift( $links, '<a href="' . esc_url( $this->get_plugin_url() ) . '&amp;sub=options">' . __( 'Settings', 'search-regex' ) . '</a>' );
65 return $links;
66 }
67
68 /**
69 * Get plugin URL
70 *
71 * @return string
72 */
73 private function get_plugin_url() {
74 return admin_url( 'tools.php?page=' . basename( SEARCHREGEX_FILE ) );
75 }
76
77 /**
78 * Get first page the current user is allowed to see
79 *
80 * @return string
81 */
82 private function get_first_available_page_url() {
83 $pages = Plugin\Capabilities::get_available_pages();
84
85 if ( count( $pages ) > 0 ) {
86 return $this->get_plugin_url() . ( $pages[0] === 'search' ? '' : '&sub=' . rawurlencode( $pages[0] ) );
87 }
88
89 return admin_url();
90 }
91
92 /**
93 * Insert stuff into the admin head
94 *
95 * @return void
96 */
97 public function searchregex_head() {
98 global $wp_version;
99
100 // Does user have access to this page?
101 if ( $this->get_current_page() === false ) {
102 // Redirect to root plugin page
103 wp_safe_redirect( $this->get_first_available_page_url() );
104 die();
105 }
106
107 if (
108 isset( $_REQUEST['action'] ) &&
109 isset( $_REQUEST['_wpnonce'] ) &&
110 (bool) wp_verify_nonce( $_REQUEST['_wpnonce'], 'wp_rest' )
111 ) {
112 if ( $_REQUEST['action'] === 'rest_api' ) {
113 $this->set_rest_api( intval( $_REQUEST['rest_api'], 10 ) );
114 }
115 }
116
117 $build = SEARCHREGEX_VERSION . '-' . SEARCHREGEX_BUILD;
118 $preload = $this->get_preload_data();
119 $settings = Plugin\Settings::init();
120
121 $versions = [
122 'Plugin: ' . SEARCHREGEX_VERSION,
123 'WordPress: ' . $wp_version . ' (' . ( is_multisite() ? 'multi' : 'single' ) . ')',
124 'PHP: ' . phpversion() . ' ' . ini_get( 'memory_limit' ) . ' ' . ini_get( 'max_execution_time' ) . 's',
125 'Browser: ' . $this->get_user_agent(),
126 'JavaScript: ' . plugin_dir_url( SEARCHREGEX_FILE ) . 'search-regex.js',
127 'REST API: ' . $settings->get_rest_api_url(),
128 ];
129
130 $vendor_file = plugin_dir_path( SEARCHREGEX_FILE ) . 'build/vendor.asset.php';
131 if ( file_exists( $vendor_file ) ) {
132 $vendor_assets = include $vendor_file;
133 wp_enqueue_script( 'search-regex-vendor', plugin_dir_url( SEARCHREGEX_FILE ) . 'build/vendor.js', $vendor_assets['dependencies'], $vendor_assets['version'], true );
134 }
135
136 $assets = include plugin_dir_path( SEARCHREGEX_FILE ) . 'build/search-regex.asset.php';
137 $dependencies = $assets['dependencies'];
138 if ( file_exists( $vendor_file ) ) {
139 $dependencies[] = 'search-regex-vendor';
140 }
141 $version = $assets['version'];
142
143 wp_enqueue_script( 'search-regex', plugin_dir_url( SEARCHREGEX_FILE ) . 'build/search-regex.js', $dependencies, $version, true );
144 wp_enqueue_style( 'search-regex', plugin_dir_url( SEARCHREGEX_FILE ) . 'build/search-regex.css', [], $version );
145
146 $pages = Plugin\Capabilities::get_available_pages();
147 $caps = Plugin\Capabilities::get_all_capabilities();
148
149 $is_new = false;
150 $major_version = implode( '.', array_slice( explode( '.', SEARCHREGEX_VERSION ), 0, 2 ) );
151
152 // phpcs:ignore
153 if ( isset( $_GET['page'] ) && $_GET['page'] === 'search-regex.php' && strpos( SEARCHREGEX_VERSION, '-beta' ) === false ) {
154 $is_new = $settings->is_new_version( $major_version );
155 }
156
157 wp_localize_script(
158 'search-regex', 'SearchRegexi10n', [
159 'api' => [
160 'WP_API_root' => esc_url_raw( $settings->get_rest_api_url() ),
161 'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
162 'site_health' => admin_url( 'site-health.php' ),
163 'current' => $settings->get_rest_api(),
164 'routes' => $settings->get_available_rest_api(),
165 ],
166 'pluginBaseUrl' => plugins_url( '', SEARCHREGEX_FILE ),
167 'pluginRoot' => $this->get_plugin_url(),
168 'locale' => implode( '-', array_slice( explode( '-', str_replace( '_', '-', get_locale() ) ), 0, 2 ) ),
169 'settings' => $settings->get_as_json(),
170 'preload' => $preload,
171 'versions' => implode( "\n", $versions ),
172 'version' => SEARCHREGEX_VERSION,
173 'caps' => [
174 'pages' => $pages,
175 'capabilities' => $caps,
176 ],
177 'update_notice' => $is_new ? $major_version : false,
178 ]
179 );
180
181 wp_set_script_translations( 'search-regex', 'search-regex', plugin_dir_path( SEARCHREGEX_FILE ) . 'languages/json/' );
182
183 $this->add_help_tab();
184 }
185
186 /**
187 * Get browser agent
188 *
189 * @return string
190 */
191 public function get_user_agent() {
192 $agent = '';
193
194 if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
195 $agent = $_SERVER['HTTP_USER_AGENT'];
196 }
197
198 return $agent;
199 }
200
201 /**
202 * Set REST API
203 *
204 * @param int $api API.
205 * @return void
206 */
207 private function set_rest_api( $api ) {
208 Plugin\Settings::init()->set_rest_api( intval( $api, 10 ) );
209 }
210
211 /**
212 * Get preloaded data
213 *
214 * @return array<string, mixed>
215 */
216 private function get_preload_data() {
217 $schema = Source\Manager::get_schema();
218 $presets = Search\Preset::get_all();
219
220 return [
221 'sources' => Source\Manager::get_all_grouped(),
222 'presets' => $presets,
223 'schema' => $schema,
224 'labels' => $this->get_preload_labels( $presets ),
225 ];
226 }
227
228 /**
229 * Get the preloaded labels.
230 *
231 * @param array<mixed> $presets
232 * @return array<int, mixed>
233 */
234 private function get_preload_labels( array $presets ) {
235 $preload = [];
236 $filters_to_preload = [];
237
238 if ( isset( $_GET['filters'] ) && is_string( $_GET['filters'] ) ) {
239 $filters = json_decode( stripslashes( $_GET['filters'] ), true );
240
241 if ( is_array( $filters ) ) {
242 $filters_to_preload = array_merge( $filters_to_preload, $filters );
243 }
244 }
245
246 foreach ( $presets as $preset ) {
247 if ( isset( $preset['search']['filters'] ) ) {
248 $filters_to_preload = array_merge( $filters_to_preload, $preset['search']['filters'] );
249 }
250
251 if ( isset( $preset['search']['action'] ) && $preset['search']['action'] === 'modify' && is_array( $preset['search']['actionOption'] ) ) {
252 $filters_to_preload = array_merge(
253 $filters_to_preload,
254 array_map(
255 fn( $action ) => [
256 'type' => $action['source'],
257 'items' => [
258 array_merge(
259 [ 'column' => $action['column'] ],
260 $action
261 ),
262 ],
263 ],
264 $preset['search']['actionOption']
265 )
266 );
267 }
268 }
269
270 foreach ( $filters_to_preload as $filter ) {
271 if ( is_array( $filter ) && isset( $filter['type'] ) && isset( $filter['items'] ) ) {
272 foreach ( $filter['items'] as $filt ) {
273 $preload = array_merge( $preload, Source\Manager::get_schema_preload( $filter['type'], $filt ) );
274 }
275 }
276 }
277
278 return array_values( array_filter( $preload ) );
279 }
280
281 /**
282 * Replace links
283 *
284 * @internal
285 * @param string $text Text to replace.
286 * @param string $url URL to insert into the link.
287 * @param string $link Link name.
288 * @return string
289 */
290 private function linkify( $text, $url, $link = 'link' ) {
291 return (string) preg_replace( '@{{' . $link . '}}(.*?){{/' . $link . '}}@', '<a target="_blank" rel="noopener noreferrer" href="' . esc_url( $url ) . '">$1</a>', $text );
292 }
293
294 /**
295 * Add help tab
296 *
297 * @internal
298 * @return void
299 */
300 private function add_help_tab() {
301 $flags = $this->linkify(
302 $this->linkify(
303 __( '{{link}}Source Flags{{/link}} - additional options for the selected source. For example, include post {{guid}}GUID{{/guid}} in the search.', 'search-regex' ),
304 'https://searchregex.com/support/search-source/'
305 ),
306 'https://deliciousbrains.com/wordpress-post-guids-sometimes-update/',
307 'guid'
308 );
309
310 /* translators: URL */
311 $content = [ '<p>' . sprintf( __( 'You can find full documentation about using Search Regex on the <a href="%s" target="_blank">searchregex.com</a> support site.', 'search-regex' ), 'https://searchregex.com/support/' ) . '</p>' ];
312 $content[] = '<p>' . __( 'The following concepts are used by Search Regex:', 'search-regex' ) . '</p>';
313 $content[] = '<ul>';
314 $content[] = '<li>' . $this->linkify( __( '{{link}}Search Flags{{/link}} - additional qualifiers for your search, to enable case insensitivity, and to enable regular expression support.', 'search-regex' ), 'https://searchregex.com/support/searching/' ) . '</li>';
315 $content[] = '<li>' . $this->linkify( __( '{{link}}Regular expression{{/link}} - a way of defining a pattern for text matching. Provides more advanced matches.', 'search-regex' ), 'https://searchregex.com/support/regular-expression/' ) . '</li>';
316 $content[] = '<li>' . $this->linkify( __( '{{link}}Source{{/link}} - the source of data you wish to search. For example, posts, pages, or comments.', 'search-regex' ), 'https://searchregex.com/support/search-source/' ) . '</li>';
317 $content[] = '<li>' . $flags . '</li>';
318 $content[] = '</ul>';
319
320 $title = __( 'Search Regex Support', 'search-regex' );
321
322 $current_screen = get_current_screen();
323 if ( $current_screen ) {
324 $current_screen->add_help_tab(
325 [
326 'id' => 'search-regex',
327 'title' => 'Search Regex',
328 'content' => "<h2>$title</h2>" . implode( "\n", $content ),
329 ]
330 );
331 }
332 }
333
334 /**
335 * Admin menu
336 *
337 * @return void
338 */
339 public function admin_menu() {
340 $access = Plugin\Capabilities::get_plugin_access();
341 $hook = add_management_page( 'Search Regex', 'Search Regex', $access, basename( SEARCHREGEX_FILE ), [ $this, 'admin_screen' ] );
342 if ( $hook ) {
343 add_action( 'load-' . $hook, [ $this, 'searchregex_head' ] );
344 }
345 }
346
347 /**
348 * Check if we meet minimum WP requirements
349 *
350 * @return bool
351 */
352 private function check_minimum_wp() {
353 $wp_version = get_bloginfo( 'version' );
354
355 if ( version_compare( $wp_version, SEARCHREGEX_MIN_WP, '<' ) ) {
356 return false;
357 }
358
359 return true;
360 }
361
362 /**
363 * Admin screen
364 *
365 * @return void
366 */
367 public function admin_screen() {
368 if ( count( Plugin\Capabilities::get_all_capabilities() ) === 0 ) {
369 die( 'You do not have sufficient permissions to access this page.' );
370 }
371
372 if ( $this->check_minimum_wp() === false ) {
373 $this->show_minimum_wordpress();
374 return;
375 }
376
377 $this->show_main();
378 }
379
380 /**
381 * Show minimum supported WP
382 *
383 * @internal
384 * @return void
385 */
386 private function show_minimum_wordpress() {
387 global $wp_version;
388
389 /* translators: 1: Expected WordPress version, 2: Actual WordPress version */
390 $wp_requirement = sprintf( __( 'Search Regex requires WordPress v%1$1s, you are using v%2$2s - please update your WordPress', 'search-regex' ), SEARCHREGEX_MIN_WP, $wp_version );
391 ?>
392 <div class="react-error">
393 <h1><?php esc_html_e( 'Unable to load Search Regex', 'search-regex' ); ?></h1>
394 <p><?php echo esc_html( $wp_requirement ); ?></p>
395 </div>
396 <?php
397 }
398
399 /**
400 * Show fail to load page
401 *
402 * @internal
403 * @return void
404 */
405 private function show_load_fail() {
406 ?>
407 <div class="react-error" style="display: none">
408 <h1><?php esc_html_e( 'Unable to load Search Regex ☹️', 'search-regex' ); ?> v<?php echo esc_html( SEARCHREGEX_VERSION ); ?></h1>
409 <p><?php esc_html_e( "This may be caused by another plugin - look at your browser's error console for more details.", 'search-regex' ); ?></p>
410 <p><?php esc_html_e( 'If you are using a page caching plugin or service (CloudFlare, OVH, etc) then you can also try clearing that cache.', 'search-regex' ); ?></p>
411 <p><?php echo wp_kses( __( 'Also check if your browser is able to load <code>search-regex.js</code>:', 'search-regex' ), [ 'code' => [] ] ); ?></p>
412 <p><code><?php echo esc_html( plugin_dir_url( SEARCHREGEX_FILE ) . 'build/search-regex.js?ver=' . rawurlencode( SEARCHREGEX_VERSION ) . '-' . rawurlencode( SEARCHREGEX_BUILD ) ); ?></code></p>
413 <p><?php esc_html_e( 'Please note that Search Regex requires the WordPress REST API to be enabled. If you have disabled this then you won\'t be able to use Search Regex', 'search-regex' ); ?></p>
414 <p><?php echo wp_kses( __( 'Please see the <a href="https://searchregex.com/support/problems/">list of common problems</a>.', 'search-regex' ), [ 'a' => [ 'href' => [] ] ] ); ?></p>
415 <p><?php esc_html_e( 'If you think Search Regex is at fault then create an issue.', 'search-regex' ); ?></p>
416 <p class="versions"><?php echo wp_kses( __( '<code>SearchRegexi10n</code> is not defined. This usually means another plugin is blocking Search Regex from loading. Please disable all plugins and try again.', 'search-regex' ), [ 'code' => [] ] ); ?></p>
417 <p>
418 <a class="button-primary" target="_blank" href="https://github.com/johngodley/search-regex/issues/new?title=Problem%20starting%20Search%20Regex%20<?php echo esc_attr( SEARCHREGEX_VERSION ); ?>">
419 <?php esc_html_e( 'Create Issue', 'search-regex' ); ?>
420 </a>
421 </p>
422 </div>
423 <?php
424 }
425
426 /**
427 * Show main UI
428 *
429 * @internal
430 * @return void
431 */
432 private function show_main() {
433 ?>
434 <div id="react-ui">
435 <div class="react-loading">
436 <h1><?php esc_html_e( 'Loading, please wait...', 'search-regex' ); ?></h1>
437
438 <span class="react-loading-spinner"></span>
439 </div>
440 <noscript><?php esc_html_e( 'Please enable JavaScript', 'search-regex' ); ?></noscript>
441
442 <?php $this->show_load_fail(); ?>
443 </div>
444
445 <script>
446 var prevError = window.onerror;
447 var errors = [];
448 var timeout = 0;
449 var timer = setInterval( function() {
450 if ( isSearchRegexLoaded() ) {
451 resetAll();
452 } else if ( errors.length > 0 || timeout++ === 5 ) {
453 showError();
454 }
455 }, 5000 );
456
457 function isSearchRegexLoaded() {
458 return typeof searchregex !== 'undefined';
459 }
460
461 function showError() {
462 var errorText = "";
463
464 if ( errors.length > 0 ) {
465 errorText = "```\n" + errors.join( ',' ) + "\n```\n\n";
466 }
467
468 resetAll();
469 document.querySelector( '.react-loading' ).style.display = 'none';
470 document.querySelector( '.react-error' ).style.display = 'block';
471
472 if ( typeof SearchRegexi10n !== 'undefined' ) {
473 document.querySelector( '.versions' ).innerHTML = SearchRegexi10n.versions.replace( /\n/g, '<br />' );
474 document.querySelector( '.react-error .button-primary' ).href += '&body=' + encodeURIComponent( errorText ) + encodeURIComponent( SearchRegexi10n.versions );
475 }
476 }
477
478 function resetAll() {
479 clearInterval( timer );
480 window.onerror = prevError;
481 }
482
483 window.onerror = function( error, url, line ) {
484 console.error( error );
485 errors.push( error + ' ' + url + ' ' + line );
486 };
487 </script>
488 <?php
489 }
490
491 /**
492 * Get the current plugin page.
493 * Uses $_GET['sub'] to determine the current page unless a page is supplied.
494 *
495 * @param string|bool $page Current page.
496 *
497 * @return string|boolean Current page, or false.
498 */
499 private function get_current_page( $page = false ) {
500 // $_GET['sub'] is validated below
501 // phpcs:ignore
502 if ( ! $page ) {
503 // phpcs:ignore
504 $page = $_GET['sub'] ?? 'search';
505 }
506
507 // Are we allowed to access this page?
508 if ( in_array( $page, Plugin\Capabilities::get_available_pages(), true ) ) {
509 // phpcs:ignore
510 return $page;
511 }
512
513 return false;
514 }
515
516 /**
517 * Get any extra actions that might be needed.
518 *
519 * @param array<string, mixed> $actions Actions.
520 * @param string $type Type.
521 * @param Search\Result $result Result.
522 * @return array<string, mixed>
523 */
524 public function extra_actions( array $actions, $type, $result ) {
525 if ( $type === 'tablepress_table' ) {
526 $tables = json_decode( get_option( 'tablepress_tables' ), true );
527
528 if ( is_array( $tables ) && isset( $tables['table_post'] ) && is_array( $tables['table_post'] ) ) {
529 foreach ( $tables['table_post'] as $id => $post_id ) {
530 if ( $post_id === $result->get_row_id() ) {
531 $actions['edit'] = 'admin.php?page=tablepress&action=edit&table_id=' . rawurlencode( $id );
532 break;
533 }
534 }
535 }
536 }
537
538 return $actions;
539 }
540 }
541
542 register_activation_hook( SEARCHREGEX_FILE, [ Admin::class, 'plugin_activated' ] );
543 add_action( 'init', [ Admin::class, 'init' ] ); // @phpstan-ignore return.void
544