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
← All changes | lib/com/bfo.php +386 -0 22.4.022.7.0 View file →
@@ -1,0 +1,386 @@
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 +}