'POST','permission_callback'=>'__return_true','callback'=>[$this,'request']]);} public static function visible(array $style,int $context=0): bool { return DisplayRules::visible($style,$context); } public static function definition(int $id,string $node,array $types,array $chain=[]) { if(!$id||!PostTypes::public_source($id,$chain)){return null;} $doc=Document::clean(json_decode(get_post_meta($id,'_digibl_published',true),true));if(is_wp_error($doc)){return null;} $scan=function($nodes)use(&$scan,$node,$types,$id){foreach($nodes as $n){if(!self::visible($n['style'],$id)){continue;}if($n['id']===$node&&in_array($n['type'],$types,true)){return $n;}if(in_array($n['type'],['loop','reusable','component','data-repeater','mega-menu'],true)){continue;}$found=$scan($n['children']);if($found){return $found;}}return null;};return $scan($doc['nodes']); } public static function ids($value): array {return array_slice(array_values(array_unique(array_filter(array_map('absint',explode(',',(string)$value))))),0,100);} public static function taxonomy(array $p): string { $name=sanitize_key($p['taxonomy']??'category');$type=sanitize_key($p['postType']??'post');$tax=get_taxonomy($name); return $tax&&$tax->public&&in_array($type,$tax->object_type,true)?$name:''; } public static function query(array $p,int $context=0): \WP_Query { $type=sanitize_key($p['postType']??'post');$object=get_post_type_object($type); $count=max(1,min(24,(int)$p['count']));$page=max(1,min(250,(int)($p['_query_page']??1))); $args=['post_type'=>$type,'post_status'=>'publish','has_password'=>false,'posts_per_page'=>$count,'ignore_sticky_posts'=>true,'no_found_rows'=>false,'order'=>($p['order']??'newest')==='oldest'?'ASC':'DESC','orderby'=>Document::choice($p['orderby']??'date',['date','title','modified','menu_order','comment_count'],'date'),'offset'=>min(10000,(int)($p['offset']??0))+($page-1)*$count]; // Internal/private post types cannot be queried even through imported documents. if(!$object||!$object->public||!is_post_type_viewable($object)){$args['post__in']=[0];} elseif($include=self::ids($p['include']??'')){$args['post__in']=$include;} $exclude=self::ids($p['exclude']??'');if($context){$exclude[]=$context;}$args['post__not_in']=$exclude; // WordPress ignores post__not_in when post__in is non-empty. Apply the // intersection here and keep an empty result as [0], never unrestricted. if(isset($args['post__in'])){$args['post__in']=array_values(array_diff($args['post__in'],$exclude))?:[0];} if($authors=self::ids($p['authors']??'')){$args['author__in']=$authors;} if(!empty($p['category'])&&$type==='post'){$args['cat']=(int)$p['category'];} $taxonomy=self::taxonomy($p);$terms=self::ids($p['terms']??'');$term=absint($p['_query_term']??0); if($taxonomy){$clauses=[];if($terms){$clauses[]=['taxonomy'=>$taxonomy,'field'=>'term_id','terms'=>$terms];}if($term){$clauses[]=['taxonomy'=>$taxonomy,'field'=>'term_id','terms'=>[$term]];}if($clauses){$args['tax_query']=array_merge(['relation'=>'AND'],$clauses);}} QueryFields::apply($args,$p); return new \WP_Query($args); } public static function controls(array $p): string { $taxonomy=self::taxonomy($p);if(($p['filters']??'no')!=='yes'||!$taxonomy){return '';} $terms=get_terms(['taxonomy'=>$taxonomy,'hide_empty'=>true,'number'=>100]);if(is_wp_error($terms)){return '';} $h='