PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / trunk
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts vtrunk
2.7.7 2.7.6 2.7.5 2.7.4 trunk 1.3 2.0.4 2.0.6 2.1.91 2.2.4 2.2.7 2.2.9 2.3.1 2.3.10 2.4.10 2.4.2 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.6.0 2.6.1 2.7.0 All 28 releases
← All changes | includes/class.execute.snippet.php +769 -257 2.4.5trunk View file →
@@ -1,14 +1,12 @@
1 1 <?php
2 2 /**
3 3 * Execute snippet
4 4 *
5 - * @author Artem Prihodko <webtemyk@yandex.ru>
6 - * @copyright (c) 2020, CreativeMotion
7 - * @version 2.4
5 + * @package Woody_Code_Snippets
8 6 */
9 7
10 -// Exit if accessed directly
8 +// Exit if accessed directly.
11 9 if ( ! defined( 'ABSPATH' ) ) {
12 10 exit;
13 11 }
14 12
@@ -28,8 +26,29 @@
28 26 * @var WINP_Insertion_Locations
29 27 */
30 28 public $snippets_locations;
31 29
30 + /**
31 + * Current snippet ID being executed.
32 + *
33 + * @var int Current snippet ID being executed (for error handling).
34 + */
35 + private static $current_snippet_id = 0;
36 +
37 + /**
38 + * Indicates whether shutdown handler has been registered.
39 + *
40 + * @var bool Whether shutdown handler has been registered.
41 + */
42 + private static $shutdown_registered = false;
43 +
44 + /**
45 + * Tracked executed snippets on current page.
46 + *
47 + * @var array<int, array{id: int, name: string, type: string, location: string, scope: string}>
48 + */
49 + private $executed_snippets = [];
50 +
32 51 public static function app() {
33 52 if ( self::$instance === null ) {
34 53 self::$instance = new self();
35 54 }
@@ -37,12 +56,36 @@
37 56 return self::$instance;
38 57 }
39 58
40 59 /**
60 + * Get executed snippets on current page.
61 + *
62 + * @return array<int, array{id: int, name: string, type: string, location: string, scope: string}>
63 + */
64 + public function get_executed_snippets() {
65 + return $this->executed_snippets;
66 + }
67 +
68 + /**
41 69 * WINP_Execute_Snippet constructor.
42 70 */
43 71 public function __construct() {
44 72 self::$instance = $this;
73 +
74 + // Check if this is an AJAX validation request and skip the snippet being validated.
75 + if ( wp_doing_ajax()
76 + && isset( $_POST['action'] ) && 'wbcr_inp_ajax_validate_snippet' === $_POST['action']
77 + && isset( $_POST['post_id'] ) ) {
78 + $validating_snippet_id = (int) $_POST['post_id'];
79 + add_filter(
80 + 'winp_skip_snippet_execution',
81 + function ( $should_skip, $snippet_id ) use ( $validating_snippet_id ) {
82 + return $should_skip || $snippet_id === $validating_snippet_id;
83 + },
84 + 1,
85 + 2
86 + );
87 + }
45 88
46 89 if ( ! defined( 'WINP_UPLOAD_DIR' ) ) {
47 90 $dir = wp_upload_dir();
48 91 define( 'WINP_UPLOAD_DIR', $dir['basedir'] . '/winp-css-js' );
@@ -53,17 +96,18 @@
53 96 define( 'WINP_UPLOAD_URL', $dir['baseurl'] . '/winp-css-js' );
54 97 }
55 98 global $wpdb;
56 99
100 + // todo: Simplify your request. Seems written ugly
57 101 $sql = "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_content, p2.meta_value as priority
58 102 FROM {$wpdb->posts}
59 103 INNER JOIN {$wpdb->postmeta} p1 ON ({$wpdb->posts}.ID = p1.post_id)
60 104 INNER JOIN {$wpdb->postmeta} p2 ON ({$wpdb->posts}.ID = p2.post_id)
61 105 INNER JOIN {$wpdb->postmeta} p3 ON ({$wpdb->posts}.ID = p3.post_id)
62 - WHERE (( p1.meta_key = '" . WINP_Plugin::app()->getPrefix() . "snippet_scope' AND p1.meta_value = '%s')
106 + WHERE (( p1.meta_key = 'wbcr_inp_snippet_scope' AND p1.meta_value = '%s')
63 107 AND
64 - ( p3.meta_key = '" . WINP_Plugin::app()->getPrefix() . "snippet_activate' AND p3.meta_value = '1')
65 - AND p2.meta_key = '" . WINP_Plugin::app()->getPrefix() . "snippet_priority' )
108 + ( p3.meta_key = 'wbcr_inp_snippet_activate' AND p3.meta_value = '1')
109 + AND p2.meta_key = 'wbcr_inp_snippet_priority' )
66 110 AND {$wpdb->posts}.post_type = '" . WINP_SNIPPETS_POST_TYPE . "'
67 111 AND ({$wpdb->posts}.post_status = 'publish')
68 112 ORDER BY CAST(priority AS UNSIGNED) %s";
69 113
@@ -76,21 +120,21 @@
76 120
77 121 /**
78 122 * Register hooks
79 123 */
80 - public function registerHooks() {
81 - add_action( 'plugins_loaded', [ $this, 'executeEverywhereSnippets' ], 1 );
124 + public function register_hooks() {
125 + add_action( 'init', [ $this, 'execute_everywhere_snippets' ], 1 );
82 126
83 - if ( ! is_admin() ) { #issue PCS-45 fix bug with WPBPage Builder Frontend Editor
84 - add_action( 'wp_head', [ $this, 'executeHeaderSnippets' ] );
85 - add_action( 'wp_footer', [ $this, 'executeFooterSnippets' ] );
86 - add_filter( 'the_post', [ $this, 'executePostSnippets' ], 10, 2 );
127 + if ( ! is_admin() ) { // issue PCS-45 fix bug with WPBPage Builder Frontend Editor
128 + add_action( 'wp_head', [ $this, 'execute_header_snippets' ] );
129 + add_action( 'wp_footer', [ $this, 'execute_footer_snippets' ] );
130 + add_action( 'the_post', [ $this, 'executePostSnippets' ], 10, 2 );
87 131 add_filter( 'the_content', [ $this, 'executeContentSnippets' ] );
88 132 add_filter( 'the_excerpt', [ $this, 'executeExcerptSnippets' ] );
89 133 // Бесполезный хук, который вызывается на каждый комментарий. Если их много, увеличивается нагрузка
90 - //add_filter( 'wp_list_comments_args', [ $this, 'executeListCommentsSnippets' ] );
134 + // add_filter( 'wp_list_comments_args', [ $this, 'executeListCommentsSnippets' ] );
91 135
92 - //add_action( 'wp_head', [ $this, 'executeWoocommerceSnippets' ] );
136 + // add_action( 'wp_head', [ $this, 'executeWoocommerceSnippets' ] );
93 137
94 138 if ( ! empty( $this->snippets_locations->getInsertion( 'custom' ) ) ) {
95 139 add_action( 'wp_head', [ $this, 'executeCustomSnippets' ] );
96 140 }
@@ -99,56 +143,51 @@
99 143
100 144 /**
101 145 * Execute the everywhere snippets once the plugins are loaded
102 146 */
103 - public function executeEverywhereSnippets() {
104 - echo $this->executeActiveSnippets( 'evrywhere' );
147 + public function execute_everywhere_snippets() {
148 + echo $this->execute_active_snippets( 'evrywhere' );
105 149 }
106 150
107 151 /**
108 152 * Execute the snippets in header of page once the plugins are loaded
109 153 */
110 - public function executeHeaderSnippets() {
111 - echo $this->executeActiveSnippets( 'auto', 'header' );
154 + public function execute_header_snippets() {
155 + echo $this->execute_active_snippets( 'auto', 'header' );
112 156 }
113 157
114 158 /**
115 159 * Execute the snippets in footer of page once the plugins are loaded
116 160 */
117 - public function executeFooterSnippets() {
118 - echo $this->executeActiveSnippets( 'auto', 'footer' );
161 + public function execute_footer_snippets() {
162 + echo $this->execute_active_snippets( 'auto', 'footer' );
119 163 }
120 164
121 165 /**
122 166 * Execute the snippets before post
123 167 *
124 - * @param $data
125 - * @param $query
168 + * @param WP_Post $post
169 + * @param WP_Query $query
126 170 */
127 - public function executePostSnippets( $data, $query ) {
128 - global $post;
129 -
171 + public function executePostSnippets( $post, $query ) {
130 172 $content = '';
131 173
132 - $post_type = ! empty( $post ) ? $post->post_type : get_post( $data->ID )->post_type;
133 - if ( is_singular( [ $post_type ] ) && $post->ID == $data->ID ) {
174 + $post_type = ! empty( $post ) ? $post->post_type : get_post( $post->ID )->post_type;
175 + if ( is_singular( [ $post_type ] ) ) {
134 176 if ( did_action( 'get_header' ) ) {
135 177 // Перед заголовком
136 - $content = $this->executeActiveSnippets( 'auto', 'before_post' );
178 + $content = $this->execute_active_snippets( 'auto', 'before_post' );
137 179 }
138 - }
139 - else {
140 - if ( $query->post_count > 0 ) {
141 - if ( $query->post_count > 1 && $query->current_post > 0 && $query->post_count > $query->current_post ) {
142 - // Между записями
143 - $content = $this->executeActiveSnippets( 'auto', 'between_posts' );
144 - }
180 + } elseif ( $query->post_count > 0 ) {
181 + if ( $query->post_count > 1 && $query->current_post > 0 && $query->post_count > $query->current_post ) {
182 + // Между записями
183 + $content = $this->execute_active_snippets( 'auto', 'between_posts' );
184 + }
145 185 // Перед записью
146 - $content .= $this->executeActiveSnippets( 'auto', 'before_posts', '', $query );
186 + $content .= $this->execute_active_snippets( 'auto', 'before_posts', '', $query );
147 187
148 188 // После записи
149 - $content .= $this->executeActiveSnippets( 'auto', 'after_posts', '', $query );
150 - }
189 + $content .= $this->execute_active_snippets( 'auto', 'after_posts', '', $query );
151 190 }
152 191
153 192 echo $content;
154 193 }
@@ -164,13 +203,12 @@
164 203 * @return mixed
165 204 */
166 205 private function handleParagraphContent( $content, $snippet_content, $paragraph_number, $type = 'before' ) {
167 206 if ( 'before' == $type ) {
168 - preg_match_all( "/<p(.*?)>/", $content, $matches );
207 + preg_match_all( '/<p(.*?)>/', $content, $matches );
208 + } else {
209 + preg_match_all( '/<\/p>/', $content, $matches );
169 210 }
170 - else {
171 - preg_match_all( "/<\/p>/", $content, $matches );
172 - }
173 211 $paragraphs = $matches[0];
174 212
175 213 if ( $paragraph_number == 0 ) {
176 214 $paragraph_number = 1;
@@ -183,15 +221,13 @@
183 221 // Если указанный номер параграфа совпадает с текущим
184 222 if ( $paragraph_key + 1 == $paragraph_number ) {
185 223 if ( 'before' == $type ) {
186 224 $content = substr( $content, 0, $position ) . $snippet_content . substr( $content, $position );
187 - }
188 - else {
225 + } else {
189 226 $content = substr( $content, 0, $position + 4 ) . $snippet_content . substr( $content, $position + 4 );
190 227 }
191 228 break;
192 - }
193 - else {
229 + } else {
194 230 $offset = $position + 1;
195 231 }
196 232 }
197 233 }
@@ -201,13 +237,13 @@
201 237
202 238 /**
203 239 * Handle posts content
204 240 *
205 - * @param string $content
206 - * @param string $snippet_content
241 + * @param string $content
242 + * @param string $snippet_content
207 243 * @param integer $post_number
208 - * @param string $type
209 - * @param object $query
244 + * @param string $type
245 + * @param object $query
210 246 *
211 247 * @return mixed
212 248 */
213 249 private function handlePostsContent( $content, $snippet_content, $post_number, $type, $query ) {
@@ -218,17 +254,15 @@
218 254 }
219 255
220 256 if ( 'before' == $type && $query->current_post + 1 == $post_number ) {
221 257 return $snippet_content;
222 - }
223 - else if ( 'after' == $type ) {
258 + } elseif ( 'after' == $type ) {
224 259 // Номер поста совпадает
225 260 if ( $query->current_post == $post_number ) {
226 261 return $snippet_content;
227 262 // Если это последний пост и указанный номер поста больше общего количества постов,
228 263 // то нужно сохранить контент сниппета для вывода в конце данного поста
229 - }
230 - else if ( $query->current_post + 1 == $query->post_count && $post_number >= $query->post_count ) {
264 + } elseif ( $query->current_post + 1 == $query->post_count && $post_number >= $query->post_count ) {
231 265 $winp_after_post_content[ $query->post->ID ] = $snippet_content;
232 266 }
233 267 }
234 268 }
@@ -249,35 +283,34 @@
249 283 $post_type = ! empty( $post ) ? $post->post_type : false;
250 284
251 285 if ( is_category() || is_archive() || is_tag() || is_tax() || is_search() ) {
252 286 // Перед коротким описанием
253 - $content = $this->executeActiveSnippets( 'auto', 'before_excerpt' ) . $content;
287 + $content = $this->execute_active_snippets( 'auto', 'before_excerpt' ) . $content;
254 288
255 289 // После короткого описания
256 - $content .= $this->executeActiveSnippets( 'auto', 'after_excerpt' );
290 + $content .= $this->execute_active_snippets( 'auto', 'after_excerpt' );
257 291 }
258 292
259 293 if ( is_singular( [ $post_type ] ) ) {
260 294 // Перед параграфом
261 - $content = $this->executeActiveSnippets( 'auto', 'before_paragraph', $content );
295 + $content = $this->execute_active_snippets( 'auto', 'before_paragraph', $content );
262 296
263 297 // После параграфа
264 - $content = $this->executeActiveSnippets( 'auto', 'after_paragraph', $content );
298 + $content = $this->execute_active_snippets( 'auto', 'after_paragraph', $content );
265 299
266 300 // После заголовка
267 - $content = $this->executeActiveSnippets( 'auto', 'before_content' ) . $content;
301 + $content = $this->execute_active_snippets( 'auto', 'before_content' ) . $content;
268 302
269 303 // После текста
270 - $content .= $this->executeActiveSnippets( 'auto', 'after_content' );
304 + $content .= $this->execute_active_snippets( 'auto', 'after_content' );
271 305
272 306 // После поста
273 - $content .= $this->executeActiveSnippets( 'auto', 'after_post' );
307 + $content .= $this->execute_active_snippets( 'auto', 'after_post' );
274 308
275 309 if ( ! comments_open( $post->ID ) && ! get_comments_number( $post->ID ) ) {
276 310 remove_filter( 'wp_list_comments_args', [ $this, 'executeListCommentsSnippets' ] );
277 311 }
278 - }
279 - else if ( ! is_null( $post ) && isset( $winp_after_post_content[ $post->ID ] ) ) {
312 + } elseif ( ! is_null( $post ) && isset( $winp_after_post_content[ $post->ID ] ) ) {
280 313 // После последнего поста в списке
281 314 $content .= $winp_after_post_content[ $post->ID ];
282 315 unset( $winp_after_post_content[ $post->ID ] );
283 316 }
@@ -294,12 +327,12 @@
294 327 */
295 328 public function executeExcerptSnippets( $excerpt ) {
296 329 if ( is_category() || is_archive() || is_tag() || is_tax() || is_search() ) {
297 330 // Перед коротким описанием
298 - $excerpt = $this->executeActiveSnippets( 'auto', 'before_excerpt' ) . $excerpt;
331 + $excerpt = $this->execute_active_snippets( 'auto', 'before_excerpt' ) . $excerpt;
299 332
300 333 // После короткого описания
301 - $excerpt .= $this->executeActiveSnippets( 'auto', 'after_excerpt' );
334 + $excerpt .= $this->execute_active_snippets( 'auto', 'after_excerpt' );
302 335 }
303 336
304 337 return $excerpt;
305 338 }
@@ -329,9 +362,9 @@
329 362 */
330 363 public function executeCommentsSnippets( $comment, $args, $depth ) {
331 364 global $winp_wp_data, $post;
332 365
333 - if ( ! empty ( $winp_wp_data['winp_comments_saved_end_callback'] ) ) {
366 + if ( ! empty( $winp_wp_data['winp_comments_saved_end_callback'] ) ) {
334 367 echo call_user_func( $winp_wp_data['winp_comments_saved_end_callback'], $comment, $args, $depth );
335 368 }
336 369
337 370 $content = '';
@@ -338,9 +371,9 @@
338 371
339 372 $post_type = ! empty( $post ) ? $post->post_type : false;
340 373 if ( is_singular( [ $post_type ] ) ) {
341 374 // После комментариев
342 - $content = $this->executeActiveSnippets( 'auto', 'after_post' );
375 + $content = $this->execute_active_snippets( 'auto', 'after_post' );
343 376 }
344 377
345 378 echo $content;
346 379 }
@@ -352,9 +385,9 @@
352 385 */
353 386 public function executeCustomSnippets() {
354 387 $locations = $this->snippets_locations->getInsertion( 'custom' );
355 388 foreach ( $locations as $location => $data ) {
356 - $this->executeActiveSnippets( 'auto', $location );
389 + $this->execute_active_snippets( 'auto', $location );
357 390 }
358 391 }
359 392
360 393 /**
@@ -371,16 +404,22 @@
371 404 };
372 405
373 406 switch ( $location ) {
374 407 case 'woo_before_shop_loop':
375 - add_filter( 'woocommerce_product_loop_start', function ( $content ) use ( $snippet_content ) {
376 - return $snippet_content . $content;
377 - } );
408 + add_filter(
409 + 'woocommerce_product_loop_start',
410 + function ( $content ) use ( $snippet_content ) {
411 + return $snippet_content . $content;
412 + }
413 + );
378 414 break;
379 415 case 'woo_after_shop_loop':
380 - add_filter( 'woocommerce_product_loop_end', function ( $content ) use ( $snippet_content ) {
381 - return $content . $snippet_content;
382 - } );
416 + add_filter(
417 + 'woocommerce_product_loop_end',
418 + function ( $content ) use ( $snippet_content ) {
419 + return $content . $snippet_content;
420 + }
421 + );
383 422 break;
384 423 case 'woo_before_single_product':
385 424 add_action( 'woocommerce_before_single_product', $action, 10, 2 );
386 425 break;
@@ -434,136 +473,144 @@
434 473 *
435 474 * @param string $scope
436 475 * @param string $location
437 476 * @param string $content
438 - * @param array $custom_params
477 + * @param array $custom_params
439 478 *
440 479 * @return string
441 480 */
442 - public function executeActiveSnippets( $scope = 'evrywhere', $location = '', $content = '', $custom_params = [] ) {
443 - /*
444 - global $wpdb;
481 + public function execute_active_snippets( $scope = 'evrywhere', $location = '', $content = '', $custom_params = [] ) {
482 + $snippets = $this->snippets[ $scope ] ?? [];
445 483
446 - if ( $scope == 'evrywhere' ) {
447 - $sort = 'DESC';
448 - } else {
449 - $sort = 'ASC';
450 - }
451 - $snippets = $wpdb->get_results( "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_content, p2.meta_value as priority
452 - FROM {$wpdb->posts}
453 - INNER JOIN {$wpdb->postmeta} p1 ON ({$wpdb->posts}.ID = p1.post_id)
454 - INNER JOIN {$wpdb->postmeta} p2 ON ({$wpdb->posts}.ID = p2.post_id)
455 - INNER JOIN {$wpdb->postmeta} p3 ON ({$wpdb->posts}.ID = p3.post_id)
456 - WHERE (( p1.meta_key = '" . WINP_Plugin::app()->getPrefix() . "snippet_scope' AND p1.meta_value = '{$scope}')
457 - AND
458 - ( p3.meta_key = '" . WINP_Plugin::app()->getPrefix() . "snippet_activate' AND p3.meta_value = '1')
459 - AND p2.meta_key = '" . WINP_Plugin::app()->getPrefix() . "snippet_priority' )
460 - AND {$wpdb->posts}.post_type = '" . WINP_SNIPPETS_POST_TYPE . "'
461 - AND ({$wpdb->posts}.post_status = 'publish')
462 - ORDER BY CAST(priority AS UNSIGNED) {$sort}" );
463 - */
464 - $snippets = $this->snippets[ $scope ] ?? [];
484 + if ( ! empty( $snippets ) ) {
485 + foreach ( (array) $snippets as $snippet ) {
486 + $id = (int) $snippet->ID;
487 +
488 + // Allow filtering to skip specific snippet IDs.
489 + $should_skip = apply_filters( 'winp_skip_snippet_execution', false, $id );
490 + if ( $should_skip ) {
491 + continue;
492 + }
493 +
494 + // $is_active = (int) WINP_Helper::getMetaOption( $id, 'snippet_activate', 0 );
495 + // Если это сниппет с автовставкой и выбранное место подходит под активный action
496 + $avail_place = ( 'auto' == $scope ? $location == WINP_Helper::getMetaOption( $id, 'snippet_location', '' ) : true );
497 + // Если условие отображения сниппета выполняется
498 + $snippet_type = WINP_Helper::getMetaOption( $id, 'snippet_type', WINP_SNIPPET_TYPE_PHP );
499 + $is_condition = $snippet_type != WINP_SNIPPET_TYPE_PHP ? $this->checkCondition( $id ) : true;
465 500
466 - if ( empty( $snippets ) ) {
467 - return $content;
468 - }
501 + if ( $avail_place && $is_condition ) {
502 + $post_id = (int) WINP_HTTP::post( 'post_ID', 0 );
469 503
470 - foreach ( (array) $snippets as $snippet ) {
471 - $id = (int) $snippet->ID;
472 - //$is_active = (int) WINP_Helper::getMetaOption( $id, 'snippet_activate', 0 );
473 - // Если это сниппет с автовставкой и выбранное место подходит под активный action
474 - $avail_place = ( 'auto' == $scope ? $location == WINP_Helper::getMetaOption( $id, 'snippet_location', '' ) : true );
475 - // Если условие отображения сниппета выполняется
476 - $snippet_type = WINP_Helper::getMetaOption( $id, 'snippet_type', WINP_SNIPPET_TYPE_PHP );
477 - $is_condition = $snippet_type != WINP_SNIPPET_TYPE_PHP ? $this->checkCondition( $id ) : true;
504 + if ( ( isset( $_POST['wbcr_inp_snippet_scope'] )
505 + && $post_id === $id
506 + && WINP_Plugin::app()->current_user_car() ) || WINP_Helper::is_safe_mode() ) {
507 + return $content;
508 + }
478 509
479 - if ( $avail_place && $is_condition ) {
480 - $post_id = (int) WINP_Plugin::app()->request->post( 'post_ID', 0 );
510 + // WPML Compatibility
511 + if ( defined( 'WPML_PLUGIN_FILE' ) ) {
512 + $wpml_langs = WINP_Helper::getMetaOption( $id, 'snippet_wpml_lang', '' );
513 + if ( $wpml_langs !== '' && defined( 'ICL_LANGUAGE_CODE' ) ) {
514 + if ( ! in_array( ICL_LANGUAGE_CODE, explode( ',', $wpml_langs ) ) ) {
515 + continue;
516 + }
517 + }
518 + }
481 519
482 - if ( isset( $_POST['wbcr_inp_snippet_scope'] ) && $post_id === $id && WINP_Plugin::app()->currentUserCan() ) {
483 - return $content;
484 - }
520 + $snippet_code = WINP_Helper::get_snippet_code( $snippet );
485 521
486 - if ( WINP_Helper::is_safe_mode() ) {
487 - return $content;
488 - }
522 + /**
523 + * Filter snippet code before execute
524 + */
525 + $snippet_code = apply_filters( 'wbcr/inp/execute_snippet/snippet_code', $snippet_code, $id );
489 526
490 - // WPML Compatibility
491 - if ( defined( 'WPML_PLUGIN_FILE' ) ) {
492 - $wpml_langs = WINP_Helper::getMetaOption( $id, 'snippet_wpml_lang', '' );
493 - if ( $wpml_langs !== '' && defined( 'ICL_LANGUAGE_CODE' ) ) {
494 - if ( ! in_array( ICL_LANGUAGE_CODE, explode( ',', $wpml_langs ) ) ) {
495 - continue;
496 - }
527 + // Track this snippet as executed.
528 + $this->track_executed_snippet( $id, $scope );
529 +
530 + if ( get_option( 'wbcr_inp_execute_shortcode' ) ) {
531 + $snippet_code = do_shortcode( $snippet_code );
497 532 }
498 - }
499 533
500 - $snippet_code = WINP_Helper::get_snippet_code( $snippet );
534 + if ( $snippet_type === WINP_SNIPPET_TYPE_TEXT || $snippet_type === WINP_SNIPPET_TYPE_AD ) {
535 + $snippet_content = '<div class="winp-text-snippet-container">' . $snippet_code . '</div>';
536 + } elseif ( $snippet_type === WINP_SNIPPET_TYPE_CSS || $snippet_type === WINP_SNIPPET_TYPE_JS ) {
537 + $snippet_content = self::getJsCssSnippetData( $id );
538 + } elseif ( $snippet_type === WINP_SNIPPET_TYPE_HTML ) {
539 + $snippet_content = $snippet_code;
540 + } else {
541 + $code = $this->prepareCode( $snippet_code, $id );
542 + ob_start();
543 + $this->executeSnippet( $code, $id, false );
544 + $snippet_content = ob_get_contents();
545 + ob_end_clean();
546 + }
501 547
502 - /**
503 - * Filter snippet code before execute
504 - */
505 - $snippet_code = apply_filters( 'wbcr/inp/execute_snippet/snippet_code', $snippet_code, $id );
548 + // If the user has prohibited the insertion of unfiltered HTML,
549 + // we prohibit the execution of snippets.
550 + if ( ( defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML )
551 + && ! in_array(
552 + $snippet_type,
553 + [
554 + WINP_SNIPPET_TYPE_TEXT,
555 + WINP_SNIPPET_TYPE_AD,
556 + WINP_SNIPPET_TYPE_CSS,
557 + ]
558 + ) ) {
559 + $snippet_content = '';
506 560
507 - if ( WINP_Plugin::app()->getOption( 'execute_shortcode' ) ) {
508 - $snippet_code = do_shortcode( $snippet_code );
509 - }
561 + if ( is_user_logged_in() && WINP_Plugin::app()->current_user_car() ) {
562 + $error_text = __( 'This Woody snippet cannot run because unfiltered HTML insertion is disabled.', 'insert-php' );
510 563
511 - if ( $snippet_type === WINP_SNIPPET_TYPE_TEXT || $snippet_type === WINP_SNIPPET_TYPE_AD ) {
512 - $snippet_content = '<div class="winp-text-snippet-container">' . $snippet_code . '</div>';
513 - }
514 - else if ( $snippet_type === WINP_SNIPPET_TYPE_CSS || $snippet_type === WINP_SNIPPET_TYPE_JS ) {
515 - $snippet_content = self::getJsCssSnippetData( $id );
516 - }
517 - else if ( $snippet_type === WINP_SNIPPET_TYPE_HTML ) {
518 - $snippet_content = $snippet_code;
519 - }
520 - else {
521 - $code = $this->prepareCode( $snippet_code, $id );
522 - ob_start();
523 - $this->executeSnippet( $code, $id, false );
524 - $snippet_content = ob_get_contents();
525 - ob_end_clean();
526 - }
564 + switch ( $location ) {
565 + case 'header':
566 + case 'footer':
567 + $snippet_content = '<!-- ' . $error_text . '-->';
568 + break;
569 + default:
570 + $snippet_content = $error_text;
571 + }
572 + }
573 + }
527 574
528 - if ( 'auto' == $scope ) {
529 - switch ( $location ) {
530 - case 'before_paragraph': // Перед параграфом
531 - $location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
532 - $content = $this->handleParagraphContent( $content, $snippet_content, $location_number );
533 - break;
534 - case 'after_paragraph': // После параграфа
535 - $location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
536 - $content = $this->handleParagraphContent( $content, $snippet_content, $location_number, 'after' );
537 - break;
538 - case 'before_posts': // Перед записью
539 - $location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
540 - $content = $this->handlePostsContent( $content, $snippet_content, $location_number, 'before', $custom_params );
541 - break;
542 - case 'after_posts': // После записи
543 - $location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
544 - $content = $this->handlePostsContent( $content, $snippet_content, $location_number, 'after', $custom_params );
545 - break;
546 - default:
547 - $content = $snippet_content . $content;
548 - }
575 + if ( 'auto' == $scope ) {
576 + switch ( $location ) {
577 + case 'before_paragraph': // Перед параграфом
578 + $location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
579 + $content = $this->handleParagraphContent( $content, $snippet_content, $location_number );
580 + break;
581 + case 'after_paragraph': // После параграфа
582 + $location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
583 + $content = $this->handleParagraphContent( $content, $snippet_content, $location_number, 'after' );
584 + break;
585 + case 'before_posts': // Перед записью
586 + $location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
587 + $content = $this->handlePostsContent( $content, $snippet_content, $location_number, 'before', $custom_params );
588 + break;
589 + case 'after_posts': // После записи
590 + $location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
591 + $content = $this->handlePostsContent( $content, $snippet_content, $location_number, 'after', $custom_params );
592 + break;
593 + default:
594 + $content = $snippet_content . $content;
595 + }
549 596
550 - /**
551 - * Action for woo actions
552 - *
553 - * @param array $location Slug of the location.
554 - * @param string $snippet_content Rendered snippet content
555 - *
556 - * @since 2.4
557 - */
558 - do_action( "wbcr/woody/do_woocommerce_actions", $location, $snippet_content );
597 + /**
598 + * Action for woo actions
599 + *
600 + * @param array $location Slug of the location.
601 + * @param string $snippet_content Rendered snippet content
602 + *
603 + * @since 2.4
604 + */
605 + do_action( 'wbcr/woody/do_woocommerce_actions', $location, $snippet_content );
559 606
560 - //$this->woocommerce_actions( $location, $snippet_content );
561 - $this->custom_actions( $location, $snippet_content );
607 + // $this->woocommerce_actions( $location, $snippet_content );
608 + $this->custom_actions( $location, $snippet_content );
609 + } else {
610 + $content = $snippet_content . $content;
611 + }
562 612 }
563 - else {
564 - $content = $snippet_content . $content;
565 - }
566 613 }
567 614 }
568 615
569 616 return $content;
@@ -569,8 +616,107 @@
569 616 return $content;
570 617 }
571 618
572 619 /**
620 + * Track shortcode snippet execution.
621 + *
622 + * Public method for shortcode classes to call.
623 + *
624 + * @param int $snippet_id Snippet ID.
625 + * @return void
626 + */
627 + public function track_shortcode_snippet( $snippet_id ) {
628 + $this->track_executed_snippet( $snippet_id, 'shortcode' );
629 + }
630 +
631 + /**
632 + * Track executed snippet.
633 + *
634 + * @param int $snippet_id Snippet ID.
635 + * @param string $scope Snippet scope.
636 + * @return void
637 + */
638 + private function track_executed_snippet( $snippet_id, $scope ) {
639 + if ( isset( $this->executed_snippets[ $snippet_id ] ) ) {
640 + return; // Already tracked.
641 + }
642 +
643 + $snippet = get_post( $snippet_id );
644 + if ( ! $snippet ) {
645 + return;
646 + }
647 +
648 + $snippet_type = WINP_Helper::getMetaOption( $snippet_id, 'snippet_type', WINP_SNIPPET_TYPE_PHP );
649 + $snippet_location = WINP_Helper::getMetaOption( $snippet_id, 'snippet_location', '' );
650 +
651 + // Map location to human-readable label.
652 + $location_label = $this->get_location_label( $scope, $snippet_location );
653 +
654 + $this->executed_snippets[ $snippet_id ] = [
655 + 'id' => $snippet_id,
656 + 'name' => $snippet->post_title,
657 + 'type' => $this->get_type_label( $snippet_type ),
658 + 'location' => $location_label,
659 + 'scope' => $scope,
660 + ];
661 + }
662 +
663 + /**
664 + * Get human-readable location label.
665 + *
666 + * @param string $scope Snippet scope.
667 + * @param string $location Snippet location.
668 + * @return string
669 + */
670 + private function get_location_label( $scope, $location ) {
671 + if ( 'evrywhere' === $scope ) {
672 + return __( 'Everywhere', 'insert-php' );
673 + }
674 +
675 + if ( 'shortcode' === $scope ) {
676 + return __( 'Shortcode', 'insert-php' );
677 + }
678 +
679 + $location_labels = [
680 + 'header' => __( 'Header', 'insert-php' ),
681 + 'footer' => __( 'Footer', 'insert-php' ),
682 + 'before_post' => __( 'Before Post', 'insert-php' ),
683 + 'before_content' => __( 'Before Content', 'insert-php' ),
684 + 'before_paragraph' => __( 'Before Paragraph', 'insert-php' ),
685 + 'after_paragraph' => __( 'After Paragraph', 'insert-php' ),
686 + 'after_content' => __( 'After Content', 'insert-php' ),
687 + 'after_post' => __( 'After Post', 'insert-php' ),
688 + 'before_excerpt' => __( 'Before Excerpt', 'insert-php' ),
689 + 'after_excerpt' => __( 'After Excerpt', 'insert-php' ),
690 + 'before_posts' => __( 'Before Posts', 'insert-php' ),
691 + 'after_posts' => __( 'After Posts', 'insert-php' ),
692 + 'after_comments' => __( 'After Comments', 'insert-php' ),
693 + ];
694 +
695 + return $location_labels[ $location ] ?? ucwords( str_replace( '_', ' ', $location ) );
696 + }
697 +
698 + /**
699 + * Get human-readable type label.
700 + *
701 + * @param string $type Snippet type.
702 + * @return string
703 + */
704 + private function get_type_label( $type ) {
705 + $type_labels = [
706 + WINP_SNIPPET_TYPE_PHP => 'PHP',
707 + WINP_SNIPPET_TYPE_UNIVERSAL => 'Universal',
708 + WINP_SNIPPET_TYPE_HTML => 'HTML',
709 + WINP_SNIPPET_TYPE_CSS => 'CSS',
710 + WINP_SNIPPET_TYPE_JS => 'JS',
711 + WINP_SNIPPET_TYPE_TEXT => 'TEXT',
712 + WINP_SNIPPET_TYPE_AD => 'AD',
713 + ];
714 +
715 + return $type_labels[ $type ] ?? 'PHP';
716 + }
717 +
718 + /**
573 719 * Get js or css snippet data
574 720 *
575 721 * @param $snippet_id
576 722 *
@@ -607,9 +753,9 @@
607 753 }
608 754 }
609 755 }
610 756
611 - return "";
757 + return '';
612 758 }
613 759
614 760 /**
615 761 * Execute a snippet
@@ -616,11 +762,11 @@
616 762 *
617 763 * Code must NOT be escaped, as
618 764 * it will be executed directly
619 765 *
620 - * @param string $code The snippet code to execute
621 - * @param int $id The snippet ID
622 - * @param bool $catch_output Whether to attempt to suppress the output of execution using buffers
766 + * @param string $code The snippet code to execute.
767 + * @param int $id The snippet ID.
768 + * @param bool $catch_output Whether to attempt to suppress the output of execution using buffers.
623 769 *
624 770 * @return mixed The result of the code execution
625 771 */
626 772 public function executeSnippet( $code, $id = 0, $catch_output = true ) {
@@ -639,18 +785,35 @@
639 785 if ( empty( $snippet ) || $snippet->post_type !== WINP_SNIPPETS_POST_TYPE ) {
640 786 return false;
641 787 }
642 788
643 - $snippet_type = WINP_Helper::getMetaOption( $id, 'snippet_type', true );
789 + $snippet_type = WINP_Helper::getMetaOption( $id, 'snippet_type', true );
790 + $is_executable = in_array( $snippet_type, [ WINP_SNIPPET_TYPE_PHP, WINP_SNIPPET_TYPE_UNIVERSAL ], true );
644 791
792 + if ( $is_executable ) {
793 + // Preserve the active snippet context if execution terminates the request.
794 + self::$current_snippet_id = $id;
795 + WINP_Error_Handler::init();
796 + WINP_Error_Handler::set_current_snippet( $id, $snippet->post_title );
797 +
798 + // Register shutdown function once to catch fatal errors.
799 + if ( ! self::$shutdown_registered ) {
800 + register_shutdown_function( [ $this, 'handle_snippet_shutdown' ] );
801 + self::$shutdown_registered = true;
802 + }
803 + }
804 +
645 805 if ( $snippet_type == WINP_SNIPPET_TYPE_UNIVERSAL ) {
646 - $result = eval( "?>" . $code . "<?php " );
647 - }
648 - else if ( $snippet_type == WINP_SNIPPET_TYPE_PHP ) {
806 + $result = eval( '?>' . $code . '<?php ' );
807 + } elseif ( $snippet_type == WINP_SNIPPET_TYPE_PHP ) {
649 808 $result = eval( $code );
809 + } else {
810 + $result = ! empty( $code );
650 811 }
651 - else {
652 - $result = ! empty( $code );
812 +
813 + if ( $is_executable ) {
814 + self::$current_snippet_id = 0;
815 + WINP_Error_Handler::clear_current_snippet();
653 816 }
654 817
655 818 if ( $catch_output ) {
656 819 ob_end_clean();
@@ -659,8 +822,330 @@
659 822 return $result;
660 823 }
661 824
662 825 /**
826 + * Handle fatal errors during snippet execution.
827 + *
828 + * @return void
829 + */
830 + public function handle_snippet_shutdown() {
831 + $error = error_get_last();
832 +
833 + // If there's a fatal error and a snippet was being executed, check if it's from the snippet.
834 + if ( self::$current_snippet_id && $error && in_array( $error['type'], [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ] ) ) {
835 + // Only log if the error originated from the snippet code itself.
836 + if ( $this->is_error_from_snippet( $error ) ) {
837 + $this->log_snippet_error( self::$current_snippet_id, $error );
838 + }
839 + }
840 +
841 + // Clear the snippet ID after processing.
842 + self::$current_snippet_id = 0;
843 + }
844 +
845 + /**
846 + * Check if the error originated from the snippet code.
847 + *
848 + * @param array<string, mixed> $error Error details.
849 + *
850 + * @return bool True if error is from snippet, false otherwise.
851 + */
852 + private function is_error_from_snippet( $error ) {
853 + if ( ! isset( $error['file'] ) ) {
854 + return false;
855 + }
856 +
857 + $error_file = $error['file'];
858 +
859 + // Check if error is from eval'd code (PHP snippets executed via eval).
860 + if ( strpos( $error_file, "eval()'d code" ) !== false ) {
861 + return true;
862 + }
863 +
864 + return false;
865 + }
866 +
867 + /**
868 + * Log snippet error via admin notice
869 + *
870 + * @param int $snippet_id Snippet ID.
871 + * @param array<string, mixed> $error Error details.
872 + *
873 + * @return void
874 + */
875 + private function log_snippet_error( $snippet_id, $error ) {
876 + // Validate error array has required keys.
877 + if ( ! isset( $error['message'], $error['file'], $error['line'] ) ) {
878 + return;
879 + }
880 +
881 + // Generate unique notice ID based on snippet ID and error details.
882 + $error_signature = md5( $snippet_id . $error['message'] . $error['file'] . $error['line'] );
883 + $notice_id = 'snippet_error_' . $error_signature;
884 +
885 + // Get snippet title for better context.
886 + $snippet = get_post( $snippet_id );
887 + $snippet_title = $snippet ? $snippet->post_title : "ID {$snippet_id}";
888 +
889 + // Extract the main error message (before stack trace).
890 + $error_message = $error['message'];
891 + if ( strpos( $error_message, ' Stack trace:' ) !== false ) {
892 + $error_message = substr( $error_message, 0, strpos( $error_message, ' Stack trace:' ) );
893 + }
894 + $error_message = trim( $error_message );
895 +
896 + // Shorten file path for readability.
897 + $file_path = str_replace( ABSPATH, '', $error['file'] );
898 +
899 + // Get edit link for the snippet.
900 + $edit_link = admin_url( 'post.php?post=' . $snippet_id . '&action=edit' );
901 +
902 + // Build notice message.
903 + $message = sprintf(
904 + '<div class="winp-error-notice-header"><strong>%s</strong> %s <a href="%s" class="winp-snippet-edit-link" target="_blank">%s</a></div><details><summary>%s</summary><div class="winp-error-details"><div class="winp-error-message"><strong>%s:</strong><br><code>%s</code></div><div class="winp-error-location"><strong>%s:</strong><br>%s <span class="winp-line-number">%s %d</span></div></div></details><div class="winp-error-notice-footer"><p class="winp-dismiss-help">%s</p><button type="button" class="button winp-manual-dismiss-btn">%s</button></div>',
905 + __( 'Snippet Error Detected', 'insert-php' ),
906 + // Translators: 1: Snippet title.
907 + sprintf( __( 'Snippet "%s" caused a fatal error.', 'insert-php' ), esc_html( $snippet_title ) ),
908 + esc_url( $edit_link ),
909 + __( 'Edit Snippet', 'insert-php' ),
910 + __( 'Show error details', 'insert-php' ),
911 + __( 'Error', 'insert-php' ),
912 + esc_html( $error_message ),
913 + __( 'Location', 'insert-php' ),
914 + esc_html( $file_path ),
915 + __( 'line', 'insert-php' ),
916 + $error['line'],
917 + __( 'If you have fixed this error, you can dismiss this notice.', 'insert-php' ),
918 + __( 'Dismiss', 'insert-php' )
919 + );
920 +
921 + // Store error notice in option to be displayed on next admin page load.
922 + $pending_notices = get_option( 'winp_pending_error_notices', [] );
923 + if ( ! is_array( $pending_notices ) ) {
924 + $pending_notices = [];
925 + }
926 +
927 + // Only add if not already present (avoid race conditions).
928 + if ( ! isset( $pending_notices[ $notice_id ] ) ) {
929 + $pending_notices[ $notice_id ] = [
930 + 'message' => $message,
931 + 'type' => 'error',
932 + ];
933 +
934 + update_option( 'winp_pending_error_notices', $pending_notices, false );
935 +
936 + // Send email notification if enabled and this is a new error.
937 + $this->send_error_email( $notice_id, $snippet_id, $snippet_title, $error_message, $file_path, $error['line'], $edit_link );
938 + }
939 + }
940 +
941 + /**
942 + * Send email notification for snippet error
943 + *
944 + * @param string $error_signature Unique error identifier.
945 + * @param int $snippet_id The ID of the snippet.
946 + * @param string $snippet_title The title of the snippet.
947 + * @param string $error_message The error message.
948 + * @param string $file_path The file path where error occurred.
949 + * @param int $line_number The line number where error occurred.
950 + * @param string $edit_link Link to edit the snippet.
951 + *
952 + * @return void
953 + */
954 + private function send_error_email( $error_signature, $snippet_id, $snippet_title, $error_message, $file_path, $line_number, $edit_link ) {
955 + // Check if email notifications are enabled.
956 + $email_enabled = get_option( 'wbcr_inp_error_email_enabled' );
957 + if ( ! $email_enabled ) {
958 + return;
959 + }
960 +
961 + // Get email address.
962 + $email_address = get_option( 'wbcr_inp_error_email_address', get_option( 'admin_email' ) );
963 + if ( empty( $email_address ) || ! is_email( $email_address ) ) {
964 + return;
965 + }
966 +
967 + // Check if we've already emailed about this error.
968 + $emailed_errors = get_option( 'winp_emailed_errors', [] );
969 + if ( ! is_array( $emailed_errors ) ) {
970 + $emailed_errors = [];
971 + }
972 +
973 + // If we've already emailed about this error, skip.
974 + if ( isset( $emailed_errors[ $error_signature ] ) ) {
975 + return;
976 + }
977 +
978 + // Clean up old entries (older than 30 days).
979 + $thirty_days_ago = time() - ( 30 * DAY_IN_SECONDS );
980 + foreach ( $emailed_errors as $hash => $timestamp ) {
981 + if ( $timestamp < $thirty_days_ago ) {
982 + unset( $emailed_errors[ $hash ] );
983 + }
984 + }
985 +
986 + // Build email content.
987 + $site_name = get_bloginfo( 'name' );
988 + $admin_url = admin_url( 'edit.php?post_type=' . WINP_SNIPPETS_POST_TYPE );
989 + $subject = sprintf( '[%s] Snippet Error Detected: %s', $site_name, $snippet_title );
990 +
991 + $email_body = $this->get_error_email_template( $site_name, $snippet_id, $snippet_title, $error_message, $file_path, $line_number, $edit_link, $admin_url );
992 +
993 + // Set email headers for HTML.
994 + $headers = [
995 + 'Content-Type: text/html; charset=UTF-8',
996 + ];
997 +
998 + // Send email.
999 + $sent = wp_mail( $email_address, $subject, $email_body, $headers );
1000 +
1001 + // If email sent successfully, mark this error as emailed.
1002 + if ( $sent ) {
1003 + $emailed_errors[ $error_signature ] = time();
1004 + update_option( 'winp_emailed_errors', $emailed_errors, false );
1005 + }
1006 + }
1007 +
1008 + /**
1009 + * Get HTML email template for error notification
1010 + *
1011 + * @param string $site_name Site name.
1012 + * @param int $snippet_id Snippet ID.
1013 + * @param string $snippet_title Snippet title.
1014 + * @param string $error_message Error message.
1015 + * @param string $file_path File path.
1016 + * @param int $line_number Line number.
1017 + * @param string $edit_link Edit link.
1018 + * @param string $admin_url Admin URL.
1019 + *
1020 + * @return string HTML email template.
1021 + */
1022 + private function get_error_email_template( $site_name, $snippet_id, $snippet_title, $error_message, $file_path, $line_number, $edit_link, $admin_url ) {
1023 + ob_start();
1024 + ?>
1025 + <!DOCTYPE html>
1026 + <html>
1027 + <head>
1028 + <meta charset="UTF-8">
1029 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
1030 + <title><?php echo esc_html( __( 'Snippet Error Notification', 'insert-php' ) ); ?></title>
1031 + </head>
1032 + <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; background-color: #f0f0f1;">
1033 + <table role="presentation" style="width: 100%; border-collapse: collapse;">
1034 + <tr>
1035 + <td align="center" style="padding: 40px 20px;">
1036 + <table role="presentation" style="width: 100%; max-width: 600px; border-collapse: collapse; background-color: #ffffff; border: 1px solid #c3c4c7; border-radius: 2px;">
1037 + <!-- Header -->
1038 + <tr>
1039 + <td style="padding: 24px 30px; background-color: #2271b1; border-bottom: 1px solid #135e96;">
1040 + <h1 style="margin: 0; color: #ffffff; font-size: 20px; font-weight: 600;">
1041 + ⚠️ <?php echo esc_html( __( 'Snippet Error Detected', 'insert-php' ) ); ?>
1042 + </h1>
1043 + </td>
1044 + </tr>
1045 +
1046 + <!-- Content -->
1047 + <tr>
1048 + <td style="padding: 30px;">
1049 + <p style="margin: 0 0 16px; color: #1d2327; font-size: 14px; line-height: 1.6;">
1050 + <?php echo esc_html( __( 'Hello,', 'insert-php' ) ); ?>
1051 + </p>
1052 +
1053 + <p style="margin: 0 0 20px; color: #1d2327; font-size: 14px; line-height: 1.6;">
1054 + <?php
1055 + printf(
1056 + // translators: 1: snippet title, 2: site name.
1057 + esc_html( __( 'A fatal error has been detected in the snippet "%1$s" on your site %2$s.', 'insert-php' ) ),
1058 + '<strong>' . esc_html( $snippet_title ) . '</strong>',
1059 + '<strong>' . esc_html( $site_name ) . '</strong>'
1060 + );
1061 + ?>
1062 + </p>
1063 +
1064 + <!-- Error Details Box -->
1065 + <table role="presentation" style="width: 100%; border-collapse: collapse; margin: 24px 0; background-color: #fcf0f1; border-left: 4px solid #d63638; border-radius: 0;">
1066 + <tr>
1067 + <td style="padding: 16px 20px;">
1068 + <p style="margin: 0 0 12px; color: #8c1c13; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
1069 + <?php echo esc_html( __( 'Error Details', 'insert-php' ) ); ?>
1070 + </p>
1071 +
1072 + <div style="margin-bottom: 12px;">
1073 + <p style="margin: 0 0 6px; color: #3c434a; font-size: 13px; font-weight: 600;">
1074 + <?php echo esc_html( __( 'Error Message:', 'insert-php' ) ); ?>
1075 + </p>
1076 + <p style="margin: 0; padding: 8px 12px; background-color: #ffffff; border: 1px solid #dcdcde; color: #1d2327; font-size: 13px; font-family: Consolas, Monaco, monospace; word-break: break-word; line-height: 1.5;">
1077 + <?php echo esc_html( $error_message ); ?>
1078 + </p>
1079 + </div>
1080 +
1081 + <div style="margin-bottom: 12px;">
1082 + <p style="margin: 0 0 6px; color: #3c434a; font-size: 13px; font-weight: 600;">
1083 + <?php echo esc_html( __( 'Location:', 'insert-php' ) ); ?>
1084 + </p>
1085 + <p style="margin: 0; padding: 8px 12px; background-color: #ffffff; border: 1px solid #dcdcde; color: #1d2327; font-size: 13px; font-family: Consolas, Monaco, monospace; line-height: 1.5;">
1086 + <?php echo esc_html( $file_path ); ?> <span style="color: #646970;"><?php echo esc_html( __( 'line', 'insert-php' ) ); ?> <?php echo intval( $line_number ); ?></span>
1087 + </p>
1088 + </div>
1089 +
1090 + <div>
1091 + <p style="margin: 0 0 6px; color: #3c434a; font-size: 13px; font-weight: 600;">
1092 + <?php echo esc_html( __( 'Snippet:', 'insert-php' ) ); ?>
1093 + </p>
1094 + <p style="margin: 0; padding: 8px 12px; background-color: #ffffff; border: 1px solid #dcdcde; color: #1d2327; font-size: 13px; line-height: 1.5;">
1095 + <?php echo esc_html( $snippet_title ); ?> <span style="color: #646970;">(ID: <?php echo intval( $snippet_id ); ?>)</span>
1096 + </p>
1097 + </div>
1098 + </td>
1099 + </tr>
1100 + </table>
1101 +
1102 + <!-- Action Buttons -->
1103 + <table role="presentation" style="width: 100%; border-collapse: collapse; margin: 24px 0;">
1104 + <tr>
1105 + <td style="padding: 0;">
1106 + <a href="<?php echo esc_url( $edit_link ); ?>" style="display: inline-block; padding: 10px 20px; background-color: #2271b1; color: #ffffff; text-decoration: none; border-radius: 3px; font-weight: 500; font-size: 13px; margin-right: 8px; border: 1px solid #2271b1;">
1107 + <?php echo esc_html( __( 'Edit Snippet', 'insert-php' ) ); ?>
1108 + </a>
1109 + <a href="<?php echo esc_url( $admin_url ); ?>" style="display: inline-block; padding: 10px 20px; background-color: #f6f7f7; color: #2c3338; text-decoration: none; border-radius: 3px; font-weight: 500; font-size: 13px; border: 1px solid #c3c4c7;">
1110 + <?php echo esc_html( __( 'View All Snippets', 'insert-php' ) ); ?>
1111 + </a>
1112 + </td>
1113 + </tr>
1114 + </table>
1115 +
1116 + <p style="margin: 20px 0 0; color: #646970; font-size: 13px; line-height: 1.6;">
1117 + <?php echo esc_html( __( 'This notification is sent only once per unique error. You can disable these notifications in the plugin settings.', 'insert-php' ) ); ?>
1118 + </p>
1119 + </td>
1120 + </tr>
1121 +
1122 + <!-- Footer -->
1123 + <tr>
1124 + <td style="padding: 16px 30px; background-color: #f6f7f7; border-top: 1px solid #dcdcde;">
1125 + <p style="margin: 0; color: #646970; font-size: 12px; text-align: center;">
1126 + <?php
1127 + printf(
1128 + // translators: %s: site name.
1129 + esc_html( __( 'This email was sent by Woody Code Snippets on %s', 'insert-php' ) ),
1130 + '<strong>' . esc_html( $site_name ) . '</strong>'
1131 + );
1132 + ?>
1133 + </p>
1134 + </td>
1135 + </tr>
1136 + </table>
1137 + </td>
1138 + </tr>
1139 + </table>
1140 + </body>
1141 + </html>
1142 + <?php
1143 + $output = ob_get_clean();
1144 + return false !== $output ? $output : '';
1145 + }
1146 +
1147 + /**
663 1148 * Get property value
664 1149 *
665 1150 * @param $value
666 1151 * @param $property
@@ -668,11 +1153,10 @@
668 1153 * @return null
669 1154 */
670 1155 private function getPropertyValue( $value, $property ) {
671 1156 if ( is_object( $value ) ) {
672 - return $value->$property;
673 - }
674 - else if ( isset( $value[ $property ] ) ) {
1157 + return $value->$property ?? null;
1158 + } elseif ( isset( $value[ $property ] ) ) {
675 1159 return $value[ $property ];
676 1160 }
677 1161
678 1162 return null;
@@ -688,9 +1172,9 @@
688 1172 public function checkCondition( $snippet_id ) {
689 1173 // Итоговый результат условий
690 1174 $result = true;
691 1175 // Получаем сохранённые параметры условий
692 - $filters = get_post_meta( $snippet_id, WINP_Plugin::app()->getPrefix() . 'snippet_filters' );
1176 + $filters = get_post_meta( $snippet_id, 'wbcr_inp_snippet_filters' );
693 1177 // Если условия указаны
694 1178 if ( ! ( empty( $filters ) || isset( $filters[0] ) && empty( $filters[0] ) ) ) {
695 1179 foreach ( $filters[0] as $filter ) {
696 1180 $conditions = $this->getPropertyValue( $filter, 'conditions' );
@@ -739,10 +1223,9 @@
739 1223 */
740 1224 private function call_method( $method_name, $operator, $value ) {
741 1225 if ( method_exists( $this, $method_name ) ) {
742 1226 return $this->$method_name( $operator, $value );
743 - }
744 - else {
1227 + } else {
745 1228 return apply_filters( 'wbcr/inp/execute/check_condition', false, $method_name, $operator, $value );
746 1229 }
747 1230 }
748 1231
@@ -784,9 +1267,9 @@
784 1267
785 1268 /**
786 1269 * Prepare the code by removing php tags from beginning and end
787 1270 *
788 - * @param string $code
1271 + * @param string $code
789 1272 * @param integer $snippet_id
790 1273 *
791 1274 * @return string
792 1275 */
@@ -791,9 +1274,14 @@
791 1274 * @return string
792 1275 */
793 1276 public function prepareCode( $code, $snippet_id ) {
794 1277 $snippet_type = WINP_Helper::get_snippet_type( $snippet_id );
795 - if ( $snippet_type != WINP_SNIPPET_TYPE_UNIVERSAL && $snippet_type != WINP_SNIPPET_TYPE_CSS && $snippet_type != WINP_SNIPPET_TYPE_JS && $snippet_type != WINP_SNIPPET_TYPE_HTML ) {
1278 +
1279 + if ( $snippet_type != WINP_SNIPPET_TYPE_UNIVERSAL
1280 + && $snippet_type != WINP_SNIPPET_TYPE_CSS
1281 + && $snippet_type != WINP_SNIPPET_TYPE_JS
1282 + && $snippet_type != WINP_SNIPPET_TYPE_HTML ) {
1283 +
796 1284 /* Remove <?php and <? from beginning of snippet */
797 1285 $code = preg_replace( '|^[\s]*<\?(php)?|', '', $code );
798 1286
799 1287 /* Remove ?> from end of snippet */
@@ -808,9 +1296,9 @@
808 1296 *
809 1297 * @return string
810 1298 */
811 1299 private function getCurrentUrl() {
812 - $out = "";
1300 + $out = '';
813 1301 $url = explode( '?', $_SERVER['REQUEST_URI'], 2 );
814 1302 if ( isset( $url[0] ) ) {
815 1303 $out = trim( $url[0], '/' );
816 1304 }
@@ -823,9 +1311,9 @@
823 1311 *
824 1312 * @return string
825 1313 */
826 1314 private function getRefererUrl() {
827 - $out = "";
1315 + $out = '';
828 1316 $url = explode( '?', str_replace( site_url(), '', $_SERVER['HTTP_REFERER'] ), 2 );
829 1317 if ( isset( $url[0] ) ) {
830 1318 $out = trim( $url[0], '/' );
831 1319 }
@@ -835,29 +1323,27 @@
835 1323
836 1324 /**
837 1325 * Check by operator
838 1326 *
839 - * @param $operation
840 - * @param $first
841 - * @param $second
842 - * @param $third
1327 + * @param string $operation Comparison operator.
1328 + * @param mixed $first First value.
1329 + * @param mixed $second Second value.
1330 + * @param bool $third Third value.
843 1331 *
844 1332 * @return bool
845 1333 */
846 - public function checkByOperator( $operation, $first, $second, $third = false ) {
1334 + public function check_by_operator( $operation, $first, $second, $third = false ) {
847 1335 switch ( $operation ) {
848 1336 case 'equals':
849 1337 if ( is_array( $second ) ) {
850 1338 return in_array( $first, $second );
851 - }
852 - else {
1339 + } else {
853 1340 return $first === $second;
854 1341 }
855 1342 case 'notequal':
856 1343 if ( is_array( $second ) ) {
857 1344 return ! in_array( $first, $second );
858 - }
859 - else {
1345 + } else {
860 1346 return $first !== $second;
861 1347 }
862 1348 case 'less':
863 1349 case 'older':
@@ -886,17 +1372,16 @@
886 1372 * @return boolean
887 1373 */
888 1374 private function user_role( $operator, $value ) {
889 1375 if ( ! is_user_logged_in() ) {
890 - return $this->checkByOperator( $operator, $value, 'guest' );
891 - }
892 - else {
1376 + return $this->check_by_operator( $operator, $value, 'guest' );
1377 + } else {
893 1378 $current_user = wp_get_current_user();
894 1379 if ( ! ( $current_user instanceof WP_User ) ) {
895 1380 return false;
896 1381 }
897 1382
898 - return $this->checkByOperator( $operator, $value, $current_user->roles[0] );
1383 + return $this->check_by_operator( $operator, $value, $current_user->roles[0] );
899 1384 }
900 1385 }
901 1386
902 1387 /**
@@ -901,14 +1386,20 @@
901 1386
902 1387 /**
903 1388 * Get timestamp
904 1389 *
905 - * @param $units
906 - * @param $count
1390 + * @param string $units Time units.
1391 + * @param mixed $count Count of units.
907 1392 *
908 1393 * @return integer
909 1394 */
910 - private function getTimestamp( $units, $count ) {
1395 + private function get_timestamp( $units, $count ) {
1396 + if ( ! is_numeric( $count ) ) {
1397 + return 0;
1398 + }
1399 +
1400 + $count = (int) $count;
1401 +
911 1402 switch ( $units ) {
912 1403 case 'seconds':
913 1404 return $count;
914 1405 case 'minutes':
@@ -931,17 +1422,20 @@
931 1422
932 1423 /**
933 1424 * Get date timestamp
934 1425 *
935 - * @param $value
1426 + * @param mixed $value Date value.
936 1427 *
937 - * @return integer
1428 + * @return int|mixed Returns 0 on validation failure, timestamp calculation, or the original value
938 1429 */
939 - public function getDateTimestamp( $value ) {
1430 + public function get_date_timestamp( $value ) {
940 1431 if ( is_object( $value ) ) {
941 - return ( current_time( 'timestamp' ) - $this->getTimestamp( $value->units, $value->unitsCount ) ) * 1000;
942 - }
943 - else {
1432 + if ( ! isset( $value->units ) || ! isset( $value->unitsCount ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1433 + return 0;
1434 + }
1435 + $current_timestamp = current_datetime()->getTimestamp();
1436 + return ( $current_timestamp - $this->get_timestamp( $value->units, $value->unitsCount ) ) * 1000; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1437 + } else {
944 1438 return $value;
945 1439 }
946 1440 }
947 1441
@@ -948,10 +1442,10 @@
948 1442 /**
949 1443 * The date when the user who views your website was registered.
950 1444 * For unregistered users this date always equals to 1 Jan 1970.
951 1445 *
952 - * @param string $operator
953 - * @param string $value
1446 + * @param string $operator Comparison operator.
1447 + * @param mixed $value Comparison value (object for 'between', mixed otherwise).
954 1448 *
955 1449 * @return boolean
956 1450 */
957 1451 private function user_registered( $operator, $value ) {
@@ -956,29 +1450,50 @@
956 1450 */
957 1451 private function user_registered( $operator, $value ) {
958 1452 if ( ! is_user_logged_in() ) {
959 1453 return false;
960 - }
961 - else {
1454 + } else {
962 1455 $user = wp_get_current_user();
963 - $registered = strtotime( $user->data->user_registered ) * 1000;
1456 + $registered = strtotime( $user->data->user_registered );
1457 +
1458 + // Validate that we have a valid registration timestamp.
1459 + if ( false === $registered ) {
1460 + return false;
1461 + }
1462 +
1463 + $registered = $registered * 1000;
964 1464
965 - if ( $operator == 'equals' || $operator == 'notequal' ) {
1465 + if ( 'equals' === $operator || 'notequal' === $operator ) {
966 1466 $registered = $registered / 1000;
967 - $timestamp = round( $this->getDateTimestamp( $value ) / 1000 );
1467 + $timestamp = $this->get_date_timestamp( $value );
1468 +
1469 + if ( ! $timestamp || $timestamp <= 0 ) {
1470 + return false;
1471 + }
1472 +
1473 + $timestamp = round( $timestamp / 1000 );
968 1474
969 - return $this->checkByOperator( $operator, date( "Y-m-d", $timestamp ), date( "Y-m-d", $registered ) );
970 - }
971 - else if ( $operator == 'between' ) {
972 - $start_timestamp = $this->getDateTimestamp( $value->start );
973 - $end_timestamp = $this->getDateTimestamp( $value->end );
1475 + return $this->check_by_operator( $operator, gmdate( 'Y-m-d', (int) $timestamp ), gmdate( 'Y-m-d', (int) $registered ) );
1476 + } elseif ( 'between' === $operator ) {
1477 + if ( ! is_object( $value ) || ! isset( $value->start ) || ! isset( $value->end ) ) {
1478 + return false;
1479 + }
1480 + $start_timestamp = $this->get_date_timestamp( $value->start );
1481 + $end_timestamp = $this->get_date_timestamp( $value->end );
1482 +
1483 + if ( ! $start_timestamp || $start_timestamp <= 0 || ! $end_timestamp || $end_timestamp <= 0 ) {
1484 + return false;
1485 + }
974 1486
975 - return $this->checkByOperator( $operator, $start_timestamp, $registered, $end_timestamp );
976 - }
977 - else {
978 - $timestamp = $this->getDateTimestamp( $value );
1487 + return $this->check_by_operator( $operator, $start_timestamp, $registered, $end_timestamp );
1488 + } else {
1489 + $timestamp = $this->get_date_timestamp( $value );
1490 +
1491 + if ( ! $timestamp || $timestamp <= 0 ) {
1492 + return false;
1493 + }
979 1494
980 - return $this->checkByOperator( $operator, $timestamp, $registered );
1495 + return $this->check_by_operator( $operator, $timestamp, $registered );
981 1496 }
982 1497 }
983 1498 }
984 1499
@@ -984,10 +1499,10 @@
984 1499
985 1500 /**
986 1501 * Check the user views your website from mobile device or not
987 1502 *
988 - * @param string $operator
989 - * @param string $value
1503 + * @param string $operator Comparison operator.
1504 + * @param string $value Comparison value.
990 1505 *
991 1506 * @return boolean
992 1507 *
993 1508 * @link https://stackoverflow.com/a/4117597
@@ -996,10 +1511,9 @@
996 1511 $useragent = $_SERVER['HTTP_USER_AGENT'];
997 1512
998 1513 if ( preg_match( '/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i', $useragent ) || preg_match( '/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i', substr( $useragent, 0, 4 ) ) ) {
999 1514 return $operator === 'equals' && $value === 'yes' || $operator === 'notequal' && $value === 'no';
1000 - }
1001 - else {
1515 + } else {
1002 1516 return $operator === 'notequal' && $value === 'yes' || $operator === 'equals' && $value === 'no';
1003 1517 }
1004 1518 }
1005 1519
@@ -1013,10 +1527,9 @@
1013 1527 */
1014 1528 private function user_cookie_name( $operator, $value ) {
1015 1529 if ( isset( $_COOKIE[ $value ] ) ) {
1016 1530 return $operator === 'equals';
1017 - }
1018 - else {
1531 + } else {
1019 1532 return $operator === 'notequal';
1020 1533 }
1021 1534 }
1022 1535
@@ -1122,9 +1635,9 @@
1122 1635 break;
1123 1636 }
1124 1637 }
1125 1638
1126 - return $this->checkByOperator( $operator, $result, true );
1639 + return $this->check_by_operator( $operator, $result, true );
1127 1640 }
1128 1641
1129 1642 /**
1130 1643 * An URL of the current page where a user who views your website is located
@@ -1136,9 +1649,9 @@
1136 1649 */
1137 1650 private function location_page( $operator, $value ) {
1138 1651 $url = $this->getCurrentUrl();
1139 1652
1140 - return $url ? $this->checkByOperator( $operator, trim( $url, '/' ), trim( $value, '/' ) ) : false;
1653 + return $url ? $this->check_by_operator( $operator, trim( $url, '/' ), trim( $value, '/' ) ) : false;
1141 1654 }
1142 1655
1143 1656 /**
1144 1657 * A referrer URL which has brought a user to the current page
@@ -1150,9 +1663,9 @@
1150 1663 */
1151 1664 private function location_referrer( $operator, $value ) {
1152 1665 $url = $this->getRefererUrl();
1153 1666
1154 - return $url ? $this->checkByOperator( $operator, trim( $url, '/' ), trim( $value, '/' ) ) : false;
1667 + return $url ? $this->check_by_operator( $operator, trim( $url, '/' ), trim( $value, '/' ) ) : false;
1155 1668 }
1156 1669
1157 1670 /**
1158 1671 * A post type of the current page
@@ -1163,9 +1676,9 @@
1163 1676 * @return boolean
1164 1677 */
1165 1678 private function location_post_type( $operator, $value ) {
1166 1679 if ( is_singular() ) {
1167 - return $this->checkByOperator( $operator, $value, get_post_type() );
1680 + return $this->check_by_operator( $operator, $value, get_post_type() );
1168 1681 }
1169 1682
1170 1683 return false;
1171 1684 }
@@ -1178,9 +1691,8 @@
1178 1691 *
1179 1692 * @return boolean
1180 1693 * @since 2.2.8 The bug is fixed, the condition was not checked
1181 1694 * for tachonomies, only posts.
1182 - *
1183 1695 */
1184 1696 private function location_taxonomy( $operator, $value ) {
1185 1697 $term_id = null;
1186 1698
@@ -1187,9 +1699,9 @@
1187 1699 if ( is_tax() || is_tag() || is_category() ) {
1188 1700 $term_id = get_queried_object()->term_id;
1189 1701
1190 1702 if ( $term_id ) {
1191 - return $this->checkByOperator( $operator, intval( $value ), $term_id );
1703 + return $this->check_by_operator( $operator, intval( $value ), $term_id );
1192 1704 }
1193 1705 }
1194 1706
1195 1707 return false;
@@ -1216,10 +1728,10 @@
1216 1728 }
1217 1729 }
1218 1730
1219 1731 if ( $term_id ) {
1220 - return $this->checkByOperator( $operator, intval( $value ), $term_id );
1732 + return $this->check_by_operator( $operator, intval( $value ), $term_id );
1221 1733 }
1222 1734
1223 1735 return false;
1224 1736 }
1225 -}
1737 +}