PluginProbe
Microthemer Lite – Visual Editor to Customize CSS / trunk
Microthemer Lite – Visual Editor to Customize CSS vtrunk
trunk 5.0.0.2
microthemer / src / Logic.php

Logic.php in Microthemer Lite – Visual Editor to Customize CSS trunk, at src/Logic.php

1,206 lines 31.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Microthemer;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 /*
10 * Logic
11 *
12 * Evaluate a PHP syntax conditional expression as a text string without using eval()
13 * Supports a handful of WP functions and: ||, or, &&, and, (, ), !, =
14 * Test String: is_page('test') and is_page("some-slug") && ! has_category() or has_tag() || is_date() === 23 or is_date() !== 'My string'
15 * Test Regex: (?:(!)?\s*([a-z_]+)\('?"?(.*?)'?"?\))|(and|&&)|(or|\|\|)|([=!]{2,3})|(\d+)|(?:['"]{1}(.+?)['"]{1})
16 */
17
18 class Logic {
19
20 protected $test = false;
21
22 // we cache condition results at various levels of granularity for maximum performance
23 public static $cache = array(
24 'conditions' => array(),
25 'statements' => array(),
26 'functions' => array(),
27 );
28 protected $statementCount = 0;
29 protected $settings = array();
30
31 // parenthesis parsing variables
32 protected $stack = null;
33 protected $current = null;
34 protected $string = null;
35 protected $position = null;
36 protected $buffer_start = null;
37 protected $length;
38
39 // Regex patterns for reading logic
40 protected $patterns = array(
41 "andOrSurrSpace" => '/\s+\b(and|AND|or|OR)\b(?=(?:[^"\']*(?:"[^"]*"|\'[^\']*\'))*[^"\']*$)\s+/', // exclude if inside double quotes
42 "functionName" => "(!)?\s*[a-zA-Z_\\\\]+",
43 "comparison" => "/\s*(?<comparison><=|<|>|>=|!==?|===?)\s*/",
44 "expressions" => array(
45 "(?:(?<negation>!)?\s*(?<functionName>[a-zA-Z_\\\\]+)\((?<parameter>.*?)\))",
46 "(?:[$]_?(?<global>GET)\['?\"?(?<key>.*?)'?\"?\])",
47 "(?<string>['\"].+?['\"])",
48 "(?<boolean>true|false|null|TRUE|FALSE|NULL)",
49 "(?<number>-?\d+)",
50
51 )
52 );
53
54 // PHP functions the user is allowed to use in the logic
55 protected $allowedFunctions = array(
56 'get_post_type',
57 'has_action',
58 'has_block',
59 'has_category',
60 'has_filter',
61 'has_meta',
62 'has_post_format',
63 'has_tag',
64 'is_404',
65 'is_admin',
66 'is_archive',
67 'is_author',
68 'is_category',
69 'is_date',
70 'is_front_page',
71 'is_home',
72 'is_page',
73 'is_post_type_archive',
74 'is_search',
75 'is_single',
76 'is_singular',
77 'is_super_admin',
78 'is_tag',
79 'is_tax',
80 'is_login',
81 'is_user_logged_in',
82
83 // custom namespaced Microthemer functions
84 '\\'.__NAMESPACE__.'\has_template',
85 '\\'.__NAMESPACE__.'\is_active',
86 '\\'.__NAMESPACE__.'\is_admin_page',
87 '\\'.__NAMESPACE__.'\is_post_or_page',
88 '\\'.__NAMESPACE__.'\is_public',
89 '\\'.__NAMESPACE__.'\is_public_or_admin',
90 '\\'.__NAMESPACE__.'\match_url_path',
91 '\\'.__NAMESPACE__.'\query_admin_screen',
92 '\\'.__NAMESPACE__.'\user_has_role',
93
94 // native PHP
95 'isset',
96 );
97
98 public function getAllowedPHPSyntax(){
99 return array(
100 'functions' => $this->allowedFunctions,
101 'superglobals' => $this->allowedSuperglobals,
102 'characters' => 'or | and & ( ) ! = > <'
103 );
104 }
105
106 protected $allowedSuperglobals = array(
107 '$_GET',
108 );
109
110 function __construct($settings = array()){
111
112 Logic::$cache['settings'] = $settings;
113
114 // maybe allow user defined whitelist of functions here
115 }
116
117 // normalise &&, || for simpler regex and logical comparisons
118 protected function normaliseAndOr($string){
119
120 return str_replace(
121 array("&&", "||"),
122 array("and", "or"),
123 $string
124 );
125 }
126
127 protected $quotedStringPlaceholders = array();
128
129 // brackets inside quotes cause issues for regex, so we can temp remove quoted strings to simplify
130 protected function removeQuotedStrings($string) {
131
132 // Clear existing placeholders
133 $this->quotedStringPlaceholders = array();
134
135 $placeholderPrefix = 'QSP_';
136
137 return preg_replace_callback(
138 //'/"[^"]*"/',
139 //'([\'"])[^\'"]*\1',
140 "/(['\"])(?:\\\\.|[^\\\\])*?\\1/",
141 function ($matches) use ($placeholderPrefix) {
142 $key = $placeholderPrefix . count($this->quotedStringPlaceholders);
143 $this->quotedStringPlaceholders[$key] = $matches[0];
144 return $key;
145 },
146 $string
147 );
148 }
149
150 protected function restoreQuotedStrings($string, $quotedStringPlaceholders = false) {
151 if (!$quotedStringPlaceholders){
152 $quotedStringPlaceholders = $this->quotedStringPlaceholders;
153 }
154 return str_replace(array_keys($quotedStringPlaceholders), array_values($quotedStringPlaceholders), $string);
155 }
156
157
158 protected function addCarets($string) {
159
160 // Step 1: Replace quoted strings with placeholders
161 $modifiedString = $this->removeQuotedStrings($string);
162
163 // Step 2: Replace brackets with carets
164 $modifiedString = preg_replace(
165 "/(".$this->patterns['functionName'].")\((.*?)\)/s",
166 '$1^^$3^^',
167 $modifiedString
168 );
169
170 // Step 3: Put the quoted strings back
171 return $this->restoreQuotedStrings($modifiedString);
172 }
173
174
175 protected function removeCarets($string){
176
177 return preg_replace(
178 "/(".$this->patterns['functionName'].")\^\^(.*?)\^\^/s",
179 '$1($3)',
180 $string
181 );
182 }
183
184 protected function splitStatements($value){
185
186 return preg_split(
187 $this->patterns["andOrSurrSpace"],
188 trim($value),
189 -1,
190 PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE
191 );
192 }
193
194 protected function push(){
195
196 if ($this->buffer_start !== null) {
197
198 // extract string from buffer start to current position
199 $buffer = substr($this->string, $this->buffer_start, $this->position - $this->buffer_start);
200
201 // clean buffer
202 $this->buffer_start = null;
203
204 // throw token into current scope
205 $statementsArray = $this->splitStatements(
206 $this->removeCarets(
207 $buffer
208 )
209 );
210
211 if (count($statementsArray)){
212 $this->current = array_merge($this->current, $statementsArray);
213 }
214 }
215 }
216
217 // Tease apart parenthesis groups
218 protected function parseStatements($string){
219 return $this->parse($string);
220 }
221
222 // walk over a multidimensional array recursively, applying a callback on non-array values
223 protected function traverseStatements(&$array, $callback, $level = 0){
224
225 $result = false;
226
227 foreach ($array as $index => &$value){
228
229 // if we are on a parenthesis group, get the result of the group
230 if (is_array($value)){
231
232 $result = $this->traverseStatements($value, $callback, (++$level));
233
234 if (Helper::$doDebug){
235 Helper::debug('Group result ', array(
236 'result' => $result,
237 'group' => $value
238 ));
239 }
240
241 }
242
243 // get the result of the individual statement
244 else {
245
246 // simply move onto the next statement if we're on and/or
247 if ($value === 'and' || $value === 'or' || $value === 'AND' || $value === 'OR'){
248 continue;
249 }
250
251 // check result
252 $result = $this->evaluateStatement($value);
253 $resultString = $result
254 ? 'true'
255 : ($result === null ? 'null' : 'false');
256
257 // now that we have processed the logical statement, add some debug info
258 $array[$index].= ' ['.$resultString.']';
259
260 if (Helper::$doDebug){
261 Helper::debug('Statement result ('.$value.'): '.$result);
262 }
263
264 }
265
266 // look for the following and/or and possibly return early
267 $nextIndex = $index + 1;
268 $nextStatement = isset($array[$nextIndex]) ? $array[$nextIndex] : false;
269
270 if (
271 !$nextStatement ||
272 ($result && ($nextStatement === 'or' || $nextStatement === 'OR')) ||
273 (!$result && ($nextStatement === 'and' || $nextStatement === 'AND'))
274 ){
275
276 // mark final result
277 if (!is_array($array[$index])){
278 $array[$index].= '[result]';
279 }
280
281 return $result;
282 }
283
284 }
285
286 return $result;
287 }
288
289 protected function parseStatement($string, $doReplacement = false){
290
291 // temp remove quoted strings
292 if ($doReplacement){
293 $string = $this->removeQuotedStrings($string);
294 }
295
296 preg_match(
297 "/" . implode('|', $this->patterns['expressions']) . "/s",
298 $string,
299 $matches
300 );
301
302 /*if (Helper::$doDebug){
303 Helper::debug('Parse Statement pattern / string', array(
304 'pattern' => $pattern,
305 'string' => $string,
306 '$matches' => $matches
307 ));
308 }*/
309
310 // restore quoted strings
311 if ($doReplacement && !empty($matches['parameter'])) {
312 $matches[0] = $this->restoreQuotedStrings($matches[0]);
313 $matches[3] = $matches['parameter'] = $this->restoreQuotedStrings($matches['parameter']);
314 }
315
316 return $matches;
317 }
318
319 protected function statementResult($parsedStatement){
320
321 if (Helper::$doDebug){
322 Helper::debug('Statement parsed in callback', $parsedStatement);
323 }
324
325 $result = false;
326
327 // query any GET values
328 $global = isset($parsedStatement['global']) ? $parsedStatement['global'] : false;
329 if ($global){
330
331 $key = $parsedStatement['key'];
332
333 if (!$key){
334 return false;
335 }
336
337 if ($global == 'GET'){
338 // Logic compares arbitrary scalar/array values; null filtering preserves valid URL/path punctuation.
339 $result = isset($_GET[$key])
340 ? map_deep(wp_unslash($_GET[$key]), 'wp_kses_no_null') // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized via map_deep callback
341 : false;
342 }
343
344 }
345
346 // query any allowed function results
347 $functionName = isset($parsedStatement['functionName']) ? $parsedStatement['functionName'] : false;
348 if ($functionName){
349
350 // bail if the function isn't allowed, or doesn't exist
351 if (
352 !in_array($functionName, $this->allowedFunctions) || !function_exists($functionName)
353 //(!function_exists($functionName) && !function_exists( 'Microthemer\\' .$functionName))
354 ){
355 if (Helper::$doDebug){
356 Helper::debug('Disallowed or does not exist:', [
357 '$functionName' => $functionName,
358 'not allowed' => !in_array($functionName, $this->allowedFunctions),
359 'does not exist' => !function_exists($functionName)
360 ]);
361 }
362
363 return null;
364 }
365
366 $parameter = isset($parsedStatement['parameter']) ? $parsedStatement['parameter'] : '';
367 $parameters = $parameter
368 ? preg_split("/\s*,\s*/", $parameter)
369 : array();
370
371 if (Helper::$doDebug){
372 Helper::debug('Parameter Strings', $parameters);
373 }
374
375 // native PHP functions cannot be called with call_user_func_array (as not user function)
376 if ($functionName === 'isset'){
377
378 // we have a parameter
379 if (isset($parameters[0])){
380
381 $parsedParameter = $this->parseStatement($parameters[0]);
382 $globalParameter = isset($parsedParameter['global']) ? $parsedParameter['global'] : false;
383
384 // we have a global parameter
385 if ($globalParameter){
386
387 $key = $parsedParameter['key'];
388
389 if (!$key){
390 return false;
391 }
392
393 if ($globalParameter == 'GET'){
394 $result = isset($_GET[$key]);
395 }
396 }
397 }
398
399 // no parameter, so false
400 else {
401 $result = null;
402 }
403 }
404
405 // run function
406 else {
407
408 $cacheKey = $functionName . '('.$parameter.')';
409
410 // draw from function call cache if available
411 if (isset(Logic::$cache['functions'][$cacheKey])){
412
413 $result = Logic::$cache['functions'][$cacheKey];
414
415 if (Helper::$doDebug){
416 Helper::debug('Pulling function result from cache:', array(
417 'function' => $cacheKey,
418 'result' => $result,
419 ));
420 }
421
422 }
423
424 else {
425
426 // convert parameter strings to PHP result
427 foreach ($parameters as $i => $parameterString){
428
429 $parsedParameter = $this->parseStatement($parameterString);
430
431 if (!$parsedParameter){
432 if (Helper::$doDebug){
433 Helper::debug('Cannot parse $parameterString: ' . $parameterString);
434 }
435 } else {
436 $parameters[$i] = $this->statementResult($parsedParameter);
437 }
438 }
439
440 if (Helper::$doDebug){
441 Helper::debug('Parameters converted', $parameters);
442 }
443
444 $result = call_user_func_array(
445 $functionName,
446 $parameters
447 );
448
449 Logic::$cache['functions'][$cacheKey] = $result;
450 }
451
452 }
453
454 // reverse result if negation has been used e.g. !is_page(20)
455 $negation = isset($parsedStatement['negation']) && $parsedStatement['negation'];
456
457 if ($negation){
458 $result = !$result;
459 }
460
461 }
462
463 // boolean
464 $boolean = isset($parsedStatement['boolean']) ? $parsedStatement['boolean'] : false;
465 if ($boolean){
466 $result = $boolean === 'true';
467 }
468
469 // number
470 $number = isset($parsedStatement['number']) ? $parsedStatement['number'] : false;
471 if ($number){
472 $result = strpos($number, '.') === false ? intval($number) : floatval($number);
473 }
474
475 // string
476 $string = isset($parsedStatement['string']) ? $parsedStatement['string'] : false;
477 if ($string){
478 $result = str_replace(array('"', "'"), '', $string);
479 }
480
481 return $result;
482 }
483
484 protected function evaluateStatement($value){
485
486 // split the statement on the comparison (e.g. ===)
487 $results = preg_split(
488 $this->patterns["comparison"],
489 trim($value),
490 -1,
491 PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE
492 );
493
494 $comparison = false;
495
496 if (Helper::$doDebug){
497 Helper::debug('Split on any comparison', $results);
498 }
499
500 foreach ($results as $index => $part){
501
502 if ($index === 1){
503 $comparison = $part;
504 }
505
506 // process the result of the statement
507 else {
508
509 // draw from statement cache if available
510 if (isset(Logic::$cache['statements'][$part])){
511
512 $results[$index] = Logic::$cache['statements'][$part];
513
514 if (Helper::$doDebug){
515 Helper::debug('Pulling statement result from cache:', array(
516 'statement' => $part,
517 'result' => $results[$index],
518 ));
519 }
520
521 }
522
523 // statement needs to be run
524 else {
525 $parsedStatement = $this->parseStatement($part, true);
526
527 if (!$parsedStatement) {
528 if (Helper::$doDebug){
529 Helper::debug( 'Cannot parse statement: ' . $part);
530 }
531
532 $results[$index] = null;
533 } else {
534 if (Helper::$doDebug){
535 Helper::debug( 'Could parse statement: ' . $part, $parsedStatement);
536 }
537
538 $results[$index] = $this->statementResult($parsedStatement);
539 }
540
541 // cache result so evaluation of the same statement only happens once
542 Logic::$cache['statements'][$part] = $results[$index];
543 }
544
545 }
546 }
547
548 if (Helper::$doDebug){
549 Helper::debug('Processed statement results:', $results);
550 }
551
552 // return comparison if defined and we have two values
553 if ($comparison && count($results) > 2){
554
555 $a = $results[0];
556 $b = $results[2];
557
558 switch ($comparison) {
559 case '==':
560 return $a == $b;
561 case '===':
562 return $a === $b;
563 case '!=':
564 return $a != $b;
565 case '!==':
566 return $a !== $b;
567 case '>':
568 return $a > $b;
569 case '<':
570 return $a < $b;
571 case '>=':
572 return $a >= $b;
573 case '<=':
574 return $a <= $b;
575 default:
576 return false;
577 }
578
579 }
580
581 // otherwise simply return the first result
582 return isset($results[0])
583 ? $results[0]
584 : null;
585
586 }
587
588 public function parse($string){
589
590 if (!$string) {
591 return array();
592 }
593
594 $this->current = array();
595 $this->stack = array();
596 $quotesOpen = array();
597
598 // use caret ^^ placeholder for function parenthesis we don't create an extra group
599 // and replace && with and for simpler regex/logic
600 $string = $this->normaliseAndOr(
601 $this->addCarets(
602 trim($string)
603 )
604 );
605
606 $this->string = $string;
607 $this->length = strlen($this->string);
608
609 if (Helper::$doDebug){
610 Helper::debug('About to parse string', array('string' => $this->string, 'length' => $this->length));
611 }
612
613 // look at each character
614 for ($this->position=0; $this->position < $this->length; $this->position++) {
615
616 $char = $this->string[$this->position];
617 $isInsideQuotes = count($quotesOpen);
618
619 switch ($char) {
620 case '(':
621 if (!$isInsideQuotes){
622 $this->push();
623 // push current scope to the stack and begin a new scope
624 $this->stack[] = $this->current;
625 $this->current = array();
626 }
627 break;
628
629 case ')':
630 if (!$isInsideQuotes){
631 $this->push();
632 // save current scope
633 $t = $this->current;
634 $this->current = array_pop($this->stack);
635
636 // add just saved scope to current scope
637 if (count($t)){
638
639 // get the last scope from stack
640 $this->current[] = $t;
641 break;
642 }
643 }
644
645 if (Helper::$doDebug){
646 Helper::debug('It is )', array('$isInsideQuotes' => $isInsideQuotes, '$char' => $char, '$this->position', $this->position, '$this->current', $this->current));
647 }
648
649 break;
650
651 default:
652 // remember the offset to do a string capture later
653 // could've also done $buffer .= $string[$position]
654 // but that would just be wasting resources…
655 if ($this->buffer_start === null) {
656 $this->buffer_start = $this->position;
657 }
658
659 // flag if we are inside quotes
660 if ($char === "'" || $char === '"'){
661 $altQuote = $char === '"' ? "'" : '"';
662 if (isset($quotesOpen[$char])){
663 unset($quotesOpen[$char]);
664 } else {
665 // if we are not inside the other type of quote (which would escape it)
666 if (!isset($quotesOpen[$altQuote])){
667 $quotesOpen[$char] = 1;
668 }
669 }
670 }
671
672 }
673 }
674
675 // catch any trailing text
676 if ($this->buffer_start <= $this->position) {
677 $this->push();
678 }
679
680 return $this->current;
681 }
682
683 public function evaluate($statementsArray, $string, $test, $fileExists = null){
684
685 $result = $this->traverseStatements($statementsArray, 'evaluateStatement');
686
687 if (Helper::$doDebug){
688 Helper::debug('Debug', array(
689 'result' => $result,
690 'load' => $result ? 'Yes' : 'No',
691 'logic' => $string,
692 'num_statements' => count($statementsArray, COUNT_RECURSIVE),
693 'analysis' => '<pre>'.print_r($statementsArray, 1).'</pre>',
694 //'cache' => Logic::$cache
695 ), false);
696 }
697
698
699 return !$test
700 ? $result
701 : array(
702 'fileExists' => $fileExists,
703 'empty' => !$fileExists,
704 'blocksOnly' => $result === 'blocksOnly',
705 'resultIsString' => is_string($result) ? $result : false, // e.g. blocksOnly
706 'result' => $result,
707 'resultString' => $result
708 ? 'true'
709 : ($result === null ? 'null' : 'false'),
710 'load' => $result ? 'Yes' : 'No',
711 'logic' => $string,
712 'num_statements' => $this->countNumStatements($statementsArray),
713 'analysis' => '<pre>'.print_r($statementsArray, 1).'</pre>'
714 );
715
716 }
717
718 public function countNumStatements($array){
719
720 foreach ($array as $value) {
721
722 if ( is_array( $value ) ) {
723 $this->countNumStatements($value);
724 } else {
725 if ($value === 'and' || $value === 'or' || $value === 'AND' || $value === 'OR'){
726 continue;
727 }
728 ++$this->statementCount;
729 }
730 }
731
732 return $this->statementCount;
733 }
734
735 public function result($string, $test = false, $fileExists = null){
736
737 if (Helper::$doDebug){
738 Helper::debug('String received: ' . $string);
739 }
740
741 $result = null;
742 $error = false;
743 $statementsArray = $this->parseStatements($string);
744
745 if (Helper::$doDebug){
746 Helper::debug('statementsArray', $statementsArray);
747 }
748
749 // draw from full conditional statements string cache if available
750 if (isset(Logic::$cache['conditions'][$string])){
751
752 $result = Logic::$cache['conditions'][$string];
753
754 if (Helper::$doDebug){
755 Helper::debug('Pulling condition result from cache:', array(
756 'condition' => $string,
757 'result' => $result,
758 ));
759 }
760
761 }
762
763 else {
764
765 // Running a function could result in an error which we should capture but suppress
766 try {
767 $result = $this->evaluate($statementsArray, $string, $test, $fileExists);
768 }
769
770 // 'Throwable' is executed in PHP 7+, but ignored in lower PHP versions
771 catch (\Throwable $t) {
772 $error = $t->getMessage();
773 }
774
775 // 'Exception' is executed in PHP 5, this will not be reached in PHP 7+
776 catch (\Exception $e) {
777 $error = $e->getMessage();
778 }
779 }
780
781
782 // return error result if a PHP exception occurs - this should fail silently
783 if ($error){
784
785 if ($test){
786
787 $result = array(
788 'error' => $error,
789 'result' => null,
790 'resultString' => 'null',
791 'load' => 'No',
792 'logic' => $string,
793 'num_statements' => 0,
794 'analysis' => 'Your condition generated a PHP error. The folder will not load until you fix it: ' . '<br /><br /><b><pre>' . $error . '</pre></b>'
795 );
796 }
797
798 // the folder just won't load, but no errors will display on the frontend
799 else {
800 $result = null;
801 }
802
803 }
804
805 // cache result in case same condition is used in another folder
806 Logic::$cache['conditions'][$string] = $result;
807
808 return $result;
809
810 }
811
812 // Integrations
813
814 // Bricks Templates
815 public static function getBricksTemplateIds($template_id, &$template_ids, $content_type = 'nested'){
816
817 if (is_numeric($template_id) && $template_id > 0
818 && !isset($template_ids[$template_id])
819 && !\Bricks\Database::is_template_disabled($content_type)) {
820
821 $template_ids[intval($template_id)] = $content_type;
822 $meta_key = $content_type === 'header'
823 ? BRICKS_DB_PAGE_HEADER
824 : ($content_type === 'footer'
825 ? BRICKS_DB_PAGE_FOOTER
826 : BRICKS_DB_PAGE_CONTENT);
827 $bricks_data = get_post_meta( $template_id, $meta_key, true );
828
829 if (is_array($bricks_data)){
830 foreach($bricks_data as $item){
831 if (!empty($item['settings']['template'])){
832 Logic::getBricksTemplateIds($item['settings']['template'], $template_ids);
833 }
834 }
835 }
836 }
837
838 }
839
840 public static function getGutenbergTemplateIds($source, &$id, &$template_ids) {
841
842 global $post, $pagenow;
843
844 $map = null;
845 $startingPoints = array();
846 $isFSE = $pagenow === 'site-editor.php';
847 $themeSlug = get_stylesheet();
848 $id = Helper::removeRedundantThemePrefix($id, $themeSlug);
849 $currentTemplate = '';
850 $urlParams = Helper::extractUrlParams($themeSlug);
851
852 // Get parameters in case we are in the FSE view
853 extract($urlParams);
854
855 // Determine the starting points and their trails
856 if ($isFSE) {
857 // FSE falls back to the home page if no $postId is set (for overview pages)
858 // So we need to grab the post for the single page if set
859 $homePageFallback = false;
860 if (!$postId){
861
862 // single page assigned to the front
863 if (get_option('show_on_front') === 'page'){
864 $postId = get_option('page_on_front');
865 $post = get_post($postId);
866 $homePageFallback = true;
867 Helper::debug('FSE fallback to home page (single page): '.$currentTemplate);
868 }
869
870 // Recent posts page
871 else {
872 $currentTemplate = 'home';
873 Helper::debug('FSE fallback to blog home: '.$currentTemplate);
874 }
875
876 }
877
878 // single template and page views
879 if ($fseType === $source && $postId == $id){ // wp_navigation, wp_template_part, wp_pattern
880 $template_ids[$id] = 'blocksOnly';
881 } if ($fseType === 'wp_template'){
882 $currentTemplate = $postId;
883 } elseif($fseType === 'page' || ($postId && $homePageFallback)){
884 if (!$homePageFallback){
885 $post = get_post($postId);
886 }
887 $currentTemplate = Helper::getTemplateFromPostId($postId, $post);
888 Helper::debug('FSE page template: '.$currentTemplate);
889 }
890
891 }
892
893 // non FSE
894 else {
895 // regular Gutenberg editor
896 if ($pagenow === 'post.php' && isset($_GET['post'])){
897 $postId = intval($_GET["post"]);
898 $post = get_post($postId);
899 $currentTemplate = Helper::getTemplateFromPostId($postId, $post);
900 }
901
902 // any other front or admin page
903 else {
904 $currentTemplate = Helper::getCurrentTemplateSlug();
905 }
906
907 }
908
909 if ($source === 'wp_template') {
910 if ($currentTemplate == $id){ // loose, so user can use quotes e.g. "404"
911 if (Helper::$doDebug){
912 Helper::debug('Found wp_template: ' . $id);
913 }
914 $template_ids[$id] = 'blocksOnly';
915 }
916 } else {
917 // we need to check the cached map
918 $map = Helper::getTemplateMap($themeSlug, Logic::$cache);
919
920 // use URL param as starting point for FSE nav/template/part/etc
921 if ($isFSE && $map && $fseType && $postId
922 && isset($map[$fseType]) && isset($map[$fseType][$postId]) ){
923 $startingPoints[] = [
924 'dataArray' => $map[$fseType][$postId],
925 'trail' => $fseType . '.' . $postId
926 ];
927 }
928
929 // we should extract any synced pattern references from the post content,
930 // so they can be checked in the map too
931 if ($post instanceof \WP_Post) {
932 $content = $post->post_content;
933
934 // allow for the live post override
935 if (Helper::isLiveContentTest()) {
936 $content = Common::$live_post_content;
937 }
938
939 $matches = Helper::extractSyncedPatterns($content);
940 $types = $matches[1];
941 $syncedPatternIds = $matches[2];
942
943 if ($syncedPatternIds && count($syncedPatternIds)){
944 foreach ($syncedPatternIds as $i => $syncedPatternId) {
945 $type = $types[$i];
946 $key = $type === 'block' ? 'wp_pattern' : 'wp_' . $type;
947
948 if (isset($map[$key][$syncedPatternId])) {
949 $startingPoints[] = [
950 'dataArray' => $map[$key][$syncedPatternId],
951 'trail' => $key . '.' . $syncedPatternId
952 ];
953
954 if ($source === $key && $id == $syncedPatternId) {
955 $template_ids[$syncedPatternId] = 'blocksOnly';
956 }
957 }
958 }
959 }
960
961 }
962
963 if ($currentTemplate) {
964 if (isset($map['wp_template'][$currentTemplate])) {
965 $startingPoints[] = [
966 'dataArray' => $map['wp_template'][$currentTemplate],
967 'trail' => 'wp_template.' . $currentTemplate,
968 ];
969 }
970 }
971
972 if (Helper::$doDebug){
973 Helper::debug('$startingPoints', array(
974 'id' => $id,
975 'postId' => $postId,
976 '$startingPoints' => $startingPoints,
977 'currentTemplate' => $currentTemplate,
978 'urlParams' => $urlParams
979 ));
980 }
981
982 foreach ($startingPoints as $startingPoint) {
983 $visited = array();
984 Logic::checkGutenbergMap(
985 $startingPoint['dataArray'], $map, $id, $template_ids, $source, $visited, 0, 50, $startingPoint['trail']
986 );
987
988 if (!empty($template_ids[$id])) {
989 break;
990 }
991 }
992 }
993 }
994
995 public static function checkGutenbergMap(
996 $array, $map, $id, &$template_ids, $source, &$visited = array(), $depth = 0, $maxDepth = 50, $trail = ''
997 ) {
998 Helper::debug("Run checkGutenbergMap (" . $depth . "): " . $id . " Trail: " . $trail);
999
1000 // Stop if maximum recursion depth is reached
1001 if ($depth > $maxDepth) {
1002 if (Helper::$doDebug) {
1003 Helper::debug("Maximum recursion depth reached in checkGutenbergMap");
1004 }
1005 return;
1006 }
1007
1008 // Use trail as the unique identifier for the current node
1009 if (isset($visited[$trail])) {
1010 if (Helper::$doDebug) {
1011 Helper::debug("Already visited node: " . $trail, $array);
1012 }
1013 return;
1014 }
1015
1016 // Mark the current node as visited
1017 $visited[$trail] = true;
1018 if (Helper::$doDebug) {
1019 Helper::debug("Mark as visited: " . $trail, $array);
1020 }
1021
1022 // Sources to process
1023 $sources = ['wp_template_part', 'wp_pattern', 'wp_navigation'];
1024
1025 foreach ($sources as $itemSource) {
1026 if (!empty($array[$itemSource])) {
1027 $subArray = $array[$itemSource];
1028
1029 if (is_array($subArray)) {
1030 foreach ($subArray as $itemId => $enabled) {
1031 $newTrail = $trail . ($trail ? '.' : '') . $itemSource . '.' . $itemId;
1032
1033 if (Helper::$doDebug) {
1034 Helper::debug("Check: " . Helper::maybeMakeNumber($itemId) . ' = ' . $id, $itemSource);
1035 }
1036
1037 // Match the current source and ID
1038 if ($itemSource === $source && Helper::maybeMakeNumber($itemId) == $id) {
1039 $template_ids[$id] = 'blocksOnly';
1040 }
1041
1042 // Recursively check nested structures
1043 elseif (!empty($map[$itemSource][$itemId])) {
1044 self::checkGutenbergMap(
1045 $map[$itemSource][$itemId],
1046 $map,
1047 $id,
1048 $template_ids,
1049 $source,
1050 $visited,
1051 $depth + 1,
1052 $maxDepth,
1053 $newTrail
1054 );
1055 }
1056 }
1057 }
1058 }
1059 }
1060 }
1061
1062
1063
1064 }
1065
1066 /*
1067 * Custom (namespaced) microthemer functions for use with logical conditions
1068 * These fill gaps in WordPress API and can support integrations with other plugins
1069 * IMPORTANT - all params must be optional to prevent user from generating a fatal error (extra params OK it seems)
1070 */
1071
1072 // check what admin page the user is on - allow the page name or an id
1073 function is_admin_page($pageNameOrId = false){
1074
1075 global $post;
1076
1077 return is_admin() && !$pageNameOrId
1078
1079 // e.g. edit.php
1080 || (isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === $pageNameOrId)
1081
1082 // e.g. 123
1083 || (is_numeric($pageNameOrId) && isset($_GET['post']) && intval($_GET['post']) === intval($pageNameOrId))
1084
1085 // e.g. my-post-slug
1086 || (!is_numeric($pageNameOrId) && isset($post->post_name) && $post->post_name === $pageNameOrId);
1087 }
1088
1089 // check what page the user is on (frontend or admin)
1090 function is_post_or_page($id = null){
1091
1092 $globalOrFrontMatch = ($id === 'front' && Helper::isFrontOrFallback()) ||
1093 ($id === 'global' && (is_public() || Helper::isBlockAdminPage('global')));
1094
1095 return is_public() && (is_page($id) || is_single($id) || $globalOrFrontMatch)
1096 ? true
1097 : (is_admin() && (Helper::isBlockAdminPage($id) || $globalOrFrontMatch )
1098 ? 'blocksOnly'
1099 : false);
1100 }
1101
1102 // check what admin page the user is on
1103 function is_public(){
1104 return !is_admin();
1105 }
1106
1107 // check what admin page the user is on
1108 function is_public_or_admin($postOrPageId = null){
1109 return !$postOrPageId
1110 || ( !is_admin() && is_post_or_page($postOrPageId) )
1111 || is_admin_page($postOrPageId);
1112 }
1113
1114 function query_admin_screen($key = null, $value = null){
1115
1116 if (!function_exists('get_current_screen')){
1117 return false;
1118 }
1119
1120 $current_screen = get_current_screen();
1121
1122 return ($key === null || isset($current_screen->$key))
1123 && ($value === null || $current_screen->$key === $value);
1124 }
1125
1126 // check if the user has a particular role or user id
1127 function user_has_role($roleOrUserId = null){
1128 return is_user_logged_in() && $roleOrUserId === null ||
1129 wp_get_current_user()->roles[0] === $roleOrUserId ||
1130 (is_numeric($roleOrUserId) && intval($roleOrUserId) === get_current_user_id());
1131 }
1132
1133 // check if a theme or plugin is active, slug is the directory slug e.g. 'microthemer' or 'divi'
1134 function is_active($item = null, $slug = null){
1135 switch ($item) {
1136 case 'plugin':
1137 $active_plugins = get_option('active_plugins', array());
1138 foreach($active_plugins as $path){
1139 if (strpos($path, $slug) !== false){
1140 return true;
1141 }
1142 }
1143 return is_plugin_active_for_network($slug);
1144 case 'theme':
1145 $theme = wp_get_theme();
1146 return $theme->get_stylesheet() === $slug;
1147 default:
1148 return false;
1149 }
1150 }
1151
1152 // check if the current url matches a path
1153 function match_url_path($value = null, $regex = false){
1154 // Keep URL encoding intact for literal and regex matching; sanitize_text_field() removes valid percent octets.
1155 $urlPath = isset($_SERVER["REQUEST_URI"]) ? esc_url_raw(wp_unslash($_SERVER["REQUEST_URI"])) : "";
1156 return $regex
1157 ? preg_match('/'.$value.'/', $urlPath)
1158 : strpos($urlPath, $value) !== false;
1159 }
1160
1161 function has_template($source = null, $id = null, $label = null){
1162
1163 global $post;
1164
1165 // todo maybe this would work if Logic::$cache[$source][$id]['template_ids'] - try later
1166 /*$cache = !empty(Logic::$cache[$source]['template_ids'])
1167 ? Logic::$cache[$source]['template_ids']
1168 : false;*/
1169 $template_ids = array(); // $cache ?: array();
1170 $returnType = true;
1171
1172 if (!$source || !$id){
1173 return false;
1174 } /*if ($cache){
1175 return !empty($cache[$id]) ? $cache[$id] : false;
1176 }*/
1177
1178 // gather template_ids
1179 switch ($source) {
1180
1181 case 'bricks':
1182 if ($post && method_exists('\Bricks\Helpers', 'render_with_bricks')){
1183 if ( \Bricks\Helpers::render_with_bricks($post->ID) ) {
1184 foreach (\Bricks\Database::$active_templates as $content_type => $template_id){
1185 Logic::getBricksTemplateIds($template_id, $template_ids, $content_type);
1186 }
1187 }
1188 }
1189 break;
1190
1191 case 'wp_template':
1192 case 'wp_template_part':
1193 case 'wp_pattern':
1194 case 'wp_navigation':
1195 $returnType = 'blocksOnly';
1196 //echo 'gothere' . $source . $id . '<br/>';
1197 Logic::getGutenbergTemplateIds($source, $id,$template_ids);
1198 break;
1199 }
1200
1201 // cache template analysis for the source - no this does not work.
1202 //Logic::$cache[$source]['template_ids'] = $template_ids;
1203
1204 return !empty($template_ids[$id]) ? $returnType : false;
1205 }
1206