PluginProbe
Easy Search Replace – Find & Replace Text/HTML/URLs, Remove Footer Credit / 1.1.3
Easy Search Replace – Find & Replace Text/HTML/URLs, Remove Footer Credit v1.1.3
1.3.1 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 1.2.2 1.3.0
easy-search-replace / easy-search-replace.php

easy-search-replace.php in Easy Search Replace – Find & Replace Text/HTML/URLs, Remove Footer Credit 1.1.3, at easy-search-replace.php

333 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Easy Search Replace
4 * Description: Find & replace text, HTML, or URLs across your WordPress site in real time — no database changes.
5 * Version: 1.1.3
6 * Author: Uzair
7 * Text Domain: easy-search-replace
8 * Domain Path: /languages
9 * License: GNU General Public License v2 or later
10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 * Requires at least: 5.0
12 * Requires PHP: 7.2
13 *
14 * @package EasySearchReplace
15 */
16
17 // Exit if accessed directly.
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /* ---------------------------------------------------------------------------
23 * Plugin constants
24 * ------------------------------------------------------------------------- */
25
26 if ( ! defined( 'ESRN_VERSION' ) ) {
27 define( 'ESRN_VERSION', '1.1.3' );
28 }
29 if ( ! defined( 'ESRN_PLUGIN_FILE' ) ) {
30 define( 'ESRN_PLUGIN_FILE', __FILE__ );
31 }
32 if ( ! defined( 'ESRN_PLUGIN_DIR' ) ) {
33 define( 'ESRN_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
34 }
35 if ( ! defined( 'ESRN_PLUGIN_URL' ) ) {
36 define( 'ESRN_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
37 }
38 if ( ! defined( 'ESRN_PLUGIN_BASENAME' ) ) {
39 define( 'ESRN_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
40 }
41 if ( ! defined( 'ESRN_OPTION_KEY' ) ) {
42 define( 'ESRN_OPTION_KEY', 'esrn_search_replace_options' );
43 }
44
45 require_once ESRN_PLUGIN_DIR . 'inc/options.php';
46 require_once ESRN_PLUGIN_DIR . 'lib/dom.php';
47
48 /* ---------------------------------------------------------------------------
49 * Internationalization
50 * ------------------------------------------------------------------------- */
51
52 /**
53 * Load the plugin translation files.
54 */
55 function esrn_load_textdomain() {
56 load_plugin_textdomain(
57 'easy-search-replace',
58 false,
59 dirname( ESRN_PLUGIN_BASENAME ) . '/languages'
60 );
61 }
62 add_action( 'plugins_loaded', 'esrn_load_textdomain' );
63
64 /* ---------------------------------------------------------------------------
65 * Front-end output-buffer search & replace
66 * ------------------------------------------------------------------------- */
67
68 /**
69 * Perform search and replace on the rendered output buffer.
70 *
71 * @param string $buffer The output buffer.
72 * @return string The modified buffer.
73 */
74 function esrn_search_replace_start( $buffer ) {
75 global $post;
76
77 $options = get_option( ESRN_OPTION_KEY );
78 $fields = isset( $options['fields'] ) && is_array( $options['fields'] ) ? $options['fields'] : array();
79
80 if ( empty( $fields ) ) {
81 return $buffer;
82 }
83
84 // Resolve the "current" context for conditional rules.
85 $current_post_id = ( is_singular() && isset( $post->ID ) ) ? (int) $post->ID : null;
86 $current_type = ( is_singular() && isset( $post->post_type ) ) ? (string) $post->post_type : null;
87
88 // On the static posts page, treat the chosen page as the current post.
89 if ( is_home() && get_option( 'page_for_posts' ) ) {
90 $current_post_id = (int) get_option( 'page_for_posts' );
91 $current_type = 'page';
92 }
93
94 $current_url = esrn_get_current_url();
95
96 // First pass: global (non-selector) replacements.
97 foreach ( $fields as $field ) {
98 if ( ! empty( $field['selector'] ) ) {
99 continue; // Handled in second pass.
100 }
101
102 if ( ! esrn_should_apply_field( $field, $current_type, $current_post_id, $current_url ) ) {
103 continue;
104 }
105
106 $search = isset( $field['search'] ) ? (string) $field['search'] : '';
107 $replace = isset( $field['replace'] ) ? (string) $field['replace'] : '';
108
109 if ( '' === $search ) {
110 continue;
111 }
112
113 if ( ! empty( $field['ignore_case'] ) ) {
114 $buffer = str_ireplace( $search, $replace, $buffer );
115 } else {
116 $buffer = str_replace( $search, $replace, $buffer );
117 }
118 }
119
120 // Check whether any selector-based rule applies before spending cycles on DOM parsing.
121 $has_selector = false;
122 foreach ( $fields as $field ) {
123 if ( empty( $field['selector'] ) ) {
124 continue;
125 }
126 if ( esrn_should_apply_field( $field, $current_type, $current_post_id, $current_url ) ) {
127 $has_selector = true;
128 break;
129 }
130 }
131
132 if ( ! $has_selector ) {
133 return $buffer;
134 }
135
136 // Second pass: selector-based replacements (parse DOM once).
137 $dom = esr_str_get_html( $buffer, false, true, 'UTF-8', false, PHP_EOL, ' ' );
138 if ( ! $dom ) {
139 return $buffer; // Fallback gracefully if parsing fails.
140 }
141
142 foreach ( $fields as $field ) {
143 if ( empty( $field['selector'] ) ) {
144 continue;
145 }
146 if ( ! esrn_should_apply_field( $field, $current_type, $current_post_id, $current_url ) ) {
147 continue;
148 }
149
150 $search = isset( $field['search'] ) ? (string) $field['search'] : '';
151 $replace = isset( $field['replace'] ) ? (string) $field['replace'] : '';
152 $selector = (string) $field['selector'];
153
154 if ( '' === $search ) {
155 continue;
156 }
157
158 $elements = $dom->find( $selector );
159 if ( empty( $elements ) ) {
160 continue;
161 }
162
163 foreach ( $elements as $element ) {
164 if ( ! empty( $field['ignore_case'] ) ) {
165 $element->innertext = str_ireplace( $search, $replace, $element->innertext );
166 } else {
167 $element->innertext = str_replace( $search, $replace, $element->innertext );
168 }
169 }
170 }
171
172 $buffer = $dom->save();
173
174 return $buffer;
175 }
176
177 /**
178 * Safely derive the current front-end URL for rule matching.
179 *
180 * Reads REQUEST_URI from $_SERVER with unslashing + sanitization, rather than
181 * trusting it raw. Falls back to home_url() when REQUEST_URI is absent.
182 *
183 * @return string The absolute URL of the current request.
184 */
185 function esrn_get_current_url() {
186 if ( empty( $_SERVER['REQUEST_URI'] ) ) {
187 return home_url( '/' );
188 }
189
190 $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
191 // esc_url_raw gives us a normalized, safe URL to compare against user-supplied targets.
192 return esc_url_raw( home_url( $request_uri ) );
193 }
194
195 /**
196 * Decide whether a rule should run for the current request.
197 *
198 * @param array $field The rule definition.
199 * @param string|null $current_type Current post type (null on non-singular views).
200 * @param int|null $current_post_id Current post ID (null on non-singular views).
201 * @param string $current_url Current absolute URL.
202 * @return bool
203 */
204 function esrn_should_apply_field( $field, $current_type, $current_post_id, $current_url ) {
205 $types = isset( $field['post_types'] ) ? (array) $field['post_types'] : array();
206 $ids = isset( $field['post_ids'] ) ? (string) $field['post_ids'] : '';
207 $urls = isset( $field['urls'] ) ? (string) $field['urls'] : '';
208
209 // Post-type gate: when types are configured but we're on a non-singular view
210 // (category, search, 404, etc.) the rule can't match a post type, so it should
211 // NOT run — this is the correct behavior for a "limit to post type" filter.
212 if ( ! empty( $types ) ) {
213 if ( null === $current_type || ! in_array( $current_type, $types, true ) ) {
214 return false;
215 }
216 }
217
218 // Post IDs / URLs are OR'd together: at least one must match if either is set.
219 $has_id_filter = ( '' !== $ids );
220 $has_url_filter = ( '' !== $urls );
221
222 if ( ! $has_id_filter && ! $has_url_filter ) {
223 return true;
224 }
225
226 if ( $has_id_filter && null !== $current_post_id ) {
227 $allowed_ids = array_filter( array_map( 'intval', array_map( 'trim', explode( ',', $ids ) ) ) );
228 if ( in_array( (int) $current_post_id, $allowed_ids, true ) ) {
229 return true;
230 }
231 }
232
233 if ( $has_url_filter ) {
234 $allowed_urls = array_filter( array_map( 'trim', explode( "\n", $urls ) ) );
235 foreach ( $allowed_urls as $allowed_url ) {
236 if ( '' !== $allowed_url && false !== stripos( $current_url, $allowed_url ) ) {
237 return true;
238 }
239 }
240 }
241
242 return false;
243 }
244
245 /**
246 * Start output buffering on the front-end when at least one rule is configured.
247 */
248 function esrn_search_replace_buffer_start() {
249 $options = get_option( ESRN_OPTION_KEY );
250 if ( ! empty( $options['fields'] ) ) {
251 ob_start( 'esrn_search_replace_start' );
252 }
253 }
254 add_action( 'template_redirect', 'esrn_search_replace_buffer_start', 101 );
255
256 /* ---------------------------------------------------------------------------
257 * Admin assets & settings link
258 * ------------------------------------------------------------------------- */
259
260 /**
261 * Enqueue admin assets only on the plugin's settings screen.
262 *
263 * @param string $hook_suffix Current admin page hook suffix.
264 */
265 function esrn_enqueue_admin_scripts( $hook_suffix ) {
266 // get_current_screen() is the safest way to identify our settings page.
267 if ( 'settings_page_easy_search_replace' !== $hook_suffix ) {
268 return;
269 }
270
271 wp_enqueue_script(
272 'esrn-search-replace',
273 ESRN_PLUGIN_URL . 'assets/admin.js',
274 array( 'jquery' ),
275 ESRN_VERSION,
276 true
277 );
278 wp_localize_script(
279 'esrn-search-replace',
280 'esrnAdminL10n',
281 array(
282 /* translators: %d: rule number starting from 1 */
283 'ruleLabel' => __( 'Rule %d', 'easy-search-replace' ),
284 'select2Placeholder' => __( 'Select post types', 'easy-search-replace' ),
285 )
286 );
287 wp_enqueue_style(
288 'esrn-search-replace',
289 ESRN_PLUGIN_URL . 'assets/admin.css',
290 array(),
291 ESRN_VERSION
292 );
293
294 // Prefer core's registered Select2 if available; otherwise fall back to bundled copy.
295 if ( wp_script_is( 'select2', 'registered' ) ) {
296 wp_enqueue_style( 'select2' );
297 wp_enqueue_script( 'select2' );
298 } else {
299 wp_enqueue_style(
300 'esrn-select2',
301 ESRN_PLUGIN_URL . 'assets/select2.css',
302 array(),
303 '4.0.13'
304 );
305 wp_enqueue_script(
306 'esrn-select2',
307 ESRN_PLUGIN_URL . 'assets/select2.js',
308 array( 'jquery' ),
309 '4.0.13',
310 true
311 );
312 }
313 }
314 add_action( 'admin_enqueue_scripts', 'esrn_enqueue_admin_scripts' );
315
316 /**
317 * Add a "Settings" link to the plugins list row.
318 *
319 * @param array $links Existing action links.
320 * @return array
321 */
322 function esrn_add_settings_link( $links ) {
323 $settings_url = admin_url( 'options-general.php?page=easy_search_replace' );
324 $settings_link = sprintf(
325 '<a href="%s">%s</a>',
326 esc_url( $settings_url ),
327 esc_html__( 'Settings', 'easy-search-replace' )
328 );
329 array_unshift( $links, $settings_link );
330 return $links;
331 }
332 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'esrn_add_settings_link' );
333