'','items'=>[]]; } public static function keys(): array { $keys=[];foreach(StylePresets::groups() as $group){$keys=array_merge($keys,$group['keys']);}return array_values(array_unique($keys)); } public static function resolve(array $style): array { $id=$style['globalStyle']??''; if(!$id){return $style;} $item=self::state()['items'][$id]??null; if(!$item){return $style;} $overrides=array_fill_keys($style['globalOverrides']??[],true); foreach($item['style'] as $key=>$value){if(!isset($overrides[$key])){$style[$key]=$value;}} return $style; } private static function response(array $state): \WP_REST_Response { $response=new \WP_REST_Response(['revision'=>$state['revision'],'items'=>array_values($state['items']),'canRestore'=>isset($state['previous'])]); $response->header('Cache-Control','no-store, private');return $response; } public function routes(): void { register_rest_route('digiter-builder/v1','/global-styles',[ ['methods'=>'GET','permission_callback'=>static fn()=>PostTypes::can_edit(),'callback'=>static fn()=>self::response(self::state())], ['methods'=>'POST','permission_callback'=>static fn()=>current_user_can('manage_options'),'callback'=>[$this,'save']], ]); register_rest_route('digiter-builder/v1','/global-styles/(?P[a-f0-9-]{36})',[ 'methods'=>'DELETE','permission_callback'=>static fn()=>current_user_can('manage_options'),'callback'=>[$this,'remove'], ]); register_rest_route('digiter-builder/v1','/global-styles/restore',[ 'methods'=>'POST','permission_callback'=>static fn()=>current_user_can('manage_options'),'callback'=>function($request){ $state=self::state();$revision=$request->get_param('revision'); if(!is_string($revision)||!isset($state['previous']['items'])){return self::error('No previous global styles are available.');} return self::commit(['items'=>$state['previous']['items']],$revision); }, ]); } private static function error(string $message,int $status=400): \WP_Error {return new \WP_Error('digibl_global_style',$message,['status'=>$status]);} private static function commit(array $state,string $revision) { global $wpdb; wp_cache_delete(self::OPTION,'options');$stored=get_option(self::OPTION,false);$current=self::state(); if(!hash_equals((string)$current['revision'],$revision)){return self::error('Global styles changed in another session. Reload before saving.',409);} $state['revision']=wp_generate_uuid4(); $state['previous']=['items'=>$current['items']]; if($stored===false){$success=add_option(self::OPTION,$state,'',false);} else{$success=1===$wpdb->update($wpdb->options,['option_value'=>maybe_serialize($state)],['option_name'=>self::OPTION,'option_value'=>maybe_serialize($stored)],['%s'],['%s','%s']);} wp_cache_delete(self::OPTION,'options'); if(!$success){return self::error('Global styles could not be saved or changed concurrently. Reload and retry.',409);} do_action('digibl_global_styles_changed',$state); return self::response($state); } public function save($request) { $name=$request->get_param('name');$style=$request->get_param('style');$revision=$request->get_param('revision');$id=$request->get_param('id'); if(!is_string($name)||!($name=Document::text($name,100))||!is_array($style)||!is_string($revision)){return self::error('A name, visual style and revision are required.');} if(strlen((string)wp_json_encode($style))>60000){return self::error('This global style is too large.',413);} $visual=StylePresets::visual($style);if(!$visual){return self::error('Choose at least one visual style property.');} $doc=Document::blank();$doc['nodes']=[['id'=>'style','type'=>'heading','props'=>[],'style'=>$visual,'children'=>[]]]; $clean=Document::clean($doc);if(is_wp_error($clean)){return $clean;} $state=self::state(); if($id!==null){if(!is_string($id)||!isset($state['items'][$id])){return self::error('This global style is unavailable.',404);}} else{if(count($state['items'])>=50){return self::error('Use at most 50 linked global styles.',409);}$id=wp_generate_uuid4();} $state['items'][$id]=['id'=>$id,'name'=>$name,'style'=>array_intersect_key(StylePresets::visual($clean['nodes'][0]['style']),$visual),'updated'=>gmdate('c')]; return self::commit($state,$revision); } public function remove($request) { $revision=$request->get_param('revision');$id=(string)$request->get_url_params()['id'];$state=self::state(); if(!is_string($revision)){return self::error('A revision is required.');} if(!isset($state['items'][$id])){return self::error('This global style is unavailable.',404);} unset($state['items'][$id]);return self::commit($state,$revision); } }