'string','single'=>true,'show_in_rest'=>false,'revisions_enabled'=>post_type_supports($type,'revisions'),'auth_callback'=>static fn($allowed,$key,$id)=>current_user_can('edit_post',(int)$id)]);
}
}
public function menu(): void {
$this->screen=add_menu_page('Digiter Builder','Digiter Builder',PostTypes::menu_capability(),'digiter-builder',[$this,'screen'],'dashicons-layout',59);
}
public function screen(): void {
if(!PostTypes::can_edit()) { wp_die(esc_html__('Access denied.','digiter-builder-lite')); }
if(empty($_GET['post'])&&empty($_GET['editor'])){Commercial::dashboard();return;}
echo '
'.esc_html(Language::text('Loading Digiter Builder…')).'
';
}
public function admin_assets($hook): void {
if($hook!==$this->screen||(empty($_GET['post'])&&empty($_GET['editor']))){return;}
$this->editor_assets(absint($_GET['post']??0),false);
}
private function editor_assets(int $id,bool $frontend): void {
Diagnostics::enqueue();
$locale=Language::wordpress_locale();
$switched=switch_to_locale($locale);
try {$this->enqueue_editor_assets($id,$frontend);}
finally {if($switched){restore_previous_locale();}}
}
private function enqueue_editor_assets(int $id,bool $frontend): void {
wp_enqueue_media();
Modules::preview_assets();
NativeKit::enqueue();
wp_enqueue_editor();
wp_enqueue_style('digibl-editor',DIGIBL_URL.'assets/editor.css',[],DIGIBL_VERSION);
wp_enqueue_style('digibl-studio-theme',DIGIBL_URL.'assets/studio-theme.css',['digibl-editor'],DIGIBL_VERSION);
wp_enqueue_style('digibl-authoring',DIGIBL_URL.'assets/content-authoring.css',['digibl-editor'],DIGIBL_VERSION);
wp_enqueue_style('digibl-pro-design',DIGIBL_URL.'assets/pro-design.css',['digibl-editor'],DIGIBL_VERSION);
wp_enqueue_script('digibl-bootstrap',DIGIBL_URL.'assets/bootstrap.js',[],DIGIBL_VERSION,false);
wp_enqueue_script('digibl-advanced',DIGIBL_URL.'assets/advanced-frontend.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-interactions',DIGIBL_URL.'assets/interactions.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-audio',DIGIBL_URL.'assets/audio.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-animation',DIGIBL_URL.'assets/animation.js',['digibl-audio'],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-front',DIGIBL_URL.'assets/frontend.js',['digibl-advanced','digibl-interactions','digibl-audio','digibl-animation'],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-icons',DIGIBL_URL.'assets/icon-picker.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-audio-studio',DIGIBL_URL.'assets/audio-studio.js',['digibl-audio'],DIGIBL_VERSION,true);
Language::enqueue();
wp_enqueue_script('digibl-layout',DIGIBL_URL.'assets/layout-model.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-studio-ui',DIGIBL_URL.'assets/studio-ui.js',['digibl-blueprints'],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-blueprints',DIGIBL_URL.'assets/blueprints.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-image-upload',DIGIBL_URL.'assets/image-upload.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-designer',DIGIBL_URL.'assets/designer.js',['digibl-layout','digibl-image-upload'],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-authoring',DIGIBL_URL.'assets/content-authoring.js',['digibl-layout'],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-components',DIGIBL_URL.'assets/component-model.js',['digibl-layout'],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-button-gallery',DIGIBL_URL.'assets/button-gallery.js',[],DIGIBL_VERSION,true);
wp_enqueue_style('digibl-bundled-fonts',DIGIBL_URL.'assets/fonts/bundled.css',[],DIGIBL_VERSION);
wp_enqueue_style('digibl-font-picker',DIGIBL_URL.'assets/font-picker.css',['digibl-card-editor'],DIGIBL_VERSION);
wp_enqueue_script('digibl-characters',DIGIBL_URL.'assets/characters.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-font-picker',DIGIBL_URL.'assets/font-picker.js',[],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-compact-type',DIGIBL_URL.'assets/compact-typography.js',['digibl-font-picker','digibl-characters'],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-field-type',DIGIBL_URL.'assets/field-typography.js',['digibl-compact-type'],DIGIBL_VERSION,true);
wp_enqueue_script('digibl-card-editor',DIGIBL_URL.'assets/card-editor.js',['digibl-button-gallery','digibl-field-type','digibl-icons','digibl-audio-studio','digibl-image-upload'],DIGIBL_VERSION,true);
wp_enqueue_style('digibl-card-editor',DIGIBL_URL.'assets/card-editor.css',['digibl-editor'],DIGIBL_VERSION);
wp_enqueue_style('digibl-studio-tools',DIGIBL_URL.'assets/studio-tools.css',['digibl-editor'],DIGIBL_VERSION);
wp_enqueue_style('digibl-workflow',DIGIBL_URL.'assets/workflow.css',['digibl-editor'],DIGIBL_VERSION);
wp_enqueue_script('digibl-workflow',DIGIBL_URL.'assets/workflow.js',['digibl-layout'],DIGIBL_VERSION,true);
$editorServices=[];foreach(['transport','appearance','document-model','history','public-api','global-styles','layout-controls','fields','components','visual-studios','saved-versions','preview-dom','preview','studio-tools'] as $service){$handle='digibl-editor-'.$service;$editorServices[]=$handle;wp_enqueue_script($handle,DIGIBL_URL.'assets/editor/'.$service.'.js',[],DIGIBL_VERSION,true);}
wp_enqueue_script('digibl-editor',DIGIBL_URL.'assets/editor.js',array_merge($editorServices,['digibl-workflow','digibl-card-editor','digibl-bootstrap','digibl-icons','digibl-audio-studio','digibl-front','digibl-language','digibl-designer','digibl-blueprints','digibl-studio-ui','digibl-native-values','digibl-authoring','digibl-components','editor']),DIGIBL_VERSION,true);
$publish_types=[];$types=[];foreach(PostTypes::all() as $type){$obj=get_post_type_object($type);if(current_user_can($obj->cap->create_posts)){$types[]=$type;}if(current_user_can($obj->cap->publish_posts)){$publish_types[]=$type;}}
$pending_area=$id&&get_post_type($id)==='digibl_template'?(string)get_post_meta($id,'_digibl_pending_view_area',true):'';
$view_area=$pending_area?:(!$id?(is_search()?'search':(is_404()?'404':((is_home()||is_archive())?'archive':''))):'');
$view_label=$view_area?ThemeBuilder::area_label($view_area):'';
$config=[
'licenseActive'=>true,
'api'=>rest_url('digiter-builder/v1/'),'mediaApi'=>rest_url('wp/v2/media'),'nonce'=>wp_create_nonce('wp_rest'),'version'=>DIGIBL_VERSION,
'mediaUpload'=>['url'=>admin_url('admin-ajax.php'),'nonce'=>wp_create_nonce('media-form'),'maxBytes'=>wp_max_upload_size(),
'editorSupport'=>array_combine(['image/jpeg','image/png','image/gif','image/webp','image/avif'],array_map(static fn($mime)=>wp_image_editor_supports(['mime_type'=>$mime]),['image/jpeg','image/png','image/gif','image/webp','image/avif']))],
'styleGroups'=>StylePresets::groups(),
'styleControls'=>json_decode(file_get_contents(DIGIBL_PATH.'data/style-controls.json'),true),
'iconsUrl'=>DIGIBL_URL.'data/icons.json','assetBase'=>DIGIBL_URL,
'fontLibrary'=>['starter'=>array_values(FontLibrary::starter()),'system'=>FontLibrary::system(),'bundled'=>FontLibrary::bundled()],
'textFields'=>FieldTypography::catalog(),
'buttonLibrary'=>json_decode(file_get_contents(DIGIBL_PATH.'data/button-styles.json'),true),
'appearance'=>['presets'=>Appearance::PRESETS,'buttonPresets'=>Appearance::BUTTON_PRESETS,'default'=>Appearance::defaults(),'buttonDefault'=>Appearance::defaults_button()],
'globalColors'=>ThemeBuilder::colors(),
'menus'=>array_map(static fn($menu)=>['id'=>$menu->term_id,'name'=>$menu->name],wp_get_nav_menus()),
'modules'=>Document::modules(),'templates'=>[],'templateCatalog'=>true,'blank'=>Document::blank(),
'css'=>add_query_arg('ver',DIGIBL_VERSION,DIGIBL_URL.'assets/frontend.css'),'themeUrl'=>current_user_can('manage_options')?admin_url('admin.php?page=digibl-site-templates'):'',
'adminUrl'=>admin_url('admin.php?page=digiter-builder'),
'nativeUrl'=>current_user_can('manage_options')?admin_url('admin.php?page=digibl-native-kit'):'',
'nativeFields'=>array_values(array_filter(NativeFields::definitions(),static fn($d)=>$d['public'])),
'designerCss'=>add_query_arg('ver',DIGIBL_VERSION,DIGIBL_URL.'assets/designer.css'),
'studioUrl'=>$this->studio_url(),
'post'=>$id,'frontend'=>$frontend,'managed'=>$id?$this->owned($id):false,
'canStart'=>$id?$this->can_start($id):false,'canPrepare'=>$id?$this->can_prepare($id):false,'pageTitle'=>$id?get_the_title($id):(get_bloginfo('name').($view_label?' · '.$view_label:'')),
'returnUrl'=>$this->return_url($id),'sourceFrameUrl'=>$this->source_frame_url($this->return_url($id)),'nativeEditUrl'=>$id?get_edit_post_link($id,'raw'):'',
'pageContext'=>$id?get_post_type($id):(is_home()?'blog':(is_archive()?'archive':'site')),
'viewArea'=>$view_area,'viewLabel'=>$view_label,'canEditView'=>(bool)($view_area&¤t_user_can('manage_options')),
'locale'=>get_user_locale(),'user'=>get_current_user_id(),
'canManage'=>current_user_can('manage_options'),'componentTypes'=>Components::types(),
'author'=>wp_get_current_user()->display_name,'canUpload'=>current_user_can('upload_files'),'types'=>$types,'publishTypes'=>$publish_types,
'categories'=>array_map(static fn($c)=>['id'=>$c->term_id,'name'=>$c->name],get_categories(['hide_empty'=>false])),
];
// This is application data, not a translation dictionary. localize_script casts
// top-level 0/false to strings, making the JavaScript string "0" truthy.
wp_add_inline_script('digibl-editor','var DIGIBL_CONFIG = '.wp_json_encode($config,JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT).';','before');
}
private function request_id($r): int { return absint($r->get_url_params()['id']??0); }
private function err(string $code,string $msg,int $status=400) { return new \WP_Error('digibl_'.$code,$msg,['status'=>$status]); }
private function owned(int $id): bool { return (bool)get_post_meta($id,'_digibl_draft',true) && in_array(get_post_type($id),PostTypes::all(),true); }
private function has_external_builder(int $id): bool {
return metadata_exists('post',$id,'_fl_builder_data') || metadata_exists('post',$id,'_fl_builder_draft')
|| (bool)get_post_meta($id,'_fl_builder_enabled',true) || metadata_exists('post',$id,'_elementor_data');
}
private function can_prepare(int $id): bool {
return in_array(get_post_type($id),PostTypes::all(),true) && current_user_can('edit_post',$id)
&& !in_array(get_post_status($id),['trash','auto-draft'],true) && !$this->has_external_builder($id);
}
private function can_start(int $id): bool {
return $this->can_prepare($id) && trim((string)get_post_field('post_content',$id))==='';
}
private function can_begin(int $id): bool { return current_user_can('edit_post',$id) && ($this->owned($id) || $this->can_prepare($id)); }
private function view_url(int $id): string {
return in_array(get_post_status($id),['publish','private'],true)?get_permalink($id):add_query_arg('preview','true',get_permalink($id));
}
public function builder_url(int $id): string {
return add_query_arg(['digibl_builder'=>$id,'_digibl_nonce'=>wp_create_nonce('digibl_editor_'.$id)],$this->view_url($id));
}
private function local_url(string $url): string {
$origin=preg_replace('~^(https?://[^/]+).*$~','$1',home_url('/'));
if(str_starts_with($url,'/')&&!str_starts_with($url,'//')){$url=$origin.$url;}
$parts=wp_parse_url($url);$home=wp_parse_url(home_url('/'));
if(!$parts||!in_array($parts['scheme']??'',['http','https'],true)||($parts['scheme']??'')!==($home['scheme']??'')||strtolower($parts['host']??'')!==strtolower($home['host']??'')||($parts['port']??null)!==($home['port']??null)||isset($parts['user'])||isset($parts['pass'])){return home_url('/');}
return remove_query_arg(['digibl_builder','digibl_frame','digibl_view','digibl_canvas','digibl_return','digibl_panel','_digibl_nonce'],$url);
}
private function current_url(): string {
$uri = isset($_SERVER['REQUEST_URI']) && is_string($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '/';
return $this->local_url(is_admin() ? home_url('/') : $uri);
}
private function return_url(int $id): string {
$requested=isset($_GET['digibl_return']) && is_string($_GET['digibl_return']) ? esc_url_raw(wp_unslash($_GET['digibl_return'])) : '';
if(is_string($requested)&&$requested!==''){return $this->local_url($requested);}
return $id?$this->view_url($id):$this->current_url();
}
public function studio_url(): string {
return add_query_arg(['digibl_builder'=>'studio','_digibl_nonce'=>wp_create_nonce('digibl_studio')],$this->current_url());
}
private function source_frame_url(string $url): string {
$url=$this->local_url($url);
return add_query_arg(['digibl_view'=>1,'_digibl_nonce'=>wp_create_nonce('digibl_view_'.hash('sha256',$url))],$url);
}
private function source_revision(int $id): string {
$p=get_post($id);if(!$p){return '';}
return hash('sha256',wp_json_encode([$p->post_content,$p->post_title,$p->post_excerpt,$p->post_status,$p->post_modified_gmt,get_post_meta($id,'_digibl_published',true)]));
}
private function initial_document(int $id): array {
$doc=Document::blank();$doc['title']=get_the_title($id);$doc['excerpt']=get_post_field('post_excerpt',$id);$doc['settings']['canvas']=false;
return Document::clean($doc);
}
private function import_node(string $type,array $props,array $children,int &$counter): array {
return ['id'=>'import_'.(++$counter),'type'=>$type,'bindings'=>[],'props'=>$props,'style'=>[],'slot'=>['label'=>'','required'=>[]],'children'=>$children];
}
private function import_image(array $attrs,string $html=''): array {
$id=absint($attrs['id']??0);$url=$id?(wp_get_attachment_image_url($id,'full')?:''):'';
if(!$url && preg_match('~
]+src=[\"\']([^\"\']+)~i',$html,$m)){$url=esc_url_raw(html_entity_decode($m[1],ENT_QUOTES,'UTF-8'));}
$alt=is_scalar($attrs['alt']??null)?(string)$attrs['alt']:($id?(string)get_post_meta($id,'_wp_attachment_image_alt',true):'');
$caption=is_scalar($attrs['caption']??null)?wp_strip_all_tags((string)$attrs['caption']):($id?(string)wp_get_attachment_caption($id):'');
return ['id'=>$id,'url'=>$url,'alt'=>$alt,'caption'=>$caption,'ratio'=>'auto','fit'=>'contain','x'=>50,'y'=>50];
}
private function convert_blocks(array $blocks,int &$counter): array {
$nodes=[];
foreach($blocks as $block){
if(!is_array($block)){continue;}$name=(string)($block['blockName']??'');$attrs=is_array($block['attrs']??null)?$block['attrs']:[];$inner=is_array($block['innerBlocks']??null)?$block['innerBlocks']:[];
if($name==='core/columns' && $inner){
$columns=[];foreach(array_slice($inner,0,6) as $column){$children=$this->convert_blocks(is_array($column['innerBlocks']??null)?$column['innerBlocks']:[],$counter);$columns[]=$this->import_node('column',['gap'=>0,'vertical'=>'start'],$children,$counter);}
if($columns){$count=count($columns);$nodes[]=$this->import_node('section',['columns'=>(string)$count,'structure'=>'columns','widths'=>implode(',',array_fill(0,$count,(string)round(100/$count,4)))],$columns,$counter);}continue;
}
if(in_array($name,['core/group','core/column'],true) && $inner){$nodes=array_merge($nodes,$this->convert_blocks($inner,$counter));continue;}
if($name==='core/heading'){
$text=trim(wp_strip_all_tags(render_block($block)));if($text!==''){$level=max(1,min(4,absint($attrs['level']??2)));$nodes[]=$this->import_node('heading',['text'=>$text,'level'=>'h'.$level],[],$counter);}continue;
}
if($name==='core/image'){
$html=render_block($block);$image=$this->import_image($attrs,$html);if($image['url']){$nodes[]=$this->import_node('image',['image'=>$image],[],$counter);}continue;
}
if($name==='core/gallery'){
$images=[];foreach($inner as $child){if(($child['blockName']??'')!=='core/image'){continue;}$image=$this->import_image(is_array($child['attrs']??null)?$child['attrs']:[],render_block($child));if($image['url']){$images[]=$image;}}
if($images){$nodes[]=$this->import_node('gallery',['images'=>$images,'columns'=>(string)min(4,max(2,count($images))),'ratio'=>'auto','layout'=>'grid','lightbox'=>'yes'],[],$counter);}continue;
}
if($name==='core/buttons' && $inner){$nodes=array_merge($nodes,$this->convert_blocks($inner,$counter));continue;}
if($name==='core/button'){
$html=render_block($block);if(preg_match('~]+href=[\"\']([^\"\']+)[\"\'][^>]*>(.*?)~is',$html,$m)){$label=trim(wp_strip_all_tags($m[2]));$url=esc_url_raw(html_entity_decode($m[1],ENT_QUOTES,'UTF-8'));if($label!==''){$nodes[]=$this->import_node('button',['label'=>$label,'url'=>$url,'variant'=>'solid','target'=>'same','nofollow'=>'no'],[],$counter);}}continue;
}
if($name==='core/separator'){$nodes[]=$this->import_node('divider',['variant'=>'line'],[],$counter);continue;}
if($name==='core/spacer'){$height=max(0,min(200,absint($attrs['height']??32)));$nodes[]=$this->import_node('spacer',['height'=>$height],[],$counter);continue;}
if($name==='core/video'){
$url=is_scalar($attrs['src']??null)?esc_url_raw((string)$attrs['src']):'';if(!$url && !empty($attrs['id'])){$url=wp_get_attachment_url(absint($attrs['id']))?:'';}if($url){$nodes[]=$this->import_node('video',['url'=>$url,'caption'=>wp_strip_all_tags((string)($attrs['caption']??''))],[],$counter);}continue;
}
if($name==='core/audio'){
$url=is_scalar($attrs['src']??null)?esc_url_raw((string)$attrs['src']):'';if(!$url && !empty($attrs['id'])){$url=wp_get_attachment_url(absint($attrs['id']))?:'';}if($url){$nodes[]=$this->import_node('audio',['url'=>$url,'caption'=>wp_strip_all_tags((string)($attrs['caption']??''))],[],$counter);}continue;
}
if($name==='core/embed' && !empty($attrs['url'])){$nodes[]=$this->import_node('video',['url'=>esc_url_raw((string)$attrs['url']),'caption'=>wp_strip_all_tags((string)($attrs['caption']??''))],[],$counter);continue;}
$html=trim((string)render_block($block));
if($html===''){if($inner){$nodes=array_merge($nodes,$this->convert_blocks($inner,$counter));}continue;}
$nodes[]=$this->import_node('text',['content'=>wp_kses_post($html)],[],$counter);
}
return $nodes;
}
private function native_document(int $id): array {
$doc=Document::blank();$doc['title']=get_the_title($id);$doc['excerpt']=(string)get_post_field('post_excerpt',$id);$doc['settings']['canvas']=false;$content=(string)get_post_field('post_content',$id);$counter=0;
if(trim($content)!==''){
if(has_blocks($content)){$doc['nodes']=$this->convert_blocks(parse_blocks($content),$counter);}
else{$doc['nodes'][]=$this->import_node('text',['content'=>wp_kses_post(wpautop($content))],[],$counter);}
if(!$doc['nodes']){$doc['nodes'][]=$this->import_node('text',['content'=>wp_kses_post(do_blocks($content))],[],$counter);}
}
return Document::clean($doc);
}
private function source_document(int $id): array {
return trim((string)get_post_field('post_content',$id))===''?$this->initial_document($id):$this->native_document($id);
}
private function source_id_from_url(string $url): int {
$url=trim($url);if($url===''){return 0;}
if(str_starts_with($url,'/')&&!str_starts_with($url,'//')){$url=rtrim(home_url('/'),'/').'/'.ltrim($url,'/');}
$parts=wp_parse_url($url);$home=wp_parse_url(home_url('/'));
if(!$parts||!in_array($parts['scheme']??'',['http','https'],true)||strtolower($parts['host']??'')!==strtolower($home['host']??'')||($parts['port']??null)!==($home['port']??null)){return 0;}
$clean=remove_query_arg(['digibl_builder','digibl_frame','digibl_view','digibl_canvas','digibl_return','digibl_panel','_digibl_nonce'],$url);
$id=url_to_postid($clean);if($id){return absint($id);}
if(untrailingslashit($clean)===untrailingslashit(home_url('/'))){$front=absint(get_option('page_on_front'));if($front){return $front;}$posts=absint(get_option('page_for_posts'));if($posts){return $posts;}}
return 0;
}
private function frame_url(int $id): string {
return add_query_arg(['digibl_frame'=>$id,'_digibl_nonce'=>wp_create_nonce('digibl_editor_'.$id)],$this->view_url($id));
}
public function front_access(int $id,string $nonce): bool {
return $id>0 && in_array(get_post_type($id),PostTypes::all(),true)
&& !in_array(get_post_status($id),['trash','auto-draft'],true)
&& current_user_can('edit_post',$id) && (bool)wp_verify_nonce($nonce,'digibl_editor_'.$id);
}
public function front_editor(): void {
if(!isset($_GET['digibl_builder']) && !isset($_GET['digibl_frame']) && !isset($_GET['digibl_view'])){return;}
if(isset($_GET['digibl_view'])){
$nonce=is_string($_GET['_digibl_nonce']??null)?sanitize_text_field(wp_unslash($_GET['_digibl_nonce'])):'';
if(!PostTypes::can_edit()||!wp_verify_nonce($nonce,'digibl_view_'.hash('sha256',$this->current_url()))){nocache_headers();wp_die('This page preview is not available.','Digiter Builder',['response'=>403]);}
if(!defined('DONOTCACHEPAGE')){define('DONOTCACHEPAGE',true);}
nocache_headers();header('X-Robots-Tag: noindex, nofollow, noarchive',true);header('X-Frame-Options: SAMEORIGIN',true);show_admin_bar(false);remove_action('template_redirect','redirect_canonical');
add_filter('post_class',static function($classes,$class,$post_id){$classes[]='dbs-source-id-'.absint($post_id);return $classes;},10,3);
return;
}
$frame=isset($_GET['digibl_frame']);$raw=$_GET[$frame?'digibl_frame':'digibl_builder'];
$id=is_scalar($raw)?absint($raw):0;$nonce=is_string($_GET['_digibl_nonce']??null)?sanitize_text_field(wp_unslash($_GET['_digibl_nonce'])):'';
$studio=!$frame && $raw==='studio';
$allowed=$studio?(PostTypes::can_edit() && wp_verify_nonce($nonce,'digibl_studio')):$this->front_access($id,$nonce);
if(!$allowed){nocache_headers();wp_die('This editor link has expired or you cannot edit this page. Reopen Digiter Builder from the page toolbar.','Digiter Builder',['response'=>403]);}
if(!defined('DONOTCACHEPAGE')){define('DONOTCACHEPAGE',true);}
nocache_headers();header('X-Robots-Tag: noindex, nofollow, noarchive',true);header('Referrer-Policy: same-origin',true);
show_admin_bar(false);remove_action('template_redirect','redirect_canonical');
if($frame){
if((!$this->can_begin($id)) || get_queried_object_id()!==$id){wp_die('The editable page could not be loaded.','Digiter Builder',['response'=>403]);}
$this->frame_id=$id;
// Permit this authenticated frame on the same origin only.
header('X-Frame-Options: SAMEORIGIN',true);
return;
}
status_header(200);$this->editor_assets($id,true);
require DIGIBL_PATH.'templates/editor.php';exit;
}
public function permission($r) {
$id=$this->request_id($r);
if(!$id || !$this->owned($id) || !current_user_can('edit_post',$id) || in_array(get_post_status($id),['trash','auto-draft'],true)) { return $this->err('access','You cannot edit this Digiter document.',403); }
return true;
}
public function routes(): void {
$editor=static fn()=>PostTypes::can_edit();
register_rest_route('digiter-builder/v1','/documents',[
['methods'=>'GET','permission_callback'=>$editor,'callback'=>[$this,'list_documents']],
['methods'=>'POST','permission_callback'=>$editor,'callback'=>[$this,'create_document']],
]);
register_rest_route('digiter-builder/v1','/documents/(?P\d+)',[
['methods'=>'GET','permission_callback'=>[$this,'permission'],'callback'=>[$this,'get_document']],
['methods'=>'PUT','permission_callback'=>[$this,'permission'],'callback'=>[$this,'save_document']],
['methods'=>'DELETE','permission_callback'=>[$this,'permission'],'callback'=>[$this,'discard_created']],
]);
register_rest_route('digiter-builder/v1','/documents/(?P\d+)/publish',['methods'=>'POST','permission_callback'=>[$this,'permission'],'callback'=>[$this,'publish_document']]);
register_rest_route('digiter-builder/v1','/documents/(?P\d+)/analyze-design',['methods'=>'POST','permission_callback'=>fn($r)=>$this->can_begin($this->request_id($r)),'callback'=>[$this,'analyze_design']]);
register_rest_route('digiter-builder/v1','/analyze-design',['methods'=>'POST','permission_callback'=>$editor,'callback'=>[$this,'analyze_design']]);
register_rest_route('digiter-builder/v1','/documents/(?P\d+)/begin',['methods'=>'GET','permission_callback'=>fn($r)=>$this->can_begin($this->request_id($r)),'callback'=>[$this,'begin_document']]);
register_rest_route('digiter-builder/v1','/documents/(?P\d+)/start',['methods'=>'POST','permission_callback'=>fn($r)=>$this->can_begin($this->request_id($r)),'callback'=>[$this,'start_document']]);
register_rest_route('digiter-builder/v1','/documents/(?P\d+)/history',['methods'=>'GET','permission_callback'=>[$this,'permission'],'callback'=>[$this,'history']]);
register_rest_route('digiter-builder/v1','/source-target',['methods'=>'POST','permission_callback'=>$editor,'callback'=>[$this,'source_target']]);
register_rest_route('digiter-builder/v1','/site-view/(?Parchive|search|404)',['methods'=>'POST','permission_callback'=>static fn()=>current_user_can('manage_options'),'callback'=>[$this,'site_view_document']]);
register_rest_route('digiter-builder/v1','/preview',['methods'=>'POST','permission_callback'=>$editor,'callback'=>[$this,'preview']]);
register_rest_route('digiter-builder/v1','/catalog/templates',['methods'=>'GET','permission_callback'=>$editor,'callback'=>fn()=>$this->response(Document::templates())]);
register_rest_route('digiter-builder/v1','/templates',[
['methods'=>'GET','permission_callback'=>$editor,'callback'=>[$this,'saved_templates']],
['methods'=>'POST','permission_callback'=>$editor,'callback'=>[$this,'save_template']],
]);
}
public function list_documents($r) {
$page=max(1,min(1000,absint($r->get_param('page')?:1)));
$q=new \WP_Query(['post_type'=>PostTypes::all(),'post_status'=>['draft','pending','publish','private','future'],'posts_per_page'=>20,'paged'=>$page,'meta_key'=>'_digibl_draft','orderby'=>'modified','order'=>'DESC','s'=>sanitize_text_field($r->get_param('search')?:'')]);
$out=[];foreach($q->posts as $p){if(!current_user_can('edit_post',$p->ID)){continue;}$env=$this->envelope($p->ID);$out[]=['id'=>$p->ID,'title'=>$env['doc']['title']?:$p->post_title,'type'=>$p->post_type,'status'=>$p->post_status,'modified'=>get_post_meta($p->ID,'_digibl_saved_at',true)?:$p->post_modified_gmt];}
return $this->response(['documents'=>$out,'pages'=>$q->max_num_pages]);
}
public function create_document($r) {
$type=$r->get_param('type')??'post';if(!is_string($type)||!in_array($type,PostTypes::all(),true)){return new \WP_Error('digibl_post_type','This content type has not enabled Digiter Builder.',['status'=>400]);}$obj=get_post_type_object($type);
if(!current_user_can($obj->cap->create_posts)){return $this->err('create','You cannot create this content type.',403);}
$doc=Document::clean($r->get_param('doc'));if(is_wp_error($doc)){return $doc;}
$id=wp_insert_post(wp_slash(['post_type'=>$type,'post_status'=>'draft','post_title'=>$doc['title']?:'Untitled Digiter draft','post_author'=>get_current_user_id()]),true);
if(is_wp_error($id)){return $id;}
$env=['revision'=>wp_generate_uuid4(),'doc'=>$doc];
$ok=add_post_meta($id,'_digibl_draft',wp_slash(wp_json_encode($env)),true);
if(!$ok){wp_delete_post($id,true);return $this->err('storage','The draft could not be created.',500);}
update_post_meta($id,'_digibl_saved_at',gmdate('Y-m-d H:i:s'));
update_post_meta($id,'_digibl_saved_by',get_current_user_id());
$token=wp_generate_uuid4();update_post_meta($id,'_digibl_creation_session',hash('sha256',get_current_user_id().':'.$token));
return $this->response(array_merge($this->payload($id),['creationToken'=>$token]));
}
private function envelope(int $id): array { $v=json_decode(get_post_meta($id,'_digibl_draft',true),true);return is_array($v)?$v:[]; }
public function site_view_document($r) {
$area=sanitize_key((string)($r->get_url_params()['area']??''));$label=ThemeBuilder::area_label($area);$name=trim(get_bloginfo('name').' — '.$label);
$id=ThemeBuilder::prepare_view_template($area,$name);if(is_wp_error($id)){return $id;}
return $this->response(['id'=>$id,'area'=>$area,'title'=>get_the_title($id),'builderUrl'=>$this->builder_url($id),'assigned'=>ThemeBuilder::assigned_source($area)===$id]);
}
public function source_target($r) {
$id=absint($r->get_param('id'));if(!$id){$url=$r->get_param('url');$id=is_string($url)?$this->source_id_from_url($url):0;}
$p=$id?get_post($id):null;if(!$p||!in_array($p->post_type,PostTypes::all(),true)||in_array($p->post_status,['trash','auto-draft'],true)||!current_user_can('edit_post',$id)){
return $this->response(['id'=>0,'editable'=>false,'message'=>'This part of the preview is not an editable WordPress post or page.']);
}
$owned=$this->owned($id);$external=$this->has_external_builder($id);$editable=$owned||$this->can_prepare($id);
return $this->response(['id'=>$id,'title'=>get_the_title($id),'type'=>$p->post_type,'editable'=>$editable,'owned'=>$owned,'convertible'=>!$owned&&!$external,'editUrl'=>get_edit_post_link($id,'raw'),'builderUrl'=>$editable?$this->builder_url($id):'',
'message'=>$editable?'':($external?'This content is controlled by Beaver Builder or Elementor, so Studio will not overwrite it automatically.':'This content cannot be edited in Studio with your current permissions.')]);
}
public function begin_document($r) {
$id=$this->request_id($r);if($this->owned($id)){return $this->response($this->payload($id));}
$p=get_post($id);$obj=get_post_type_object($p->post_type);
return $this->response(['id'=>$id,'doc'=>$this->source_document($id),'revision'=>'','sourceRevision'=>$this->source_revision($id),'virtual'=>true,'status'=>$p->post_status,'type'=>$p->post_type,'canPublish'=>current_user_can($obj->cap->publish_posts),'frameUrl'=>$this->frame_url($id),'url'=>$this->view_url($id),'editUrl'=>get_edit_post_link($id,'raw'),'publishedRevision'=>'']);
}
public function start_document($r) {
$id=$this->request_id($r);
return $this->locked($id,function()use($id,$r){
clean_post_cache($id);
if(!$this->can_prepare($id)){return $this->err('start','This page cannot be prepared safely in Digiter Studio.',409);}
if($this->owned($id)){return $r->get_param('doc')?$this->err('conflict','This page was started by another session. Reopen it before saving.',409):$this->response($this->payload($id));}
if($r->get_param('sourceRevision')&&!hash_equals($this->source_revision($id),(string)$r->get_param('sourceRevision'))){return $this->err('conflict','The original page changed. Reopen it before saving.',409);}
$doc=Document::clean($r->get_param('doc')?:$this->source_document($id));if(is_wp_error($doc)){return $doc;}
if(!add_post_meta($id,'_digibl_draft',wp_slash(wp_json_encode(['revision'=>wp_generate_uuid4(),'doc'=>$doc])),true)){return $this->err('storage','The draft could not be started.',500);}
$token=wp_generate_uuid4();update_post_meta($id,'_digibl_adoption_session',hash('sha256',get_current_user_id().':'.$token));
update_post_meta($id,'_digibl_adoption_source_revision',$this->source_revision($id));
update_post_meta($id,'_digibl_saved_at',gmdate('Y-m-d H:i:s'));
update_post_meta($id,'_digibl_saved_by',get_current_user_id());return $this->response(array_merge($this->payload($id),['adoptionToken'=>$token]));
});
}
public function discard_created($r) {
$id=$this->request_id($r);
return $this->locked($id,function()use($id,$r){
$env=$this->envelope($id);$kind=$r->get_param('kind')==='adoption'?'adoption':'creation';
$stored=get_post_meta($id,'_digibl_'.$kind.'_session',true);
if(!$stored||!hash_equals($stored,hash('sha256',get_current_user_id().':'.(string)$r->get_param('token')))){return $this->err('session','This page was not created in the current session.',403);}
if(!hash_equals($env['revision'],(string)$r->get_param('revision'))||get_post_meta($id,'_digibl_published',true)){return $this->err('conflict','The page changed after this session. It was not discarded.',409);}
if('adoption'===$kind){
$source=(string)get_post_meta($id,'_digibl_adoption_source_revision',true);
if(!$source||!hash_equals($source,$this->source_revision($id))){return $this->err('conflict','The original page changed after Studio opened it. The original content was not touched.',409);}
foreach(['_digibl_draft','_digibl_saved_at','_digibl_history','_digibl_adoption_session','_digibl_adoption_source_revision'] as $key){delete_post_meta($id,$key);}
}else{
if(get_post_status($id)!=='draft'){return $this->err('conflict','Only this session’s unpublished draft can be discarded.',409);}
// Always move to Trash, including sites configured for immediate permanent deletion.
update_post_meta($id,'_wp_trash_meta_status','draft');update_post_meta($id,'_wp_trash_meta_time',time());
$result=wp_update_post(['ID'=>$id,'post_status'=>'trash'],true);if(is_wp_error($result)){return $result;}
}
return $this->response(['discarded'=>true,'id'=>$id]);
});
}
private function payload(int $id): array {
$p=get_post($id);$obj=get_post_type_object($p->post_type);$env=$this->envelope($id);
return ['id'=>$id,'doc'=>$env['doc'],'sourceRevision'=>$this->source_revision($id),'revision'=>$env['revision'],'status'=>$p->post_status,'type'=>$p->post_type,'url'=>$this->view_url($id),'builderUrl'=>$this->builder_url($id),'frameUrl'=>$this->frame_url($id),'editUrl'=>get_edit_post_link($id,'raw'),'canPublish'=>current_user_can($obj->cap->publish_posts),'savedAt'=>get_post_meta($id,'_digibl_saved_at',true),'publishedRevision'=>get_post_meta($id,'_digibl_published_revision',true)];
}
public function get_document($r) { $payload=$this->payload($this->request_id($r));$doc=Document::clean($payload['doc']);if(is_wp_error($doc)){return $doc;}$payload['doc']=json_decode(wp_json_encode($doc),true);return $this->response($payload); }
private function response($data) { $res=new \WP_REST_Response($data);$res->header('Cache-Control','no-store, private');return $res; }
/** Database-backed lock; the token prevents one request from releasing another's lock. */
private function locked(int $id, callable $run) {
global $wpdb;$name='digibl_write_lock_'.$id;$token=wp_generate_uuid4();$value=wp_json_encode(['token'=>$token,'time'=>time()]);
if(!add_option($name,$value,'','no')) {
$old=get_option($name);$decoded=json_decode((string)$old,true);
if(is_array($decoded) && ($decoded['time']??time())