'integration','code'=>'cache_adapter_error','component'=>'plugins/'.Wire::slug($name).'/adapter']);}); add_action('admin_init',[self::class,'privacy']); add_action('digibl_module_render_error',static function($type,$error){self::record(['kind'=>'integration','code'=>'module_render_error','component'=>$error instanceof \Throwable?self::component($error->getFile()):'unknown','line'=>$error instanceof \Throwable?$error->getLine():0]);},10,2); if(!self::settings()['local'])return; self::$reserve=str_repeat('x',65536); self::$previous=set_error_handler([self::class,'php_error']); register_shutdown_function([self::class,'shutdown']); } public static function settings(): array { $v=get_option('digibl_diagnostics_settings',[]);$v=is_array($v)?$v:[]; return ['local'=>($v['local']??true)===true]; } public static function context(): string { if(isset($_GET['digibl_builder'])||isset($_GET['digibl_frame'])||(is_admin()&&($_GET['page']??'')==='digiter-builder'))return 'editor'; $uri=is_string($_SERVER['REQUEST_URI']??null)?$_SERVER['REQUEST_URI']:''; return str_contains($uri,'digiter-builder/v1/')||str_contains(is_string($_GET['rest_route']??null)?$_GET['rest_route']:'','digiter-builder/v1/')?'rest':'public'; } public static function component(string $file): string { $file=str_replace('\\','/',$file);$root=str_replace('\\','/',WP_CONTENT_DIR).'/'; if(str_starts_with($file,$root))return Wire::slug(substr($file,strlen($root)),180); if(str_starts_with($file,str_replace('\\','/',ABSPATH).'wp-includes/'))return 'core/'.Wire::slug(basename($file)); return 'unknown'; } private static function relevant(string $file): bool {return str_starts_with(str_replace('\\','/',$file),str_replace('\\','/',DIGIBL_PATH))||in_array(self::context(),['editor','rest'],true);} public static function php_error(int $level,string $message,string $file,int $line): bool { // Zero arguments READ the current mask; never change the site's error reporting. // Respect errors suppressed by the host or PHP's @ operator, then chain the prior handler. if(!self::$busy&&(error_reporting()&$level)&&self::relevant($file)){ // phpcs:ignore PluginCheck.CodeAnalysis.PHPErrorReporting.DirectErrorReportingCall,WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting -- Zero-argument getter reads the existing mask to respect suppressed errors; does not change PHP configuration. $code=in_array($level,[E_DEPRECATED,E_USER_DEPRECATED],true)?'php_deprecated':(in_array($level,[E_NOTICE,E_USER_NOTICE],true)?'php_notice':'php_warning'); self::record(['kind'=>'php','code'=>$code,'component'=>self::component($file),'line'=>$line]); } return is_callable(self::$previous)?(bool)call_user_func(self::$previous,$level,$message,$file,$line):false; } public static function shutdown(): void { self::$reserve='';$e=error_get_last(); if(!$e||!in_array($e['type'],[E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR,E_USER_ERROR],true)||!self::relevant($e['file']??''))return; $code=str_contains($e['message'],'Allowed memory size')?'memory_exhausted':'php_fatal'; self::record(['kind'=>'php','code'=>$code,'component'=>self::component($e['file']),'line'=>$e['line']]); } public static function record(array $event): bool { if(self::$busy||(self::$captured>=10&&!in_array($event['code']??'',['php_fatal','memory_exhausted'],true))||!self::settings()['local'])return false; $event['context']=$event['context']??self::context();$event['count']=1;$event['first_seen']=$event['last_seen']=time();$event=Wire::event($event);if(!$event)return false; self::$busy=true;$lock=null; try{ $lock=Wire::lock('digibl_diag_event_lock',30);if(!$lock)return false; $data=self::log();$hash=hash('sha256',wp_json_encode(array_intersect_key($event,array_flip(['kind','code','component','line','context','status'])))); if(isset($data['events'][$hash])){$event['count']=min(1000000,$data['events'][$hash]['count']+1);$event['first_seen']=$data['events'][$hash]['first_seen'];unset($data['events'][$hash]);} $data['events'][$hash]=$event;$data['events']=array_slice($data['events'],-100,null,true);$data['seq']++; $ok=update_option('digibl_diagnostics_events',$data,false);self::$captured++;return $ok; }catch(\Throwable $e){return false;}finally{if($lock)Wire::unlock('digibl_diag_event_lock',$lock);self::$busy=false;} } public static function log(): array { $raw=get_option('digibl_diagnostics_events',[]);$out=['seq'=>is_array($raw)?(int)($raw['seq']??0):0,'events'=>[]]; foreach(array_slice(is_array($raw['events']??null)?$raw['events']:[],-100,null,true) as $key=>$event){$e=Wire::event($event);if($e&&$e['last_seen']>=time()-14*DAY_IN_SECONDS)$out['events'][$key]=$e;}return $out; } public static function report(): array { global $wpdb; require_once ABSPATH.'wp-admin/includes/plugin.php';$plugins=get_plugins();$active=array_unique(array_merge((array)get_option('active_plugins',[]),is_multisite()?array_keys((array)get_site_option('active_sitewide_plugins',[])):[]));$list=[]; foreach($active as $file)$list[]=['slug'=>$file,'version'=>$plugins[$file]['Version']??'unknown']; $theme=wp_get_theme();$upload=wp_upload_dir(null,false);$flags=['multisite'=>is_multisite(),'object_cache'=>wp_using_ext_object_cache(),'cron_disabled'=>defined('DISABLE_WP_CRON')&&DISABLE_WP_CRON,'uploads_writable'=>empty($upload['error'])&&wp_is_writable($upload['basedir']),'theme_regions_supported'=>ThemeSupport::supported()]; $extensions=[];foreach(['curl','mbstring','zip','fileinfo','gd','imagick'] as $ext)$extensions[$ext]=extension_loaded($ext); $limit=ini_get('memory_limit');$env=['builder'=>DIGIBL_VERSION,'wordpress'=>get_bloginfo('version'),'php'=>PHP_VERSION,'db_engine'=>!empty($wpdb->is_mysql)?'mysql':(defined('DB_ENGINE')&&DB_ENGINE==='sqlite'?'sqlite':'unknown'),'db_version'=>$wpdb->db_version(),'memory_limit_mb'=>$limit==='-1'?-1:(int)(wp_convert_hr_to_bytes($limit)/1048576),'theme'=>['slug'=>$theme->get_stylesheet(),'version'=>$theme->get('Version')],'plugins'=>$list,'flags'=>$flags,'extensions'=>$extensions]; $report=Wire::clean(['schema'=>1,'product'=>'digiter-builder-lite','report_id'=>wp_generate_uuid4(),'created_at'=>time(),'site'=>home_url('/'),'environment'=>$env,'events'=>array_values(self::log()['events'])]); return is_wp_error($report)?[]:$report; } public static function preview_report(): array { return self::report() ?: []; } public static function routes(): void { register_rest_route('digiter-builder/v1','/diagnostics/event',['methods'=>'POST','permission_callback'=>static fn()=>PostTypes::can_edit(),'callback'=>[self::class,'browser_event']]); } public static function browser_event($r) { if(!self::settings()['local'])return new \WP_REST_Response(['recorded'=>false],200); if(strlen($r->get_body())>2048)return new \WP_Error('digibl_diag_size','Event too large.',['status'=>413]); $data=$r->get_json_params();if(!is_array($data)||!in_array($data['code']??'', ['javascript_error','resource_error','promise_error'],true))return new \WP_Error('digibl_diag_event','Invalid event.',['status'=>400]); $key='digibl_diag_browser_'.get_current_user_id();$lock=Wire::lock($key.'_lock',30);if(!$lock)return new \WP_Error('digibl_diag_busy','Try later.',['status'=>429]); try{$rate=get_option($key,[]);if(($rate['until']??0)
')); } /** Settings and deletion cannot race an in-flight report or recreate deleted data. */ public static function update_settings(array $next) { $next = ['local'=>!empty($next['local'])]; $lock=Wire::lock('digibl_diag_send_lock',120);if(!$lock)return new \WP_Error('busy','A report is being sent. Wait a moment and try again.'); try{ $old=self::settings(); if(!update_option('digibl_diagnostics_settings',$next,false)&&self::settings()!==$next)return new \WP_Error('storage','Settings could not be saved.'); delete_option('digibl_diagnostics_delivery'); return true; }finally{Wire::unlock('digibl_diag_send_lock',$lock);} } public static function clear_records() { $send=Wire::lock('digibl_diag_send_lock',120);if(!$send)return new \WP_Error('busy','A report is being sent. Wait a moment and try again.'); $events=null; try{ $events=Wire::lock('digibl_diag_event_lock',30);if(!$events)return new \WP_Error('busy','Records are being updated. Try again.'); delete_option('digibl_diagnostics_events');delete_option('digibl_diagnostics_delivery'); if(get_option('digibl_diagnostics_events',null)!==null||get_option('digibl_diagnostics_delivery',null)!==null)return new \WP_Error('storage','Records could not be deleted.'); return true; }finally{if($events)Wire::unlock('digibl_diag_event_lock',$events);Wire::unlock('digibl_diag_send_lock',$send);} } public static function action(): void { if(!current_user_can('manage_options'))wp_die(esc_html(Language::text('Access denied.')),'',['response'=>403]);check_admin_referer('digibl_diagnostics'); $op=is_string($_POST['operation']??null)?$_POST['operation']:''; if($op==='export'){nocache_headers();header('Content-Type: application/json; charset=utf-8');header('Content-Disposition: attachment; filename="digiter-builder-diagnostics.json"');wp_send_json(self::preview_report(),200,JSON_PRETTY_PRINT|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT);} $notice='saved'; if($op==='settings'){ $next=['local'=>isset($_POST['local'])]; $result=self::update_settings($next);if(is_wp_error($result))wp_die(esc_html(Language::text($result->get_error_message()))); }elseif($op==='clear'){$result=self::clear_records();if(is_wp_error($result))wp_die(esc_html(Language::text($result->get_error_message())));$notice='cleared';} else wp_die(esc_html(Language::text('Invalid operation.'))); wp_safe_redirect(admin_url('admin.php?page=digibl-diagnostics¬ice='.$notice));exit; } private static function form(string $op): void {echo '