PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /home/u588966950/domains/vtsinfotech.online/public_html/eduon/wp-admin/includes
Viewing File: /home/u588966950/domains/vtsinfotech.online/public_html/eduon/wp-admin/includes/privacy-tools.php
<?php /** * WordPress Administration Privacy Tools API. * * @package WordPress * @subpackage Administration */ /** * Resend an existing request and return the result. * * @since 4.9.6 * @access private * * @param int $request_id Request ID. * @return true|WP_Error Returns true if sending the email was successful, or a WP_Error object. */ function _wp_privacy_resend_request( $request_id ) { $request_id = absint( $request_id ); $request = get_post( $request_id ); if ( ! $request || 'user_request' !== $request->post_type ) { return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) ); } $result = wp_send_user_request( $request_id ); if ( is_wp_error( $result ) ) { return $result; } elseif ( ! $result ) { return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation for personal data request.' ) ); } return true; } /** * Marks a request as completed by the admin and logs the current timestamp. * * @since 4.9.6 * @access private * * @param int $request_id Request ID. * @return int|WP_Error Request ID on success, or a WP_Error on failure. */ function _wp_privacy_completed_request( $request_id ) { // Get the request. $request_id = absint( $request_id ); $request = wp_get_user_request( $request_id ); if ( ! $request ) { return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) ); } update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); $result = wp_update_post( array( 'ID' => $request_id, 'post_status' => 'request-completed', ) ); return $result; } /** * Handle list table actions. * * @since 4.9.6 * @access private */ function _wp_personal_data_handle_actions() { if ( isset( $_POST['privacy_action_email_retry'] ) ) { check_admin_referer( 'bulk-privacy_requests' ); $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['privacy_action_email_retry'] ) ) ) ); $result = _wp_privacy_resend_request( $request_id ); if ( is_wp_error( $result ) ) { add_settings_error( 'privacy_action_email_retry', 'privacy_action_email_retry', $result->get_error_message(), 'error' ); } else { add_settings_error( 'privacy_action_email_retry', 'privacy_action_email_retry', __( 'Confirmation request sent again successfully.' ), 'success' ); } } elseif ( isset( $_POST['action'] ) ) { $action = ! empty( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : ''; switch ( $action ) { case 'add_export_personal_data_request': case 'add_remove_personal_data_request': check_admin_referer( 'personal-data-request' ); if ( ! isset( $_POST['type_of_action'], $_POST['username_or_email_for_privacy_request'] ) ) { add_settings_error( 'action_type', 'action_type', __( 'Invalid personal data action.' ), 'error' ); } $action_type = sanitize_text_field( wp_unslash( $_POST['type_of_action'] ) ); $username_or_email_address = sanitize_text_field( wp_unslash( $_POST['username_or_email_for_privacy_request'] ) ); $email_address = ''; $status = 'pending'; if ( ! isset( $_POST['send_confirmation_email'] ) ) { $status = 'confirmed'; } if ( ! in_array( $action_type, _wp_privacy_action_request_types(), true ) ) { add_settings_error( 'action_type', 'action_type', __( 'Invalid personal data action.' ), 'error' ); } if ( ! is_email( $username_or_email_address ) ) { $user = get_user_by( 'login', $username_or_email_address ); if ( ! $user instanceof WP_User ) { add_settings_error( 'username_or_email_for_privacy_request', 'username_or_email_for_privacy_request', __( 'Unable to add this request. A valid email address or username must be supplied.' ), 'error' ); } else { $email_address = $user->user_email; } } else { $email_address = $username_or_email_address; } if ( empty( $email_address ) ) { break; } $request_id = wp_create_user_request( $email_address, $action_type, array(), $status ); $message = ''; if ( is_wp_error( $request_id ) ) { $message = $request_id->get_error_message(); } elseif ( ! $request_id ) { $message = __( 'Unable to initiate confirmation request.' ); } if ( $message ) { add_settings_error( 'username_or_email_for_privacy_request', 'username_or_email_for_privacy_request', $message, 'error' ); break; } if ( 'pending' === $status ) { wp_send_user_request( $request_id ); $message = __( 'Confirmation request initiated successfully.' ); } elseif ( 'confirmed' === $status ) { $message = __( 'Request added successfully.' ); } if ( $message ) { add_settings_error( 'username_or_email_for_privacy_request', 'username_or_email_for_privacy_request', $message, 'success' ); break; } } } } /** * Cleans up failed and expired requests before displaying the list table. * * @since 4.9.6 * @access private */ function _wp_personal_data_cleanup_requests() { /** This filter is documented in wp-includes/user.php */ $expires = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS ); $requests_query = new WP_Query( array( 'post_type' => 'user_request', 'posts_per_page' => -1, 'post_status' => 'request-pending', 'fields' => 'ids', 'date_query' => array( array( 'column' => 'post_modified_gmt', 'before' => $expires . ' seconds ago', ), ), ) ); $request_ids = $requests_query->posts; foreach ( $request_ids as $request_id ) { wp_update_post( array( 'ID' => $request_id, 'post_status' => 'request-failed', 'post_password' => '', ) ); } } /** * Generate a single group for the personal data export report. * * @since 4.9.6 * @since 5.4.0 Added the `$group_id` and `$groups_count` parameters. * * @param array $group_data { * The group data to render. * * @type string $group_label The user-facing heading for the group, e.g. 'Comments'. * @type array $items { * An array of group items. * * @type array $group_item_data { * An array of name-value pairs for the item. * * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'. * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. * } * } * } * @param string $group_id The group identifier. * @param int $groups_count The number of all groups * @return string The HTML for this group and its items. */ function wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id = '', $groups_count = 1 ) { $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id ); $group_html = '<h2 id="' . esc_attr( $group_id_attr ) . '">'; $group_html .= esc_html( $group_data['group_label'] ); $items_count = count( (array) $group_data['items'] ); if ( $items_count > 1 ) { $group_html .= sprintf( ' <span class="count">(%d)</span>', $items_count ); } $group_html .= '</h2>'; if ( ! empty( $group_data['group_description'] ) ) { $group_html .= '<p>' . esc_html( $group_data['group_description'] ) . '</p>'; } $group_html .= '<div>'; foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { $group_html .= '<table>'; $group_html .= '<tbody>'; foreach ( (array) $group_item_data as $group_item_datum ) { $value = $group_item_datum['value']; // If it looks like a link, make it a link. if ( ! str_contains( $value, ' ' ) && ( str_starts_with( $value, 'http://' ) || str_starts_with( $value, 'https://' ) ) ) { $value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>'; } $group_html .= '<tr>'; $group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>'; $group_html .= '<td>' . wp_kses( $value, 'personal_data_export' ) . '</td>'; $group_html .= '</tr>'; } $group_html .= '</tbody>'; $group_html .= '</table>'; } if ( $groups_count > 1 ) { $group_html .= '<div class="return-to-top">'; $group_html .= '<a href="#top"><span aria-hidden="true">&uarr; </span> ' . esc_html__( 'Go to top' ) . '</a>'; $group_html .= '</div>'; } $group_html .= '</div>'; return $group_html; } /** * Generate the personal data export file. * * @since 4.9.6 * * @param int $request_id The export request ID. */ function wp_privacy_generate_personal_data_export_file( $request_id ) { if ( ! class_exists( 'ZipArchive' ) ) { wp_send_json_error( __( 'Unable to generate personal data export file. ZipArchive not available.' ) ); } // Get the request. $request = wp_get_user_request( $request_id ); if ( ! $request || 'export_personal_data' !== $request->action_name ) { wp_send_json_error( __( 'Invalid request ID when generating personal data export file.' ) ); } $email_address = $request->email; if ( ! is_email( $email_address ) ) { wp_send_json_error( __( 'Invalid email address when generating personal data export file.' ) ); } // Create the exports folder if needed. $exports_dir = wp_privacy_exports_dir(); $exports_url = wp_privacy_exports_url(); if ( ! wp_mkdir_p( $exports_dir ) ) { wp_send_json_error( __( 'Unable to create personal data export folder.' ) ); } // Protect export folder from browsing. $index_pathname = $exports_dir . 'index.php'; if ( ! file_exists( $index_pathname ) ) { $file = fopen( $index_pathname, 'w' ); if ( false === $file ) { wp_send_json_error( __( 'Unable to protect personal data export folder from browsing.' ) ); } fwrite( $file, "<?php\n// Silence is golden.\n" ); fclose( $file ); } $obscura = wp_generate_password( 32, false, false ); $file_basename = 'wp-personal-data-file-' . $obscura; $html_report_filename = wp_unique_filename( $exports_dir, $file_basename . '.html' ); $html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename ); $json_report_filename = $file_basename . '.json'; $json_report_pathname = wp_normalize_path( $exports_dir . $json_report_filename ); /* * Gather general data needed. */ // Title. $title = sprintf( /* translators: %s: User's email address. */ __( 'Personal Data Export for %s' ), $email_address ); // First, build an "About" group on the fly for this report. $about_group = array( /* translators: Header for the About section in a personal data export. */ 'group_label' => _x( 'About', 'personal data group label' ), /* translators: Description for the About section in a personal data export. */ 'group_description' => _x( 'Overview of export report.', 'personal data group description' ), 'items' => array( 'about-1' => array( array( 'name' => _x( 'Report generated for', 'email address' ), 'value' => $email_address, ), array( 'name' => _x( 'For site', 'website name' ), 'value' => get_bloginfo( 'name' ), ), array( 'name' => _x( 'At URL', 'website URL' ), 'value' => get_bloginfo( 'url' ), ), array( 'name' => _x( 'On', 'date/time' ), 'value' => current_time( 'mysql' ), ), ), ), ); // And now, all the Groups. $groups = get_post_meta( $request_id, '_export_data_grouped', true ); if ( is_array( $groups ) ) { // Merge in the special "About" group. $groups = array_merge( array( 'about' => $about_group ), $groups ); $groups_count = count( $groups ); } else { if ( false !== $groups ) { _doing_it_wrong( __FUNCTION__, /* translators: %s: Post meta key. */ sprintf( __( 'The %s post meta must be an array.' ), '<code>_export_data_grouped</code>' ), '5.8.0' ); } $groups = null; $groups_count = 0; } // Convert the groups to JSON format. $groups_json = wp_json_encode( $groups ); if ( false === $groups_json ) { $error_message = sprintf( /* translators: %s: Error message. */ __( 'Unable to encode the personal data for export. Error: %s' ), json_last_error_msg() ); wp_send_json_error( $error_message ); } /* * Handle the JSON export. */ $file = fopen( $json_report_pathname, 'w' ); if ( false === $file ) { wp_send_json_error( __( 'Unable to open personal data export file (JSON report) for writing.' ) ); } fwrite( $file, '{' ); fwrite( $file, '"' . $title . '":' ); fwrite( $file, $groups_json ); fwrite( $file, '}' ); fclose( $file ); /* * Handle the HTML export. */ $file = fopen( $html_report_pathname, 'w' ); if ( false === $file ) { wp_send_json_error( __( 'Unable to open personal data export (HTML report) for writing.' ) ); } fwrite( $file, "<!DOCTYPE html>\n" ); fwrite( $file, "<html>\n" ); fwrite( $file, "<head>\n" ); fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" ); fwrite( $file, "<style type='text/css'>" ); fwrite( $file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }' ); fwrite( $file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }' ); fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' ); fwrite( $file, 'td { padding: 5px; }' ); fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' ); fwrite( $file, '.return-to-top { text-align: right; }' ); fwrite( $file, '</style>' ); fwrite( $file, '<title>' ); fwrite( $file, esc_html( $title ) ); fwrite( $file, '</title>' ); fwrite( $file, "</head>\n" ); fwrite( $file, "<body>\n" ); fwrite( $file, '<h1 id="top">' . esc_html__( 'Personal Data Export' ) . '</h1>' ); // Create TOC. if ( $groups_count > 1 ) { fwrite( $file, '<div id="table_of_contents">' ); fwrite( $file, '<h2>' . esc_html__( 'Table of Contents' ) . '</h2>' ); fwrite( $file, '<ul>' ); foreach ( (array) $groups as $group_id => $group_data ) { $group_label = esc_html( $group_data['group_label'] ); $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id ); $group_items_count = count( (array) $group_data['items'] ); if ( $group_items_count > 1 ) { $group_label .= sprintf( ' <span class="count">(%d)</span>', $group_items_count ); } fwrite( $file, '<li>' ); fwrite( $file, '<a href="#' . esc_attr( $group_id_attr ) . '">' . $group_label . '</a>' ); fwrite( $file, '</li>' ); } fwrite( $file, '</ul>' ); fwrite( $file, '</div>' ); } // Now, iterate over every group in $groups and have the formatter render it in HTML. foreach ( (array) $groups as $group_id => $group_data ) { fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id, $groups_count ) ); } fwrite( $file, "</body>\n" ); fwrite( $file, "</html>\n" ); fclose( $file ); /* * Now, generate the ZIP. * * If an archive has already been generated, then remove it and reuse the filename, * to avoid breaking any URLs that may have been previously sent via email. */ $error = false; // This meta value is used from version 5.5. $archive_filename = get_post_meta( $request_id, '_export_file_name', true ); // This one stored an absolute path and is used for backward compatibility. $archive_pathname = get_post_meta( $request_id, '_export_file_path', true ); // If a filename meta exists, use it. if ( ! empty( $archive_filename ) ) { $archive_pathname = $exports_dir . $archive_filename; } elseif ( ! empty( $archive_pathname ) ) { // If a full path meta exists, use it and create the new meta value. $archive_filename = basename( $archive_pathname ); update_post_meta( $request_id, '_export_file_name', $archive_filename ); // Remove the back-compat meta values. delete_post_meta( $request_id, '_export_file_url' ); delete_post_meta( $request_id, '_export_file_path' ); } else { // If there's no filename or full path stored, create a new file. $archive_filename = $file_basename . '.zip'; $archive_pathname = $exports_dir . $archive_filename; update_post_meta( $request_id, '_export_file_name', $archive_filename ); } $archive_url = $exports_url . $archive_filename; if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) { wp_delete_file( $archive_pathname ); } $zip = new ZipArchive(); if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { if ( ! $zip->addFile( $json_report_pathname, 'export.json' ) ) { $error = __( 'Unable to archive the personal data export file (JSON format).' ); } if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) { $error = __( 'Unable to archive the personal data export file (HTML format).' ); } $zip->close(); if ( ! $error ) { /** * Fires right after all personal data has been written to the export file. * * @since 4.9.6 * @since 5.4.0 Added the `$json_report_pathname` parameter. * * @param string $archive_pathname The full path to the export file on the filesystem. * @param string $archive_url The URL of the archive file. * @param string $html_report_pathname The full path to the HTML personal data report on the filesystem. * @param int $request_id The export request ID. * @param string $json_report_pathname The full path to the JSON personal data report on the filesystem. */ do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id, $json_report_pathname ); } } else { $error = __( 'Unable to open personal data export file (archive) for writing.' ); } // Remove the JSON file. unlink( $json_report_pathname ); // Remove the HTML file. unlink( $html_report_pathname ); if ( $error ) { wp_send_json_error( $error ); } } /** * Send an email to the user with a link to the personal data export file * * @since 4.9.6 * * @param int $request_id The request ID for this personal data export. * @return true|WP_Error True on success or `WP_Error` on failure. */ function wp_privacy_send_personal_data_export_email( $request_id ) { // Get the request. $request = wp_get_user_request( $request_id ); if ( ! $request || 'export_personal_data' !== $request->action_name ) { return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) ); } // Localize message content for user; fallback to site default for visitors. if ( ! empty( $request->user_id ) ) { $switched_locale = switch_to_user_locale( $request->user_id ); } else { $switched_locale = switch_to_locale( get_locale() ); } /** This filter is documented in wp-includes/functions.php */ $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); $exports_url = wp_privacy_exports_url(); $export_file_name = get_post_meta( $request_id, '_export_file_name', true ); $export_file_url = $exports_url . $export_file_name; $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); $site_url = home_url(); /** * Filters the recipient of the personal data export email notification. * Should be used with great caution to avoid sending the data export link to wrong emails. * * @since 5.3.0 * * @param string $request_email The email address of the notification recipient. * @param WP_User_Request $request The request that is initiating the notification. */ $request_email = apply_filters( 'wp_privacy_personal_data_email_to', $request->email, $request ); $email_data = array( 'request' => $request, 'expiration' => $expiration, 'expiration_date' => $expiration_date, 'message_recipient' => $request_email, 'export_file_url' => $export_file_url, 'sitename' => $site_name, 'siteurl' => $site_url, ); /* translators: Personal data export notification email subject. %s: Site title. */ $subject = sprintf( __( '[%s] Personal Data Export' ), $site_name ); /** * Filters the subject of the email sent when an export request is completed. * * @since 5.3.0 * * @param string $subject The email subject. * @param string $sitename The name of the site. * @param array $email_data { * Data relating to the account action email. * * @type WP_User_Request $request User request object. * @type int $expiration The time in seconds until the export file expires. * @type string $expiration_date The localized date and time when the export file expires. * @type string $message_recipient The address that the email will be sent to. Defaults * to the value of `$request->email`, but can be changed * by the `wp_privacy_personal_data_email_to` filter. * @type string $export_file_url The export file URL. * @type string $sitename The site name sending the mail. * @type string $siteurl The site URL sending the mail. * } */ $subject = apply_filters( 'wp_privacy_personal_data_email_subject', $subject, $site_name, $email_data ); /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */ $email_text = __( 'Howdy, Your request for an export of personal data has been completed. You may download your personal data by clicking on the link below. For privacy and security, we will automatically delete the file on ###EXPIRATION###, so please download it before then. ###LINK### Regards, All at ###SITENAME### ###SITEURL###' ); /** * Filters the text of the email sent with a personal data export file. * * The following strings have a special meaning and will get replaced dynamically: * ###EXPIRATION### The date when the URL will be automatically deleted. * ###LINK### URL of the personal data export file for the user. * ###SITENAME### The name of the site. * ###SITEURL### The URL to the site. * * @since 4.9.6 * @since 5.3.0 Introduced the `$email_data` array. * * @param string $email_text Text in the email. * @param int $request_id The request ID for this personal data export. * @param array $email_data { * Data relating to the account action email. * * @type WP_User_Request $request User request object. * @type int $expiration The time in seconds until the export file expires. * @type string $expiration_date The localized date and time when the export file expires. * @type string $message_recipient The address that the email will be sent to. Defaults * to the value of `$request->email`, but can be changed * by the `wp_privacy_personal_data_email_to` filter. * @type string $export_file_url The export file URL. * @type string $sitename The site name sending the mail. * @type string $siteurl The site URL sending the mail. */ $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id, $email_data ); $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); $content = str_replace( '###LINK###', sanitize_url( $export_file_url ), $content ); $content = str_replace( '###EMAIL###', $request_email, $content ); $content = str_replace( '###SITENAME###', $site_name, $content ); $content = str_replace( '###SITEURL###', sanitize_url( $site_url ), $content ); $headers = ''; /** * Filters the headers of the email sent with a personal data export file. * * @since 5.4.0 * * @param string|array $headers The email headers. * @param string $subject The email subject. * @param string $content The email content. * @param int $request_id The request ID. * @param array $email_data { * Data relating to the account action email. * * @type WP_User_Request $request User request object. * @type int $expiration The time in seconds until the export file expires. * @type string $expiration_date The localized date and time when the export file expires. * @type string $message_recipient The address that the email will be sent to. Defaults * to the value of `$request->email`, but can be changed * by the `wp_privacy_personal_data_email_to` filter. * @type string $export_file_url The export file URL. * @type string $sitename The site name sending the mail. * @type string $siteurl The site URL sending the mail. * } */ $headers = apply_filters( 'wp_privacy_personal_data_email_headers', $headers, $subject, $content, $request_id, $email_data ); $mail_success = wp_mail( $request_email, $subject, $content, $headers ); if ( $switched_locale ) { restore_previous_locale(); } if ( ! $mail_success ) { return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) ); } return true; } /** * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file. * * @since 4.9.6 * * @see 'wp_privacy_personal_data_export_page' * * @param array $response The response from the personal data exporter for the given page. * @param int $exporter_index The index of the personal data exporter. Begins at 1. * @param string $email_address The email address of the user whose personal data this is. * @param int $page The page of personal data for this exporter. Begins at 1. * @param int $request_id The request ID for this personal data export. * @param bool $send_as_email Whether the final results of the export should be emailed to the user. * @param string $exporter_key The slug (key) of the exporter. * @return array The filtered response. */ function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { /* Do some simple checks on the shape of the response from the exporter. * If the exporter response is malformed, don't attempt to consume it - let it * pass through to generate a warning to the user by default Ajax processing. */ if ( ! is_array( $response ) ) { return $response; } if ( ! array_key_exists( 'done', $response ) ) { return $response; } if ( ! array_key_exists( 'data', $response ) ) { return $response; } if ( ! is_array( $response['data'] ) ) { return $response; } // Get the request. $request = wp_get_user_request( $request_id ); if ( ! $request || 'export_personal_data' !== $request->action_name ) { wp_send_json_error( __( 'Invalid request ID when merging personal data to export.' ) ); } $export_data = array(); // First exporter, first page? Reset the report data accumulation array. if ( 1 === $exporter_index && 1 === $page ) { update_post_meta( $request_id, '_export_data_raw', $export_data ); } else { $accumulated_data = get_post_meta( $request_id, '_export_data_raw', true ); if ( $accumulated_data ) { $export_data = $accumulated_data; } } // Now, merge the data from the exporter response into the data we have accumulated already. $export_data = array_merge( $export_data, $response['data'] ); update_post_meta( $request_id, '_export_data_raw', $export_data ); // If we are not yet on the last page of the last exporter, return now. /** This filter is documented in wp-admin/includes/ajax-actions.php */ $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); $is_last_exporter = count( $exporters ) === $exporter_index; $exporter_done = $response['done']; if ( ! $is_last_exporter || ! $exporter_done ) { return $response; } // Last exporter, last page - let's prepare the export file. // First we need to re-organize the raw data hierarchically in groups and items. $groups = array(); foreach ( (array) $export_data as $export_datum ) { $group_id = $export_datum['group_id']; $group_label = $export_datum['group_label']; $group_description = ''; if ( ! empty( $export_datum['group_description'] ) ) { $group_description = $export_datum['group_description']; } if ( ! array_key_exists( $group_id, $groups ) ) { $groups[ $group_id ] = array( 'group_label' => $group_label, 'group_description' => $group_description, 'items' => array(), ); } $item_id = $export_datum['item_id']; if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { $groups[ $group_id ]['items'][ $item_id ] = array(); } $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; } // Then save the grouped data into the request. delete_post_meta( $request_id, '_export_data_raw' ); update_post_meta( $request_id, '_export_data_grouped', $groups ); /** * Generate the export file from the collected, grouped personal data. * * @since 4.9.6 * * @param int $request_id The export request ID. */ do_action( 'wp_privacy_personal_data_export_file', $request_id ); // Clear the grouped data now that it is no longer needed. delete_post_meta( $request_id, '_export_data_grouped' ); // If the destination is email, send it now. if ( $send_as_email ) { $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); if ( is_wp_error( $mail_success ) ) { wp_send_json_error( $mail_success->get_error_message() ); } // Update the request to completed state when the export email is sent. _wp_privacy_completed_request( $request_id ); } else { // Modify the response to include the URL of the export file so the browser can fetch it. $exports_url = wp_privacy_exports_url(); $export_file_name = get_post_meta( $request_id, '_export_file_name', true ); $export_file_url = $exports_url . $export_file_name; if ( ! empty( $export_file_url ) ) { $response['url'] = $export_file_url; } } return $response; } /** * Mark erasure requests as completed after processing is finished. * * This intercepts the Ajax responses to personal data eraser page requests, and * monitors the status of a request. Once all of the processing has finished, the * request is marked as completed. * * @since 4.9.6 * * @see 'wp_privacy_personal_data_erasure_page' * * @param array $response The response from the personal data eraser for * the given page. * @param int $eraser_index The index of the personal data eraser. Begins * at 1. * @param string $email_address The email address of the user whose personal * data this is. * @param int $page The page of personal data for this eraser. * Begins at 1. * @param int $request_id The request ID for this personal data erasure. * @return array The filtered response. */ function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) { /* * If the eraser response is malformed, don't attempt to consume it; let it * pass through, so that the default Ajax processing will generate a warning * to the user. */ if ( ! is_array( $response ) ) { return $response; } if ( ! array_key_exists( 'done', $response ) ) { return $response; } if ( ! array_key_exists( 'items_removed', $response ) ) { return $response; } if ( ! array_key_exists( 'items_retained', $response ) ) { return $response; } if ( ! array_key_exists( 'messages', $response ) ) { return $response; } // Get the request. $request = wp_get_user_request( $request_id ); if ( ! $request || 'remove_personal_data' !== $request->action_name ) { wp_send_json_error( __( 'Invalid request ID when processing personal data to erase.' ) ); } /** This filter is documented in wp-admin/includes/ajax-actions.php */ $erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() ); $is_last_eraser = count( $erasers ) === $eraser_index; $eraser_done = $response['done']; if ( ! $is_last_eraser || ! $eraser_done ) { return $response; } _wp_privacy_completed_request( $request_id ); /** * Fires immediately after a personal data erasure request has been marked completed. * * @since 4.9.6 * * @param int $request_id The privacy request post ID associated with this request. */ do_action( 'wp_privacy_personal_data_erased', $request_id ); return $response; }