'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)time()+3600,'count'=>0];if($rate['count']>=20)return new \WP_Error('digibl_diag_rate','Event limit reached.',['status'=>429]);$rate['count']++;update_option($key,$rate,false);}finally{Wire::unlock($key.'_lock',$lock);} $component=Wire::slug($data['component']??'unknown',180); if(!preg_match('~^(plugins|themes)/~',$component))$component='unknown'; $ok=self::record(['kind'=>'javascript','code'=>$data['code'],'component'=>$component,'line'=>(int)($data['line']??0),'context'=>'editor']); $response=new \WP_REST_Response(['recorded'=>$ok],200);$response->header('Cache-Control','no-store, private');return $response; } public static function rest_error($result,$server,$request) { $route=$request->get_route(); if(!str_starts_with($route,'/digiter-builder/v1/')||str_contains($route,'/diagnostics/'))return $result; $status=$result->get_status();if($status>=500||($status===409&&is_user_logged_in()))self::record(['kind'=>'rest','code'=>$status===409?'revision_conflict':'rest_server_error','component'=>'plugins/digiter-builder-lite/rest','status'=>$status,'context'=>'rest']);return $result; } public static function enqueue(): void { if(!self::settings()['local']||!PostTypes::can_edit())return; wp_enqueue_script('digibl-diagnostics',DIGIBL_URL.'assets/diagnostics.js',[],DIGIBL_VERSION,false); wp_add_inline_script('digibl-diagnostics','window.DIGIBL_DIAGNOSTICS='.wp_json_encode(['url'=>rest_url('digiter-builder/v1/diagnostics/event'),'nonce'=>wp_create_nonce('wp_rest'),'contentPath'=>wp_parse_url(content_url('/'),PHP_URL_PATH)],JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT).';','before'); } public static function tick(): void { $lock=Wire::lock('digibl_diag_event_lock',30);if($lock){try{update_option('digibl_diagnostics_events',self::log(),false);}finally{Wire::unlock('digibl_diag_event_lock',$lock);}} // Bounded cleanup of per-editor rate counters, using exact option names returned by SQL. global $wpdb;$like=$wpdb->esc_like('digibl_diag_browser_').'%'; foreach($wpdb->get_results($wpdb->prepare("SELECT option_name,option_value FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT 500",$like)) as $row){$v=maybe_unserialize($row->option_value);if(is_array($v)&&max((int)($v['until']??0),(int)($v['time']??0)+60)option_name,$v);} $sendLock=Wire::lock('digibl_diag_send_lock',120);if($sendLock){try{$state=get_option('digibl_diagnostics_delivery',[]);if(!empty($state['pending'])&&($state['pending']['created_at']??0)' . esc_html(Language::text('Builder records bounded technical error categories, component paths relative to wp-content, line numbers and timestamps locally for up to 14 days (cleanup requires cron). Raw error messages, request data, cookies, user identifiers and page content are not collected. Administrators can inspect, export, disable or delete these records. Lite does not send diagnostic reports to external services. An administrator may download a JSON report containing the site address, software inventory and technical events and decide whether to share that file manually.')) . '

')); } /** 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 '
';wp_nonce_field('digibl_diagnostics');} public static function screen(): void { if(!current_user_can('manage_options'))return; nocache_headers();$settings=self::settings();$report=self::preview_report(); echo '

'.esc_html(Language::text('Digiter Builder Diagnostics')).'

'; echo '

'.esc_html(Language::text('Technical records stay on this website. Lite does not send reports to external services. Download a JSON report if you wish to review or share it manually.')).'

'; self::form('settings'); echo '

'; submit_button(Language::text('Save diagnostics settings'));echo ''; echo '

'.esc_html(Language::text('Assessment')).'

    '; foreach($report['findings']??[] as $finding){ echo '
  • '.esc_html(Language::text($finding['severity']).' ยท '.Language::text($finding['confidence'])).': '.esc_html(Language::text($finding['recommendation'])).'
  • '; } echo '
'; self::form('export');submit_button(Language::text('Download JSON for review'),'secondary');echo ''; self::form('clear');submit_button(Language::text('Delete local records'),'secondary');echo ''; echo '
'.esc_html(Language::text('Inspect report data')).'
'.esc_html(wp_json_encode($report,JSON_PRETTY_PRINT|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)).'
'; } }