| 1 |
<?php |
| 2 |
/** Editable native template catalogue. Elementor is not required. */ |
| 3 |
defined('ABSPATH') || exit; |
| 4 |
require_once __DIR__.'/template-library-client.php'; |
| 5 |
function bkbg_template_manifest(){return bkbg_library_catalog()['templates'];} |
| 6 |
function bkbg_template_keys(){return array_keys(array_filter(bkbg_template_manifest(),function($e){return ($e['status']??'')==='ready';}));} |
| 7 |
function bkbg_template_dir(){return dirname(__DIR__,2).'/templates/';} |
| 8 |
function bkbg_template_url(){return plugins_url('templates/',dirname(__DIR__,2).'/blockenberg.php');} |
| 9 |
function bkbg_template_data($key,$authoring_preview=false){return bkbg_library_template_bundle($key,$authoring_preview);} |
| 10 |
function bkbg_template_resolve($value){ |
| 11 |
if(is_string($value))return str_replace('{{assets}}/',bkbg_library_assets_url(),$value); |
| 12 |
if(is_array($value))foreach($value as &$v)$v=bkbg_template_resolve($v); |
| 13 |
return $value; |
| 14 |
} |
| 15 |
function bkbg_template_serialize($node){ |
| 16 |
$name=$node['blockName'];$attrs=bkbg_template_resolve($node['attrs']); |
| 17 |
$json=wp_json_encode($attrs,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); |
| 18 |
$json=str_replace(array('--','<','>','&'),array('\\u002d\\u002d','\\u003c','\\u003e','\\u0026'),$json); |
| 19 |
$short=str_replace('core/','',$name); |
| 20 |
if(empty($node['innerBlocks']))return '<!-- wp:'.$short.' '.$json.' /-->'; |
| 21 |
$content='';foreach($node['innerBlocks'] as $child)$content.=bkbg_template_serialize($child); |
| 22 |
return '<!-- wp:'.$short.' '.$json.' -->'.$content.'<!-- /wp:'.$short.' -->'; |
| 23 |
} |
| 24 |
function bkbg_template_import($key,$progress=null){ |
| 25 |
$notify=function($percent,$message)use($progress){if(is_callable($progress))$progress($percent,$message);}; |
| 26 |
$notify(5,__('Downloading and verifying template…','blockenberg')); |
| 27 |
$data=bkbg_template_data($key);if(is_wp_error($data))return $data;if(!$data||empty($data['blocks'])||!is_array($data['blocks']))return new WP_Error('missing_template','Template is not available.'); |
| 28 |
$valid=bkbg_template_validate_nodes($data['blocks']);if(is_wp_error($valid))return $valid; |
| 29 |
$notify(20,__('Creating your draft…','blockenberg')); |
| 30 |
$content='';foreach($data['blocks'] as $node)$content.=bkbg_template_serialize($node); |
| 31 |
$id=wp_insert_post(wp_slash(array('post_type'=>'page','post_status'=>'draft','post_title'=>$data['title'].' — Blockenberg','post_name'=>wp_unique_post_slug('bkbg-'.$key,0,'publish','page',0),'post_content'=>$content)),true); |
| 32 |
if(is_wp_error($id))return $id; |
| 33 |
$total=bkbg_template_media_count($data);$done=0; |
| 34 |
$notify(25,__('Preparing images…','blockenberg')); |
| 35 |
$data=bkbg_template_prepare_media($data,$id,function()use(&$done,$total,$notify){$done++;$notify(25+(int)floor(65*$done/max(1,$total)),sprintf(__('Preparing images: %1$d of %2$d','blockenberg'),$done,$total));}); |
| 36 |
$notify(95,__('Saving editable blocks…','blockenberg'));$content='';foreach($data['blocks'] as $node)$content.=bkbg_template_serialize($node); |
| 37 |
$updated=wp_update_post(wp_slash(array('ID'=>$id,'post_content'=>$content)),true);if(is_wp_error($updated))return $updated; |
| 38 |
update_post_meta($id,'_bkbg_template_key',$key);update_post_meta($id,'_bkbg_template_canvas','1'); |
| 39 |
$notify(100,__('Your draft is ready.','blockenberg')); |
| 40 |
return $id; |
| 41 |
} |
| 42 |
function bkbg_template_classes($a){ |
| 43 |
$class=$a['classes']??'';return implode(' ',array_map('sanitize_html_class',preg_split('/\s+/',trim($class)))); |
| 44 |
} |
| 45 |
function bkbg_template_image($image){ |
| 46 |
$out='<img';foreach(array('src','alt','width','height','class','loading','decoding','fetchpriority','srcset','sizes') as $k){ |
| 47 |
if($k==='alt'){$out.=' alt="'.esc_attr($image['alt']??'').'"';continue;} |
| 48 |
if(!isset($image[$k])||$image[$k]==='')continue; |
| 49 |
$v=$k==='src'?esc_url($image[$k]):esc_attr($image[$k]);$out.=' '.$k.'="'.$v.'"'; |
| 50 |
}return $out.' />'; |
| 51 |
} |
| 52 |
function bkbg_template_override_css($a){ |
| 53 |
$id='bkbg-tpl-element-'.sanitize_html_class($a['uid']??'');$selector='.'. $id.' > .bkbg-tpl-widget-container';$out=''; |
| 54 |
foreach(array(''=>'','Tablet'=>'@media(max-width:1024px)','Mobile'=>'@media(max-width:767px)') as $suffix=>$media){ |
| 55 |
$props=array();foreach(array('fontSize'=>'font-size','lineHeight'=>'line-height','letterSpacing'=>'letter-spacing','textColor'=>'color','textAlign'=>'text-align','fontWeight'=>'font-weight') as $key=>$css){ |
| 56 |
$value=$a[$key.$suffix]??'';if($value==='')continue; |
| 57 |
if(in_array($key,array('fontSize','letterSpacing'),true))$value=(float)$value.'px'; |
| 58 |
elseif($key==='lineHeight')$value=(float)$value; |
| 59 |
elseif($key==='fontWeight')$value=(int)$value; |
| 60 |
elseif($key==='textAlign'&&!in_array($value,array('left','center','right','justify'),true))continue; |
| 61 |
elseif($key==='textColor'&&!preg_match('/^(#[a-f0-9]{3,8}|rgba?\([0-9.,%\s]+\)|[a-z]+)$/i',$value))continue; |
| 62 |
$props[]=$css.':'.$value.'!important'; |
| 63 |
} |
| 64 |
if($props){$rule=$selector.','.$selector.' .bkbg-tpl-heading-title,'.$selector.' .bkbg-catalog-styled-text-builder-items,'.$selector.' *{'.implode(';',$props).'}';$out.=$media?$media.'{'.$rule.'}':$rule;} |
| 65 |
} |
| 66 |
if(!empty($a['imageWidth']))$out.=$selector.' img{width:'.max(1,min(3000,(float)$a['imageWidth'])).'px!important}'; |
| 67 |
return $out?'<style>'.$out.'</style>':''; |
| 68 |
} |
| 69 |
function bkbg_template_render($a,$content,$block){ |
| 70 |
$name=$block->name;$classes=bkbg_template_classes($a);$uid=sanitize_html_class($a['uid']??''); |
| 71 |
if($name==='blockenberg/template-page'){ |
| 72 |
$key=$a['templateKey']??'';if(!preg_match('/^[a-z0-9][a-z0-9-]{0,159}$/D',$key))return ''; |
| 73 |
return '<main '.get_block_wrapper_attributes(array('class'=>'bkbg-template-page','data-bkbg-template'=>$key)).'><div class="'.esc_attr($a['sourceClasses']??'').'">'.$content.'</div></main>'; |
| 74 |
} |
| 75 |
if($name==='blockenberg/template-layout'){ |
| 76 |
$tag=($a['tag']??'')==='section'?'section':'div';$attrs=get_block_wrapper_attributes(array('class'=>$classes,'data-bkbg-id'=>$uid,'data-bkbg-type'=>$a['kind']??'','style'=>safecss_filter_attr($a['inlineStyle']??''))); |
| 77 |
$overlay=!empty($a['overlay'])?'<div class="bkbg-tpl-background-overlay"></div>':''; |
| 78 |
return '<'.$tag.' '.$attrs.'>'.(($a['kind']??'')==='column'?'':bkbg_template_background($a).$overlay).'<div class="'.esc_attr($a['boxClasses']??'').'">'.(($a['kind']??'')==='column'?bkbg_template_background($a).$overlay:'').$content.'</div>'.bkbg_template_shapes($a).'</'.$tag.'>'; |
| 79 |
} |
| 80 |
$body=''; |
| 81 |
if($name==='blockenberg/template-text')$body=wp_kses_post($a['content']??''); |
| 82 |
elseif($name==='blockenberg/template-heading'){ |
| 83 |
$tag=in_array($a['headingTag']??'',array('h1','h2','h3','h4','h5','h6','p','div'),true)?$a['headingTag']:'h2'; |
| 84 |
$body='<'.$tag.' class="'.esc_attr($a['contentClasses']??'').'">'.wp_kses_post($a['content']??'').'</'.$tag.'>'; |
| 85 |
}elseif($name==='blockenberg/template-image'){ |
| 86 |
$body=bkbg_template_image($a['image']??array());if(!empty($a['link']))$body='<a href="'.esc_url($a['link']).'">'.$body.'</a>'; |
| 87 |
}elseif($name==='blockenberg/template-button'){ |
| 88 |
$target=($a['target']??'')==='_blank'?' target="_blank" rel="noopener noreferrer"':''; |
| 89 |
$icon=($a['buttonIcon']??'')==='play'?'<span class="bkbg-tpl-button-icon" aria-hidden="true"><svg viewBox="0 0 448 512" width="1em" height="1em" fill="currentColor"><path d="M0 0L448 256L0 512Z"/></svg></span>':''; |
| 90 |
$body='<div class="bkbg-tpl-button-wrapper"><a class="'.esc_attr($a['buttonClasses']??'').'" href="'.esc_url($a['url']??'#').'"'.$target.'><span class="bkbg-tpl-button-content-wrapper">'.$icon.'<span class="bkbg-tpl-button-text">'.wp_kses_post($a['content']??'').'</span></span></a></div>'; |
| 91 |
}elseif(in_array($name,array('blockenberg/template-styled-text','blockenberg/template-marquee'),true)){ |
| 92 |
$marquee=$name==='blockenberg/template-marquee';$items=''; |
| 93 |
foreach($a['segments']??array() as $segment){ |
| 94 |
$inner=isset($segment['image'])?bkbg_template_image($segment['image']):'<span>'.wp_kses_post($segment['text']??'').'</span>'; |
| 95 |
$items.='<span class="'.esc_attr($segment['classes']??'').'"><span class="bkbg-catalog-'.($marquee?'auto-scrolling-text':'styled-text').'-inner">'.$inner.'</span></span>'; |
| 96 |
} |
| 97 |
if(!$marquee)$body='<div class="bkbg-catalog-styled-text-builder-items">'.$items.'</div>'; |
| 98 |
else $body='<div class="bkbg-catalog-auto-scrolling-text-items-wrapper"><div class="bkbg-catalog-auto-scrolling-text-items bkbg-catalog-auto-scrolling-text-items-'.$uid.'"><div class="bkbg-catalog-auto-scrolling-text-wrapper bkbg-catalog-auto-scrolling-text-wrapper-'.$uid.' bkbg-template-marquee-track" data-bkbg-marquee="'.max(1,min(200,(float)($a['speed']??60))).'" data-pause="'.(!empty($a['pauseOnHover'])?'1':'0').'">'.$items.'</div></div></div>'; |
| 99 |
} |
| 100 |
return '<div '.get_block_wrapper_attributes(array('class'=>$classes,'data-bkbg-id'=>$uid)).'><div class="bkbg-tpl-widget-container">'.$body.'</div></div>'.bkbg_template_override_css($a); |
| 101 |
} |
| 102 |
add_action('init',function(){ |
| 103 |
$base=dirname(__DIR__,2);$url=plugins_url('', $base.'/blockenberg.php'); |
| 104 |
wp_register_script('bkbg-template-editor-styles',$url.'/assets/js/template-editor-styles.js',array(),filemtime($base.'/assets/js/template-editor-styles.js'),false); |
| 105 |
wp_register_script('bkbg-template-pilot-schemas',$url.'/assets/js/template-pilot-schemas.js',array(),filemtime($base.'/assets/js/template-pilot-schemas.js'),false); |
| 106 |
wp_register_script('bkbg-template-pilot-editor',$url.'/assets/js/template-pilot.js',array('wp-blocks','wp-block-editor','wp-element','wp-components','wp-i18n','bkbg-motion-editor','bkbg-template-pilot-schemas','bkbg-inspector-tabs','bkbg-template-editor-styles'),filemtime($base.'/assets/js/template-pilot.js'),false); |
| 107 |
wp_register_style('bkbg-template-pilot',$url.'/assets/css/template-pilot.css',array(),filemtime($base.'/assets/css/template-pilot.css')); |
| 108 |
wp_register_script('bkbg-template-pilot-front',$url.'/assets/js/template-pilot-front.js',array(),filemtime($base.'/assets/js/template-pilot-front.js'),true); |
| 109 |
},5); |
| 110 |
function bkbg_template_content_keys($blocks){ |
| 111 |
$keys=array();foreach($blocks as $block){ |
| 112 |
if(($block['blockName']??'')==='blockenberg/template-page')$keys[]=$block['attrs']['templateKey']??''; |
| 113 |
if(!empty($block['innerBlocks']))$keys=array_merge($keys,bkbg_template_content_keys($block['innerBlocks'])); |
| 114 |
}return array_values(array_unique(array_filter($keys))); |
| 115 |
} |
| 116 |
add_action('enqueue_block_assets',function(){ |
| 117 |
$post=get_post();if(!$post&&is_admin()&&!empty($_GET['post']))$post=get_post(absint($_GET['post'])); |
| 118 |
if(!$post)return; |
| 119 |
$visit=function($blocks)use(&$visit){foreach($blocks as $block){ |
| 120 |
if(($block['blockName']??'')==='blockenberg/template-page')bkbg_library_enqueue_template_style($block['attrs']['templateKey']??'', $block['attrs']['templateStyleUrl']??''); |
| 121 |
if(!empty($block['innerBlocks']))$visit($block['innerBlocks']); |
| 122 |
}};$visit(parse_blocks($post->post_content)); |
| 123 |
}); |
| 124 |
add_filter('template_include',function($template){ |
| 125 |
if(is_singular('page')&&get_post_meta(get_the_ID(),'_bkbg_template_canvas',true)==='1'&&has_block('blockenberg/template-page'))return __DIR__.'/template-canvas.php'; |
| 126 |
return $template; |
| 127 |
}); |
| 128 |
add_action('admin_menu',function(){add_submenu_page('blockenberg',__('Template Library','blockenberg'),__('Template Library','blockenberg'),'manage_options','blockenberg-templates','bkbg_template_catalog_screen');},25); |
| 129 |
require_once __DIR__.'/template-catalog-screen.php'; |
| 130 |
add_action('admin_post_bkbg_import_template',function(){ |
| 131 |
if(!current_user_can('manage_options'))wp_die('Insufficient permissions.');check_admin_referer('bkbg_import_template'); |
| 132 |
$id=bkbg_template_import(sanitize_key(wp_unslash($_POST['key']??'')));if(is_wp_error($id))wp_die(esc_html($id->get_error_message())); |
| 133 |
wp_safe_redirect(admin_url('post.php?post='.(int)$id.'&action=edit'));exit; |
| 134 |
}); |
| 135 |
|
| 136 |
|
| 137 |
/** Validate bundled block data before creating a draft. */ |
| 138 |
function bkbg_template_validate_nodes($nodes,$depth=0){ |
| 139 |
if($depth>40)return new WP_Error('template_depth','Template structure is too deeply nested.'); |
| 140 |
foreach($nodes as $node){ |
| 141 |
$name=$node['blockName']??''; |
| 142 |
if(strpos($name,'blockenberg/template-')!==0||!WP_Block_Type_Registry::get_instance()->is_registered($name))return new WP_Error('template_block','Unknown template block.'); |
| 143 |
if(!is_array($node['attrs']??null)||!is_array($node['innerBlocks']??null))return new WP_Error('template_schema','Invalid template structure.'); |
| 144 |
$nested=bkbg_template_validate_nodes($node['innerBlocks'],$depth+1);if(is_wp_error($nested))return $nested; |
| 145 |
}return true; |
| 146 |
} |
| 147 |
/** Images are imported only when a user imports a template, with source deduplication. */ |
| 148 |
function bkbg_template_media_count($value){ |
| 149 |
if(!is_array($value))return 0; |
| 150 |
if(isset($value['src'])&&is_string($value['src'])&&$value['src']!==''){ |
| 151 |
$source=$value['src'];$token=strpos($source,'{{assets}}/')===0;$host=strtolower(wp_parse_url($source,PHP_URL_HOST)??''); |
| 152 |
if(!$token&&!bkbg_library_asset_url_allowed($source)&&$host!=='kingaddons.com'&&!preg_match('/(^|\.)kingaddons\.com$/',$host))return 0; |
| 153 |
$name=$token?basename(substr($source,11)):basename(wp_parse_url($source,PHP_URL_PATH)??'');$mime=wp_check_filetype($name)['type']; |
| 154 |
return $mime&&strpos($mime,'image/')===0&&$mime!=='image/svg+xml'?1:0; |
| 155 |
} |
| 156 |
$count=0;foreach($value as $child)$count+=bkbg_template_media_count($child);return $count; |
| 157 |
} |
| 158 |
function bkbg_template_prepare_media($value,$post_id,$progress=null){ |
| 159 |
if(!is_array($value))return $value; |
| 160 |
if(isset($value['src'])&&is_string($value['src'])&&$value['src']!==''){ |
| 161 |
$source=$value['src'];$token=strpos($source,'{{assets}}/')===0; |
| 162 |
$host=strtolower(wp_parse_url($source,PHP_URL_HOST)??''); |
| 163 |
if(!$token&&!bkbg_library_asset_url_allowed($source)&&$host!=='kingaddons.com'&&!preg_match('/(^|\.)kingaddons\.com$/',$host))return $value; |
| 164 |
$name=$token?basename(substr($source,11)):basename(wp_parse_url($source,PHP_URL_PATH)??''); |
| 165 |
$mime=wp_check_filetype($name)['type'];if(!$mime||strpos($mime,'image/')!==0||$mime==='image/svg+xml')return $value; |
| 166 |
try { |
| 167 |
$meta=$token?'_bkbg_template_asset':'_bkbg_template_source';$identity=$token?$name:hash('sha256',$source); |
| 168 |
$ids=get_posts(array('post_type'=>'attachment','post_status'=>'inherit','fields'=>'ids','numberposts'=>1,'meta_key'=>$meta,'meta_value'=>$identity)); |
| 169 |
$id=$ids?(int)$ids[0]:0; |
| 170 |
if(!$id){ |
| 171 |
if($token)$source=bkbg_library_assets_url().$name; |
| 172 |
{ |
| 173 |
$response=wp_safe_remote_get($source,array('timeout'=>20,'limit_response_size'=>20*MB_IN_BYTES)); |
| 174 |
if(is_wp_error($response)||wp_remote_retrieve_response_code($response)!==200)return $value; |
| 175 |
$bytes=wp_remote_retrieve_body($response); |
| 176 |
if(!$bytes||strpos(strtolower(substr($bytes,0,100)),'<html')!==false)return $value; |
| 177 |
} |
| 178 |
$upload=wp_upload_bits('blockenberg-'.substr(hash('sha256',$source),0,16).'-'.$name,null,$bytes); |
| 179 |
if(!empty($upload['error']))return $value; |
| 180 |
$actual=wp_check_filetype_and_ext($upload['file'],$name);if(empty($actual['type'])||strpos($actual['type'],'image/')!==0){wp_delete_file($upload['file']);return $value;} |
| 181 |
$id=wp_insert_attachment(array('post_mime_type'=>$actual['type'],'post_title'=>'Blockenberg — '.$name,'post_status'=>'inherit'),$upload['file'],$post_id,true); |
| 182 |
if(is_wp_error($id))return $value; |
| 183 |
require_once ABSPATH.'wp-admin/includes/image.php'; |
| 184 |
wp_update_attachment_metadata($id,wp_generate_attachment_metadata($id,$upload['file'])); |
| 185 |
update_post_meta($id,$meta,$identity);update_post_meta($id,'_wp_attachment_image_alt',$value['alt']??''); |
| 186 |
} |
| 187 |
$value['id']=$id;$value['src']=wp_get_attachment_url($id); |
| 188 |
// Responsive candidates must follow the new local attachment. |
| 189 |
if(isset($value['srcset'])){$set=wp_get_attachment_image_srcset($id,'full');if($set)$value['srcset']=$set;else unset($value['srcset']);} |
| 190 |
return $value; |
| 191 |
} finally {if(is_callable($progress))$progress();} |
| 192 |
} |
| 193 |
foreach($value as &$child)$child=bkbg_template_prepare_media($child,$post_id,$progress); |
| 194 |
return $value; |
| 195 |
} |
| 196 |
|
| 197 |
/** Stream actual completed import stages; the normal form remains a no-JS fallback. */ |
| 198 |
add_action('wp_ajax_bkbg_import_template',function(){ |
| 199 |
if(!current_user_can('manage_options'))wp_send_json_error(array('message'=>__('Insufficient permissions.','blockenberg')),403); |
| 200 |
check_ajax_referer('bkbg_import_template','_wpnonce'); |
| 201 |
$key=sanitize_key(wp_unslash($_POST['key']??'')); |
| 202 |
while(ob_get_level()>0){if(!@ob_end_clean())break;} |
| 203 |
nocache_headers();header('Content-Type: application/x-ndjson; charset=utf-8');header('X-Accel-Buffering: no'); |
| 204 |
$emit=function($data){echo wp_json_encode($data)."\n";flush();}; |
| 205 |
$id=bkbg_template_import($key,function($percent,$message)use($emit){$emit(array('type'=>'progress','percent'=>$percent,'message'=>$message));}); |
| 206 |
if(is_wp_error($id))$emit(array('type'=>'error','message'=>$id->get_error_message())); |
| 207 |
else $emit(array('type'=>'complete','postId'=>(int)$id,'editUrl'=>admin_url('post.php?post='.(int)$id.'&action=edit'))); |
| 208 |
exit; |
| 209 |
}); |
| 210 |
|