PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / integrations / front-end / crawl-cleanup-searches.php

crawl-cleanup-searches.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI trunk, at src/integrations/front-end/crawl-cleanup-searches.php

207 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\Integrations\Front_End;
4
5 use WP_Query;
6 use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
7 use Yoast\WP\SEO\Helpers\Options_Helper;
8 use Yoast\WP\SEO\Helpers\Redirect_Helper;
9 use Yoast\WP\SEO\Integrations\Integration_Interface;
10
11 /**
12 * Class Crawl_Cleanup_Searches.
13 */
14 class Crawl_Cleanup_Searches implements Integration_Interface {
15
16 /**
17 * Patterns to match against to find spam.
18 *
19 * @var array
20 */
21 private $patterns = [
22 '/[:()【】[]]+/u',
23 '/(TALK|QQ)\:/iu',
24 ];
25
26 /**
27 * The options helper.
28 *
29 * @var Options_Helper
30 */
31 private $options_helper;
32
33 /**
34 * The redirect helper.
35 *
36 * @var Redirect_Helper
37 */
38 private $redirect_helper;
39
40 /**
41 * Crawl_Cleanup_Searches integration constructor.
42 *
43 * @param Options_Helper $options_helper The option helper.
44 * @param Redirect_Helper $redirect_helper The redirect helper.
45 */
46 public function __construct( Options_Helper $options_helper, Redirect_Helper $redirect_helper ) {
47 $this->options_helper = $options_helper;
48 $this->redirect_helper = $redirect_helper;
49 }
50
51 /**
52 * Initializes the integration.
53 *
54 * This is the place to register hooks and filters.
55 *
56 * @return void
57 */
58 public function register_hooks() {
59 if ( $this->options_helper->get( 'search_cleanup' ) ) {
60 \add_filter( 'pre_get_posts', [ $this, 'validate_search' ] );
61 }
62 if ( $this->options_helper->get( 'redirect_search_pretty_urls' ) && ! empty( \get_option( 'permalink_structure' ) ) ) {
63 \add_action( 'template_redirect', [ $this, 'maybe_redirect_searches' ], 2 );
64 }
65 }
66
67 /**
68 * Returns the conditionals based in which this loadable should be active.
69 *
70 * @return array The array of conditionals.
71 */
72 public static function get_conditionals() {
73 return [ Front_End_Conditional::class ];
74 }
75
76 /**
77 * Check if we want to allow this search to happen.
78 *
79 * @param WP_Query $query The main query.
80 *
81 * @return WP_Query
82 */
83 public function validate_search( WP_Query $query ) {
84 if ( ! $query->is_search() ) {
85 return $query;
86 }
87 // First check against emoji and patterns we might not want.
88 $this->check_unwanted_patterns( $query );
89
90 // Then limit characters if still needed.
91 $this->limit_characters();
92
93 return $query;
94 }
95
96 /**
97 * Redirect pretty search URLs to the "raw" equivalent
98 *
99 * @return void
100 */
101 public function maybe_redirect_searches() {
102 if ( ! \is_search() ) {
103 return;
104 }
105
106 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
107 if ( isset( $_SERVER['REQUEST_URI'] ) && \stripos( $_SERVER['REQUEST_URI'], '/search/' ) === 0 ) {
108 $args = [];
109
110 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
111 $parsed = \wp_parse_url( $_SERVER['REQUEST_URI'] );
112
113 if ( ! empty( $parsed['query'] ) ) {
114 \wp_parse_str( $parsed['query'], $args );
115 }
116
117 $args['s'] = \get_search_query();
118
119 $proper_url = \home_url( '/' );
120
121 if ( (int) \get_query_var( 'paged' ) > 1 ) {
122 $proper_url .= \sprintf( 'page/%s/', \get_query_var( 'paged' ) );
123 unset( $args['paged'] );
124 }
125
126 $proper_url = \add_query_arg( \array_map( 'rawurlencode_deep', $args ), $proper_url );
127
128 if ( ! empty( $parsed['fragment'] ) ) {
129 $proper_url .= '#' . \rawurlencode( $parsed['fragment'] );
130 }
131
132 $this->redirect_away( 'We redirect pretty URLs to the raw format.', $proper_url );
133 }
134 }
135
136 /**
137 * Check query against unwanted search patterns.
138 *
139 * @param WP_Query $query The main WordPress query.
140 *
141 * @return void
142 */
143 private function check_unwanted_patterns( WP_Query $query ) {
144 $s = \rawurldecode( $query->query_vars['s'] );
145 if ( $this->options_helper->get( 'search_cleanup_emoji' ) && $this->has_emoji( $s ) ) {
146 $this->redirect_away( 'We don\'t allow searches with emojis and other special characters.' );
147 }
148
149 if ( ! $this->options_helper->get( 'search_cleanup_patterns' ) ) {
150 return;
151 }
152 foreach ( $this->patterns as $pattern ) {
153 $outcome = \preg_match( $pattern, $s, $matches );
154 if ( $outcome && $matches !== [] ) {
155 $this->redirect_away( 'Your search matched a common spam pattern.' );
156 }
157 }
158 }
159
160 /**
161 * Redirect to the homepage for invalid searches.
162 *
163 * @param string $reason The reason for redirecting away.
164 * @param string $to_url The URL to redirect to.
165 *
166 * @return void
167 */
168 private function redirect_away( $reason, $to_url = '' ) {
169 if ( empty( $to_url ) ) {
170 $to_url = \get_home_url();
171 }
172
173 $this->redirect_helper->do_safe_redirect( $to_url, 301, 'Yoast Search Filtering: ' . $reason );
174 }
175
176 /**
177 * Limits the number of characters in the search query.
178 *
179 * @return void
180 */
181 private function limit_characters() {
182 // We retrieve the search term unescaped because we want to count the characters properly. We make sure to escape it afterwards, if we do something with it.
183 $unescaped_s = \get_search_query( false );
184
185 // We then unslash the search term, again because we want to count the characters properly. We make sure to slash it afterwards, if we do something with it.
186 $raw_s = \wp_unslash( $unescaped_s );
187 if ( \mb_strlen( $raw_s, 'UTF-8' ) > $this->options_helper->get( 'search_character_limit' ) ) {
188 $new_s = \mb_substr( $raw_s, 0, $this->options_helper->get( 'search_character_limit' ), 'UTF-8' );
189 \set_query_var( 's', \wp_slash( \esc_attr( $new_s ) ) );
190 }
191 }
192
193 /**
194 * Determines if a text string contains an emoji or not.
195 *
196 * @param string $text The text string to detect emoji in.
197 *
198 * @return bool
199 */
200 private function has_emoji( $text ) {
201 $emojis_regex = '/([^-\p{L}\x00-\x7F]+)/u';
202 \preg_match( $emojis_regex, $text, $matches );
203
204 return ! empty( $matches );
205 }
206 }
207