PluginProbe
WPSSO Core – Complete Schema Markup and Meta Tags / 22.7.0
WPSSO Core – Complete Schema Markup and Meta Tags v22.7.0
22.7.0 22.6.1 22.6.0 22.5.3 22.5.2 22.5.1 22.4.0 22.3.0 22.2.1 22.2.0 22.1.3 22.1.2 22.1.1 22.1.0 22.0.0 21.13.3 21.13.2 trunk 21.13.1
wpsso / lib / com / bfo.php

bfo.php in WPSSO Core – Complete Schema Markup and Meta Tags 22.7.0, at lib/com/bfo.php

387 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * License: GPLv3
4 * License URI: https://www.gnu.org/licenses/gpl.txt
5 * Copyright 2017-2026 Jean-Sebastien Morisset (https://surniaulula.com/)
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9
10 die( 'These aren\'t the droids you\'re looking for.' );
11 }
12
13 if ( ! class_exists( 'SucomBFO' ) ) {
14
15 class SucomBFO {
16
17 private $p; // Plugin class object.
18 private $plugin_id = 'sucom';
19 private $text_domain = 'sucom';
20 private $label_transl = '';
21 private $bfo_check_id = 'check_output_buffer'; // String id to detect our check callback using __call().
22
23 /*
24 * The SucomBFO common library class may be called by more than one plugin, so track which filters have been hooked
25 * using the $filter_hooked static property, and only hook a filter once. This allows different plugins to hook
26 * different filters, but not the same filter - which would be redundant - we only need to warn about filter output
27 * once. ;-)
28 */
29 private static $filter_hooked = array();
30
31 public function __construct( $plugin = null, $plugin_id = null, $text_domain = null, $label_transl = null ) {
32
33 $this->set_config( $plugin, $plugin_id, $text_domain, $label_transl );
34 }
35
36 /*
37 * Wildcard method callbacks are added after each filter hook to check the output buffer for a non-empty string.
38 *
39 * The urlencoded wildcard suffix is used to extract the previous / reference hook prority and name.
40 */
41 public function __call( $method_name, $args ) {
42
43 if ( strpos( $method_name, $this->bfo_check_id . '_' ) === 0 ) { // Method name starts with 'check_output_buffer_'.
44
45 array_unshift( $args, $method_name ); // Set $method_name as first element.
46
47 return call_user_func_array( array( $this, '__check_output_buffer' ), $args );
48 }
49 }
50
51 /*
52 * Loop through each filter name in the $filter_names argument and add a start hook (which starts the output
53 * buffer, adds a check hook after each callback, and adds a stop output buffer hook at the end).
54 */
55 public function add_start_hooks( array $filter_names = array( 'the_content' ) ) {
56
57 global $wp_actions;
58
59 foreach ( $filter_names as $filter_name ) {
60
61 if ( empty( $wp_actions[ $filter_name ] ) ) { // Just in case - skip actions.
62
63 if ( ! isset( self::$filter_hooked[ $filter_name ] ) ) { // Only hook a filter once.
64
65 self::$filter_hooked[ $filter_name ] = true;
66
67 add_filter( $filter_name, array( $this, 'start_output_buffer' ), PHP_INT_MIN, 1 );
68 }
69 }
70 }
71 }
72
73 /*
74 * Loop through each filter name in the $filter_names argument and add remove the start, check, and stop output
75 * hooks.
76 */
77 public function remove_all_hooks( array $filter_names = array( 'the_content' ) ) {
78
79 global $wp_actions;
80
81 foreach ( $filter_names as $filter_name ) {
82
83 if ( empty( $wp_actions[ $filter_name ] ) ) { // Just in case - skip actions.
84
85 if ( isset( self::$filter_hooked[ $filter_name ] ) ) { // Skip if not already hooked.
86
87 unset( self::$filter_hooked[ $filter_name ] );
88
89 remove_filter( $filter_name, array( $this, 'start_output_buffer' ), PHP_INT_MIN, 1 );
90
91 $this->remove_check_output_hooks( $filter_name );
92
93 remove_filter( $filter_name, array( $this, 'stop_output_buffer' ), PHP_INT_MAX, 1 );
94 }
95 }
96 }
97 }
98
99 /*
100 * Runs at the beginning of a filter to start the PHP output buffer, add a check hook after each callback, and add
101 * a stop hook at the end. When the special 'all' filter is hooked, this method will be called for actions as well,
102 * so check $wp_actions to exclude actions.
103 */
104 public function start_output_buffer( $value ) {
105
106 global $wp_actions;
107
108 $filter_name = current_filter();
109
110 if ( empty( $wp_actions[ $filter_name ] ) ) { // Only check filters, not actions.
111
112 static $filter_count = array();
113
114 $filter_count[ $filter_name ] = isset( $filter_count[ $filter_name ] ) ? $filter_count[ $filter_name ]++ : 1;
115
116 if ( ob_start() ) {
117
118 if ( $filter_count[ $filter_name ] === 1 ) { // Only check output on the first run.
119
120 $this->add_check_output_hooks( $filter_name );
121
122 } elseif ( $filter_count[ $filter_name ] === 2 ) { // Remove check hooks on second run.
123
124 $this->remove_check_output_hooks( $filter_name );
125 }
126
127 add_filter( $filter_name, array( $this, 'stop_output_buffer' ), PHP_INT_MAX, 1 );
128 }
129 }
130
131 return $value;
132 }
133
134 /*
135 * Runs at the end of a filter to clean (truncate) and end (terminate) the output buffer.
136 */
137 public function stop_output_buffer( $value ) {
138
139 ob_end_clean();
140
141 return $value;
142 }
143
144 /*
145 * Called once by start_output_buffer() at the beginning of a filter to add a check hook after each callback.
146 */
147 private function add_check_output_hooks( $filter_name ) {
148
149 global $wp_filter;
150
151 if ( isset( $wp_filter[ $filter_name ]->callbacks ) ) {
152
153 $bfo_check_str = '_' . __CLASS__ . '::' . $this->bfo_check_id; // '_SucomBFO::check_output_buffer'
154
155 foreach ( $wp_filter[ $filter_name ]->callbacks as $hook_prio => &$hook_group ) { // Use reference to modify $hook_group.
156
157 $new_hook_group = array(); // Create a new group to insert a check after each hook.
158
159 foreach ( $hook_group as $hook_ref => $hook_info ) {
160
161 $new_hook_group[ $hook_ref ] = $hook_info; // Add the original callback first, followed by the check.
162
163 $hook_name = self::get_filter_hook_function( $hook_info );
164
165 if ( $hook_name === '' ) { // Just in case.
166
167 continue;
168
169 } elseif ( strpos( $hook_name, __CLASS__ . '::' ) === 0 ) { // Exclude our own class methods from being checked.
170
171 continue;
172
173 } elseif ( false !== strpos( $hook_ref, $bfo_check_str ) ) { // Just in case - don't check the check hooks.
174
175 continue;
176 }
177
178 $check_ref = $hook_ref . $bfo_check_str; // Include the previous hook ref for visual clue.
179
180 $check_arg = urlencode( '[' . $hook_prio . ']' . $hook_name ); // Include previous hook priority and name.
181
182 $new_hook_group[ $check_ref ] = array(
183 'function' => array(
184 $this,
185 $this->bfo_check_id . '_' . $check_arg // Hooks the __call() method.
186 ),
187 'accepted_args' => 1,
188 );
189 }
190
191 $hook_group = $new_hook_group;
192 }
193 }
194 }
195
196 /*
197 * Remove the output check hooks if/when a filter is applied a second time.
198 */
199 private function remove_check_output_hooks( $filter_name ) {
200
201 global $wp_filter;
202
203 if ( isset( $wp_filter[ $filter_name ]->callbacks ) ) {
204
205 $bfo_check_str = '_' . __CLASS__ . '::' . $this->bfo_check_id;
206
207 foreach ( $wp_filter[ $filter_name ]->callbacks as $hook_prio => &$hook_group ) { // Use reference to modify $hook_group.
208
209 foreach ( $hook_group as $hook_ref => $hook_info ) {
210
211 if ( false !== strpos( $hook_ref, $bfo_check_str ) ) {
212
213 unset( $hook_group[ $hook_ref ] );
214 }
215 }
216 }
217 }
218 }
219
220 /*
221 * Set property values for text domain, notice label, etc.
222 */
223 private function set_config( $plugin = null, $plugin_id = null, $text_domain = null, $label_transl = null ) {
224
225 if ( $plugin !== null ) {
226
227 $this->p =& $plugin;
228
229 if ( ! empty( $this->p->debug->enabled ) ) {
230
231 $this->p->debug->mark();
232 }
233 }
234
235 if ( $plugin_id !== null ) {
236
237 $this->plugin_id = $plugin_id;
238
239 } elseif ( ! empty( $this->p->id ) ) {
240
241 $this->plugin_id = $this->p->id;
242 }
243
244 if ( $text_domain !== null ) {
245
246 $this->text_domain = $text_domain;
247
248 } elseif ( ! empty( $this->p->cf[ 'plugin' ][ $this->plugin_id ][ 'text_domain' ] ) ) {
249
250 $this->text_domain = $this->p->cf[ 'plugin' ][ $this->plugin_id ][ 'text_domain' ];
251 }
252
253 if ( $label_transl !== null ) {
254
255 $this->label_transl = $label_transl; // Argument is already translated.
256
257 } elseif ( ! empty( $this->p->cf[ 'menu' ][ 'title' ] ) ) {
258
259 $this->label_transl = sprintf( __( '%s Notice', $this->text_domain ),
260 _x( $this->p->cf[ 'menu' ][ 'title' ], 'menu title', $this->text_domain ) );
261
262 } else {
263
264 $this->label_transl = __( 'Notice', $this->text_domain );
265 }
266 }
267
268 /*
269 * Called by the __call() method after each filter hook. Checks the output buffer for any non-empty string.
270 */
271 private function __check_output_buffer( $method_name, $value ) {
272
273 $output = ob_get_contents();
274
275 /*
276 * Check if the previous hook has contributed some output.
277 */
278 if ( $output !== '' ) {
279
280 $error_text = __( 'The "%1$s" hook with priority %2$d in the "%3$s" filter has incorrectly sent output to the webpage.',
281 $this->text_domain ) . ' ';
282
283 $error_text .= __( 'Unlike WordPress actions, WordPress filters must always return their text, not echo it to the webpage output.',
284 $this->text_domain ) . ' ';
285
286 $error_text .= __( 'Please contact the author of that filter and report this issue as a coding error.',
287 $this->text_domain );
288
289 if ( preg_match( '/^' . $this->bfo_check_id . '_\[([0-9]+)\](.+)$/', urldecode( $method_name ), $matches ) ) {
290
291 $error_pre = sprintf( '%s error:', __METHOD__ );
292
293 $error_msg = sprintf( $error_text, $matches[ 2 ], $matches[ 1 ], current_filter() );
294
295 /*
296 * Filters are rarely applied on the admin / back-end side, but if they are, then take
297 * advantage of this and show a notice. :)
298 */
299 if ( is_admin() ) {
300
301 if ( isset( $this->p->notice ) ) {
302
303 /*
304 * Add notice only if the admin notices have not already been shown.
305 */
306 if ( $this->p->notice->is_admin_pre_notices() ) {
307
308 $this->p->notice->err( $error_msg );
309 }
310
311 } else {
312
313 $lib_dir = trailingslashit( realpath( dirname( __FILE__ ) ) );
314
315 require_once $lib_dir . 'com/notice.php'; // Load the SucomNotice class.
316
317 $notice = new SucomNotice( $this->p, $this->plugin_id, $this->text_domain, $this->label_transl );
318
319 /*
320 * Add notice only if the admin notices have not already been shown.
321 */
322 if ( $notice->is_admin_pre_notices() ) {
323
324 $notice->err( $error_msg );
325 }
326 }
327 }
328
329 $output_msg = __( 'Incorrect webpage output:', $this->text_domain ) . "\n" .
330 '-----' . __( 'BEGIN OUTPUT', $this->text_domain ) . '-----' . "\n" . print_r( $output, true ) . "\n" .
331 '-----' . __( 'END OUTPUT', $this->text_domain ) . '-----' . "\n";
332
333 /*
334 * Use SucomUtil::safe_error_log() if available to define the debug.log path and prevent
335 * the error message from being displayed in the webpage.
336 */
337 if ( method_exists( 'SucomUtil', 'safe_error_log' ) ) {
338
339 SucomUtil::safe_error_log( $error_pre . ' ' . $error_msg . "\n" . $output_msg );
340
341 } else error_log( $error_pre . ' ' . $error_msg . "\n" . $output_msg );
342 }
343
344 ob_clean(); // Clean the output buffer for the next hook check.
345 }
346
347 return $value;
348 }
349
350 private static function get_filter_hook_function( array $hook_info ) {
351
352 $hook_name = '';
353
354 if ( isset( $hook_info[ 'function' ] ) ) {
355
356 if ( is_array( $hook_info[ 'function' ] ) ) {
357
358 $class_name = '';
359 $function_name = '';
360
361 if ( is_object( $hook_info[ 'function' ][ 0 ] ) ) {
362
363 $class_name = get_class( $hook_info[ 'function' ][ 0 ] );
364
365 } elseif ( is_string( $hook_info[ 'function' ][ 0 ] ) ) {
366
367 $class_name = $hook_info[ 'function' ][ 0 ];
368 }
369
370 if ( is_string( $hook_info[ 'function' ][ 1 ] ) ) {
371
372 $function_name = $hook_info[ 'function' ][ 1 ];
373 }
374
375 $hook_name = $class_name . '::' . $function_name;
376
377 } elseif ( is_string( $hook_info[ 'function' ] ) ) {
378
379 $hook_name = $hook_info[ 'function' ];
380 }
381 }
382
383 return $hook_name;
384 }
385 }
386 }
387