....................................../////.===Shadow-Here===./////................................................ > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < > < ------------------------------------------------------------------------------------------------------------------- /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// RIFF¤ WEBPVP8 ˜ ðÑ *ôô>‘HŸK¥¤"§£±¨àð enü¹%½_F‘åè¿2ºQú³íªú`N¿­3ÿƒügµJžaÿ¯ÿ°~¼ÎùnúîÞÖô•òíôÁÉß®Sm¥Ü/ ‡ó˜f£Ùà<˜„xëJ¢Ù€SO3x<ªÔ©4¿+ç¶A`q@Ì“Úñè™ÍÿJÌ´ª-˜ÆtÊÛL]Ïq*‘Ý”ì#ŸÌÏãY]@ê`¿ /ªfkØB4·®£ó z—Üw¥Pxù–ÞLШKÇN¾AkÙTf½è'‰g gÆv›Øuh~ a˜Z— ïj*á¥t d£“uÒ ¨`K˜¹ßþ]b>˜]_ÏÔ6W—è2r4x•íÖ…"ƒÖNîä!¦å Ú}ýxGøÌ —@ ;ÆÚŠ=ɾ1ý8lªË¥ô ^yf®Œ¢u&2©nÙÇ›ñÂñŒ³ aPo['½»øFùà­+4ê“$!lövlüÞ=;N®3ð‚õ›DÉKòÞ>ÄÍ ¥ˆuߤ#ˆ$6ù™¥îЇy’ÍB¼ çxÛ;X"WL£R÷͝*ó-¶Zu}º.s¸sšXqù–DþÿvªhüïwyŸ ¯é³lÀ:KCûÄ£Ëá\…­ ~—ýóî ¼ûûÜTÓüÇy…ŽÆvc»¾×U ñ¸žþоP÷¦ó:Ò¨¨5;Ð#&#ÖúñläÿÁœ GxÉ­/ñ‡áQðìYÉtÒw޼GÔ´zàÒò ð*ëzƒ•4~H]Ø‹f ñÓÈñ`NåWçs'ÆÏW^ø¹!XžµmQ5ÃËoLœÎ: ÞËÍ¥J ù…î èo£ßPÎñ¶ž8.Œ]ʵ~5›ÙË-ù*8ÙÖß±~ ©¹rÓê‚j¶d¸{^Q'˜±Crß ÚH—#¥¥QlÀ×ëã‡DÜ«èî þ&Çæžî;ŽÏºò6ÒLÃXy&ZŒ'j‚¢Ù€IßÚù+–MGi‰*jE€‘JcÜ ÓÌ EÏÚj]o˜ Þr <¾U ûŪæÍ/šÝH¥˜b”¼ ÁñßX GP›ï2›4WŠÏà×£…íÓk†¦H·ÅíMh–*nó÷à]ÁjCº€b7<ب‹¨5車bp2:Á[UªM„QŒçiNMa#<5›áËó¸HýÊ"…×Éw¹¦ì2º–x<›»a±¸3Weü®FÝ⑱ö–î–³|LPÈ~çð~Çå‡|º kD¢µÏàÆAI %1À% ¹Ò – ”ϝS¦‰4&¶£°à Öý”û_Ò Áw°A«Å€?mÇÛgHÉ/8)á¾ÛìáöŽP í¨PŸNÙµº¦‡§Ùš"ÿ«>+ªÕ`Ê÷‡‚ß Õû˜þãÇ-PÍ.¾XV‘€ dÜ"þ4¹ ±Oú‘©t¥¦FªÄÃÄ•b‚znýu½—#cDs˜ÃiÑOˆñ×QO=*IAÊ,¶ŽZƒ;‡wøXè%EÐk:F±Ú” .Ѽ+Áu&Ç`."pÈÉw o&¿dE6‘’EqTuK@Ì¥ã™À(Êk(h‰,H}RÀIXÛš3µ1©_OqÚÒJAñ$ÊÙÜ;D3çŒ[þùœh¬Ã³™ö6ç†NY".Ú‰ï[ªŸŒ '²Ð öø_¨ÂÉ9ué¶³ÒŠõTàîMØ#û¯gN‡bÙ놚X„ö …ÉeüÌ^J ‹€.œ$Æ)βÄeæW#óüßĺŸ€ ÀzwV 9oä»f4V*uB «Ë†¹ì¯žR霓æHXa=&“I4K;¯ç‹h×·"UŠ~<•╪Vêª&ÍSÃÆÅ?ÔqÎ*mTM ˜›µwêd#[C¡©§‘D<©àb†–ÁœøvH/,í:¯( ²£|4-„Æövv„Yͼ™^Á$ˆ„¢Û[6yB.åH*V¨æ?$=˜Ñ€•ñ·­(VlŸ‘ nÀt8W÷´Bûba?q9ú¶Xƒl«ÿ\ù¶’þòUÐj/õ¢Ìµ³g$ƒÎR!¸»|Oߍë’BhîÚÑ¢ñåŒJ„®„£2Ð3•ô02Nt…!£Í]Ïc½Qÿ?ˆ<&ÃA¾Ú,JˆijÌ#5yz„‰Î|ÊŽ5QÏ:‹ÐaóVÔxW—CpeÏzÐïíçôÿÅ_[hãsÐ_/ŽTÝ?BîˆííV$<¿i>²F¬_Eß¿ †bÊŒº­ÿ®Z H“C}”¬,Mp ý/Bá£w>˜YV°aƒúh+cŠ- r/[%|üUMHäQ°X»|û/@|°¥Ð !BÔ Ç¢Ä©š+Õì D«7ìN¶ŽðÔ " ƶ’ÖçtA‰Û×}{tþz­¾GÍ›k¹OEJR$ Â׃ «ëÁ"oÉôž$oUK(Ä)Ãz³Ê-‹êN[Ò3Œñbï8P 4ƒ×q¢bo|?<ÛX¬òÄͰL–±›(™ûG?ýË©ÚÄ–ÂDØÐ_Ç¡ô ¾–ÄÏø ×e8Ë©$ÄF¹Å‹ì[©óìl:F¾f´‹‹Xì²ï®\¬ôùƒ ÿat¥óèÒùHß0äe‚;ü×h:ÆWðHž=Ã8骣"kœ'Y?³}Tûè€>?0l›e1Lòñ„aæKÆw…hÖŠùW…ÈÆÄ0ši·›[pcwËþñiêíY/~-Á5˜!¿†A›™Mÿþ(±“t@â“ö2­´TG5yé]çå僳 .·ÍïçÝ7UÚ±Ð/Nè»,_Ï ùdj7\ï Wì4›„»c¸àešg#ÒÊ⥭áØo5‘?ÌdÝô¯ ¹kzsƒ=´#ëÉK›Ø´±-¥eW?‡çßtòTã…$Ý+qÿ±ƒ÷_3Ô¥í÷:æ–ž<·Ö‡‰Å¢ š‡%Ô—utÌÈìðžgÖÀz²À—ï÷Óîäõ{K'´È÷³yaÏÁjƒô}ž§®æÊydÕÈë5¯èˆõvÕ©ã*çD„ “z„Ó‡^^xÂ3M§A´JG‚öï 3W'ˆ.OvXè¡ÊÕª?5º7†˜(˜Ç¶#çê’¶!ÌdZK§æ 0fãaN]òY³RV ™î$®K2R¨`W!1Ôó\;Ý ýB%qæK•&ÓÈe9È0êI±žeŸß -ú@žQr¦ ö4»M¼Áè¹µmw 9 EÆE_°2ó„ŸXKWÁ×Hóì^´²GѝF©óäR†¦‰ç"V»eØ<3ùd3ÿÚ¤Žú“Gi" —‘_ÙËÎ~Üö¯¥½Î»üŸEÚŽåmÞþí ;ÞólËΦMzA"Âf(´òá;Éï(/7½ûñÌ­cïÕçлþÝz¾-ÍvÑ“pH­–ðÓj$¸Äû¤‚‘ãUBË-n“2åPkS5&‹Â|+g^œ®Ì͆d!OïäîU«c;{Û!ÅŽ«ëZ9Ókóˆ]¯ƒ›né `ÇÒ+tÆš (ØKá¾—=3œ®•vuMñg²\ï Ec€ 05±d™‡×iÇ×›UúvÌ¢£Èþ¡ÕØô¶ßÎA"ß±#Ö²ˆÊŸ¦*Ä~ij|àø.-¼'»Ú¥£h ofº¦‡VsR=N½„Î v˜Z*SÌ{=jÑB‹tê…;’HžH¯8–îDù8ñ¢|Q•bÛçš–‹m³“ê¨ åÏ^m¬Žãþ©ïêO‡½6] µÆ„Ooòü ²x}N¦Ë3ïé¿»€›HA˜m%çÞ/¿í7Fø“‹léUk)É°Œµ8Q8›:ÀŠeT*šõ~ôڝG6 ¢}`ùH­–”¡k ‰P1>š†®9z11!X wKfmÁ¦xÑ,N1Q”–æB¶M…ÒÃv6SMˆhU¬ÊPŽï‘öj=·CŒ¯u¹ƒVIЃsx4’ömÛýcå¡¶7ßŠß 57^\wÒÐÆ k§h,Œý î«q^R½3]J¸ÇðN ‚çU¬ôº^Áì} ³f©Õœ§ˆã:FÄÈ‚é(€™?àýÓüè1Gô£¼éj‚OÅñ  #>×—ßtà 0G¥Åa뀐kßhc™À_ÉñÞ#±)GD" YîäË-ÿÙ̪ ¹™a¯´¢E\ÝÒö‚;™„ë]_ p8‰o¡ñ+^÷ 3‘'dT4œŽ ðVë½° :¬víÑ«£tßÚS-3¶“þ2 †üüʨòrš¹M{É_¤`Û¨0ìjœøJ‡:÷ÃáZ˜†@GP&œÑDGÏs¡þ¦þDGú‘1Yá9Ôþ¼ ûø…§÷8&–ÜÑnÄ_m®^üÆ`;ÉVÁJ£?â€-ßê}suÍ2sõA NÌúA磸‘îÿÚ»ƒìö·á¿±tÑÐ"Tÿü˜[@/äj¬€uüªìù¥Ý˜á8Ý´sõj 8@rˆð äþZÇD®ÿUÏ2ùôõrBzÆÏÞž>Ì™xœ“ wiÎ×7_… ¸ \#€MɁV¶¥üÕÿPÔ9Z‡ø§É8#H:ƒ5ÀÝå9ÍIŒ5åKÙŠ÷qÄ>1AÈøžj"µÂд/ªnÀ qªã}"iŸBå˜ÓÛŽ¦…&ݧ;G@—³b¯“•"´4í¨ôM¨åñC‹ïùÉó¯ÓsSH2Ý@ßáM‡ˆKÀªÛUeø/4\gnm¥‹ŸŒ qÄ b9ÞwÒNÏ_4Ég³ú=܆‚´ •â¥õeíþkjz>éÚyU«Íӝ݃6"8/ø{=Ô¢»G¥ äUw°W«,ô—¿ãㆅү¢³xŠUû™yŒ (øSópÐ 9\åTâ»—*oG$/×ÍT†Y¿1¤Þ¢_‡ ¼ „±ÍçèSaÓ 3ÛMÁBkxs‰’R/¡¤ˆÙçª(*õ„üXÌ´ƒ E§´¬EF"Ù”R/ÐNyÆÂ^°?™6¡œïJ·±$§?º>ÖüœcNÌù¯G ‹ñ2ЁBB„^·úìaz¨k:#¨Æ¨8LÎõލ£^§S&cŒÐU€ü(‡F±Š¼&P>8ÙÁ ‰ p5?0ÊÆƒZl¸aô š¼¡}gÿ¶zÆC²¹¬ÎÖG*HB¡O<º2#ñŒAƒ–¡B˜´É$¥›É:FÀÔx¾u?XÜÏÓvN©RS{2ʈãk9rmP¼Qq̳ è¼ÐFׄ^¡Öì fE“F4A…!ì/…¦Lƒ… … $%´¾yã@CI¬ á—3PþBÏNÿ<ý°4Ü ËÃ#ØÍ~âW«rEñw‹eùMMHß²`¬Öó½íf³:‹k˜¯÷}Z!ã¿<¥,\#öµÀ¯aÒNÆIé,Ћ–lŽ#Àæ9ÀÒS·I’½-Ïp Äz¤Š Â* ­íÄ9­< h>׍3ZkËU¹§˜ŒŠ±f­’¤º³Q ÏB?‹#µíÃ¥®@(Gs«†vI¥Mµ‹Á©e~2ú³ÁP4ìÕi‚²Ê^ö@-DþÓàlÜOÍ]n"µã:žpsŽ¢:! Aõ.ç~ÓBûH÷JCÌ]õVƒd «ú´QÙEA–¯¯Œ!.ˆˆëQ±ù œ·Ì!Õâ )ùL„ÅÀlÚè5@B…o´Æ¸XÓ&Û…O«˜”_#‡ƒ„ûÈt!¤ÁÏ›ÎÝŠ?c9 â\>lÓÁVÄÑ™£eØY]:fÝ–—ù+p{™ðè û³”g±OƒÚSù£áÁÊ„ä,ï7š²G ÕÌBk)~ÑiCµ|h#u¤¶îK¨² #²vݯGãeÖ϶ú…¾múÀ¶þÔñ‚Š9'^($¤§ò “š½{éúp÷J›ušS¹áªCÂubÃH9™D™/ZöØÁ‡¦ÝÙŸ·kð*_”.C‹{áXó€‡c¡c€§/šò/&éš÷,àéJþ‰X›fµ“C¨œ®r¬"kL‰Â_q…Z–.ÉL~O µ›zn‚¹À¦Öª7\àHµšÖ %»ÇníV[¥*Õ;ƒ#½¾HK-ÖIÊdÏEÚ#=o÷Óò³´Š: Ç?{¾+9›–‘OEáU·S€˜j"ÄaÜ ŒÛWt› á–c#a»pÔZÞdŽtWê=9éöÊ¢µ~ ë ;Öe‡Œ®:bî3±ýê¢wà¼îpêñ¹¾4 zc¾ðÖÿzdêŒÑÒŝÀ‰s6¤í³ÎÙB¿OZ”+F¤á‡3@Ñëäg©·Ž ˆèª<ù@É{&S„œÕúÀA)‰h:YÀ5^ÂÓŒ°õäU\ ùËÍû#²?Xe¬tu‰^zÒÔãë¼ÛWtEtû …‚g¶Úüâî*moGè¨7%u!]PhÏd™Ý%Îx: VÒ¦ôÊD3ÀŽKÛËãvÆî…N¯ä>Eró–ð`5 Œ%u5XkñÌ*NU%¶áœÊ:Qÿú»“úzyÏ6å-၇¾ ´ ÒÊ]y žO‘w2Äøæ…H’²f±ÎÇ.ª|¥'gîV•Ü .̘¯€šòü¤U~Ù†*¢!?ò wý,}´°ÔÞnïoKq5µb!áÓ3"vAßH¡³¡·G(ÐÎ0Îò¼MG!/ài®@—¬04*`…«é8ªøøló“ˆÊ”èù¤…ßÊoÿé'ËuÌÖ5×È¡§ˆˆfŽë9}hìâ_!!¯  B&Ëö¶‰ÀAÙNVŸ Wh›¸®XÑJì¨ú“¿÷3uj²˜¨ÍÎìë±aúŠÝå¯ð*Ó¨ôJ“yºØ)m°WýOè68†ŸÏ2—‰Ïüꪫٚ¥‹l1 ø ÏÄFjêµvÌbü¦èÝx:X±¢H=MÐß—,ˆÉÇ´(9ú¾^ÅÚ4¿m‡$âX‘å%(AlZo@½¨UOÌÕ”1ø¸jÎÀÃÃ_ µ‘Ü.œº¦Ut: Æï’!=¯uwû#,“pþÇúŒø(é@?³ü¥‘Mo §—s@Œ#)§ŒùkL}NOÆêA›¸~r½¼ÙA—HJ«eˆÖ´*¡ÓpÌŸö.m<-"³ûÈ$¬_6­åf£ïÚâj1y§ÕJ½@dÞÁr&Í\Z%D£Íñ·AZ Û³øüd/ªAi†/Й~  ‡âĮҮÏh§°b—›Û«mJžòG'[ÈYýŒ¦9psl ýÁ ®±f¦x,‰½tN ‚Xª9 ÙÖH.«Lo0×?͹m¡å†Ѽ+›2ƒF ±Ê8 7Hցϓ²Æ–m9…òŸï]Â1äN†VLâCˆU .ÿ‰Ts +ÅÎx(%¦u]6AF Š ØF鈄‘ |¢¶c±soŒ/t[a¾–û:s·`i햍ê›ËchÈ…8ßÀUÜewŒðNOƒõD%q#éû\9¤x¹&UE×G¥ Í—™$ð E6-‡¼!ýpãÔM˜ Âsìe¯ñµK¢Ç¡ùôléœ4Ö£”À Š®Ðc ^¨À}ÙËŸ§›ºê{ÊuÉC ×Sr€¤’fÉ*j!úÓ’Gsùìoîßîn%ò· àc Wp÷$¨˜)û»H ×8ŽÒ€Zj¤3ÀÙºY'Ql¦py{-6íÔCeiØp‘‡XÊîÆUߢ܂ž£Xé¼Y8þ©ëgñß}é.ÎógÒ„ÃØËø¯»™§Xýy M%@NŠ À(~áÐvu7&•,Ù˜ó€uP‡^^®=_E„jt’ 403WebShell
403Webshell
Server IP : 51.91.236.193  /  Your IP : 216.73.216.177
Web Server : Apache
System : Linux webm019.cluster128.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64
User : thillelirn ( 8290)
PHP Version : 8.0.30
Disable Function : _dyuweyrj4,_dyuweyrj4r,dl
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/thillelirn/sexo_tlse/wp-content/plugins/burst-statistics/includes/Admin/App/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/thillelirn/sexo_tlse/wp-content/plugins/burst-statistics/includes/Admin/App/class-app.php
<?php
namespace Burst\Admin\App;

use Burst\Admin\App\Fields\Fields;
use Burst\Admin\App\Fields\Reporting_Fields;
use Burst\Admin\App\Menu\Menu;
use Burst\Admin\Burst_Onboarding\Burst_Onboarding;
use Burst\Admin\Reports\Reports;
use Burst\Admin\Statistics\Goal_Statistics;
use Burst\Admin\Tasks;
use Burst\Frontend\Endpoint;
use Burst\Frontend\Goals\Goal;
use Burst\Frontend\Goals\Goals;
use Burst\TeamUpdraft\Installer\Installer;
use Burst\Traits\Admin_Helper;
use Burst\Traits\Database_Helper;
use Burst\Traits\Helper;
use Burst\Traits\Sanitize;
use Burst\Traits\Save;
use Burst\UserAgentParser\UserAgentParser;

use function Burst\burst_loader;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require_once BURST_PATH . 'includes/Admin/App/rest-api-optimizer/rest-api-optimizer.php';
require_once BURST_PATH . 'includes/Admin/App/media/media-override.php';

class App {
	use Helper;
	use Admin_Helper;
	use Database_Helper;
	use Save;
	use Sanitize;

	public Menu $menu;
	public Fields $fields;
	public Tasks $tasks;

	/**
	 * Reporting fields.
	 */
	public Reporting_Fields $reporting_fields;
	public string $nonce_expired_feedback = 'Session expired. Try refreshing the page.';

	/**
	 * Initialize the App class
	 */
	public function init(): void {
		add_action( 'admin_menu', [ $this, 'add_menu' ] );
		add_action( 'wp_ajax_burst_rest_api_fallback_do_action', [ $this, 'rest_api_fallback_do_action' ] );
		add_action( 'wp_ajax_burst_rest_api_fallback_get_action', [ $this, 'rest_api_fallback_get_action' ] );
		add_action( 'admin_footer', [ $this, 'fix_duplicate_menu_item' ], 1 );
		add_action( 'burst_after_save_field', [ $this, 'update_for_multisite' ], 10, 4 );
		add_action( 'rest_api_init', [ $this, 'settings_rest_route' ], 8 );
		add_filter( 'burst_localize_script', [ $this, 'extend_localized_settings_for_dashboard' ], 10, 1 );
		add_action( 'burst_weekly', [ $this, 'init_cleanup' ] );
		add_action( 'burst_weekly_clear_referrers_cron', [ $this, 'weekly_clear_referrers_table' ] );
		add_action( 'burst_weekly_clear_spam_browsers_cron', [ $this, 'weekly_clear_spam_browsers' ] );
		add_action( 'burst_daily', [ $this, 'maybe_update_plugin_path' ] );
		$this->menu             = new Menu();
		$this->fields           = new Fields();
		$this->reporting_fields = new Reporting_Fields();
		$this->reporting_fields->init();

		$onboarding = new Burst_Onboarding();
		$onboarding->init();
	}

	/**
	 * Check plugin path daily and maybe update the path if it is changed.
	 */
	public function maybe_update_plugin_path(): void {
		$stored_path  = get_option( 'burst_plugin_path', '' );
		$current_path = BURST_PATH;
		if ( $stored_path !== $current_path ) {
			update_option( 'burst_plugin_path', $current_path, true );
		}
	}

	/**
	 * Remove the fallback notice if REST API is working again
	 */
	public function remove_fallback_notice(): void {
		if ( get_option( 'burst_ajax_fallback_active' ) !== false ) {
			delete_option( 'burst_ajax_fallback_active' );
			delete_option( 'burst_ajax_fallback_active_timestamp' );
			\Burst\burst_loader()->admin->tasks->schedule_task_validation();
		}
	}

	/**
	 * Fix the duplicate menu item
	 */
	public function fix_duplicate_menu_item(): void {
		/**
		 * Handles URL changes to update the active menu item
		 * Ensures the WordPress admin menu stays in sync with the React app navigation
		 */
		// not processing form data, only a conditional script on the burst page.
		// phpcs:ignore
		if ( isset( $_GET['page'] ) && $_GET['page'] === 'burst' ) {
			?>
			<script>
				window.addEventListener("load", () => {
					const submenu = document.querySelector('li.wp-has-current-submenu.toplevel_page_burst .wp-submenu');
					const burstMain = document.querySelector('li.toplevel_page_burst ul.wp-submenu li.wp-first-item a');
					if (burstMain) burstMain.href = '#/';
					if (!submenu) return;

					const menuItems = submenu.querySelectorAll('li');

					const getBaseHash = (url) => {
						const [base, hash = ''] = url.split('#');
						const section = hash.split('/')[1] || '';
						return `${base}#/${section}`;
					};

					const normalize = (url) => {
						try {
							const u = new URL(url);
							const page = u.searchParams.get('page');
							if (!page) return url;
							const hash = url.includes('#') ? '#' + url.split('#')[1] : '';
							return getBaseHash(`${u.origin}${u.pathname}?page=${page}${hash}`);
						} catch {
							return url;
						}
					};

					const updateActiveMenu = () => {
						const current = normalize(location.href);
						menuItems.forEach(item => {
							const link = item.querySelector('a');
							item.classList.toggle('current', link && normalize(link.href) === current);
						});
					};

					updateActiveMenu();

					['pushState', 'replaceState'].forEach(type => {
						const original = history[type];
						history[type] = function () {
							original.apply(this, arguments);
							updateActiveMenu();
						};
					});

					window.addEventListener('popstate', updateActiveMenu);
					window.addEventListener('hashchange', updateActiveMenu);
				});
			</script>
			<?php
		}
	}


	/**
	 * Add a menu item for the plugin
	 */
	public function add_menu(): void {
		if ( ! $this->user_can_view() ) {
			return;
		}

		// if track network wide is enabled, show the menu only on the main site.
		if ( is_multisite() && get_site_option( 'burst_track_network_wide' ) && self::is_networkwide_active() ) {
			if ( ! is_main_site() ) {
				return;
			}
		}

		$menu_label    = __( 'Statistics', 'burst-statistics' );
		$count         = burst_loader()->admin->tasks->plusone_count();
		$warning_title = esc_attr( $this->sprintf( '%d plugin warnings', $count ) );
		if ( $count > 0 ) {
			$warning_title .= ' ' . esc_attr( $this->sprintf( '(%d plus ones)', $count ) );
			$menu_label    .=
				"<span class='update-plugins count-$count' title='$warning_title'>
			<span class='update-count'>
				" . number_format_i18n( $count ) . '
			</span>
		</span>';
		}

		$page_hook_suffix = add_menu_page(
			'Burst Statistics',
			$menu_label,
			'view_burst_statistics',
			'burst',
			[ $this, 'dashboard' ],
			BURST_URL . 'assets/img/burst-wink.svg',
			apply_filters( 'burst_menu_position', 3 )
		);

		// Get menu configuration and create submenu items dynamically.
		$menu_config = $this->menu->get();
		$this->create_submenu_items( $menu_config );

		// Add "Upgrade to Pro" menu item if not Pro version.
		$this->add_upgrade_menu_item();

		add_action( "admin_print_scripts-{$page_hook_suffix}", [ $this, 'plugin_admin_scripts' ], 1 );
	}

	/**
	 * Create submenu items from configuration
	 *
	 * @param array<int, array<string, mixed>> $menu_config Menu configuration array.
	 */
	private function create_submenu_items( array $menu_config ): void {
		foreach ( $menu_config as $menu_item ) {
			// Skip items that shouldn't appear in WordPress admin menu.
			if ( ! isset( $menu_item['show_in_admin'] ) || ! $menu_item['show_in_admin'] ) {
				continue;
			}

			$capability = $menu_item['capabilities'] ?? 'view_burst_statistics';
			if ( ! current_user_can( $capability ) ) {
				continue;
			}

			$page_title = $menu_item['title'] ?? '';
			$menu_title = $menu_item['title'] ?? '';
			$menu_slug  = $menu_item['menu_slug'] ?? 'burst';

			add_submenu_page(
				'burst',
				$page_title,
				$menu_title,
				$capability,
				$menu_slug,
				[ $this, 'dashboard' ]
			);
		}
	}

	/**
	 * Add "Upgrade to Pro" menu item if not Pro version
	 */
	// phpcs:disable
	private function add_upgrade_menu_item(): void {
		if ( defined( 'BURST_PRO' ) ) {
			return;
		}

		global $submenu;
		if ( ! isset( $submenu['burst'] ) ) {
			return;
		}

		$class              = 'burst-link-upgrade';
		$highest_index      = count( $submenu['burst'] );
		$submenu['burst'][] = [
			__( 'Upgrade to Pro', 'burst-statistics' ),
			'manage_burst_statistics',
			$this->get_website_url( 'pricing/', [ 'utm_source' => 'plugin-submenu-upgrade' ] ),
		];

		if ( isset( $submenu['burst'][ $highest_index ] ) ) {
			if ( ! isset( $submenu['burst'][ $highest_index ][4] ) ) {
				$submenu['burst'][ $highest_index ][4] = '';
			}
			$submenu['burst'][ $highest_index ][4] .= ' ' . $class;
		}
	}
	// phpcs:enable

	/**
	 * Enqueue scripts for the plugin
	 */
	public function plugin_admin_scripts(): void {
		$js_data = $this->get_chunk_translations( 'includes/Admin/App/build' );
		if ( empty( $js_data ) ) {
			return;
		}

		$version = $js_data['version'];
		wp_enqueue_style(
			'burst-tailwind',
			plugins_url( '/src/tailwind.generated.css', __FILE__ ),
			[],
			$version
		);

		// @phpstan-ignore-next-line
		burst_wp_enqueue_media();

		// add 'wp-core-data' to the dependencies.
		$js_data['dependencies'][] = 'wp-core-data';

		// Load the main script in the head with high priority.
		wp_enqueue_script(
			'burst-settings',
			plugins_url( 'build/' . $js_data['js_file'], __FILE__ ),
			$js_data['dependencies'],
			$js_data['version'],
			[
				'strategy'  => 'async',
				'in_footer' => false,
			]
		);

		// Add high priority to the script.
		add_filter(
			'script_loader_tag',
			function ( $tag, $handle, $src ) {
				// Unused variable, but required by the function signature.
				unset( $src );
				if ( $handle === 'burst-settings' ) {
					return str_replace( ' src', ' fetchpriority="high" src', $tag );
				}
				return $tag;
			},
			10,
			3
		);

		$path = defined( 'BURST_PRO' ) ? BURST_PATH . 'languages' : false;
		wp_set_script_translations( 'burst-settings', 'burst-statistics', $path );

		wp_localize_script(
			'burst-settings',
			'burst_settings',
			$this->localized_settings( $js_data )
		);
	}

	/**
	 * Get available date ranges for the dashboard.
	 *
	 * @return string[] List of date range slugs.
	 */
	public function get_date_ranges(): array {
		return apply_filters(
			'burst_date_ranges',
			[
				'today',
				'yesterday',
				'last-7-days',
				'last-30-days',
				'last-90-days',
				'last-month',
				'last-year',
				'week-to-date',
				'month-to-date',
				'year-to-date',
				'all-time',
			]
		);
	}

	/**
	 * Extend the localized settings for the dashboard.
	 *
	 * @param array<string, mixed> $data
	 * @return array<string, mixed>
	 */
	public function extend_localized_settings_for_dashboard( array $data ): array {
		$data['menu']   = $this->menu->get();
		$data['fields'] = $this->fields->get();
		return $data;
	}

	/**
	 * AJAX fallback handler for write actions (manage capability).
	 */
	public function rest_api_fallback_do_action(): void {
		if ( ! $this->user_can_manage() ) {
			$this->send_ajax_fallback_forbidden_response();
		}

		$this->rest_api_fallback( 'do_action' );
	}

	/**
	 * AJAX fallback handler for read actions (view capability).
	 */
	public function rest_api_fallback_get_action(): void {
		if ( ! $this->user_can_view() ) {
			$this->send_ajax_fallback_forbidden_response();
		}

		$this->rest_api_fallback( 'get_action' );
	}

	/**
	 * Return a standardized forbidden response for AJAX fallback requests.
	 */
	private function send_ajax_fallback_forbidden_response(): void {
		$response = new \WP_REST_Response(
			[
				'success' => false,
				'message' => 'You do not have permission to perform this action.',
			]
		);

		if ( ob_get_length() ) {
			ob_clean();
		}
		header( 'Content-Type: application/json' );
		echo wp_json_encode( $response );
		exit;
	}

	/**
	 * Determine whether a fallback action belongs to the write/do_action scope.
	 */
	private function is_do_action_fallback_request( string $action ): bool {
		if ( $action === '' ) {
			return false;
		}

		$do_action_fragments = [
			'/options/set',
			'/fields/set',
			'/goals/add',
			'/goals/delete',
			'/goals/set',
			'/goals/add_predefined',
			'/do_action/',
		];

		foreach ( $do_action_fragments as $fragment ) {
			if ( str_contains( $action, $fragment ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * If the rest api is blocked, the code will try an admin ajax call as fall back.
	 */
	public function rest_api_fallback( string $context = '' ): void {
		$response  = [];
		$error     = false;
		$action    = false;
		$do_action = false;
		$data      = [];
		$data_type = false;

		if ( ! $this->user_can_view() ) {
			$error = true;
		}

		// --- Parse GET ---
		// phpcs:ignore
		if ( isset( $_GET['rest_action'] ) ) {
			// phpcs:ignore
			$action = sanitize_text_field( $_GET['rest_action'] );
			if ( str_contains( $action, 'burst/v1/data/ecommerce/' ) ) {
				$data_type = strtolower( str_replace( 'burst/v1/data/ecommerce/', '', $action ) );
			} elseif ( str_contains( $action, 'burst/v1/data/' ) ) {
				$data_type = strtolower( str_replace( 'burst/v1/data/', '', $action ) );
			}
		}

		// --- Collect GET params ---
		// phpcs:ignore
		$get_params = $_GET;
		unset( $get_params['rest_action'] );

		// --- Parse POST body, if present ---
		$request_data = json_decode( file_get_contents( 'php://input' ), true );
		if ( is_array( $request_data ) ) {
			$req_path = isset( $request_data['path'] ) ? sanitize_text_field( $request_data['path'] ) : false;
			if ( $req_path ) {
				// override if provided by POST.
				$action = $req_path;
				if ( ! $data_type && strpos( $action, 'burst/v1/data/' ) !== false ) {
					// Extract data type for /data/* when using POST.
					$data_type = strtolower( str_replace( 'burst/v1/data/', '', $action ) );
				}
			}
			$data = isset( $request_data['data'] ) && is_array( $request_data['data'] ) ? $request_data['data'] : [];

			if ( strpos( $action, 'burst/v1/do_action/' ) !== false ) {
				$do_action = strtolower( str_replace( 'burst/v1/do_action/', '', $action ) );
			}
		}

		// Enforce read/write split for fallback handlers.
		if ( $context !== '' ) {
			$is_do_action = $this->is_do_action_fallback_request( (string) $action );

			if ( ( $context === 'do_action' && ! $is_do_action ) || ( $context === 'get_action' && $is_do_action ) ) {
				$this->send_ajax_fallback_forbidden_response();
			}
		}

		$nonce = $get_params['nonce'] ?? ( $request_data['data']['nonce'] ?? null );
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			$response = new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
			ob_get_clean();
			header( 'Content-Type: application/json' );
			echo wp_json_encode( $response );
			exit;
		}

		// Fallback notice.
		$fallback_already_active = (int) get_option( 'burst_ajax_fallback_active_timestamp', 0 );
		if ( $fallback_already_active === 0 ) {
			update_option( 'burst_ajax_fallback_active_timestamp', time(), false );
		}
		if ( $fallback_already_active > 0 && ( time() - $fallback_already_active ) > 48 * HOUR_IN_SECONDS ) {
			update_option( 'burst_ajax_fallback_active', true, false );
		}

		// Normalize/merge params from GET and POST data.
		$merged = $get_params;
		foreach ( [ 'goal_id', 'type', 'date_start', 'date_end', 'args', 'search', 'filters', 'metrics', 'group_by', 'isOnboarding' ] as $k ) {
			if ( array_key_exists( $k, $data ) ) {
				$merged[ $k ] = $data[ $k ];
			}
		}

		// Convert metrics string -> array.
		if ( isset( $merged['metrics'] ) && is_string( $merged['metrics'] ) ) {
			$merged['metrics'] = explode( ',', $merged['metrics'] );
		}

		// Handle filters slashes (string JSON coming from GET); keep arrays as-is.
		if ( isset( $merged['filters'] ) && is_string( $merged['filters'] ) ) {
			$merged['filters'] = stripslashes( $merged['filters'] );
		}

		// Build WP_REST_Request with merged params.
		$request = new \WP_REST_Request();
		foreach ( [ 'goal_id', 'type', 'nonce', 'date_start', 'date_end', 'args', 'search', 'filters', 'metrics', 'group_by' ] as $arg ) {
			if ( isset( $merged[ $arg ] ) ) {
				$request->set_param( $arg, $merged[ $arg ] );
			}
		}

		// If we detected /data/, make sure 'type' is set from the path.
		if ( $data_type ) {
			$request->set_param( 'type', $data_type );
		}

		if ( ! $error ) {
			if ( str_contains( $action, '/fields/get' ) ) {
				$response = $this->rest_api_fields_get( $request );
			} elseif ( str_contains( $action, '/fields/set' ) ) {
				$response = $this->rest_api_fields_set( $request, $data );
			} elseif ( str_contains( $action, '/options/set' ) ) {
				$response = $this->rest_api_options_set( $request, $data );
			} elseif ( str_contains( $action, '/goals/get' ) ) {
				$response = $this->rest_api_goals_get( $request );
			} elseif ( str_contains( $action, '/goals/add' ) ) {
				$response = $this->rest_api_goals_add( $request, $data );
			} elseif ( str_contains( $action, '/goals/delete' ) ) {
				$response = $this->rest_api_goals_delete( $request, $data );
			} elseif ( str_contains( $action, '/goal_fields/get' ) ) {
				$response = $this->rest_api_goal_fields_get( $request );
			} elseif ( str_contains( $action, '/goals/set' ) ) {
				$response = $this->rest_api_goals_set( $request, $data );
			} elseif ( str_contains( $action, '/posts/' ) ) {
				$response = $this->get_posts( $request, $data );
			} elseif ( str_contains( $action, '/data/' ) ) {
				$response = $this->get_data( $request );
			} elseif ( strpos( $action, '/reports' ) ) {
				$reports  = new Reports();
				$response = $reports->get_reports( $request );
			} elseif ( $do_action ) {
				$req = new \WP_REST_Request();
				$req->set_param( 'action', $do_action );
				$response = $this->do_action( $req, $data );
			} elseif ( strpos( $action, 'burst/v1/get_action/' ) !== false ) {
				$get_action = strtolower( str_replace( 'burst/v1/get_action/', '', $action ) );
				$req        = new \WP_REST_Request();
				$req->set_param( 'action', $get_action );
				$response = $this->get_action( $req, $merged );
			}
		}

		ob_get_clean();
		header( 'Content-Type: application/json' );
		echo wp_json_encode( $response );
		exit;
	}

	/**
	 * Render the settings page
	 */
	public function dashboard(): void {
		if ( ! $this->user_can_view() ) {
			return;
		}
		?>
		<style id="burst-skeleton-styles">
			/* Hide notices in the Burst menu */
			.toplevel_page_burst .notice, .toplevel_page_burst .error {
				display: none;
			}

			/* Skeleton color tokens. Dark values are single-sourced on :root so both
				the .dark class path and the media-query path reuse the same literals. */
			:root {
				--burst-skeleton-dark-page: oklch(0.184 0.015 144.76);
				--burst-skeleton-dark-panel: oklch(0.234 0.0095 144.76);
				--burst-skeleton-dark-pulse: oklch(0.34 0.0147 144.76);
			}

			#burst-statistics {
				--burst-skeleton-panel: rgb(255 255 255);
				--burst-skeleton-pulse: rgb(229 231 235);
			}

			/* Background colors */
			#burst-statistics .bg-white {
				background-color: var(--burst-skeleton-panel);
			}

			#burst-statistics .bg-gray-200 {
				background-color: var(--burst-skeleton-pulse);
			}

			/* Layout */
			#burst-statistics .mx-auto {
				margin-left: auto;
				margin-right: auto;
			}

			#burst-statistics .flex {
				display: flex;
			}

			#burst-statistics .grid {
				display: grid;
			}

			#burst-statistics .grid-cols-12 {
				grid-template-columns: repeat(12, minmax(0, 1fr));
			}

			#burst-statistics .grid-rows-5 {
				grid-template-rows: repeat(5, minmax(0, 1fr));
			}

			#burst-statistics .col-span-6 {
				grid-column: span 6 / span 6;
			}

			#burst-statistics .col-span-3 {
				grid-column: span 3 / span 3;
			}

			#burst-statistics .row-span-2 {
				grid-row: span 2 / span 2;
			}

			#burst-statistics .items-center {
				align-items: center;
			}

			/* Spacing */
			#burst-statistics .gap-5 {
				gap: 1.25rem;
			}

			#burst-statistics .px-5 {
				padding-left: 1.25rem;
				padding-right: 1.25rem;
			}

			#burst-statistics .py-2 {
				padding-top: 0.5rem;
				padding-bottom: 0.5rem;
			}

			#burst-statistics .py-6 {
				padding-top: 1.5rem;
				padding-bottom: 1.5rem;
			}

			#burst-statistics .p-5 {
				padding: 1.25rem;
			}

			#burst-statistics .m-5 {
				margin: 1.25rem;
			}

			#burst-statistics .mb-5 {
				margin-bottom: 1.25rem;
			}

			#burst-statistics .ml-2 {
				margin-left: 0.5rem;
			}

			/* Sizing */
			#burst-statistics .h-6 {
				height: 1.5rem;
			}

			#burst-statistics .h-11 {
				height: 2.75rem;
			}

			#burst-statistics .w-auto {
				width: auto;
			}

			#burst-statistics .w-1\/2 {
				width: 50%;
			}

			#burst-statistics .w-4\/5 {
				width: 80%;
			}

			#burst-statistics .w-5\/6 {
				width: 83.333333%;
			}

			#burst-statistics .w-full {
				width: 100%;
			}

			#burst-statistics .min-h-full {
				min-height: 100%;
			}

			#burst-statistics .max-w-(--breakpoint-2xl) {
				max-width: 1600px;
			}

			/* Effects */
			#burst-statistics .shadow-md {
				box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
			}

			#burst-statistics .rounded-md {
				border-radius: 0.375rem;
			}

			#burst-statistics .rounded-xl {
				border-radius: 0.75rem;
			}

			#burst-statistics .animate-pulse {
				animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
			}

			@keyframes pulse {
				0%, 100% {
					opacity: 1;
				}
				50% {
					opacity: .5;
				}
			}

			#burst-statistics .blur-sm {
				filter: blur(4px);
			}

			/* Borders */
			#burst-statistics .border-b-4 {
				border-bottom-width: 4px;
			}

			#burst-statistics .border-transparent {
				border-color: transparent;
			}

			#burst-statistics .overflow-x-hidden {
				overflow-x: hidden;
			}

			@media not all and (min-width: 640px) {
				#burst-statistics .max-sm\:w-32 {
					width: 8rem;
				}

				#burst-statistics .max-sm\:col-span-12 {
					grid-column: span 12 / span 12;
				}

				#burst-statistics .max-sm\:row-span-1 {
					grid-row: span 1 / span 1;
				}
			}

			/* Dark mode overrides — mirror token values from dark-scope-tokens.css so the
				skeleton renders correctly before Tailwind loads. Utility rules keep using
				--burst-skeleton-panel/pulse; we just swap those to the dark literals here.
				background-color on #burst-statistics itself covers the container bg. */
			#burst-statistics.dark {
				--burst-skeleton-panel: var(--burst-skeleton-dark-panel);
				--burst-skeleton-pulse: var(--burst-skeleton-dark-pulse);
				background-color: var(--burst-skeleton-dark-page);
			}

			/* System dark preference — applies from first paint, independent of JS,
				and also covers the WP admin body bg around the skeleton to prevent flash.
				The JS above can still override via stored preference after the fact. */
			@media (prefers-color-scheme: dark) {
				body.toplevel_page_burst {
					background-color: var(--burst-skeleton-dark-page);
				}

				#burst-statistics {
					--burst-skeleton-panel: var(--burst-skeleton-dark-panel);
					--burst-skeleton-pulse: var(--burst-skeleton-dark-pulse);
					background-color: var(--burst-skeleton-dark-page);
				}
			}
		</style>
		<div id="burst-statistics" class="burst">
			<script>
				// Apply dark class from stored preference or system preference to prevent white flash.
				// Stored value is JSON-stringified by the React app (setLocalStorage) and may be
				// '"light"', '"dark"', or '"system"'. Treat 'system' and missing value as "follow OS".
				(function() {
					var raw = localStorage.getItem( 'burst_theme_preference' );
					var pref = null;
					if ( raw ) {
						try { pref = JSON.parse( raw ); } catch ( e ) { pref = raw; }
					}
					var prefersDark = window.matchMedia && window.matchMedia( '(prefers-color-scheme: dark)' ).matches;
					var isDark = pref === 'dark' || ( ( !pref || pref === 'system' ) && prefersDark );
					if ( isDark ) {
						document.getElementById( 'burst-statistics' ).classList.add( 'dark' );
					}
				})();
			</script>
			<div class="bg-white">
				<div class="mx-auto flex max-w-(--breakpoint-2xl) items-center gap-5 px-5">
					<div class="max-xxs:w-16 max-xxs:h-auto shrink-0">
						<img width="100" src="<?php echo esc_url_raw( BURST_URL ) . 'assets/img/burst-logo.svg'; ?>" alt="Logo Burst" class="h-11 w-auto px-0 py-2">
					</div>
					<div class="flex items-center blur-sm animate-pulse overflow-x-hidden">
						<div class="py-6 px-5 border-b-4 border-transparent"><?php esc_html_e( 'Dashboard', 'burst-statistics' ); ?></div>
						<div class="py-6 px-5 border-b-4 border-transparent ml-2"><?php esc_html_e( 'Statistics', 'burst-statistics' ); ?></div>
						<div class="py-6 px-5 border-b-4 border-transparent ml-2"><?php esc_html_e( 'Settings', 'burst-statistics' ); ?></div>
					</div>
				</div>
			</div>

			<!-- Content Grid -->
			<div class="mx-auto flex max-w-(--breakpoint-2xl)">
				<div class="m-5 grid min-h-full w-full grid-cols-12 grid-rows-5 gap-5">
					<!-- Left Block -->
					<div class="col-span-6 row-span-2 bg-white shadow-md rounded-xl p-5 max-sm:col-span-12 max-sm:row-span-1">
						<div class="h-6 w-1/2 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-4/5 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-full px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-4/5 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-full px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
					</div>

					<!-- Middle Block -->
					<div class="col-span-3 row-span-2 bg-white shadow-md rounded-xl p-5 max-sm:col-span-12 max-sm:row-span-1">
						<div class="h-6 w-1/2 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-4/5 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-full px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-4/5 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-full px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
					</div>

					<!-- Right Block -->
					<div class="col-span-3 row-span-2 bg-white shadow-md rounded-xl p-5 max-sm:col-span-12 max-sm:row-span-1">
						<div class="h-6 w-1/2 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-4/5 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-full px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-4/5 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-full px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
						<div class="h-6 w-5/6 px-5 py-2 bg-gray-200 rounded-md mb-5 animate-pulse"></div>
					</div>
				</div>
			</div>
		</div>
		<div id="burst-adblocker-modal" style="display:none;">
			<div style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:100000;display:flex;align-items:center;justify-content:center;">
				<div style="background:#fff;border-radius:12px;padding:32px;max-width:520px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.15);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
					<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
						<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
						<h2 style="margin:0;font-size:18px;font-weight:600;color:#111827;"><?php esc_html_e( 'Burst Statistics could not load', 'burst-statistics' ); ?></h2>
					</div>
					<p style="margin:0 0 12px;font-size:14px;line-height:1.6;color:#4b5563;">
						<?php esc_html_e( 'It looks like an ad blocker or browser extension is preventing Burst Statistics from loading. Burst Statistics does not display ads, but some ad blockers may block analytics tools.', 'burst-statistics' ); ?>
					</p>
					<p style="margin:0 0 20px;font-size:14px;line-height:1.6;color:#4b5563;">
						<?php esc_html_e( 'Please disable your ad blocker for this site and reload the page.', 'burst-statistics' ); ?>
					</p>
					<div style="display:flex;gap:12px;">
						<button onclick="location.reload();" style="padding:8px 20px;background:#4f46e5;color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;">
							<?php esc_html_e( 'Reload page', 'burst-statistics' ); ?>
						</button>
						<button onclick="document.getElementById('burst-adblocker-modal').style.display='none';" style="padding:8px 20px;background:#f3f4f6;color:#374151;border:1px solid #d1d5db;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;">
							<?php esc_html_e( 'Dismiss', 'burst-statistics' ); ?>
						</button>
					</div>
				</div>
			</div>
		</div>
		<script>
			setTimeout( function() {
				if ( ! window.burstLoaded ) {
					var modal = document.getElementById( 'burst-adblocker-modal' );
					if ( modal ) {
						modal.style.display = 'block';
					}
				}
			}, 2000 );
		</script>
		<?php
	}

	/**
	 * Register REST API routes for the plugin.
	 */
	public function settings_rest_route(): void {
		// for our ajax fallback test, we don't want to register the REST API routes.
		if ( defined( 'BURST_FALLBACK_TEST' ) ) {
			return;
		}

		if ( get_transient( 'burst_running_upgrade' ) ) {
			self::error_log( 'Database installation in progress, delaying REST API response with 2 seconds.' );
			// sleep for 0.5 seconds to allow the database installation to finish.
			usleep( 500000 );
		}

		register_rest_route(
			'burst/v1',
			'options/set',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'rest_api_options_set' ],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'fields/get',
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'rest_api_fields_get' ],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'fields/set',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'rest_api_fields_set' ],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'goals/get',
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'rest_api_goals_get' ],
				'permission_callback' => function () {
					return $this->user_can_view();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'goals/delete',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'rest_api_goals_delete' ],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'goals/add_predefined',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'rest_api_goals_add_predefined' ],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);
		// add_predefined.
		register_rest_route(
			'burst/v1',
			'goals/add',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'rest_api_goals_add' ],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'goals/set',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'rest_api_goals_set' ],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'data/ecommerce/(?P<type>[a-z\_\-]+)',
			[
				'methods'             => 'GET',
				'callback'            => function ( \WP_REST_Request $request ) {
					$request->set_param( 'is_ecommerce', true );
					return $this->get_data( $request );
				},
				'permission_callback' => function () {
					return $this->user_can_view_sales();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'data/(?P<type>[a-z\_\-]+)',
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_data' ],
				'permission_callback' => function () {
					return $this->user_can_view();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'do_action/(?P<action>[a-z\_\-]+)',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'do_action' ],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'get_action/(?P<action>[a-z\_\-]+)',
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_action' ],
				'permission_callback' => function () {
					return $this->user_can_view();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'get_action/ecommerce/(?P<action>[a-z\_\-]+)',
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_action' ],
				'permission_callback' => function () {
					return $this->user_can_view_sales();
				},
			]
		);

		register_rest_route(
			'burst/v1',
			'/posts/',
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_posts' ],
				'args'                => [
					'search_input' => [
						'required'          => false,
						'sanitize_callback' => 'sanitize_title',
					],
				],
				'permission_callback' => function () {
					return $this->user_can_manage();
				},
			]
		);
	}


	/**
	 * Perform a specific action based on the provided request.
	 *
	 * @param \WP_REST_Request $request //The REST API request object.
	 * @param array            $ajax_data //Optional AJAX data to process.
	 * @return \WP_REST_Response //The response object or error.
	 */
	public function do_action( \WP_REST_Request $request, array $ajax_data = [] ): \WP_REST_Response {
		// ← was user_can_view
		if ( ! $this->user_can_manage() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}

		$action = sanitize_title( $request->get_param( 'action' ) );
		$data   = empty( $ajax_data ) ? $request->get_params() : $ajax_data;
		$nonce  = $data['nonce'];
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}

		$data = $data['action_data'];
		if ( empty( $ajax_data ) ) {
			$this->remove_fallback_notice();
		}
		switch ( $action ) {
			case 'plugin_actions':
				$data = $this->plugin_actions( $request, $data );
				break;
			case 'fix_task':
				$task_id   = $data['task_id'];
				$task      = \Burst\burst_loader()->admin->tasks->get_task_by_id( $task_id );
				$option_id = sanitize_text_field( $task['fix'] );
				$task_id   = sanitize_text_field( $task['id'] );
				// should start with burst_ .
				if ( str_starts_with( $option_id, 'burst_option_' ) ) {
					$burst_option_id = str_replace( 'burst_option_', '', $option_id );
					$this->update_option( $burst_option_id, true );
				} elseif ( str_starts_with( $option_id, 'burst_' ) ) {
					update_option( $option_id, true );
					wp_schedule_single_event( time(), 'burst_scheduled_task_fix_' . $task_id );
				}

				\Burst\burst_loader()->admin->tasks->dismiss_task( $task_id );
				break;
			case 'dismiss_task':
				if ( isset( $data['id'] ) ) {
					$id = sanitize_title( $data['id'] );
					\Burst\burst_loader()->admin->tasks->dismiss_task( $id );
				}
				break;
			default:
				$data = is_array( $data ) ? $data : [];
				$data = apply_filters( 'burst_do_action', [], $action, $data );
		}

		if ( ob_get_length() ) {
			ob_clean();
		}

		return $this->create_rest_response( $data );
	}

	/**
	 * Perform a read-only action based on the provided GET request.
	 * Only actions requiring burst_viewer capability should be handled here.
	 *
	 * @param \WP_REST_Request $request   The REST API request object.
	 * @param array            $ajax_data Optional AJAX data (used by rest_api_fallback).
	 */
	public function get_action( \WP_REST_Request $request, array $ajax_data = [] ): \WP_REST_Response {
		if ( ! $this->user_can_view() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}

		$action = sanitize_title( $request->get_param( 'action' ) );
		$data   = empty( $ajax_data ) ? $request->get_params() : $ajax_data;
		$nonce  = $data['nonce'] ?? '';

		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}

		if ( empty( $ajax_data ) ) {
			$this->remove_fallback_notice();
		}

		switch ( $action ) {
			case 'tasks':
				$data = \Burst\burst_loader()->admin->tasks->get();
				break;
			case 'tracking':
				$data = Endpoint::get_tracking_status_and_time();
				break;
			case 'get_article_data':
				$data = $this->get_articles();
				break;
			case 'get_filter_options':
				$data_type = isset( $data['data_type'] ) ? sanitize_title( $data['data_type'] ) : '';
				$search    = isset( $data['search'] ) ? sanitize_text_field( $data['search'] ) : '';
				$data      = $this->get_filter_options( $data_type, $search );
				break;
			case 'otherpluginsdata':
				$data = $this->other_plugins_data();
				break;
			default:
				$data = is_array( $data ) ? $data : [];
				$data = apply_filters( 'burst_get_action', [], $action, $data );
		}

		if ( ob_get_length() ) {
			ob_clean();
		}

		return $this->create_rest_response( $data );
	}

	/**
	 * Get advanced filter options.
	 *
	 * @param string $data_type The specific data type to return (devices, browsers, platforms, countries, pages, referrers, campaigns).
	 * @param string $search The search string, optional.
	 * @return array the filter options.
	 */
	private function get_filter_options( string $data_type, string $search = '' ): array {
		if ( ! $this->user_can_view() ) {
			return [];
		}

		global $wpdb;
		$valid_types = [ 'hosts', 'devices', 'browsers', 'platforms', 'countries', 'states', 'continents', 'cities', 'pages', 'referrers', 'campaigns', 'sources', 'mediums', 'contents', 'terms' ];

		// Return invalid data type error.
		if ( empty( $data_type ) || ! in_array( $data_type, $valid_types, true ) ) {
			return [
				'success' => false,
				'message' => 'Invalid data type',
			];
		}
		$where_queries = [];
		$search        = sanitize_text_field( $search );
		if ( strlen( $search ) > 0 ) {
			$like          = '%' . $wpdb->esc_like( $search ) . '%';
			$where_queries = [
				'pages' => $wpdb->prepare( 'WHERE page_url LIKE %s ', $like ),
			];
		}

		$where = $where_queries[ $data_type ] ?? '';
		// Define data type queries.
		$queries = [
			'devices'   => "SELECT MIN(ID) as ID, name FROM {$wpdb->prefix}burst_devices GROUP BY name ORDER BY name ASC",
			'browsers'  => "SELECT MIN(ID) as ID, name FROM {$wpdb->prefix}burst_browsers GROUP BY name ORDER BY name ASC",
			'platforms' => "SELECT MIN(ID) as ID, name FROM {$wpdb->prefix}burst_platforms GROUP BY name ORDER BY name ASC",
			'states'    => "SELECT DISTINCT state AS name FROM {$wpdb->prefix}burst_locations ORDER BY name ASC",
			'cities'    => "SELECT DISTINCT city AS name FROM {$wpdb->prefix}burst_locations ORDER BY name ASC",
			'pages'     => "SELECT page_url as name FROM {$wpdb->prefix}burst_statistics $where GROUP BY page_url HAVING COUNT(*) > 1 ORDER BY COUNT(*) DESC limit 1000",
			'campaigns' => "SELECT DISTINCT campaign AS name FROM {$wpdb->prefix}burst_campaigns ORDER BY name ASC",
			'sources'   => "SELECT DISTINCT source AS name FROM {$wpdb->prefix}burst_campaigns ORDER BY name ASC",
			'mediums'   => "SELECT DISTINCT medium AS name FROM {$wpdb->prefix}burst_campaigns ORDER BY name ASC",
			'contents'  => "SELECT DISTINCT content AS name FROM {$wpdb->prefix}burst_campaigns ORDER BY name ASC",
			'terms'     => "SELECT DISTINCT term AS name FROM {$wpdb->prefix}burst_campaigns ORDER BY name ASC",
			'hosts'     => "SELECT DISTINCT host as name FROM {$wpdb->prefix}burst_sessions ORDER BY name ASC",
		];

		// Get raw data based on data type.
		if ( $data_type === 'countries' ) {
			$raw_data = apply_filters( 'burst_countries', [] );
			// filter out localhost.
			unset( $raw_data['LO'] );
			$raw_data = array_map(
				fn( $key, $value ) => [
					'ID'   => $key,
					'name' => $value,
				],
				array_keys( $raw_data ),
				$raw_data
			);
		} elseif ( $data_type === 'continents' ) {
			$raw_data = apply_filters( 'burst_continents', [] );
			$raw_data = array_map(
				function ( $key, $value ) {
					return [
						'ID'   => $key,
						'name' => $value,
					];
				},
				array_keys( $raw_data ),
				array_values( $raw_data )
			);
		} elseif ( $data_type === 'referrers' ) {
			$raw_data = $this->get_referrer_options( $search );
			$raw_data = array_map(
				fn( $value ) => [
					'ID'   => $value['name'],
					'name' => $value['name'],
				],
				array_values( $raw_data )
			);
		} else {
			$cache_key   = $data_type;
			$cache_group = 'burst';
			$raw_data    = wp_cache_get( $cache_key, $cache_group );

			if ( false === $raw_data ) {
				// Cache miss - get from database.
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $queries are predefined and safe.
				$raw_data = $wpdb->get_results( $queries[ $data_type ], ARRAY_A );

				// Store in cache.
				wp_cache_set( $cache_key, $raw_data, $cache_group );
			}
			$raw_data = array_filter(
				$raw_data,
				function ( $item ) {
					foreach ( [ 'name', 'id', 'key' ] as $field ) {
						if ( isset( $item[ $field ] ) ) {
							$value = trim( (string) $item[ $field ] );
							if ( $value === '' || $value === '-1' || $value === 'null' ) {
								return false;
							}
						}
					}

					return true;
				}
			);

			if ( $data_type === 'devices' ) {
				$raw_data = array_map(
					function ( $item ) {
						$item['key'] = $item['name'];
						// get nicename for device.
						switch ( $item['name'] ) {
							case 'desktop':
								$item['name'] = __( 'Desktop', 'burst-statistics' );
								break;
							case 'mobile':
								$item['name'] = __( 'Mobile', 'burst-statistics' );
								break;
							case 'tablet':
								$item['name'] = __( 'Tablet', 'burst-statistics' );
								break;
							default:
								$item['name'] = __( 'Other', 'burst-statistics' );
								break;
						}

						return $item;
					},
					$raw_data
				);
			}
		}

		return [
			'success' => true,
			'data'    => [
				$data_type => $raw_data,
			],
		];
	}

	/**
	 * Initialize weekly cleanup cron jobs
	 * Schedules both referrer and browser cleanup if not already scheduled
	 */
	public function init_cleanup(): void {
		if ( ! wp_next_scheduled( 'burst_weekly_clear_referrers_cron' ) ) {
			wp_schedule_single_event( time() + 60, 'burst_weekly_clear_referrers_cron' );
		}

		if ( ! wp_next_scheduled( 'burst_weekly_clear_spam_browsers_cron' ) ) {
			wp_schedule_single_event( time() + 120, 'burst_weekly_clear_spam_browsers_cron' );
		}
	}
	/**
	 * On a weekly basis, clear the referrers table.
	 *
	 * @hooked burst_weekly
	 */
	public function weekly_clear_referrers_table(): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}

		if ( ! $this->table_exists( 'burst_referrers' ) ) {
			return;
		}

		global $wpdb;
		$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}burst_referrers" );
	}

	/**
	 * Weekly cleanup of one spam/invalid browser from the database
	 * Only removes browsers with limited statistics to keep queries fast
	 */
	public function weekly_clear_spam_browsers(): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}

		global $wpdb;

		$browsers = $wpdb->get_results(
			"SELECT ID, name FROM {$wpdb->prefix}burst_browsers",
			ARRAY_A
		);

		if ( empty( $browsers ) ) {
			return;
		}

		$parser = new UserAgentParser();
		// Find the first invalid browser with limited statistics.
		foreach ( $browsers as $browser ) {
			$browser_name = $browser['name'];
			$browser_id   = (int) $browser['ID'];

			// Skip if browser name is valid.
			if ( ! $parser->is_invalid_browser_name( $browser_name ) ) {
				continue;
			}

			// Check how many statistics use this browser.
			$count = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$wpdb->prefix}burst_statistics WHERE browser_id = %d",
					$browser_id
				)
			);

			// Only clean up browsers with max 50 statistics to keep it fast.
			if ( $count > 50 ) {
				continue;
			}

			// Delete statistics for this browser.
			$deleted_stats = $wpdb->query(
				$wpdb->prepare(
					"DELETE FROM {$wpdb->prefix}burst_statistics WHERE browser_id = %d",
					$browser_id
				)
			);

			// Delete the browser entry.
			$wpdb->query(
				$wpdb->prepare(
					"DELETE FROM {$wpdb->prefix}burst_browsers WHERE ID = %d",
					$browser_id
				)
			);

			self::error_log(
				sprintf(
					'Burst: Weekly cleanup removed spam browser "%s" (ID: %d) and %d related statistics.',
					$browser_name,
					$browser_id,
					$deleted_stats
				)
			);

			return;
		}

		self::error_log( 'Burst: No spam browsers found with limited statistics during weekly cleanup.' );
	}

	/**
	 * Populate the referrers table from the sessions table if it is empty.
	 * Used both by the filter UI (lazy populate on first read) and by data sharing
	 * (which otherwise sees an empty table right after the weekly TRUNCATE).
	 */
	public function maybe_populate_referrers_table(): void {
		global $wpdb;

		if ( ! $this->table_exists( 'burst_referrers' ) ) {
			return;
		}

		$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}burst_referrers" );
		if ( $count > 0 ) {
			return;
		}

		$wpdb->query(
			"INSERT IGNORE INTO {$wpdb->prefix}burst_referrers (name)
             SELECT TRIM(TRAILING '/' FROM domain) AS domain
             FROM (
               SELECT
                 LOWER(SUBSTRING_INDEX(referrer, '/', 1)) AS domain
               FROM {$wpdb->prefix}burst_sessions
               WHERE referrer IS NOT NULL
                 AND referrer != ''
                 AND referrer NOT LIKE '/%'
             ) AS derived
             WHERE domain != ''
               AND SUBSTRING_INDEX(domain, ':', 1) NOT REGEXP '^[0-9]{1,3}(\\.[0-9]{1,3}){3}$'
             GROUP BY domain
             ORDER BY COUNT(*) DESC
             LIMIT 2000;"
		);
	}

	/**
	 * Get referrer options for the advanced filter. The table is cleared weekly, to ensure up to date data.
	 *
	 * @param string $search the optional search string.
	 * @return array the referrer options.
	 */
	private function get_referrer_options( string $search = '' ): array {
		global $wpdb;

		$this->maybe_populate_referrers_table();

		$search = sanitize_text_field( $search );
		$like   = '%' . $wpdb->esc_like( $search ) . '%';
		$where  = strlen( $search ) > 0 ? $wpdb->prepare( 'WHERE name LIKE %s ', $like ) : '';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where is prepared above.
		return $wpdb->get_results( "SELECT TRIM(TRAILING '/' FROM name) as name FROM {$wpdb->prefix}burst_referrers $where ORDER BY ID ASC limit 1000", ARRAY_A );
	}

	/**
	 * Process plugin installation or activation actions based on the provided request.
	 *
	 * @param \WP_REST_Request      $request The REST API request object.
	 * @param array<string, string> $data    Associative array with 'slug' and 'pluginAction'.
	 * @return array<string, mixed>     Plugin data for the affected plugin.
	 */
	public function plugin_actions( \WP_REST_Request $request, array $data ): array {
		if ( ! $this->user_can_manage() ) {
			return [];
		}
		$slug      = sanitize_title( $data['slug'] );
		$action    = sanitize_title( $data['action'] );
		$installer = new Installer( 'burst-statistics', $slug );
		if ( $action === 'download' ) {
			$installer->download_plugin();
		} elseif ( $action === 'activate' ) {
			$installer->activate_plugin();
		}
		return $this->other_plugins_data( $slug );
	}

	/**
	 * Get plugin data for the "Other Plugins" section.
	 *
	 * @param string $slug Optional plugin slug to retrieve a single plugin entry.
	 * @return array<string, mixed>|array<int, array<string, mixed>> A single plugin data array if $slug is provided
	 *                       and matches, or a list of plugin data arrays otherwise.
	 */
	public function other_plugins_data( string $slug = '' ): array {
		if ( ! $this->user_can_view() ) {
			return [];
		}

		$installer = new Installer( 'burst-statistics' );
		if ( empty( $slug ) ) {
			return $installer->get_plugins( true );
		} else {
			return $installer->get_plugin( $slug );
		}
	}

	/**
	 * Process common REST API request patterns
	 *
	 * @param \WP_REST_Request $request The REST request object.
	 * @param string           $permission_level 'view' or 'manage'.
	 * @return array<string, mixed> Processed request data or error.
	 */
	private function process_rest_request( \WP_REST_Request $request, string $permission_level = 'view' ): array {
		$can_access = $permission_level === 'manage' ? $this->user_can_manage() : $this->user_can_view();
		if ( ! $can_access ) {
			return [
				'success' => false,
				'type'    => 'error',
				'message' => 'Invalid permissions',
			];
		}

		$nonce = $request->get_param( 'nonce' );
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return [
				'success' => false,
				'type'    => 'error',
				'message' => 'Invalid nonce',
			];
		}

		return [
			'success' => true,
			'type'    => sanitize_title( $request->get_param( 'type' ) ),
		];
	}

	/**
	 * Process and sanitize request arguments for data requests.
	 *
	 * @param \WP_REST_Request $request The REST request object.
	 * @param string           $type The data type being requested.
	 * @param array            $base_args Base arguments to include in the result.
	 * @return array<string, mixed> Sanitized arguments from the request.
	 */
	public function normalize_values( \WP_REST_Request $request, string $type, array $base_args = [] ): array {
		$available_args = $this->get_data_available_args( $type );

		foreach ( $available_args as $arg ) {
			if ( $request->get_param( $arg ) ) {
				$base_args[ $arg ] = $this->normalize_value( $arg, $request->get_param( $arg ) );
			}
		}
		return $base_args;
	}

	/**
	 * Sanitize argument based on its type.
	 *
	 * @param string $arg The argument name.
	 * @param mixed  $value The value to sanitize.
	 * @return mixed Sanitized value.
	 */
	// phpcs:disable
	public function normalize_value( string $arg, $value ) {
		// phpcs:enable

		switch ( $arg ) {
			case 'filters':
				return array_filter(
					$this->ensure_array_if_applicable( $value ),
					static function ( $item ) {
						// Keep values that are not false and not empty string, OR are exactly zero (int or string).
						if ( $item === 0 || $item === '0' ) {
							return true;
						}
						return $item !== false && $item !== '';
					}
				);
			case 'group_by':
			case 'order_by':
			case 'metrics':
				$processed_value = $this->ensure_array_if_applicable( $value );
				if ( is_array( $processed_value ) ) {
					return $processed_value;
				} else {
					return [ $processed_value ];
				}
			case 'goal_id':
				return absint( $value );
			case 'date_start':
				return $this->normalize_date( $value . ' 00:00:00' );
			case 'date_end':
				return $this->normalize_date( $value . ' 23:59:59' );
			default:
				// Allow other plugins/extensions to handle custom argument sanitization.
				// Apply smart transformation for consistent filter interface.
				$processed_value = $this->ensure_array_if_applicable( $value );
				$sanitized_value = apply_filters( 'burst_sanitize_arg', null, $arg, $processed_value );
				if ( $sanitized_value !== null ) {
					return $sanitized_value;
				}
				return $value;
		}
	}

	/**
	 * Get data from the REST API.
	 */
	public function get_data( \WP_REST_Request $request ): \WP_REST_Response {
		// Process common request patterns.
		$processed = $this->process_rest_request( $request );

		if ( $processed['success'] === false ) {
			return $this->create_rest_response( $processed, 403 );
		}

		$type = $processed['type'];
		$args = apply_filters( 'burst_get_data_request_args', $this->normalize_values( $request, $type ), $type, $request );

		switch ( $type ) {
			case 'live-visitors':
				$is_onboarding = $request->get_param( 'isOnboarding' );
				if ( $is_onboarding ) {
					wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'burst_clear_test_visit' );
				}
				$count = \Burst\burst_loader()->admin->statistics->get_live_visitors_data();
				$data  = [ 'visitors' => $count ];
				break;
			case 'live-traffic':
				$data = \Burst\burst_loader()->admin->statistics->get_live_traffic_data();
				break;
			case 'today':
				$data = \Burst\burst_loader()->admin->statistics->get_today_data( $args );
				break;
			case 'goals':
				$goal_statistics = new Goal_Statistics();
				$data            = $goal_statistics->get_goals_data( $args );
				break;
			case 'live-goals':
				$goal_statistics = new Goal_Statistics();
				$goals_count     = $goal_statistics->get_live_goals_count( $args );
				$data            = [ 'goals_count' => $goals_count ];
				break;
			case 'insights':
				$data = \Burst\burst_loader()->admin->statistics->get_insights_data( $args );
				break;
			case 'compare':
				if ( isset( $args['filters']['goal_id'] ) ) {
					$data = \Burst\burst_loader()->admin->statistics->get_compare_goals_data( $args );
				} else {
					$data = \Burst\burst_loader()->admin->statistics->get_compare_data( $args );
				}
				break;
			case 'devicestitleandvalue':
				$data = \Burst\burst_loader()->admin->statistics->get_devices_title_and_value_data( $args );
				break;
			case 'devicessubtitle':
				$data = \Burst\burst_loader()->admin->statistics->get_devices_subtitle_data( $args );
				break;
			case 'datatable':
				$data = \Burst\burst_loader()->admin->statistics->get_datatables_data( $args );
				break;
			default:
				$data = apply_filters( 'burst_get_data', [], $type, $args, $request );
		}

		return $this->create_rest_response( $data );
	}

	/**
	 * Create standardized REST response
	 *
	 * @param array $data Response data.
	 * @param int   $status HTTP status code.
	 * @return \WP_REST_Response The REST response object.
	 */
	private function create_rest_response( array $data, int $status = 200 ): \WP_REST_Response {
		if ( ob_get_length() ) {
			ob_clean();
		}

		if ( ( isset( $data['success'] ) && ! $data['success'] ) || $status !== 200 ) {
			unset( $data['success'] );

			if ( isset( $data['message'] ) ) {
				return new \WP_REST_Response(
					[
						'message' => $data['message'],
						'success' => false,
					],
					$status
				);
			} else {
				return new \WP_REST_Response(
					[
						'data'    => $data,
						'success' => false,
					],
					$status
				);
			}
		}

		return new \WP_REST_Response(
			[
				'data'            => $data,
				'request_success' => true,
				'success'         => true,
			],
			$status
		);
	}

	/**
	 * Save options through the rest api
	 */
	public function rest_api_options_set( \WP_REST_Request $request, array $ajax_data = [] ): \WP_REST_Response {
		if ( ! $this->user_can_manage() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}
		$data = empty( $ajax_data ) ? $request->get_json_params() : $ajax_data;

		// get the nonce.
		$nonce   = $data['nonce'];
		$options = $data['option'];
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'Invalid nonce.',
				]
			);
		}

		// sanitize the options.
		$option = sanitize_title( $options['option'] );
		$value  = sanitize_text_field( $options['value'] );

		// option should be prefixed with burst_, if not add it.
		if ( strpos( $option, 'burst_' ) !== 0 ) {
			$option = 'burst_' . $option;
		}
		update_option( $option, $value );
		if ( ob_get_length() ) {
			ob_clean();
		}

		return new \WP_REST_Response(
			[
				'status'          => 'success',
				'request_success' => true,
			],
			200
		);
	}

	/**
	 * Save multiple Burst settings fields via REST API
	 */
	public function rest_api_fields_set( \WP_REST_Request $request, array $ajax_data = [] ): \WP_REST_Response {
		if ( ! $this->user_can_manage() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}

		// Get and validate data.
		try {
			$data = empty( $ajax_data ) ? $request->get_json_params() : $ajax_data;
			if ( ! isset( $data['nonce'], $data['fields'] ) || ! is_array( $data['fields'] ) ) {
				return new \WP_REST_Response(
					[
						'success' => false,
						'message' => 'Invalid request format.',
					]
				);
			}

			if ( ! $this->verify_nonce( $data['nonce'], 'burst_nonce' ) ) {
				return new \WP_REST_Response(
					[
						'success' => false,
						'message' => 'Invalid nonce.',
					]
				);
			}

			if ( empty( $ajax_data ) ) {
				$this->remove_fallback_notice();
			}

			// Get config fields and index them by ID for faster lookup.
			$config_fields = array_column( $this->fields->get( false ), null, 'id' );

			// Get current options.
			$options = get_option( 'burst_options_settings', [] );

			// Handle case where options are stored as JSON string.
			if ( is_string( $options ) ) {
				$decoded = json_decode( $options, true );
				if ( json_last_error() === JSON_ERROR_NONE ) {
					$options = $decoded;
				} else {
					$options = [];
				}
			}

			// Ensure options is an array.
			if ( ! is_array( $options ) ) {
				$options = [];
			}

			// Track which fields were actually updated.
			$updated_fields = [];
			foreach ( $data['fields'] as $field_id => $value ) {
				// Validate field exists in config.
				if ( ! isset( $config_fields[ $field_id ] ) ) {
					continue;
				}

				$config_field = $config_fields[ $field_id ];
				$type         = $this->sanitize_field_type( $config_field['type'] );
				$prev_value   = $options[ $field_id ] ?? false;

				// Allow modification before save.
				// deprecated.
				do_action( 'burst_before_save_option', $field_id, $value, $prev_value, $type );
				// Sanitize the value.
				$sanitized_value = $this->sanitize_field( $value, $type );
				do_action( 'burst_before_save_field', $field_id, $sanitized_value, $prev_value, $type );

				// Allow filtering of sanitized value.
				$sanitized_value = apply_filters(
					'burst_fieldvalue',
					$sanitized_value,
					$field_id,
					$type
				);

				// error log the sanitized value.
				$options[ $field_id ]        = $sanitized_value;
				$updated_fields[ $field_id ] = $sanitized_value;
			}

			// Only save if we have updates.
			if ( ! empty( $updated_fields ) ) {
				$updated = update_option( 'burst_options_settings', $options );

				// Process after-save actions only for updated fields.
				foreach ( $updated_fields as $field_id => $value ) {

					$type       = $config_fields[ $field_id ]['type'];
					$prev_value = $options[ $field_id ] ?? false;
					do_action( 'burst_after_save_field', $field_id, $value, $prev_value, $type );
				}
				do_action( 'burst_after_saved_fields', $updated_fields );
			}

			// Return success response.
			return new \WP_REST_Response(
				[
					'success'         => true,
					'request_success' => true,
					'message'         => ! empty( $updated_fields )
						? __( 'Settings saved successfully', 'burst-statistics' )
						: __( 'No changes were made', 'burst-statistics' ),
				],
				200
			);

		} catch ( \Exception $e ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $e->getMessage(),
				]
			);
		}
	}

	/**
	 * Get the rest api fields
	 */
	public function rest_api_fields_get( \WP_REST_Request $request ): \WP_REST_Response {

		if ( ! $this->user_can_view() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}

		$nonce = $request->get_param( 'nonce' );
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}

		$output = [];
		$fields = $this->fields->get();
		$menu   = $this->menu->get();
		foreach ( $fields as $index => $field ) {
			$fields[ $index ] = $field;
		}

		// remove empty menu items.
		foreach ( $menu as $key => $menu_group ) {
			$menu_group['menu_items'] = $this->drop_empty_menu_items( $menu_group['menu_items'], $fields );
			$menu[ $key ]             = $menu_group;
		}
		$output['fields']          = $fields;
		$output['request_success'] = true;
		$output['progress']        = \Burst\burst_loader()->admin->tasks->get();

		$output = apply_filters( 'burst_rest_api_fields_get', $output );
		if ( ob_get_length() ) {
			ob_clean();
		}

		return new \WP_REST_Response( $output, 200 );
	}

	/**
	 * Get goals for the react dashboard
	 */
	public function rest_api_goals_get( \WP_REST_Request $request ): \WP_REST_Response {
		if ( ! $this->user_can_view() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}

		$nonce = $request->get_param( 'nonce' );
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}

		$goal_object = new Goals();
		$goals       = $goal_object->get_goals();

		$goals = apply_filters( 'burst_rest_api_goals_get', $goals );

		$predefined_goals = $goal_object->get_predefined_goals();
		if ( ob_get_length() ) {
			ob_clean();
		}

		return new \WP_REST_Response(
			[
				'request_success' => true,
				'goals'           => $goals,
				'predefinedGoals' => $predefined_goals,
				'goalFields'      => $this->fields->get_goal_fields(),
			],
			200
		);
	}

	/**
	 * Get the rest api fields
	 */
	public function rest_api_goal_fields_get( \WP_REST_Request $request ): \WP_REST_Response {
		if ( ! $this->user_can_manage() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}

		$nonce = $request->get_param( 'nonce' );
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}

		$goals = apply_filters( 'burst_rest_api_goals_get', ( new Goals() )->get_goals() );
		if ( ob_get_length() ) {
			ob_clean();
		}

		$response = new \WP_REST_Response(
			[
				'request_success' => true,
				'goals'           => $goals,
			]
		);
		$response->set_status( 200 );

		return $response;
	}


	/**
	 * Save goals via REST API
	 */
	public function rest_api_goals_set( \WP_REST_Request $request, array $ajax_data = [] ): \WP_REST_Response {
		if ( ! $this->user_can_manage() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}
		$data  = empty( $ajax_data ) ? $request->get_json_params() : $ajax_data;
		$nonce = $data['nonce'];
		$goals = $data['goals'];
		// get the nonce.
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}

		foreach ( $goals as $index => $goal_data ) {
			$id = (int) $goal_data['id'];
			unset( $goal_data['id'] );

			$goal = new Goal( $id );
			foreach ( $goal_data as $name => $value ) {
				if ( property_exists( $goal, $name ) ) {
					$goal->{$name} = $value;
				}
			}
			$goal->save();

		}
		// ensure bundled script update.
		do_action( 'burst_after_updated_goals' );

		if ( ob_get_length() ) {
			ob_clean();
		}
		$response = new \WP_REST_Response(
			[
				'request_success' => true,
			]
		);
		$response->set_status( 200 );

		return $response;
	}

	/**
	 * Delete a goal via REST API
	 */
	public function rest_api_goals_delete( \WP_REST_Request $request, array $ajax_data = [] ): \WP_REST_Response {
		if ( ! $this->user_can_manage() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}
		$data  = empty( $ajax_data ) ? $request->get_json_params() : $ajax_data;
		$nonce = $data['nonce'];
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}
		$id = $data['id'];

		$goal    = new Goal( $id );
		$deleted = $goal->delete();

		// get resulting goals, in case the last one was deleted, and a new one was created.
		// ensure at least one goal.
		$goals = ( new Goals() )->get_goals();

		// ensure bundled js file updates.
		do_action( 'burst_after_updated_goals' );

		// if not null return true.
		$response_data = [
			'deleted'         => $deleted,
			'request_success' => true,
		];
		if ( ob_get_length() ) {
			ob_clean();
		}
		$response = new \WP_REST_Response( $response_data );
		$response->set_status( 200 );

		return $response;
	}

	/**
	 * Add predefined goals through REST API
	 *
	 * @param \WP_REST_Request $request The REST API request object.
	 * @param array            $ajax_data Optional AJAX data to process.
	 * @return \WP_REST_Response The response object or error.
	 */
	public function rest_api_goals_add_predefined( \WP_REST_Request $request, array $ajax_data = [] ): \WP_REST_Response {
		if ( ! $this->user_can_manage() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}
		$data  = empty( $ajax_data ) ? $request->get_json_params() : $ajax_data;
		$nonce = $data['nonce'];
		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}
		$id = $data['id'];

		$goal    = new Goal();
		$goal_id = $goal->add_predefined( $id );

		if ( ob_get_length() ) {
			ob_clean();
		}

		$goal = [];
		if ( $goal_id > 0 ) {
			$goal = new Goal( $goal_id );
		}

		$response = new \WP_REST_Response(
			[
				'request_success' => true,
				'goal'            => $goal,
			]
		);
		$response->set_status( 200 );
		return $response;
	}

	/**
	 * Add a new goal via REST API
	 *
	 * @param \WP_REST_Request $request The REST API request object.
	 * @param array            $ajax_data Optional AJAX data to process.
	 * @return \WP_REST_Response $response
	 */
	public function rest_api_goals_add( \WP_REST_Request $request, array $ajax_data = [] ): \WP_REST_Response {
		if ( ! $this->user_can_manage() ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => 'You do not have permission to perform this action.',
				]
			);
		}
		$goal = empty( $ajax_data ) ? $request->get_json_params() : $ajax_data;

		if ( ! $this->verify_nonce( $goal['nonce'], 'burst_nonce' ) ) {
			return new \WP_REST_Response(
				[
					'success' => false,
					'message' => $this->nonce_expired_feedback,
				]
			);
		}

		$goal = new Goal();
		$goal->save();

		// ensure bundled js file updates.
		do_action( 'burst_after_updated_goals' );

		if ( ob_get_length() ) {
			ob_clean();
		}
		$response = new \WP_REST_Response(
			[
				'request_success' => true,
				'goal'            => $goal,
			]
		);
		$response->set_status( 200 );

		return $response;
	}

	/**
	 * Get the menu for the settings page in Burst
	 *
	 * @param \WP_REST_Request $request The REST API request object.
	 * @return array<int, array<string, mixed>> List of field definitions.
	 */
	public function rest_api_menu( \WP_REST_Request $request ): array {
		// Unused parameter, but required by the method signature.
		unset( $request );
		if ( ! $this->user_can_manage() ) {
			return [];
		}
		if ( ob_get_length() ) {
			ob_clean();
		}

		return $this->fields->get();
	}
	/**
	 * Removes menu items that have no associated fields from a nested menu structure.
	 *
	 * @param array<int, array<string, mixed>> $menu_items Array of menu items to filter.
	 * @param array<int, array{menu_id: int}>  $fields Array of fields referencing menu items.
	 * @return array<int, array<string, mixed>> Filtered array of menu items with only those linked to fields.
	 */
	public function drop_empty_menu_items( array $menu_items, array $fields ): array {
		if ( ! $this->user_can_manage() ) {
			return $menu_items;
		}
		$new_menu_items = $menu_items;
		foreach ( $menu_items as $key => $menu_item ) {
			$search_result = in_array( $menu_item['id'], array_column( $fields, 'menu_id' ), true );
			if ( $search_result === false ) {
				unset( $new_menu_items[ $key ] );
				// reset array keys to prevent issues with react.
				$new_menu_items = array_values( $new_menu_items );
			} elseif ( isset( $menu_item['menu_items'] ) ) {
				$new_menu_items[ $key ]['menu_items'] = $this->drop_empty_menu_items( $menu_item['menu_items'], $fields );
			}
		}

		return $new_menu_items;
	}


	/**
	 * Get raw posts array
	 *
	 * @return array<int, array<string, mixed>>
	 *         Returns a list of plugin arrays.
	 */
	private function get_articles(): array {
		$json_path = __DIR__ . '/posts.json';
		// if the file is over one month old, delete it, so we can download a new one.
		if ( file_exists( $json_path ) && ( time() - filemtime( $json_path ) > MONTH_IN_SECONDS ) ) {
			wp_delete_file( $json_path );
		}

		if ( ! file_exists( $json_path ) ) {
			$this->download_articles_json_file();
		}
		if ( ! file_exists( $json_path ) ) {
			$json_path = __DIR__ . '/posts-fallback.json';
		}

		// phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$json = file_get_contents( $json_path );
		// decode the json file.
		$decoded = json_decode( $json, true );
		if ( ! is_array( $decoded ) ) {
			return [];
		}

		// Shuffle array and take 6 random entries.
		shuffle( $decoded );
		return array_slice( $decoded, 0, 6 );
	}

	/**
	 * Get the posts.json file from the remote server.
	 */
	private function download_articles_json_file(): void {
		$remote_json = 'https://burst.ams3.cdn.digitaloceanspaces.com/posts/posts.json';
		$response    = wp_remote_get( $remote_json );
		if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
			return;
		}
		$json = wp_remote_retrieve_body( $response );
		if ( ! empty( $json ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			global $wp_filesystem;
			if ( ! WP_Filesystem() ) {
				return;
			}

			if ( $wp_filesystem->is_writable( __DIR__ ) ) {
				$wp_filesystem->put_contents( __DIR__ . '/posts.json', $json, FS_CHMOD_FILE );
			}
		}
	}

	/**
	 * Sanitize an ip number
	 */
	public function sanitize_ip_field( string $value ): string {
		if ( ! $this->user_can_manage() ) {
			return '';
		}

		$ips = explode( PHP_EOL, $value );
		// remove whitespace.
		$ips = array_map( 'trim', $ips );
		$ips = array_filter( $ips, static fn( $ip ) => $ip !== '' );
		// remove duplicates.
		$ips = array_unique( $ips );
		// sanitize each ip.
		$ips = array_map( 'sanitize_text_field', $ips );
		return implode( PHP_EOL, $ips );
	}

	/**
	 * Get an array of posts
	 *
	 * @param \WP_REST_Request $request The REST API request object.
	 * @param array             $ajax_data Optional AJAX data to process.
	 * @return \WP_REST_Response|\WP_Error The response object or error.
	 */
	//phpcs:ignore
	public function get_posts( \WP_REST_Request $request, array $ajax_data = [] ) {
		if ( ! $this->user_can_manage() ) {
			return new \WP_Error( 'rest_forbidden', 'You do not have permission to perform this action.', [ 'status' => 403 ] );
		}

		$max_post_count = 100;
		$data           = empty( $ajax_data ) ? $request->get_params() : $ajax_data;
		$nonce          = $data['nonce'];
		$search         = isset( $data['search'] ) ? $data['search'] : '';

		if ( ! $this->verify_nonce( $nonce, 'burst_nonce' ) ) {
			return new \WP_Error( 'rest_invalid_nonce', $this->nonce_expired_feedback, [ 'status' => 400 ] );
		}

		// do full search for string length above 3, but set a cap at 1000.
		if ( strlen( $search ) > 3 ) {
			$max_post_count = 1000;
		}

		global $wpdb;

		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT p.ID as page_id,
            p.post_title,
            COALESCE(s.pageviews, 0) as pageviews
             FROM {$wpdb->prefix}posts p
             LEFT JOIN (
                 SELECT page_id, COUNT(*) as pageviews
                 FROM {$wpdb->prefix}burst_statistics
                 WHERE page_id > 0
                 GROUP BY page_id
             ) s ON p.ID = s.page_id
             WHERE p.post_type IN ('post', 'page')
               AND p.post_status = 'publish'
             ORDER BY p.post_title ASC
             LIMIT %d",
				$max_post_count
			),
			ARRAY_A
		);

		$result_array = [];
		foreach ( $results as $result ) {
			$result_array[] = [
				'page_url'   => str_replace( site_url(), '', get_permalink( $result['page_id'] ) ),
				'page_id'    => (int) $result['page_id'],
				'post_title' => $result['post_title'],
				'pageviews'  => (int) $result['pageviews'],
			];
		}

		if ( ob_get_length() ) {
			ob_clean();
		}

		return new \WP_REST_Response(
			[
				'request_success' => true,
				'posts'           => $result_array,
				'max_post_count'  => $max_post_count,
			],
			200
		);
	}

	/**
	 * If the track_network_wide option is saved, we update the site_option which is used to handle this behaviour.
	 *
	 * @param string $name The name of the option.
	 * @param mixed  $value The new value of the option.
	 * @param mixed  $prev_value The previous value of the option.
	 * @param string $type The type of the option.
	 */
	// $value and $prev_value are mixed types, only supported as of php 8.
	// phpcs:ignore
	public function update_for_multisite( string $name, $value, $prev_value, string $type ): void {
		if ( $name === 'track_network_wide' ) {
			update_site_option( 'burst_track_network_wide', (bool) $value );
		}
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit